From a0798214231c652ac6142228f5ddfc4b65c921f8 Mon Sep 17 00:00:00 2001 From: Tomaz Canabrava Date: Wed, 2 Sep 2015 19:09:08 -0300 Subject: Start to clean CMake CMake can be a good system but if we keep everything into one big cmake file things can go nuts really quick. Since I already took quite a start on an subsurface layer separation some time ago, I'm improving it by making each module on Subsurface depend on it's own CMake module. This first patch moves the qt-ui part to qt-ui/CMakeLists.txt file, it cleans tons of the main cmake file ( moving all parts to the in ternal folder ), and makes things more easily manageable by the programmer that will change the ui bits, he doesn't need to play hide and seek with the CMakeLists.txt file anymore, trying to figure out where should he put his newly generated file. Signed-off-by: Tomaz Canabrava Signed-off-by: Dirk Hohndel --- qt-ui/CMakeLists.txt | 104 ++++++++++++++++++ qt-ui/printer.cpp | 273 +++++++++++++++++++++++++++++++++++++++++++++++ qt-ui/printer.h | 48 +++++++++ qt-ui/templatelayout.cpp | 182 +++++++++++++++++++++++++++++++ qt-ui/templatelayout.h | 168 +++++++++++++++++++++++++++++ 5 files changed, 775 insertions(+) create mode 100644 qt-ui/CMakeLists.txt create mode 100644 qt-ui/printer.cpp create mode 100644 qt-ui/printer.h create mode 100644 qt-ui/templatelayout.cpp create mode 100644 qt-ui/templatelayout.h (limited to 'qt-ui') diff --git a/qt-ui/CMakeLists.txt b/qt-ui/CMakeLists.txt new file mode 100644 index 000000000..9def39ff3 --- /dev/null +++ b/qt-ui/CMakeLists.txt @@ -0,0 +1,104 @@ +# create the libraries +file(GLOB SUBSURFACE_UI *.ui) +qt5_wrap_ui(SUBSURFACE_UI_HDRS ${SUBSURFACE_UI}) +qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc) +source_group("Subsurface Interface Files" FILES ${SUBSURFACE_UI}) + +# the interface, in C++ +set(SUBSURFACE_INTERFACE + updatemanager.cpp + about.cpp + divecomputermanagementdialog.cpp + divelistview.cpp + diveplanner.cpp + diveshareexportdialog.cpp + downloadfromdivecomputer.cpp + globe.cpp + graphicsview-common.cpp + kmessagewidget.cpp + maintab.cpp + mainwindow.cpp + modeldelegates.cpp + metrics.cpp + notificationwidget.cpp + preferences.cpp + simplewidgets.cpp + starwidget.cpp + subsurfacewebservices.cpp + tableview.cpp + divelogimportdialog.cpp + tagwidget.cpp + groupedlineedit.cpp + divelogexportdialog.cpp + divepicturewidget.cpp + usersurvey.cpp + configuredivecomputerdialog.cpp + undocommands.cpp + locationinformation.cpp + qtwaitingspinner.cpp +) + +if(NOT NO_USERMANUAL) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + usermanual.cpp + ) +endif() + +if(NOT NO_PRINTING) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + templateedit.cpp + printdialog.cpp + printoptions.cpp + printer.cpp + templatelayout.cpp + ) +endif() + +if (FBSUPPORT) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + socialnetworks.cpp + ) +endif() + +if (BTSUPPORT) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + btdeviceselectiondialog.cpp + ) +endif() + +source_group("Subsurface Interface" FILES ${SUBSURFACE_INTERFACE}) + +# the profile widget +set(SUBSURFACE_PROFILE_LIB_SRCS + profile/profilewidget2.cpp + profile/diverectitem.cpp + profile/divepixmapitem.cpp + profile/divelineitem.cpp + profile/divetextitem.cpp + profile/animationfunctions.cpp + profile/divecartesianaxis.cpp + profile/diveprofileitem.cpp + profile/diveeventitem.cpp + profile/divetooltipitem.cpp + profile/ruleritem.cpp + profile/tankitem.cpp +) +source_group("Subsurface Profile" FILES ${SUBSURFACE_PROFILE_LIB_SRCS}) + +# the yearly statistics widget. +set(SUBSURFACE_STATISTICS_LIB_SRCS + statistics/statisticswidget.cpp + statistics/yearstatistics.cpp + statistics/statisticsbar.cpp + statistics/monthstatistics.cpp +) +source_group("Subsurface Statistics" FILES ${SUBSURFACE_STATISTICS_LIB_SRCS}) + +add_library(subsurface_profile STATIC ${SUBSURFACE_PROFILE_LIB_SRCS}) +target_link_libraries(subsurface_profile ${QT_LIBRARIES}) +add_library(subsurface_statistics STATIC ${SUBSURFACE_STATISTICS_LIB_SRCS}) +target_link_libraries(subsurface_statistics ${QT_LIBRARIES}) +add_library(subsurface_generated_ui STATIC ${SUBSURFACE_UI_HDRS}) +target_link_libraries(subsurface_generated_ui ${QT_LIBRARIES}) +add_library(subsurface_interface STATIC ${SUBSURFACE_INTERFACE}) +target_link_libraries(subsurface_interface ${QT_LIBRARIES} ${MARBLE_LIBRARIES}) diff --git a/qt-ui/printer.cpp b/qt-ui/printer.cpp new file mode 100644 index 000000000..f0197d446 --- /dev/null +++ b/qt-ui/printer.cpp @@ -0,0 +1,273 @@ +#include "printer.h" +#include "templatelayout.h" +#include "statistics.h" +#include "helpers.h" + +#include +#include +#include +#include +#include + +Printer::Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode) +{ + this->paintDevice = paintDevice; + this->printOptions = printOptions; + this->templateOptions = templateOptions; + this->printMode = printMode; + dpi = 0; + done = 0; + webView = new QWebView(); +} + +Printer::~Printer() +{ + delete webView; +} + +void Printer::putProfileImage(QRect profilePlaceholder, QRect viewPort, QPainter *painter, struct dive *dive, QPointer profile) +{ + int x = profilePlaceholder.x() - viewPort.x(); + int y = profilePlaceholder.y() - viewPort.y(); + // use the placeHolder and the viewPort position to calculate the relative position of the dive profile. + QRect pos(x, y, profilePlaceholder.width(), profilePlaceholder.height()); + profile->plotDive(dive, true); + + if (!printOptions->color_selected) { + QImage image(pos.width(), pos.height(), QImage::Format_ARGB32); + QPainter imgPainter(&image); + imgPainter.setRenderHint(QPainter::Antialiasing); + imgPainter.setRenderHint(QPainter::SmoothPixmapTransform); + profile->render(&imgPainter, QRect(0, 0, pos.width(), pos.height())); + imgPainter.end(); + + // convert QImage to grayscale before rendering + for (int i = 0; i < image.height(); i++) { + QRgb *pixel = reinterpret_cast(image.scanLine(i)); + QRgb *end = pixel + image.width(); + for (; pixel != end; pixel++) { + int gray_val = qGray(*pixel); + *pixel = QColor(gray_val, gray_val, gray_val).rgb(); + } + } + + painter->drawImage(pos, image); + } else { + profile->render(painter, pos); + } +} + +void Printer::flowRender() +{ + // add extra padding at the bottom to pages with height not divisible by view port + int paddingBottom = pageSize.height() - (webView->page()->mainFrame()->contentsSize().height() % pageSize.height()); + QString styleString = QString::fromUtf8("padding-bottom: ") + QString::number(paddingBottom) + "px;"; + webView->page()->mainFrame()->findFirstElement("body").setAttribute("style", styleString); + + // render the Qwebview + QPainter painter; + QRect viewPort(0, 0, 0, 0); + painter.begin(paintDevice); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // get all references to dontbreak divs + int start = 0, end = 0; + int fullPageResolution = webView->page()->mainFrame()->contentsSize().height(); + QWebElementCollection dontbreak = webView->page()->mainFrame()->findAllElements(".dontbreak"); + foreach (QWebElement dontbreakElement, dontbreak) { + if ((dontbreakElement.geometry().y() + dontbreakElement.geometry().height()) - start < pageSize.height()) { + // One more element can be placed + end = dontbreakElement.geometry().y() + dontbreakElement.geometry().height(); + } else { + // fill the page with background color + QRect fullPage(0, 0, pageSize.width(), pageSize.height()); + QBrush fillBrush(templateOptions->color_palette.color1); + painter.fillRect(fullPage, fillBrush); + QRegion reigon(0, 0, pageSize.width(), end - start); + viewPort.setRect(0, start, pageSize.width(), end - start); + + // render the base Html template + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); + + // scroll the webview to the next page + webView->page()->mainFrame()->scroll(0, dontbreakElement.geometry().y() - start); + + // rendering progress is 4/5 of total work + emit(progessUpdated((end * 80.0 / fullPageResolution) + done)); + + // add new pages only in print mode, while previewing we don't add new pages + if (printMode == Printer::PRINT) + static_cast(paintDevice)->newPage(); + else { + painter.end(); + return; + } + start = dontbreakElement.geometry().y(); + } + } + // render the remianing page + QRect fullPage(0, 0, pageSize.width(), pageSize.height()); + QBrush fillBrush(templateOptions->color_palette.color1); + painter.fillRect(fullPage, fillBrush); + QRegion reigon(0, 0, pageSize.width(), end - start); + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); + + painter.end(); +} + +void Printer::render(int Pages = 0) +{ + // keep original preferences + QPointer profile = MainWindow::instance()->graphics(); + int profileFrameStyle = profile->frameStyle(); + int animationOriginal = prefs.animation_speed; + double fontScale = profile->getFontPrintScale(); + double printFontScale = 1.0; + + // apply printing settings to profile + profile->setFrameStyle(QFrame::NoFrame); + profile->setPrintMode(true, !printOptions->color_selected); + profile->setToolTipVisibile(false); + prefs.animation_speed = 0; + + // render the Qwebview + QPainter painter; + QRect viewPort(0, 0, pageSize.width(), pageSize.height()); + painter.begin(paintDevice); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // get all refereces to diveprofile class in the Html template + QWebElementCollection collection = webView->page()->mainFrame()->findAllElements(".diveprofile"); + + QSize originalSize = profile->size(); + if (collection.count() > 0) { + printFontScale = (double)collection.at(0).geometry().size().height() / (double)profile->size().height(); + profile->resize(collection.at(0).geometry().size()); + } + profile->setFontPrintScale(printFontScale); + + int elemNo = 0; + for (int i = 0; i < Pages; i++) { + // render the base Html template + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer); + + // render all the dive profiles in the current page + while (elemNo < collection.count() && collection.at(elemNo).geometry().y() < viewPort.y() + viewPort.height()) { + // dive id field should be dive_{{dive_no}} se we remove the first 5 characters + QString diveIdString = collection.at(elemNo).attribute("id"); + int diveId = diveIdString.remove(0, 5).toInt(0, 10); + putProfileImage(collection.at(elemNo).geometry(), viewPort, &painter, get_dive_by_uniq_id(diveId), profile); + elemNo++; + } + + // scroll the webview to the next page + webView->page()->mainFrame()->scroll(0, pageSize.height()); + viewPort.adjust(0, pageSize.height(), 0, pageSize.height()); + + // rendering progress is 4/5 of total work + emit(progessUpdated((i * 80.0 / Pages) + done)); + if (i < Pages - 1 && printMode == Printer::PRINT) + static_cast(paintDevice)->newPage(); + } + painter.end(); + + // return profle settings + profile->setFrameStyle(profileFrameStyle); + profile->setPrintMode(false); + profile->setFontPrintScale(fontScale); + profile->setToolTipVisibile(true); + profile->resize(originalSize); + prefs.animation_speed = animationOriginal; + + //replot the dive after returning the settings + profile->plotDive(0, true); +} + +//value: ranges from 0 : 100 and shows the progress of the templating engine +void Printer::templateProgessUpdated(int value) +{ + done = value / 5; //template progess if 1/5 of total work + emit progessUpdated(done); +} + +void Printer::print() +{ + // we can only print if "PRINT" mode is selected + if (printMode != Printer::PRINT) { + return; + } + + + QPrinter *printerPtr; + printerPtr = static_cast(paintDevice); + + TemplateLayout t(printOptions, templateOptions); + connect(&t, SIGNAL(progressUpdated(int)), this, SLOT(templateProgessUpdated(int))); + dpi = printerPtr->resolution(); + //rendering resolution = selected paper size in inchs * printer dpi + pageSize.setHeight(qCeil(printerPtr->pageRect(QPrinter::Inch).height() * dpi)); + pageSize.setWidth(qCeil(printerPtr->pageRect(QPrinter::Inch).width() * dpi)); + webView->page()->setViewportSize(pageSize); + webView->page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff); + // export border width with at least 1 pixel + templateOptions->border_width = std::max(1, pageSize.width() / 1000); + if (printOptions->type == print_options::DIVELIST) { + webView->setHtml(t.generate()); + } else if (printOptions->type == print_options::STATISTICS ) { + webView->setHtml(t.generateStatistics()); + } + if (printOptions->color_selected && printerPtr->colorMode()) { + printerPtr->setColorMode(QPrinter::Color); + } else { + printerPtr->setColorMode(QPrinter::GrayScale); + } + // apply user settings + int divesPerPage; + + // get number of dives per page from data-numberofdives attribute in the body of the selected template + bool ok; + divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); + if (!ok) { + divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed + //TODO: show warning + } + int Pages; + if (divesPerPage == 0) { + flowRender(); + } else { + Pages = qCeil(getTotalWork(printOptions) / (float)divesPerPage); + render(Pages); + } +} + +void Printer::previewOnePage() +{ + if (printMode == PREVIEW) { + TemplateLayout t(printOptions, templateOptions); + + pageSize.setHeight(paintDevice->height()); + pageSize.setWidth(paintDevice->width()); + webView->page()->setViewportSize(pageSize); + // initialize the border settings + templateOptions->border_width = std::max(1, pageSize.width() / 1000); + if (printOptions->type == print_options::DIVELIST) { + webView->setHtml(t.generate()); + } else if (printOptions->type == print_options::STATISTICS ) { + webView->setHtml(t.generateStatistics()); + } + + bool ok; + int divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); + if (!ok) { + divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed + //TODO: show warning + } + if (divesPerPage == 0) { + flowRender(); + } else { + render(1); + } + } +} diff --git a/qt-ui/printer.h b/qt-ui/printer.h new file mode 100644 index 000000000..979cacd6a --- /dev/null +++ b/qt-ui/printer.h @@ -0,0 +1,48 @@ +#ifndef PRINTER_H +#define PRINTER_H + +#include +#include +#include +#include + +#include "profile/profilewidget2.h" +#include "printoptions.h" +#include "templateedit.h" + +class Printer : public QObject { + Q_OBJECT + +public: + enum PrintMode { + PRINT, + PREVIEW + }; + +private: + QPaintDevice *paintDevice; + QWebView *webView; + print_options *printOptions; + template_options *templateOptions; + QSize pageSize; + PrintMode printMode; + int done; + int dpi; + void render(int Pages); + void flowRender(); + void putProfileImage(QRect box, QRect viewPort, QPainter *painter, struct dive *dive, QPointer profile); + +private slots: + void templateProgessUpdated(int value); + +public: + Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode); + ~Printer(); + void print(); + void previewOnePage(); + +signals: + void progessUpdated(int value); +}; + +#endif //PRINTER_H diff --git a/qt-ui/templatelayout.cpp b/qt-ui/templatelayout.cpp new file mode 100644 index 000000000..a376459a6 --- /dev/null +++ b/qt-ui/templatelayout.cpp @@ -0,0 +1,182 @@ +#include + +#include "templatelayout.h" +#include "helpers.h" +#include "display.h" + +QList grantlee_templates, grantlee_statistics_templates; + +int getTotalWork(print_options *printOptions) +{ + if (printOptions->print_selected) { + // return the correct number depending on all/selected dives + // but don't return 0 as we might divide by this number + return amount_selected ? amount_selected : 1; + } + int dives = 0, i; + struct dive *dive; + for_each_dive (i, dive) { + dives++; + } + return dives; +} + +void find_all_templates() +{ + grantlee_templates.clear(); + grantlee_statistics_templates.clear(); + QDir dir(getPrintingTemplatePathUser()); + QFileInfoList list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + foreach (QFileInfo finfo, list) { + QString filename = finfo.fileName(); + if (filename.at(filename.size() - 1) != '~') { + grantlee_templates.append(finfo.fileName()); + } + } + // find statistics templates + dir.setPath(getPrintingTemplatePathUser() + QDir::separator() + "statistics"); + list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + foreach (QFileInfo finfo, list) { + QString filename = finfo.fileName(); + if (filename.at(filename.size() - 1) != '~') { + grantlee_statistics_templates.append(finfo.fileName()); + } + } +} + +TemplateLayout::TemplateLayout(print_options *PrintOptions, template_options *templateOptions) : + m_engine(NULL) +{ + this->PrintOptions = PrintOptions; + this->templateOptions = templateOptions; +} + +TemplateLayout::~TemplateLayout() +{ + delete m_engine; +} + +QString TemplateLayout::generate() +{ + int progress = 0; + int totalWork = getTotalWork(PrintOptions); + + QString htmlContent; + m_engine = new Grantlee::Engine(this); + + QSharedPointer m_templateLoader = + QSharedPointer(new Grantlee::FileSystemTemplateLoader()); + m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser()); + m_engine->addTemplateLoader(m_templateLoader); + + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + + QVariantList diveList; + + struct dive *dive; + int i; + for_each_dive (i, dive) { + //TODO check for exporting selected dives only + if (!dive->selected && PrintOptions->print_selected) + continue; + Dive d(dive); + diveList.append(QVariant::fromValue(d)); + progress++; + emit progressUpdated(progress * 100.0 / totalWork); + } + Grantlee::Context c; + c.insert("dives", diveList); + c.insert("template_options", QVariant::fromValue(*templateOptions)); + c.insert("print_options", QVariant::fromValue(*PrintOptions)); + + Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); + if (!t || t->error()) { + qDebug() << "Can't load template"; + return htmlContent; + } + + htmlContent = t->render(&c); + + if (t->error()) { + qDebug() << "Can't render template"; + return htmlContent; + } + return htmlContent; +} + +QString TemplateLayout::generateStatistics() +{ + QString htmlContent; + m_engine = new Grantlee::Engine(this); + + QSharedPointer m_templateLoader = + QSharedPointer(new Grantlee::FileSystemTemplateLoader()); + m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser() + QDir::separator() + QString("statistics")); + m_engine->addTemplateLoader(m_templateLoader); + + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + + QVariantList years; + + int i = 0; + while (stats_yearly != NULL && stats_yearly[i].period) { + YearInfo year(stats_yearly[i]); + years.append(QVariant::fromValue(year)); + i++; + } + + Grantlee::Context c; + c.insert("years", years); + c.insert("template_options", QVariant::fromValue(*templateOptions)); + c.insert("print_options", QVariant::fromValue(*PrintOptions)); + + Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); + if (!t || t->error()) { + qDebug() << "Can't load template"; + return htmlContent; + } + + htmlContent = t->render(&c); + + if (t->error()) { + qDebug() << "Can't render template"; + return htmlContent; + } + + emit progressUpdated(100); + return htmlContent; +} + +QString TemplateLayout::readTemplate(QString template_name) +{ + QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); + if (qfile.open(QFile::ReadOnly | QFile::Text)) { + QTextStream in(&qfile); + return in.readAll(); + } + return ""; +} + +void TemplateLayout::writeTemplate(QString template_name, QString grantlee_template) +{ + QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); + if (qfile.open(QFile::ReadWrite | QFile::Text)) { + qfile.write(grantlee_template.toUtf8().data()); + qfile.resize(qfile.pos()); + qfile.close(); + } +} + +YearInfo::YearInfo() +{ + +} + +YearInfo::~YearInfo() +{ + +} diff --git a/qt-ui/templatelayout.h b/qt-ui/templatelayout.h new file mode 100644 index 000000000..a2868e7ff --- /dev/null +++ b/qt-ui/templatelayout.h @@ -0,0 +1,168 @@ +#ifndef TEMPLATELAYOUT_H +#define TEMPLATELAYOUT_H + +#include +#include "mainwindow.h" +#include "printoptions.h" +#include "statistics.h" +#include "qthelper.h" +#include "helpers.h" + +int getTotalWork(print_options *printOptions); +void find_all_templates(); + +extern QList grantlee_templates, grantlee_statistics_templates; + +class TemplateLayout : public QObject { + Q_OBJECT +public: + TemplateLayout(print_options *PrintOptions, template_options *templateOptions); + ~TemplateLayout(); + QString generate(); + QString generateStatistics(); + static QString readTemplate(QString template_name); + static void writeTemplate(QString template_name, QString grantlee_template); + +private: + Grantlee::Engine *m_engine; + print_options *PrintOptions; + template_options *templateOptions; + +signals: + void progressUpdated(int value); +}; + +class YearInfo { +public: + stats_t *year; + YearInfo(stats_t& year) + :year(&year) + { + + } + YearInfo(); + ~YearInfo(); +}; + +Q_DECLARE_METATYPE(Dive) +Q_DECLARE_METATYPE(template_options) +Q_DECLARE_METATYPE(print_options) +Q_DECLARE_METATYPE(YearInfo) + +GRANTLEE_BEGIN_LOOKUP(Dive) +if (property == "number") + return object.number(); +else if (property == "id") + return object.id(); +else if (property == "date") + return object.date(); +else if (property == "time") + return object.time(); +else if (property == "location") + return object.location(); +else if (property == "duration") + return object.duration(); +else if (property == "depth") + return object.depth(); +else if (property == "divemaster") + return object.divemaster(); +else if (property == "buddy") + return object.buddy(); +else if (property == "airTemp") + return object.airTemp(); +else if (property == "waterTemp") + return object.waterTemp(); +else if (property == "notes") + return object.notes(); +else if (property == "rating") + return object.rating(); +else if (property == "sac") + return object.sac(); +else if (property == "tags") + return object.tags(); +else if (property == "gas") + return object.gas(); +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(template_options) +if (property == "font") { + switch (object.font_index) { + case 0: + return "Arial, Helvetica, sans-serif"; + case 1: + return "Impact, Charcoal, sans-serif"; + case 2: + return "Georgia, serif"; + case 3: + return "Courier, monospace"; + case 4: + return "Verdana, Geneva, sans-serif"; + } +} else if (property == "borderwidth") { + return object.border_width; +} else if (property == "font_size") { + return object.font_size / 9.0; +} else if (property == "line_spacing") { + return object.line_spacing; +} else if (property == "color1") { + return object.color_palette.color1.name(); +} else if (property == "color2") { + return object.color_palette.color2.name(); +} else if (property == "color3") { + return object.color_palette.color3.name(); +} else if (property == "color4") { + return object.color_palette.color4.name(); +} else if (property == "color5") { + return object.color_palette.color5.name(); +} else if (property == "color6") { + return object.color_palette.color6.name(); +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(print_options) +if (property == "grayscale") { + if (object.color_selected) { + return ""; + } else { + return "-webkit-filter: grayscale(100%)"; + } +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(YearInfo) +if (property == "year") { + return object.year->period; +} else if (property == "dives") { + return object.year->selection_size; +} else if (property == "min_temp") { + const char *unit; + double temp = get_temp_units(object.year->min_temp, &unit); + return object.year->min_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; +} else if (property == "max_temp") { + const char *unit; + double temp = get_temp_units(object.year->max_temp, &unit); + return object.year->max_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; +} else if (property == "total_time") { + return get_time_string(object.year->total_time.seconds, 0); +} else if (property == "avg_time") { + return get_minutes(object.year->total_time.seconds / object.year->selection_size); +} else if (property == "shortest_time") { + return get_minutes(object.year->shortest_time.seconds); +} else if (property == "longest_time") { + return get_minutes(object.year->longest_time.seconds); +} else if (property == "avg_depth") { + return get_depth_string(object.year->avg_depth); +} else if (property == "min_depth") { + return get_depth_string(object.year->min_depth); +} else if (property == "max_depth") { + return get_depth_string(object.year->max_depth); +} else if (property == "avg_sac") { + return get_volume_string(object.year->avg_sac); +} else if (property == "min_sac") { + return get_volume_string(object.year->min_sac); +} else if (property == "max_sac") { + return get_volume_string(object.year->max_sac); +} +GRANTLEE_END_LOOKUP + +#endif -- cgit v1.2.3-70-g09d2 From 4c0156e3d51b389db8eccc3fa3da4b8f248f9b13 Mon Sep 17 00:00:00 2001 From: Tomaz Canabrava Date: Wed, 2 Sep 2015 20:52:34 -0300 Subject: Move all core-functionality to subsurface-core And adapt a new CMakeLists.txt file for it. On the way I've also found out that we where double-compilling a few files. I've also set the subsurface-core as a include_path but that was just to reduce the noise on this commit, since I plan to remove it from the include path to make it obligatory to specify something like include "subsurface-core/dive.h" for the header files. Since the app is growing quite a bit we ended up having a few different files with almost same name that did similar things, I want to kill that (for instance Dive.h, dive.h, PrintDive.h and such). Signed-off-by: Tomaz Canabrava Signed-off-by: Dirk Hohndel --- CMakeLists.txt | 81 +- android.cpp | 205 -- checkcloudconnection.cpp | 95 - checkcloudconnection.h | 22 - cochran.c | 805 ----- cochran.h | 44 - color.h | 67 - configuredivecomputer.cpp | 681 ---- configuredivecomputer.h | 68 - configuredivecomputerthreads.cpp | 1760 ----------- configuredivecomputerthreads.h | 62 - datatrak.c | 695 ----- datatrak.h | 41 - deco.c | 600 ---- deco.h | 19 - device.c | 180 -- device.h | 18 - devicedetails.cpp | 78 - devicedetails.h | 97 - display.h | 63 - dive.c | 3465 -------------------- dive.h | 894 ------ divecomputer.cpp | 228 -- divecomputer.h | 38 - divelist.c | 1141 ------- divelist.h | 62 - divelogexportlogic.cpp | 161 - divelogexportlogic.h | 20 - divesite.c | 337 -- divesite.cpp | 31 - divesite.h | 80 - divesitehelpers.cpp | 208 -- divesitehelpers.h | 18 - equipment.c | 235 -- exif.cpp | 587 ---- exif.h | 147 - file.c | 1066 ------- file.h | 24 - gaspressures.c | 435 --- gaspressures.h | 35 - gettext.h | 10 - gettextfromc.cpp | 27 - gettextfromc.h | 18 - git-access.c | 891 ------ git-access.h | 31 - helpers.h | 52 - libdivecomputer.c | 1083 ------- libdivecomputer.h | 66 - linux.c | 225 -- liquivision.c | 414 --- load-git.c | 1638 ---------- macos.c | 206 -- membuffer.c | 285 -- membuffer.h | 74 - ostctools.c | 193 -- parse-xml.c | 3641 ---------------------- planner.c | 1455 --------- planner.h | 32 - pref.h | 158 - prefs-macros.h | 68 - profile.c | 1463 --------- profile.h | 109 - qt-gui.h | 17 - qt-init.cpp | 48 - qt-models/models.h | 6 +- qt-ui/CMakeLists.txt | 4 + qt-ui/configuredivecomputerdialog.h | 2 +- qt-ui/divelogimportdialog.h | 4 +- qt-ui/graphicsview-common.h | 2 +- qthelper.cpp | 1653 ---------- qthelper.h | 135 - qthelperfromc.h | 21 - qtserialbluetooth.cpp | 415 --- save-git.c | 1235 -------- save-html.c | 533 ---- save-html.h | 30 - save-xml.c | 743 ----- serial_ftdi.c | 664 ---- sha1.c | 300 -- sha1.h | 38 - statistics.c | 379 --- statistics.h | 58 - strndup.h | 21 - strtod.c | 128 - subsurface-core/CMakeLists.txt | 81 + subsurface-core/android.cpp | 205 ++ subsurface-core/checkcloudconnection.cpp | 95 + subsurface-core/checkcloudconnection.h | 22 + subsurface-core/cochran.c | 805 +++++ subsurface-core/cochran.h | 44 + subsurface-core/color.h | 67 + subsurface-core/configuredivecomputer.cpp | 681 ++++ subsurface-core/configuredivecomputer.h | 68 + subsurface-core/configuredivecomputerthreads.cpp | 1760 +++++++++++ subsurface-core/configuredivecomputerthreads.h | 62 + subsurface-core/datatrak.c | 695 +++++ subsurface-core/datatrak.h | 41 + subsurface-core/deco.c | 600 ++++ subsurface-core/deco.h | 19 + subsurface-core/device.c | 180 ++ subsurface-core/device.h | 18 + subsurface-core/devicedetails.cpp | 78 + subsurface-core/devicedetails.h | 97 + subsurface-core/display.h | 63 + subsurface-core/dive.c | 3465 ++++++++++++++++++++ subsurface-core/dive.h | 894 ++++++ subsurface-core/divecomputer.cpp | 228 ++ subsurface-core/divecomputer.h | 38 + subsurface-core/divelist.c | 1141 +++++++ subsurface-core/divelist.h | 62 + subsurface-core/divelogexportlogic.cpp | 161 + subsurface-core/divelogexportlogic.h | 20 + subsurface-core/divesite.c | 337 ++ subsurface-core/divesite.cpp | 31 + subsurface-core/divesite.h | 80 + subsurface-core/divesitehelpers.cpp | 208 ++ subsurface-core/divesitehelpers.h | 18 + subsurface-core/equipment.c | 235 ++ subsurface-core/exif.cpp | 587 ++++ subsurface-core/exif.h | 147 + subsurface-core/file.c | 1066 +++++++ subsurface-core/file.h | 24 + subsurface-core/gaspressures.c | 435 +++ subsurface-core/gaspressures.h | 35 + subsurface-core/gettext.h | 10 + subsurface-core/gettextfromc.cpp | 27 + subsurface-core/gettextfromc.h | 18 + subsurface-core/git-access.c | 891 ++++++ subsurface-core/git-access.h | 31 + subsurface-core/helpers.h | 52 + subsurface-core/libdivecomputer.c | 1083 +++++++ subsurface-core/libdivecomputer.h | 66 + subsurface-core/linux.c | 225 ++ subsurface-core/liquivision.c | 414 +++ subsurface-core/load-git.c | 1638 ++++++++++ subsurface-core/macos.c | 206 ++ subsurface-core/membuffer.c | 285 ++ subsurface-core/membuffer.h | 74 + subsurface-core/ostctools.c | 193 ++ subsurface-core/parse-xml.c | 3641 ++++++++++++++++++++++ subsurface-core/planner.c | 1455 +++++++++ subsurface-core/planner.h | 32 + subsurface-core/pref.h | 158 + subsurface-core/prefs-macros.h | 68 + subsurface-core/profile.c | 1463 +++++++++ subsurface-core/profile.h | 109 + subsurface-core/qt-gui.h | 17 + subsurface-core/qt-init.cpp | 48 + subsurface-core/qthelper.cpp | 1653 ++++++++++ subsurface-core/qthelper.h | 135 + subsurface-core/qthelperfromc.h | 21 + subsurface-core/qtserialbluetooth.cpp | 415 +++ subsurface-core/save-git.c | 1235 ++++++++ subsurface-core/save-html.c | 533 ++++ subsurface-core/save-html.h | 30 + subsurface-core/save-xml.c | 743 +++++ subsurface-core/serial_ftdi.c | 664 ++++ subsurface-core/sha1.c | 300 ++ subsurface-core/sha1.h | 38 + subsurface-core/statistics.c | 379 +++ subsurface-core/statistics.h | 58 + subsurface-core/strndup.h | 21 + subsurface-core/strtod.c | 128 + subsurface-core/subsurfacestartup.c | 319 ++ subsurface-core/subsurfacestartup.h | 26 + subsurface-core/subsurfacesysinfo.cpp | 620 ++++ subsurface-core/subsurfacesysinfo.h | 65 + subsurface-core/taxonomy.c | 48 + subsurface-core/taxonomy.h | 41 + subsurface-core/time.c | 98 + subsurface-core/uemis-downloader.c | 1359 ++++++++ subsurface-core/uemis.c | 392 +++ subsurface-core/uemis.h | 54 + subsurface-core/units.h | 277 ++ subsurface-core/version.c | 16 + subsurface-core/version.h | 16 + subsurface-core/webservice.h | 24 + subsurface-core/windows.c | 448 +++ subsurface-core/windowtitleupdate.cpp | 31 + subsurface-core/windowtitleupdate.h | 20 + subsurface-core/worldmap-options.h | 7 + subsurface-core/worldmap-save.c | 114 + subsurface-core/worldmap-save.h | 15 + subsurfacestartup.c | 319 -- subsurfacestartup.h | 26 - subsurfacesysinfo.cpp | 620 ---- subsurfacesysinfo.h | 65 - taxonomy.c | 48 - taxonomy.h | 41 - time.c | 98 - uemis-downloader.c | 1359 -------- uemis.c | 392 --- uemis.h | 54 - units.h | 277 -- version.c | 16 - version.h | 16 - webservice.h | 24 - windows.c | 448 --- windowtitleupdate.cpp | 31 - windowtitleupdate.h | 20 - worldmap-options.h | 7 - worldmap-save.c | 114 - worldmap-save.h | 15 - 203 files changed, 37461 insertions(+), 37437 deletions(-) delete mode 100644 android.cpp delete mode 100644 checkcloudconnection.cpp delete mode 100644 checkcloudconnection.h delete mode 100644 cochran.c delete mode 100644 cochran.h delete mode 100644 color.h delete mode 100644 configuredivecomputer.cpp delete mode 100644 configuredivecomputer.h delete mode 100644 configuredivecomputerthreads.cpp delete mode 100644 configuredivecomputerthreads.h delete mode 100644 datatrak.c delete mode 100644 datatrak.h delete mode 100644 deco.c delete mode 100644 deco.h delete mode 100644 device.c delete mode 100644 device.h delete mode 100644 devicedetails.cpp delete mode 100644 devicedetails.h delete mode 100644 display.h delete mode 100644 dive.c delete mode 100644 dive.h delete mode 100644 divecomputer.cpp delete mode 100644 divecomputer.h delete mode 100644 divelist.c delete mode 100644 divelist.h delete mode 100644 divelogexportlogic.cpp delete mode 100644 divelogexportlogic.h delete mode 100644 divesite.c delete mode 100644 divesite.cpp delete mode 100644 divesite.h delete mode 100644 divesitehelpers.cpp delete mode 100644 divesitehelpers.h delete mode 100644 equipment.c delete mode 100644 exif.cpp delete mode 100644 exif.h delete mode 100644 file.c delete mode 100644 file.h delete mode 100644 gaspressures.c delete mode 100644 gaspressures.h delete mode 100644 gettext.h delete mode 100644 gettextfromc.cpp delete mode 100644 gettextfromc.h delete mode 100644 git-access.c delete mode 100644 git-access.h delete mode 100644 helpers.h delete mode 100644 libdivecomputer.c delete mode 100644 libdivecomputer.h delete mode 100644 linux.c delete mode 100644 liquivision.c delete mode 100644 load-git.c delete mode 100644 macos.c delete mode 100644 membuffer.c delete mode 100644 membuffer.h delete mode 100644 ostctools.c delete mode 100644 parse-xml.c delete mode 100644 planner.c delete mode 100644 planner.h delete mode 100644 pref.h delete mode 100644 prefs-macros.h delete mode 100644 profile.c delete mode 100644 profile.h delete mode 100644 qt-gui.h delete mode 100644 qt-init.cpp delete mode 100644 qthelper.cpp delete mode 100644 qthelper.h delete mode 100644 qthelperfromc.h delete mode 100644 qtserialbluetooth.cpp delete mode 100644 save-git.c delete mode 100644 save-html.c delete mode 100644 save-html.h delete mode 100644 save-xml.c delete mode 100644 serial_ftdi.c delete mode 100644 sha1.c delete mode 100644 sha1.h delete mode 100644 statistics.c delete mode 100644 statistics.h delete mode 100644 strndup.h delete mode 100644 strtod.c create mode 100644 subsurface-core/CMakeLists.txt create mode 100644 subsurface-core/android.cpp create mode 100644 subsurface-core/checkcloudconnection.cpp create mode 100644 subsurface-core/checkcloudconnection.h create mode 100644 subsurface-core/cochran.c create mode 100644 subsurface-core/cochran.h create mode 100644 subsurface-core/color.h create mode 100644 subsurface-core/configuredivecomputer.cpp create mode 100644 subsurface-core/configuredivecomputer.h create mode 100644 subsurface-core/configuredivecomputerthreads.cpp create mode 100644 subsurface-core/configuredivecomputerthreads.h create mode 100644 subsurface-core/datatrak.c create mode 100644 subsurface-core/datatrak.h create mode 100644 subsurface-core/deco.c create mode 100644 subsurface-core/deco.h create mode 100644 subsurface-core/device.c create mode 100644 subsurface-core/device.h create mode 100644 subsurface-core/devicedetails.cpp create mode 100644 subsurface-core/devicedetails.h create mode 100644 subsurface-core/display.h create mode 100644 subsurface-core/dive.c create mode 100644 subsurface-core/dive.h create mode 100644 subsurface-core/divecomputer.cpp create mode 100644 subsurface-core/divecomputer.h create mode 100644 subsurface-core/divelist.c create mode 100644 subsurface-core/divelist.h create mode 100644 subsurface-core/divelogexportlogic.cpp create mode 100644 subsurface-core/divelogexportlogic.h create mode 100644 subsurface-core/divesite.c create mode 100644 subsurface-core/divesite.cpp create mode 100644 subsurface-core/divesite.h create mode 100644 subsurface-core/divesitehelpers.cpp create mode 100644 subsurface-core/divesitehelpers.h create mode 100644 subsurface-core/equipment.c create mode 100644 subsurface-core/exif.cpp create mode 100644 subsurface-core/exif.h create mode 100644 subsurface-core/file.c create mode 100644 subsurface-core/file.h create mode 100644 subsurface-core/gaspressures.c create mode 100644 subsurface-core/gaspressures.h create mode 100644 subsurface-core/gettext.h create mode 100644 subsurface-core/gettextfromc.cpp create mode 100644 subsurface-core/gettextfromc.h create mode 100644 subsurface-core/git-access.c create mode 100644 subsurface-core/git-access.h create mode 100644 subsurface-core/helpers.h create mode 100644 subsurface-core/libdivecomputer.c create mode 100644 subsurface-core/libdivecomputer.h create mode 100644 subsurface-core/linux.c create mode 100644 subsurface-core/liquivision.c create mode 100644 subsurface-core/load-git.c create mode 100644 subsurface-core/macos.c create mode 100644 subsurface-core/membuffer.c create mode 100644 subsurface-core/membuffer.h create mode 100644 subsurface-core/ostctools.c create mode 100644 subsurface-core/parse-xml.c create mode 100644 subsurface-core/planner.c create mode 100644 subsurface-core/planner.h create mode 100644 subsurface-core/pref.h create mode 100644 subsurface-core/prefs-macros.h create mode 100644 subsurface-core/profile.c create mode 100644 subsurface-core/profile.h create mode 100644 subsurface-core/qt-gui.h create mode 100644 subsurface-core/qt-init.cpp create mode 100644 subsurface-core/qthelper.cpp create mode 100644 subsurface-core/qthelper.h create mode 100644 subsurface-core/qthelperfromc.h create mode 100644 subsurface-core/qtserialbluetooth.cpp create mode 100644 subsurface-core/save-git.c create mode 100644 subsurface-core/save-html.c create mode 100644 subsurface-core/save-html.h create mode 100644 subsurface-core/save-xml.c create mode 100644 subsurface-core/serial_ftdi.c create mode 100644 subsurface-core/sha1.c create mode 100644 subsurface-core/sha1.h create mode 100644 subsurface-core/statistics.c create mode 100644 subsurface-core/statistics.h create mode 100644 subsurface-core/strndup.h create mode 100644 subsurface-core/strtod.c create mode 100644 subsurface-core/subsurfacestartup.c create mode 100644 subsurface-core/subsurfacestartup.h create mode 100644 subsurface-core/subsurfacesysinfo.cpp create mode 100644 subsurface-core/subsurfacesysinfo.h create mode 100644 subsurface-core/taxonomy.c create mode 100644 subsurface-core/taxonomy.h create mode 100644 subsurface-core/time.c create mode 100644 subsurface-core/uemis-downloader.c create mode 100644 subsurface-core/uemis.c create mode 100644 subsurface-core/uemis.h create mode 100644 subsurface-core/units.h create mode 100644 subsurface-core/version.c create mode 100644 subsurface-core/version.h create mode 100644 subsurface-core/webservice.h create mode 100644 subsurface-core/windows.c create mode 100644 subsurface-core/windowtitleupdate.cpp create mode 100644 subsurface-core/windowtitleupdate.h create mode 100644 subsurface-core/worldmap-options.h create mode 100644 subsurface-core/worldmap-save.c create mode 100644 subsurface-core/worldmap-save.h delete mode 100644 subsurfacestartup.c delete mode 100644 subsurfacestartup.h delete mode 100644 subsurfacesysinfo.cpp delete mode 100644 subsurfacesysinfo.h delete mode 100644 taxonomy.c delete mode 100644 taxonomy.h delete mode 100644 time.c delete mode 100644 uemis-downloader.c delete mode 100644 uemis.c delete mode 100644 uemis.h delete mode 100644 units.h delete mode 100644 version.c delete mode 100644 version.h delete mode 100644 webservice.h delete mode 100644 windows.c delete mode 100644 windowtitleupdate.cpp delete mode 100644 windowtitleupdate.h delete mode 100644 worldmap-options.h delete mode 100644 worldmap-save.c delete mode 100644 worldmap-save.h (limited to 'qt-ui') diff --git a/CMakeLists.txt b/CMakeLists.txt index ba6544210..0a14db6de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,12 @@ option(FBSUPPORT "allow posting to Facebook" ON) option(BTSUPPORT "enable support for QtBluetooth (requires Qt5.4 or newer)" ON) option(FTDISUPPORT "enable support for libftdi based serial" OFF) +add_definitions(-DSUBSURFACE_SOURCE="${CMAKE_SOURCE_DIR}") + +if(BTSUPPORT) + add_definitions(-DBT_SUPPORT) +endif() + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${${PROJECT_NAME}_SOURCE_DIR}/cmake/Modules @@ -34,6 +40,7 @@ include_directories(. qt-ui qt-models qt-ui/profile + subsurface-core/ ) # get the version string -- this is only used for Mac Bundle at this point @@ -268,19 +275,16 @@ add_custom_target( set(PLATFORM_SRC unknown_platform.c) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(SUBSURFACE_TARGET subsurface) - set(PLATFORM_SRC linux.c) # in some builds we appear to be missing libz for some strange reason... set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} -lz) endif() if(ANDROID) - set(PLATFORM_SRC android.cpp) set(SUBSURFACE_TARGET subsurface) # To allow us to debug log to logcat set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} -llog) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(SUBSURFACE_TARGET Subsurface) - set(PLATFORM_SRC macos.c) find_library(APP_SERVICES_LIBRARY ApplicationServices) find_library(HID_LIB HidApi) set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} ${HID_LIB}) @@ -299,7 +303,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") endif() if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(SUBSURFACE_TARGET subsurface) - set(PLATFORM_SRC windows.c) set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} -lwsock32 -lws2_32) remove_definitions(-DUNICODE) add_definitions(-mwindows -D_WIN32) @@ -307,68 +310,7 @@ endif() # include translations add_subdirectory(translations) - -if(BTSUPPORT) - add_definitions(-DBT_SUPPORT) - set(BT_SRC_FILES qt-ui/btdeviceselectiondialog.cpp) - set(BT_CORE_SRC_FILES qtserialbluetooth.cpp) -endif() - -# compile the core library, in C. -set(SUBSURFACE_CORE_LIB_SRCS - cochran.c - datatrak.c - deco.c - device.c - dive.c - divesite.c - divesite.cpp # some new stuff that is not c code but belongs to divesite. - divelist.c - equipment.c - file.c - git-access.c - libdivecomputer.c - liquivision.c - load-git.c - membuffer.c - ostctools.c - parse-xml.c - planner.c - profile.c - gaspressures.c - worldmap-save.c - save-git.c - save-xml.c - save-html.c - sha1.c - statistics.c - strtod.c - subsurfacestartup.c - time.c - uemis.c - uemis-downloader.c - version.c - # gettextfrommoc should be added because we are using it on the c-code. - gettextfromc.cpp - # dirk ported some core functionality to c++. - qthelper.cpp - divecomputer.cpp - exif.cpp - subsurfacesysinfo.cpp - devicedetails.cpp - configuredivecomputer.cpp - configuredivecomputerthreads.cpp - divesitehelpers.cpp - taxonomy.c - checkcloudconnection.cpp - windowtitleupdate.cpp - divelogexportlogic.cpp - qt-init.cpp - ${BT_CORE_SRC_FILES} - ${SERIAL_FTDI} - ${PLATFORM_SRC} -) -source_group("Subsurface Core" FILES ${SUBSURFACE_CORE_LIB_SRCS}) +add_subdirectory(subsurface-core) if(FBSUPPORT) add_definitions(-DFBSUPPORT) @@ -406,12 +348,9 @@ source_group("Subsurface Models" FILES ${SUBSURFACE_MODELS}) set(SUBSURFACE_APP main.cpp qt-gui.cpp - qthelper.cpp ) source_group("Subsurface App" FILES ${SUBSURFACE_APP}) -add_library(subsurface_corelib STATIC ${SUBSURFACE_CORE_LIB_SRCS} ) -target_link_libraries(subsurface_corelib ${QT_LIBRARIES}) add_library(subsurface_models STATIC ${SUBSURFACE_MODELS_LIB_SRCS}) target_link_libraries(subsurface_models ${QT_LIBRARIES}) @@ -483,7 +422,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") endif() # build an automated html exporter -add_executable(export-html EXCLUDE_FROM_ALL export-html.cpp qt-init.cpp qthelper.cpp ${SUBSURFACE_RESOURCES}) +add_executable(export-html EXCLUDE_FROM_ALL export-html.cpp ${SUBSURFACE_RESOURCES}) target_link_libraries(export-html subsurface_corelib ${SUBSURFACE_LINK_LIBRARIES}) # QTest based tests @@ -495,7 +434,7 @@ macro(TEST NAME FILE) set_tests_properties(${NAME}_run PROPERTIES DEPENDS ${NAME}_build) endmacro() -add_definitions(-DSUBSURFACE_SOURCE="${CMAKE_SOURCE_DIR}") + add_definitions(-g) if(NOT NO_TESTS) enable_testing() diff --git a/android.cpp b/android.cpp deleted file mode 100644 index 3e14bec02..000000000 --- a/android.cpp +++ /dev/null @@ -1,205 +0,0 @@ -/* implements Android specific functions */ -#include "dive.h" -#include "display.h" -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#define FTDI_VID 0x0403 -#define USB_SERVICE "usb" - -extern "C" { - -const char android_system_divelist_default_font[] = "Roboto"; -const char *system_divelist_default_font = android_system_divelist_default_font; -double system_divelist_default_font_size = 8.0; - -int get_usb_fd(uint16_t idVendor, uint16_t idProduct); -void subsurface_OS_pref_setup(void) -{ - // Abusing this function to get a decent place where we can wire in - // our open callback into libusb -#ifdef libusb_android_open_callback_func - libusb_set_android_open_callback(get_usb_fd); -#elif __ANDROID__ -#error we need libusb_android_open_callback -#endif -} - -bool subsurface_ignore_font(const char *font) -{ - // there are no old default fonts that we would want to ignore - return false; -} - -void subsurface_user_info(struct user_info *user) -{ /* Encourage use of at least libgit2-0.20 */ } - -static const char *system_default_path_append(const char *append) -{ - /* Replace this when QtCore/QStandardPaths getExternalStorageDirectory landed */ - QAndroidJniObject externalStorage = QAndroidJniObject::callStaticObjectMethod("android/os/Environment", "getExternalStorageDirectory", "()Ljava/io/File;"); - QAndroidJniObject externalStorageAbsolute = externalStorage.callObjectMethod("getAbsolutePath", "()Ljava/lang/String;"); - QString path = externalStorageAbsolute.toString(); - QAndroidJniEnvironment env; - if (env->ExceptionCheck()) { - // FIXME: Handle exception here. - env->ExceptionClear(); - path = QString("/sdcard"); - } - if (append) - path += QString("/%1").arg(append); - return strdup(path.toUtf8().data()); -} - -const char *system_default_directory(void) -{ - static const char *path = NULL; - if (!path) - path = system_default_path_append(NULL); - return path; -} - -const char *system_default_filename(void) -{ - static const char *filename = "subsurface.xml"; - static const char *path = NULL; - if (!path) - path = system_default_path_append(filename); - return path; -} - -int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) -{ - /* FIXME: we need to enumerate in some other way on android */ - /* qtserialport maybee? */ - return -1; -} - -/** - * Get the file descriptor of first available matching device attached to usb in android. - * - * returns a fd to the device, or -1 and errno is set. - */ -int get_usb_fd(uint16_t idVendor, uint16_t idProduct) -{ - int i; - jint fd, vendorid, productid; - QAndroidJniObject usbName, usbDevice; - - // Get the current main activity of the application. - QAndroidJniObject activity = QtAndroid::androidActivity(); - - QAndroidJniObject usb_service = QAndroidJniObject::fromString(USB_SERVICE); - - // Get UsbManager from activity - QAndroidJniObject usbManager = activity.callObjectMethod("getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", usb_service.object()); - - // Get a HashMap of all USB devices attached to Android - QAndroidJniObject deviceMap = usbManager.callObjectMethod("getDeviceList", "()Ljava/util/HashMap;"); - jint num_devices = deviceMap.callMethod("size", "()I"); - if (num_devices == 0) { - // No USB device is attached. - return -1; - } - - // Iterate over all the devices and find the first available FTDI device. - QAndroidJniObject keySet = deviceMap.callObjectMethod("keySet", "()Ljava/util/Set;"); - QAndroidJniObject iterator = keySet.callObjectMethod("iterator", "()Ljava/util/Iterator;"); - - for (i = 0; i < num_devices; i++) { - usbName = iterator.callObjectMethod("next", "()Ljava/lang/Object;"); - usbDevice = deviceMap.callObjectMethod ("get", "(Ljava/lang/Object;)Ljava/lang/Object;", usbName.object()); - vendorid = usbDevice.callMethod("getVendorId", "()I"); - productid = usbDevice.callMethod("getProductId", "()I"); - if(vendorid == idVendor && productid == idProduct) // Found the requested device - break; - } - if (i == num_devices) { - // No device found. - errno = ENOENT; - return -1; - } - - jboolean hasPermission = usbManager.callMethod("hasPermission", "(Landroid/hardware/usb/UsbDevice;)Z", usbDevice.object()); - if (!hasPermission) { - // You do not have permission to use the usbDevice. - // Please remove and reinsert the USB device. - // Could also give an dialogbox asking for permission. - errno = EPERM; - return -1; - } - - // An device is present and we also have permission to use the device. - // Open the device and get its file descriptor. - QAndroidJniObject usbDeviceConnection = usbManager.callObjectMethod("openDevice", "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/UsbDeviceConnection;", usbDevice.object()); - if (usbDeviceConnection.object() == NULL) { - // Some error occurred while opening the device. Exit. - errno = EINVAL; - return -1; - } - - // Finally get the required file descriptor. - fd = usbDeviceConnection.callMethod("getFileDescriptor", "()I"); - if (fd == -1) { - // The device is not opened. Some error. - errno = ENODEV; - return -1; - } - return fd; -} - -/* NOP wrappers to comform with windows.c */ -int subsurface_rename(const char *path, const char *newpath) -{ - return rename(path, newpath); -} - -int subsurface_open(const char *path, int oflags, mode_t mode) -{ - return open(path, oflags, mode); -} - -FILE *subsurface_fopen(const char *path, const char *mode) -{ - return fopen(path, mode); -} - -void *subsurface_opendir(const char *path) -{ - return (void *)opendir(path); -} - -int subsurface_access(const char *path, int mode) -{ - return access(path, mode); -} - -struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) -{ - return zip_open(path, flags, errorp); -} - -int subsurface_zip_close(struct zip *zip) -{ - return zip_close(zip); -} - -/* win32 console */ -void subsurface_console_init(bool dedicated) -{ - /* NOP */ -} - -void subsurface_console_exit(void) -{ - /* NOP */ -} -} diff --git a/checkcloudconnection.cpp b/checkcloudconnection.cpp deleted file mode 100644 index be2a2fa18..000000000 --- a/checkcloudconnection.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include -#include -#include - -#include "pref.h" -#include "helpers.h" - -#include "checkcloudconnection.h" - - -CheckCloudConnection::CheckCloudConnection(QObject *parent) : - QObject(parent), - reply(0) -{ - -} - -#define TEAPOT "/make-latte?number-of-shots=3" -#define HTTP_I_AM_A_TEAPOT 418 -#define MILK "Linus does not like non-fat milk" -bool CheckCloudConnection::checkServer() -{ - QTimer timer; - timer.setSingleShot(true); - QEventLoop loop; - QNetworkRequest request; - request.setRawHeader("Accept", "text/plain"); - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); - request.setUrl(QString(prefs.cloud_base_url) + TEAPOT); - QNetworkAccessManager *mgr = new QNetworkAccessManager(); - reply = mgr->get(request); - connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); - timer.start(5000); // wait five seconds - loop.exec(); - if (timer.isActive()) { - // didn't time out, did we get the right response? - timer.stop(); - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == HTTP_I_AM_A_TEAPOT && - reply->readAll() == QByteArray(MILK)) { - reply->deleteLater(); - mgr->deleteLater(); - if (verbose > 1) - qWarning() << "Cloud storage: successfully checked connection to cloud server"; - return true; - } - } else { - disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - reply->abort(); - } - if (verbose) - qDebug() << "connection test to cloud server failed" << - reply->error() << reply->errorString() << - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << - reply->readAll(); - reply->deleteLater(); - mgr->deleteLater(); - if (verbose) - qWarning() << "Cloud storage: unable to connect to cloud server"; - return false; -} - -void CheckCloudConnection::sslErrors(QList errorList) -{ - if (verbose) { - qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; - Q_FOREACH (QSslError err, errorList) { - qDebug() << err.errorString(); - } - } - QSslConfiguration conf = reply->sslConfiguration(); - QSslCertificate cert = conf.peerCertificate(); - QByteArray hexDigest = cert.digest().toHex(); - if (reply->url().toString().contains(prefs.cloud_base_url) && - hexDigest == "13ff44c62996cfa5cd69d6810675490e") { - if (verbose) - qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; - reply->ignoreSslErrors(); - } else { - if (verbose) - qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; - } -} - -// helper to be used from C code -extern "C" bool canReachCloudServer() -{ - if (verbose) - qWarning() << "Cloud storage: checking connection to cloud server"; - CheckCloudConnection *checker = new CheckCloudConnection; - return checker->checkServer(); -} diff --git a/checkcloudconnection.h b/checkcloudconnection.h deleted file mode 100644 index 58a412797..000000000 --- a/checkcloudconnection.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef CHECKCLOUDCONNECTION_H -#define CHECKCLOUDCONNECTION_H - -#include -#include -#include - -#include "checkcloudconnection.h" - -class CheckCloudConnection : public QObject { - Q_OBJECT -public: - CheckCloudConnection(QObject *parent = 0); - bool checkServer(); -private: - QNetworkReply *reply; -private -slots: - void sslErrors(QList errorList); -}; - -#endif // CHECKCLOUDCONNECTION_H diff --git a/cochran.c b/cochran.c deleted file mode 100644 index 267fe2b4a..000000000 --- a/cochran.c +++ /dev/null @@ -1,805 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "dive.h" -#include "file.h" -#include "units.h" -#include "gettext.h" -#include "cochran.h" -#include "divelist.h" - -#include - -#define POUND 0.45359237 -#define FEET 0.3048 -#define INCH 0.0254 -#define GRAVITY 9.80665 -#define ATM 101325.0 -#define BAR 100000.0 -#define FSW (ATM / 33.0) -#define MSW (BAR / 10.0) -#define PSI ((POUND * GRAVITY) / (INCH * INCH)) - -// Some say 0x4a14 and 0x4b14 are the right number for this offset -// This works with CAN files from Analyst 4.01v and computers -// such as Commander, Gemini, EMC-16, and EMC-20H -#define LOG_ENTRY_OFFSET 0x4914 - -enum cochran_type { - TYPE_GEMINI, - TYPE_COMMANDER, - TYPE_EMC -}; - -struct config { - enum cochran_type type; - unsigned int logbook_size; - unsigned int sample_size; -} config; - - -// Convert 4 bytes into an INT -#define array_uint16_le(p) ((unsigned int) (p)[0] \ - + ((p)[1]<<8) ) -#define array_uint32_le(p) ((unsigned int) (p)[0] \ - + ((p)[1]<<8) + ((p)[2]<<16) \ - + ((p)[3]<<24)) - -/* - * The Cochran file format is designed to be annoying to read. It's roughly: - * - * 0x00000: room for 65534 4-byte words, giving the starting offsets - * of the dives themselves. - * - * 0x3fff8: the size of the file + 1 - * 0x3ffff: 0 (high 32 bits of filesize? Bogus: the offsets into the file - * are 32-bit, so it can't be a large file anyway) - * - * 0x40000: byte 0x46 - * 0x40001: "block 0": 256 byte encryption key - * 0x40101: the random modulus, or length of the key to use - * 0x40102: block 1: Version and date of Analyst and a feature string identifying - * the computer features and the features of the file - * 0x40138: Computer configuration page 1, 512 bytes - * 0x40338: Computer configuration page 2, 512 bytes - * 0x40538: Misc data (tissues) 1500 bytes - * 0x40b14: Ownership data 512 bytes ??? - * - * 0x4171c: Ownership data 512 bytes ??? - * - * 0x45415: Time stamp 17 bytes - * 0x45426: Computer configuration page 1, 512 bytes - * 0x45626: Computer configuration page 2, 512 bytes - * - */ -static unsigned int partial_decode(unsigned int start, unsigned int end, - const unsigned char *decode, unsigned offset, unsigned mod, - const unsigned char *buf, unsigned int size, unsigned char *dst) -{ - unsigned i, sum = 0; - - for (i = start; i < end; i++) { - unsigned char d = decode[offset++]; - if (i >= size) - break; - if (offset == mod) - offset = 0; - d += buf[i]; - if (dst) - dst[i] = d; - sum += d; - } - return sum; -} - -#ifdef COCHRAN_DEBUG - -#define hexchar(n) ("0123456789abcdef"[(n) & 15]) - -static int show_line(unsigned offset, const unsigned char *data, - unsigned size, int show_empty) -{ - unsigned char bits; - int i, off; - char buffer[120]; - - if (size > 16) - size = 16; - - bits = 0; - memset(buffer, ' ', sizeof(buffer)); - off = sprintf(buffer, "%06x ", offset); - for (i = 0; i < size; i++) { - char *hex = buffer + off + 3 * i; - char *asc = buffer + off + 50 + i; - unsigned char byte = data[i]; - - hex[0] = hexchar(byte >> 4); - hex[1] = hexchar(byte); - bits |= byte; - if (byte < 32 || byte > 126) - byte = '.'; - asc[0] = byte; - asc[1] = 0; - } - - if (bits) { - puts(buffer); - return 1; - } - if (show_empty) - puts("..."); - return 0; -} - -static void cochran_debug_write(const unsigned char *data, unsigned size) -{ - return; - - int show = 1, i; - for (i = 0; i < size; i += 16) - show = show_line(i, data + i, size - i, show); -} - -static void cochran_debug_sample(const char *s, unsigned int seconds) -{ - switch (config.type) { - case TYPE_GEMINI: - switch (seconds % 4) { - case 0: - printf("Hex: %02x %02x ", s[0], s[1]); - break; - case 1: - printf("Hex: %02x %02x ", s[0], s[1]); - break; - case 2: - printf("Hex: %02x %02x ", s[0], s[1]); - break; - case 3: - printf("Hex: %02x %02x ", s[0], s[1]); - break; - } - break; - case TYPE_COMMANDER: - switch (seconds % 2) { - case 0: - printf("Hex: %02x %02x ", s[0], s[1]); - break; - case 1: - printf("Hex: %02x %02x ", s[0], s[1]); - break; - } - break; - case TYPE_EMC: - switch (seconds % 2) { - case 0: - printf("Hex: %02x %02x %02x ", s[0], s[1], s[2]); - break; - case 1: - printf("Hex: %02x %02x %02x ", s[0], s[1], s[2]); - break; - } - break; - } - - printf ("%02dh %02dm %02ds: Depth: %-5.2f, ", seconds / 3660, - (seconds % 3660) / 60, seconds % 60, depth); -} - -#endif // COCHRAN_DEBUG - -static void cochran_parse_header(const unsigned char *decode, unsigned mod, - const unsigned char *in, unsigned size) -{ - unsigned char *buf = malloc(size); - - /* Do the "null decode" using a one-byte decode array of '\0' */ - /* Copies in plaintext, will be overwritten later */ - partial_decode(0, 0x0102, (const unsigned char *)"", 0, 1, in, size, buf); - - /* - * The header scrambling is different form the dive - * scrambling. Oh yay! - */ - partial_decode(0x0102, 0x010e, decode, 0, mod, in, size, buf); - partial_decode(0x010e, 0x0b14, decode, 0, mod, in, size, buf); - partial_decode(0x0b14, 0x1b14, decode, 0, mod, in, size, buf); - partial_decode(0x1b14, 0x2b14, decode, 0, mod, in, size, buf); - partial_decode(0x2b14, 0x3b14, decode, 0, mod, in, size, buf); - partial_decode(0x3b14, 0x5414, decode, 0, mod, in, size, buf); - partial_decode(0x5414, size, decode, 0, mod, in, size, buf); - - // Detect log type - switch (buf[0x133]) { - case '2': // Cochran Commander, version II log format - config.logbook_size = 256; - if (buf[0x132] == 0x10) { - config.type = TYPE_GEMINI; - config.sample_size = 2; // Gemini with tank PSI samples - } else { - config.type = TYPE_COMMANDER; - config.sample_size = 2; // Commander - } - break; - case '3': // Cochran EMC, version III log format - config.type = TYPE_EMC; - config.logbook_size = 512; - config.sample_size = 3; - break; - default: - printf ("Unknown log format v%c\n", buf[0x137]); - free(buf); - exit(1); - break; - } - -#ifdef COCHRAN_DEBUG - puts("Header\n======\n\n"); - cochran_debug_write(buf, size); -#endif - - free(buf); -} - -/* -* Bytes expected after a pre-dive event code -*/ -static int cochran_predive_event_bytes(unsigned char code) -{ - int x = 0; - int gem_event_bytes[15][2] = {{0x00, 10}, {0x02, 17}, {0x08, 18}, - {0x09, 18}, {0x0c, 18}, {0x0d, 18}, - {0x0e, 18}, - {-1, 0}}; - int cmdr_event_bytes[15][2] = {{0x00, 16}, {0x01, 20}, {0x02, 17}, - {0x03, 16}, {0x06, 18}, {0x07, 18}, - {0x08, 18}, {0x09, 18}, {0x0a, 18}, - {0x0b, 20}, {0x0c, 18}, {0x0d, 18}, - {0x0e, 18}, {0x10, 20}, - {-1, 0}}; - int emc_event_bytes[15][2] = {{0x00, 18}, {0x01, 22}, {0x02, 19}, - {0x03, 18}, {0x06, 20}, {0x07, 20}, - {0x0a, 20}, {0x0b, 20}, {0x0f, 18}, - {0x10, 20}, - {-1, 0}}; - - switch (config.type) { - case TYPE_GEMINI: - while (gem_event_bytes[x][0] != code && gem_event_bytes[x][0] != -1) - x++; - return gem_event_bytes[x][1]; - break; - case TYPE_COMMANDER: - while (cmdr_event_bytes[x][0] != code && cmdr_event_bytes[x][0] != -1) - x++; - return cmdr_event_bytes[x][1]; - break; - case TYPE_EMC: - while (emc_event_bytes[x][0] != code && emc_event_bytes[x][0] != -1) - x++; - return emc_event_bytes[x][1]; - break; - } - - return 0; -} - -int cochran_dive_event_bytes(unsigned char event) -{ - return (event == 0xAD || event == 0xAB) ? 4 : 0; -} - -static void cochran_dive_event(struct divecomputer *dc, const unsigned char *s, - unsigned int seconds, unsigned int *in_deco, - unsigned int *deco_ceiling, unsigned int *deco_time) -{ - switch (s[0]) { - case 0xC5: // Deco obligation begins - *in_deco = 1; - add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); - break; - case 0xDB: // Deco obligation ends - *in_deco = 0; - add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, - SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); - break; - case 0xAD: // Raise deco ceiling 10 ft - *deco_ceiling -= 10; // ft - *deco_time = (array_uint16_le(s + 3) + 1) * 60; - break; - case 0xAB: // Lower deco ceiling 10 ft - *deco_ceiling += 10; // ft - *deco_time = (array_uint16_le(s + 3) + 1) * 60; - break; - case 0xA8: // Entered Post Dive interval mode (surfaced) - break; - case 0xA9: // Exited PDI mode (re-submierged) - break; - case 0xBD: // Switched to normal PO2 setting - break; - case 0xC0: // Switched to FO2 21% mode (generally upon surface) - break; - case 0xC1: // "Ascent rate alarm - add_event(dc, seconds, SAMPLE_EVENT_ASCENT, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "ascent")); - break; - case 0xC2: // Low battery warning -#ifdef SAMPLE_EVENT_BATTERY - add_event(dc, seconds, SAMPLE_EVENT_BATTERY, - SAMPLE_FLAGS_NONE, 0, - QT_TRANSLATE_NOOP("gettextFromC", "battery")); -#endif - break; - case 0xC3: // CNS warning - add_event(dc, seconds, SAMPLE_EVENT_OLF, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "OLF")); - break; - case 0xC4: // Depth alarm begin - add_event(dc, seconds, SAMPLE_EVENT_MAXDEPTH, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "maxdepth")); - break; - case 0xC8: // PPO2 alarm begin - add_event(dc, seconds, SAMPLE_EVENT_PO2, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚")); - break; - case 0xCC: // Low cylinder 1 pressure"; - break; - case 0xCD: // Switch to deco blend setting - add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, - SAMPLE_FLAGS_NONE, 0, - QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); - break; - case 0xCE: // NDL alarm begin - add_event(dc, seconds, SAMPLE_EVENT_RBT, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "rbt")); - break; - case 0xD0: // Breathing rate alarm begin - break; - case 0xD3: // Low gas 1 flow rate alarm begin"; - break; - case 0xD6: // Ceiling alarm begin - add_event(dc, seconds, SAMPLE_EVENT_CEILING, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "ceiling")); - break; - case 0xD8: // End decompression mode - *in_deco = 0; - add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, - SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); - break; - case 0xE1: // Ascent alarm end - add_event(dc, seconds, SAMPLE_EVENT_ASCENT, - SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "ascent")); - break; - case 0xE2: // Low transmitter battery alarm - add_event(dc, seconds, SAMPLE_EVENT_TRANSMITTER, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "transmitter")); - break; - case 0xE3: // Switch to FO2 mode - break; - case 0xE5: // Switched to PO2 mode - break; - case 0xE8: // PO2 too low alarm - add_event(dc, seconds, SAMPLE_EVENT_PO2, - SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚")); - break; - case 0xEE: // NDL alarm end - add_event(dc, seconds, SAMPLE_EVENT_RBT, - SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "rbt")); - break; - case 0xEF: // Switch to blend 2 - add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, - SAMPLE_FLAGS_NONE, 0, - QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); - break; - case 0xF0: // Breathing rate alarm end - break; - case 0xF3: // Switch to blend 1 (often at dive start) - add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, - SAMPLE_FLAGS_NONE, 0, - QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); - break; - case 0xF6: // Ceiling alarm end - add_event(dc, seconds, SAMPLE_EVENT_CEILING, - SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "ceiling")); - break; - default: - break; - } -} - -/* -* Parse sample data, extract events and build a dive -*/ -static void cochran_parse_samples(struct dive *dive, const unsigned char *log, - const unsigned char *samples, int size, - unsigned int *duration, double *max_depth, - double *avg_depth, double *min_temp) -{ - const unsigned char *s; - unsigned int offset = 0, seconds = 0; - double depth = 0, temp = 0, depth_sample = 0, psi = 0, sgc_rate = 0; - int ascent_rate = 0; - unsigned int ndl = 0; - unsigned int in_deco = 0, deco_ceiling = 0, deco_time = 0; - - struct divecomputer *dc = &dive->dc; - struct sample *sample; - - // Initialize stat variables - *max_depth = 0, *avg_depth = 0, *min_temp = 0xFF; - - // Get starting depth and temp (tank PSI???) - switch (config.type) { - case TYPE_GEMINI: - depth = (float) (log[CMD_START_DEPTH] - + log[CMD_START_DEPTH + 1] * 256) / 4; - temp = log[CMD_START_TEMP]; - psi = log[CMD_START_PSI] + log[CMD_START_PSI + 1] * 256; - sgc_rate = (float)(log[CMD_START_SGC] - + log[CMD_START_SGC + 1] * 256) / 2; - break; - case TYPE_COMMANDER: - depth = (float) (log[CMD_START_DEPTH] - + log[CMD_START_DEPTH + 1] * 256) / 4; - temp = log[CMD_START_TEMP]; - break; - - case TYPE_EMC: - depth = (float) log [EMC_START_DEPTH] / 256 - + log[EMC_START_DEPTH + 1]; - temp = log[EMC_START_TEMP]; - break; - } - - // Skip past pre-dive events - unsigned int x = 0; - if (samples[x] != 0x40) { - unsigned int c; - while ((samples[x] & 0x80) == 0 && samples[x] != 0x40 && x < size) { - c = cochran_predive_event_bytes(samples[x]) + 1; -#ifdef COCHRAN_DEBUG - printf("Predive event: ", samples[x]); - for (int y = 0; y < c; y++) printf("%02x ", samples[x + y]); - putchar('\n'); -#endif - x += c; - } - } - - // Now process samples - offset = x; - while (offset < size) { - s = samples + offset; - - // Start with an empty sample - sample = prepare_sample(dc); - sample->time.seconds = seconds; - - // Check for event - if (s[0] & 0x80) { - cochran_dive_event(dc, s, seconds, &in_deco, &deco_ceiling, &deco_time); - offset += cochran_dive_event_bytes(s[0]) + 1; - continue; - } - - // Depth is in every sample - depth_sample = (float)(s[0] & 0x3F) / 4 * (s[0] & 0x40 ? -1 : 1); - depth += depth_sample; - -#ifdef COCHRAN_DEBUG - cochran_debug_sample(s, seconds); -#endif - - switch (config.type) { - case TYPE_COMMANDER: - switch (seconds % 2) { - case 0: // Ascent rate - ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1: -1); - break; - case 1: // Temperature - temp = s[1] / 2 + 20; - break; - } - break; - case TYPE_GEMINI: - // Gemini with tank pressure and SAC rate. - switch (seconds % 4) { - case 0: // Ascent rate - ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1); - break; - case 2: // PSI change - psi -= (float)(s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1) / 4; - break; - case 1: // SGC rate - sgc_rate -= (float)(s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1) / 2; - break; - case 3: // Temperature - temp = (float)s[1] / 2 + 20; - break; - } - break; - case TYPE_EMC: - switch (seconds % 2) { - case 0: // Ascent rate - ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1: -1); - break; - case 1: // Temperature - temp = (float)s[1] / 2 + 20; - break; - } - // Get NDL and deco information - switch (seconds % 24) { - case 20: - if (in_deco) { - // Fist stop time - //first_deco_time = (s[2] + s[5] * 256 + 1) * 60; // seconds - ndl = 0; - } else { - // NDL - ndl = (s[2] + s[5] * 256 + 1) * 60; // seconds - deco_time = 0; - } - break; - case 22: - if (in_deco) { - // Total stop time - deco_time = (s[2] + s[5] * 256 + 1) * 60; // seconds - ndl = 0; - } - break; - } - } - - // Track dive stats - if (depth > *max_depth) *max_depth = depth; - if (temp < *min_temp) *min_temp = temp; - *avg_depth = (*avg_depth * seconds + depth) / (seconds + 1); - - sample->depth.mm = depth * FEET * 1000; - sample->ndl.seconds = ndl; - sample->in_deco = in_deco; - sample->stoptime.seconds = deco_time; - sample->stopdepth.mm = deco_ceiling * FEET * 1000; - sample->temperature.mkelvin = C_to_mkelvin((temp - 32) / 1.8); - sample->sensor = 0; - sample->cylinderpressure.mbar = psi * PSI / 100; - - finish_sample(dc); - - offset += config.sample_size; - seconds++; - } - (void)ascent_rate; // mark the variable as unused - - if (seconds > 0) - *duration = seconds - 1; -} - -static void cochran_parse_dive(const unsigned char *decode, unsigned mod, - const unsigned char *in, unsigned size) -{ - unsigned char *buf = malloc(size); - struct dive *dive; - struct divecomputer *dc; - struct tm tm = {0}; - uint32_t csum[5]; - - double max_depth, avg_depth, min_temp; - unsigned int duration = 0, corrupt_dive = 0; - - /* - * The scrambling has odd boundaries. I think the boundaries - * match some data structure size, but I don't know. They were - * discovered the same way we dynamically discover the decode - * size: automatically looking for least random output. - * - * The boundaries are also this confused "off-by-one" thing, - * the same way the file size is off by one. It's as if the - * cochran software forgot to write one byte at the beginning. - */ - partial_decode(0, 0x0fff, decode, 1, mod, in, size, buf); - partial_decode(0x0fff, 0x1fff, decode, 0, mod, in, size, buf); - partial_decode(0x1fff, 0x2fff, decode, 0, mod, in, size, buf); - partial_decode(0x2fff, 0x48ff, decode, 0, mod, in, size, buf); - - /* - * This is not all the descrambling you need - the above are just - * what appears to be the fixed-size blocks. The rest is also - * scrambled, but there seems to be size differences in the data, - * so this just descrambles part of it: - */ - // Decode log entry (512 bytes + random prefix) - partial_decode(0x48ff, 0x4914 + config.logbook_size, decode, - 0, mod, in, size, buf); - - unsigned int sample_size = size - 0x4914 - config.logbook_size; - int g; - - // Decode sample data - partial_decode(0x4914 + config.logbook_size, size, decode, - 0, mod, in, size, buf); - -#ifdef COCHRAN_DEBUG - // Display pre-logbook data - puts("\nPre Logbook Data\n"); - cochran_debug_write(buf, 0x4914); - - // Display log book - puts("\nLogbook Data\n"); - cochran_debug_write(buf + 0x4914, config.logbook_size + 0x400); - - // Display sample data - puts("\nSample Data\n"); -#endif - - dive = alloc_dive(); - dc = &dive->dc; - - unsigned char *log = (buf + 0x4914); - - switch (config.type) { - case TYPE_GEMINI: - case TYPE_COMMANDER: - if (config.type == TYPE_GEMINI) { - dc->model = "Gemini"; - dc->deviceid = buf[0x18c] * 256 + buf[0x18d]; // serial no - fill_default_cylinder(&dive->cylinder[0]); - dive->cylinder[0].gasmix.o2.permille = (log[CMD_O2_PERCENT] / 256 - + log[CMD_O2_PERCENT + 1]) * 10; - dive->cylinder[0].gasmix.he.permille = 0; - } else { - dc->model = "Commander"; - dc->deviceid = array_uint32_le(buf + 0x31e); // serial no - for (g = 0; g < 2; g++) { - fill_default_cylinder(&dive->cylinder[g]); - dive->cylinder[g].gasmix.o2.permille = (log[CMD_O2_PERCENT + g * 2] / 256 - + log[CMD_O2_PERCENT + g * 2 + 1]) * 10; - dive->cylinder[g].gasmix.he.permille = 0; - } - } - - tm.tm_year = log[CMD_YEAR]; - tm.tm_mon = log[CMD_MON] - 1; - tm.tm_mday = log[CMD_DAY]; - tm.tm_hour = log[CMD_HOUR]; - tm.tm_min = log[CMD_MIN]; - tm.tm_sec = log[CMD_SEC]; - tm.tm_isdst = -1; - - dive->when = dc->when = utc_mktime(&tm); - dive->number = log[CMD_NUMBER] + log[CMD_NUMBER + 1] * 256 + 1; - dc->duration.seconds = (log[CMD_BT] + log[CMD_BT + 1] * 256) * 60; - dc->surfacetime.seconds = (log[CMD_SIT] + log[CMD_SIT + 1] * 256) * 60; - dc->maxdepth.mm = (log[CMD_MAX_DEPTH] + - log[CMD_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000; - dc->meandepth.mm = (log[CMD_AVG_DEPTH] + - log[CMD_AVG_DEPTH + 1] * 256) / 4 * FEET * 1000; - dc->watertemp.mkelvin = C_to_mkelvin((log[CMD_MIN_TEMP] / 32) - 1.8); - dc->surface_pressure.mbar = ATM / BAR * pow(1 - 0.0000225577 - * (double) log[CMD_ALTITUDE] * 250 * FEET, 5.25588) * 1000; - dc->salinity = 10000 + 150 * log[CMD_WATER_CONDUCTIVITY]; - - SHA1(log + CMD_NUMBER, 2, (unsigned char *)csum); - dc->diveid = csum[0]; - - if (log[CMD_MAX_DEPTH] == 0xff && log[CMD_MAX_DEPTH + 1] == 0xff) - corrupt_dive = 1; - - break; - case TYPE_EMC: - dc->model = "EMC"; - dc->deviceid = array_uint32_le(buf + 0x31e); // serial no - for (g = 0; g < 4; g++) { - fill_default_cylinder(&dive->cylinder[g]); - dive->cylinder[g].gasmix.o2.permille = - (log[EMC_O2_PERCENT + g * 2] / 256 - + log[EMC_O2_PERCENT + g * 2 + 1]) * 10; - dive->cylinder[g].gasmix.he.permille = - (log[EMC_HE_PERCENT + g * 2] / 256 - + log[EMC_HE_PERCENT + g * 2 + 1]) * 10; - } - - tm.tm_year = log[EMC_YEAR]; - tm.tm_mon = log[EMC_MON] - 1; - tm.tm_mday = log[EMC_DAY]; - tm.tm_hour = log[EMC_HOUR]; - tm.tm_min = log[EMC_MIN]; - tm.tm_sec = log[EMC_SEC]; - tm.tm_isdst = -1; - - dive->when = dc->when = utc_mktime(&tm); - dive->number = log[EMC_NUMBER] + log[EMC_NUMBER + 1] * 256 + 1; - dc->duration.seconds = (log[EMC_BT] + log[EMC_BT + 1] * 256) * 60; - dc->surfacetime.seconds = (log[EMC_SIT] + log[EMC_SIT + 1] * 256) * 60; - dc->maxdepth.mm = (log[EMC_MAX_DEPTH] + - log[EMC_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000; - dc->meandepth.mm = (log[EMC_AVG_DEPTH] + - log[EMC_AVG_DEPTH + 1] * 256) / 4 * FEET * 1000; - dc->watertemp.mkelvin = C_to_mkelvin((log[EMC_MIN_TEMP] - 32) / 1.8); - dc->surface_pressure.mbar = ATM / BAR * pow(1 - 0.0000225577 - * (double) log[EMC_ALTITUDE] * 250 * FEET, 5.25588) * 1000; - dc->salinity = 10000 + 150 * (log[EMC_WATER_CONDUCTIVITY] & 0x3); - - SHA1(log + EMC_NUMBER, 2, (unsigned char *)csum); - dc->diveid = csum[0]; - - if (log[EMC_MAX_DEPTH] == 0xff && log[EMC_MAX_DEPTH + 1] == 0xff) - corrupt_dive = 1; - - break; - } - - cochran_parse_samples(dive, buf + 0x4914, buf + 0x4914 - + config.logbook_size, sample_size, - &duration, &max_depth, &avg_depth, &min_temp); - - // Check for corrupt dive - if (corrupt_dive) { - dc->maxdepth.mm = max_depth * FEET * 1000; - dc->meandepth.mm = avg_depth * FEET * 1000; - dc->watertemp.mkelvin = C_to_mkelvin((min_temp - 32) / 1.8); - dc->duration.seconds = duration; - } - - dive->downloaded = true; - record_dive(dive); - mark_divelist_changed(true); - - free(buf); -} - -int try_to_open_cochran(const char *filename, struct memblock *mem) -{ - unsigned int i; - unsigned int mod; - unsigned int *offsets, dive1, dive2; - unsigned char *decode = mem->buffer + 0x40001; - - if (mem->size < 0x40000) - return 0; - - offsets = (unsigned int *) mem->buffer; - dive1 = offsets[0]; - dive2 = offsets[1]; - - if (dive1 < 0x40000 || dive2 < dive1 || dive2 > mem->size) - return 0; - - mod = decode[0x100] + 1; - cochran_parse_header(decode, mod, mem->buffer + 0x40000, dive1 - 0x40000); - - // Decode each dive - for (i = 0; i < 65534; i++) { - dive1 = offsets[i]; - dive2 = offsets[i + 1]; - if (dive2 < dive1) - break; - if (dive2 > mem->size) - break; - - cochran_parse_dive(decode, mod, mem->buffer + dive1, - dive2 - dive1); - } - - return 1; // no further processing needed -} diff --git a/cochran.h b/cochran.h deleted file mode 100644 index 97d4361c8..000000000 --- a/cochran.h +++ /dev/null @@ -1,44 +0,0 @@ -// Commander log fields -#define CMD_SEC 1 -#define CMD_MIN 0 -#define CMD_HOUR 3 -#define CMD_DAY 2 -#define CMD_MON 5 -#define CMD_YEAR 4 -#define CME_START_OFFSET 6 // 4 bytes -#define CMD_WATER_CONDUCTIVITY 25 // 1 byte, 0=low, 2=high -#define CMD_START_SGC 42 // 2 bytes -#define CMD_START_TEMP 45 // 1 byte, F -#define CMD_START_DEPTH 56 // 2 bytes, /4=ft -#define CMD_START_PSI 62 -#define CMD_SIT 68 // 2 bytes, minutes -#define CMD_NUMBER 70 // 2 bytes -#define CMD_ALTITUDE 73 // 1 byte, /4=Kilofeet -#define CMD_END_OFFSET 128 // 4 bytes -#define CMD_MIN_TEMP 153 // 1 byte, F -#define CMD_BT 166 // 2 bytes, minutes -#define CMD_MAX_DEPTH 168 // 2 bytes, /4=ft -#define CMD_AVG_DEPTH 170 // 2 bytes, /4=ft -#define CMD_O2_PERCENT 210 // 8 bytes, 4 x 2 byte, /256=% - -// EMC log fields -#define EMC_SEC 0 -#define EMC_MIN 1 -#define EMC_HOUR 2 -#define EMC_DAY 3 -#define EMC_MON 4 -#define EMC_YEAR 5 -#define EMC_START_OFFSET 6 // 4 bytes -#define EMC_WATER_CONDUCTIVITY 24 // 1 byte bits 0:1, 0=low, 2=high -#define EMC_START_DEPTH 42 // 2 byte, /256=ft -#define EMC_START_TEMP 55 // 1 byte, F -#define EMC_SIT 84 // 2 bytes, minutes, LE -#define EMC_NUMBER 86 // 2 bytes -#define EMC_ALTITUDE 89 // 1 byte, /4=Kilofeet -#define EMC_O2_PERCENT 144 // 20 bytes, 10 x 2 bytes, /256=% -#define EMC_HE_PERCENT 164 // 20 bytes, 10 x 2 bytes, /256=% -#define EMC_END_OFFSET 256 // 4 bytes -#define EMC_MIN_TEMP 293 // 1 byte, F -#define EMC_BT 304 // 2 bytes, minutes -#define EMC_MAX_DEPTH 306 // 2 bytes, /4=ft -#define EMC_AVG_DEPTH 310 // 2 bytes, /4=ft diff --git a/color.h b/color.h deleted file mode 100644 index 7938e59a6..000000000 --- a/color.h +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef COLOR_H -#define COLOR_H - -/* The colors are named by picking the closest match - from http://chir.ag/projects/name-that-color */ - -#include - -// Greens -#define CAMARONE1 QColor::fromRgbF(0.0, 0.4, 0.0, 1) -#define FUNGREEN1 QColor::fromRgbF(0.0, 0.4, 0.2, 1) -#define FUNGREEN1_HIGH_TRANS QColor::fromRgbF(0.0, 0.4, 0.2, 0.25) -#define KILLARNEY1 QColor::fromRgbF(0.2, 0.4, 0.2, 1) -#define APPLE1 QColor::fromRgbF(0.2, 0.6, 0.2, 1) -#define APPLE1_MED_TRANS QColor::fromRgbF(0.2, 0.6, 0.2, 0.5) -#define APPLE1_HIGH_TRANS QColor::fromRgbF(0.2, 0.6, 0.2, 0.25) -#define LIMENADE1 QColor::fromRgbF(0.4, 0.8, 0.0, 1) -#define ATLANTIS1 QColor::fromRgbF(0.4, 0.8, 0.2, 1) -#define ATLANTIS2 QColor::fromRgbF(0.6, 0.8, 0.2, 1) -#define RIOGRANDE1 QColor::fromRgbF(0.8, 0.8, 0.0, 1) -#define EARLSGREEN1 QColor::fromRgbF(0.8, 0.8, 0.2, 1) -#define FORESTGREEN1 QColor::fromRgbF(0.1, 0.5, 0.1, 1) -#define NITROX_GREEN QColor::fromRgbF(0, 0.54, 0.375, 1) - -// Reds -#define PERSIANRED1 QColor::fromRgbF(0.8, 0.2, 0.2, 1) -#define TUSCANY1 QColor::fromRgbF(0.8, 0.4, 0.2, 1) -#define PIRATEGOLD1 QColor::fromRgbF(0.8, 0.5, 0.0, 1) -#define HOKEYPOKEY1 QColor::fromRgbF(0.8, 0.6, 0.2, 1) -#define CINNABAR1 QColor::fromRgbF(0.9, 0.3, 0.2, 1) -#define REDORANGE1 QColor::fromRgbF(1.0, 0.2, 0.2, 1) -#define REDORANGE1_HIGH_TRANS QColor::fromRgbF(1.0, 0.2, 0.2, 0.25) -#define REDORANGE1_MED_TRANS QColor::fromRgbF(1.0, 0.2, 0.2, 0.5) -#define RED1_MED_TRANS QColor::fromRgbF(1.0, 0.0, 0.0, 0.5) -#define RED1 QColor::fromRgbF(1.0, 0.0, 0.0, 1) - -// Monochromes -#define BLACK1 QColor::fromRgbF(0.0, 0.0, 0.0, 1) -#define BLACK1_LOW_TRANS QColor::fromRgbF(0.0, 0.0, 0.0, 0.75) -#define BLACK1_HIGH_TRANS QColor::fromRgbF(0.0, 0.0, 0.0, 0.25) -#define TUNDORA1_MED_TRANS QColor::fromRgbF(0.3, 0.3, 0.3, 0.5) -#define MED_GRAY_HIGH_TRANS QColor::fromRgbF(0.5, 0.5, 0.5, 0.25) -#define MERCURY1_MED_TRANS QColor::fromRgbF(0.9, 0.9, 0.9, 0.5) -#define CONCRETE1_LOWER_TRANS QColor::fromRgbF(0.95, 0.95, 0.95, 0.9) -#define WHITE1_MED_TRANS QColor::fromRgbF(1.0, 1.0, 1.0, 0.5) -#define WHITE1 QColor::fromRgbF(1.0, 1.0, 1.0, 1) - -// Blues -#define GOVERNORBAY2 QColor::fromRgbF(0.2, 0.2, 0.7, 1) -#define GOVERNORBAY1_MED_TRANS QColor::fromRgbF(0.2, 0.2, 0.8, 0.5) -#define ROYALBLUE2 QColor::fromRgbF(0.2, 0.2, 0.9, 1) -#define ROYALBLUE2_LOW_TRANS QColor::fromRgbF(0.2, 0.2, 0.9, 0.75) -#define AIR_BLUE QColor::fromRgbF(0.25, 0.75, 1.0, 1) -#define AIR_BLUE_TRANS QColor::fromRgbF(0.25, 0.75, 1.0, 0.5) - -// Yellows / BROWNS -#define SPRINGWOOD1 QColor::fromRgbF(0.95, 0.95, 0.9, 1) -#define SPRINGWOOD1_MED_TRANS QColor::fromRgbF(0.95, 0.95, 0.9, 0.5) -#define BROOM1_LOWER_TRANS QColor::fromRgbF(1.0, 1.0, 0.1, 0.9) -#define PEANUT QColor::fromRgbF(0.5, 0.2, 0.1, 1.0) -#define PEANUT_MED_TRANS QColor::fromRgbF(0.5, 0.2, 0.1, 0.5) -#define NITROX_YELLOW QColor::fromRgbF(0.98, 0.89, 0.07, 1.0) - -// Magentas -#define MEDIUMREDVIOLET1_HIGHER_TRANS QColor::fromRgbF(0.7, 0.2, 0.7, 0.1) - -#endif // COLOR_H diff --git a/configuredivecomputer.cpp b/configuredivecomputer.cpp deleted file mode 100644 index 2457ffe82..000000000 --- a/configuredivecomputer.cpp +++ /dev/null @@ -1,681 +0,0 @@ -#include "configuredivecomputer.h" -#include "libdivecomputer/hw.h" -#include -#include -#include -#include -#include -#include -#include -#include - -ConfigureDiveComputer::ConfigureDiveComputer() : readThread(0), - writeThread(0), - resetThread(0), - firmwareThread(0) -{ - setState(INITIAL); -} - -void ConfigureDiveComputer::readSettings(device_data_t *data) -{ - setState(READING); - - if (readThread) - readThread->deleteLater(); - - readThread = new ReadSettingsThread(this, data); - connect(readThread, SIGNAL(finished()), - this, SLOT(readThreadFinished()), Qt::QueuedConnection); - connect(readThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); - connect(readThread, SIGNAL(devicedetails(DeviceDetails *)), this, - SIGNAL(deviceDetailsChanged(DeviceDetails *))); - connect(readThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); - - readThread->start(); -} - -void ConfigureDiveComputer::saveDeviceDetails(DeviceDetails *details, device_data_t *data) -{ - setState(WRITING); - - if (writeThread) - writeThread->deleteLater(); - - writeThread = new WriteSettingsThread(this, data); - connect(writeThread, SIGNAL(finished()), - this, SLOT(writeThreadFinished()), Qt::QueuedConnection); - connect(writeThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); - connect(writeThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); - - writeThread->setDeviceDetails(details); - writeThread->start(); -} - -bool ConfigureDiveComputer::saveXMLBackup(QString fileName, DeviceDetails *details, device_data_t *data) -{ - QString xml = ""; - QString vendor = data->vendor; - QString product = data->product; - QXmlStreamWriter writer(&xml); - writer.setAutoFormatting(true); - - writer.writeStartDocument(); - writer.writeStartElement("DiveComputerSettingsBackup"); - writer.writeStartElement("DiveComputer"); - writer.writeTextElement("Vendor", vendor); - writer.writeTextElement("Product", product); - writer.writeEndElement(); - writer.writeStartElement("Settings"); - writer.writeTextElement("CustomText", details->customText); - //Add gasses - QString gas1 = QString("%1,%2,%3,%4") - .arg(QString::number(details->gas1.oxygen), - QString::number(details->gas1.helium), - QString::number(details->gas1.type), - QString::number(details->gas1.depth)); - QString gas2 = QString("%1,%2,%3,%4") - .arg(QString::number(details->gas2.oxygen), - QString::number(details->gas2.helium), - QString::number(details->gas2.type), - QString::number(details->gas2.depth)); - QString gas3 = QString("%1,%2,%3,%4") - .arg(QString::number(details->gas3.oxygen), - QString::number(details->gas3.helium), - QString::number(details->gas3.type), - QString::number(details->gas3.depth)); - QString gas4 = QString("%1,%2,%3,%4") - .arg(QString::number(details->gas4.oxygen), - QString::number(details->gas4.helium), - QString::number(details->gas4.type), - QString::number(details->gas4.depth)); - QString gas5 = QString("%1,%2,%3,%4") - .arg(QString::number(details->gas5.oxygen), - QString::number(details->gas5.helium), - QString::number(details->gas5.type), - QString::number(details->gas5.depth)); - writer.writeTextElement("Gas1", gas1); - writer.writeTextElement("Gas2", gas2); - writer.writeTextElement("Gas3", gas3); - writer.writeTextElement("Gas4", gas4); - writer.writeTextElement("Gas5", gas5); - // - //Add dil values - QString dil1 = QString("%1,%2,%3,%4") - .arg(QString::number(details->dil1.oxygen), - QString::number(details->dil1.helium), - QString::number(details->dil1.type), - QString::number(details->dil1.depth)); - QString dil2 = QString("%1,%2,%3,%4") - .arg(QString::number(details->dil2.oxygen), - QString::number(details->dil2.helium), - QString::number(details->dil2.type), - QString::number(details->dil2.depth)); - QString dil3 = QString("%1,%2,%3,%4") - .arg(QString::number(details->dil3.oxygen), - QString::number(details->dil3.helium), - QString::number(details->dil3.type), - QString::number(details->dil3.depth)); - QString dil4 = QString("%1,%2,%3,%4") - .arg(QString::number(details->dil4.oxygen), - QString::number(details->dil4.helium), - QString::number(details->dil4.type), - QString::number(details->dil4.depth)); - QString dil5 = QString("%1,%2,%3,%4") - .arg(QString::number(details->dil5.oxygen), - QString::number(details->dil5.helium), - QString::number(details->dil5.type), - QString::number(details->dil5.depth)); - writer.writeTextElement("Dil1", dil1); - writer.writeTextElement("Dil2", dil2); - writer.writeTextElement("Dil3", dil3); - writer.writeTextElement("Dil4", dil4); - writer.writeTextElement("Dil5", dil5); - // - //Add set point values - QString sp1 = QString("%1,%2") - .arg(QString::number(details->sp1.sp), - QString::number(details->sp1.depth)); - QString sp2 = QString("%1,%2") - .arg(QString::number(details->sp2.sp), - QString::number(details->sp2.depth)); - QString sp3 = QString("%1,%2") - .arg(QString::number(details->sp3.sp), - QString::number(details->sp3.depth)); - QString sp4 = QString("%1,%2") - .arg(QString::number(details->sp4.sp), - QString::number(details->sp4.depth)); - QString sp5 = QString("%1,%2") - .arg(QString::number(details->sp5.sp), - QString::number(details->sp5.depth)); - writer.writeTextElement("SetPoint1", sp1); - writer.writeTextElement("SetPoint2", sp2); - writer.writeTextElement("SetPoint3", sp3); - writer.writeTextElement("SetPoint4", sp4); - writer.writeTextElement("SetPoint5", sp5); - - //Other Settings - writer.writeTextElement("DiveMode", QString::number(details->diveMode)); - writer.writeTextElement("Saturation", QString::number(details->saturation)); - writer.writeTextElement("Desaturation", QString::number(details->desaturation)); - writer.writeTextElement("LastDeco", QString::number(details->lastDeco)); - writer.writeTextElement("Brightness", QString::number(details->brightness)); - writer.writeTextElement("Units", QString::number(details->units)); - writer.writeTextElement("SamplingRate", QString::number(details->samplingRate)); - writer.writeTextElement("Salinity", QString::number(details->salinity)); - writer.writeTextElement("DiveModeColor", QString::number(details->diveModeColor)); - writer.writeTextElement("Language", QString::number(details->language)); - writer.writeTextElement("DateFormat", QString::number(details->dateFormat)); - writer.writeTextElement("CompassGain", QString::number(details->compassGain)); - writer.writeTextElement("SafetyStop", QString::number(details->safetyStop)); - writer.writeTextElement("GfHigh", QString::number(details->gfHigh)); - writer.writeTextElement("GfLow", QString::number(details->gfLow)); - writer.writeTextElement("PressureSensorOffset", QString::number(details->pressureSensorOffset)); - writer.writeTextElement("PpO2Min", QString::number(details->ppO2Min)); - writer.writeTextElement("PpO2Max", QString::number(details->ppO2Max)); - writer.writeTextElement("FutureTTS", QString::number(details->futureTTS)); - writer.writeTextElement("CcrMode", QString::number(details->ccrMode)); - writer.writeTextElement("DecoType", QString::number(details->decoType)); - writer.writeTextElement("AGFSelectable", QString::number(details->aGFSelectable)); - writer.writeTextElement("AGFHigh", QString::number(details->aGFHigh)); - writer.writeTextElement("AGFLow", QString::number(details->aGFLow)); - writer.writeTextElement("CalibrationGas", QString::number(details->calibrationGas)); - writer.writeTextElement("FlipScreen", QString::number(details->flipScreen)); - writer.writeTextElement("SetPointFallback", QString::number(details->setPointFallback)); - writer.writeTextElement("LeftButtonSensitivity", QString::number(details->leftButtonSensitivity)); - writer.writeTextElement("RightButtonSensitivity", QString::number(details->rightButtonSensitivity)); - writer.writeTextElement("BottomGasConsumption", QString::number(details->bottomGasConsumption)); - writer.writeTextElement("DecoGasConsumption", QString::number(details->decoGasConsumption)); - writer.writeTextElement("ModWarning", QString::number(details->modWarning)); - writer.writeTextElement("DynamicAscendRate", QString::number(details->dynamicAscendRate)); - writer.writeTextElement("GraphicalSpeedIndicator", QString::number(details->graphicalSpeedIndicator)); - writer.writeTextElement("AlwaysShowppO2", QString::number(details->alwaysShowppO2)); - - // Suunto vyper settings. - writer.writeTextElement("Altitude", QString::number(details->altitude)); - writer.writeTextElement("PersonalSafety", QString::number(details->personalSafety)); - writer.writeTextElement("TimeFormat", QString::number(details->timeFormat)); - - writer.writeStartElement("Light"); - writer.writeAttribute("enabled", QString::number(details->lightEnabled)); - writer.writeCharacters(QString::number(details->light)); - writer.writeEndElement(); - - writer.writeStartElement("AlarmTime"); - writer.writeAttribute("enabled", QString::number(details->alarmTimeEnabled)); - writer.writeCharacters(QString::number(details->alarmTime)); - writer.writeEndElement(); - - writer.writeStartElement("AlarmDepth"); - writer.writeAttribute("enabled", QString::number(details->alarmDepthEnabled)); - writer.writeCharacters(QString::number(details->alarmDepth)); - writer.writeEndElement(); - - writer.writeEndElement(); - writer.writeEndElement(); - - writer.writeEndDocument(); - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) { - lastError = tr("Could not save the backup file %1. Error Message: %2") - .arg(fileName, file.errorString()); - return false; - } - //file open successful. write data and save. - QTextStream out(&file); - out << xml; - - file.close(); - return true; -} - -bool ConfigureDiveComputer::restoreXMLBackup(QString fileName, DeviceDetails *details) -{ - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - lastError = tr("Could not open backup file: %1").arg(file.errorString()); - return false; - } - - QString xml = file.readAll(); - - QXmlStreamReader reader(xml); - while (!reader.atEnd()) { - if (reader.isStartElement()) { - QString settingName = reader.name().toString(); - QXmlStreamAttributes attributes = reader.attributes(); - reader.readNext(); - QString keyString = reader.text().toString(); - - if (settingName == "CustomText") - details->customText = keyString; - - if (settingName == "Gas1") { - QStringList gasData = keyString.split(","); - gas gas1; - gas1.oxygen = gasData.at(0).toInt(); - gas1.helium = gasData.at(1).toInt(); - gas1.type = gasData.at(2).toInt(); - gas1.depth = gasData.at(3).toInt(); - details->gas1 = gas1; - } - - if (settingName == "Gas2") { - QStringList gasData = keyString.split(","); - gas gas2; - gas2.oxygen = gasData.at(0).toInt(); - gas2.helium = gasData.at(1).toInt(); - gas2.type = gasData.at(2).toInt(); - gas2.depth = gasData.at(3).toInt(); - details->gas2 = gas2; - } - - if (settingName == "Gas3") { - QStringList gasData = keyString.split(","); - gas gas3; - gas3.oxygen = gasData.at(0).toInt(); - gas3.helium = gasData.at(1).toInt(); - gas3.type = gasData.at(2).toInt(); - gas3.depth = gasData.at(3).toInt(); - details->gas3 = gas3; - } - - if (settingName == "Gas4") { - QStringList gasData = keyString.split(","); - gas gas4; - gas4.oxygen = gasData.at(0).toInt(); - gas4.helium = gasData.at(1).toInt(); - gas4.type = gasData.at(2).toInt(); - gas4.depth = gasData.at(3).toInt(); - details->gas4 = gas4; - } - - if (settingName == "Gas5") { - QStringList gasData = keyString.split(","); - gas gas5; - gas5.oxygen = gasData.at(0).toInt(); - gas5.helium = gasData.at(1).toInt(); - gas5.type = gasData.at(2).toInt(); - gas5.depth = gasData.at(3).toInt(); - details->gas5 = gas5; - } - - if (settingName == "Dil1") { - QStringList dilData = keyString.split(","); - gas dil1; - dil1.oxygen = dilData.at(0).toInt(); - dil1.helium = dilData.at(1).toInt(); - dil1.type = dilData.at(2).toInt(); - dil1.depth = dilData.at(3).toInt(); - details->dil1 = dil1; - } - - if (settingName == "Dil2") { - QStringList dilData = keyString.split(","); - gas dil2; - dil2.oxygen = dilData.at(0).toInt(); - dil2.helium = dilData.at(1).toInt(); - dil2.type = dilData.at(2).toInt(); - dil2.depth = dilData.at(3).toInt(); - details->dil1 = dil2; - } - - if (settingName == "Dil3") { - QStringList dilData = keyString.split(","); - gas dil3; - dil3.oxygen = dilData.at(0).toInt(); - dil3.helium = dilData.at(1).toInt(); - dil3.type = dilData.at(2).toInt(); - dil3.depth = dilData.at(3).toInt(); - details->dil3 = dil3; - } - - if (settingName == "Dil4") { - QStringList dilData = keyString.split(","); - gas dil4; - dil4.oxygen = dilData.at(0).toInt(); - dil4.helium = dilData.at(1).toInt(); - dil4.type = dilData.at(2).toInt(); - dil4.depth = dilData.at(3).toInt(); - details->dil4 = dil4; - } - - if (settingName == "Dil5") { - QStringList dilData = keyString.split(","); - gas dil5; - dil5.oxygen = dilData.at(0).toInt(); - dil5.helium = dilData.at(1).toInt(); - dil5.type = dilData.at(2).toInt(); - dil5.depth = dilData.at(3).toInt(); - details->dil5 = dil5; - } - - if (settingName == "SetPoint1") { - QStringList spData = keyString.split(","); - setpoint sp1; - sp1.sp = spData.at(0).toInt(); - sp1.depth = spData.at(1).toInt(); - details->sp1 = sp1; - } - - if (settingName == "SetPoint2") { - QStringList spData = keyString.split(","); - setpoint sp2; - sp2.sp = spData.at(0).toInt(); - sp2.depth = spData.at(1).toInt(); - details->sp2 = sp2; - } - - if (settingName == "SetPoint3") { - QStringList spData = keyString.split(","); - setpoint sp3; - sp3.sp = spData.at(0).toInt(); - sp3.depth = spData.at(1).toInt(); - details->sp3 = sp3; - } - - if (settingName == "SetPoint4") { - QStringList spData = keyString.split(","); - setpoint sp4; - sp4.sp = spData.at(0).toInt(); - sp4.depth = spData.at(1).toInt(); - details->sp4 = sp4; - } - - if (settingName == "SetPoint5") { - QStringList spData = keyString.split(","); - setpoint sp5; - sp5.sp = spData.at(0).toInt(); - sp5.depth = spData.at(1).toInt(); - details->sp5 = sp5; - } - - if (settingName == "Saturation") - details->saturation = keyString.toInt(); - - if (settingName == "Desaturation") - details->desaturation = keyString.toInt(); - - if (settingName == "DiveMode") - details->diveMode = keyString.toInt(); - - if (settingName == "LastDeco") - details->lastDeco = keyString.toInt(); - - if (settingName == "Brightness") - details->brightness = keyString.toInt(); - - if (settingName == "Units") - details->units = keyString.toInt(); - - if (settingName == "SamplingRate") - details->samplingRate = keyString.toInt(); - - if (settingName == "Salinity") - details->salinity = keyString.toInt(); - - if (settingName == "DiveModeColour") - details->diveModeColor = keyString.toInt(); - - if (settingName == "Language") - details->language = keyString.toInt(); - - if (settingName == "DateFormat") - details->dateFormat = keyString.toInt(); - - if (settingName == "CompassGain") - details->compassGain = keyString.toInt(); - - if (settingName == "SafetyStop") - details->safetyStop = keyString.toInt(); - - if (settingName == "GfHigh") - details->gfHigh = keyString.toInt(); - - if (settingName == "GfLow") - details->gfLow = keyString.toInt(); - - if (settingName == "PressureSensorOffset") - details->pressureSensorOffset = keyString.toInt(); - - if (settingName == "PpO2Min") - details->ppO2Min = keyString.toInt(); - - if (settingName == "PpO2Max") - details->ppO2Max = keyString.toInt(); - - if (settingName == "FutureTTS") - details->futureTTS = keyString.toInt(); - - if (settingName == "CcrMode") - details->ccrMode = keyString.toInt(); - - if (settingName == "DecoType") - details->decoType = keyString.toInt(); - - if (settingName == "AGFSelectable") - details->aGFSelectable = keyString.toInt(); - - if (settingName == "AGFHigh") - details->aGFHigh = keyString.toInt(); - - if (settingName == "AGFLow") - details->aGFLow = keyString.toInt(); - - if (settingName == "CalibrationGas") - details->calibrationGas = keyString.toInt(); - - if (settingName == "FlipScreen") - details->flipScreen = keyString.toInt(); - - if (settingName == "SetPointFallback") - details->setPointFallback = keyString.toInt(); - - if (settingName == "LeftButtonSensitivity") - details->leftButtonSensitivity = keyString.toInt(); - - if (settingName == "RightButtonSensitivity") - details->rightButtonSensitivity = keyString.toInt(); - - if (settingName == "BottomGasConsumption") - details->bottomGasConsumption = keyString.toInt(); - - if (settingName == "DecoGasConsumption") - details->decoGasConsumption = keyString.toInt(); - - if (settingName == "ModWarning") - details->modWarning = keyString.toInt(); - - if (settingName == "DynamicAscendRate") - details->dynamicAscendRate = keyString.toInt(); - - if (settingName == "GraphicalSpeedIndicator") - details->graphicalSpeedIndicator = keyString.toInt(); - - if (settingName == "AlwaysShowppO2") - details->alwaysShowppO2 = keyString.toInt(); - - if (settingName == "Altitude") - details->altitude = keyString.toInt(); - - if (settingName == "PersonalSafety") - details->personalSafety = keyString.toInt(); - - if (settingName == "TimeFormat") - details->timeFormat = keyString.toInt(); - - if (settingName == "Light") { - if (attributes.hasAttribute("enabled")) - details->lightEnabled = attributes.value("enabled").toString().toInt(); - details->light = keyString.toInt(); - } - - if (settingName == "AlarmDepth") { - if (attributes.hasAttribute("enabled")) - details->alarmDepthEnabled = attributes.value("enabled").toString().toInt(); - details->alarmDepth = keyString.toInt(); - } - - if (settingName == "AlarmTime") { - if (attributes.hasAttribute("enabled")) - details->alarmTimeEnabled = attributes.value("enabled").toString().toInt(); - details->alarmTime = keyString.toInt(); - } - } - reader.readNext(); - } - - return true; -} - -void ConfigureDiveComputer::startFirmwareUpdate(QString fileName, device_data_t *data) -{ - setState(FWUPDATE); - if (firmwareThread) - firmwareThread->deleteLater(); - - firmwareThread = new FirmwareUpdateThread(this, data, fileName); - connect(firmwareThread, SIGNAL(finished()), - this, SLOT(firmwareThreadFinished()), Qt::QueuedConnection); - connect(firmwareThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); - connect(firmwareThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); - - firmwareThread->start(); -} - -void ConfigureDiveComputer::resetSettings(device_data_t *data) -{ - setState(RESETTING); - - if (resetThread) - resetThread->deleteLater(); - - resetThread = new ResetSettingsThread(this, data); - connect(resetThread, SIGNAL(finished()), - this, SLOT(resetThreadFinished()), Qt::QueuedConnection); - connect(resetThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); - connect(resetThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); - - resetThread->start(); -} - -void ConfigureDiveComputer::progressEvent(int percent) -{ - emit progress(percent); -} - -void ConfigureDiveComputer::setState(ConfigureDiveComputer::states newState) -{ - currentState = newState; - emit stateChanged(currentState); -} - -void ConfigureDiveComputer::setError(QString err) -{ - lastError = err; - emit error(err); -} - -void ConfigureDiveComputer::readThreadFinished() -{ - setState(DONE); - if (lastError.isEmpty()) { - //No error - emit message(tr("Dive computer details read successfully")); - } -} - -void ConfigureDiveComputer::writeThreadFinished() -{ - setState(DONE); - if (lastError.isEmpty()) { - //No error - emit message(tr("Setting successfully written to device")); - } -} - -void ConfigureDiveComputer::firmwareThreadFinished() -{ - setState(DONE); - if (lastError.isEmpty()) { - //No error - emit message(tr("Device firmware successfully updated")); - } -} - -void ConfigureDiveComputer::resetThreadFinished() -{ - setState(DONE); - if (lastError.isEmpty()) { - //No error - emit message(tr("Device settings successfully reset")); - } -} - -QString ConfigureDiveComputer::dc_open(device_data_t *data) -{ - FILE *fp = NULL; - dc_status_t rc; - - if (data->libdc_log) - fp = subsurface_fopen(logfile_name, "w"); - - data->libdc_logfile = fp; - - rc = dc_context_new(&data->context); - if (rc != DC_STATUS_SUCCESS) { - return tr("Unable to create libdivecomputer context"); - } - - if (fp) { - dc_context_set_loglevel(data->context, DC_LOGLEVEL_ALL); - dc_context_set_logfunc(data->context, logfunc, fp); - } - -#if defined(SSRF_CUSTOM_SERIAL) - dc_serial_t *serial_device = NULL; - - if (data->bluetooth_mode) { -#ifdef BT_SUPPORT - rc = dc_serial_qt_open(&serial_device, data->context, data->devname); -#endif -#ifdef SERIAL_FTDI - } else if (!strcmp(data->devname, "ftdi")) { - rc = dc_serial_ftdi_open(&serial_device, data->context); -#endif - } - - if (rc != DC_STATUS_SUCCESS) { - return errmsg(rc); - } else if (serial_device) { - rc = dc_device_custom_open(&data->device, data->context, data->descriptor, serial_device); - } else { -#else - { -#endif - rc = dc_device_open(&data->device, data->context, data->descriptor, data->devname); - } - - if (rc != DC_STATUS_SUCCESS) { - return tr("Could not a establish connection to the dive computer."); - } - - setState(OPEN); - - return NULL; -} - -void ConfigureDiveComputer::dc_close(device_data_t *data) -{ - if (data->device) - dc_device_close(data->device); - data->device = NULL; - if (data->context) - dc_context_free(data->context); - data->context = NULL; - - if (data->libdc_logfile) - fclose(data->libdc_logfile); - - setState(INITIAL); -} diff --git a/configuredivecomputer.h b/configuredivecomputer.h deleted file mode 100644 index f14eeeca3..000000000 --- a/configuredivecomputer.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef CONFIGUREDIVECOMPUTER_H -#define CONFIGUREDIVECOMPUTER_H - -#include -#include -#include -#include "libdivecomputer.h" -#include "configuredivecomputerthreads.h" -#include - -#include "libxml/xmlreader.h" - -class ConfigureDiveComputer : public QObject { - Q_OBJECT -public: - explicit ConfigureDiveComputer(); - void readSettings(device_data_t *data); - - enum states { - OPEN, - INITIAL, - READING, - WRITING, - RESETTING, - FWUPDATE, - CANCELLING, - CANCELLED, - ERROR, - DONE, - }; - - QString lastError; - states currentState; - void saveDeviceDetails(DeviceDetails *details, device_data_t *data); - void fetchDeviceDetails(); - bool saveXMLBackup(QString fileName, DeviceDetails *details, device_data_t *data); - bool restoreXMLBackup(QString fileName, DeviceDetails *details); - void startFirmwareUpdate(QString fileName, device_data_t *data); - void resetSettings(device_data_t *data); - - QString dc_open(device_data_t *data); -public -slots: - void dc_close(device_data_t *data); -signals: - void progress(int percent); - void message(QString msg); - void error(QString err); - void stateChanged(states newState); - void deviceDetailsChanged(DeviceDetails *newDetails); - -private: - ReadSettingsThread *readThread; - WriteSettingsThread *writeThread; - ResetSettingsThread *resetThread; - FirmwareUpdateThread *firmwareThread; - void setState(states newState); -private -slots: - void progressEvent(int percent); - void readThreadFinished(); - void writeThreadFinished(); - void resetThreadFinished(); - void firmwareThreadFinished(); - void setError(QString err); -}; - -#endif // CONFIGUREDIVECOMPUTER_H diff --git a/configuredivecomputerthreads.cpp b/configuredivecomputerthreads.cpp deleted file mode 100644 index 78edcb37c..000000000 --- a/configuredivecomputerthreads.cpp +++ /dev/null @@ -1,1760 +0,0 @@ -#include "configuredivecomputerthreads.h" -#include "libdivecomputer/hw.h" -#include "libdivecomputer.h" -#include -#include - -#define OSTC3_GAS1 0x10 -#define OSTC3_GAS2 0x11 -#define OSTC3_GAS3 0x12 -#define OSTC3_GAS4 0x13 -#define OSTC3_GAS5 0x14 -#define OSTC3_DIL1 0x15 -#define OSTC3_DIL2 0x16 -#define OSTC3_DIL3 0x17 -#define OSTC3_DIL4 0x18 -#define OSTC3_DIL5 0x19 -#define OSTC3_SP1 0x1A -#define OSTC3_SP2 0x1B -#define OSTC3_SP3 0x1C -#define OSTC3_SP4 0x1D -#define OSTC3_SP5 0x1E -#define OSTC3_CCR_MODE 0x1F -#define OSTC3_DIVE_MODE 0x20 -#define OSTC3_DECO_TYPE 0x21 -#define OSTC3_PPO2_MAX 0x22 -#define OSTC3_PPO2_MIN 0x23 -#define OSTC3_FUTURE_TTS 0x24 -#define OSTC3_GF_LOW 0x25 -#define OSTC3_GF_HIGH 0x26 -#define OSTC3_AGF_LOW 0x27 -#define OSTC3_AGF_HIGH 0x28 -#define OSTC3_AGF_SELECTABLE 0x29 -#define OSTC3_SATURATION 0x2A -#define OSTC3_DESATURATION 0x2B -#define OSTC3_LAST_DECO 0x2C -#define OSTC3_BRIGHTNESS 0x2D -#define OSTC3_UNITS 0x2E -#define OSTC3_SAMPLING_RATE 0x2F -#define OSTC3_SALINITY 0x30 -#define OSTC3_DIVEMODE_COLOR 0x31 -#define OSTC3_LANGUAGE 0x32 -#define OSTC3_DATE_FORMAT 0x33 -#define OSTC3_COMPASS_GAIN 0x34 -#define OSTC3_PRESSURE_SENSOR_OFFSET 0x35 -#define OSTC3_SAFETY_STOP 0x36 -#define OSTC3_CALIBRATION_GAS_O2 0x37 -#define OSTC3_SETPOINT_FALLBACK 0x38 -#define OSTC3_FLIP_SCREEN 0x39 -#define OSTC3_LEFT_BUTTON_SENSIVITY 0x3A -#define OSTC3_RIGHT_BUTTON_SENSIVITY 0x3A -#define OSTC3_BOTTOM_GAS_CONSUMPTION 0x3C -#define OSTC3_DECO_GAS_CONSUMPTION 0x3D -#define OSTC3_MOD_WARNING 0x3E -#define OSTC3_DYNAMIC_ASCEND_RATE 0x3F -#define OSTC3_GRAPHICAL_SPEED_INDICATOR 0x40 -#define OSTC3_ALWAYS_SHOW_PPO2 0x41 - -#define OSTC3_HW_OSTC_3 0x0A -#define OSTC3_HW_OSTC_3P 0x1A -#define OSTC3_HW_OSTC_CR 0x05 -#define OSTC3_HW_OSTC_SPORT 0x12 -#define OSTC3_HW_OSTC_2 0x11 - - -#define SUUNTO_VYPER_MAXDEPTH 0x1e -#define SUUNTO_VYPER_TOTAL_TIME 0x20 -#define SUUNTO_VYPER_NUMBEROFDIVES 0x22 -#define SUUNTO_VYPER_COMPUTER_TYPE 0x24 -#define SUUNTO_VYPER_FIRMWARE 0x25 -#define SUUNTO_VYPER_SERIALNUMBER 0x26 -#define SUUNTO_VYPER_CUSTOM_TEXT 0x2c -#define SUUNTO_VYPER_SAMPLING_RATE 0x53 -#define SUUNTO_VYPER_ALTITUDE_SAFETY 0x54 -#define SUUNTO_VYPER_TIMEFORMAT 0x60 -#define SUUNTO_VYPER_UNITS 0x62 -#define SUUNTO_VYPER_MODEL 0x63 -#define SUUNTO_VYPER_LIGHT 0x64 -#define SUUNTO_VYPER_ALARM_DEPTH_TIME 0x65 -#define SUUNTO_VYPER_ALARM_TIME 0x66 -#define SUUNTO_VYPER_ALARM_DEPTH 0x68 -#define SUUNTO_VYPER_CUSTOM_TEXT_LENGHT 30 - -#ifdef DEBUG_OSTC -// Fake io to ostc memory banks -#define hw_ostc_device_eeprom_read local_hw_ostc_device_eeprom_read -#define hw_ostc_device_eeprom_write local_hw_ostc_device_eeprom_write -#define hw_ostc_device_clock local_hw_ostc_device_clock -#define OSTC_FILE "../OSTC-data-dump.bin" - -// Fake the open function. -static dc_status_t local_dc_device_open(dc_device_t **out, dc_context_t *context, dc_descriptor_t *descriptor, const char *name) -{ - if (strcmp(dc_descriptor_get_vendor(descriptor), "Heinrichs Weikamp") == 0 &&strcmp(dc_descriptor_get_product(descriptor), "OSTC 2N") == 0) - return DC_STATUS_SUCCESS; - else - return dc_device_open(out, context, descriptor, name); -} -#define dc_device_open local_dc_device_open - -// Fake the custom open function -static dc_status_t local_dc_device_custom_open(dc_device_t **out, dc_context_t *context, dc_descriptor_t *descriptor, dc_serial_t *serial) -{ - if (strcmp(dc_descriptor_get_vendor(descriptor), "Heinrichs Weikamp") == 0 &&strcmp(dc_descriptor_get_product(descriptor), "OSTC 2N") == 0) - return DC_STATUS_SUCCESS; - else - return dc_device_custom_open(out, context, descriptor, serial); -} -#define dc_device_custom_open local_dc_device_custom_open - -static dc_status_t local_hw_ostc_device_eeprom_read(void *ignored, unsigned char bank, unsigned char data[], unsigned int data_size) -{ - FILE *f; - if ((f = fopen(OSTC_FILE, "r")) == NULL) - return DC_STATUS_NODEVICE; - fseek(f, bank * 256, SEEK_SET); - if (fread(data, sizeof(unsigned char), data_size, f) != data_size) { - fclose(f); - return DC_STATUS_IO; - } - fclose(f); - - return DC_STATUS_SUCCESS; -} - -static dc_status_t local_hw_ostc_device_eeprom_write(void *ignored, unsigned char bank, unsigned char data[], unsigned int data_size) -{ - FILE *f; - if ((f = fopen(OSTC_FILE, "r+")) == NULL) - f = fopen(OSTC_FILE, "w"); - fseek(f, bank * 256, SEEK_SET); - fwrite(data, sizeof(unsigned char), data_size, f); - fclose(f); - - return DC_STATUS_SUCCESS; -} - -static dc_status_t local_hw_ostc_device_clock(void *ignored, dc_datetime_t *time) -{ - return DC_STATUS_SUCCESS; -} -#endif - -static int read_ostc_cf(unsigned char data[], unsigned char cf) -{ - return data[128 + (cf % 32) * 4 + 3] << 8 ^ data[128 + (cf % 32) * 4 + 2]; -} - -static void write_ostc_cf(unsigned char data[], unsigned char cf, unsigned char max_CF, unsigned int value) -{ - // Only write settings supported by this firmware. - if (cf > max_CF) - return; - - data[128 + (cf % 32) * 4 + 3] = (value & 0xff00) >> 8; - data[128 + (cf % 32) * 4 + 2] = (value & 0x00ff); -} - -#define EMIT_PROGRESS() do { \ - progress.current++; \ - progress_cb(device, DC_EVENT_PROGRESS, &progress, userdata); \ - } while (0) - -static dc_status_t read_suunto_vyper_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) -{ - unsigned char data[SUUNTO_VYPER_CUSTOM_TEXT_LENGHT + 1]; - dc_status_t rc; - dc_event_progress_t progress; - progress.current = 0; - progress.maximum = 16; - - rc = dc_device_read(device, SUUNTO_VYPER_COMPUTER_TYPE, data, 1); - if (rc == DC_STATUS_SUCCESS) { - const char *model; - // FIXME: grab this info from libdivecomputer descriptor - // instead of hard coded here - switch (data[0]) { - case 0x03: - model = "Stinger"; - break; - case 0x04: - model = "Mosquito"; - break; - case 0x05: - model = "D3"; - break; - case 0x0A: - model = "Vyper"; - break; - case 0x0B: - model = "Vytec"; - break; - case 0x0C: - model = "Cobra"; - break; - case 0x0D: - model = "Gekko"; - break; - case 0x16: - model = "Zoop"; - break; - case 20: - case 30: - case 60: - // Suunto Spyder have there sample interval at this position - // Fallthrough - default: - return DC_STATUS_UNSUPPORTED; - } - // We found a supported device - // we can safely proceed with reading/writing to this device. - m_deviceDetails->model = model; - } - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_MAXDEPTH, data, 2); - if (rc != DC_STATUS_SUCCESS) - return rc; - // in ft * 128.0 - int depth = feet_to_mm(data[0] << 8 ^ data[1]) / 128; - m_deviceDetails->maxDepth = depth; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_TOTAL_TIME, data, 2); - if (rc != DC_STATUS_SUCCESS) - return rc; - int total_time = data[0] << 8 ^ data[1]; - m_deviceDetails->totalTime = total_time; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_NUMBEROFDIVES, data, 2); - if (rc != DC_STATUS_SUCCESS) - return rc; - int number_of_dives = data[0] << 8 ^ data[1]; - m_deviceDetails->numberOfDives = number_of_dives; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_FIRMWARE, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->firmwareVersion = QString::number(data[0]) + ".0.0"; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_SERIALNUMBER, data, 4); - if (rc != DC_STATUS_SUCCESS) - return rc; - int serial_number = data[0] * 1000000 + data[1] * 10000 + data[2] * 100 + data[3]; - m_deviceDetails->serialNo = QString::number(serial_number); - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_CUSTOM_TEXT, data, SUUNTO_VYPER_CUSTOM_TEXT_LENGHT); - if (rc != DC_STATUS_SUCCESS) - return rc; - data[SUUNTO_VYPER_CUSTOM_TEXT_LENGHT] = 0; - m_deviceDetails->customText = (const char *)data; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_SAMPLING_RATE, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->samplingRate = (int)data[0]; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_ALTITUDE_SAFETY, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->altitude = data[0] & 0x03; - m_deviceDetails->personalSafety = data[0] >> 2 & 0x03; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_TIMEFORMAT, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->timeFormat = data[0] & 0x01; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_UNITS, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->units = data[0] & 0x01; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_MODEL, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->diveMode = data[0] & 0x03; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_LIGHT, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->lightEnabled = data[0] >> 7; - m_deviceDetails->light = data[0] & 0x7F; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_ALARM_DEPTH_TIME, data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - m_deviceDetails->alarmTimeEnabled = data[0] & 0x01; - m_deviceDetails->alarmDepthEnabled = data[0] >> 1 & 0x01; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_ALARM_TIME, data, 2); - if (rc != DC_STATUS_SUCCESS) - return rc; - int time = data[0] << 8 ^ data[1]; - // The stinger stores alarm time in seconds instead of minutes. - if (m_deviceDetails->model == "Stinger") - time /= 60; - m_deviceDetails->alarmTime = time; - EMIT_PROGRESS(); - - rc = dc_device_read(device, SUUNTO_VYPER_ALARM_DEPTH, data, 2); - if (rc != DC_STATUS_SUCCESS) - return rc; - depth = feet_to_mm(data[0] << 8 ^ data[1]) / 128; - m_deviceDetails->alarmDepth = depth; - EMIT_PROGRESS(); - - return DC_STATUS_SUCCESS; -} - -static dc_status_t write_suunto_vyper_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) -{ - dc_status_t rc; - dc_event_progress_t progress; - progress.current = 0; - progress.maximum = 10; - unsigned char data; - unsigned char data2[2]; - int time; - - // Maybee we should read the model from the device to sanity check it here too.. - // For now we just check that we actually read a device before writing to one. - if (m_deviceDetails->model == "") - return DC_STATUS_UNSUPPORTED; - - rc = dc_device_write(device, SUUNTO_VYPER_CUSTOM_TEXT, - // Convert the customText to a 30 char wide padded with " " - (const unsigned char *)QString("%1").arg(m_deviceDetails->customText, -30, QChar(' ')).toUtf8().data(), - SUUNTO_VYPER_CUSTOM_TEXT_LENGHT); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->samplingRate; - rc = dc_device_write(device, SUUNTO_VYPER_SAMPLING_RATE, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->personalSafety << 2 ^ m_deviceDetails->altitude; - rc = dc_device_write(device, SUUNTO_VYPER_ALTITUDE_SAFETY, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->timeFormat; - rc = dc_device_write(device, SUUNTO_VYPER_TIMEFORMAT, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->units; - rc = dc_device_write(device, SUUNTO_VYPER_UNITS, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->diveMode; - rc = dc_device_write(device, SUUNTO_VYPER_MODEL, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->lightEnabled << 7 ^ (m_deviceDetails->light & 0x7F); - rc = dc_device_write(device, SUUNTO_VYPER_LIGHT, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data = m_deviceDetails->alarmDepthEnabled << 1 ^ m_deviceDetails->alarmTimeEnabled; - rc = dc_device_write(device, SUUNTO_VYPER_ALARM_DEPTH_TIME, &data, 1); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - // The stinger stores alarm time in seconds instead of minutes. - time = m_deviceDetails->alarmTime; - if (m_deviceDetails->model == "Stinger") - time *= 60; - data2[0] = time >> 8; - data2[1] = time & 0xFF; - rc = dc_device_write(device, SUUNTO_VYPER_ALARM_TIME, data2, 2); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - data2[0] = (int)(mm_to_feet(m_deviceDetails->alarmDepth) * 128) >> 8; - data2[1] = (int)(mm_to_feet(m_deviceDetails->alarmDepth) * 128) & 0x0FF; - rc = dc_device_write(device, SUUNTO_VYPER_ALARM_DEPTH, data2, 2); - EMIT_PROGRESS(); - return rc; -} - -#if DC_VERSION_CHECK(0, 5, 0) -static dc_status_t read_ostc3_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) -{ - dc_status_t rc; - dc_event_progress_t progress; - progress.current = 0; - progress.maximum = 52; - unsigned char hardware[1]; - - //Read hardware type - rc = hw_ostc3_device_hardware (device, hardware, sizeof (hardware)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - // FIXME: can we grab this info from libdivecomputer descriptor - // instead of hard coded here? - switch(hardware[0]) { - case OSTC3_HW_OSTC_3: - m_deviceDetails->model = "3"; - break; - case OSTC3_HW_OSTC_3P: - m_deviceDetails->model = "3+"; - break; - case OSTC3_HW_OSTC_CR: - m_deviceDetails->model = "CR"; - break; - case OSTC3_HW_OSTC_SPORT: - m_deviceDetails->model = "Sport"; - break; - case OSTC3_HW_OSTC_2: - m_deviceDetails->model = "2"; - break; - } - - //Read gas mixes - gas gas1; - gas gas2; - gas gas3; - gas gas4; - gas gas5; - unsigned char gasData[4] = { 0, 0, 0, 0 }; - - rc = hw_ostc3_device_config_read(device, OSTC3_GAS1, gasData, sizeof(gasData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - gas1.oxygen = gasData[0]; - gas1.helium = gasData[1]; - gas1.type = gasData[2]; - gas1.depth = gasData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_GAS2, gasData, sizeof(gasData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - gas2.oxygen = gasData[0]; - gas2.helium = gasData[1]; - gas2.type = gasData[2]; - gas2.depth = gasData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_GAS3, gasData, sizeof(gasData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - gas3.oxygen = gasData[0]; - gas3.helium = gasData[1]; - gas3.type = gasData[2]; - gas3.depth = gasData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_GAS4, gasData, sizeof(gasData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - gas4.oxygen = gasData[0]; - gas4.helium = gasData[1]; - gas4.type = gasData[2]; - gas4.depth = gasData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_GAS5, gasData, sizeof(gasData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - gas5.oxygen = gasData[0]; - gas5.helium = gasData[1]; - gas5.type = gasData[2]; - gas5.depth = gasData[3]; - EMIT_PROGRESS(); - - m_deviceDetails->gas1 = gas1; - m_deviceDetails->gas2 = gas2; - m_deviceDetails->gas3 = gas3; - m_deviceDetails->gas4 = gas4; - m_deviceDetails->gas5 = gas5; - EMIT_PROGRESS(); - - //Read Dil Values - gas dil1; - gas dil2; - gas dil3; - gas dil4; - gas dil5; - unsigned char dilData[4] = { 0, 0, 0, 0 }; - - rc = hw_ostc3_device_config_read(device, OSTC3_DIL1, dilData, sizeof(dilData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - dil1.oxygen = dilData[0]; - dil1.helium = dilData[1]; - dil1.type = dilData[2]; - dil1.depth = dilData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_DIL2, dilData, sizeof(dilData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - dil2.oxygen = dilData[0]; - dil2.helium = dilData[1]; - dil2.type = dilData[2]; - dil2.depth = dilData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_DIL3, dilData, sizeof(dilData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - dil3.oxygen = dilData[0]; - dil3.helium = dilData[1]; - dil3.type = dilData[2]; - dil3.depth = dilData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_DIL4, dilData, sizeof(dilData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - dil4.oxygen = dilData[0]; - dil4.helium = dilData[1]; - dil4.type = dilData[2]; - dil4.depth = dilData[3]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_DIL5, dilData, sizeof(dilData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - dil5.oxygen = dilData[0]; - dil5.helium = dilData[1]; - dil5.type = dilData[2]; - dil5.depth = dilData[3]; - EMIT_PROGRESS(); - - m_deviceDetails->dil1 = dil1; - m_deviceDetails->dil2 = dil2; - m_deviceDetails->dil3 = dil3; - m_deviceDetails->dil4 = dil4; - m_deviceDetails->dil5 = dil5; - - //Read set point Values - setpoint sp1; - setpoint sp2; - setpoint sp3; - setpoint sp4; - setpoint sp5; - unsigned char spData[2] = { 0, 0 }; - - rc = hw_ostc3_device_config_read(device, OSTC3_SP1, spData, sizeof(spData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - sp1.sp = spData[0]; - sp1.depth = spData[1]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_SP2, spData, sizeof(spData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - sp2.sp = spData[0]; - sp2.depth = spData[1]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_SP3, spData, sizeof(spData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - sp3.sp = spData[0]; - sp3.depth = spData[1]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_SP4, spData, sizeof(spData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - sp4.sp = spData[0]; - sp4.depth = spData[1]; - EMIT_PROGRESS(); - - rc = hw_ostc3_device_config_read(device, OSTC3_SP5, spData, sizeof(spData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - sp5.sp = spData[0]; - sp5.depth = spData[1]; - EMIT_PROGRESS(); - - m_deviceDetails->sp1 = sp1; - m_deviceDetails->sp2 = sp2; - m_deviceDetails->sp3 = sp3; - m_deviceDetails->sp4 = sp4; - m_deviceDetails->sp5 = sp5; - - //Read other settings - unsigned char uData[1] = { 0 }; - -#define READ_SETTING(_OSTC3_SETTING, _DEVICE_DETAIL) \ - do { \ - rc = hw_ostc3_device_config_read(device, _OSTC3_SETTING, uData, sizeof(uData)); \ - if (rc != DC_STATUS_SUCCESS) \ - return rc; \ - m_deviceDetails->_DEVICE_DETAIL = uData[0]; \ - EMIT_PROGRESS(); \ - } while (0) - - READ_SETTING(OSTC3_DIVE_MODE, diveMode); - READ_SETTING(OSTC3_SATURATION, saturation); - READ_SETTING(OSTC3_DESATURATION, desaturation); - READ_SETTING(OSTC3_LAST_DECO, lastDeco); - READ_SETTING(OSTC3_BRIGHTNESS, brightness); - READ_SETTING(OSTC3_UNITS, units); - READ_SETTING(OSTC3_SAMPLING_RATE, samplingRate); - READ_SETTING(OSTC3_SALINITY, salinity); - READ_SETTING(OSTC3_DIVEMODE_COLOR, diveModeColor); - READ_SETTING(OSTC3_LANGUAGE, language); - READ_SETTING(OSTC3_DATE_FORMAT, dateFormat); - READ_SETTING(OSTC3_COMPASS_GAIN, compassGain); - READ_SETTING(OSTC3_SAFETY_STOP, safetyStop); - READ_SETTING(OSTC3_GF_HIGH, gfHigh); - READ_SETTING(OSTC3_GF_LOW, gfLow); - READ_SETTING(OSTC3_PPO2_MIN, ppO2Min); - READ_SETTING(OSTC3_PPO2_MAX, ppO2Max); - READ_SETTING(OSTC3_FUTURE_TTS, futureTTS); - READ_SETTING(OSTC3_CCR_MODE, ccrMode); - READ_SETTING(OSTC3_DECO_TYPE, decoType); - READ_SETTING(OSTC3_AGF_SELECTABLE, aGFSelectable); - READ_SETTING(OSTC3_AGF_HIGH, aGFHigh); - READ_SETTING(OSTC3_AGF_LOW, aGFLow); - READ_SETTING(OSTC3_CALIBRATION_GAS_O2, calibrationGas); - READ_SETTING(OSTC3_FLIP_SCREEN, flipScreen); - READ_SETTING(OSTC3_SETPOINT_FALLBACK, setPointFallback); - READ_SETTING(OSTC3_LEFT_BUTTON_SENSIVITY, leftButtonSensitivity); - READ_SETTING(OSTC3_RIGHT_BUTTON_SENSIVITY, rightButtonSensitivity); - READ_SETTING(OSTC3_BOTTOM_GAS_CONSUMPTION, bottomGasConsumption); - READ_SETTING(OSTC3_DECO_GAS_CONSUMPTION, decoGasConsumption); - READ_SETTING(OSTC3_MOD_WARNING, modWarning); - - //Skip things not supported on the sport, if its a sport. - if (m_deviceDetails->model == "Sport") { - EMIT_PROGRESS(); - EMIT_PROGRESS(); - EMIT_PROGRESS(); - } else { - READ_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate); - READ_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator); - READ_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2); - } - -#undef READ_SETTING - - rc = hw_ostc3_device_config_read(device, OSTC3_PRESSURE_SENSOR_OFFSET, uData, sizeof(uData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - // OSTC3 stores the pressureSensorOffset in two-complement - m_deviceDetails->pressureSensorOffset = (signed char)uData[0]; - EMIT_PROGRESS(); - - //read firmware settings - unsigned char fData[64] = { 0 }; - rc = hw_ostc3_device_version(device, fData, sizeof(fData)); - if (rc != DC_STATUS_SUCCESS) - return rc; - int serial = fData[0] + (fData[1] << 8); - m_deviceDetails->serialNo = QString::number(serial); - m_deviceDetails->firmwareVersion = QString::number(fData[2]) + "." + QString::number(fData[3]); - QByteArray ar((char *)fData + 4, 60); - m_deviceDetails->customText = ar.trimmed(); - EMIT_PROGRESS(); - - return rc; -} - -static dc_status_t write_ostc3_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) -{ - dc_status_t rc; - dc_event_progress_t progress; - progress.current = 0; - progress.maximum = 51; - - //write gas values - unsigned char gas1Data[4] = { - m_deviceDetails->gas1.oxygen, - m_deviceDetails->gas1.helium, - m_deviceDetails->gas1.type, - m_deviceDetails->gas1.depth - }; - - unsigned char gas2Data[4] = { - m_deviceDetails->gas2.oxygen, - m_deviceDetails->gas2.helium, - m_deviceDetails->gas2.type, - m_deviceDetails->gas2.depth - }; - - unsigned char gas3Data[4] = { - m_deviceDetails->gas3.oxygen, - m_deviceDetails->gas3.helium, - m_deviceDetails->gas3.type, - m_deviceDetails->gas3.depth - }; - - unsigned char gas4Data[4] = { - m_deviceDetails->gas4.oxygen, - m_deviceDetails->gas4.helium, - m_deviceDetails->gas4.type, - m_deviceDetails->gas4.depth - }; - - unsigned char gas5Data[4] = { - m_deviceDetails->gas5.oxygen, - m_deviceDetails->gas5.helium, - m_deviceDetails->gas5.type, - m_deviceDetails->gas5.depth - }; - //gas 1 - rc = hw_ostc3_device_config_write(device, OSTC3_GAS1, gas1Data, sizeof(gas1Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //gas 2 - rc = hw_ostc3_device_config_write(device, OSTC3_GAS2, gas2Data, sizeof(gas2Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //gas 3 - rc = hw_ostc3_device_config_write(device, OSTC3_GAS3, gas3Data, sizeof(gas3Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //gas 4 - rc = hw_ostc3_device_config_write(device, OSTC3_GAS4, gas4Data, sizeof(gas4Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //gas 5 - rc = hw_ostc3_device_config_write(device, OSTC3_GAS5, gas5Data, sizeof(gas5Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - //write set point values - unsigned char sp1Data[2] = { - m_deviceDetails->sp1.sp, - m_deviceDetails->sp1.depth - }; - - unsigned char sp2Data[2] = { - m_deviceDetails->sp2.sp, - m_deviceDetails->sp2.depth - }; - - unsigned char sp3Data[2] = { - m_deviceDetails->sp3.sp, - m_deviceDetails->sp3.depth - }; - - unsigned char sp4Data[2] = { - m_deviceDetails->sp4.sp, - m_deviceDetails->sp4.depth - }; - - unsigned char sp5Data[2] = { - m_deviceDetails->sp5.sp, - m_deviceDetails->sp5.depth - }; - - //sp 1 - rc = hw_ostc3_device_config_write(device, OSTC3_SP1, sp1Data, sizeof(sp1Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //sp 2 - rc = hw_ostc3_device_config_write(device, OSTC3_SP2, sp2Data, sizeof(sp2Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //sp 3 - rc = hw_ostc3_device_config_write(device, OSTC3_SP3, sp3Data, sizeof(sp3Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //sp 4 - rc = hw_ostc3_device_config_write(device, OSTC3_SP4, sp4Data, sizeof(sp4Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //sp 5 - rc = hw_ostc3_device_config_write(device, OSTC3_SP5, sp5Data, sizeof(sp5Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - //write dil values - unsigned char dil1Data[4] = { - m_deviceDetails->dil1.oxygen, - m_deviceDetails->dil1.helium, - m_deviceDetails->dil1.type, - m_deviceDetails->dil1.depth - }; - - unsigned char dil2Data[4] = { - m_deviceDetails->dil2.oxygen, - m_deviceDetails->dil2.helium, - m_deviceDetails->dil2.type, - m_deviceDetails->dil2.depth - }; - - unsigned char dil3Data[4] = { - m_deviceDetails->dil3.oxygen, - m_deviceDetails->dil3.helium, - m_deviceDetails->dil3.type, - m_deviceDetails->dil3.depth - }; - - unsigned char dil4Data[4] = { - m_deviceDetails->dil4.oxygen, - m_deviceDetails->dil4.helium, - m_deviceDetails->dil4.type, - m_deviceDetails->dil4.depth - }; - - unsigned char dil5Data[4] = { - m_deviceDetails->dil5.oxygen, - m_deviceDetails->dil5.helium, - m_deviceDetails->dil5.type, - m_deviceDetails->dil5.depth - }; - //dil 1 - rc = hw_ostc3_device_config_write(device, OSTC3_DIL1, dil1Data, sizeof(gas1Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //dil 2 - rc = hw_ostc3_device_config_write(device, OSTC3_DIL2, dil2Data, sizeof(dil2Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //dil 3 - rc = hw_ostc3_device_config_write(device, OSTC3_DIL3, dil3Data, sizeof(dil3Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //dil 4 - rc = hw_ostc3_device_config_write(device, OSTC3_DIL4, dil4Data, sizeof(dil4Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //dil 5 - rc = hw_ostc3_device_config_write(device, OSTC3_DIL5, dil5Data, sizeof(dil5Data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - //write general settings - //custom text - rc = hw_ostc3_device_customtext(device, m_deviceDetails->customText.toUtf8().data()); - if (rc != DC_STATUS_SUCCESS) - return rc; - - unsigned char data[1] = { 0 }; -#define WRITE_SETTING(_OSTC3_SETTING, _DEVICE_DETAIL) \ - do { \ - data[0] = m_deviceDetails->_DEVICE_DETAIL; \ - rc = hw_ostc3_device_config_write(device, _OSTC3_SETTING, data, sizeof(data)); \ - if (rc != DC_STATUS_SUCCESS) \ - return rc; \ - EMIT_PROGRESS(); \ - } while (0) - - WRITE_SETTING(OSTC3_DIVE_MODE, diveMode); - WRITE_SETTING(OSTC3_SATURATION, saturation); - WRITE_SETTING(OSTC3_DESATURATION, desaturation); - WRITE_SETTING(OSTC3_LAST_DECO, lastDeco); - WRITE_SETTING(OSTC3_BRIGHTNESS, brightness); - WRITE_SETTING(OSTC3_UNITS, units); - WRITE_SETTING(OSTC3_SAMPLING_RATE, samplingRate); - WRITE_SETTING(OSTC3_SALINITY, salinity); - WRITE_SETTING(OSTC3_DIVEMODE_COLOR, diveModeColor); - WRITE_SETTING(OSTC3_LANGUAGE, language); - WRITE_SETTING(OSTC3_DATE_FORMAT, dateFormat); - WRITE_SETTING(OSTC3_COMPASS_GAIN, compassGain); - WRITE_SETTING(OSTC3_SAFETY_STOP, safetyStop); - WRITE_SETTING(OSTC3_GF_HIGH, gfHigh); - WRITE_SETTING(OSTC3_GF_LOW, gfLow); - WRITE_SETTING(OSTC3_PPO2_MIN, ppO2Min); - WRITE_SETTING(OSTC3_PPO2_MAX, ppO2Max); - WRITE_SETTING(OSTC3_FUTURE_TTS, futureTTS); - WRITE_SETTING(OSTC3_CCR_MODE, ccrMode); - WRITE_SETTING(OSTC3_DECO_TYPE, decoType); - WRITE_SETTING(OSTC3_AGF_SELECTABLE, aGFSelectable); - WRITE_SETTING(OSTC3_AGF_HIGH, aGFHigh); - WRITE_SETTING(OSTC3_AGF_LOW, aGFLow); - WRITE_SETTING(OSTC3_CALIBRATION_GAS_O2, calibrationGas); - WRITE_SETTING(OSTC3_FLIP_SCREEN, flipScreen); - WRITE_SETTING(OSTC3_SETPOINT_FALLBACK, setPointFallback); - WRITE_SETTING(OSTC3_LEFT_BUTTON_SENSIVITY, leftButtonSensitivity); - WRITE_SETTING(OSTC3_RIGHT_BUTTON_SENSIVITY, rightButtonSensitivity); - WRITE_SETTING(OSTC3_BOTTOM_GAS_CONSUMPTION, bottomGasConsumption); - WRITE_SETTING(OSTC3_DECO_GAS_CONSUMPTION, decoGasConsumption); - WRITE_SETTING(OSTC3_MOD_WARNING, modWarning); - - //Skip things not supported on the sport, if its a sport. - if (m_deviceDetails->model == "Sport") { - EMIT_PROGRESS(); - EMIT_PROGRESS(); - EMIT_PROGRESS(); - } else { - WRITE_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate); - WRITE_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator); - WRITE_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2); - } - -#undef WRITE_SETTING - - // OSTC3 stores the pressureSensorOffset in two-complement - data[0] = (unsigned char)m_deviceDetails->pressureSensorOffset; - rc = hw_ostc3_device_config_write(device, OSTC3_PRESSURE_SENSOR_OFFSET, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - //sync date and time - if (m_deviceDetails->syncTime) { - QDateTime timeToSet = QDateTime::currentDateTime(); - dc_datetime_t time; - time.year = timeToSet.date().year(); - time.month = timeToSet.date().month(); - time.day = timeToSet.date().day(); - time.hour = timeToSet.time().hour(); - time.minute = timeToSet.time().minute(); - time.second = timeToSet.time().second(); - rc = hw_ostc3_device_clock(device, &time); - } - EMIT_PROGRESS(); - - return rc; -} -#endif /* DC_VERSION_CHECK(0, 5, 0) */ - -static dc_status_t read_ostc_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) -{ - dc_status_t rc; - dc_event_progress_t progress; - progress.current = 0; - progress.maximum = 3; - - unsigned char data[256] = {}; -#ifdef DEBUG_OSTC_CF - // FIXME: how should we report settings not supported back? - unsigned char max_CF = 0; -#endif - rc = hw_ostc_device_eeprom_read(device, 0, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - m_deviceDetails->serialNo = QString::number(data[1] << 8 ^ data[0]); - m_deviceDetails->numberOfDives = data[3] << 8 ^ data[2]; - //Byte5-6: - //Gas 1 default (%O2=21, %He=0) - gas gas1; - gas1.oxygen = data[6]; - gas1.helium = data[7]; - //Byte9-10: - //Gas 2 default (%O2=21, %He=0) - gas gas2; - gas2.oxygen = data[10]; - gas2.helium = data[11]; - //Byte13-14: - //Gas 3 default (%O2=21, %He=0) - gas gas3; - gas3.oxygen = data[14]; - gas3.helium = data[15]; - //Byte17-18: - //Gas 4 default (%O2=21, %He=0) - gas gas4; - gas4.oxygen = data[18]; - gas4.helium = data[19]; - //Byte21-22: - //Gas 5 default (%O2=21, %He=0) - gas gas5; - gas5.oxygen = data[22]; - gas5.helium = data[23]; - //Byte25-26: - //Gas 6 current (%O2, %He) - m_deviceDetails->salinity = data[26]; - // Active Gas Flag Register - gas1.type = data[27] & 0x01; - gas2.type = (data[27] & 0x02) >> 1; - gas3.type = (data[27] & 0x04) >> 2; - gas4.type = (data[27] & 0x08) >> 3; - gas5.type = (data[27] & 0x10) >> 4; - - // Gas switch depths - gas1.depth = data[28]; - gas2.depth = data[29]; - gas3.depth = data[30]; - gas4.depth = data[31]; - gas5.depth = data[32]; - // 33 which gas is Fist gas - switch (data[33]) { - case 1: - gas1.type = 2; - break; - case 2: - gas2.type = 2; - break; - case 3: - gas3.type = 2; - break; - case 4: - gas4.type = 2; - break; - case 5: - gas5.type = 2; - break; - default: - //Error? - break; - } - // Data filled up, set the gases. - m_deviceDetails->gas1 = gas1; - m_deviceDetails->gas2 = gas2; - m_deviceDetails->gas3 = gas3; - m_deviceDetails->gas4 = gas4; - m_deviceDetails->gas5 = gas5; - m_deviceDetails->decoType = data[34]; - //Byte36: - //Use O2 Sensor Module in CC Modes (0= OFF, 1= ON) (Only available in old OSTC1 - unused for OSTC Mk.2/2N) - //m_deviceDetails->ccrMode = data[35]; - setpoint sp1; - sp1.sp = data[36]; - sp1.depth = 0; - setpoint sp2; - sp2.sp = data[37]; - sp2.depth = 0; - setpoint sp3; - sp3.sp = data[38]; - sp3.depth = 0; - m_deviceDetails->sp1 = sp1; - m_deviceDetails->sp2 = sp2; - m_deviceDetails->sp3 = sp3; - // Byte41-42: - // Lowest Battery voltage seen (in mV) - // Byte43: - // Lowest Battery voltage seen at (Month) - // Byte44: - // Lowest Battery voltage seen at (Day) - // Byte45: - // Lowest Battery voltage seen at (Year) - // Byte46-47: - // Lowest Battery voltage seen at (Temperature in 0.1 °C) - // Byte48: - // Last complete charge at (Month) - // Byte49: - // Last complete charge at (Day) - // Byte50: - // Last complete charge at (Year) - // Byte51-52: - // Total charge cycles - // Byte53-54: - // Total complete charge cycles - // Byte55-56: - // Temperature Extrema minimum (Temperature in 0.1 °C) - // Byte57: - // Temperature Extrema minimum at (Month) - // Byte58: - // Temperature Extrema minimum at (Day) - // Byte59: - // Temperature Extrema minimum at (Year) - // Byte60-61: - // Temperature Extrema maximum (Temperature in 0.1 °C) - // Byte62: - // Temperature Extrema maximum at (Month) - // Byte63: - // Temperature Extrema maximum at (Day) - // Byte64: - // Temperature Extrema maximum at (Year) - // Byte65: - // Custom Text active (=1), Custom Text Disabled (<>1) - // Byte66-90: - // TO FIX EDITOR SYNTAX/INDENT { - // (25Bytes): Custom Text for Surfacemode (Real text must end with "}") - // Example: OSTC Dive Computer} (19 Characters incl. "}") Bytes 85-90 will be ignored. - if (data[64] == 1) { - // Make shure the data is null-terminated - data[89] = 0; - // Find the internal termination and replace it with 0 - char *term = strchr((char *)data + 65, (int)'}'); - if (term) - *term = 0; - m_deviceDetails->customText = (const char *)data + 65; - } - // Byte91: - // Dim OLED in Divemode (>0), Normal mode (=0) - // Byte92: - // Date format for all outputs: - // =0: MM/DD/YY - // =1: DD/MM/YY - // =2: YY/MM/DD - m_deviceDetails->dateFormat = data[91]; -// Byte93: -// Total number of CF used in installed firmware -#ifdef DEBUG_OSTC_CF - max_CF = data[92]; -#endif - // Byte94: - // Last selected view for customview area in surface mode - // Byte95: - // Last selected view for customview area in dive mode - // Byte96-97: - // Diluent 1 Default (%O2,%He) - // Byte98-99: - // Diluent 1 Current (%O2,%He) - gas dil1 = {}; - dil1.oxygen = data[97]; - dil1.helium = data[98]; - // Byte100-101: - // Gasuent 2 Default (%O2,%He) - // Byte102-103: - // Gasuent 2 Current (%O2,%He) - gas dil2 = {}; - dil2.oxygen = data[101]; - dil2.helium = data[102]; - // Byte104-105: - // Gasuent 3 Default (%O2,%He) - // Byte106-107: - // Gasuent 3 Current (%O2,%He) - gas dil3 = {}; - dil3.oxygen = data[105]; - dil3.helium = data[106]; - // Byte108-109: - // Gasuent 4 Default (%O2,%He) - // Byte110-111: - // Gasuent 4 Current (%O2,%He) - gas dil4 = {}; - dil4.oxygen = data[109]; - dil4.helium = data[110]; - // Byte112-113: - // Gasuent 5 Default (%O2,%He) - // Byte114-115: - // Gasuent 5 Current (%O2,%He) - gas dil5 = {}; - dil5.oxygen = data[113]; - dil5.helium = data[114]; - // Byte116: - // First Diluent (1-5) - switch (data[115]) { - case 1: - dil1.type = 2; - break; - case 2: - dil2.type = 2; - break; - case 3: - dil3.type = 2; - break; - case 4: - dil4.type = 2; - break; - case 5: - dil5.type = 2; - break; - default: - //Error? - break; - } - m_deviceDetails->dil1 = dil1; - m_deviceDetails->dil2 = dil2; - m_deviceDetails->dil3 = dil3; - m_deviceDetails->dil4 = dil4; - m_deviceDetails->dil5 = dil5; - // Byte117-128: - // not used/reserved - // Byte129-256: - // 32 custom Functions (CF0-CF31) - - // Decode the relevant ones - // CF11: Factor for saturation processes - m_deviceDetails->saturation = read_ostc_cf(data, 11); - // CF12: Factor for desaturation processes - m_deviceDetails->desaturation = read_ostc_cf(data, 12); - // CF17: Lower threshold for ppO2 warning - m_deviceDetails->ppO2Min = read_ostc_cf(data, 17); - // CF18: Upper threshold for ppO2 warning - m_deviceDetails->ppO2Max = read_ostc_cf(data, 18); - // CF20: Depth sampling rate for Profile storage - m_deviceDetails->samplingRate = read_ostc_cf(data, 20); - // CF29: Depth of last decompression stop - m_deviceDetails->lastDeco = read_ostc_cf(data, 29); - -#ifdef DEBUG_OSTC_CF - for (int cf = 0; cf <= 31 && cf <= max_CF; cf++) - printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); -#endif - - rc = hw_ostc_device_eeprom_read(device, 1, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - // Byte1: - // Logbook version indicator (Not writable!) - // Byte2-3: - // Last Firmware installed, 1st Byte.2nd Byte (e.g. „1.90“) (Not writable!) - m_deviceDetails->firmwareVersion = QString::number(data[1]) + "." + QString::number(data[2]); - // Byte4: - // OLED brightness (=0: Eco, =1 High) (Not writable!) - // Byte5-11: - // Time/Date vault during firmware updates - // Byte12-128 - // not used/reserved - // Byte129-256: - // 32 custom Functions (CF 32-63) - - // Decode the relevant ones - // CF32: Gradient Factor low - m_deviceDetails->gfLow = read_ostc_cf(data, 32); - // CF33: Gradient Factor high - m_deviceDetails->gfHigh = read_ostc_cf(data, 33); - // CF56: Bottom gas consumption - m_deviceDetails->bottomGasConsumption = read_ostc_cf(data, 56); - // CF57: Ascent gas consumption - m_deviceDetails->decoGasConsumption = read_ostc_cf(data, 57); - // CF58: Future time to surface setFutureTTS - m_deviceDetails->futureTTS = read_ostc_cf(data, 58); - -#ifdef DEBUG_OSTC_CF - for (int cf = 32; cf <= 63 && cf <= max_CF; cf++) - printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); -#endif - - rc = hw_ostc_device_eeprom_read(device, 2, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - // Byte1-4: - // not used/reserved (Not writable!) - // Byte5-128: - // not used/reserved - // Byte129-256: - // 32 custom Functions (CF 64-95) - - // Decode the relevant ones - // CF60: Graphic velocity - m_deviceDetails->graphicalSpeedIndicator = read_ostc_cf(data, 60); - // CF65: Show safety stop - m_deviceDetails->safetyStop = read_ostc_cf(data, 65); - // CF67: Alternaitve Gradient Factor low - m_deviceDetails->aGFLow = read_ostc_cf(data, 67); - // CF68: Alternative Gradient Factor high - m_deviceDetails->aGFHigh = read_ostc_cf(data, 68); - // CF69: Allow Gradient Factor change - m_deviceDetails->aGFSelectable = read_ostc_cf(data, 69); -#ifdef DEBUG_OSTC_CF - for (int cf = 64; cf <= 95 && cf <= max_CF; cf++) - printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); -#endif - - return rc; -} - -static dc_status_t write_ostc_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) -{ - dc_status_t rc; - dc_event_progress_t progress; - progress.current = 0; - progress.maximum = 7; - unsigned char data[256] = {}; - unsigned char max_CF = 0; - - // Because we write whole memory blocks, we read all the current - // values out and then change then ones we should change. - rc = hw_ostc_device_eeprom_read(device, 0, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - //Byte5-6: - //Gas 1 default (%O2=21, %He=0) - gas gas1 = m_deviceDetails->gas1; - data[6] = gas1.oxygen; - data[7] = gas1.helium; - //Byte9-10: - //Gas 2 default (%O2=21, %He=0) - gas gas2 = m_deviceDetails->gas2; - data[10] = gas2.oxygen; - data[11] = gas2.helium; - //Byte13-14: - //Gas 3 default (%O2=21, %He=0) - gas gas3 = m_deviceDetails->gas3; - data[14] = gas3.oxygen; - data[15] = gas3.helium; - //Byte17-18: - //Gas 4 default (%O2=21, %He=0) - gas gas4 = m_deviceDetails->gas4; - data[18] = gas4.oxygen; - data[19] = gas4.helium; - //Byte21-22: - //Gas 5 default (%O2=21, %He=0) - gas gas5 = m_deviceDetails->gas5; - data[22] = gas5.oxygen; - data[23] = gas5.helium; - //Byte25-26: - //Gas 6 current (%O2, %He) - data[26] = m_deviceDetails->salinity; - // Gas types, 0=Disabled, 1=Active, 2=Fist - // Active Gas Flag Register - data[27] = 0; - if (gas1.type) - data[27] ^= 0x01; - if (gas2.type) - data[27] ^= 0x02; - if (gas3.type) - data[27] ^= 0x04; - if (gas4.type) - data[27] ^= 0x08; - if (gas5.type) - data[27] ^= 0x10; - - // Gas switch depths - data[28] = gas1.depth; - data[29] = gas2.depth; - data[30] = gas3.depth; - data[31] = gas4.depth; - data[32] = gas5.depth; - // 33 which gas is Fist gas - if (gas1.type == 2) - data[33] = 1; - else if (gas2.type == 2) - data[33] = 2; - else if (gas3.type == 2) - data[33] = 3; - else if (gas4.type == 2) - data[33] = 4; - else if (gas5.type == 2) - data[33] = 5; - else - // FIXME: No gas was First? - // Set gas 1 to first - data[33] = 1; - - data[34] = m_deviceDetails->decoType; - //Byte36: - //Use O2 Sensor Module in CC Modes (0= OFF, 1= ON) (Only available in old OSTC1 - unused for OSTC Mk.2/2N) - //m_deviceDetails->ccrMode = data[35]; - data[36] = m_deviceDetails->sp1.sp; - data[37] = m_deviceDetails->sp2.sp; - data[38] = m_deviceDetails->sp3.sp; - // Byte41-42: - // Lowest Battery voltage seen (in mV) - // Byte43: - // Lowest Battery voltage seen at (Month) - // Byte44: - // Lowest Battery voltage seen at (Day) - // Byte45: - // Lowest Battery voltage seen at (Year) - // Byte46-47: - // Lowest Battery voltage seen at (Temperature in 0.1 °C) - // Byte48: - // Last complete charge at (Month) - // Byte49: - // Last complete charge at (Day) - // Byte50: - // Last complete charge at (Year) - // Byte51-52: - // Total charge cycles - // Byte53-54: - // Total complete charge cycles - // Byte55-56: - // Temperature Extrema minimum (Temperature in 0.1 °C) - // Byte57: - // Temperature Extrema minimum at (Month) - // Byte58: - // Temperature Extrema minimum at (Day) - // Byte59: - // Temperature Extrema minimum at (Year) - // Byte60-61: - // Temperature Extrema maximum (Temperature in 0.1 °C) - // Byte62: - // Temperature Extrema maximum at (Month) - // Byte63: - // Temperature Extrema maximum at (Day) - // Byte64: - // Temperature Extrema maximum at (Year) - // Byte65: - // Custom Text active (=1), Custom Text Disabled (<>1) - // Byte66-90: - // (25Bytes): Custom Text for Surfacemode (Real text must end with "}") - // Example: "OSTC Dive Computer}" (19 Characters incl. "}") Bytes 85-90 will be ignored. - if (m_deviceDetails->customText == "") { - data[64] = 0; - } else { - data[64] = 1; - // Copy the string to the right place in the memory, padded with 0x20 (" ") - strncpy((char *)data + 65, QString("%1").arg(m_deviceDetails->customText, -23, QChar(' ')).toUtf8().data(), 23); - // And terminate the string. - if (m_deviceDetails->customText.length() <= 23) - data[65 + m_deviceDetails->customText.length()] = '}'; - else - data[90] = '}'; - } - // Byte91: - // Dim OLED in Divemode (>0), Normal mode (=0) - // Byte92: - // Date format for all outputs: - // =0: MM/DD/YY - // =1: DD/MM/YY - // =2: YY/MM/DD - data[91] = m_deviceDetails->dateFormat; - // Byte93: - // Total number of CF used in installed firmware - max_CF = data[92]; - // Byte94: - // Last selected view for customview area in surface mode - // Byte95: - // Last selected view for customview area in dive mode - // Byte96-97: - // Diluent 1 Default (%O2,%He) - // Byte98-99: - // Diluent 1 Current (%O2,%He) - gas dil1 = m_deviceDetails->dil1; - data[97] = dil1.oxygen; - data[98] = dil1.helium; - // Byte100-101: - // Gasuent 2 Default (%O2,%He) - // Byte102-103: - // Gasuent 2 Current (%O2,%He) - gas dil2 = m_deviceDetails->dil2; - data[101] = dil2.oxygen; - data[102] = dil2.helium; - // Byte104-105: - // Gasuent 3 Default (%O2,%He) - // Byte106-107: - // Gasuent 3 Current (%O2,%He) - gas dil3 = m_deviceDetails->dil3; - data[105] = dil3.oxygen; - data[106] = dil3.helium; - // Byte108-109: - // Gasuent 4 Default (%O2,%He) - // Byte110-111: - // Gasuent 4 Current (%O2,%He) - gas dil4 = m_deviceDetails->dil4; - data[109] = dil4.oxygen; - data[110] = dil4.helium; - // Byte112-113: - // Gasuent 5 Default (%O2,%He) - // Byte114-115: - // Gasuent 5 Current (%O2,%He) - gas dil5 = m_deviceDetails->dil5; - data[113] = dil5.oxygen; - data[114] = dil5.helium; - // Byte116: - // First Diluent (1-5) - if (dil1.type == 2) - data[115] = 1; - else if (dil2.type == 2) - data[115] = 2; - else if (dil3.type == 2) - data[115] = 3; - else if (dil4.type == 2) - data[115] = 4; - else if (dil5.type == 2) - data[115] = 5; - else - // FIXME: No first diluent? - // Set gas 1 to fist - data[115] = 1; - - // Byte117-128: - // not used/reserved - // Byte129-256: - // 32 custom Functions (CF0-CF31) - - // Write the relevant ones - // CF11: Factor for saturation processes - write_ostc_cf(data, 11, max_CF, m_deviceDetails->saturation); - // CF12: Factor for desaturation processes - write_ostc_cf(data, 12, max_CF, m_deviceDetails->desaturation); - // CF17: Lower threshold for ppO2 warning - write_ostc_cf(data, 17, max_CF, m_deviceDetails->ppO2Min); - // CF18: Upper threshold for ppO2 warning - write_ostc_cf(data, 18, max_CF, m_deviceDetails->ppO2Max); - // CF20: Depth sampling rate for Profile storage - write_ostc_cf(data, 20, max_CF, m_deviceDetails->samplingRate); - // CF29: Depth of last decompression stop - write_ostc_cf(data, 29, max_CF, m_deviceDetails->lastDeco); - -#ifdef DEBUG_OSTC_CF - for (int cf = 0; cf <= 31 && cf <= max_CF; cf++) - printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); -#endif - rc = hw_ostc_device_eeprom_write(device, 0, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - rc = hw_ostc_device_eeprom_read(device, 1, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - // Byte1: - // Logbook version indicator (Not writable!) - // Byte2-3: - // Last Firmware installed, 1st Byte.2nd Byte (e.g. „1.90“) (Not writable!) - // Byte4: - // OLED brightness (=0: Eco, =1 High) (Not writable!) - // Byte5-11: - // Time/Date vault during firmware updates - // Byte12-128 - // not used/reserved - // Byte129-256: - // 32 custom Functions (CF 32-63) - - // Decode the relevant ones - // CF32: Gradient Factor low - write_ostc_cf(data, 32, max_CF, m_deviceDetails->gfLow); - // CF33: Gradient Factor high - write_ostc_cf(data, 33, max_CF, m_deviceDetails->gfHigh); - // CF56: Bottom gas consumption - write_ostc_cf(data, 56, max_CF, m_deviceDetails->bottomGasConsumption); - // CF57: Ascent gas consumption - write_ostc_cf(data, 57, max_CF, m_deviceDetails->decoGasConsumption); - // CF58: Future time to surface setFutureTTS - write_ostc_cf(data, 58, max_CF, m_deviceDetails->futureTTS); -#ifdef DEBUG_OSTC_CF - for (int cf = 32; cf <= 63 && cf <= max_CF; cf++) - printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); -#endif - rc = hw_ostc_device_eeprom_write(device, 1, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - rc = hw_ostc_device_eeprom_read(device, 2, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - // Byte1-4: - // not used/reserved (Not writable!) - // Byte5-128: - // not used/reserved - // Byte129-256: - // 32 custom Functions (CF 64-95) - - // Decode the relevant ones - // CF60: Graphic velocity - write_ostc_cf(data, 60, max_CF, m_deviceDetails->graphicalSpeedIndicator); - // CF65: Show safety stop - write_ostc_cf(data, 65, max_CF, m_deviceDetails->safetyStop); - // CF67: Alternaitve Gradient Factor low - write_ostc_cf(data, 67, max_CF, m_deviceDetails->aGFLow); - // CF68: Alternative Gradient Factor high - write_ostc_cf(data, 68, max_CF, m_deviceDetails->aGFHigh); - // CF69: Allow Gradient Factor change - write_ostc_cf(data, 69, max_CF, m_deviceDetails->aGFSelectable); -#ifdef DEBUG_OSTC_CF - for (int cf = 64; cf <= 95 && cf <= max_CF; cf++) - printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); -#endif - rc = hw_ostc_device_eeprom_write(device, 2, data, sizeof(data)); - if (rc != DC_STATUS_SUCCESS) - return rc; - EMIT_PROGRESS(); - - //sync date and time - if (m_deviceDetails->syncTime) { - QDateTime timeToSet = QDateTime::currentDateTime(); - dc_datetime_t time; - time.year = timeToSet.date().year(); - time.month = timeToSet.date().month(); - time.day = timeToSet.date().day(); - time.hour = timeToSet.time().hour(); - time.minute = timeToSet.time().minute(); - time.second = timeToSet.time().second(); - rc = hw_ostc_device_clock(device, &time); - } - EMIT_PROGRESS(); - return rc; -} - -#undef EMIT_PROGRESS - -DeviceThread::DeviceThread(QObject *parent, device_data_t *data) : QThread(parent), m_data(data) -{ -} - -void DeviceThread::progressCB(int percent) -{ - emit progress(percent); -} - -void DeviceThread::event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata) -{ - const dc_event_progress_t *progress = (dc_event_progress_t *) data; - DeviceThread *dt = static_cast(userdata); - - switch (event) { - case DC_EVENT_PROGRESS: - dt->progressCB(100.0 * (double)progress->current / (double)progress->maximum); - break; - default: - emit dt->error("Unexpected event recived"); - break; - } -} - -ReadSettingsThread::ReadSettingsThread(QObject *parent, device_data_t *data) : DeviceThread(parent, data) -{ -} - -void ReadSettingsThread::run() -{ - dc_status_t rc; - - DeviceDetails *m_deviceDetails = new DeviceDetails(0); - switch (dc_device_get_type(m_data->device)) { - case DC_FAMILY_SUUNTO_VYPER: - rc = read_suunto_vyper_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); - if (rc == DC_STATUS_SUCCESS) { - emit devicedetails(m_deviceDetails); - } else if (rc == DC_STATUS_UNSUPPORTED) { - emit error(tr("This feature is not yet available for the selected dive computer.")); - } else { - emit error("Failed!"); - } - break; -#if DC_VERSION_CHECK(0, 5, 0) - case DC_FAMILY_HW_OSTC3: - rc = read_ostc3_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); - if (rc == DC_STATUS_SUCCESS) - emit devicedetails(m_deviceDetails); - else - emit error("Failed!"); - break; -#endif // divecomputer 0.5.0 -#ifdef DEBUG_OSTC - case DC_FAMILY_NULL: -#endif - case DC_FAMILY_HW_OSTC: - rc = read_ostc_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); - if (rc == DC_STATUS_SUCCESS) - emit devicedetails(m_deviceDetails); - else - emit error("Failed!"); - break; - default: - emit error(tr("This feature is not yet available for the selected dive computer.")); - break; - } -} - -WriteSettingsThread::WriteSettingsThread(QObject *parent, device_data_t *data) : - DeviceThread(parent, data), - m_deviceDetails(NULL) -{ -} - -void WriteSettingsThread::setDeviceDetails(DeviceDetails *details) -{ - m_deviceDetails = details; -} - -void WriteSettingsThread::run() -{ - dc_status_t rc; - - switch (dc_device_get_type(m_data->device)) { - case DC_FAMILY_SUUNTO_VYPER: - rc = write_suunto_vyper_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); - if (rc == DC_STATUS_UNSUPPORTED) { - emit error(tr("This feature is not yet available for the selected dive computer.")); - } else if (rc != DC_STATUS_SUCCESS) { - emit error(tr("Failed!")); - } - break; -#if DC_VERSION_CHECK(0, 5, 0) - case DC_FAMILY_HW_OSTC3: - rc = write_ostc3_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); - if (rc != DC_STATUS_SUCCESS) - emit error(tr("Failed!")); - break; -#endif // divecomputer 0.5.0 -#ifdef DEBUG_OSTC - case DC_FAMILY_NULL: -#endif - case DC_FAMILY_HW_OSTC: - rc = write_ostc_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); - if (rc != DC_STATUS_SUCCESS) - emit error(tr("Failed!")); - break; - default: - emit error(tr("This feature is not yet available for the selected dive computer.")); - break; - } -} - - -FirmwareUpdateThread::FirmwareUpdateThread(QObject *parent, device_data_t *data, QString fileName) : DeviceThread(parent, data), m_fileName(fileName) -{ -} - -void FirmwareUpdateThread::run() -{ - dc_status_t rc; - - rc = dc_device_set_events(m_data->device, DC_EVENT_PROGRESS, DeviceThread::event_cb, this); - if (rc != DC_STATUS_SUCCESS) { - emit error("Error registering the event handler."); - return; - } - switch (dc_device_get_type(m_data->device)) { -#if DC_VERSION_CHECK(0, 5, 0) - case DC_FAMILY_HW_OSTC3: - rc = hw_ostc3_device_fwupdate(m_data->device, m_fileName.toUtf8().data()); - break; - case DC_FAMILY_HW_OSTC: - rc = hw_ostc_device_fwupdate(m_data->device, m_fileName.toUtf8().data()); - break; -#endif // divecomputer 0.5.0 - default: - emit error(tr("This feature is not yet available for the selected dive computer.")); - return; - } - - if (rc != DC_STATUS_SUCCESS) { - emit error(tr("Firmware update failed!")); - } -} - - -ResetSettingsThread::ResetSettingsThread(QObject *parent, device_data_t *data) : DeviceThread(parent, data) -{ -} - -void ResetSettingsThread::run() -{ - dc_status_t rc = DC_STATUS_SUCCESS; - -#if DC_VERSION_CHECK(0, 5, 0) - if (dc_device_get_type(m_data->device) == DC_FAMILY_HW_OSTC3) { - rc = hw_ostc3_device_config_reset(m_data->device); - emit progress(100); - } -#endif // divecomputer 0.5.0 - if (rc != DC_STATUS_SUCCESS) { - emit error(tr("Reset settings failed!")); - } -} diff --git a/configuredivecomputerthreads.h b/configuredivecomputerthreads.h deleted file mode 100644 index 1d7a36f9b..000000000 --- a/configuredivecomputerthreads.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef CONFIGUREDIVECOMPUTERTHREADS_H -#define CONFIGUREDIVECOMPUTERTHREADS_H - -#include -#include -#include -#include "libdivecomputer.h" -#include -#include "devicedetails.h" - -class DeviceThread : public QThread { - Q_OBJECT -public: - DeviceThread(QObject *parent, device_data_t *data); - virtual void run() = 0; -signals: - void error(QString err); - void progress(int value); -protected: - device_data_t *m_data; - void progressCB(int value); - static void event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata); -}; - -class ReadSettingsThread : public DeviceThread { - Q_OBJECT -public: - ReadSettingsThread(QObject *parent, device_data_t *data); - void run(); -signals: - void devicedetails(DeviceDetails *newDeviceDetails); -}; - -class WriteSettingsThread : public DeviceThread { - Q_OBJECT -public: - WriteSettingsThread(QObject *parent, device_data_t *data); - void setDeviceDetails(DeviceDetails *details); - void run(); - -private: - DeviceDetails *m_deviceDetails; -}; - -class FirmwareUpdateThread : public DeviceThread { - Q_OBJECT -public: - FirmwareUpdateThread(QObject *parent, device_data_t *data, QString fileName); - void run(); - -private: - QString m_fileName; -}; - -class ResetSettingsThread : public DeviceThread { - Q_OBJECT -public: - ResetSettingsThread(QObject *parent, device_data_t *data); - void run(); -}; - -#endif // CONFIGUREDIVECOMPUTERTHREADS_H diff --git a/datatrak.c b/datatrak.c deleted file mode 100644 index 03fa71a50..000000000 --- a/datatrak.c +++ /dev/null @@ -1,695 +0,0 @@ -#include -#include -#include -#include - -#include "datatrak.h" -#include "dive.h" -#include "units.h" -#include "device.h" -#include "gettext.h" - -extern struct sample *add_sample(struct sample *sample, int time, struct divecomputer *dc); - -unsigned char lector_bytes[2], lector_word[4], tmp_1byte, *byte; -unsigned int tmp_2bytes; -char is_nitrox, is_O2, is_SCR; -unsigned long tmp_4bytes; - -static unsigned int two_bytes_to_int(unsigned char x, unsigned char y) -{ - return (x << 8) + y; -} - -static unsigned long four_bytes_to_long(unsigned char x, unsigned char y, unsigned char z, unsigned char t) -{ - return ((long)x << 24) + ((long)y << 16) + ((long)z << 8) + (long)t; -} - -static unsigned char *byte_to_bits(unsigned char byte) -{ - unsigned char i, *bits = (unsigned char *)malloc(8); - - for (i = 0; i < 8; i++) - bits[i] = byte & (1 << i); - return bits; -} - -/* - * Datatrak stores the date in days since 01-01-1600, while Subsurface uses - * time_t (seconds since 00:00 01-01-1970). Function substracts - * (1970 - 1600) * 365,2425 = 135139,725 to our date variable, getting the - * days since Epoch. - */ -static time_t date_time_to_ssrfc(unsigned long date, int time) -{ - time_t tmp; - tmp = (date - 135140) * 86400 + time * 60; - return tmp; -} - -static unsigned char to_8859(unsigned char char_cp850) -{ - static const unsigned char char_8859[46] = { 0xc7, 0xfc, 0xe9, 0xe2, 0xe4, 0xe0, 0xe5, 0xe7, - 0xea, 0xeb, 0xe8, 0xef, 0xee, 0xec, 0xc4, 0xc5, - 0xc9, 0xe6, 0xc6, 0xf4, 0xf6, 0xf2, 0xfb, 0xf9, - 0xff, 0xd6, 0xdc, 0xf8, 0xa3, 0xd8, 0xd7, 0x66, - 0xe1, 0xed, 0xf3, 0xfa, 0xf1, 0xd1, 0xaa, 0xba, - 0xbf, 0xae, 0xac, 0xbd, 0xbc, 0xa1 }; - return char_8859[char_cp850 - 0x80]; -} - -static char *to_utf8(unsigned char *in_string) -{ - int outlen, inlen, i = 0, j = 0; - inlen = strlen((char *)in_string); - outlen = inlen * 2 + 1; - - char *out_string = calloc(outlen, 1); - for (i = 0; i < inlen; i++) { - if (in_string[i] < 127) - out_string[j] = in_string[i]; - else { - if (in_string[i] > 127 && in_string[i] <= 173) - in_string[i] = to_8859(in_string[i]); - out_string[j] = (in_string[i] >> 6) | 0xC0; - j++; - out_string[j] = (in_string[i] & 0x3F) | 0x80; - } - j++; - } - out_string[j + 1] = '\0'; - return out_string; -} - -/* - * Subsurface sample structure doesn't support the flags and alarms in the dt .log - * so will treat them as dc events. - */ -static struct sample *dtrak_profile(struct dive *dt_dive, FILE *archivo) -{ - int i, j = 1, interval, o2percent = dt_dive->cylinder[0].gasmix.o2.permille / 10; - struct sample *sample = dt_dive->dc.sample; - struct divecomputer *dc = &dt_dive->dc; - - for (i = 1; i <= dt_dive->dc.alloc_samples; i++) { - if (fread(&lector_bytes, 1, 2, archivo) != 2) - return sample; - interval= 20 * (i + 1); - sample = add_sample(sample, interval, dc); - sample->depth.mm = (two_bytes_to_int(lector_bytes[0], lector_bytes[1]) & 0xFFC0) * 1000 / 410; - byte = byte_to_bits(two_bytes_to_int(lector_bytes[0], lector_bytes[1]) & 0x003F); - if (byte[0] != 0) - sample->in_deco = true; - else - sample->in_deco = false; - if (byte[1] != 0) - add_event(dc, sample->time.seconds, 0, 0, 0, "rbt"); - if (byte[2] != 0) - add_event(dc, sample->time.seconds, 0, 0, 0, "ascent"); - if (byte[3] != 0) - add_event(dc, sample->time.seconds, 0, 0, 0, "ceiling"); - if (byte[4] != 0) - add_event(dc, sample->time.seconds, 0, 0, 0, "workload"); - if (byte[5] != 0) - add_event(dc, sample->time.seconds, 0, 0, 0, "transmitter"); - if (j == 3) { - read_bytes(1); - if (is_O2) { - read_bytes(1); - o2percent = tmp_1byte; - } - j = 0; - } - free(byte); - - // In commit 5f44fdd setpoint replaced po2, so although this is not necesarily CCR dive ... - if (is_O2) - sample->setpoint.mbar = calculate_depth_to_mbar(sample->depth.mm, dt_dive->surface_pressure, 0) * o2percent / 100; - j++; - } -bail: - return sample; -} - -/* - * Reads the header of a file and returns the header struct - * If it's not a DATATRAK file returns header zero initalized - */ -static dtrakheader read_file_header(FILE *archivo) -{ - dtrakheader fileheader = { 0 }; - const short headerbytes = 12; - unsigned char *lector = (unsigned char *)malloc(headerbytes); - - if (fread(lector, 1, headerbytes, archivo) != headerbytes) { - free(lector); - return fileheader; - } - if (two_bytes_to_int(lector[0], lector[1]) != 0xA100) { - report_error(translate("gettextFromC", "Error: the file does not appear to be a DATATRAK divelog")); - free(lector); - return fileheader; - } - fileheader.header = (lector[0] << 8) + lector[1]; - fileheader.dc_serial_1 = two_bytes_to_int(lector[2], lector[3]); - fileheader.dc_serial_2 = two_bytes_to_int(lector[4], lector[5]); - fileheader.divesNum = two_bytes_to_int(lector[7], lector[6]); - free(lector); - return fileheader; -} - -#define CHECK(_func, _val) if ((_func) != (_val)) goto bail - -/* - * Parses the dive extracting its data and filling a subsurface's dive structure - */ -bool dt_dive_parser(FILE *archivo, struct dive *dt_dive) -{ - unsigned char n; - int profile_length; - char *tmp_notes_str = NULL; - unsigned char *tmp_string1 = NULL, - *locality = NULL, - *dive_point = NULL; - char buffer[1024]; - struct divecomputer *dc = &dt_dive->dc; - - is_nitrox = is_O2 = is_SCR = 0; - - /* - * Parse byte to byte till next dive entry - */ - n = 0; - CHECK(fread(&lector_bytes[n], 1, 1, archivo), 1); - while (lector_bytes[n] != 0xA0) - CHECK(fread(&lector_bytes[n], 1, 1, archivo), 1); - - /* - * Found dive header 0xA000, verify second byte - */ - CHECK(fread(&lector_bytes[n+1], 1, 1, archivo), 1); - if (two_bytes_to_int(lector_bytes[0], lector_bytes[1]) != 0xA000) { - printf("Error: byte = %4x\n", two_bytes_to_int(lector_bytes[0], lector_bytes[1])); - return false; - } - - /* - * Begin parsing - * First, Date of dive, 4 bytes - */ - read_bytes(4); - - - /* - * Next, Time in minutes since 00:00 - */ - read_bytes(2); - - dt_dive->dc.when = dt_dive->when = (timestamp_t)date_time_to_ssrfc(tmp_4bytes, tmp_2bytes); - - /* - * Now, Locality, 1st byte is long of string, rest is string - */ - read_bytes(1); - read_string(locality); - - /* - * Next, Dive point, defined as Locality - */ - read_bytes(1); - read_string(dive_point); - - /* - * Subsurface only have a location variable, so we have to merge DTrak's - * Locality and Dive points. - */ - snprintf(buffer, sizeof(buffer), "%s, %s", locality, dive_point); - dt_dive->dive_site_uuid = get_dive_site_uuid_by_name(buffer, NULL); - if (dt_dive->dive_site_uuid == 0) - dt_dive->dive_site_uuid = create_dive_site(buffer, dt_dive->when); - free(locality); - free(dive_point); - - /* - * Altitude. Don't exist in Subsurface, the equivalent would be - * surface air pressure which can, be calculated from altitude. - * As dtrak registers altitude intervals, we, arbitrarily, choose - * the lower altitude/pressure equivalence for each segment. So - * - * Datatrak table * Conversion formula: - * * - * byte = 1 0 - 700 m * P = P0 * exp(-(g * M * h ) / (R * T0)) - * byte = 2 700 - 1700m * P0 = sealevel pressure = 101325 Pa - * byte = 3 1700 - 2700 m * g = grav. acceleration = 9,80665 m/s² - * byte = 4 2700 - * m * M = molar mass (dry air) = 0,0289644 Kg/mol - * * h = altitude over sea level (m) - * * R = Universal gas constant = 8,31447 J/(mol*K) - * * T0 = sea level standard temperature = 288,15 K - */ - read_bytes(1); - switch (tmp_1byte) { - case 1: - dt_dive->dc.surface_pressure.mbar = 1013; - break; - case 2: - dt_dive->dc.surface_pressure.mbar = 932; - break; - case 3: - dt_dive->dc.surface_pressure.mbar = 828; - break; - case 4: - dt_dive->dc.surface_pressure.mbar = 735; - break; - default: - dt_dive->dc.surface_pressure.mbar = 1013; - } - - /* - * Interval (minutes) - */ - read_bytes(2); - if (tmp_2bytes != 0x7FFF) - dt_dive->dc.surfacetime.seconds = (uint32_t) tmp_2bytes * 60; - - /* - * Weather, values table, 0 to 6 - * Subsurface don't have this record but we can use tags - */ - dt_dive->tag_list = NULL; - read_bytes(1); - switch (tmp_1byte) { - case 1: - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "clear"))); - break; - case 2: - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "misty"))); - break; - case 3: - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "fog"))); - break; - case 4: - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "rain"))); - break; - case 5: - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "storm"))); - break; - case 6: - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "snow"))); - break; - default: - // unknown, do nothing - break; - } - - /* - * Air Temperature - */ - read_bytes(2); - if (tmp_2bytes != 0x7FFF) - dt_dive->dc.airtemp.mkelvin = C_to_mkelvin((double)(tmp_2bytes / 100)); - - /* - * Dive suit, values table, 0 to 6 - */ - read_bytes(1); - switch (tmp_1byte) { - case 1: - dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "No suit")); - break; - case 2: - dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Shorty")); - break; - case 3: - dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Combi")); - break; - case 4: - dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Wet suit")); - break; - case 5: - dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Semidry suit")); - break; - case 6: - dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Dry suit")); - break; - default: - // unknown, do nothing - break; - } - - /* - * Tank, volume size in liter*100. And initialize gasmix to air (default). - * Dtrak don't record init and end pressures, but consumed bar, so let's - * init a default pressure of 200 bar. - */ - read_bytes(2); - if (tmp_2bytes != 0x7FFF) { - dt_dive->cylinder[0].type.size.mliter = tmp_2bytes * 10; - dt_dive->cylinder[0].type.description = strdup(""); - dt_dive->cylinder[0].start.mbar = 200000; - dt_dive->cylinder[0].gasmix.he.permille = 0; - dt_dive->cylinder[0].gasmix.o2.permille = 210; - dt_dive->cylinder[0].manually_added = true; - } - - /* - * Maximum depth, in cm. - */ - read_bytes(2); - if (tmp_2bytes != 0x7FFF) - dt_dive->maxdepth.mm = dt_dive->dc.maxdepth.mm = (int32_t)tmp_2bytes * 10; - - /* - * Dive time in minutes. - */ - read_bytes(2); - if (tmp_2bytes != 0x7FFF) - dt_dive->duration.seconds = dt_dive->dc.duration.seconds = (uint32_t)tmp_2bytes * 60; - - /* - * Minimum water temperature in C*100. If unknown, set it to 0K which - * is subsurface's value for "unknown" - */ - read_bytes(2); - if (tmp_2bytes != 0x7fff) - dt_dive->watertemp.mkelvin = dt_dive->dc.watertemp.mkelvin = C_to_mkelvin((double)(tmp_2bytes / 100)); - else - dt_dive->watertemp.mkelvin = 0; - - /* - * Air used in bar*100. - */ - read_bytes(2); - if (tmp_2bytes != 0x7FFF && dt_dive->cylinder[0].type.size.mliter) - dt_dive->cylinder[0].gas_used.mliter = dt_dive->cylinder[0].type.size.mliter * (tmp_2bytes / 100.0); - - /* - * Dive Type 1 - Bit table. Subsurface don't have this record, but - * will use tags. Bits 0 and 1 are not used. Reuse coincident tags. - */ - read_bytes(1); - byte = byte_to_bits(tmp_1byte); - if (byte[2] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "no stop"))); - if (byte[3] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "deco"))); - if (byte[4] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "single ascent"))); - if (byte[5] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "multiple ascent"))); - if (byte[6] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "fresh"))); - if (byte[7] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "salt water"))); - free(byte); - - /* - * Dive Type 2 - Bit table, use tags again - */ - read_bytes(1); - byte = byte_to_bits(tmp_1byte); - if (byte[0] != 0) { - taglist_add_tag(&dt_dive->tag_list, strdup("nitrox")); - is_nitrox = 1; - } - if (byte[1] != 0) { - taglist_add_tag(&dt_dive->tag_list, strdup("rebreather")); - is_SCR = 1; - dt_dive->dc.divemode = PSCR; - } - free(byte); - - /* - * Dive Activity 1 - Bit table, use tags again - */ - read_bytes(1); - byte = byte_to_bits(tmp_1byte); - if (byte[0] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "sight seeing"))); - if (byte[1] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "club dive"))); - if (byte[2] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "instructor"))); - if (byte[3] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "instruction"))); - if (byte[4] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "night"))); - if (byte[5] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "cave"))); - if (byte[6] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "ice"))); - if (byte[7] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "search"))); - free(byte); - - - /* - * Dive Activity 2 - Bit table, use tags again - */ - read_bytes(1); - byte = byte_to_bits(tmp_1byte); - if (byte[0] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "wreck"))); - if (byte[1] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "river"))); - if (byte[2] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "drift"))); - if (byte[3] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "photo"))); - if (byte[4] != 0) - taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "other"))); - free(byte); - - /* - * Other activities - String 1st byte = long - * Will put this in dive notes before the true notes - */ - read_bytes(1); - if (tmp_1byte != 0) { - read_string(tmp_string1); - snprintf(buffer, sizeof(buffer), "%s: %s\n", - QT_TRANSLATE_NOOP("gettextFromC", "Other activities"), - tmp_string1); - tmp_notes_str = strdup(buffer); - free(tmp_string1); - } - - /* - * Dive buddies - */ - read_bytes(1); - if (tmp_1byte != 0) { - read_string(tmp_string1); - dt_dive->buddy = strdup((char *)tmp_string1); - free(tmp_string1); - } - - /* - * Dive notes - */ - read_bytes(1); - if (tmp_1byte != 0) { - read_string(tmp_string1); - int len = snprintf(buffer, sizeof(buffer), "%s%s:\n%s", - tmp_notes_str ? tmp_notes_str : "", - QT_TRANSLATE_NOOP("gettextFromC", "Datatrak/Wlog notes"), - tmp_string1); - dt_dive->notes = calloc((len +1), 1); - dt_dive->notes = memcpy(dt_dive->notes, buffer, len); - free(tmp_string1); - if (tmp_notes_str != NULL) - free(tmp_notes_str); - } - - /* - * Alarms 1 - Bit table - Not in Subsurface, we use the profile - */ - read_bytes(1); - - /* - * Alarms 2 - Bit table - Not in Subsurface, we use the profile - */ - read_bytes(1); - - /* - * Dive number (in datatrak, after import user has to renumber) - */ - read_bytes(2); - dt_dive->number = tmp_2bytes; - - /* - * Computer timestamp - Useless for Subsurface - */ - read_bytes(4); - - /* - * Model - table - Not included 0x14, 0x24, 0x41, and 0x73 - * known to exist, but not its model name - To add in the future. - * Strangely 0x00 serves for manually added dives and a dc too, at - * least in EXAMPLE.LOG file, shipped with the software. - */ - read_bytes(1); - switch (tmp_1byte) { - case 0x00: - dt_dive->dc.model = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Manually entered dive")); - break; - case 0x1C: - dt_dive->dc.model = strdup("Aladin Air"); - break; - case 0x1D: - dt_dive->dc.model = strdup("Spiro Monitor 2 plus"); - break; - case 0x1E: - dt_dive->dc.model = strdup("Aladin Sport"); - break; - case 0x1F: - dt_dive->dc.model = strdup("Aladin Pro"); - break; - case 0x34: - dt_dive->dc.model = strdup("Aladin Air X"); - break; - case 0x3D: - dt_dive->dc.model = strdup("Spiro Monitor 2 plus"); - break; - case 0x3F: - dt_dive->dc.model = strdup("Mares Genius"); - break; - case 0x44: - dt_dive->dc.model = strdup("Aladin Air X"); - break; - case 0x48: - dt_dive->dc.model = strdup("Spiro Monitor 3 Air"); - break; - case 0xA4: - dt_dive->dc.model = strdup("Aladin Air X O2"); - break; - case 0xB1: - dt_dive->dc.model = strdup("Citizen Hyper Aqualand"); - break; - case 0xB2: - dt_dive->dc.model = strdup("Citizen ProMaster"); - break; - case 0xB3: - dt_dive->dc.model = strdup("Mares Guardian"); - break; - case 0xBC: - dt_dive->dc.model = strdup("Aladin Air X Nitrox"); - break; - case 0xF4: - dt_dive->dc.model = strdup("Aladin Air X Nitrox"); - break; - case 0xFF: - dt_dive->dc.model = strdup("Aladin Pro Nitrox"); - break; - default: - dt_dive->dc.model = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Unknown")); - break; - } - if ((tmp_1byte & 0xF0) == 0xF0) - is_nitrox = 1; - if ((tmp_1byte & 0xF0) == 0xA0) - is_O2 = 1; - - /* - * Air usage, unknown use. Probably allows or deny manually entering gas - * comsumption based on dc model - Useless for Subsurface - */ - read_bytes(1); - if (fseek(archivo, 6, 1) != 0) // jump over 6 bytes whitout known use - goto bail; - /* - * Profile data length - */ - read_bytes(2); - profile_length = tmp_2bytes; - if (profile_length != 0) { - /* - * 8 x 2 bytes for the tissues saturation useless for subsurface - * and other 6 bytes without known use - */ - if (fseek(archivo, 22, 1) != 0) - goto bail; - if (is_nitrox || is_O2) { - - /* - * CNS % (unsure) values table (only nitrox computers) - */ - read_bytes(1); - - /* - * % O2 in nitrox mix - (only nitrox and O2 computers but differents) - */ - read_bytes(1); - if (is_nitrox) { - dt_dive->cylinder[0].gasmix.o2.permille = - (tmp_1byte & 0x0F ? 20.0 + 2 * (tmp_1byte & 0x0F) : 21.0) * 10; - } else { - dt_dive->cylinder[0].gasmix.o2.permille = tmp_1byte * 10; - read_bytes(1) // Jump over one byte, unknown use - } - } - /* - * profileLength = Nº bytes, need to know how many samples are there. - * 2bytes per sample plus another one each three samples. Also includes the - * bytes jumped over (22) and the nitrox (2) or O2 (3). - */ - int samplenum = is_O2 ? (profile_length - 25) * 3 / 8 : (profile_length - 24) * 3 / 7; - - dc->events = calloc(samplenum, sizeof(struct event)); - dc->alloc_samples = samplenum; - dc->samples = 0; - dc->sample = calloc(samplenum, sizeof(struct sample)); - - dtrak_profile(dt_dive, archivo); - } - /* - * Initialize some dive data not supported by Datatrak/WLog - */ - if (!strcmp(dt_dive->dc.model, "Manually entered dive")) - dt_dive->dc.deviceid = 0; - else - dt_dive->dc.deviceid = 0xffffffff; - create_device_node(dt_dive->dc.model, dt_dive->dc.deviceid, "", "", dt_dive->dc.model); - dt_dive->dc.next = NULL; - if (!is_SCR && dt_dive->cylinder[0].type.size.mliter) { - dt_dive->cylinder[0].end.mbar = dt_dive->cylinder[0].start.mbar - - ((dt_dive->cylinder[0].gas_used.mliter / dt_dive->cylinder[0].type.size.mliter) * 1000); - } - return true; - -bail: - return false; -} - -void datatrak_import(const char *file, struct dive_table *table) -{ - FILE *archivo; - dtrakheader *fileheader = (dtrakheader *)malloc(sizeof(dtrakheader)); - int i = 0; - - if ((archivo = subsurface_fopen(file, "rb")) == NULL) { - report_error(translate("gettextFromC", "Error: couldn't open the file %s"), file); - free(fileheader); - return; - } - - /* - * Verify fileheader, get number of dives in datatrak divelog - */ - *fileheader = read_file_header(archivo); - while (i < fileheader->divesNum) { - struct dive *ptdive = alloc_dive(); - - if (!dt_dive_parser(archivo, ptdive)) { - report_error(translate("gettextFromC", "Error: no dive")); - free(ptdive); - } else { - record_dive(ptdive); - } - i++; - } - taglist_cleanup(&g_tag_list); - fclose(archivo); - sort_table(table); - free(fileheader); -} diff --git a/datatrak.h b/datatrak.h deleted file mode 100644 index 3a37e0465..000000000 --- a/datatrak.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef DATATRAK_HEADER_H -#define DATATRAK_HEADER_H - -#include - -typedef struct dtrakheader_ { - int header; //Must be 0xA100; - int divesNum; - int dc_serial_1; - int dc_serial_2; -} dtrakheader; - -#define read_bytes(_n) \ - switch (_n) { \ - case 1: \ - if (fread (&lector_bytes, sizeof(char), _n, archivo) != _n) \ - goto bail; \ - tmp_1byte = lector_bytes[0]; \ - break; \ - case 2: \ - if (fread (&lector_bytes, sizeof(char), _n, archivo) != _n) \ - goto bail; \ - tmp_2bytes = two_bytes_to_int (lector_bytes[1], lector_bytes[0]); \ - break; \ - default: \ - if (fread (&lector_word, sizeof(char), _n, archivo) != _n) \ - goto bail; \ - tmp_4bytes = four_bytes_to_long(lector_word[3], lector_word[2], lector_word[1], lector_word[0]); \ - break; \ - } - -#define read_string(_property) \ - unsigned char *_property##tmp = (unsigned char *)calloc(tmp_1byte + 1, 1); \ - if (fread((char *)_property##tmp, 1, tmp_1byte, archivo) != tmp_1byte) { \ - free(_property##tmp); \ - goto bail; \ - } \ - _property = (unsigned char *)strcat(to_utf8(_property##tmp), ""); \ - free(_property##tmp); - -#endif // DATATRAK_HEADER_H diff --git a/deco.c b/deco.c deleted file mode 100644 index 86acc0351..000000000 --- a/deco.c +++ /dev/null @@ -1,600 +0,0 @@ -/* calculate deco values - * based on Bühlmann ZHL-16b - * based on an implemention by heinrichs weikamp for the DR5 - * the original file was given to Subsurface under the GPLv2 - * by Matthias Heinrichs - * - * The implementation below is a fairly complete rewrite since then - * (C) Robert C. Helling 2013 and released under the GPLv2 - * - * add_segment() - add at the given pressure, breathing gasmix - * deco_allowed_depth() - ceiling based on lead tissue, surface pressure, 3m increments or smooth - * set_gf() - set Buehlmann gradient factors - * clear_deco() - * cache_deco_state() - * restore_deco_state() - * dump_tissues() - */ -#include -#include -#include "dive.h" -#include -#include - -#define cube(x) (x * x * x) - -// Subsurface appears to produce marginally less conservative plans than our benchmarks -// Introduce 1.2% additional conservatism -#define subsurface_conservatism_factor 1.012 - - -extern bool in_planner(); - -extern pressure_t first_ceiling_pressure; - -//! Option structure for Buehlmann decompression. -struct buehlmann_config { - double satmult; //! safety at inert gas accumulation as percentage of effect (more than 100). - double desatmult; //! safety at inert gas depletion as percentage of effect (less than 100). - unsigned int last_deco_stop_in_mtr; //! depth of last_deco_stop. - double gf_high; //! gradient factor high (at surface). - double gf_low; //! gradient factor low (at bottom/start of deco calculation). - double gf_low_position_min; //! gf_low_position below surface_min_shallow. - bool gf_low_at_maxdepth; //! if true, gf_low applies at max depth instead of at deepest ceiling. -}; - -struct buehlmann_config buehlmann_config = { - .satmult = 1.0, - .desatmult = 1.01, - .last_deco_stop_in_mtr = 0, - .gf_high = 0.75, - .gf_low = 0.35, - .gf_low_position_min = 1.0, - .gf_low_at_maxdepth = false -}; - -//! Option structure for VPM-B decompression. -struct vpmb_config { - double crit_radius_N2; //! Critical radius of N2 nucleon (microns). - double crit_radius_He; //! Critical radius of He nucleon (microns). - double crit_volume_lambda; //! Constant corresponding to critical gas volume (bar * min). - double gradient_of_imperm; //! Gradient after which bubbles become impermeable (bar). - double surface_tension_gamma; //! Nucleons surface tension constant (N / bar = m2). - double skin_compression_gammaC; //! Skin compression gammaC (N / bar = m2). - double regeneration_time; //! Time needed for the bubble to regenerate to the start radius (min). - double other_gases_pressure; //! Always present pressure of other gasses in tissues (bar). -}; - -struct vpmb_config vpmb_config = { - .crit_radius_N2 = 0.55, - .crit_radius_He = 0.45, - .crit_volume_lambda = 199.58, - .gradient_of_imperm = 8.30865, // = 8.2 atm - .surface_tension_gamma = 0.18137175, // = 0.0179 N/msw - .skin_compression_gammaC = 2.6040525, // = 0.257 N/msw - .regeneration_time = 20160.0, - .other_gases_pressure = 0.1359888 -}; - -const double buehlmann_N2_a[] = { 1.1696, 1.0, 0.8618, 0.7562, - 0.62, 0.5043, 0.441, 0.4, - 0.375, 0.35, 0.3295, 0.3065, - 0.2835, 0.261, 0.248, 0.2327 }; - -const double buehlmann_N2_b[] = { 0.5578, 0.6514, 0.7222, 0.7825, - 0.8126, 0.8434, 0.8693, 0.8910, - 0.9092, 0.9222, 0.9319, 0.9403, - 0.9477, 0.9544, 0.9602, 0.9653 }; - -const double buehlmann_N2_t_halflife[] = { 5.0, 8.0, 12.5, 18.5, - 27.0, 38.3, 54.3, 77.0, - 109.0, 146.0, 187.0, 239.0, - 305.0, 390.0, 498.0, 635.0 }; - -const double buehlmann_N2_factor_expositon_one_second[] = { - 2.30782347297664E-003, 1.44301447809736E-003, 9.23769302935806E-004, 6.24261986779007E-004, - 4.27777107246730E-004, 3.01585140931371E-004, 2.12729727268379E-004, 1.50020603047807E-004, - 1.05980191127841E-004, 7.91232600646508E-005, 6.17759153688224E-005, 4.83354552742732E-005, - 3.78761777920511E-005, 2.96212356654113E-005, 2.31974277413727E-005, 1.81926738960225E-005 -}; - -const double buehlmann_He_a[] = { 1.6189, 1.383, 1.1919, 1.0458, - 0.922, 0.8205, 0.7305, 0.6502, - 0.595, 0.5545, 0.5333, 0.5189, - 0.5181, 0.5176, 0.5172, 0.5119 }; - -const double buehlmann_He_b[] = { 0.4770, 0.5747, 0.6527, 0.7223, - 0.7582, 0.7957, 0.8279, 0.8553, - 0.8757, 0.8903, 0.8997, 0.9073, - 0.9122, 0.9171, 0.9217, 0.9267 }; - -const double buehlmann_He_t_halflife[] = { 1.88, 3.02, 4.72, 6.99, - 10.21, 14.48, 20.53, 29.11, - 41.20, 55.19, 70.69, 90.34, - 115.29, 147.42, 188.24, 240.03 }; - -const double buehlmann_He_factor_expositon_one_second[] = { - 6.12608039419837E-003, 3.81800836683133E-003, 2.44456078654209E-003, 1.65134647076792E-003, - 1.13084424730725E-003, 7.97503165599123E-004, 5.62552521860549E-004, 3.96776399429366E-004, - 2.80360036664540E-004, 2.09299583354805E-004, 1.63410794820518E-004, 1.27869320250551E-004, - 1.00198406028040E-004, 7.83611475491108E-005, 6.13689891868496E-005, 4.81280465299827E-005 -}; - -const double conservatism_lvls[] = { 1.0, 1.05, 1.12, 1.22, 1.35 }; - -/* Inspired gas loading equations depend on the partial pressure of inert gas in the alveolar. - * P_alv = (P_amb - P_H2O + (1 - Rq) / Rq * P_CO2) * f - * where: - * P_alv alveolar partial pressure of inert gas - * P_amb ambient pressure - * P_H2O water vapour partial pressure = ~0.0627 bar - * P_CO2 carbon dioxide partial pressure = ~0.0534 bar - * Rq respiratory quotient (O2 consumption / CO2 production) - * f fraction of inert gas - * - * In our calculations, we simplify this to use an effective water vapour pressure - * WV = P_H20 - (1 - Rq) / Rq * P_CO2 - * - * Buhlmann ignored the contribution of CO2 (i.e. Rq = 1.0), whereas Schreiner adopted Rq = 0.8. - * WV_Buhlmann = PP_H2O = 0.0627 bar - * WV_Schreiner = 0.0627 - (1 - 0.8) / Rq * 0.0534 = 0.0493 bar - - * Buhlmann calculations use the Buhlmann value, VPM-B calculations use the Schreiner value. -*/ -#define WV_PRESSURE 0.0627 // water vapor pressure in bar, based on respiratory quotient Rq = 1.0 (Buhlmann value) -#define WV_PRESSURE_SCHREINER 0.0493 // water vapor pressure in bar, based on respiratory quotient Rq = 0.8 (Schreiner value) - -#define DECO_STOPS_MULTIPLIER_MM 3000.0 -#define NITROGEN_FRACTION 0.79 - -double tissue_n2_sat[16]; -double tissue_he_sat[16]; -int ci_pointing_to_guiding_tissue; -double gf_low_pressure_this_dive; -#define TISSUE_ARRAY_SZ sizeof(tissue_n2_sat) - -double tolerated_by_tissue[16]; -double tissue_inertgas_saturation[16]; -double buehlmann_inertgas_a[16], buehlmann_inertgas_b[16]; - -double max_n2_crushing_pressure[16]; -double max_he_crushing_pressure[16]; - -double crushing_onset_tension[16]; // total inert gas tension in the t* moment -double n2_regen_radius[16]; // rs -double he_regen_radius[16]; -double max_ambient_pressure; // last moment we were descending - -double bottom_n2_gradient[16]; -double bottom_he_gradient[16]; - -double initial_n2_gradient[16]; -double initial_he_gradient[16]; - -double get_crit_radius_He() -{ - if (prefs.conservatism_level <= 4) - return vpmb_config.crit_radius_He * conservatism_lvls[prefs.conservatism_level] * subsurface_conservatism_factor; - return vpmb_config.crit_radius_He; -} - -double get_crit_radius_N2() -{ - if (prefs.conservatism_level <= 4) - return vpmb_config.crit_radius_N2 * conservatism_lvls[prefs.conservatism_level] * subsurface_conservatism_factor; - return vpmb_config.crit_radius_N2; -} - -// Solve another cubic equation, this time -// x^3 - B x - C == 0 -// Use trigonometric formula for negative discriminants (see Wikipedia for details) - -double solve_cubic2(double B, double C) -{ - double discriminant = 27 * C * C - 4 * cube(B); - if (discriminant < 0.0) { - return 2.0 * sqrt(B / 3.0) * cos(acos(3.0 * C * sqrt(3.0 / B) / (2.0 * B)) / 3.0); - } - - double denominator = pow(9 * C + sqrt(3 * discriminant), 1 / 3.0); - - return pow(2.0 / 3.0, 1.0 / 3.0) * B / denominator + denominator / pow(18.0, 1.0 / 3.0); -} - -// This is a simplified formula avoiding radii. It uses the fact that Boyle's law says -// pV = (G + P_amb) / G^3 is constant to solve for the new gradient G. - -double update_gradient(double next_stop_pressure, double first_gradient) -{ - double B = cube(first_gradient) / (first_ceiling_pressure.mbar / 1000.0 + first_gradient); - double C = next_stop_pressure * B; - - double new_gradient = solve_cubic2(B, C); - - if (new_gradient < 0.0) - report_error("Negative gradient encountered!"); - return new_gradient; -} - -double vpmb_tolerated_ambient_pressure(double reference_pressure, int ci) -{ - double n2_gradient, he_gradient, total_gradient; - - if (reference_pressure >= first_ceiling_pressure.mbar / 1000.0 || !first_ceiling_pressure.mbar) { - n2_gradient = bottom_n2_gradient[ci]; - he_gradient = bottom_he_gradient[ci]; - } else { - n2_gradient = update_gradient(reference_pressure, bottom_n2_gradient[ci]); - he_gradient = update_gradient(reference_pressure, bottom_he_gradient[ci]); - } - - total_gradient = ((n2_gradient * tissue_n2_sat[ci]) + (he_gradient * tissue_he_sat[ci])) / (tissue_n2_sat[ci] + tissue_he_sat[ci]); - - return tissue_n2_sat[ci] + tissue_he_sat[ci] + vpmb_config.other_gases_pressure - total_gradient; -} - - -double tissue_tolerance_calc(const struct dive *dive, double pressure) -{ - int ci = -1; - double ret_tolerance_limit_ambient_pressure = 0.0; - double gf_high = buehlmann_config.gf_high; - double gf_low = buehlmann_config.gf_low; - double surface = get_surface_pressure_in_mbar(dive, true) / 1000.0; - double lowest_ceiling = 0.0; - double tissue_lowest_ceiling[16]; - - if (prefs.deco_mode != VPMB || !in_planner()) { - for (ci = 0; ci < 16; ci++) { - tissue_inertgas_saturation[ci] = tissue_n2_sat[ci] + tissue_he_sat[ci]; - buehlmann_inertgas_a[ci] = ((buehlmann_N2_a[ci] * tissue_n2_sat[ci]) + (buehlmann_He_a[ci] * tissue_he_sat[ci])) / tissue_inertgas_saturation[ci]; - buehlmann_inertgas_b[ci] = ((buehlmann_N2_b[ci] * tissue_n2_sat[ci]) + (buehlmann_He_b[ci] * tissue_he_sat[ci])) / tissue_inertgas_saturation[ci]; - - - /* tolerated = (tissue_inertgas_saturation - buehlmann_inertgas_a) * buehlmann_inertgas_b; */ - - tissue_lowest_ceiling[ci] = (buehlmann_inertgas_b[ci] * tissue_inertgas_saturation[ci] - gf_low * buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci]) / - ((1.0 - buehlmann_inertgas_b[ci]) * gf_low + buehlmann_inertgas_b[ci]); - if (tissue_lowest_ceiling[ci] > lowest_ceiling) - lowest_ceiling = tissue_lowest_ceiling[ci]; - if (!buehlmann_config.gf_low_at_maxdepth) { - if (lowest_ceiling > gf_low_pressure_this_dive) - gf_low_pressure_this_dive = lowest_ceiling; - } - } - for (ci = 0; ci < 16; ci++) { - double tolerated; - - if ((surface / buehlmann_inertgas_b[ci] + buehlmann_inertgas_a[ci] - surface) * gf_high + surface < - (gf_low_pressure_this_dive / buehlmann_inertgas_b[ci] + buehlmann_inertgas_a[ci] - gf_low_pressure_this_dive) * gf_low + gf_low_pressure_this_dive) - tolerated = (-buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci] * (gf_high * gf_low_pressure_this_dive - gf_low * surface) - - (1.0 - buehlmann_inertgas_b[ci]) * (gf_high - gf_low) * gf_low_pressure_this_dive * surface + - buehlmann_inertgas_b[ci] * (gf_low_pressure_this_dive - surface) * tissue_inertgas_saturation[ci]) / - (-buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci] * (gf_high - gf_low) + - (1.0 - buehlmann_inertgas_b[ci]) * (gf_low * gf_low_pressure_this_dive - gf_high * surface) + - buehlmann_inertgas_b[ci] * (gf_low_pressure_this_dive - surface)); - else - tolerated = ret_tolerance_limit_ambient_pressure; - - - tolerated_by_tissue[ci] = tolerated; - - if (tolerated >= ret_tolerance_limit_ambient_pressure) { - ci_pointing_to_guiding_tissue = ci; - ret_tolerance_limit_ambient_pressure = tolerated; - } - } - } else { - // VPM-B ceiling - double reference_pressure; - - ret_tolerance_limit_ambient_pressure = pressure; - // The Boyle compensated gradient depends on ambient pressure. For the ceiling, this should set the ambient pressure. - do { - reference_pressure = ret_tolerance_limit_ambient_pressure; - ret_tolerance_limit_ambient_pressure = 0.0; - for (ci = 0; ci < 16; ci++) { - double tolerated = vpmb_tolerated_ambient_pressure(reference_pressure, ci); - if (tolerated >= ret_tolerance_limit_ambient_pressure) { - ci_pointing_to_guiding_tissue = ci; - ret_tolerance_limit_ambient_pressure = tolerated; - } - tolerated_by_tissue[ci] = tolerated; - } - // We are doing ok if the gradient was computed within ten centimeters of the ceiling. - } while (fabs(ret_tolerance_limit_ambient_pressure - reference_pressure) > 0.01); - } - return ret_tolerance_limit_ambient_pressure; -} - -/* - * Return buelman factor for a particular period and tissue index. - * - * We cache the last factor, since we commonly call this with the - * same values... We have a special "fixed cache" for the one second - * case, although I wonder if that's even worth it considering the - * more general-purpose cache. - */ -struct factor_cache { - int last_period; - double last_factor; -}; - -double n2_factor(int period_in_seconds, int ci) -{ - static struct factor_cache cache[16]; - - if (period_in_seconds == 1) - return buehlmann_N2_factor_expositon_one_second[ci]; - - if (period_in_seconds != cache[ci].last_period) { - cache[ci].last_period = period_in_seconds; - cache[ci].last_factor = 1 - pow(2.0, -period_in_seconds / (buehlmann_N2_t_halflife[ci] * 60)); - } - - return cache[ci].last_factor; -} - -double he_factor(int period_in_seconds, int ci) -{ - static struct factor_cache cache[16]; - - if (period_in_seconds == 1) - return buehlmann_He_factor_expositon_one_second[ci]; - - if (period_in_seconds != cache[ci].last_period) { - cache[ci].last_period = period_in_seconds; - cache[ci].last_factor = 1 - pow(2.0, -period_in_seconds / (buehlmann_He_t_halflife[ci] * 60)); - } - - return cache[ci].last_factor; -} - -double calc_surface_phase(double surface_pressure, double he_pressure, double n2_pressure, double he_time_constant, double n2_time_constant) -{ - double inspired_n2 = (surface_pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE)) * NITROGEN_FRACTION; - - if (n2_pressure > inspired_n2) - return (he_pressure / he_time_constant + (n2_pressure - inspired_n2) / n2_time_constant) / (he_pressure + n2_pressure - inspired_n2); - - if (he_pressure + n2_pressure >= inspired_n2){ - double gradient_decay_time = 1.0 / (n2_time_constant - he_time_constant) * log ((inspired_n2 - n2_pressure) / he_pressure); - double gradients_integral = he_pressure / he_time_constant * (1.0 - exp(-he_time_constant * gradient_decay_time)) + (n2_pressure - inspired_n2) / n2_time_constant * (1.0 - exp(-n2_time_constant * gradient_decay_time)); - return gradients_integral / (he_pressure + n2_pressure - inspired_n2); - } - - return 0; -} - -void vpmb_start_gradient() -{ - int ci; - - for (ci = 0; ci < 16; ++ci) { - initial_n2_gradient[ci] = bottom_n2_gradient[ci] = 2.0 * (vpmb_config.surface_tension_gamma / vpmb_config.skin_compression_gammaC) * ((vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma) / n2_regen_radius[ci]); - initial_he_gradient[ci] = bottom_he_gradient[ci] = 2.0 * (vpmb_config.surface_tension_gamma / vpmb_config.skin_compression_gammaC) * ((vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma) / he_regen_radius[ci]); - } -} - -void vpmb_next_gradient(double deco_time, double surface_pressure) -{ - int ci; - double n2_b, n2_c; - double he_b, he_c; - double desat_time; - deco_time /= 60.0; - - for (ci = 0; ci < 16; ++ci) { - desat_time = deco_time + calc_surface_phase(surface_pressure, tissue_he_sat[ci], tissue_n2_sat[ci], log(2.0) / buehlmann_He_t_halflife[ci], log(2.0) / buehlmann_N2_t_halflife[ci]); - - n2_b = initial_n2_gradient[ci] + (vpmb_config.crit_volume_lambda * vpmb_config.surface_tension_gamma) / (vpmb_config.skin_compression_gammaC * desat_time); - he_b = initial_he_gradient[ci] + (vpmb_config.crit_volume_lambda * vpmb_config.surface_tension_gamma) / (vpmb_config.skin_compression_gammaC * desat_time); - - n2_c = vpmb_config.surface_tension_gamma * vpmb_config.surface_tension_gamma * vpmb_config.crit_volume_lambda * max_n2_crushing_pressure[ci]; - n2_c = n2_c / (vpmb_config.skin_compression_gammaC * vpmb_config.skin_compression_gammaC * desat_time); - he_c = vpmb_config.surface_tension_gamma * vpmb_config.surface_tension_gamma * vpmb_config.crit_volume_lambda * max_he_crushing_pressure[ci]; - he_c = he_c / (vpmb_config.skin_compression_gammaC * vpmb_config.skin_compression_gammaC * desat_time); - - bottom_n2_gradient[ci] = 0.5 * ( n2_b + sqrt(n2_b * n2_b - 4.0 * n2_c)); - bottom_he_gradient[ci] = 0.5 * ( he_b + sqrt(he_b * he_b - 4.0 * he_c)); - } -} - -// A*r^3 - B*r^2 - C == 0 -// Solved with the help of mathematica - -double solve_cubic(double A, double B, double C) -{ - double BA = B/A; - double CA = C/A; - - double discriminant = CA * (4 * cube(BA) + 27 * CA); - - // Let's make sure we have a real solution: - if (discriminant < 0.0) { - // This should better not happen - report_error("Complex solution for inner pressure encountered!\n A=%f\tB=%f\tC=%f\n", A, B, C); - return 0.0; - } - double denominator = pow(cube(BA) + 1.5 * (9 * CA + sqrt(3.0) * sqrt(discriminant)), 1/3.0); - return (BA + BA * BA / denominator + denominator) / 3.0; - -} - - -void nuclear_regeneration(double time) -{ - time /= 60.0; - int ci; - double crushing_radius_N2, crushing_radius_He; - for (ci = 0; ci < 16; ++ci) { - //rm - crushing_radius_N2 = 1.0 / (max_n2_crushing_pressure[ci] / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / get_crit_radius_N2()); - crushing_radius_He = 1.0 / (max_he_crushing_pressure[ci] / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / get_crit_radius_He()); - //rs - n2_regen_radius[ci] = crushing_radius_N2 + (get_crit_radius_N2() - crushing_radius_N2) * (1.0 - exp (-time / vpmb_config.regeneration_time)); - he_regen_radius[ci] = crushing_radius_He + (get_crit_radius_He() - crushing_radius_He) * (1.0 - exp (-time / vpmb_config.regeneration_time)); - } -} - - -// Calculates the nucleons inner pressure during the impermeable period -double calc_inner_pressure(double crit_radius, double onset_tension, double current_ambient_pressure) -{ - double onset_radius = 1.0 / (vpmb_config.gradient_of_imperm / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / crit_radius); - - - double A = current_ambient_pressure - vpmb_config.gradient_of_imperm + (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) / onset_radius; - double B = 2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma); - double C = onset_tension * pow(onset_radius, 3); - - double current_radius = solve_cubic(A, B, C); - - return onset_tension * onset_radius * onset_radius * onset_radius / (current_radius * current_radius * current_radius); -} - -// Calculates the crushing pressure in the given moment. Updates crushing_onset_tension and critical radius if needed -void calc_crushing_pressure(double pressure) -{ - int ci; - double gradient; - double gas_tension; - double n2_crushing_pressure, he_crushing_pressure; - double n2_inner_pressure, he_inner_pressure; - - for (ci = 0; ci < 16; ++ci) { - gas_tension = tissue_n2_sat[ci] + tissue_he_sat[ci] + vpmb_config.other_gases_pressure; - gradient = pressure - gas_tension; - - if (gradient <= vpmb_config.gradient_of_imperm) { // permeable situation - n2_crushing_pressure = he_crushing_pressure = gradient; - crushing_onset_tension[ci] = gas_tension; - } - else { // impermeable - if (max_ambient_pressure >= pressure) - return; - - n2_inner_pressure = calc_inner_pressure(get_crit_radius_N2(), crushing_onset_tension[ci], pressure); - he_inner_pressure = calc_inner_pressure(get_crit_radius_He(), crushing_onset_tension[ci], pressure); - - n2_crushing_pressure = pressure - n2_inner_pressure; - he_crushing_pressure = pressure - he_inner_pressure; - } - max_n2_crushing_pressure[ci] = MAX(max_n2_crushing_pressure[ci], n2_crushing_pressure); - max_he_crushing_pressure[ci] = MAX(max_he_crushing_pressure[ci], he_crushing_pressure); - } - max_ambient_pressure = MAX(pressure, max_ambient_pressure); -} - -/* add period_in_seconds at the given pressure and gas to the deco calculation */ -void add_segment(double pressure, const struct gasmix *gasmix, int period_in_seconds, int ccpo2, const struct dive *dive, int sac) -{ - int ci; - struct gas_pressures pressures; - - fill_pressures(&pressures, pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE), - gasmix, (double) ccpo2 / 1000.0, dive->dc.divemode); - - if (buehlmann_config.gf_low_at_maxdepth && pressure > gf_low_pressure_this_dive) - gf_low_pressure_this_dive = pressure; - - for (ci = 0; ci < 16; ci++) { - double pn2_oversat = pressures.n2 - tissue_n2_sat[ci]; - double phe_oversat = pressures.he - tissue_he_sat[ci]; - double n2_f = n2_factor(period_in_seconds, ci); - double he_f = he_factor(period_in_seconds, ci); - double n2_satmult = pn2_oversat > 0 ? buehlmann_config.satmult : buehlmann_config.desatmult; - double he_satmult = phe_oversat > 0 ? buehlmann_config.satmult : buehlmann_config.desatmult; - - tissue_n2_sat[ci] += n2_satmult * pn2_oversat * n2_f; - tissue_he_sat[ci] += he_satmult * phe_oversat * he_f; - } - if(prefs.deco_mode == VPMB && in_planner()) - calc_crushing_pressure(pressure); - return; -} - -void dump_tissues() -{ - int ci; - printf("N2 tissues:"); - for (ci = 0; ci < 16; ci++) - printf(" %6.3e", tissue_n2_sat[ci]); - printf("\nHe tissues:"); - for (ci = 0; ci < 16; ci++) - printf(" %6.3e", tissue_he_sat[ci]); - printf("\n"); -} - -void clear_deco(double surface_pressure) -{ - int ci; - for (ci = 0; ci < 16; ci++) { - tissue_n2_sat[ci] = (surface_pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE)) * N2_IN_AIR / 1000; - tissue_he_sat[ci] = 0.0; - max_n2_crushing_pressure[ci] = 0.0; - max_he_crushing_pressure[ci] = 0.0; - n2_regen_radius[ci] = get_crit_radius_N2(); - he_regen_radius[ci] = get_crit_radius_He(); - } - gf_low_pressure_this_dive = surface_pressure; - if (!buehlmann_config.gf_low_at_maxdepth) - gf_low_pressure_this_dive += buehlmann_config.gf_low_position_min; - max_ambient_pressure = 0.0; -} - -void cache_deco_state(char **cached_datap) -{ - char *data = *cached_datap; - - if (!data) { - data = malloc(2 * TISSUE_ARRAY_SZ + sizeof(double) + sizeof(int)); - *cached_datap = data; - } - memcpy(data, tissue_n2_sat, TISSUE_ARRAY_SZ); - data += TISSUE_ARRAY_SZ; - memcpy(data, tissue_he_sat, TISSUE_ARRAY_SZ); - data += TISSUE_ARRAY_SZ; - memcpy(data, &gf_low_pressure_this_dive, sizeof(double)); - data += sizeof(double); - memcpy(data, &ci_pointing_to_guiding_tissue, sizeof(int)); -} - -void restore_deco_state(char *data) -{ - memcpy(tissue_n2_sat, data, TISSUE_ARRAY_SZ); - data += TISSUE_ARRAY_SZ; - memcpy(tissue_he_sat, data, TISSUE_ARRAY_SZ); - data += TISSUE_ARRAY_SZ; - memcpy(&gf_low_pressure_this_dive, data, sizeof(double)); - data += sizeof(double); - memcpy(&ci_pointing_to_guiding_tissue, data, sizeof(int)); -} - -unsigned int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth) -{ - unsigned int depth; - double pressure_delta; - - /* Avoid negative depths */ - pressure_delta = tissues_tolerance > surface_pressure ? tissues_tolerance - surface_pressure : 0.0; - - depth = rel_mbar_to_depth(pressure_delta * 1000, dive); - - if (!smooth) - depth = ceil(depth / DECO_STOPS_MULTIPLIER_MM) * DECO_STOPS_MULTIPLIER_MM; - - if (depth > 0 && depth < buehlmann_config.last_deco_stop_in_mtr * 1000) - depth = buehlmann_config.last_deco_stop_in_mtr * 1000; - - return depth; -} - -void set_gf(short gflow, short gfhigh, bool gf_low_at_maxdepth) -{ - if (gflow != -1) - buehlmann_config.gf_low = (double)gflow / 100.0; - if (gfhigh != -1) - buehlmann_config.gf_high = (double)gfhigh / 100.0; - buehlmann_config.gf_low_at_maxdepth = gf_low_at_maxdepth; -} diff --git a/deco.h b/deco.h deleted file mode 100644 index 08ff93422..000000000 --- a/deco.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef DECO_H -#define DECO_H - -#ifdef __cplusplus -extern "C" { -#endif - -extern double tolerated_by_tissue[]; -extern double buehlmann_N2_t_halflife[]; -extern double tissue_inertgas_saturation[16]; -extern double buehlmann_inertgas_a[16], buehlmann_inertgas_b[16]; -extern double gf_low_pressure_this_dive; - - -#ifdef __cplusplus -} -#endif - -#endif // DECO_H diff --git a/device.c b/device.c deleted file mode 100644 index c952c84be..000000000 --- a/device.c +++ /dev/null @@ -1,180 +0,0 @@ -#include -#include "dive.h" -#include "device.h" - -/* - * Good fake dive profiles are hard. - * - * "depthtime" is the integral of the dive depth over - * time ("area" of the dive profile). We want that - * area to match the average depth (avg_d*max_t). - * - * To do that, we generate a 6-point profile: - * - * (0, 0) - * (t1, max_d) - * (t2, max_d) - * (t3, d) - * (t4, d) - * (max_t, 0) - * - * with the same ascent/descent rates between the - * different depths. - * - * NOTE: avg_d, max_d and max_t are given constants. - * The rest we can/should play around with to get a - * good-looking profile. - * - * That six-point profile gives a total area of: - * - * (max_d*max_t) - (max_d*t1) - (max_d-d)*(t4-t3) - * - * And the "same ascent/descent rates" requirement - * gives us (time per depth must be same): - * - * t1 / max_d = (t3-t2) / (max_d-d) - * t1 / max_d = (max_t-t4) / d - * - * We also obviously require: - * - * 0 <= t1 <= t2 <= t3 <= t4 <= max_t - * - * Let us call 'd_frac = d / max_d', and we get: - * - * Total area must match average depth-time: - * - * (max_d*max_t) - (max_d*t1) - (max_d-d)*(t4-t3) = avg_d*max_t - * max_d*(max_t-t1-(1-d_frac)*(t4-t3)) = avg_d*max_t - * max_t-t1-(1-d_frac)*(t4-t3) = avg_d*max_t/max_d - * t1+(1-d_frac)*(t4-t3) = max_t*(1-avg_d/max_d) - * - * and descent slope must match ascent slopes: - * - * t1 / max_d = (t3-t2) / (max_d*(1-d_frac)) - * t1 = (t3-t2)/(1-d_frac) - * - * and - * - * t1 / max_d = (max_t-t4) / (max_d*d_frac) - * t1 = (max_t-t4)/d_frac - * - * In general, we have more free variables than we have constraints, - * but we can aim for certain basics, like a good ascent slope. - */ -static int fill_samples(struct sample *s, int max_d, int avg_d, int max_t, double slope, double d_frac) -{ - double t_frac = max_t * (1 - avg_d / (double)max_d); - int t1 = max_d / slope; - int t4 = max_t - t1 * d_frac; - int t3 = t4 - (t_frac - t1) / (1 - d_frac); - int t2 = t3 - t1 * (1 - d_frac); - - if (t1 < 0 || t1 > t2 || t2 > t3 || t3 > t4 || t4 > max_t) - return 0; - - s[1].time.seconds = t1; - s[1].depth.mm = max_d; - s[2].time.seconds = t2; - s[2].depth.mm = max_d; - s[3].time.seconds = t3; - s[3].depth.mm = max_d * d_frac; - s[4].time.seconds = t4; - s[4].depth.mm = max_d * d_frac; - - return 1; -} - -/* we have no average depth; instead of making up a random average depth - * we should assume either a PADI recrangular profile (for short and/or - * shallow dives) or more reasonably a six point profile with a 3 minute - * safety stop at 5m */ -static void fill_samples_no_avg(struct sample *s, int max_d, int max_t, double slope) -{ - // shallow or short dives are just trapecoids based on the given slope - if (max_d < 10000 || max_t < 600) { - s[1].time.seconds = max_d / slope; - s[1].depth.mm = max_d; - s[2].time.seconds = max_t - max_d / slope; - s[2].depth.mm = max_d; - } else { - s[1].time.seconds = max_d / slope; - s[1].depth.mm = max_d; - s[2].time.seconds = max_t - max_d / slope - 180; - s[2].depth.mm = max_d; - s[3].time.seconds = max_t - 5000 / slope - 180; - s[3].depth.mm = 5000; - s[4].time.seconds = max_t - 5000 / slope; - s[4].depth.mm = 5000; - } -} - -struct divecomputer *fake_dc(struct divecomputer *dc) -{ - static struct sample fake[6]; - static struct divecomputer fakedc; - - fakedc = (*dc); - fakedc.sample = fake; - fakedc.samples = 6; - - /* The dive has no samples, so create a few fake ones */ - int max_t = dc->duration.seconds; - int max_d = dc->maxdepth.mm; - int avg_d = dc->meandepth.mm; - - memset(fake, 0, sizeof(fake)); - fake[5].time.seconds = max_t; - if (!max_t || !max_d) - return &fakedc; - - /* - * We want to fake the profile so that the average - * depth ends up correct. However, in the absense of - * a reasonable average, let's just make something - * up. Note that 'avg_d == max_d' is _not_ a reasonable - * average. - * We explicitly treat avg_d == 0 differently */ - if (avg_d == 0) { - /* we try for a sane slope, but bow to the insanity of - * the user supplied data */ - fill_samples_no_avg(fake, max_d, max_t, MAX(2.0 * max_d / max_t, 5000.0 / 60)); - if (fake[3].time.seconds == 0) { // just a 4 point profile - fakedc.samples = 4; - fake[3].time.seconds = max_t; - } - return &fakedc; - } - if (avg_d < max_d / 10 || avg_d >= max_d) { - avg_d = (max_d + 10000) / 3; - if (avg_d > max_d) - avg_d = max_d * 2 / 3; - } - if (!avg_d) - avg_d = 1; - - /* - * Ok, first we try a basic profile with a specific ascent - * rate (5 meters per minute) and d_frac (1/3). - */ - if (fill_samples(fake, max_d, avg_d, max_t, 5000.0 / 60, 0.33)) - return &fakedc; - - /* - * Ok, assume that didn't work because we cannot make the - * average come out right because it was a quick deep dive - * followed by a much shallower region - */ - if (fill_samples(fake, max_d, avg_d, max_t, 10000.0 / 60, 0.10)) - return &fakedc; - - /* - * Uhhuh. That didn't work. We'd need to find a good combination that - * satisfies our constraints. Currently, we don't, we just give insane - * slopes. - */ - if (fill_samples(fake, max_d, avg_d, max_t, 10000.0, 0.01)) - return &fakedc; - - /* Even that didn't work? Give up, there's something wrong */ - return &fakedc; -} diff --git a/device.h b/device.h deleted file mode 100644 index 9ff2ce23a..000000000 --- a/device.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef DEVICE_H -#define DEVICE_H - -#ifdef __cplusplus -#include "dive.h" -extern "C" { -#endif - -extern struct divecomputer *fake_dc(struct divecomputer *dc); -extern void create_device_node(const char *model, uint32_t deviceid, const char *serial, const char *firmware, const char *nickname); -extern void call_for_each_dc(void *f, void (*callback)(void *, const char *, uint32_t, - const char *, const char *, const char *), bool select_only); - -#ifdef __cplusplus -} -#endif - -#endif // DEVICE_H diff --git a/devicedetails.cpp b/devicedetails.cpp deleted file mode 100644 index 1ac56375d..000000000 --- a/devicedetails.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "devicedetails.h" - -// This can probably be done better by someone with better c++-FU -const struct gas zero_gas = {0}; -const struct setpoint zero_setpoint = {0}; - -DeviceDetails::DeviceDetails(QObject *parent) : - QObject(parent), - data(0), - serialNo(""), - firmwareVersion(""), - customText(""), - model(""), - syncTime(false), - gas1(zero_gas), - gas2(zero_gas), - gas3(zero_gas), - gas4(zero_gas), - gas5(zero_gas), - dil1(zero_gas), - dil2(zero_gas), - dil3(zero_gas), - dil4(zero_gas), - dil5(zero_gas), - sp1(zero_setpoint), - sp2(zero_setpoint), - sp3(zero_setpoint), - sp4(zero_setpoint), - sp5(zero_setpoint), - setPointFallback(0), - ccrMode(0), - calibrationGas(0), - diveMode(0), - decoType(0), - ppO2Max(0), - ppO2Min(0), - futureTTS(0), - gfLow(0), - gfHigh(0), - aGFLow(0), - aGFHigh(0), - aGFSelectable(0), - saturation(0), - desaturation(0), - lastDeco(0), - brightness(0), - units(0), - samplingRate(0), - salinity(0), - diveModeColor(0), - language(0), - dateFormat(0), - compassGain(0), - pressureSensorOffset(0), - flipScreen(0), - safetyStop(0), - maxDepth(0), - totalTime(0), - numberOfDives(0), - altitude(0), - personalSafety(0), - timeFormat(0), - lightEnabled(false), - light(0), - alarmTimeEnabled(false), - alarmTime(0), - alarmDepthEnabled(false), - alarmDepth(0), - leftButtonSensitivity(0), - rightButtonSensitivity(0), - bottomGasConsumption(0), - decoGasConsumption(0), - modWarning(false), - dynamicAscendRate(false), - graphicalSpeedIndicator(false), - alwaysShowppO2(false) -{ -} diff --git a/devicedetails.h b/devicedetails.h deleted file mode 100644 index 1ed9914ef..000000000 --- a/devicedetails.h +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef DEVICEDETAILS_H -#define DEVICEDETAILS_H - -#include -#include -#include "libdivecomputer.h" - -struct gas { - unsigned char oxygen; - unsigned char helium; - unsigned char type; - unsigned char depth; -}; - -struct setpoint { - unsigned char sp; - unsigned char depth; -}; - -class DeviceDetails : public QObject -{ - Q_OBJECT -public: - explicit DeviceDetails(QObject *parent = 0); - - device_data_t *data; - QString serialNo; - QString firmwareVersion; - QString customText; - QString model; - bool syncTime; - gas gas1; - gas gas2; - gas gas3; - gas gas4; - gas gas5; - gas dil1; - gas dil2; - gas dil3; - gas dil4; - gas dil5; - setpoint sp1; - setpoint sp2; - setpoint sp3; - setpoint sp4; - setpoint sp5; - bool setPointFallback; - int ccrMode; - int calibrationGas; - int diveMode; - int decoType; - int ppO2Max; - int ppO2Min; - int futureTTS; - int gfLow; - int gfHigh; - int aGFLow; - int aGFHigh; - int aGFSelectable; - int saturation; - int desaturation; - int lastDeco; - int brightness; - int units; - int samplingRate; - int salinity; - int diveModeColor; - int language; - int dateFormat; - int compassGain; - int pressureSensorOffset; - bool flipScreen; - bool safetyStop; - int maxDepth; - int totalTime; - int numberOfDives; - int altitude; - int personalSafety; - int timeFormat; - bool lightEnabled; - int light; - bool alarmTimeEnabled; - int alarmTime; - bool alarmDepthEnabled; - int alarmDepth; - int leftButtonSensitivity; - int rightButtonSensitivity; - int bottomGasConsumption; - int decoGasConsumption; - bool modWarning; - bool dynamicAscendRate; - bool graphicalSpeedIndicator; - bool alwaysShowppO2; -}; - - -#endif // DEVICEDETAILS_H diff --git a/display.h b/display.h deleted file mode 100644 index 9e3e1d159..000000000 --- a/display.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef DISPLAY_H -#define DISPLAY_H - -#ifdef __cplusplus -extern "C" { -#endif - -struct membuffer; - -#define SCALE_SCREEN 1.0 -#define SCALE_PRINT (1.0 / get_screen_dpi()) - -extern double get_screen_dpi(void); - -/* Plot info with smoothing, velocity indication - * and one-, two- and three-minute minimums and maximums */ -struct plot_info { - int nr; - int maxtime; - int meandepth, maxdepth; - int minpressure, maxpressure; - int minhr, maxhr; - int mintemp, maxtemp; - enum {AIR, NITROX, TRIMIX, FREEDIVING} dive_type; - double endtempcoord; - double maxpp; - bool has_ndl; - struct plot_data *entry; -}; - -typedef enum { - SC_SCREEN, - SC_PRINT -} scale_mode_t; - -extern struct divecomputer *select_dc(struct dive *); - -extern unsigned int dc_number; - -extern unsigned int amount_selected; - -extern int is_default_dive_computer_device(const char *); -extern int is_default_dive_computer(const char *, const char *); - -typedef void (*device_callback_t)(const char *name, void *userdata); - -#define DC_TYPE_SERIAL 1 -#define DC_TYPE_UEMIS 2 -#define DC_TYPE_OTHER 3 - -int enumerate_devices(device_callback_t callback, void *userdata, int dc_type); - -extern const char *default_dive_computer_vendor; -extern const char *default_dive_computer_product; -extern const char *default_dive_computer_device; -extern int default_dive_computer_download_mode; -#define AMB_PERCENTAGE 50.0 - -#ifdef __cplusplus -} -#endif - -#endif // DISPLAY_H diff --git a/dive.c b/dive.c deleted file mode 100644 index 2ae84ca1e..000000000 --- a/dive.c +++ /dev/null @@ -1,3465 +0,0 @@ -/* dive.c */ -/* maintains the internal dive list structure */ -#include -#include -#include -#include -#include "gettext.h" -#include "dive.h" -#include "libdivecomputer.h" -#include "device.h" -#include "divelist.h" -#include "qthelperfromc.h" - -/* one could argue about the best place to have this variable - - * it's used in the UI, but it seems to make the most sense to have it - * here */ -struct dive displayed_dive; -struct dive_site displayed_dive_site; - -struct tag_entry *g_tag_list = NULL; - -static const char *default_tags[] = { - QT_TRANSLATE_NOOP("gettextFromC", "boat"), QT_TRANSLATE_NOOP("gettextFromC", "shore"), QT_TRANSLATE_NOOP("gettextFromC", "drift"), - QT_TRANSLATE_NOOP("gettextFromC", "deep"), QT_TRANSLATE_NOOP("gettextFromC", "cavern"), QT_TRANSLATE_NOOP("gettextFromC", "ice"), - QT_TRANSLATE_NOOP("gettextFromC", "wreck"), QT_TRANSLATE_NOOP("gettextFromC", "cave"), QT_TRANSLATE_NOOP("gettextFromC", "altitude"), - QT_TRANSLATE_NOOP("gettextFromC", "pool"), QT_TRANSLATE_NOOP("gettextFromC", "lake"), QT_TRANSLATE_NOOP("gettextFromC", "river"), - QT_TRANSLATE_NOOP("gettextFromC", "night"), QT_TRANSLATE_NOOP("gettextFromC", "fresh"), QT_TRANSLATE_NOOP("gettextFromC", "student"), - QT_TRANSLATE_NOOP("gettextFromC", "instructor"), QT_TRANSLATE_NOOP("gettextFromC", "photo"), QT_TRANSLATE_NOOP("gettextFromC", "video"), - QT_TRANSLATE_NOOP("gettextFromC", "deco") -}; - -const char *cylinderuse_text[] = { - QT_TRANSLATE_NOOP("gettextFromC", "OC-gas"), QT_TRANSLATE_NOOP("gettextFromC", "diluent"), QT_TRANSLATE_NOOP("gettextFromC", "oxygen") -}; -const char *divemode_text[] = { "OC", "CCR", "PSCR", "Freedive" }; - -int event_is_gaschange(struct event *ev) -{ - return ev->type == SAMPLE_EVENT_GASCHANGE || - ev->type == SAMPLE_EVENT_GASCHANGE2; -} - -/* - * Does the gas mix data match the legacy - * libdivecomputer event format? If so, - * we can skip saving it, in order to maintain - * the old save formats. We'll re-generate the - * gas mix when loading. - */ -int event_gasmix_redundant(struct event *ev) -{ - int value = ev->value; - int o2, he; - - o2 = (value & 0xffff) * 10; - he = (value >> 16) * 10; - return o2 == ev->gas.mix.o2.permille && - he == ev->gas.mix.he.permille; -} - -struct event *add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name) -{ - int gas_index = -1; - struct event *ev, **p; - unsigned int size, len = strlen(name); - - size = sizeof(*ev) + len + 1; - ev = malloc(size); - if (!ev) - return NULL; - memset(ev, 0, size); - memcpy(ev->name, name, len); - ev->time.seconds = time; - ev->type = type; - ev->flags = flags; - ev->value = value; - - /* - * Expand the events into a sane format. Currently - * just gas switches - */ - switch (type) { - case SAMPLE_EVENT_GASCHANGE2: - /* High 16 bits are He percentage */ - ev->gas.mix.he.permille = (value >> 16) * 10; - - /* Extension to the GASCHANGE2 format: cylinder index in 'flags' */ - if (flags > 0 && flags <= MAX_CYLINDERS) - gas_index = flags-1; - /* Fallthrough */ - case SAMPLE_EVENT_GASCHANGE: - /* Low 16 bits are O2 percentage */ - ev->gas.mix.o2.permille = (value & 0xffff) * 10; - ev->gas.index = gas_index; - break; - } - - p = &dc->events; - - /* insert in the sorted list of events */ - while (*p && (*p)->time.seconds <= time) - p = &(*p)->next; - ev->next = *p; - *p = ev; - remember_event(name); - return ev; -} - -static int same_event(struct event *a, struct event *b) -{ - if (a->time.seconds != b->time.seconds) - return 0; - if (a->type != b->type) - return 0; - if (a->flags != b->flags) - return 0; - if (a->value != b->value) - return 0; - return !strcmp(a->name, b->name); -} - -void remove_event(struct event *event) -{ - struct event **ep = ¤t_dc->events; - while (ep && !same_event(*ep, event)) - ep = &(*ep)->next; - if (ep) { - /* we can't link directly with event->next - * because 'event' can be a copy from another - * dive (for instance the displayed_dive - * that we use on the interface to show things). */ - struct event *temp = (*ep)->next; - free(*ep); - *ep = temp; - } -} - -/* since the name is an array as part of the structure (how silly is that?) we - * have to actually remove the existing event and replace it with a new one. - * WARNING, WARNING... this may end up freeing event in case that event is indeed - * WARNING, WARNING... part of this divecomputer on this dive! */ -void update_event_name(struct dive *d, struct event *event, char *name) -{ - if (!d || !event) - return; - struct divecomputer *dc = get_dive_dc(d, dc_number); - if (!dc) - return; - struct event **removep = &dc->events; - struct event *remove; - while ((*removep)->next && !same_event(*removep, event)) - removep = &(*removep)->next; - if (!same_event(*removep, event)) - return; - remove = *removep; - *removep = (*removep)->next; - add_event(dc, event->time.seconds, event->type, event->flags, event->value, name); - free(remove); -} - -void add_extra_data(struct divecomputer *dc, const char *key, const char *value) -{ - struct extra_data **ed = &dc->extra_data; - - while (*ed) - ed = &(*ed)->next; - *ed = malloc(sizeof(struct extra_data)); - if (*ed) { - (*ed)->key = strdup(key); - (*ed)->value = strdup(value); - (*ed)->next = NULL; - } -} - -/* this returns a pointer to static variable - so use it right away after calling */ -struct gasmix *get_gasmix_from_event(struct event *ev) -{ - static struct gasmix dummy; - if (ev && event_is_gaschange(ev)) - return &ev->gas.mix; - - return &dummy; -} - -int get_pressure_units(int mb, const char **units) -{ - int pressure; - const char *unit; - struct units *units_p = get_units(); - - switch (units_p->pressure) { - case PASCAL: - pressure = mb * 100; - unit = translate("gettextFromC", "pascal"); - break; - case BAR: - default: - pressure = (mb + 500) / 1000; - unit = translate("gettextFromC", "bar"); - break; - case PSI: - pressure = mbar_to_PSI(mb); - unit = translate("gettextFromC", "psi"); - break; - } - if (units) - *units = unit; - return pressure; -} - -double get_temp_units(unsigned int mk, const char **units) -{ - double deg; - const char *unit; - struct units *units_p = get_units(); - - if (units_p->temperature == FAHRENHEIT) { - deg = mkelvin_to_F(mk); - unit = UTF8_DEGREE "F"; - } else { - deg = mkelvin_to_C(mk); - unit = UTF8_DEGREE "C"; - } - if (units) - *units = unit; - return deg; -} - -double get_volume_units(unsigned int ml, int *frac, const char **units) -{ - int decimals; - double vol; - const char *unit; - struct units *units_p = get_units(); - - switch (units_p->volume) { - case LITER: - default: - vol = ml / 1000.0; - unit = translate("gettextFromC", "â„“"); - decimals = 1; - break; - case CUFT: - vol = ml_to_cuft(ml); - unit = translate("gettextFromC", "cuft"); - decimals = 2; - break; - } - if (frac) - *frac = decimals; - if (units) - *units = unit; - return vol; -} - -int units_to_sac(double volume) -{ - if (get_units()->volume == CUFT) - return rint(cuft_to_l(volume) * 1000.0); - else - return rint(volume * 1000); -} - -unsigned int units_to_depth(double depth) -{ - if (get_units()->length == METERS) - return rint(depth * 1000); - return feet_to_mm(depth); -} - -double get_depth_units(int mm, int *frac, const char **units) -{ - int decimals; - double d; - const char *unit; - struct units *units_p = get_units(); - - switch (units_p->length) { - case METERS: - default: - d = mm / 1000.0; - unit = translate("gettextFromC", "m"); - decimals = d < 20; - break; - case FEET: - d = mm_to_feet(mm); - unit = translate("gettextFromC", "ft"); - decimals = 0; - break; - } - if (frac) - *frac = decimals; - if (units) - *units = unit; - return d; -} - -double get_vertical_speed_units(unsigned int mms, int *frac, const char **units) -{ - double d; - const char *unit; - const struct units *units_p = get_units(); - const double time_factor = units_p->vertical_speed_time == MINUTES ? 60.0 : 1.0; - - switch (units_p->length) { - case METERS: - default: - d = mms / 1000.0 * time_factor; - if (units_p->vertical_speed_time == MINUTES) - unit = translate("gettextFromC", "m/min"); - else - unit = translate("gettextFromC", "m/s"); - break; - case FEET: - d = mm_to_feet(mms) * time_factor; - if (units_p->vertical_speed_time == MINUTES) - unit = translate("gettextFromC", "ft/min"); - else - unit = translate("gettextFromC", "ft/s"); - break; - } - if (frac) - *frac = d < 10; - if (units) - *units = unit; - return d; -} - -double get_weight_units(unsigned int grams, int *frac, const char **units) -{ - int decimals; - double value; - const char *unit; - struct units *units_p = get_units(); - - if (units_p->weight == LBS) { - value = grams_to_lbs(grams); - unit = translate("gettextFromC", "lbs"); - decimals = 0; - } else { - value = grams / 1000.0; - unit = translate("gettextFromC", "kg"); - decimals = 1; - } - if (frac) - *frac = decimals; - if (units) - *units = unit; - return value; -} - -bool has_hr_data(struct divecomputer *dc) -{ - int i; - struct sample *sample; - - if (!dc) - return false; - - sample = dc->sample; - for (i = 0; i < dc->samples; i++) - if (sample[i].heartbeat) - return true; - return false; -} - -struct dive *alloc_dive(void) -{ - struct dive *dive; - - dive = malloc(sizeof(*dive)); - if (!dive) - exit(1); - memset(dive, 0, sizeof(*dive)); - dive->id = dive_getUniqID(dive); - return dive; -} - -static void free_dc(struct divecomputer *dc); -static void free_pic(struct picture *picture); - -/* this is very different from the copy_divecomputer later in this file; - * this function actually makes full copies of the content */ -static void copy_dc(struct divecomputer *sdc, struct divecomputer *ddc) -{ - *ddc = *sdc; - ddc->model = copy_string(sdc->model); - copy_samples(sdc, ddc); - copy_events(sdc, ddc); -} - -/* copy an element in a list of pictures */ -static void copy_pl(struct picture *sp, struct picture *dp) -{ - *dp = *sp; - dp->filename = copy_string(sp->filename); - dp->hash = copy_string(sp->hash); -} - -/* copy an element in a list of tags */ -static void copy_tl(struct tag_entry *st, struct tag_entry *dt) -{ - dt->tag = malloc(sizeof(struct divetag)); - dt->tag->name = copy_string(st->tag->name); - dt->tag->source = copy_string(st->tag->source); -} - -/* Clear everything but the first element; - * this works for taglist, picturelist, even dive computers */ -#define STRUCTURED_LIST_FREE(_type, _start, _free) \ - { \ - _type *_ptr = _start; \ - while (_ptr) { \ - _type *_next = _ptr->next; \ - _free(_ptr); \ - _ptr = _next; \ - } \ - } - -#define STRUCTURED_LIST_COPY(_type, _first, _dest, _cpy) \ - { \ - _type *_sptr = _first; \ - _type **_dptr = &_dest; \ - while (_sptr) { \ - *_dptr = malloc(sizeof(_type)); \ - _cpy(_sptr, *_dptr); \ - _sptr = _sptr->next; \ - _dptr = &(*_dptr)->next; \ - } \ - *_dptr = 0; \ - } - -/* copy_dive makes duplicates of many components of a dive; - * in order not to leak memory, we need to free those . - * copy_dive doesn't play with the divetrip and forward/backward pointers - * so we can ignore those */ -void clear_dive(struct dive *d) -{ - if (!d) - return; - /* free the strings */ - free(d->buddy); - free(d->divemaster); - free(d->notes); - free(d->suit); - /* free tags, additional dive computers, and pictures */ - taglist_free(d->tag_list); - STRUCTURED_LIST_FREE(struct divecomputer, d->dc.next, free_dc); - STRUCTURED_LIST_FREE(struct picture, d->picture_list, free_pic); - for (int i = 0; i < MAX_CYLINDERS; i++) - free((void *)d->cylinder[i].type.description); - for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) - free((void *)d->weightsystem[i].description); - memset(d, 0, sizeof(struct dive)); -} - -/* make a true copy that is independent of the source dive; - * all data structures are duplicated, so the copy can be modified without - * any impact on the source */ -void copy_dive(struct dive *s, struct dive *d) -{ - clear_dive(d); - /* simply copy things over, but then make actual copies of the - * relevant components that are referenced through pointers, - * so all the strings and the structured lists */ - *d = *s; - d->buddy = copy_string(s->buddy); - d->divemaster = copy_string(s->divemaster); - d->notes = copy_string(s->notes); - d->suit = copy_string(s->suit); - for (int i = 0; i < MAX_CYLINDERS; i++) - d->cylinder[i].type.description = copy_string(s->cylinder[i].type.description); - for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) - d->weightsystem[i].description = copy_string(s->weightsystem[i].description); - STRUCTURED_LIST_COPY(struct picture, s->picture_list, d->picture_list, copy_pl); - STRUCTURED_LIST_COPY(struct tag_entry, s->tag_list, d->tag_list, copy_tl); - STRUCTURED_LIST_COPY(struct divecomputer, s->dc.next, d->dc.next, copy_dc); - /* this only copied dive computers 2 and up. The first dive computer is part - * of the struct dive, so let's make copies of its samples and events */ - copy_samples(&s->dc, &d->dc); - copy_events(&s->dc, &d->dc); -} - -/* make a clone of the source dive and clean out the source dive; - * this is specifically so we can create a dive in the displayed_dive and then - * add it to the divelist. - * Note the difference to copy_dive() / clean_dive() */ -struct dive *clone_dive(struct dive *s) -{ - struct dive *dive = alloc_dive(); - *dive = *s; // so all the pointers in dive point to the things s pointed to - memset(s, 0, sizeof(struct dive)); // and now the pointers in s are gone - return dive; -} - -#define CONDITIONAL_COPY_STRING(_component) \ - if (what._component) \ - d->_component = copy_string(s->_component) - -// copy elements, depending on bits in what that are set -void selective_copy_dive(struct dive *s, struct dive *d, struct dive_components what, bool clear) -{ - if (clear) - clear_dive(d); - CONDITIONAL_COPY_STRING(notes); - CONDITIONAL_COPY_STRING(divemaster); - CONDITIONAL_COPY_STRING(buddy); - CONDITIONAL_COPY_STRING(suit); - if (what.rating) - d->rating = s->rating; - if (what.visibility) - d->visibility = s->visibility; - if (what.divesite) - d->dive_site_uuid = s->dive_site_uuid; - if (what.tags) - STRUCTURED_LIST_COPY(struct tag_entry, s->tag_list, d->tag_list, copy_tl); - if (what.cylinders) - copy_cylinders(s, d, false); - if (what.weights) - for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { - free((void *)d->weightsystem[i].description); - d->weightsystem[i] = s->weightsystem[i]; - d->weightsystem[i].description = copy_string(s->weightsystem[i].description); - } -} -#undef CONDITIONAL_COPY_STRING - -/* copies all events in this dive computer */ -void copy_events(struct divecomputer *s, struct divecomputer *d) -{ - struct event *ev, **pev; - if (!s || !d) - return; - ev = s->events; - pev = &d->events; - while (ev != NULL) { - int size = sizeof(*ev) + strlen(ev->name) + 1; - struct event *new_ev = malloc(size); - memcpy(new_ev, ev, size); - *pev = new_ev; - pev = &new_ev->next; - ev = ev->next; - } - *pev = NULL; -} - -int nr_cylinders(struct dive *dive) -{ - int nr; - - for (nr = MAX_CYLINDERS; nr; --nr) { - cylinder_t *cylinder = dive->cylinder + nr - 1; - if (!cylinder_nodata(cylinder)) - break; - } - return nr; -} - -int nr_weightsystems(struct dive *dive) -{ - int nr; - - for (nr = MAX_WEIGHTSYSTEMS; nr; --nr) { - weightsystem_t *ws = dive->weightsystem + nr - 1; - if (!weightsystem_none(ws)) - break; - } - return nr; -} - -/* copy the equipment data part of the cylinders */ -void copy_cylinders(struct dive *s, struct dive *d, bool used_only) -{ - int i,j; - if (!s || !d) - return; - for (i = 0; i < MAX_CYLINDERS; i++) { - free((void *)d->cylinder[i].type.description); - memset(&d->cylinder[i], 0, sizeof(cylinder_t)); - } - for (i = j = 0; i < MAX_CYLINDERS; i++) { - if (!used_only || is_cylinder_used(s, i)) { - d->cylinder[j].type = s->cylinder[i].type; - d->cylinder[j].type.description = copy_string(s->cylinder[i].type.description); - d->cylinder[j].gasmix = s->cylinder[i].gasmix; - d->cylinder[j].depth = s->cylinder[i].depth; - d->cylinder[j].cylinder_use = s->cylinder[i].cylinder_use; - d->cylinder[j].manually_added = true; - j++; - } - } -} - -int cylinderuse_from_text(const char *text) -{ - for (enum cylinderuse i = 0; i < NUM_GAS_USE; i++) { - if (same_string(text, cylinderuse_text[i]) || same_string(text, translate("gettextFromC", cylinderuse_text[i]))) - return i; - } - return -1; -} - -void copy_samples(struct divecomputer *s, struct divecomputer *d) -{ - /* instead of carefully copying them one by one and calling add_sample - * over and over again, let's just copy the whole blob */ - if (!s || !d) - return; - int nr = s->samples; - d->samples = nr; - d->alloc_samples = nr; - // We expect to be able to read the memory in the other end of the pointer - // if its a valid pointer, so don't expect malloc() to return NULL for - // zero-sized malloc, do it ourselves. - d->sample = NULL; - - if(!nr) - return; - - d->sample = malloc(nr * sizeof(struct sample)); - if (d->sample) - memcpy(d->sample, s->sample, nr * sizeof(struct sample)); -} - -struct sample *prepare_sample(struct divecomputer *dc) -{ - if (dc) { - int nr = dc->samples; - int alloc_samples = dc->alloc_samples; - struct sample *sample; - if (nr >= alloc_samples) { - struct sample *newsamples; - - alloc_samples = (alloc_samples * 3) / 2 + 10; - newsamples = realloc(dc->sample, alloc_samples * sizeof(struct sample)); - if (!newsamples) - return NULL; - dc->alloc_samples = alloc_samples; - dc->sample = newsamples; - } - sample = dc->sample + nr; - memset(sample, 0, sizeof(*sample)); - return sample; - } - return NULL; -} - -void finish_sample(struct divecomputer *dc) -{ - dc->samples++; -} - -/* - * So when we re-calculate maxdepth and meandepth, we will - * not override the old numbers if they are close to the - * new ones. - * - * Why? Because a dive computer may well actually track the - * max depth and mean depth at finer granularity than the - * samples it stores. So it's possible that the max and mean - * have been reported more correctly originally. - * - * Only if the values calculated from the samples are clearly - * different do we override the normal depth values. - * - * This considers 1m to be "clearly different". That's - * a totally random number. - */ -static void update_depth(depth_t *depth, int new) -{ - if (new) { - int old = depth->mm; - - if (abs(old - new) > 1000) - depth->mm = new; - } -} - -static void update_temperature(temperature_t *temperature, int new) -{ - if (new) { - int old = temperature->mkelvin; - - if (abs(old - new) > 1000) - temperature->mkelvin = new; - } -} - -/* - * Calculate how long we were actually under water, and the average - * depth while under water. - * - * This ignores any surface time in the middle of the dive. - */ -void fixup_dc_duration(struct divecomputer *dc) -{ - int duration, i; - int lasttime, lastdepth, depthtime; - - duration = 0; - lasttime = 0; - lastdepth = 0; - depthtime = 0; - for (i = 0; i < dc->samples; i++) { - struct sample *sample = dc->sample + i; - int time = sample->time.seconds; - int depth = sample->depth.mm; - - /* We ignore segments at the surface */ - if (depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) { - duration += time - lasttime; - depthtime += (time - lasttime) * (depth + lastdepth) / 2; - } - lastdepth = depth; - lasttime = time; - } - if (duration) { - dc->duration.seconds = duration; - dc->meandepth.mm = (depthtime + duration / 2) / duration; - } -} - -void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration) -{ - int i; - int depthtime[MAX_CYLINDERS] = { 0, }; - int lasttime = 0, lastdepth = 0; - int idx = 0; - - for (i = 0; i < MAX_CYLINDERS; i++) - mean[i] = duration[i] = 0; - if (!dc) - return; - struct event *ev = get_next_event(dc->events, "gaschange"); - if (!ev || (dc && dc->sample && ev->time.seconds == dc->sample[0].time.seconds && get_next_event(ev->next, "gaschange") == NULL)) { - // we have either no gas change or only one gas change and that's setting an explicit first cylinder - mean[explicit_first_cylinder(dive, dc)] = dc->meandepth.mm; - duration[explicit_first_cylinder(dive, dc)] = dc->duration.seconds; - - if (dc->divemode == CCR) { - // Do the same for the O2 cylinder - int o2_cyl = get_cylinder_idx_by_use(dive, OXYGEN); - if (o2_cyl < 0) - return; - mean[o2_cyl] = dc->meandepth.mm; - duration[o2_cyl] = dc->duration.seconds; - } - return; - } - if (!dc->samples) - dc = fake_dc(dc); - for (i = 0; i < dc->samples; i++) { - struct sample *sample = dc->sample + i; - int time = sample->time.seconds; - int depth = sample->depth.mm; - - /* Make sure to move the event past 'lasttime' */ - while (ev && lasttime >= ev->time.seconds) { - idx = get_cylinder_index(dive, ev); - ev = get_next_event(ev->next, "gaschange"); - } - - /* Do we need to fake a midway sample at an event? */ - if (ev && time > ev->time.seconds) { - int newtime = ev->time.seconds; - int newdepth = interpolate(lastdepth, depth, newtime - lasttime, time - lasttime); - - time = newtime; - depth = newdepth; - i--; - } - /* We ignore segments at the surface */ - if (depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) { - duration[idx] += time - lasttime; - depthtime[idx] += (time - lasttime) * (depth + lastdepth) / 2; - } - lastdepth = depth; - lasttime = time; - } - for (i = 0; i < MAX_CYLINDERS; i++) { - if (duration[i]) - mean[i] = (depthtime[i] + duration[i] / 2) / duration[i]; - } -} - -static void fixup_pressure(struct dive *dive, struct sample *sample, enum cylinderuse cyl_use) -{ - int pressure, index; - cylinder_t *cyl; - - if (cyl_use != OXYGEN) { - pressure = sample->cylinderpressure.mbar; - index = sample->sensor; - } else { // for the CCR oxygen cylinder: - pressure = sample->o2cylinderpressure.mbar; - index = get_cylinder_idx_by_use(dive, OXYGEN); - } - if (index < 0) - return; - if (!pressure) - return; - - /* - * Ignore surface samples for tank pressure information. - * - * At the beginning of the dive, let the cylinder cool down - * if the diver starts off at the surface. And at the end - * of the dive, there may be surface pressures where the - * diver has already turned off the air supply (especially - * for computers like the Uemis Zurich that end up saving - * quite a bit of samples after the dive has ended). - */ - if (sample->depth.mm < SURFACE_THRESHOLD) - return; - - /* FIXME! sensor -> cylinder mapping? */ - if (index >= MAX_CYLINDERS) - return; - cyl = dive->cylinder + index; - if (!cyl->sample_start.mbar) - cyl->sample_start.mbar = pressure; - cyl->sample_end.mbar = pressure; -} - -static void update_min_max_temperatures(struct dive *dive, temperature_t temperature) -{ - if (temperature.mkelvin) { - if (!dive->maxtemp.mkelvin || temperature.mkelvin > dive->maxtemp.mkelvin) - dive->maxtemp = temperature; - if (!dive->mintemp.mkelvin || temperature.mkelvin < dive->mintemp.mkelvin) - dive->mintemp = temperature; - } -} - -/* - * At high pressures air becomes less compressible, and - * does not follow the ideal gas law any more. - * - * This tries to correct for that, becoming the same - * as to_ATM() at lower pressures. - * - * THIS IS A ROUGH APPROXIMATION! The real numbers will - * depend on the exact gas mix and temperature. - */ -double surface_volume_multiplier(pressure_t pressure) -{ - double bar = pressure.mbar / 1000.0; - - if (bar > 200) - bar = 0.00038 * bar * bar + 0.51629 * bar + 81.542; - return bar_to_atm(bar); -} - -int gas_volume(cylinder_t *cyl, pressure_t p) -{ - return cyl->type.size.mliter * surface_volume_multiplier(p); -} - -int wet_volume(double cuft, pressure_t p) -{ - return cuft_to_l(cuft) * 1000 / surface_volume_multiplier(p); -} - -/* - * If the cylinder tank pressures are within half a bar - * (about 8 PSI) of the sample pressures, we consider it - * to be a rounding error, and throw them away as redundant. - */ -static int same_rounded_pressure(pressure_t a, pressure_t b) -{ - return abs(a.mbar - b.mbar) <= 500; -} - -/* Some dive computers (Cobalt) don't start the dive with cylinder 0 but explicitly - * tell us what the first gas is with a gas change event in the first sample. - * Sneakily we'll use a return value of 0 (or FALSE) when there is no explicit - * first cylinder - in which case cylinder 0 is indeed the first cylinder */ -int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc) -{ - if (dc) { - struct event *ev = get_next_event(dc->events, "gaschange"); - if (ev && dc->sample && ev->time.seconds == dc->sample[0].time.seconds) - return get_cylinder_index(dive, ev); - else if (dc->divemode == CCR) - return MAX(get_cylinder_idx_by_use(dive, DILUENT), 0); - } - return 0; -} - -/* this gets called when the dive mode has changed (so OC vs. CC) - * there are two places we might have setpoints... events or in the samples - */ -void update_setpoint_events(struct divecomputer *dc) -{ - struct event *ev; - int new_setpoint = 0; - - if (dc->divemode == CCR) - new_setpoint = prefs.defaultsetpoint; - - if (dc->divemode == OC && - (same_string(dc->model, "Shearwater Predator") || - same_string(dc->model, "Shearwater Petrel") || - same_string(dc->model, "Shearwater Nerd"))) { - // make sure there's no setpoint in the samples - // this is an irreversible change - so switching a dive to OC - // by mistake when it's actually CCR is _bad_ - // So we make sure, this comes from a Predator or Petrel and we only remove - // pO2 values we would have computed anyway. - struct event *ev = get_next_event(dc->events, "gaschange"); - struct gasmix *gasmix = get_gasmix_from_event(ev); - struct event *next = get_next_event(ev, "gaschange"); - - for (int i = 0; i < dc->samples; i++) { - struct gas_pressures pressures; - if (next && dc->sample[i].time.seconds >= next->time.seconds) { - ev = next; - gasmix = get_gasmix_from_event(ev); - next = get_next_event(ev, "gaschange"); - } - fill_pressures(&pressures, calculate_depth_to_mbar(dc->sample[i].depth.mm, dc->surface_pressure, 0), gasmix ,0, OC); - if (abs(dc->sample[i].setpoint.mbar - (int)(1000 * pressures.o2) <= 50)) - dc->sample[i].setpoint.mbar = 0; - } - } - - // an "SP change" event at t=0 is currently our marker for OC vs CCR - // this will need to change to a saner setup, but for now we can just - // check if such an event is there and adjust it, or add that event - ev = get_next_event(dc->events, "SP change"); - if (ev && ev->time.seconds == 0) { - ev->value = new_setpoint; - } else { - if (!add_event(dc, 0, SAMPLE_EVENT_PO2, 0, new_setpoint, "SP change")) - fprintf(stderr, "Could not add setpoint change event\n"); - } -} - -void sanitize_gasmix(struct gasmix *mix) -{ - unsigned int o2, he; - - o2 = mix->o2.permille; - he = mix->he.permille; - - /* Regular air: leave empty */ - if (!he) { - if (!o2) - return; - /* 20.8% to 21% O2 is just air */ - if (gasmix_is_air(mix)) { - mix->o2.permille = 0; - return; - } - } - - /* Sane mix? */ - if (o2 <= 1000 && he <= 1000 && o2 + he <= 1000) - return; - fprintf(stderr, "Odd gasmix: %u O2 %u He\n", o2, he); - memset(mix, 0, sizeof(*mix)); -} - -/* - * See if the size/workingpressure looks like some standard cylinder - * size, eg "AL80". - */ -static void match_standard_cylinder(cylinder_type_t *type) -{ - double cuft; - int psi, len; - const char *fmt; - char buffer[40], *p; - - /* Do we already have a cylinder description? */ - if (type->description) - return; - - cuft = ml_to_cuft(type->size.mliter); - cuft *= surface_volume_multiplier(type->workingpressure); - psi = to_PSI(type->workingpressure); - - switch (psi) { - case 2300 ... 2500: /* 2400 psi: LP tank */ - fmt = "LP%d"; - break; - case 2600 ... 2700: /* 2640 psi: LP+10% */ - fmt = "LP%d"; - break; - case 2900 ... 3100: /* 3000 psi: ALx tank */ - fmt = "AL%d"; - break; - case 3400 ... 3500: /* 3442 psi: HP tank */ - fmt = "HP%d"; - break; - case 3700 ... 3850: /* HP+10% */ - fmt = "HP%d+"; - break; - default: - return; - } - len = snprintf(buffer, sizeof(buffer), fmt, (int)rint(cuft)); - p = malloc(len + 1); - if (!p) - return; - memcpy(p, buffer, len + 1); - type->description = p; -} - - -/* - * There are two ways to give cylinder size information: - * - total amount of gas in cuft (depends on working pressure and physical size) - * - physical size - * - * where "physical size" is the one that actually matters and is sane. - * - * We internally use physical size only. But we save the workingpressure - * so that we can do the conversion if required. - */ -static void sanitize_cylinder_type(cylinder_type_t *type) -{ - double volume_of_air, volume; - - /* If we have no working pressure, it had *better* be just a physical size! */ - if (!type->workingpressure.mbar) - return; - - /* No size either? Nothing to go on */ - if (!type->size.mliter) - return; - - if (xml_parsing_units.volume == CUFT) { - /* confusing - we don't really start from ml but millicuft !*/ - volume_of_air = cuft_to_l(type->size.mliter); - /* milliliters at 1 atm: "true size" */ - volume = volume_of_air / surface_volume_multiplier(type->workingpressure); - type->size.mliter = rint(volume); - } - - /* Ok, we have both size and pressure: try to match a description */ - match_standard_cylinder(type); -} - -static void sanitize_cylinder_info(struct dive *dive) -{ - int i; - - for (i = 0; i < MAX_CYLINDERS; i++) { - sanitize_gasmix(&dive->cylinder[i].gasmix); - sanitize_cylinder_type(&dive->cylinder[i].type); - } -} - -/* some events should never be thrown away */ -static bool is_potentially_redundant(struct event *event) -{ - if (!strcmp(event->name, "gaschange")) - return false; - if (!strcmp(event->name, "bookmark")) - return false; - if (!strcmp(event->name, "heading")) - return false; - return true; -} - -/* match just by name - we compare the details in the code that uses this helper */ -static struct event *find_previous_event(struct divecomputer *dc, struct event *event) -{ - struct event *ev = dc->events; - struct event *previous = NULL; - - if (same_string(event->name, "")) - return NULL; - while (ev && ev != event) { - if (same_string(ev->name, event->name)) - previous = ev; - ev = ev->next; - } - return previous; -} - -static void fixup_surface_pressure(struct dive *dive) -{ - struct divecomputer *dc; - int sum = 0, nr = 0; - - for_each_dc (dive, dc) { - if (dc->surface_pressure.mbar) { - sum += dc->surface_pressure.mbar; - nr++; - } - } - if (nr) - dive->surface_pressure.mbar = (sum + nr / 2) / nr; -} - -static void fixup_water_salinity(struct dive *dive) -{ - struct divecomputer *dc; - int sum = 0, nr = 0; - - for_each_dc (dive, dc) { - if (dc->salinity) { - sum += dc->salinity; - nr++; - } - } - if (nr) - dive->salinity = (sum + nr / 2) / nr; -} - -static void fixup_meandepth(struct dive *dive) -{ - struct divecomputer *dc; - int sum = 0, nr = 0; - - for_each_dc (dive, dc) { - if (dc->meandepth.mm) { - sum += dc->meandepth.mm; - nr++; - } - } - if (nr) - dive->meandepth.mm = (sum + nr / 2) / nr; -} - -static void fixup_duration(struct dive *dive) -{ - struct divecomputer *dc; - unsigned int duration = 0; - - for_each_dc (dive, dc) - duration = MAX(duration, dc->duration.seconds); - - dive->duration.seconds = duration; -} - -/* - * What do the dive computers say the water temperature is? - * (not in the samples, but as dc property for dcs that support that) - */ -unsigned int dc_watertemp(struct divecomputer *dc) -{ - int sum = 0, nr = 0; - - do { - if (dc->watertemp.mkelvin) { - sum += dc->watertemp.mkelvin; - nr++; - } - } while ((dc = dc->next) != NULL); - if (!nr) - return 0; - return (sum + nr / 2) / nr; -} - -static void fixup_watertemp(struct dive *dive) -{ - if (!dive->watertemp.mkelvin) - dive->watertemp.mkelvin = dc_watertemp(&dive->dc); -} - -/* - * What do the dive computers say the air temperature is? - */ -unsigned int dc_airtemp(struct divecomputer *dc) -{ - int sum = 0, nr = 0; - - do { - if (dc->airtemp.mkelvin) { - sum += dc->airtemp.mkelvin; - nr++; - } - } while ((dc = dc->next) != NULL); - if (!nr) - return 0; - return (sum + nr / 2) / nr; -} - -static void fixup_cylinder_use(struct dive *dive) // for CCR dives, store the indices -{ // of the oxygen and diluent cylinders - dive->oxygen_cylinder_index = get_cylinder_idx_by_use(dive, OXYGEN); - dive->diluent_cylinder_index = get_cylinder_idx_by_use(dive, DILUENT); -} - -static void fixup_airtemp(struct dive *dive) -{ - if (!dive->airtemp.mkelvin) - dive->airtemp.mkelvin = dc_airtemp(&dive->dc); -} - -/* zero out the airtemp in the dive structure if it was just created by - * running fixup on the dive. keep it if it had been edited by hand */ -static void un_fixup_airtemp(struct dive *a) -{ - if (a->airtemp.mkelvin && a->airtemp.mkelvin == dc_airtemp(&a->dc)) - a->airtemp.mkelvin = 0; -} - -/* - * events are stored as a linked list, so the concept of - * "consecutive, identical events" is somewhat hard to - * implement correctly (especially given that on some dive - * computers events are asynchronous, so they can come in - * between what would be the non-constant sample rate). - * - * So what we do is that we throw away clearly redundant - * events that are fewer than 61 seconds apart (assuming there - * is no dive computer with a sample rate of more than 60 - * seconds... that would be pretty pointless to plot the - * profile with) - * - * We first only mark the events for deletion so that we - * still know when the previous event happened. - */ -static void fixup_dc_events(struct divecomputer *dc) -{ - struct event *event; - - event = dc->events; - while (event) { - struct event *prev; - if (is_potentially_redundant(event)) { - prev = find_previous_event(dc, event); - if (prev && prev->value == event->value && - prev->flags == event->flags && - event->time.seconds - prev->time.seconds < 61) - event->deleted = true; - } - event = event->next; - } - event = dc->events; - while (event) { - if (event->next && event->next->deleted) { - struct event *nextnext = event->next->next; - free(event->next); - event->next = nextnext; - } else { - event = event->next; - } - } -} - -static int interpolate_depth(struct divecomputer *dc, int idx, int lastdepth, int lasttime, int now) -{ - int i; - int nextdepth = lastdepth; - int nexttime = now; - - for (i = idx+1; i < dc->samples; i++) { - struct sample *sample = dc->sample + i; - if (sample->depth.mm < 0) - continue; - nextdepth = sample->depth.mm; - nexttime = sample->time.seconds; - break; - } - return interpolate(lastdepth, nextdepth, now-lasttime, nexttime-lasttime); -} - -static void fixup_dive_dc(struct dive *dive, struct divecomputer *dc) -{ - int i, j; - double depthtime = 0; - int lasttime = 0; - int lastindex = -1; - int maxdepth = dc->maxdepth.mm; - int mintemp = 0; - int lastdepth = 0; - int lasttemp = 0; - int lastpressure = 0, lasto2pressure = 0; - int pressure_delta[MAX_CYLINDERS] = { INT_MAX, }; - int first_cylinder; - - /* Add device information to table */ - if (dc->deviceid && (dc->serial || dc->fw_version)) - create_device_node(dc->model, dc->deviceid, dc->serial, dc->fw_version, ""); - - /* Fixup duration and mean depth */ - fixup_dc_duration(dc); - update_min_max_temperatures(dive, dc->watertemp); - - /* make sure we know for which tank the pressure values are intended */ - first_cylinder = explicit_first_cylinder(dive, dc); - for (i = 0; i < dc->samples; i++) { - struct sample *sample = dc->sample + i; - int time = sample->time.seconds; - int depth = sample->depth.mm; - int temp = sample->temperature.mkelvin; - int pressure = sample->cylinderpressure.mbar; - int o2_pressure = sample->o2cylinderpressure.mbar; - int index; - - if (depth < 0) { - depth = interpolate_depth(dc, i, lastdepth, lasttime, time); - sample->depth.mm = depth; - } - - /* if we have an explicit first cylinder */ - if (sample->sensor == 0 && first_cylinder != 0) - sample->sensor = first_cylinder; - - index = sample->sensor; - - if (index == lastindex) { - /* Remove duplicate redundant pressure information */ - if (pressure == lastpressure) - sample->cylinderpressure.mbar = 0; - if (o2_pressure == lasto2pressure) - sample->o2cylinderpressure.mbar = 0; - /* check for simply linear data in the samples - +INT_MAX means uninitialized, -INT_MAX means not linear */ - if (pressure_delta[index] != -INT_MAX && lastpressure) { - if (pressure_delta[index] == INT_MAX) { - pressure_delta[index] = abs(pressure - lastpressure); - } else { - int cur_delta = abs(pressure - lastpressure); - if (cur_delta && abs(cur_delta - pressure_delta[index]) > 150) { - /* ok the samples aren't just a linearisation - * between start and end */ - pressure_delta[index] = -INT_MAX; - } - } - } - } - lastindex = index; - lastpressure = pressure; - lasto2pressure = o2_pressure; - - if (depth > SURFACE_THRESHOLD) { - if (depth > maxdepth) - maxdepth = depth; - } - - fixup_pressure(dive, sample, OC_GAS); - if (dive->dc.divemode == CCR) - fixup_pressure(dive, sample, OXYGEN); - - if (temp) { - /* - * If we have consecutive identical - * temperature readings, throw away - * the redundant ones. - */ - if (lasttemp == temp) - sample->temperature.mkelvin = 0; - else - lasttemp = temp; - - if (!mintemp || temp < mintemp) - mintemp = temp; - } - - update_min_max_temperatures(dive, sample->temperature); - - depthtime += (time - lasttime) * (lastdepth + depth) / 2; - lastdepth = depth; - lasttime = time; - if (sample->cns > dive->maxcns) - dive->maxcns = sample->cns; - } - - /* if all the samples for a cylinder have pressure data that - * is basically equidistant throw out the sample cylinder pressure - * information but make sure we still have a valid start and end - * pressure - * this happens when DivingLog decides to linearalize the - * pressure between beginning and end and for strange reasons - * decides to put that in the sample data as if it came from - * the dive computer; we don't want that (we'll visualize with - * constant SAC rate instead) - * WARNING WARNING - I have only seen this in single tank dives - * --- maybe I should try to create a multi tank dive and see what - * --- divinglog does there - but the code right now is only tested - * --- for the single tank case */ - for (j = 0; j < MAX_CYLINDERS; j++) { - if (abs(pressure_delta[j]) != INT_MAX) { - cylinder_t *cyl = dive->cylinder + j; - for (i = 0; i < dc->samples; i++) - if (dc->sample[i].sensor == j) - dc->sample[i].cylinderpressure.mbar = 0; - if (!cyl->start.mbar) - cyl->start.mbar = cyl->sample_start.mbar; - if (!cyl->end.mbar) - cyl->end.mbar = cyl->sample_end.mbar; - cyl->sample_start.mbar = 0; - cyl->sample_end.mbar = 0; - } - } - - update_temperature(&dc->watertemp, mintemp); - update_depth(&dc->maxdepth, maxdepth); - if (maxdepth > dive->maxdepth.mm) - dive->maxdepth.mm = maxdepth; - fixup_dc_events(dc); -} - -struct dive *fixup_dive(struct dive *dive) -{ - int i; - struct divecomputer *dc; - - sanitize_cylinder_info(dive); - dive->maxcns = dive->cns; - - /* - * Use the dive's temperatures for minimum and maximum in case - * we do not have temperatures recorded by DC. - */ - - update_min_max_temperatures(dive, dive->watertemp); - - for_each_dc (dive, dc) - fixup_dive_dc(dive, dc); - - fixup_water_salinity(dive); - fixup_surface_pressure(dive); - fixup_meandepth(dive); - fixup_duration(dive); - fixup_watertemp(dive); - fixup_airtemp(dive); - fixup_cylinder_use(dive); // store indices for CCR oxygen and diluent cylinders - for (i = 0; i < MAX_CYLINDERS; i++) { - cylinder_t *cyl = dive->cylinder + i; - add_cylinder_description(&cyl->type); - if (same_rounded_pressure(cyl->sample_start, cyl->start)) - cyl->start.mbar = 0; - if (same_rounded_pressure(cyl->sample_end, cyl->end)) - cyl->end.mbar = 0; - } - update_cylinder_related_info(dive); - for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) { - weightsystem_t *ws = dive->weightsystem + i; - add_weightsystem_description(ws); - } - /* we should always have a uniq ID as that gets assigned during alloc_dive(), - * but we want to make sure... */ - if (!dive->id) - dive->id = dive_getUniqID(dive); - - return dive; -} - -/* Don't pick a zero for MERGE_MIN() */ -#define MERGE_MAX(res, a, b, n) res->n = MAX(a->n, b->n) -#define MERGE_MIN(res, a, b, n) res->n = (a->n) ? (b->n) ? MIN(a->n, b->n) : (a->n) : (b->n) -#define MERGE_TXT(res, a, b, n) res->n = merge_text(a->n, b->n) -#define MERGE_NONZERO(res, a, b, n) res->n = a->n ? a->n : b->n - -struct sample *add_sample(struct sample *sample, int time, struct divecomputer *dc) -{ - struct sample *p = prepare_sample(dc); - - if (p) { - *p = *sample; - p->time.seconds = time; - finish_sample(dc); - } - return p; -} - -/* - * This is like add_sample(), but if the distance from the last sample - * is excessive, we add two surface samples in between. - * - * This is so that if you merge two non-overlapping dives, we make sure - * that the time in between the dives is at the surface, not some "last - * sample that happened to be at a depth of 1.2m". - */ -static void merge_one_sample(struct sample *sample, int time, struct divecomputer *dc) -{ - int last = dc->samples - 1; - if (last >= 0) { - static struct sample surface; - struct sample *prev = dc->sample + last; - int last_time = prev->time.seconds; - int last_depth = prev->depth.mm; - - /* - * Only do surface events if the samples are more than - * a minute apart, and shallower than 5m - */ - if (time > last_time + 60 && last_depth < 5000) { - add_sample(&surface, last_time + 20, dc); - add_sample(&surface, time - 20, dc); - } - } - add_sample(sample, time, dc); -} - - -/* - * Merge samples. Dive 'a' is "offset" seconds before Dive 'b' - */ -static void merge_samples(struct divecomputer *res, struct divecomputer *a, struct divecomputer *b, int offset) -{ - int asamples = a->samples; - int bsamples = b->samples; - struct sample *as = a->sample; - struct sample *bs = b->sample; - - /* - * We want a positive sample offset, so that sample - * times are always positive. So if the samples for - * 'b' are before the samples for 'a' (so the offset - * is negative), we switch a and b around, and use - * the reverse offset. - */ - if (offset < 0) { - offset = -offset; - asamples = bsamples; - bsamples = a->samples; - as = bs; - bs = a->sample; - } - - for (;;) { - int at, bt; - struct sample sample; - - if (!res) - return; - - at = asamples ? as->time.seconds : -1; - bt = bsamples ? bs->time.seconds + offset : -1; - - /* No samples? All done! */ - if (at < 0 && bt < 0) - return; - - /* Only samples from a? */ - if (bt < 0) { - add_sample_a: - merge_one_sample(as, at, res); - as++; - asamples--; - continue; - } - - /* Only samples from b? */ - if (at < 0) { - add_sample_b: - merge_one_sample(bs, bt, res); - bs++; - bsamples--; - continue; - } - - if (at < bt) - goto add_sample_a; - if (at > bt) - goto add_sample_b; - - /* same-time sample: add a merged sample. Take the non-zero ones */ - sample = *bs; - if (as->depth.mm) - sample.depth = as->depth; - if (as->temperature.mkelvin) - sample.temperature = as->temperature; - if (as->cylinderpressure.mbar) - sample.cylinderpressure = as->cylinderpressure; - if (as->sensor) - sample.sensor = as->sensor; - if (as->cns) - sample.cns = as->cns; - if (as->setpoint.mbar) - sample.setpoint = as->setpoint; - if (as->ndl.seconds) - sample.ndl = as->ndl; - if (as->stoptime.seconds) - sample.stoptime = as->stoptime; - if (as->stopdepth.mm) - sample.stopdepth = as->stopdepth; - if (as->in_deco) - sample.in_deco = true; - - merge_one_sample(&sample, at, res); - - as++; - bs++; - asamples--; - bsamples--; - } -} - -static char *merge_text(const char *a, const char *b) -{ - char *res; - if (!a && !b) - return NULL; - if (!a || !*a) - return copy_string(b); - if (!b || !*b) - return strdup(a); - if (!strcmp(a, b)) - return copy_string(a); - res = malloc(strlen(a) + strlen(b) + 32); - if (!res) - return (char *)a; - sprintf(res, translate("gettextFromC", "(%s) or (%s)"), a, b); - return res; -} - -#define SORT(a, b, field) \ - if (a->field != b->field) \ - return a->field < b->field ? -1 : 1 - -static int sort_event(struct event *a, struct event *b) -{ - SORT(a, b, time.seconds); - SORT(a, b, type); - SORT(a, b, flags); - SORT(a, b, value); - return strcmp(a->name, b->name); -} - -static void merge_events(struct divecomputer *res, struct divecomputer *src1, struct divecomputer *src2, int offset) -{ - struct event *a, *b; - struct event **p = &res->events; - - /* Always use positive offsets */ - if (offset < 0) { - struct divecomputer *tmp; - - offset = -offset; - tmp = src1; - src1 = src2; - src2 = tmp; - } - - a = src1->events; - b = src2->events; - while (b) { - b->time.seconds += offset; - b = b->next; - } - b = src2->events; - - while (a || b) { - int s; - if (!b) { - *p = a; - break; - } - if (!a) { - *p = b; - break; - } - s = sort_event(a, b); - /* Pick b */ - if (s > 0) { - *p = b; - p = &b->next; - b = b->next; - continue; - } - /* Pick 'a' or neither */ - if (s < 0) { - *p = a; - p = &a->next; - } - a = a->next; - continue; - } -} - -/* Pick whichever has any info (if either). Prefer 'a' */ -static void merge_cylinder_type(cylinder_type_t *src, cylinder_type_t *dst) -{ - if (!dst->size.mliter) - dst->size.mliter = src->size.mliter; - if (!dst->workingpressure.mbar) - dst->workingpressure.mbar = src->workingpressure.mbar; - if (!dst->description) { - dst->description = src->description; - src->description = NULL; - } -} - -static void merge_cylinder_mix(struct gasmix *src, struct gasmix *dst) -{ - if (!dst->o2.permille) - *dst = *src; -} - -static void merge_cylinder_info(cylinder_t *src, cylinder_t *dst) -{ - merge_cylinder_type(&src->type, &dst->type); - merge_cylinder_mix(&src->gasmix, &dst->gasmix); - MERGE_MAX(dst, dst, src, start.mbar); - MERGE_MIN(dst, dst, src, end.mbar); -} - -static void merge_weightsystem_info(weightsystem_t *res, weightsystem_t *a, weightsystem_t *b) -{ - if (!a->weight.grams) - a = b; - *res = *a; -} - -/* get_cylinder_idx_by_use(): Find the index of the first cylinder with a particular CCR use type. - * The index returned corresponds to that of the first cylinder with a cylinder_use that - * equals the appropriate enum value [oxygen, diluent, bailout] given by cylinder_use_type. - * A negative number returned indicates that a match could not be found. - * Call parameters: dive = the dive being processed - * cylinder_use_type = an enum, one of {oxygen, diluent, bailout} */ -extern int get_cylinder_idx_by_use(struct dive *dive, enum cylinderuse cylinder_use_type) -{ - int cylinder_index; - for (cylinder_index = 0; cylinder_index < MAX_CYLINDERS; cylinder_index++) { - if (dive->cylinder[cylinder_index].cylinder_use == cylinder_use_type) - return cylinder_index; // return the index of the cylinder with that cylinder use type - } - return -1; // negative number means cylinder_use_type not found in list of cylinders -} - -int gasmix_distance(const struct gasmix *a, const struct gasmix *b) -{ - int a_o2 = get_o2(a), b_o2 = get_o2(b); - int a_he = get_he(a), b_he = get_he(b); - int delta_o2 = a_o2 - b_o2, delta_he = a_he - b_he; - - delta_he = delta_he * delta_he; - delta_o2 = delta_o2 * delta_o2; - return delta_he + delta_o2; -} - -/* fill_pressures(): Compute partial gas pressures in bar from gasmix and ambient pressures, possibly for OC or CCR, to be - * extended to PSCT. This function does the calculations of gass pressures applicable to a single point on the dive profile. - * The structure "pressures" is used to return calculated gas pressures to the calling software. - * Call parameters: po2 = po2 value applicable to the record in calling function - * amb_pressure = ambient pressure applicable to the record in calling function - * *pressures = structure for communicating o2 sensor values from and gas pressures to the calling function. - * *mix = structure containing cylinder gas mixture information. - * This function called by: calculate_gas_information_new() in profile.c; add_segment() in deco.c. - */ -extern void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, const struct gasmix *mix, double po2, enum dive_comp_type divemode) -{ - if (po2) { // This is probably a CCR dive where pressures->o2 is defined - if (po2 >= amb_pressure) { - pressures->o2 = amb_pressure; - pressures->n2 = pressures->he = 0.0; - } else { - pressures->o2 = po2; - if (get_o2(mix) == 1000) { - pressures->he = pressures->n2 = 0; - } else { - pressures->he = (amb_pressure - pressures->o2) * (double)get_he(mix) / (1000 - get_o2(mix)); - pressures->n2 = amb_pressure - pressures->o2 - pressures->he; - } - } - } else { - if (divemode == PSCR) { /* The steady state approximation should be good enough */ - pressures->o2 = get_o2(mix) / 1000.0 * amb_pressure - (1.0 - get_o2(mix) / 1000.0) * prefs.o2consumption / (prefs.bottomsac * prefs.pscr_ratio / 1000.0); - if (pressures->o2 < 0) // He's dead, Jim. - pressures->o2 = 0; - if (get_o2(mix) != 1000) { - pressures->he = (amb_pressure - pressures->o2) * get_he(mix) / (1000.0 - get_o2(mix)); - pressures->n2 = (amb_pressure - pressures->o2) * (1000 - get_o2(mix) - get_he(mix)) / (1000.0 - get_o2(mix)); - } else { - pressures->he = pressures->n2 = 0; - } - } else { - // Open circuit dives: no gas pressure values available, they need to be calculated - pressures->o2 = get_o2(mix) / 1000.0 * amb_pressure; // These calculations are also used if the CCR calculation above.. - pressures->he = get_he(mix) / 1000.0 * amb_pressure; // ..returned a po2 of zero (i.e. o2 sensor data not resolvable) - pressures->n2 = (1000 - get_o2(mix) - get_he(mix)) / 1000.0 * amb_pressure; - } - } -} - -static int find_cylinder_match(cylinder_t *cyl, cylinder_t array[], unsigned int used) -{ - int i; - int best = -1, score = INT_MAX; - - if (cylinder_nodata(cyl)) - return -1; - for (i = 0; i < MAX_CYLINDERS; i++) { - const cylinder_t *match; - int distance; - - if (used & (1 << i)) - continue; - match = array + i; - distance = gasmix_distance(&cyl->gasmix, &match->gasmix); - if (distance >= score) - continue; - best = i; - score = distance; - } - return best; -} - -/* Force an initial gaschange event to the (old) gas #0 */ -static void add_initial_gaschange(struct dive *dive, struct divecomputer *dc) -{ - struct event *ev = get_next_event(dc->events, "gaschange"); - - if (ev && ev->time.seconds < 30) - return; - - /* Old starting gas mix */ - add_gas_switch_event(dive, dc, 0, 0); -} - -void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]) -{ - int i; - struct event *ev; - - /* Did the first gas get remapped? Add gas switch event */ - if (mapping[0] > 0) - add_initial_gaschange(dive, dc); - - /* Remap the sensor indexes */ - for (i = 0; i < dc->samples; i++) { - struct sample *s = dc->sample + i; - int sensor; - - if (!s->cylinderpressure.mbar) - continue; - sensor = mapping[s->sensor]; - if (sensor >= 0) - s->sensor = sensor; - } - - /* Remap the gas change indexes */ - for (ev = dc->events; ev; ev = ev->next) { - if (!event_is_gaschange(ev)) - continue; - if (ev->gas.index < 0) - continue; - ev->gas.index = mapping[ev->gas.index]; - } -} - -/* - * If the cylinder indexes change (due to merging dives or deleting - * cylinders in the middle), we need to change the indexes in the - * dive computer data for this dive. - * - * Also note that we assume that the initial cylinder is cylinder 0, - * so if that got renamed, we need to create a fake gas change event - */ -static void cylinder_renumber(struct dive *dive, int mapping[]) -{ - struct divecomputer *dc; - for_each_dc (dive, dc) - dc_cylinder_renumber(dive, dc, mapping); -} - -/* - * Merging cylinder information is non-trivial, because the two dive computers - * may have different ideas of what the different cylinder indexing is. - * - * Logic: take all the cylinder information from the preferred dive ('a'), and - * then try to match each of the cylinders in the other dive by the gasmix that - * is the best match and hasn't been used yet. - */ -static void merge_cylinders(struct dive *res, struct dive *a, struct dive *b) -{ - int i, renumber = 0; - int mapping[MAX_CYLINDERS]; - unsigned int used = 0; - - /* Copy the cylinder info raw from 'a' */ - memcpy(res->cylinder, a->cylinder, sizeof(res->cylinder)); - memset(a->cylinder, 0, sizeof(a->cylinder)); - - for (i = 0; i < MAX_CYLINDERS; i++) { - int j; - cylinder_t *cyl = b->cylinder + i; - - j = find_cylinder_match(cyl, res->cylinder, used); - mapping[i] = j; - if (j < 0) - continue; - used |= 1 << j; - merge_cylinder_info(cyl, res->cylinder + j); - - /* If that renumbered the cylinders, fix it up! */ - if (i != j) - renumber = 1; - } - if (renumber) - cylinder_renumber(b, mapping); -} - -static void merge_equipment(struct dive *res, struct dive *a, struct dive *b) -{ - int i; - - merge_cylinders(res, a, b); - for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) - merge_weightsystem_info(res->weightsystem + i, a->weightsystem + i, b->weightsystem + i); -} - -static void merge_airtemps(struct dive *res, struct dive *a, struct dive *b) -{ - un_fixup_airtemp(a); - un_fixup_airtemp(b); - MERGE_NONZERO(res, a, b, airtemp.mkelvin); -} - -/* - * When merging two dives, this picks the trip from one, and removes it - * from the other. - * - * The 'next' dive is not involved in the dive merging, but is the dive - * that will be the next dive after the merged dive. - */ -static void pick_trip(struct dive *res, struct dive *pick) -{ - tripflag_t tripflag = pick->tripflag; - dive_trip_t *trip = pick->divetrip; - - res->tripflag = tripflag; - add_dive_to_trip(res, trip); -} - -/* - * Pick a trip for a dive - */ -static void merge_trip(struct dive *res, struct dive *a, struct dive *b) -{ - dive_trip_t *atrip, *btrip; - - /* - * The larger tripflag is more relevant: we prefer - * take manually assigned trips over auto-generated - * ones. - */ - if (a->tripflag > b->tripflag) - goto pick_a; - - if (a->tripflag < b->tripflag) - goto pick_b; - - /* Otherwise, look at the trip data and pick the "better" one */ - atrip = a->divetrip; - btrip = b->divetrip; - if (!atrip) - goto pick_b; - if (!btrip) - goto pick_a; - if (!atrip->location) - goto pick_b; - if (!btrip->location) - goto pick_a; - if (!atrip->notes) - goto pick_b; - if (!btrip->notes) - goto pick_a; - - /* - * Ok, so both have location and notes. - * Pick the earlier one. - */ - if (a->when < b->when) - goto pick_a; - goto pick_b; - -pick_a: - b = a; -pick_b: - pick_trip(res, b); -} - -#if CURRENTLY_NOT_USED -/* - * Sample 's' is between samples 'a' and 'b'. It is 'offset' seconds before 'b'. - * - * If 's' and 'a' are at the same time, offset is 0, and b is NULL. - */ -static int compare_sample(struct sample *s, struct sample *a, struct sample *b, int offset) -{ - unsigned int depth = a->depth.mm; - int diff; - - if (offset) { - unsigned int interval = b->time.seconds - a->time.seconds; - unsigned int depth_a = a->depth.mm; - unsigned int depth_b = b->depth.mm; - - if (offset > interval) - return -1; - - /* pick the average depth, scaled by the offset from 'b' */ - depth = (depth_a * offset) + (depth_b * (interval - offset)); - depth /= interval; - } - diff = s->depth.mm - depth; - if (diff < 0) - diff = -diff; - /* cut off at one meter difference */ - if (diff > 1000) - diff = 1000; - return diff * diff; -} - -/* - * Calculate a "difference" in samples between the two dives, given - * the offset in seconds between them. Use this to find the best - * match of samples between two different dive computers. - */ -static unsigned long sample_difference(struct divecomputer *a, struct divecomputer *b, int offset) -{ - int asamples = a->samples; - int bsamples = b->samples; - struct sample *as = a->sample; - struct sample *bs = b->sample; - unsigned long error = 0; - int start = -1; - - if (!asamples || !bsamples) - return 0; - - /* - * skip the first sample - this way we know can always look at - * as/bs[-1] to look at the samples around it in the loop. - */ - as++; - bs++; - asamples--; - bsamples--; - - for (;;) { - int at, bt, diff; - - - /* If we run out of samples, punt */ - if (!asamples) - return INT_MAX; - if (!bsamples) - return INT_MAX; - - at = as->time.seconds; - bt = bs->time.seconds + offset; - - /* b hasn't started yet? Ignore it */ - if (bt < 0) { - bs++; - bsamples--; - continue; - } - - if (at < bt) { - diff = compare_sample(as, bs - 1, bs, bt - at); - as++; - asamples--; - } else if (at > bt) { - diff = compare_sample(bs, as - 1, as, at - bt); - bs++; - bsamples--; - } else { - diff = compare_sample(as, bs, NULL, 0); - as++; - bs++; - asamples--; - bsamples--; - } - - /* Invalid comparison point? */ - if (diff < 0) - continue; - - if (start < 0) - start = at; - - error += diff; - - if (at - start > 120) - break; - } - return error; -} - -/* - * Dive 'a' is 'offset' seconds before dive 'b' - * - * This is *not* because the dive computers clocks aren't in sync, - * it is because the dive computers may "start" the dive at different - * points in the dive, so the sample at time X in dive 'a' is the - * same as the sample at time X+offset in dive 'b'. - * - * For example, some dive computers take longer to "wake up" when - * they sense that you are under water (ie Uemis Zurich if it was off - * when the dive started). And other dive computers have different - * depths that they activate at, etc etc. - * - * If we cannot find a shared offset, don't try to merge. - */ -static int find_sample_offset(struct divecomputer *a, struct divecomputer *b) -{ - int offset, best; - unsigned long max; - - /* No samples? Merge at any time (0 offset) */ - if (!a->samples) - return 0; - if (!b->samples) - return 0; - - /* - * Common special-case: merging a dive that came from - * the same dive computer, so the samples are identical. - * Check this first, without wasting time trying to find - * some minimal offset case. - */ - best = 0; - max = sample_difference(a, b, 0); - if (!max) - return 0; - - /* - * Otherwise, look if we can find anything better within - * a thirty second window.. - */ - for (offset = -30; offset <= 30; offset++) { - unsigned long diff; - - diff = sample_difference(a, b, offset); - if (diff > max) - continue; - best = offset; - max = diff; - } - - return best; -} -#endif - -/* - * Are a and b "similar" values, when given a reasonable lower end expected - * difference? - * - * So for example, we'd expect different dive computers to give different - * max depth readings. You might have them on different arms, and they - * have different pressure sensors and possibly different ideas about - * water salinity etc. - * - * So have an expected minimum difference, but also allow a larger relative - * error value. - */ -static int similar(unsigned long a, unsigned long b, unsigned long expected) -{ - if (a && b) { - unsigned long min, max, diff; - - min = a; - max = b; - if (a > b) { - min = b; - max = a; - } - diff = max - min; - - /* Smaller than expected difference? */ - if (diff < expected) - return 1; - /* Error less than 10% or the maximum */ - if (diff * 10 < max) - return 1; - } - return 0; -} - -/* - * Match two dive computer entries against each other, and - * tell if it's the same dive. Return 0 if "don't know", - * positive for "same dive" and negative for "definitely - * not the same dive" - */ -int match_one_dc(struct divecomputer *a, struct divecomputer *b) -{ - /* Not same model? Don't know if matching.. */ - if (!a->model || !b->model) - return 0; - if (strcasecmp(a->model, b->model)) - return 0; - - /* Different device ID's? Don't know */ - if (a->deviceid != b->deviceid) - return 0; - - /* Do we have dive IDs? */ - if (!a->diveid || !b->diveid) - return 0; - - /* - * If they have different dive ID's on the same - * dive computer, that's a definite "same or not" - */ - return a->diveid == b->diveid ? 1 : -1; -} - -/* - * Match every dive computer against each other to see if - * we have a matching dive. - * - * Return values: - * -1 for "is definitely *NOT* the same dive" - * 0 for "don't know" - * 1 for "is definitely the same dive" - */ -static int match_dc_dive(struct divecomputer *a, struct divecomputer *b) -{ - do { - struct divecomputer *tmp = b; - do { - int match = match_one_dc(a, tmp); - if (match) - return match; - tmp = tmp->next; - } while (tmp); - a = a->next; - } while (a); - return 0; -} - -static bool new_without_trip(struct dive *a) -{ - return a->downloaded && !a->divetrip; -} - -/* - * Do we want to automatically try to merge two dives that - * look like they are the same dive? - * - * This happens quite commonly because you download a dive - * that you already had, or perhaps because you maintained - * multiple dive logs and want to load them all together - * (possibly one of them was imported from another dive log - * application entirely). - * - * NOTE! We mainly look at the dive time, but it can differ - * between two dives due to a few issues: - * - * - rounding the dive date to the nearest minute in other dive - * applications - * - * - dive computers with "relative datestamps" (ie the dive - * computer doesn't actually record an absolute date at all, - * but instead at download-time syncronizes its internal - * time with real-time on the downloading computer) - * - * - using multiple dive computers with different real time on - * the same dive - * - * We do not merge dives that look radically different, and if - * the dates are *too* far off the user will have to join two - * dives together manually. But this tries to handle the sane - * cases. - */ -static int likely_same_dive(struct dive *a, struct dive *b) -{ - int match, fuzz = 20 * 60; - - /* don't merge manually added dives with anything */ - if (same_string(a->dc.model, "manually added dive") || - same_string(b->dc.model, "manually added dive")) - return 0; - - /* Don't try to merge dives with different trip information */ - if (a->divetrip != b->divetrip) { - /* - * Exception: if the dive is downloaded without any - * explicit trip information, we do want to merge it - * with existing old dives even if they have trips. - */ - if (!new_without_trip(a) && !new_without_trip(b)) - return 0; - } - - /* - * Do some basic sanity testing of the values we - * have filled in during 'fixup_dive()' - */ - if (!similar(a->maxdepth.mm, b->maxdepth.mm, 1000) || - (a->meandepth.mm && b->meandepth.mm && !similar(a->meandepth.mm, b->meandepth.mm, 1000)) || - !similar(a->duration.seconds, b->duration.seconds, 5 * 60)) - return 0; - - /* See if we can get an exact match on the dive computer */ - match = match_dc_dive(&a->dc, &b->dc); - if (match) - return match > 0; - - /* - * Allow a time difference due to dive computer time - * setting etc. Check if they overlap. - */ - fuzz = MAX(a->duration.seconds, b->duration.seconds) / 2; - if (fuzz < 60) - fuzz = 60; - - return ((a->when <= b->when + fuzz) && (a->when >= b->when - fuzz)); -} - -/* - * This could do a lot more merging. Right now it really only - * merges almost exact duplicates - something that happens easily - * with overlapping dive downloads. - */ -struct dive *try_to_merge(struct dive *a, struct dive *b, bool prefer_downloaded) -{ - if (likely_same_dive(a, b)) - return merge_dives(a, b, 0, prefer_downloaded); - return NULL; -} - -void free_events(struct event *ev) -{ - while (ev) { - struct event *next = ev->next; - free(ev); - ev = next; - } -} - -static void free_dc(struct divecomputer *dc) -{ - free(dc->sample); - free((void *)dc->model); - free_events(dc->events); - free(dc); -} - -static void free_pic(struct picture *picture) -{ - if (picture) { - free(picture->filename); - free(picture); - } -} - -static int same_sample(struct sample *a, struct sample *b) -{ - if (a->time.seconds != b->time.seconds) - return 0; - if (a->depth.mm != b->depth.mm) - return 0; - if (a->temperature.mkelvin != b->temperature.mkelvin) - return 0; - if (a->cylinderpressure.mbar != b->cylinderpressure.mbar) - return 0; - return a->sensor == b->sensor; -} - -static int same_dc(struct divecomputer *a, struct divecomputer *b) -{ - int i; - struct event *eva, *evb; - - i = match_one_dc(a, b); - if (i) - return i > 0; - - if (a->when && b->when && a->when != b->when) - return 0; - if (a->samples != b->samples) - return 0; - for (i = 0; i < a->samples; i++) - if (!same_sample(a->sample + i, b->sample + i)) - return 0; - eva = a->events; - evb = b->events; - while (eva && evb) { - if (!same_event(eva, evb)) - return 0; - eva = eva->next; - evb = evb->next; - } - return eva == evb; -} - -static int might_be_same_device(struct divecomputer *a, struct divecomputer *b) -{ - /* No dive computer model? That matches anything */ - if (!a->model || !b->model) - return 1; - - /* Otherwise at least the model names have to match */ - if (strcasecmp(a->model, b->model)) - return 0; - - /* No device ID? Match */ - if (!a->deviceid || !b->deviceid) - return 1; - - return a->deviceid == b->deviceid; -} - -static void remove_redundant_dc(struct divecomputer *dc, int prefer_downloaded) -{ - do { - struct divecomputer **p = &dc->next; - - /* Check this dc against all the following ones.. */ - while (*p) { - struct divecomputer *check = *p; - if (same_dc(dc, check) || (prefer_downloaded && might_be_same_device(dc, check))) { - *p = check->next; - check->next = NULL; - free_dc(check); - continue; - } - p = &check->next; - } - - /* .. and then continue down the chain, but we */ - prefer_downloaded = 0; - dc = dc->next; - } while (dc); -} - -static void clear_dc(struct divecomputer *dc) -{ - memset(dc, 0, sizeof(*dc)); -} - -static struct divecomputer *find_matching_computer(struct divecomputer *match, struct divecomputer *list) -{ - struct divecomputer *p; - - while ((p = list) != NULL) { - list = list->next; - - if (might_be_same_device(match, p)) - break; - } - return p; -} - - -static void copy_dive_computer(struct divecomputer *res, struct divecomputer *a) -{ - *res = *a; - res->model = copy_string(a->model); - res->samples = res->alloc_samples = 0; - res->sample = NULL; - res->events = NULL; - res->next = NULL; -} - -/* - * Join dive computers with a specific time offset between - * them. - * - * Use the dive computer ID's (or names, if ID's are missing) - * to match them up. If we find a matching dive computer, we - * merge them. If not, we just take the data from 'a'. - */ -static void interleave_dive_computers(struct divecomputer *res, - struct divecomputer *a, struct divecomputer *b, int offset) -{ - do { - struct divecomputer *match; - - copy_dive_computer(res, a); - - match = find_matching_computer(a, b); - if (match) { - merge_events(res, a, match, offset); - merge_samples(res, a, match, offset); - /* Use the diveid of the later dive! */ - if (offset > 0) - res->diveid = match->diveid; - } else { - res->sample = a->sample; - res->samples = a->samples; - res->events = a->events; - a->sample = NULL; - a->samples = 0; - a->events = NULL; - } - a = a->next; - if (!a) - break; - res->next = calloc(1, sizeof(struct divecomputer)); - res = res->next; - } while (res); -} - - -/* - * Join dive computer information. - * - * If we have old-style dive computer information (no model - * name etc), we will prefer a new-style one and just throw - * away the old. We're assuming it's a re-download. - * - * Otherwise, we'll just try to keep all the information, - * unless the user has specified that they prefer the - * downloaded computer, in which case we'll aggressively - * try to throw out old information that *might* be from - * that one. - */ -static void join_dive_computers(struct divecomputer *res, struct divecomputer *a, struct divecomputer *b, int prefer_downloaded) -{ - struct divecomputer *tmp; - - if (a->model && !b->model) { - *res = *a; - clear_dc(a); - return; - } - if (b->model && !a->model) { - *res = *b; - clear_dc(b); - return; - } - - *res = *a; - clear_dc(a); - tmp = res; - while (tmp->next) - tmp = tmp->next; - - tmp->next = calloc(1, sizeof(*tmp)); - *tmp->next = *b; - clear_dc(b); - - remove_redundant_dc(res, prefer_downloaded); -} - -static bool tag_seen_before(struct tag_entry *start, struct tag_entry *before) -{ - while (start && start != before) { - if (same_string(start->tag->name, before->tag->name)) - return true; - start = start->next; - } - return false; -} - -/* remove duplicates and empty nodes */ -void taglist_cleanup(struct tag_entry **tag_list) -{ - struct tag_entry **tl = tag_list; - while (*tl) { - /* skip tags that are empty or that we have seen before */ - if (same_string((*tl)->tag->name, "") || tag_seen_before(*tag_list, *tl)) { - *tl = (*tl)->next; - continue; - } - tl = &(*tl)->next; - } -} - -int taglist_get_tagstring(struct tag_entry *tag_list, char *buffer, int len) -{ - int i = 0; - struct tag_entry *tmp; - tmp = tag_list; - memset(buffer, 0, len); - while (tmp != NULL) { - int newlength = strlen(tmp->tag->name); - if (i > 0) - newlength += 2; - if ((i + newlength) < len) { - if (i > 0) { - strcpy(buffer + i, ", "); - strcpy(buffer + i + 2, tmp->tag->name); - } else { - strcpy(buffer, tmp->tag->name); - } - } else { - return i; - } - i += newlength; - tmp = tmp->next; - } - return i; -} - -static inline void taglist_free_divetag(struct divetag *tag) -{ - if (tag->name != NULL) - free(tag->name); - if (tag->source != NULL) - free(tag->source); - free(tag); -} - -/* Add a tag to the tag_list, keep the list sorted */ -static struct divetag *taglist_add_divetag(struct tag_entry **tag_list, struct divetag *tag) -{ - struct tag_entry *next, *entry; - - while ((next = *tag_list) != NULL) { - int cmp = strcmp(next->tag->name, tag->name); - - /* Already have it? */ - if (!cmp) - return next->tag; - /* Is the entry larger? If so, insert here */ - if (cmp > 0) - break; - /* Continue traversing the list */ - tag_list = &next->next; - } - - /* Insert in front of it */ - entry = malloc(sizeof(struct tag_entry)); - entry->next = next; - entry->tag = tag; - *tag_list = entry; - return tag; -} - -struct divetag *taglist_add_tag(struct tag_entry **tag_list, const char *tag) -{ - int i = 0, is_default_tag = 0; - struct divetag *ret_tag, *new_tag; - const char *translation; - new_tag = malloc(sizeof(struct divetag)); - - for (i = 0; i < sizeof(default_tags) / sizeof(char *); i++) { - if (strcmp(default_tags[i], tag) == 0) { - is_default_tag = 1; - break; - } - } - /* Only translate default tags */ - if (is_default_tag) { - translation = translate("gettextFromC", tag); - new_tag->name = malloc(strlen(translation) + 1); - memcpy(new_tag->name, translation, strlen(translation) + 1); - new_tag->source = malloc(strlen(tag) + 1); - memcpy(new_tag->source, tag, strlen(tag) + 1); - } else { - new_tag->source = NULL; - new_tag->name = malloc(strlen(tag) + 1); - memcpy(new_tag->name, tag, strlen(tag) + 1); - } - /* Try to insert new_tag into g_tag_list if we are not operating on it */ - if (tag_list != &g_tag_list) { - ret_tag = taglist_add_divetag(&g_tag_list, new_tag); - /* g_tag_list already contains new_tag, free the duplicate */ - if (ret_tag != new_tag) - taglist_free_divetag(new_tag); - ret_tag = taglist_add_divetag(tag_list, ret_tag); - } else { - ret_tag = taglist_add_divetag(tag_list, new_tag); - if (ret_tag != new_tag) - taglist_free_divetag(new_tag); - } - return ret_tag; -} - -void taglist_free(struct tag_entry *entry) -{ - STRUCTURED_LIST_FREE(struct tag_entry, entry, free) -} - -/* Merge src1 and src2, write to *dst */ -static void taglist_merge(struct tag_entry **dst, struct tag_entry *src1, struct tag_entry *src2) -{ - struct tag_entry *entry; - - for (entry = src1; entry; entry = entry->next) - taglist_add_divetag(dst, entry->tag); - for (entry = src2; entry; entry = entry->next) - taglist_add_divetag(dst, entry->tag); -} - -void taglist_init_global() -{ - int i; - - for (i = 0; i < sizeof(default_tags) / sizeof(char *); i++) - taglist_add_tag(&g_tag_list, default_tags[i]); -} - -bool taglist_contains(struct tag_entry *tag_list, const char *tag) -{ - while (tag_list) { - if (same_string(tag_list->tag->name, tag)) - return true; - tag_list = tag_list->next; - } - return false; -} - -// check if all tags in subtl are included in supertl (so subtl is a subset of supertl) -static bool taglist_contains_all(struct tag_entry *subtl, struct tag_entry *supertl) -{ - while (subtl) { - if (!taglist_contains(supertl, subtl->tag->name)) - return false; - subtl = subtl->next; - } - return true; -} - -struct tag_entry *taglist_added(struct tag_entry *original_list, struct tag_entry *new_list) -{ - struct tag_entry *added_list = NULL; - while (new_list) { - if (!taglist_contains(original_list, new_list->tag->name)) - taglist_add_tag(&added_list, new_list->tag->name); - new_list = new_list->next; - } - return added_list; -} - -void dump_taglist(const char *intro, struct tag_entry *tl) -{ - char *comma = ""; - fprintf(stderr, "%s", intro); - while(tl) { - fprintf(stderr, "%s %s", comma, tl->tag->name); - comma = ","; - tl = tl->next; - } - fprintf(stderr, "\n"); -} - -// if tl1 is both a subset and superset of tl2 they must be the same -bool taglist_equal(struct tag_entry *tl1, struct tag_entry *tl2) -{ - return taglist_contains_all(tl1, tl2) && taglist_contains_all(tl2, tl1); -} - -// count the dives where the tag list contains the given tag -int count_dives_with_tag(const char *tag) -{ - int i, counter = 0; - struct dive *d; - - for_each_dive (i, d) { - if (same_string(tag, "")) { - // count dives with no tags - if (d->tag_list == NULL) - counter++; - } else if (taglist_contains(d->tag_list, tag)) { - counter++; - } - } - return counter; -} - -extern bool string_sequence_contains(const char *string_sequence, const char *text); - -// count the dives where the person is included in the comma separated string sequences of buddies or divemasters -int count_dives_with_person(const char *person) -{ - int i, counter = 0; - struct dive *d; - - for_each_dive (i, d) { - if (same_string(person, "")) { - // solo dive - if (same_string(d->buddy, "") && same_string(d->divemaster, "")) - counter++; - } else if (string_sequence_contains(d->buddy, person) || string_sequence_contains(d->divemaster, person)) { - counter++; - } - } - return counter; -} - -// count the dives with exactly the location -int count_dives_with_location(const char *location) -{ - int i, counter = 0; - struct dive *d; - - for_each_dive (i, d) { - if (same_string(get_dive_location(d), location)) - counter++; - } - return counter; -} - -// count the dives with exactly the suit -int count_dives_with_suit(const char *suit) -{ - int i, counter = 0; - struct dive *d; - - for_each_dive (i, d) { - if (same_string(d->suit, suit)) - counter++; - } - return counter; -} - -/* - * Merging two dives can be subtle, because there's two different ways - * of merging: - * - * (a) two distinctly _different_ dives that have the same dive computer - * are merged into one longer dive, because the user asked for it - * in the divelist. - * - * Because this case is with teh same dive computer, we *know* the - * two must have a different start time, and "offset" is the relative - * time difference between the two. - * - * (a) two different dive computers that we migth want to merge into - * one single dive with multiple dive computers. - * - * This is the "try_to_merge()" case, which will have offset == 0, - * even if the dive times migth be different. - */ -struct dive *merge_dives(struct dive *a, struct dive *b, int offset, bool prefer_downloaded) -{ - struct dive *res = alloc_dive(); - struct dive *dl = NULL; - - if (offset) { - /* - * If "likely_same_dive()" returns true, that means that - * it is *not* the same dive computer, and we do not want - * to try to turn it into a single longer dive. So we'd - * join them as two separate dive computers at zero offset. - */ - if (likely_same_dive(a, b)) - offset = 0; - } else { - /* Aim for newly downloaded dives to be 'b' (keep old dive data first) */ - if (a->downloaded && !b->downloaded) { - struct dive *tmp = a; - a = b; - b = tmp; - } - if (prefer_downloaded && b->downloaded) - dl = b; - } - - res->when = dl ? dl->when : a->when; - res->selected = a->selected || b->selected; - merge_trip(res, a, b); - MERGE_TXT(res, a, b, notes); - MERGE_TXT(res, a, b, buddy); - MERGE_TXT(res, a, b, divemaster); - MERGE_MAX(res, a, b, rating); - MERGE_TXT(res, a, b, suit); - MERGE_MAX(res, a, b, number); - MERGE_NONZERO(res, a, b, cns); - MERGE_NONZERO(res, a, b, visibility); - MERGE_NONZERO(res, a, b, picture_list); - taglist_merge(&res->tag_list, a->tag_list, b->tag_list); - merge_equipment(res, a, b); - merge_airtemps(res, a, b); - if (dl) { - /* If we prefer downloaded, do those first, and get rid of "might be same" computers */ - join_dive_computers(&res->dc, &dl->dc, &a->dc, 1); - } else if (offset && might_be_same_device(&a->dc, &b->dc)) - interleave_dive_computers(&res->dc, &a->dc, &b->dc, offset); - else - join_dive_computers(&res->dc, &a->dc, &b->dc, 0); - res->dive_site_uuid = a->dive_site_uuid ?: b->dive_site_uuid; - fixup_dive(res); - return res; -} - -// copy_dive(), but retaining the new ID for the copied dive -static struct dive *create_new_copy(struct dive *from) -{ - struct dive *to = alloc_dive(); - int id; - - // alloc_dive() gave us a new ID, we just need to - // make sure it's not overwritten. - id = to->id; - copy_dive(from, to); - to->id = id; - return to; -} - -static void force_fixup_dive(struct dive *d) -{ - struct divecomputer *dc = &d->dc; - int old_maxdepth = dc->maxdepth.mm; - int old_temp = dc->watertemp.mkelvin; - int old_mintemp = d->mintemp.mkelvin; - int old_maxtemp = d->maxtemp.mkelvin; - duration_t old_duration = d->duration; - - d->maxdepth.mm = 0; - dc->maxdepth.mm = 0; - d->watertemp.mkelvin = 0; - dc->watertemp.mkelvin = 0; - d->duration.seconds = 0; - d->maxtemp.mkelvin = 0; - d->mintemp.mkelvin = 0; - - fixup_dive(d); - - if (!d->watertemp.mkelvin) - d->watertemp.mkelvin = old_temp; - - if (!dc->watertemp.mkelvin) - dc->watertemp.mkelvin = old_temp; - - if (!d->maxtemp.mkelvin) - d->maxtemp.mkelvin = old_maxtemp; - - if (!d->mintemp.mkelvin) - d->mintemp.mkelvin = old_mintemp; - - if (!d->duration.seconds) - d->duration = old_duration; - -} - -/* - * Split a dive that has a surface interval from samples 'a' to 'b' - * into two dives. - */ -static int split_dive_at(struct dive *dive, int a, int b) -{ - int i, t, nr; - struct dive *d1, *d2; - struct divecomputer *dc1, *dc2; - struct event *event, **evp; - - /* if we can't find the dive in the dive list, don't bother */ - if ((nr = get_divenr(dive)) < 0) - return 0; - - /* We're not trying to be efficient here.. */ - d1 = create_new_copy(dive); - d2 = create_new_copy(dive); - - /* now unselect the first first segment so we don't keep all - * dives selected by mistake. But do keep the second one selected - * so the algorithm keeps splitting the dive further */ - d1->selected = false; - - dc1 = &d1->dc; - dc2 = &d2->dc; - /* - * Cut off the samples of d1 at the beginning - * of the interval. - */ - dc1->samples = a; - - /* And get rid of the 'b' first samples of d2 */ - dc2->samples -= b; - memmove(dc2->sample, dc2->sample+b, dc2->samples * sizeof(struct sample)); - - /* - * This is where we cut off events from d1, - * and shift everything in d2 - */ - t = dc2->sample[0].time.seconds; - d2->when += t; - for (i = 0; i < dc2->samples; i++) - dc2->sample[i].time.seconds -= t; - - /* Remove the events past 't' from d1 */ - evp = &dc1->events; - while ((event = *evp) != NULL && event->time.seconds < t) - evp = &event->next; - *evp = NULL; - while (event) { - struct event *next = event->next; - free(event); - event = next; - } - - /* Remove the events before 't' from d2, and shift the rest */ - evp = &dc2->events; - while ((event = *evp) != NULL) { - if (event->time.seconds < t) { - *evp = event->next; - free(event); - } else { - event->time.seconds -= t; - } - } - - force_fixup_dive(d1); - force_fixup_dive(d2); - - if (dive->divetrip) { - d1->divetrip = d2->divetrip = 0; - add_dive_to_trip(d1, dive->divetrip); - add_dive_to_trip(d2, dive->divetrip); - } - - delete_single_dive(nr); - add_single_dive(nr, d1); - - /* - * Was the dive numbered? If it was the last dive, then we'll - * increment the dive number for the tail part that we split off. - * Otherwise the tail is unnumbered. - */ - if (d2->number) { - if (dive_table.nr == nr + 1) - d2->number++; - else - d2->number = 0; - } - add_single_dive(nr + 1, d2); - - mark_divelist_changed(true); - - return 1; -} - -/* in freedive mode we split for as little as 10 seconds on the surface, - * otherwise we use a minute */ -static bool should_split(struct divecomputer *dc, int t1, int t2) -{ - int threshold = dc->divemode == FREEDIVE ? 10 : 60; - - return t2 - t1 >= threshold; -} - -/* - * Try to split a dive into multiple dives at a surface interval point. - * - * NOTE! We will not split dives with multiple dive computers, and - * only split when there is at least one surface event that has - * non-surface events on both sides. - * - * In other words, this is a (simplified) reversal of the dive merging. - */ -int split_dive(struct dive *dive) -{ - int i; - int at_surface, surface_start; - struct divecomputer *dc; - - if (!dive || (dc = &dive->dc)->next) - return 0; - - surface_start = 0; - at_surface = 1; - for (i = 1; i < dc->samples; i++) { - struct sample *sample = dc->sample+i; - int surface_sample = sample->depth.mm < SURFACE_THRESHOLD; - - /* - * We care about the transition from and to depth 0, - * not about the depth staying similar. - */ - if (at_surface == surface_sample) - continue; - at_surface = surface_sample; - - // Did it become surface after having been non-surface? We found the start - if (at_surface) { - surface_start = i; - continue; - } - - // Goind down again? We want at least a minute from - // the surface start. - if (!surface_start) - continue; - if (!should_split(dc, dc->sample[surface_start].time.seconds, sample[i - 1].time.seconds)) - continue; - - return split_dive_at(dive, surface_start, i-1); - } - return 0; -} - -/* - * "dc_maxtime()" is how much total time this dive computer - * has for this dive. Note that it can differ from "duration" - * if there are surface events in the middle. - * - * Still, we do ignore all but the last surface samples from the - * end, because some divecomputers just generate lots of them. - */ -static inline int dc_totaltime(const struct divecomputer *dc) -{ - int time = dc->duration.seconds; - int nr = dc->samples; - - while (nr--) { - struct sample *s = dc->sample + nr; - time = s->time.seconds; - if (s->depth.mm >= SURFACE_THRESHOLD) - break; - } - return time; -} - -/* - * The end of a dive is actually not trivial, because "duration" - * is not the duration until the end, but the time we spend under - * water, which can be very different if there are surface events - * during the dive. - * - * So walk the dive computers, looking for the longest actual - * time in the samples (and just default to the dive duration if - * there are no samples). - */ -static inline int dive_totaltime(const struct dive *dive) -{ - int time = dive->duration.seconds; - const struct divecomputer *dc; - - for_each_dc(dive, dc) { - int dc_time = dc_totaltime(dc); - if (dc_time > time) - time = dc_time; - } - return time; -} - -timestamp_t dive_endtime(const struct dive *dive) -{ - return dive->when + dive_totaltime(dive); -} - -struct dive *find_dive_including(timestamp_t when) -{ - int i; - struct dive *dive; - - /* binary search, anyone? Too lazy for now; - * also we always use the duration from the first divecomputer - * could this ever be a problem? */ - for_each_dive (i, dive) { - if (dive->when <= when && when <= dive_endtime(dive)) - return dive; - } - return NULL; -} - -bool time_during_dive_with_offset(struct dive *dive, timestamp_t when, timestamp_t offset) -{ - timestamp_t start = dive->when; - timestamp_t end = dive_endtime(dive); - return start - offset <= when && when <= end + offset; -} - -bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset) -{ - timestamp_t start = dive->when; - timestamp_t end = dive_endtime(dive); - return when - offset <= start && end <= when + offset; -} - -/* find the n-th dive that is part of a group of dives within the offset around 'when'. - * How is that for a vague definition of what this function should do... */ -struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset) -{ - int i, j = 0; - struct dive *dive; - - for_each_dive (i, dive) { - if (dive_within_time_range(dive, when, offset)) - if (++j == n) - return dive; - } - return NULL; -} - -void shift_times(const timestamp_t amount) -{ - int i; - struct dive *dive; - - for_each_dive (i, dive) { - if (!dive->selected) - continue; - dive->when += amount; - } -} - -timestamp_t get_times() -{ - int i; - struct dive *dive; - - for_each_dive (i, dive) { - if (dive->selected) - break; - } - return dive->when; -} - -void set_save_userid_local(short value) -{ - prefs.save_userid_local = value; -} - -void set_userid(char *rUserId) -{ - if (prefs.userid) - free(prefs.userid); - prefs.userid = strdup(rUserId); - if (strlen(prefs.userid) > 30) - prefs.userid[30]='\0'; -} - -/* this sets a usually unused copy of the preferences with the units - * that were active the last time the dive list was saved to git storage - * (this isn't used in XML files); storing the unit preferences in the - * data file is usually pointless (that's a setting of the software, - * not a property of the data), but it's a great hint of what the user - * might expect to see when creating a backend service that visualizes - * the dive list without Subsurface running - so this is basically a - * functionality for the core library that Subsurface itself doesn't - * use but that another consumer of the library (like an HTML exporter) - * will need */ -void set_informational_units(char *units) -{ - if (strstr(units, "METRIC")) { - informational_prefs.unit_system = METRIC; - } else if (strstr(units, "IMPERIAL")) { - informational_prefs.unit_system = IMPERIAL; - } else if (strstr(units, "PERSONALIZE")) { - informational_prefs.unit_system = PERSONALIZE; - if (strstr(units, "METERS")) - informational_prefs.units.length = METERS; - if (strstr(units, "FEET")) - informational_prefs.units.length = FEET; - if (strstr(units, "LITER")) - informational_prefs.units.volume = LITER; - if (strstr(units, "CUFT")) - informational_prefs.units.volume = CUFT; - if (strstr(units, "BAR")) - informational_prefs.units.pressure = BAR; - if (strstr(units, "PSI")) - informational_prefs.units.pressure = PSI; - if (strstr(units, "PASCAL")) - informational_prefs.units.pressure = PASCAL; - if (strstr(units, "CELSIUS")) - informational_prefs.units.temperature = CELSIUS; - if (strstr(units, "FAHRENHEIT")) - informational_prefs.units.temperature = FAHRENHEIT; - if (strstr(units, "KG")) - informational_prefs.units.weight = KG; - if (strstr(units, "LBS")) - informational_prefs.units.weight = LBS; - if (strstr(units, "SECONDS")) - informational_prefs.units.vertical_speed_time = SECONDS; - if (strstr(units, "MINUTES")) - informational_prefs.units.vertical_speed_time = MINUTES; - } -} - -void average_max_depth(struct diveplan *dive, int *avg_depth, int *max_depth) -{ - int integral = 0; - int last_time = 0; - int last_depth = 0; - struct divedatapoint *dp = dive->dp; - - *max_depth = 0; - - while (dp) { - if (dp->time) { - /* Ignore gas indication samples */ - integral += (dp->depth + last_depth) * (dp->time - last_time) / 2; - last_time = dp->time; - last_depth = dp->depth; - if (dp->depth > *max_depth) - *max_depth = dp->depth; - } - dp = dp->next; - } - if (last_time) - *avg_depth = integral / last_time; - else - *avg_depth = *max_depth = 0; -} - -struct picture *alloc_picture() -{ - struct picture *pic = malloc(sizeof(struct picture)); - if (!pic) - exit(1); - memset(pic, 0, sizeof(struct picture)); - return pic; -} - -static bool new_picture_for_dive(struct dive *d, char *filename) -{ - FOR_EACH_PICTURE (d) { - if (same_string(picture->filename, filename)) - return false; - } - return true; -} - -// only add pictures that have timestamps between 30 minutes before the dive and -// 30 minutes after the dive ends -#define D30MIN (30 * 60) -bool dive_check_picture_time(struct dive *d, int shift_time, timestamp_t timestamp) -{ - offset_t offset; - if (timestamp) { - offset.seconds = timestamp - d->when + shift_time; - if (offset.seconds > -D30MIN && offset.seconds < dive_totaltime(d) + D30MIN) { - // this picture belongs to this dive - return true; - } - } - return false; -} - -bool picture_check_valid(char *filename, int shift_time) -{ - int i; - struct dive *dive; - - timestamp_t timestamp = picture_get_timestamp(filename); - for_each_dive (i, dive) - if (dive->selected && dive_check_picture_time(dive, shift_time, timestamp)) - return true; - return false; -} - -void dive_create_picture(struct dive *dive, char *filename, int shift_time, bool match_all) -{ - timestamp_t timestamp = picture_get_timestamp(filename); - if (!new_picture_for_dive(dive, filename)) - return; - if (!match_all && !dive_check_picture_time(dive, shift_time, timestamp)) - return; - - struct picture *picture = alloc_picture(); - picture->filename = strdup(filename); - picture->offset.seconds = timestamp - dive->when + shift_time; - picture_load_exif_data(picture); - - dive_add_picture(dive, picture); - dive_set_geodata_from_picture(dive, picture); -} - -void dive_add_picture(struct dive *dive, struct picture *newpic) -{ - struct picture **pic_ptr = &dive->picture_list; - /* let's keep the list sorted by time */ - while (*pic_ptr && (*pic_ptr)->offset.seconds <= newpic->offset.seconds) - pic_ptr = &(*pic_ptr)->next; - newpic->next = *pic_ptr; - *pic_ptr = newpic; - cache_picture(newpic); - return; -} - -unsigned int dive_get_picture_count(struct dive *dive) -{ - unsigned int i = 0; - FOR_EACH_PICTURE (dive) - i++; - return i; -} - -void dive_set_geodata_from_picture(struct dive *dive, struct picture *picture) -{ - struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); - if (!dive_site_has_gps_location(ds) && (picture->latitude.udeg || picture->longitude.udeg)) { - if (ds) { - ds->latitude = picture->latitude; - ds->longitude = picture->longitude; - } else { - dive->dive_site_uuid = create_dive_site_with_gps("", picture->latitude, picture->longitude, dive->when); - } - } -} - -static void picture_free(struct picture *picture) -{ - if (!picture) - return; - free(picture->filename); - free(picture->hash); - free(picture); -} - -void dive_remove_picture(char *filename) -{ - struct picture **picture = ¤t_dive->picture_list; - while (picture && !same_string((*picture)->filename, filename)) - picture = &(*picture)->next; - if (picture) { - struct picture *temp = (*picture)->next; - picture_free(*picture); - *picture = temp; - } -} - -/* this always acts on the current divecomputer of the current dive */ -void make_first_dc() -{ - struct divecomputer *dc = ¤t_dive->dc; - struct divecomputer *newdc = malloc(sizeof(*newdc)); - struct divecomputer *cur_dc = current_dc; /* needs to be in a local variable so the macro isn't re-executed */ - - /* skip the current DC in the linked list */ - while (dc && dc->next != cur_dc) - dc = dc->next; - if (!dc) { - free(newdc); - fprintf(stderr, "data inconsistent: can't find the current DC"); - return; - } - dc->next = cur_dc->next; - *newdc = current_dive->dc; - current_dive->dc = *cur_dc; - current_dive->dc.next = newdc; - free(cur_dc); -} - -/* always acts on the current dive */ -int count_divecomputers(void) -{ - int ret = 1; - struct divecomputer *dc = current_dive->dc.next; - while (dc) { - ret++; - dc = dc->next; - } - return ret; -} - -/* always acts on the current dive */ -void delete_current_divecomputer(void) -{ - struct divecomputer *dc = current_dc; - - if (dc == ¤t_dive->dc) { - /* remove the first one, so copy the second one in place of the first and free the second one - * be careful about freeing the no longer needed structures - since we copy things around we can't use free_dc()*/ - struct divecomputer *fdc = dc->next; - free(dc->sample); - free((void *)dc->model); - free_events(dc->events); - memcpy(dc, fdc, sizeof(struct divecomputer)); - free(fdc); - } else { - struct divecomputer *pdc = ¤t_dive->dc; - while (pdc->next != dc && pdc->next) - pdc = pdc->next; - if (pdc->next == dc) { - pdc->next = dc->next; - free_dc(dc); - } - } - if (dc_number == count_divecomputers()) - dc_number--; -} - -/* helper function to make it easier to work with our structures - * we don't interpolate here, just use the value from the last sample up to that time */ -int get_depth_at_time(struct divecomputer *dc, int time) -{ - int depth = 0; - if (dc && dc->sample) - for (int i = 0; i < dc->samples; i++) { - if (dc->sample[i].time.seconds > time) - break; - depth = dc->sample[i].depth.mm; - } - return depth; -} diff --git a/dive.h b/dive.h deleted file mode 100644 index cef1106fd..000000000 --- a/dive.h +++ /dev/null @@ -1,894 +0,0 @@ -#ifndef DIVE_H -#define DIVE_H - -#include -#include -#include -#include -#include -#include -#include -#include "divesite.h" - -/* Windows has no MIN/MAX macros - so let's just roll our own */ -#define MIN(x, y) ({ \ - __typeof__(x) _min1 = (x); \ - __typeof__(y) _min2 = (y); \ - (void) (&_min1 == &_min2); \ - _min1 < _min2 ? _min1 : _min2; }) - -#define MAX(x, y) ({ \ - __typeof__(x) _max1 = (x); \ - __typeof__(y) _max2 = (y); \ - (void) (&_max1 == &_max2); \ - _max1 > _max2 ? _max1 : _max2; }) - -#define IS_FP_SAME(_a, _b) (fabs((_a) - (_b)) <= 0.000001 * MAX(fabs(_a), fabs(_b))) - -static inline int same_string(const char *a, const char *b) -{ - return !strcmp(a ?: "", b ?: ""); -} - -static inline char *copy_string(const char *s) -{ - return (s && *s) ? strdup(s) : NULL; -} - -#include -#include -#include - -#include "sha1.h" -#include "units.h" - -#ifdef __cplusplus -extern "C" { -#else -#include -#endif - -extern int last_xml_version; - -enum dive_comp_type {OC, CCR, PSCR, FREEDIVE, NUM_DC_TYPE}; // Flags (Open-circuit and Closed-circuit-rebreather) for setting dive computer type -enum cylinderuse {OC_GAS, DILUENT, OXYGEN, NUM_GAS_USE}; // The different uses for cylinders - -extern const char *cylinderuse_text[]; -extern const char *divemode_text[]; - -struct gasmix { - fraction_t o2; - fraction_t he; -}; - -typedef struct -{ - volume_t size; - pressure_t workingpressure; - const char *description; /* "LP85", "AL72", "AL80", "HP100+" or whatever */ -} cylinder_type_t; - -typedef struct -{ - cylinder_type_t type; - struct gasmix gasmix; - pressure_t start, end, sample_start, sample_end; - depth_t depth; - bool manually_added; - volume_t gas_used; - volume_t deco_gas_used; - enum cylinderuse cylinder_use; -} cylinder_t; - -typedef struct -{ - weight_t weight; - const char *description; /* "integrated", "belt", "ankle" */ -} weightsystem_t; - -/* - * Events are currently based straight on what libdivecomputer gives us. - * We need to wrap these into our own events at some point to remove some of the limitations. - */ -struct event { - struct event *next; - duration_t time; - int type; - /* This is the annoying libdivecomputer format. */ - int flags, value; - /* .. and this is our "extended" data for some event types */ - union { - /* - * Currently only for gas switch events. - * - * NOTE! The index may be -1, which means "unknown". In that - * case, the get_cylinder_index() function will give the best - * match with the cylinders in the dive based on gasmix. - */ - struct { - int index; - struct gasmix mix; - } gas; - }; - bool deleted; - char name[]; -}; - -extern int event_is_gaschange(struct event *ev); -extern int event_gasmix_redundant(struct event *ev); - -extern int get_pressure_units(int mb, const char **units); -extern double get_depth_units(int mm, int *frac, const char **units); -extern double get_volume_units(unsigned int ml, int *frac, const char **units); -extern double get_temp_units(unsigned int mk, const char **units); -extern double get_weight_units(unsigned int grams, int *frac, const char **units); -extern double get_vertical_speed_units(unsigned int mms, int *frac, const char **units); - -extern unsigned int units_to_depth(double depth); -extern int units_to_sac(double volume); - -/* Volume in mliter of a cylinder at pressure 'p' */ -extern int gas_volume(cylinder_t *cyl, pressure_t p); -extern int wet_volume(double cuft, pressure_t p); - - -static inline int get_o2(const struct gasmix *mix) -{ - return mix->o2.permille ?: O2_IN_AIR; -} - -static inline int get_he(const struct gasmix *mix) -{ - return mix->he.permille; -} - -struct gas_pressures { - double o2, n2, he; -}; - -extern void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, const struct gasmix *mix, double po2, enum dive_comp_type dctype); - -extern void sanitize_gasmix(struct gasmix *mix); -extern int gasmix_distance(const struct gasmix *a, const struct gasmix *b); -extern struct gasmix *get_gasmix_from_event(struct event *ev); - -static inline bool gasmix_is_air(const struct gasmix *gasmix) -{ - int o2 = gasmix->o2.permille; - int he = gasmix->he.permille; - return (he == 0) && (o2 == 0 || ((o2 >= O2_IN_AIR - 1) && (o2 <= O2_IN_AIR + 1))); -} - -/* Linear interpolation between 'a' and 'b', when we are 'part'way into the 'whole' distance from a to b */ -static inline int interpolate(int a, int b, int part, int whole) -{ - /* It is doubtful that we actually need floating point for this, but whatever */ - double x = (double)a * (whole - part) + (double)b * part; - return rint(x / whole); -} - -void get_gas_string(const struct gasmix *gasmix, char *text, int len); -const char *gasname(const struct gasmix *gasmix); - -struct sample // BASE TYPE BYTES UNITS RANGE DESCRIPTION -{ // --------- ----- ----- ----- ----------- - duration_t time; // uint32_t 4 seconds (0-68 yrs) elapsed dive time up to this sample - duration_t stoptime; // uint32_t 4 seconds (0-18 h) time duration of next deco stop - duration_t ndl; // uint32_t 4 seconds (0-18 h) time duration before no-deco limit - duration_t tts; // uint32_t 4 seconds (0-18 h) time duration to reach the surface - duration_t rbt; // uint32_t 4 seconds (0-18 h) remaining bottom time - depth_t depth; // int32_t 4 mm (0-2000 km) dive depth of this sample - depth_t stopdepth; // int32_t 4 mm (0-2000 km) depth of next deco stop - temperature_t temperature; // int32_t 4 mdegrK (0-2 MdegK) ambient temperature - pressure_t cylinderpressure; // int32_t 4 mbar (0-2 Mbar) main cylinder pressure - pressure_t o2cylinderpressure; // int32_t 4 mbar (0-2 Mbar) CCR o2 cylinder pressure (rebreather) - o2pressure_t setpoint; // uint16_t 2 mbar (0-65 bar) O2 partial pressure (will be setpoint) - o2pressure_t o2sensor[3]; // uint16_t 6 mbar (0-65 bar) Up to 3 PO2 sensor values (rebreather) - bearing_t bearing; // int16_t 2 degrees (-32k to 32k deg) compass bearing - uint8_t sensor; // uint8_t 1 sensorID (0-255) ID of cylinder pressure sensor - uint8_t cns; // uint8_t 1 % (0-255 %) cns% accumulated - uint8_t heartbeat; // uint8_t 1 beats/m (0-255) heart rate measurement - volume_t sac; // 4 ml/min predefined SAC - bool in_deco; // bool 1 y/n y/n this sample is part of deco - bool manually_entered; // bool 1 y/n y/n this sample was entered by the user, - // not calculated when planning a dive -}; // Total size of structure: 57 bytes, excluding padding at end - -struct divetag { - /* - * The name of the divetag. If a translation is available, name contains - * the translated tag - */ - char *name; - /* - * If a translation is available, we write the original tag to source. - * This enables us to write a non-localized tag to the xml file. - */ - char *source; -}; - -struct tag_entry { - struct divetag *tag; - struct tag_entry *next; -}; - -/* - * divetags are only stored once, each dive only contains - * a list of tag_entries which then point to the divetags - * in the global g_tag_list - */ - -extern struct tag_entry *g_tag_list; - -struct divetag *taglist_add_tag(struct tag_entry **tag_list, const char *tag); -struct tag_entry *taglist_added(struct tag_entry *original_list, struct tag_entry *new_list); -void dump_taglist(const char *intro, struct tag_entry *tl); - -/* - * Writes all divetags in tag_list to buffer, limited by the buffer's (len)gth. - * Returns the characters written - */ -int taglist_get_tagstring(struct tag_entry *tag_list, char *buffer, int len); - -/* cleans up a list: removes empty tags and duplicates */ -void taglist_cleanup(struct tag_entry **tag_list); - -void taglist_init_global(); -void taglist_free(struct tag_entry *tag_list); - -bool taglist_contains(struct tag_entry *tag_list, const char *tag); -bool taglist_equal(struct tag_entry *tl1, struct tag_entry *tl2); -int count_dives_with_tag(const char *tag); -int count_dives_with_person(const char *person); -int count_dives_with_location(const char *location); -int count_dives_with_suit(const char *suit); - -struct extra_data { - const char *key; - const char *value; - struct extra_data *next; -}; - -/* - * NOTE! The deviceid and diveid are model-specific *hashes* of - * whatever device identification that model may have. Different - * dive computers will have different identifying data, it could - * be a firmware number or a serial ID (in either string or in - * numeric format), and we do not care. - * - * The only thing we care about is that subsurface will hash - * that information the same way. So then you can check the ID - * of a dive computer by comparing the hashes for equality. - * - * A deviceid or diveid of zero is assumed to be "no ID". - */ -struct divecomputer { - timestamp_t when; - duration_t duration, surfacetime; - depth_t maxdepth, meandepth; - temperature_t airtemp, watertemp; - pressure_t surface_pressure; - enum dive_comp_type divemode; // dive computer type: OC(default) or CCR - uint8_t no_o2sensors; // rebreathers: number of O2 sensors used - int salinity; // kg per 10000 l - const char *model, *serial, *fw_version; - uint32_t deviceid, diveid; - int samples, alloc_samples; - struct sample *sample; - struct event *events; - struct extra_data *extra_data; - struct divecomputer *next; -}; - -#define MAX_CYLINDERS (8) -#define MAX_WEIGHTSYSTEMS (6) -#define W_IDX_PRIMARY 0 -#define W_IDX_SECONDARY 1 - -typedef enum { - TF_NONE, - NO_TRIP, - IN_TRIP, - ASSIGNED_TRIP, - NUM_TRIPFLAGS -} tripflag_t; - -typedef struct dive_trip -{ - timestamp_t when; - char *location; - char *notes; - struct dive *dives; - int nrdives; - int index; - unsigned expanded : 1, selected : 1, autogen : 1, fixup : 1; - struct dive_trip *next; -} dive_trip_t; - -/* List of dive trips (sorted by date) */ -extern dive_trip_t *dive_trip_list; -struct picture; -struct dive { - int number; - tripflag_t tripflag; - dive_trip_t *divetrip; - struct dive *next, **pprev; - bool selected; - bool hidden_by_filter; - bool downloaded; - timestamp_t when; - uint32_t dive_site_uuid; - char *notes; - char *divemaster, *buddy; - int rating; - int visibility; /* 0 - 5 star rating */ - cylinder_t cylinder[MAX_CYLINDERS]; - weightsystem_t weightsystem[MAX_WEIGHTSYSTEMS]; - char *suit; - int sac, otu, cns, maxcns; - - /* Calculated based on dive computer data */ - temperature_t mintemp, maxtemp, watertemp, airtemp; - depth_t maxdepth, meandepth; - pressure_t surface_pressure; - duration_t duration; - int salinity; // kg per 10000 l - - struct tag_entry *tag_list; - struct divecomputer dc; - int id; // unique ID for this dive - struct picture *picture_list; - int oxygen_cylinder_index, diluent_cylinder_index; // CCR dive cylinder indices -}; - -extern int get_cylinder_idx_by_use(struct dive *dive, enum cylinderuse cylinder_use_type); -extern void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]); - -/* when selectively copying dive information, which parts should be copied? */ -struct dive_components { - unsigned int divesite : 1; - unsigned int notes : 1; - unsigned int divemaster : 1; - unsigned int buddy : 1; - unsigned int suit : 1; - unsigned int rating : 1; - unsigned int visibility : 1; - unsigned int tags : 1; - unsigned int cylinders : 1; - unsigned int weights : 1; -}; - -/* picture list and methods related to dive picture handling */ -struct picture { - char *filename; - char *hash; - offset_t offset; - degrees_t latitude; - degrees_t longitude; - struct picture *next; -}; - -#define FOR_EACH_PICTURE(_dive) \ - if (_dive) \ - for (struct picture *picture = (_dive)->picture_list; picture; picture = picture->next) - -#define FOR_EACH_PICTURE_NON_PTR(_divestruct) \ - for (struct picture *picture = (_divestruct).picture_list; picture; picture = picture->next) - -extern struct picture *alloc_picture(); -extern bool dive_check_picture_time(struct dive *d, int shift_time, timestamp_t timestamp); -extern void dive_create_picture(struct dive *d, char *filename, int shift_time, bool match_all); -extern void dive_add_picture(struct dive *d, struct picture *newpic); -extern void dive_remove_picture(char *filename); -extern unsigned int dive_get_picture_count(struct dive *d); -extern bool picture_check_valid(char *filename, int shift_time); -extern void picture_load_exif_data(struct picture *p); -extern timestamp_t picture_get_timestamp(char *filename); -extern void dive_set_geodata_from_picture(struct dive *d, struct picture *pic); - -extern int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc); -extern int get_depth_at_time(struct divecomputer *dc, int time); - -static inline int get_surface_pressure_in_mbar(const struct dive *dive, bool non_null) -{ - int mbar = dive->surface_pressure.mbar; - if (!mbar && non_null) - mbar = SURFACE_PRESSURE; - return mbar; -} - -/* Pa = N/m^2 - so we determine the weight (in N) of the mass of 10m - * of water (and use standard salt water at 1.03kg per liter if we don't know salinity) - * and add that to the surface pressure (or to 1013 if that's unknown) */ -static inline int calculate_depth_to_mbar(int depth, pressure_t surface_pressure, int salinity) -{ - double specific_weight; - int mbar = surface_pressure.mbar; - - if (!mbar) - mbar = SURFACE_PRESSURE; - if (!salinity) - salinity = SEAWATER_SALINITY; - specific_weight = salinity / 10000.0 * 0.981; - mbar += rint(depth / 10.0 * specific_weight); - return mbar; -} - -static inline int depth_to_mbar(int depth, struct dive *dive) -{ - return calculate_depth_to_mbar(depth, dive->surface_pressure, dive->salinity); -} - -static inline double depth_to_bar(int depth, struct dive *dive) -{ - return depth_to_mbar(depth, dive) / 1000.0; -} - -static inline double depth_to_atm(int depth, struct dive *dive) -{ - return mbar_to_atm(depth_to_mbar(depth, dive)); -} - -/* for the inverse calculation we use just the relative pressure - * (that's the one that some dive computers like the Uemis Zurich - * provide - for the other models that do this libdivecomputer has to - * take care of this, but the Uemis we support natively */ -static inline int rel_mbar_to_depth(int mbar, struct dive *dive) -{ - int cm; - double specific_weight = 1.03 * 0.981; - if (dive->dc.salinity) - specific_weight = dive->dc.salinity / 10000.0 * 0.981; - /* whole mbar gives us cm precision */ - cm = rint(mbar / specific_weight); - return cm * 10; -} - -static inline int mbar_to_depth(int mbar, struct dive *dive) -{ - pressure_t surface_pressure; - if (dive->surface_pressure.mbar) - surface_pressure = dive->surface_pressure; - else - surface_pressure.mbar = SURFACE_PRESSURE; - return rel_mbar_to_depth(mbar - surface_pressure.mbar, dive); -} - -/* MOD rounded to multiples of roundto mm */ -static inline depth_t gas_mod(struct gasmix *mix, pressure_t po2_limit, struct dive *dive, int roundto) { - depth_t rounded_depth; - - double depth = (double) mbar_to_depth(po2_limit.mbar * 1000 / get_o2(mix), dive); - rounded_depth.mm = rint(depth / roundto) * roundto; - return rounded_depth; -} - -#define SURFACE_THRESHOLD 750 /* somewhat arbitrary: only below 75cm is it really diving */ - -/* this is a global spot for a temporary dive structure that we use to - * be able to edit a dive without unintended side effects */ -extern struct dive edit_dive; - -extern short autogroup; -/* random threashold: three days without diving -> new trip - * this works very well for people who usually dive as part of a trip and don't - * regularly dive at a local facility; this is why trips are an optional feature */ -#define TRIP_THRESHOLD 3600 * 24 * 3 - -#define UNGROUPED_DIVE(_dive) ((_dive)->tripflag == NO_TRIP) -#define DIVE_IN_TRIP(_dive) ((_dive)->tripflag == IN_TRIP || (_dive)->tripflag == ASSIGNED_TRIP) -#define DIVE_NEEDS_TRIP(_dive) ((_dive)->tripflag == TF_NONE) - -extern void add_dive_to_trip(struct dive *, dive_trip_t *); - -extern void delete_single_dive(int idx); -extern void add_single_dive(int idx, struct dive *dive); - -extern void insert_trip(dive_trip_t **trip); - - -extern const struct units SI_units, IMPERIAL_units; -extern struct units xml_parsing_units; - -extern struct units *get_units(void); -extern int run_survey, verbose, quit; - -struct dive_table { - int nr, allocated, preexisting; - struct dive **dives; -}; - -extern struct dive_table dive_table, downloadTable; -extern struct dive displayed_dive; -extern struct dive_site displayed_dive_site; -extern int selected_dive; -extern unsigned int dc_number; -#define current_dive (get_dive(selected_dive)) -#define current_dc (get_dive_dc(current_dive, dc_number)) - -static inline struct dive *get_dive(int nr) -{ - if (nr >= dive_table.nr || nr < 0) - return NULL; - return dive_table.dives[nr]; -} - -static inline struct dive *get_dive_from_table(int nr, struct dive_table *dt) -{ - if (nr >= dt->nr || nr < 0) - return NULL; - return dt->dives[nr]; -} - -static inline struct dive_site *get_dive_site_for_dive(struct dive *dive) -{ - if (dive) - return get_dive_site_by_uuid(dive->dive_site_uuid); - return NULL; -} - -static inline char *get_dive_location(struct dive *dive) -{ - struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); - if (ds && ds->name) - return ds->name; - return NULL; -} - -static inline unsigned int number_of_computers(struct dive *dive) -{ - unsigned int total_number = 0; - struct divecomputer *dc = &dive->dc; - - if (!dive) - return 1; - - do { - total_number++; - dc = dc->next; - } while (dc); - return total_number; -} - -static inline struct divecomputer *get_dive_dc(struct dive *dive, int nr) -{ - struct divecomputer *dc = &dive->dc; - - while (nr-- > 0) { - dc = dc->next; - if (!dc) - return &dive->dc; - } - return dc; -} - -extern timestamp_t dive_endtime(const struct dive *dive); - -extern void make_first_dc(void); -extern int count_divecomputers(void); -extern void delete_current_divecomputer(void); - -/* - * Iterate over each dive, with the first parameter being the index - * iterator variable, and the second one being the dive one. - * - * I don't think anybody really wants the index, and we could make - * it local to the for-loop, but that would make us requires C99. - */ -#define for_each_dive(_i, _x) \ - for ((_i) = 0; ((_x) = get_dive(_i)) != NULL; (_i)++) - -#define for_each_dc(_dive, _dc) \ - for (_dc = &_dive->dc; _dc; _dc = _dc->next) - -#define for_each_gps_location(_i, _x) \ - for ((_i) = 0; ((_x) = get_gps_location(_i, &gps_location_table)) != NULL; (_i)++) - -static inline struct dive *get_dive_by_uniq_id(int id) -{ - int i; - struct dive *dive = NULL; - - for_each_dive (i, dive) { - if (dive->id == id) - break; - } -#ifdef DEBUG - if (dive == NULL) { - fprintf(stderr, "Invalid id %x passed to get_dive_by_diveid, try to fix the code\n", id); - exit(1); - } -#endif - return dive; -} - -static inline int get_idx_by_uniq_id(int id) -{ - int i; - struct dive *dive = NULL; - - for_each_dive (i, dive) { - if (dive->id == id) - break; - } -#ifdef DEBUG - if (dive == NULL) { - fprintf(stderr, "Invalid id %x passed to get_dive_by_diveid, try to fix the code\n", id); - exit(1); - } -#endif - return i; -} - -static inline bool dive_site_has_gps_location(struct dive_site *ds) -{ - return ds && (ds->latitude.udeg || ds->longitude.udeg); -} - -static inline int dive_has_gps_location(struct dive *dive) -{ - if (!dive) - return false; - return dive_site_has_gps_location(get_dive_site_by_uuid(dive->dive_site_uuid)); -} - -#ifdef __cplusplus -extern "C" { -#endif - -extern int report_error(const char *fmt, ...); -extern const char *get_error_string(void); - -extern struct dive *find_dive_including(timestamp_t when); -extern bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset); -extern bool time_during_dive_with_offset(struct dive *dive, timestamp_t when, timestamp_t offset); -struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset); - -/* Check if two dive computer entries are the exact same dive (-1=no/0=maybe/1=yes) */ -extern int match_one_dc(struct divecomputer *a, struct divecomputer *b); - -extern void parse_xml_init(void); -extern int parse_xml_buffer(const char *url, const char *buf, int size, struct dive_table *table, const char **params); -extern void parse_xml_exit(void); -extern void set_filename(const char *filename, bool force); - -extern int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); -extern int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); -extern int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); -extern int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); -extern int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); -extern int parse_dlf_buffer(unsigned char *buffer, size_t size); - -extern int parse_file(const char *filename); -extern int parse_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate); -extern int parse_seabear_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate); -extern int parse_txt_file(const char *filename, const char *csv); -extern int parse_manual_file(const char *filename, char **params, int pnr); -extern int save_dives(const char *filename); -extern int save_dives_logic(const char *filename, bool select_only); -extern int save_dive(FILE *f, struct dive *dive); -extern int export_dives_xslt(const char *filename, const bool selected, const int units, const char *export_xslt); - -struct membuffer; -extern void save_one_dive_to_mb(struct membuffer *b, struct dive *dive); - -int cylinderuse_from_text(const char *text); - - -struct user_info { - const char *name; - const char *email; -}; - -extern void subsurface_user_info(struct user_info *); -extern int subsurface_rename(const char *path, const char *newpath); -extern int subsurface_dir_rename(const char *path, const char *newpath); -extern int subsurface_open(const char *path, int oflags, mode_t mode); -extern FILE *subsurface_fopen(const char *path, const char *mode); -extern void *subsurface_opendir(const char *path); -extern int subsurface_access(const char *path, int mode); -extern struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp); -extern int subsurface_zip_close(struct zip *zip); -extern void subsurface_console_init(bool dedicated); -extern void subsurface_console_exit(void); - -extern void shift_times(const timestamp_t amount); -extern timestamp_t get_times(); - -extern xsltStylesheetPtr get_stylesheet(const char *name); - -extern timestamp_t utc_mktime(struct tm *tm); -extern void utc_mkdate(timestamp_t, struct tm *tm); - -extern struct dive *alloc_dive(void); -extern void record_dive_to_table(struct dive *dive, struct dive_table *table); -extern void record_dive(struct dive *dive); -extern void clear_dive(struct dive *dive); -extern void copy_dive(struct dive *s, struct dive *d); -extern void selective_copy_dive(struct dive *s, struct dive *d, struct dive_components what, bool clear); -extern struct dive *clone_dive(struct dive *s); - -extern void clear_table(struct dive_table *table); - -extern struct sample *prepare_sample(struct divecomputer *dc); -extern void finish_sample(struct divecomputer *dc); - -extern bool has_hr_data(struct divecomputer *dc); - -extern void sort_table(struct dive_table *table); -extern struct dive *fixup_dive(struct dive *dive); -extern void fixup_dc_duration(struct divecomputer *dc); -extern int dive_getUniqID(struct dive *d); -extern unsigned int dc_airtemp(struct divecomputer *dc); -extern unsigned int dc_watertemp(struct divecomputer *dc); -extern int split_dive(struct dive *); -extern struct dive *merge_dives(struct dive *a, struct dive *b, int offset, bool prefer_downloaded); -extern struct dive *try_to_merge(struct dive *a, struct dive *b, bool prefer_downloaded); -extern void renumber_dives(int start_nr, bool selected_only); -extern void copy_events(struct divecomputer *s, struct divecomputer *d); -extern void free_events(struct event *ev); -extern void copy_cylinders(struct dive *s, struct dive *d, bool used_only); -extern void copy_samples(struct divecomputer *s, struct divecomputer *d); -extern bool is_cylinder_used(struct dive *dive, int idx); -extern void fill_default_cylinder(cylinder_t *cyl); -extern void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int time, int idx); -extern struct event *add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name); -extern void remove_event(struct event *event); -extern void update_event_name(struct dive *d, struct event* event, char *name); -extern void add_extra_data(struct divecomputer *dc, const char *key, const char *value); -extern void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration); -extern int get_cylinder_index(struct dive *dive, struct event *ev); -extern int nr_cylinders(struct dive *dive); -extern int nr_weightsystems(struct dive *dive); - -/* UI related protopypes */ - -// extern void report_error(GError* error); - -extern void add_cylinder_description(cylinder_type_t *); -extern void add_weightsystem_description(weightsystem_t *); -extern void remember_event(const char *eventname); - -#if WE_DONT_USE_THIS /* this is a missing feature in Qt - selecting which events to display */ -extern int evn_foreach(void (*callback)(const char *, bool *, void *), void *data); -#endif /* WE_DONT_USE_THIS */ - -extern void clear_events(void); - -extern void set_dc_nickname(struct dive *dive); -extern void set_autogroup(bool value); -extern int total_weight(struct dive *); - -#ifdef __cplusplus -} -#endif - -#define DIVE_ERROR_PARSE 1 -#define DIVE_ERROR_PLAN 2 - -const char *weekday(int wday); -const char *monthname(int mon); - -#define UTF8_DEGREE "\xc2\xb0" -#define UTF8_DELTA "\xce\x94" -#define UTF8_UPWARDS_ARROW "\xE2\x86\x91" -#define UTF8_DOWNWARDS_ARROW "\xE2\x86\x93" -#define UTF8_AVERAGE "\xc3\xb8" -#define UTF8_SUBSCRIPT_2 "\xe2\x82\x82" -#define UTF8_WHITESTAR "\xe2\x98\x86" -#define UTF8_BLACKSTAR "\xe2\x98\x85" - -extern const char *existing_filename; -extern void subsurface_command_line_init(int *, char ***); -extern void subsurface_command_line_exit(int *, char ***); - -#define FRACTION(n, x) ((unsigned)(n) / (x)), ((unsigned)(n) % (x)) - -extern void add_segment(double pressure, const struct gasmix *gasmix, int period_in_seconds, int setpoint, const struct dive *dive, int sac); -extern void clear_deco(double surface_pressure); -extern void dump_tissues(void); -extern unsigned int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth); -extern void set_gf(short gflow, short gfhigh, bool gf_low_at_maxdepth); -extern void cache_deco_state(char **datap); -extern void restore_deco_state(char *data); -extern void nuclear_regeneration(double time); -extern void vpmb_start_gradient(); -extern void vpmb_next_gradient(double deco_time, double surface_pressure); -extern double tissue_tolerance_calc(const struct dive *dive, double pressure); - -/* this should be converted to use our types */ -struct divedatapoint { - int time; - unsigned int depth; - struct gasmix gasmix; - int setpoint; - bool entered; - struct divedatapoint *next; -}; - -struct diveplan { - timestamp_t when; - int surface_pressure; /* mbar */ - int bottomsac; /* ml/min */ - int decosac; /* ml/min */ - int salinity; - short gflow; - short gfhigh; - struct divedatapoint *dp; -}; - -struct divedatapoint *plan_add_segment(struct diveplan *diveplan, int duration, int depth, struct gasmix gasmix, int po2, bool entered); -struct divedatapoint *create_dp(int time_incr, int depth, struct gasmix gasmix, int po2); -#if DEBUG_PLAN -void dump_plan(struct diveplan *diveplan); -#endif -bool plan(struct diveplan *diveplan, char **cached_datap, bool is_planner, bool show_disclaimer); -void calc_crushing_pressure(double pressure); -void vpmb_start_gradient(); - -void delete_single_dive(int idx); - -struct event *get_next_event(struct event *event, const char *name); - - -/* these structs holds the information that - * describes the cylinders / weight systems. - * they are global variables initialized in equipment.c - * used to fill the combobox in the add/edit cylinder - * dialog - */ - -struct tank_info_t { - const char *name; - int cuft, ml, psi, bar; -}; -extern struct tank_info_t tank_info[100]; - -struct ws_info_t { - const char *name; - int grams; -}; -extern struct ws_info_t ws_info[100]; - -extern bool cylinder_nodata(cylinder_t *cyl); -extern bool cylinder_none(void *_data); -extern bool weightsystem_none(void *_data); -extern bool no_weightsystems(weightsystem_t *ws); -extern bool weightsystems_equal(weightsystem_t *ws1, weightsystem_t *ws2); -extern void remove_cylinder(struct dive *dive, int idx); -extern void remove_weightsystem(struct dive *dive, int idx); -extern void reset_cylinders(struct dive *dive, bool track_gas); - -/* - * String handling. - */ -#define STRTOD_NO_SIGN 0x01 -#define STRTOD_NO_DOT 0x02 -#define STRTOD_NO_COMMA 0x04 -#define STRTOD_NO_EXPONENT 0x08 -extern double strtod_flags(const char *str, const char **ptr, unsigned int flags); - -#define STRTOD_ASCII (STRTOD_NO_COMMA) - -#define ascii_strtod(str, ptr) strtod_flags(str, ptr, STRTOD_ASCII) - -extern void set_save_userid_local(short value); -extern void set_userid(char *user_id); -extern void set_informational_units(char *units); - -extern const char *get_dive_date_c_string(timestamp_t when); -extern void update_setpoint_events(struct divecomputer *dc); -#ifdef __cplusplus -} -#endif - -extern weight_t string_to_weight(const char *str); -extern depth_t string_to_depth(const char *str); -extern pressure_t string_to_pressure(const char *str); -extern volume_t string_to_volume(const char *str, pressure_t workp); -extern fraction_t string_to_fraction(const char *str); -extern void average_max_depth(struct diveplan *dive, int *avg_depth, int *max_depth); - -#include "pref.h" - -#endif // DIVE_H diff --git a/divecomputer.cpp b/divecomputer.cpp deleted file mode 100644 index e4081e1cd..000000000 --- a/divecomputer.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "divecomputer.h" -#include "dive.h" - -#include - -const char *default_dive_computer_vendor; -const char *default_dive_computer_product; -const char *default_dive_computer_device; -int default_dive_computer_download_mode; -DiveComputerList dcList; - -DiveComputerList::DiveComputerList() -{ -} - -DiveComputerList::~DiveComputerList() -{ -} - -bool DiveComputerNode::operator==(const DiveComputerNode &a) const -{ - return model == a.model && - deviceId == a.deviceId && - firmware == a.firmware && - serialNumber == a.serialNumber && - nickName == a.nickName; -} - -bool DiveComputerNode::operator!=(const DiveComputerNode &a) const -{ - return !(*this == a); -} - -bool DiveComputerNode::changesValues(const DiveComputerNode &b) const -{ - if (model != b.model || deviceId != b.deviceId) { - qDebug("DiveComputerNodes were not for the same DC"); - return false; - } - return (firmware != b.firmware) || - (serialNumber != b.serialNumber) || - (nickName != b.nickName); -} - -const DiveComputerNode *DiveComputerList::getExact(const QString &m, uint32_t d) -{ - for (QMap::iterator it = dcMap.find(m); it != dcMap.end() && it.key() == m; ++it) - if (it->deviceId == d) - return &*it; - return NULL; -} - -const DiveComputerNode *DiveComputerList::get(const QString &m) -{ - QMap::iterator it = dcMap.find(m); - if (it != dcMap.end()) - return &*it; - return NULL; -} - -void DiveComputerNode::showchanges(const QString &n, const QString &s, const QString &f) const -{ - if (nickName != n) - qDebug("new nickname %s for DC model %s deviceId 0x%x", n.toUtf8().data(), model.toUtf8().data(), deviceId); - if (serialNumber != s) - qDebug("new serial number %s for DC model %s deviceId 0x%x", s.toUtf8().data(), model.toUtf8().data(), deviceId); - if (firmware != f) - qDebug("new firmware version %s for DC model %s deviceId 0x%x", f.toUtf8().data(), model.toUtf8().data(), deviceId); -} - -void DiveComputerList::addDC(QString m, uint32_t d, QString n, QString s, QString f) -{ - if (m.isEmpty() || d == 0) - return; - const DiveComputerNode *existNode = this->getExact(m, d); - - if (existNode) { - // Update any non-existent fields from the old entry - if (n.isEmpty()) - n = existNode->nickName; - if (s.isEmpty()) - s = existNode->serialNumber; - if (f.isEmpty()) - f = existNode->firmware; - - // Do all the old values match? - if (n == existNode->nickName && s == existNode->serialNumber && f == existNode->firmware) - return; - - // debugging: show changes - if (verbose) - existNode->showchanges(n, s, f); - dcMap.remove(m, *existNode); - } - - DiveComputerNode newNode(m, d, s, f, n); - dcMap.insert(m, newNode); -} - -extern "C" void create_device_node(const char *model, uint32_t deviceid, const char *serial, const char *firmware, const char *nickname) -{ - dcList.addDC(model, deviceid, nickname, serial, firmware); -} - -extern "C" bool compareDC(const DiveComputerNode &a, const DiveComputerNode &b) -{ - return a.deviceId < b.deviceId; -} - -extern "C" void call_for_each_dc (void *f, void (*callback)(void *, const char *, uint32_t, - const char *, const char *, const char *), - bool select_only) -{ - QList values = dcList.dcMap.values(); - qSort(values.begin(), values.end(), compareDC); - for (int i = 0; i < values.size(); i++) { - const DiveComputerNode *node = &values.at(i); - bool found = false; - if (select_only) { - int j; - struct dive *d; - for_each_dive (j, d) { - struct divecomputer *dc; - if (!d->selected) - continue; - for_each_dc(d, dc) { - if (dc->deviceid == node->deviceId) { - found = true; - break; - } - } - if (found) - break; - } - } else { - found = true; - } - if (found) - callback(f, node->model.toUtf8().data(), node->deviceId, node->nickName.toUtf8().data(), - node->serialNumber.toUtf8().data(), node->firmware.toUtf8().data()); - } -} - - -extern "C" int is_default_dive_computer(const char *vendor, const char *product) -{ - return default_dive_computer_vendor && !strcmp(vendor, default_dive_computer_vendor) && - default_dive_computer_product && !strcmp(product, default_dive_computer_product); -} - -extern "C" int is_default_dive_computer_device(const char *name) -{ - return default_dive_computer_device && !strcmp(name, default_dive_computer_device); -} - -void set_default_dive_computer(const char *vendor, const char *product) -{ - QSettings s; - - if (!vendor || !*vendor) - return; - if (!product || !*product) - return; - if (is_default_dive_computer(vendor, product)) - return; - - free((void *)default_dive_computer_vendor); - free((void *)default_dive_computer_product); - default_dive_computer_vendor = strdup(vendor); - default_dive_computer_product = strdup(product); - s.beginGroup("DiveComputer"); - s.setValue("dive_computer_vendor", vendor); - s.setValue("dive_computer_product", product); - s.endGroup(); -} - -void set_default_dive_computer_device(const char *name) -{ - QSettings s; - - if (!name || !*name) - return; - if (is_default_dive_computer_device(name)) - return; - - free((void *)default_dive_computer_device); - default_dive_computer_device = strdup(name); - s.beginGroup("DiveComputer"); - s.setValue("dive_computer_device", name); - s.endGroup(); -} - -void set_default_dive_computer_download_mode(int download_mode) -{ - QSettings s; - - default_dive_computer_download_mode = download_mode; - s.beginGroup("DiveComputer"); - s.setValue("dive_computer_download_mode", download_mode); - s.endGroup(); -} - -extern "C" void set_dc_nickname(struct dive *dive) -{ - if (!dive) - return; - - struct divecomputer *dc; - - for_each_dc (dive, dc) { - if (dc->model && *dc->model && dc->deviceid && - !dcList.getExact(dc->model, dc->deviceid)) { - // we don't have this one, yet - const DiveComputerNode *existNode = dcList.get(dc->model); - if (existNode) { - // we already have this model but a different deviceid - QString simpleNick(dc->model); - if (dc->deviceid == 0) - simpleNick.append(" (unknown deviceid)"); - else - simpleNick.append(" (").append(QString::number(dc->deviceid, 16)).append(")"); - dcList.addDC(dc->model, dc->deviceid, simpleNick); - } else { - dcList.addDC(dc->model, dc->deviceid); - } - } - } -} diff --git a/divecomputer.h b/divecomputer.h deleted file mode 100644 index 98d12ab8b..000000000 --- a/divecomputer.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef DIVECOMPUTER_H -#define DIVECOMPUTER_H - -#include -#include -#include - -class DiveComputerNode { -public: - DiveComputerNode(QString m, uint32_t d, QString s, QString f, QString n) - : model(m), deviceId(d), serialNumber(s), firmware(f), nickName(n) {}; - bool operator==(const DiveComputerNode &a) const; - bool operator!=(const DiveComputerNode &a) const; - bool changesValues(const DiveComputerNode &b) const; - void showchanges(const QString &n, const QString &s, const QString &f) const; - QString model; - uint32_t deviceId; - QString serialNumber; - QString firmware; - QString nickName; -}; - -class DiveComputerList { -public: - DiveComputerList(); - ~DiveComputerList(); - const DiveComputerNode *getExact(const QString &m, uint32_t d); - const DiveComputerNode *get(const QString &m); - void addDC(QString m, uint32_t d, QString n = QString(), QString s = QString(), QString f = QString()); - DiveComputerNode matchDC(const QString &m, uint32_t d); - DiveComputerNode matchModel(const QString &m); - QMultiMap dcMap; - QMultiMap dcWorkingMap; -}; - -extern DiveComputerList dcList; - -#endif diff --git a/divelist.c b/divelist.c deleted file mode 100644 index a14fabf88..000000000 --- a/divelist.c +++ /dev/null @@ -1,1141 +0,0 @@ -/* divelist.c */ -/* core logic for the dive list - - * accessed through the following interfaces: - * - * dive_trip_t *dive_trip_list; - * unsigned int amount_selected; - * void dump_selection(void) - * dive_trip_t *find_trip_by_idx(int idx) - * int trip_has_selected_dives(dive_trip_t *trip) - * void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p) - * int total_weight(struct dive *dive) - * int get_divenr(struct dive *dive) - * double init_decompression(struct dive *dive) - * void update_cylinder_related_info(struct dive *dive) - * void dump_trip_list(void) - * dive_trip_t *find_matching_trip(timestamp_t when) - * void insert_trip(dive_trip_t **dive_trip_p) - * void remove_dive_from_trip(struct dive *dive) - * void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) - * dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) - * void autogroup_dives(void) - * void delete_single_dive(int idx) - * void add_single_dive(int idx, struct dive *dive) - * void merge_two_dives(struct dive *a, struct dive *b) - * void select_dive(int idx) - * void deselect_dive(int idx) - * void mark_divelist_changed(int changed) - * int unsaved_changes() - * void remove_autogen_trips() - */ -#include -#include -#include -#include -#include -#include -#include "gettext.h" -#include -#include -#include - -#include "dive.h" -#include "divelist.h" -#include "display.h" -#include "planner.h" -#include "qthelperfromc.h" - -static short dive_list_changed = false; - -short autogroup = false; - -dive_trip_t *dive_trip_list; - -unsigned int amount_selected; - -#if DEBUG_SELECTION_TRACKING -void dump_selection(void) -{ - int i; - struct dive *dive; - - printf("currently selected are %u dives:", amount_selected); - for_each_dive(i, dive) { - if (dive->selected) - printf(" %d", i); - } - printf("\n"); -} -#endif - -void set_autogroup(bool value) -{ - /* if we keep the UI paradigm, this needs to toggle - * the checkbox on the autogroup menu item */ - autogroup = value; -} - -dive_trip_t *find_trip_by_idx(int idx) -{ - dive_trip_t *trip = dive_trip_list; - - if (idx >= 0) - return NULL; - idx = -idx; - while (trip) { - if (trip->index == idx) - return trip; - trip = trip->next; - } - return NULL; -} - -int trip_has_selected_dives(dive_trip_t *trip) -{ - struct dive *dive; - for (dive = trip->dives; dive; dive = dive->next) { - if (dive->selected) - return 1; - } - return 0; -} - -/* - * Get "maximal" dive gas for a dive. - * Rules: - * - Trimix trumps nitrox (highest He wins, O2 breaks ties) - * - Nitrox trumps air (even if hypoxic) - * These are the same rules as the inter-dive sorting rules. - */ -void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2max_p) -{ - int i; - int maxo2 = -1, maxhe = -1, mino2 = 1000; - - - for (i = 0; i < MAX_CYLINDERS; i++) { - cylinder_t *cyl = dive->cylinder + i; - int o2 = get_o2(&cyl->gasmix); - int he = get_he(&cyl->gasmix); - - if (!is_cylinder_used(dive, i)) - continue; - if (cylinder_none(cyl)) - continue; - if (o2 > maxo2) - maxo2 = o2; - if (he > maxhe) - goto newmax; - if (he < maxhe) - continue; - if (o2 <= maxo2) - continue; - newmax: - maxhe = he; - mino2 = o2; - } - /* All air? Show/sort as "air"/zero */ - if ((!maxhe && maxo2 == O2_IN_AIR && mino2 == maxo2) || - (maxo2 == -1 && maxhe == -1 && mino2 == 1000)) - maxo2 = mino2 = 0; - *o2_p = mino2; - *he_p = maxhe; - *o2max_p = maxo2; -} - -int total_weight(struct dive *dive) -{ - int i, total_grams = 0; - - if (dive) - for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) - total_grams += dive->weightsystem[i].weight.grams; - return total_grams; -} - -static int active_o2(struct dive *dive, struct divecomputer *dc, duration_t time) -{ - struct gasmix gas; - get_gas_at_time(dive, dc, time, &gas); - return get_o2(&gas); -} - -/* calculate OTU for a dive - this only takes the first divecomputer into account */ -static int calculate_otu(struct dive *dive) -{ - int i; - double otu = 0.0; - struct divecomputer *dc = &dive->dc; - - for (i = 1; i < dc->samples; i++) { - int t; - int po2; - struct sample *sample = dc->sample + i; - struct sample *psample = sample - 1; - t = sample->time.seconds - psample->time.seconds; - if (sample->setpoint.mbar) { - po2 = sample->setpoint.mbar; - } else { - int o2 = active_o2(dive, dc, sample->time); - po2 = o2 * depth_to_atm(sample->depth.mm, dive); - } - if (po2 >= 500) - otu += pow((po2 - 500) / 1000.0, 0.83) * t / 30.0; - } - return rint(otu); -} -/* calculate CNS for a dive - this only takes the first divecomputer into account */ -int const cns_table[][3] = { - /* po2, Maximum Single Exposure, Maximum 24 hour Exposure */ - { 1600, 45 * 60, 150 * 60 }, - { 1500, 120 * 60, 180 * 60 }, - { 1400, 150 * 60, 180 * 60 }, - { 1300, 180 * 60, 210 * 60 }, - { 1200, 210 * 60, 240 * 60 }, - { 1100, 240 * 60, 270 * 60 }, - { 1000, 300 * 60, 300 * 60 }, - { 900, 360 * 60, 360 * 60 }, - { 800, 450 * 60, 450 * 60 }, - { 700, 570 * 60, 570 * 60 }, - { 600, 720 * 60, 720 * 60 } -}; - -/* this only gets called if dive->maxcns == 0 which means we know that - * none of the divecomputers has tracked any CNS for us - * so we calculated it "by hand" */ -static int calculate_cns(struct dive *dive) -{ - int i, j, divenr; - double cns = 0.0; - struct divecomputer *dc = &dive->dc; - struct dive *prev_dive; - timestamp_t endtime; - - /* shortcut */ - if (dive->cns) - return dive->cns; - /* - * Do we start with a cns loading from a previous dive? - * Check if we did a dive 12 hours prior, and what cns we had from that. - * Then apply ha 90min halftime to see whats left. - */ - divenr = get_divenr(dive); - if (divenr) { - prev_dive = get_dive(divenr - 1); - if (prev_dive) { - endtime = prev_dive->when + prev_dive->duration.seconds; - if (dive->when < (endtime + 3600 * 12)) { - cns = calculate_cns(prev_dive); - cns = cns * 1 / pow(2, (dive->when - endtime) / (90.0 * 60.0)); - } - } - } - /* Caclulate the cns for each sample in this dive and sum them */ - for (i = 1; i < dc->samples; i++) { - int t; - int po2; - struct sample *sample = dc->sample + i; - struct sample *psample = sample - 1; - t = sample->time.seconds - psample->time.seconds; - if (sample->setpoint.mbar) { - po2 = sample->setpoint.mbar; - } else { - int o2 = active_o2(dive, dc, sample->time); - po2 = o2 * depth_to_atm(sample->depth.mm, dive); - } - /* CNS don't increse when below 500 matm */ - if (po2 < 500) - continue; - /* Find what table-row we should calculate % for */ - for (j = 1; j < sizeof(cns_table) / (sizeof(int) * 3); j++) - if (po2 > cns_table[j][0]) - break; - j--; - cns += ((double)t) / ((double)cns_table[j][1]) * 100; - } - /* save calculated cns in dive struct */ - dive->cns = cns; - return dive->cns; -} -/* - * Return air usage (in liters). - */ -static double calculate_airuse(struct dive *dive) -{ - int airuse = 0; - int i; - - for (i = 0; i < MAX_CYLINDERS; i++) { - pressure_t start, end; - cylinder_t *cyl = dive->cylinder + i; - - start = cyl->start.mbar ? cyl->start : cyl->sample_start; - end = cyl->end.mbar ? cyl->end : cyl->sample_end; - if (!end.mbar || start.mbar <= end.mbar) - continue; - - airuse += gas_volume(cyl, start) - gas_volume(cyl, end); - } - return airuse / 1000.0; -} - -/* this only uses the first divecomputer to calculate the SAC rate */ -static int calculate_sac(struct dive *dive) -{ - struct divecomputer *dc = &dive->dc; - double airuse, pressure, sac; - int duration, meandepth; - - airuse = calculate_airuse(dive); - if (!airuse) - return 0; - - duration = dc->duration.seconds; - if (!duration) - return 0; - - meandepth = dc->meandepth.mm; - if (!meandepth) - return 0; - - /* Mean pressure in ATM (SAC calculations are in atm*l/min) */ - pressure = depth_to_atm(meandepth, dive); - sac = airuse / pressure * 60 / duration; - - /* milliliters per minute.. */ - return sac * 1000; -} - -/* for now we do this based on the first divecomputer */ -static void add_dive_to_deco(struct dive *dive) -{ - struct divecomputer *dc = &dive->dc; - int i; - - if (!dc) - return; - for (i = 1; i < dc->samples; i++) { - struct sample *psample = dc->sample + i - 1; - struct sample *sample = dc->sample + i; - int t0 = psample->time.seconds; - int t1 = sample->time.seconds; - int j; - - for (j = t0; j < t1; j++) { - int depth = interpolate(psample->depth.mm, sample->depth.mm, j - t0, t1 - t0); - add_segment(depth_to_bar(depth, dive), - &dive->cylinder[sample->sensor].gasmix, 1, sample->setpoint.mbar, dive, dive->sac); - } - } -} - -int get_divenr(struct dive *dive) -{ - int i; - struct dive *d; - // tempting as it may be, don't die when called with dive=NULL - if (dive) - for_each_dive(i, d) { - if (d->id == dive->id) // don't compare pointers, we could be passing in a copy of the dive - return i; - } - return -1; -} - -int get_divesite_idx(struct dive_site *ds) -{ - int i; - struct dive_site *d; - // tempting as it may be, don't die when called with dive=NULL - if (ds) - for_each_dive_site(i, d) { - if (d->uuid == ds->uuid) // don't compare pointers, we could be passing in a copy of the dive - return i; - } - return -1; -} - -static struct gasmix air = { .o2.permille = O2_IN_AIR, .he.permille = 0 }; - -/* take into account previous dives until there is a 48h gap between dives */ -double init_decompression(struct dive *dive) -{ - int i, divenr = -1; - unsigned int surface_time; - timestamp_t when, lasttime = 0, laststart = 0; - bool deco_init = false; - double surface_pressure; - - if (!dive) - return 0.0; - - surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; - divenr = get_divenr(dive); - when = dive->when; - i = divenr; - if (i < 0) { - i = dive_table.nr - 1; - while (i >= 0 && get_dive(i)->when > when) - --i; - i++; - } - while (i--) { - struct dive *pdive = get_dive(i); - /* we don't want to mix dives from different trips as we keep looking - * for how far back we need to go */ - if (dive->divetrip && pdive->divetrip != dive->divetrip) - continue; - if (!pdive || pdive->when >= when || pdive->when + pdive->duration.seconds + 48 * 60 * 60 < when) - break; - /* For simultaneous dives, only consider the first */ - if (pdive->when == laststart) - continue; - when = pdive->when; - lasttime = when + pdive->duration.seconds; - } - while (++i < (divenr >= 0 ? divenr : dive_table.nr)) { - struct dive *pdive = get_dive(i); - /* again skip dives from different trips */ - if (dive->divetrip && dive->divetrip != pdive->divetrip) - continue; - surface_pressure = get_surface_pressure_in_mbar(pdive, true) / 1000.0; - if (!deco_init) { - clear_deco(surface_pressure); - deco_init = true; -#if DECO_CALC_DEBUG & 2 - dump_tissues(); -#endif - } - add_dive_to_deco(pdive); - laststart = pdive->when; -#if DECO_CALC_DEBUG & 2 - printf("added dive #%d\n", pdive->number); - dump_tissues(); -#endif - if (pdive->when > lasttime) { - surface_time = pdive->when - lasttime; - lasttime = pdive->when + pdive->duration.seconds; - add_segment(surface_pressure, &air, surface_time, 0, dive, prefs.decosac); -#if DECO_CALC_DEBUG & 2 - printf("after surface intervall of %d:%02u\n", FRACTION(surface_time, 60)); - dump_tissues(); -#endif - } - } - /* add the final surface time */ - if (lasttime && dive->when > lasttime) { - surface_time = dive->when - lasttime; - surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; - add_segment(surface_pressure, &air, surface_time, 0, dive, prefs.decosac); -#if DECO_CALC_DEBUG & 2 - printf("after surface intervall of %d:%02u\n", FRACTION(surface_time, 60)); - dump_tissues(); -#endif - } - if (!deco_init) { - surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; - clear_deco(surface_pressure); -#if DECO_CALC_DEBUG & 2 - printf("no previous dive\n"); - dump_tissues(); -#endif - } - return tissue_tolerance_calc(dive, surface_pressure); -} - -void update_cylinder_related_info(struct dive *dive) -{ - if (dive != NULL) { - dive->sac = calculate_sac(dive); - dive->otu = calculate_otu(dive); - if (dive->maxcns == 0) - dive->maxcns = calculate_cns(dive); - } -} - -#define MAX_GAS_STRING 80 -#define UTF8_ELLIPSIS "\xE2\x80\xA6" - -/* callers needs to free the string */ -char *get_dive_gas_string(struct dive *dive) -{ - int o2, he, o2max; - char *buffer = malloc(MAX_GAS_STRING); - - if (buffer) { - get_dive_gas(dive, &o2, &he, &o2max); - o2 = (o2 + 5) / 10; - he = (he + 5) / 10; - o2max = (o2max + 5) / 10; - - if (he) - if (o2 == o2max) - snprintf(buffer, MAX_GAS_STRING, "%d/%d", o2, he); - else - snprintf(buffer, MAX_GAS_STRING, "%d/%d" UTF8_ELLIPSIS "%d%%", o2, he, o2max); - else if (o2) - if (o2 == o2max) - snprintf(buffer, MAX_GAS_STRING, "%d%%", o2); - else - snprintf(buffer, MAX_GAS_STRING, "%d" UTF8_ELLIPSIS "%d%%", o2, o2max); - else - strcpy(buffer, translate("gettextFromC", "air")); - } - return buffer; -} - -/* - * helper functions for dive_trip handling - */ -#ifdef DEBUG_TRIP -void dump_trip_list(void) -{ - dive_trip_t *trip; - int i = 0; - timestamp_t last_time = 0; - - for (trip = dive_trip_list; trip; trip = trip->next) { - struct tm tm; - utc_mkdate(trip->when, &tm); - if (trip->when < last_time) - printf("\n\ndive_trip_list OUT OF ORDER!!!\n\n\n"); - printf("%s trip %d to \"%s\" on %04u-%02u-%02u %02u:%02u:%02u (%d dives - %p)\n", - trip->autogen ? "autogen " : "", - ++i, trip->location, - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, - trip->nrdives, trip); - last_time = trip->when; - } - printf("-----\n"); -} -#endif - -/* this finds the last trip that at or before the time given */ -dive_trip_t *find_matching_trip(timestamp_t when) -{ - dive_trip_t *trip = dive_trip_list; - - if (!trip || trip->when > when) { -#ifdef DEBUG_TRIP - printf("no matching trip\n"); -#endif - return NULL; - } - while (trip->next && trip->next->when <= when) - trip = trip->next; -#ifdef DEBUG_TRIP - { - struct tm tm; - utc_mkdate(trip->when, &tm); - printf("found trip %p @ %04d-%02d-%02d %02d:%02d:%02d\n", - trip, - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec); - } -#endif - return trip; -} - -/* insert the trip into the dive_trip_list - but ensure you don't have - * two trips for the same date; but if you have, make sure you don't - * keep the one with less information */ -void insert_trip(dive_trip_t **dive_trip_p) -{ - dive_trip_t *dive_trip = *dive_trip_p; - dive_trip_t **p = &dive_trip_list; - dive_trip_t *trip; - struct dive *divep; - - /* Walk the dive trip list looking for the right location.. */ - while ((trip = *p) != NULL && trip->when < dive_trip->when) - p = &trip->next; - - if (trip && trip->when == dive_trip->when) { - if (!trip->location) - trip->location = dive_trip->location; - if (!trip->notes) - trip->notes = dive_trip->notes; - divep = dive_trip->dives; - while (divep) { - add_dive_to_trip(divep, trip); - divep = divep->next; - } - *dive_trip_p = trip; - } else { - dive_trip->next = trip; - *p = dive_trip; - } -#ifdef DEBUG_TRIP - dump_trip_list(); -#endif -} - -static void delete_trip(dive_trip_t *trip) -{ - dive_trip_t **p, *tmp; - - assert(!trip->dives); - - /* Remove the trip from the list of trips */ - p = &dive_trip_list; - while ((tmp = *p) != NULL) { - if (tmp == trip) { - *p = trip->next; - break; - } - p = &tmp->next; - } - - /* .. and free it */ - free(trip->location); - free(trip->notes); - free(trip); -} - -void find_new_trip_start_time(dive_trip_t *trip) -{ - struct dive *dive = trip->dives; - timestamp_t when = dive->when; - - while ((dive = dive->next) != NULL) { - if (dive->when < when) - when = dive->when; - } - trip->when = when; -} - -/* check if we have a trip right before / after this dive */ -bool is_trip_before_after(struct dive *dive, bool before) -{ - int idx = get_idx_by_uniq_id(dive->id); - if (before) { - if (idx > 0 && get_dive(idx - 1)->divetrip) - return true; - } else { - if (idx < dive_table.nr - 1 && get_dive(idx + 1)->divetrip) - return true; - } - return false; -} - -struct dive *first_selected_dive() -{ - int idx; - struct dive *d; - - for_each_dive (idx, d) { - if (d->selected) - return d; - } - return NULL; -} - -struct dive *last_selected_dive() -{ - int idx; - struct dive *d, *ret = NULL; - - for_each_dive (idx, d) { - if (d->selected) - ret = d; - } - return ret; -} - -void remove_dive_from_trip(struct dive *dive, short was_autogen) -{ - struct dive *next, **pprev; - dive_trip_t *trip = dive->divetrip; - - if (!trip) - return; - - /* Remove the dive from the trip's list of dives */ - next = dive->next; - pprev = dive->pprev; - *pprev = next; - if (next) - next->pprev = pprev; - - dive->divetrip = NULL; - if (was_autogen) - dive->tripflag = TF_NONE; - else - dive->tripflag = NO_TRIP; - assert(trip->nrdives > 0); - if (!--trip->nrdives) - delete_trip(trip); - else if (trip->when == dive->when) - find_new_trip_start_time(trip); -} - -void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) -{ - if (dive->divetrip == trip) - return; - remove_dive_from_trip(dive, false); - trip->nrdives++; - dive->divetrip = trip; - dive->tripflag = ASSIGNED_TRIP; - - /* Add it to the trip's list of dives*/ - dive->next = trip->dives; - if (dive->next) - dive->next->pprev = &dive->next; - trip->dives = dive; - dive->pprev = &trip->dives; - - if (dive->when && trip->when > dive->when) - trip->when = dive->when; -} - -dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) -{ - dive_trip_t *dive_trip = calloc(1, sizeof(dive_trip_t)); - - dive_trip->when = dive->when; - dive_trip->location = copy_string(get_dive_location(dive)); - insert_trip(&dive_trip); - - dive->tripflag = IN_TRIP; - add_dive_to_trip(dive, dive_trip); - return dive_trip; -} - -/* - * Walk the dives from the oldest dive, and see if we can autogroup them - */ -void autogroup_dives(void) -{ - int i; - struct dive *dive, *lastdive = NULL; - - for_each_dive(i, dive) { - dive_trip_t *trip; - - if (dive->divetrip) { - lastdive = dive; - continue; - } - - if (!DIVE_NEEDS_TRIP(dive)) { - lastdive = NULL; - continue; - } - - /* Do we have a trip we can combine this into? */ - if (lastdive && dive->when < lastdive->when + TRIP_THRESHOLD) { - dive_trip_t *trip = lastdive->divetrip; - add_dive_to_trip(dive, trip); - if (get_dive_location(dive) && !trip->location) - trip->location = copy_string(get_dive_location(dive)); - lastdive = dive; - continue; - } - - lastdive = dive; - trip = create_and_hookup_trip_from_dive(dive); - trip->autogen = 1; - } - -#ifdef DEBUG_TRIP - dump_trip_list(); -#endif -} - -/* this implements the mechanics of removing the dive from the table, - * but doesn't deal with updating dive trips, etc */ -void delete_single_dive(int idx) -{ - int i; - struct dive *dive = get_dive(idx); - if (!dive) - return; /* this should never happen */ - remove_dive_from_trip(dive, false); - if (dive->selected) - deselect_dive(idx); - for (i = idx; i < dive_table.nr - 1; i++) - dive_table.dives[i] = dive_table.dives[i + 1]; - dive_table.dives[--dive_table.nr] = NULL; - /* free all allocations */ - free(dive->dc.sample); - free((void *)dive->notes); - free((void *)dive->divemaster); - free((void *)dive->buddy); - free((void *)dive->suit); - taglist_free(dive->tag_list); - free(dive); -} - -struct dive **grow_dive_table(struct dive_table *table) -{ - int nr = table->nr, allocated = table->allocated; - struct dive **dives = table->dives; - - if (nr >= allocated) { - allocated = (nr + 32) * 3 / 2; - dives = realloc(dives, allocated * sizeof(struct dive *)); - if (!dives) - exit(1); - table->dives = dives; - table->allocated = allocated; - } - return dives; -} - -void add_single_dive(int idx, struct dive *dive) -{ - int i; - grow_dive_table(&dive_table); - dive_table.nr++; - if (dive->selected) - amount_selected++; - for (i = idx; i < dive_table.nr; i++) { - struct dive *tmp = dive_table.dives[i]; - dive_table.dives[i] = dive; - dive = tmp; - } -} - -bool consecutive_selected() -{ - struct dive *d; - int i; - bool consecutive = true; - bool firstfound = false; - bool lastfound = false; - - if (amount_selected == 0 || amount_selected == 1) - return true; - - for_each_dive(i, d) { - if (d->selected) { - if (!firstfound) - firstfound = true; - else if (lastfound) - consecutive = false; - } else if (firstfound) { - lastfound = true; - } - } - return consecutive; -} - -struct dive *merge_two_dives(struct dive *a, struct dive *b) -{ - struct dive *res; - int i, j; - int id; - - if (!a || !b) - return NULL; - - id = a->id; - i = get_divenr(a); - j = get_divenr(b); - if (i < 0 || j < 0) - // something is wrong with those dives. Bail - return NULL; - res = merge_dives(a, b, b->when - a->when, false); - if (!res) - return NULL; - - add_single_dive(i, res); - delete_single_dive(i + 1); - delete_single_dive(j); - // now make sure that we keep the id of the first dive. - // why? - // because this way one of the previously selected ids is still around - res->id = id; - mark_divelist_changed(true); - return res; -} - -void select_dive(int idx) -{ - struct dive *dive = get_dive(idx); - if (dive) { - /* never select an invalid dive that isn't displayed */ - if (!dive->selected) { - dive->selected = 1; - amount_selected++; - } - selected_dive = idx; - } -} - -void deselect_dive(int idx) -{ - struct dive *dive = get_dive(idx); - if (dive && dive->selected) { - dive->selected = 0; - if (amount_selected) - amount_selected--; - if (selected_dive == idx && amount_selected > 0) { - /* pick a different dive as selected */ - while (--selected_dive >= 0) { - dive = get_dive(selected_dive); - if (dive && dive->selected) - return; - } - selected_dive = idx; - while (++selected_dive < dive_table.nr) { - dive = get_dive(selected_dive); - if (dive && dive->selected) - return; - } - } - if (amount_selected == 0) - selected_dive = -1; - } -} - -void deselect_dives_in_trip(struct dive_trip *trip) -{ - struct dive *dive; - if (!trip) - return; - for (dive = trip->dives; dive; dive = dive->next) - deselect_dive(get_divenr(dive)); -} - -void select_dives_in_trip(struct dive_trip *trip) -{ - struct dive *dive; - if (!trip) - return; - for (dive = trip->dives; dive; dive = dive->next) - if (!dive->hidden_by_filter) - select_dive(get_divenr(dive)); -} - -void filter_dive(struct dive *d, bool shown) -{ - if (!d) - return; - d->hidden_by_filter = !shown; - if (!shown && d->selected) - deselect_dive(get_divenr(d)); -} - - -/* This only gets called with non-NULL trips. - * It does not combine notes or location, just picks the first one - * (or the second one if the first one is empty */ -void combine_trips(struct dive_trip *trip_a, struct dive_trip *trip_b) -{ - if (same_string(trip_a->location, "") && trip_b->location) { - free(trip_a->location); - trip_a->location = strdup(trip_b->location); - } - if (same_string(trip_a->notes, "") && trip_b->notes) { - free(trip_a->notes); - trip_a->notes = strdup(trip_b->notes); - } - /* this also removes the dives from trip_b and eventually - * calls delete_trip(trip_b) when the last dive has been moved */ - while (trip_b->dives) - add_dive_to_trip(trip_b->dives, trip_a); -} - -void mark_divelist_changed(int changed) -{ - dive_list_changed = changed; - updateWindowTitle(); -} - -int unsaved_changes() -{ - return dive_list_changed; -} - -void remove_autogen_trips() -{ - int i; - struct dive *dive; - - for_each_dive(i, dive) { - dive_trip_t *trip = dive->divetrip; - - if (trip && trip->autogen) - remove_dive_from_trip(dive, true); - } -} - -/* - * When adding dives to the dive table, we try to renumber - * the new dives based on any old dives in the dive table. - * - * But we only do it if: - * - * - there are no dives in the dive table - * - * OR - * - * - the last dive in the old dive table was numbered - * - * - all the new dives are strictly at the end (so the - * "last dive" is at the same location in the dive table - * after re-sorting the dives. - * - * - none of the new dives have any numbers - * - * This catches the common case of importing new dives from - * a dive computer, and gives them proper numbers based on - * your old dive list. But it tries to be very conservative - * and not give numbers if there is *any* question about - * what the numbers should be - in which case you need to do - * a manual re-numbering. - */ -static void try_to_renumber(struct dive *last, int preexisting) -{ - int i, nr; - - /* - * If the new dives aren't all strictly at the end, - * we're going to expect the user to do a manual - * renumbering. - */ - if (preexisting && get_dive(preexisting - 1) != last) - return; - - /* - * If any of the new dives already had a number, - * we'll have to do a manual renumbering. - */ - for (i = preexisting; i < dive_table.nr; i++) { - struct dive *dive = get_dive(i); - if (dive->number) - return; - } - - /* - * Ok, renumber.. - */ - if (last) - nr = last->number; - else - nr = 0; - for (i = preexisting; i < dive_table.nr; i++) { - struct dive *dive = get_dive(i); - dive->number = ++nr; - } -} - -void process_dives(bool is_imported, bool prefer_imported) -{ - int i; - int preexisting = dive_table.preexisting; - bool did_merge = false; - struct dive *last; - - /* check if we need a nickname for the divecomputer for newly downloaded dives; - * since we know they all came from the same divecomputer we just check for the - * first one */ - if (preexisting < dive_table.nr && dive_table.dives[preexisting]->downloaded) - set_dc_nickname(dive_table.dives[preexisting]); - else - /* they aren't downloaded, so record / check all new ones */ - for (i = preexisting; i < dive_table.nr; i++) - set_dc_nickname(dive_table.dives[i]); - - for (i = preexisting; i < dive_table.nr; i++) - dive_table.dives[i]->downloaded = true; - - /* This does the right thing for -1: NULL */ - last = get_dive(preexisting - 1); - - sort_table(&dive_table); - - for (i = 1; i < dive_table.nr; i++) { - struct dive **pp = &dive_table.dives[i - 1]; - struct dive *prev = pp[0]; - struct dive *dive = pp[1]; - struct dive *merged; - int id; - - /* only try to merge overlapping dives - or if one of the dives has - * zero duration (that might be a gps marker from the webservice) */ - if (prev->duration.seconds && dive->duration.seconds && - prev->when + prev->duration.seconds < dive->when) - continue; - - merged = try_to_merge(prev, dive, prefer_imported); - if (!merged) - continue; - - // remember the earlier dive's id - id = prev->id; - - /* careful - we might free the dive that last points to. Oops... */ - if (last == prev || last == dive) - last = merged; - - /* Redo the new 'i'th dive */ - i--; - add_single_dive(i, merged); - delete_single_dive(i + 1); - delete_single_dive(i + 1); - // keep the id or the first dive for the merged dive - merged->id = id; - - /* this means the table was changed */ - did_merge = true; - } - /* make sure no dives are still marked as downloaded */ - for (i = 1; i < dive_table.nr; i++) - dive_table.dives[i]->downloaded = false; - - if (is_imported) { - /* If there are dives in the table, are they numbered */ - if (!last || last->number) - try_to_renumber(last, preexisting); - - /* did we add dives or divecomputers to the dive table? */ - if (did_merge || preexisting < dive_table.nr) { - mark_divelist_changed(true); - } - } -} - -void set_dive_nr_for_current_dive() -{ - if (dive_table.nr == 1) - current_dive->number = 1; - else if (selected_dive == dive_table.nr - 1 && get_dive(dive_table.nr - 2)->number) - current_dive->number = get_dive(dive_table.nr - 2)->number + 1; -} - -static int min_datafile_version; - -int get_min_datafile_version() -{ - return min_datafile_version; -} - -void reset_min_datafile_version() -{ - min_datafile_version = 0; -} - -void report_datafile_version(int version) -{ - if (min_datafile_version == 0 || min_datafile_version > version) - min_datafile_version = version; -} - -void clear_dive_file_data() -{ - while (dive_table.nr) - delete_single_dive(0); - while (dive_site_table.nr) - delete_dive_site(get_dive_site(0)->uuid); - - clear_dive(&displayed_dive); - clear_dive_site(&displayed_dive_site); - - free((void *)existing_filename); - existing_filename = NULL; - - reset_min_datafile_version(); -} diff --git a/divelist.h b/divelist.h deleted file mode 100644 index 5bae09cff..000000000 --- a/divelist.h +++ /dev/null @@ -1,62 +0,0 @@ -#ifndef DIVELIST_H -#define DIVELIST_H - -#ifdef __cplusplus -extern "C" { -#endif - -/* this is used for both git and xml format */ -#define DATAFORMAT_VERSION 3 - -struct dive; - -extern void update_cylinder_related_info(struct dive *); -extern void mark_divelist_changed(int); -extern int unsaved_changes(void); -extern void remove_autogen_trips(void); -extern double init_decompression(struct dive *dive); - -/* divelist core logic functions */ -extern void process_dives(bool imported, bool prefer_imported); -extern char *get_dive_gas_string(struct dive *dive); - -extern dive_trip_t *find_trip_by_idx(int idx); - -struct dive **grow_dive_table(struct dive_table *table); -extern int trip_has_selected_dives(dive_trip_t *trip); -extern void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p); -extern int get_divenr(struct dive *dive); -extern int get_divesite_idx(struct dive_site *ds); -extern dive_trip_t *find_matching_trip(timestamp_t when); -extern void remove_dive_from_trip(struct dive *dive, short was_autogen); -extern dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive); -extern void autogroup_dives(void); -extern struct dive *merge_two_dives(struct dive *a, struct dive *b); -extern bool consecutive_selected(); -extern void select_dive(int idx); -extern void deselect_dive(int idx); -extern void select_dives_in_trip(struct dive_trip *trip); -extern void deselect_dives_in_trip(struct dive_trip *trip); -extern void filter_dive(struct dive *d, bool shown); -extern void combine_trips(struct dive_trip *trip_a, struct dive_trip *trip_b); -extern void find_new_trip_start_time(dive_trip_t *trip); -extern struct dive *first_selected_dive(); -extern struct dive *last_selected_dive(); -extern bool is_trip_before_after(struct dive *dive, bool before); -extern void set_dive_nr_for_current_dive(); - -int get_min_datafile_version(); -void reset_min_datafile_version(); -void report_datafile_version(int version); -void clear_dive_file_data(); - -#ifdef DEBUG_TRIP -extern void dump_selection(void); -extern void dump_trip_list(void); -#endif - -#ifdef __cplusplus -} -#endif - -#endif // DIVELIST_H diff --git a/divelogexportlogic.cpp b/divelogexportlogic.cpp deleted file mode 100644 index af5157f4a..000000000 --- a/divelogexportlogic.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "divelogexportlogic.h" -#include "helpers.h" -#include "units.h" -#include "statistics.h" -#include "save-html.h" - -void file_copy_and_overwrite(const QString &fileName, const QString &newName) -{ - QFile file(newName); - if (file.exists()) - file.remove(); - QFile::copy(fileName, newName); -} - -void exportHTMLsettings(const QString &filename, struct htmlExportSetting &hes) -{ - QString fontSize = hes.fontSize; - QString fontFamily = hes.fontFamily; - QFile file(filename); - file.open(QIODevice::WriteOnly | QIODevice::Text); - QTextStream out(&file); - out << "settings = {\"fontSize\":\"" << fontSize << "\",\"fontFamily\":\"" << fontFamily << "\",\"listOnly\":\"" - << hes.listOnly << "\",\"subsurfaceNumbers\":\"" << hes.subsurfaceNumbers << "\","; - //save units preferences - if (prefs.unit_system == METRIC) { - out << "\"unit_system\":\"Meteric\""; - } else if (prefs.unit_system == IMPERIAL) { - out << "\"unit_system\":\"Imperial\""; - } else { - QVariant v; - QString length, pressure, volume, temperature, weight; - length = prefs.units.length == units::METERS ? "METER" : "FEET"; - pressure = prefs.units.pressure == units::BAR ? "BAR" : "PSI"; - volume = prefs.units.volume == units::LITER ? "LITER" : "CUFT"; - temperature = prefs.units.temperature == units::CELSIUS ? "CELSIUS" : "FAHRENHEIT"; - weight = prefs.units.weight == units::KG ? "KG" : "LBS"; - out << "\"unit_system\":\"Personalize\","; - out << "\"units\":{\"depth\":\"" << length << "\",\"pressure\":\"" << pressure << "\",\"volume\":\"" << volume << "\",\"temperature\":\"" << temperature << "\",\"weight\":\"" << weight << "\"}"; - } - out << "}"; - file.close(); -} - -static void exportHTMLstatisticsTotal(QTextStream &out, stats_t *total_stats) -{ - out << "{"; - out << "\"YEAR\":\"Total\","; - out << "\"DIVES\":\"" << total_stats->selection_size << "\","; - out << "\"TOTAL_TIME\":\"" << get_time_string(total_stats->total_time.seconds, 0) << "\","; - out << "\"AVERAGE_TIME\":\"--\","; - out << "\"SHORTEST_TIME\":\"--\","; - out << "\"LONGEST_TIME\":\"--\","; - out << "\"AVG_DEPTH\":\"--\","; - out << "\"MIN_DEPTH\":\"--\","; - out << "\"MAX_DEPTH\":\"--\","; - out << "\"AVG_SAC\":\"--\","; - out << "\"MIN_SAC\":\"--\","; - out << "\"MAX_SAC\":\"--\","; - out << "\"AVG_TEMP\":\"--\","; - out << "\"MIN_TEMP\":\"--\","; - out << "\"MAX_TEMP\":\"--\","; - out << "},"; -} - - -static void exportHTMLstatistics(const QString filename, struct htmlExportSetting &hes) -{ - QFile file(filename); - file.open(QIODevice::WriteOnly | QIODevice::Text); - QTextStream out(&file); - - stats_t total_stats; - - total_stats.selection_size = 0; - total_stats.total_time.seconds = 0; - - int i = 0; - out << "divestat=["; - if (hes.yearlyStatistics) { - while (stats_yearly != NULL && stats_yearly[i].period) { - out << "{"; - out << "\"YEAR\":\"" << stats_yearly[i].period << "\","; - out << "\"DIVES\":\"" << stats_yearly[i].selection_size << "\","; - out << "\"TOTAL_TIME\":\"" << get_time_string(stats_yearly[i].total_time.seconds, 0) << "\","; - out << "\"AVERAGE_TIME\":\"" << get_minutes(stats_yearly[i].total_time.seconds / stats_yearly[i].selection_size) << "\","; - out << "\"SHORTEST_TIME\":\"" << get_minutes(stats_yearly[i].shortest_time.seconds) << "\","; - out << "\"LONGEST_TIME\":\"" << get_minutes(stats_yearly[i].longest_time.seconds) << "\","; - out << "\"AVG_DEPTH\":\"" << get_depth_string(stats_yearly[i].avg_depth) << "\","; - out << "\"MIN_DEPTH\":\"" << get_depth_string(stats_yearly[i].min_depth) << "\","; - out << "\"MAX_DEPTH\":\"" << get_depth_string(stats_yearly[i].max_depth) << "\","; - out << "\"AVG_SAC\":\"" << get_volume_string(stats_yearly[i].avg_sac) << "\","; - out << "\"MIN_SAC\":\"" << get_volume_string(stats_yearly[i].min_sac) << "\","; - out << "\"MAX_SAC\":\"" << get_volume_string(stats_yearly[i].max_sac) << "\","; - if ( stats_yearly[i].combined_count ) - out << "\"AVG_TEMP\":\"" << QString::number(stats_yearly[i].combined_temp / stats_yearly[i].combined_count, 'f', 1) << "\","; - else - out << "\"AVG_TEMP\":\"0.0\","; - out << "\"MIN_TEMP\":\"" << ( stats_yearly[i].min_temp == 0 ? 0 : get_temp_units(stats_yearly[i].min_temp, NULL)) << "\","; - out << "\"MAX_TEMP\":\"" << ( stats_yearly[i].max_temp == 0 ? 0 : get_temp_units(stats_yearly[i].max_temp, NULL)) << "\","; - out << "},"; - total_stats.selection_size += stats_yearly[i].selection_size; - total_stats.total_time.seconds += stats_yearly[i].total_time.seconds; - i++; - } - exportHTMLstatisticsTotal(out, &total_stats); - } - out << "]"; - file.close(); - -} - -void exportHtmlInitLogic(const QString &filename, struct htmlExportSetting &hes) -{ - QString photosDirectory; - QFile file(filename); - QFileInfo info(file); - QDir mainDir = info.absoluteDir(); - mainDir.mkdir(file.fileName() + "_files"); - QString exportFiles = file.fileName() + "_files"; - - QString json_dive_data = exportFiles + QDir::separator() + "file.js"; - QString json_settings = exportFiles + QDir::separator() + "settings.js"; - QString translation = exportFiles + QDir::separator() + "translation.js"; - QString stat_file = exportFiles + QDir::separator() + "stat.js"; - exportFiles += "/"; - - if (hes.exportPhotos) { - photosDirectory = exportFiles + QDir::separator() + "photos" + QDir::separator(); - mainDir.mkdir(photosDirectory); - } - - - exportHTMLsettings(json_settings, hes); - exportHTMLstatistics(stat_file, hes); - export_translation(translation.toUtf8().data()); - - export_HTML(qPrintable(json_dive_data), qPrintable(photosDirectory), hes.selectedOnly, hes.listOnly); - - QString searchPath = getSubsurfaceDataPath("theme"); - if (searchPath.isEmpty()) - return; - - searchPath += QDir::separator(); - - file_copy_and_overwrite(searchPath + "dive_export.html", filename); - file_copy_and_overwrite(searchPath + "list_lib.js", exportFiles + "list_lib.js"); - file_copy_and_overwrite(searchPath + "poster.png", exportFiles + "poster.png"); - file_copy_and_overwrite(searchPath + "jqplot.highlighter.min.js", exportFiles + "jqplot.highlighter.min.js"); - file_copy_and_overwrite(searchPath + "jquery.jqplot.min.js", exportFiles + "jquery.jqplot.min.js"); - file_copy_and_overwrite(searchPath + "jqplot.canvasAxisTickRenderer.min.js", exportFiles + "jqplot.canvasAxisTickRenderer.min.js"); - file_copy_and_overwrite(searchPath + "jqplot.canvasTextRenderer.min.js", exportFiles + "jqplot.canvasTextRenderer.min.js"); - file_copy_and_overwrite(searchPath + "jquery.min.js", exportFiles + "jquery.min.js"); - file_copy_and_overwrite(searchPath + "jquery.jqplot.css", exportFiles + "jquery.jqplot.css"); - file_copy_and_overwrite(searchPath + hes.themeFile, exportFiles + "theme.css"); -} diff --git a/divelogexportlogic.h b/divelogexportlogic.h deleted file mode 100644 index 84f09c362..000000000 --- a/divelogexportlogic.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef DIVELOGEXPORTLOGIC_H -#define DIVELOGEXPORTLOGIC_H - -struct htmlExportSetting { - bool exportPhotos; - bool selectedOnly; - bool listOnly; - QString fontFamily; - QString fontSize; - int themeSelection; - bool subsurfaceNumbers; - bool yearlyStatistics; - QString themeFile; -}; - -void file_copy_and_overwrite(const QString &fileName, const QString &newName); -void exportHtmlInitLogic(const QString &filename, struct htmlExportSetting &hes); - -#endif // DIVELOGEXPORTLOGIC_H - diff --git a/divesite.c b/divesite.c deleted file mode 100644 index e9eed2a07..000000000 --- a/divesite.c +++ /dev/null @@ -1,337 +0,0 @@ -/* divesite.c */ -#include "divesite.h" -#include "dive.h" -#include "divelist.h" - -#include - -struct dive_site_table dive_site_table; - -/* there could be multiple sites of the same name - return the first one */ -uint32_t get_dive_site_uuid_by_name(const char *name, struct dive_site **dsp) -{ - int i; - struct dive_site *ds; - for_each_dive_site (i, ds) { - if (same_string(ds->name, name)) { - if (dsp) - *dsp = ds; - return ds->uuid; - } - } - return 0; -} - -/* there could be multiple sites at the same GPS fix - return the first one */ -uint32_t get_dive_site_uuid_by_gps(degrees_t latitude, degrees_t longitude, struct dive_site **dsp) -{ - int i; - struct dive_site *ds; - for_each_dive_site (i, ds) { - if (ds->latitude.udeg == latitude.udeg && ds->longitude.udeg == longitude.udeg) { - if (dsp) - *dsp = ds; - return ds->uuid; - } - } - return 0; -} - - -/* to avoid a bug where we have two dive sites with different name and the same GPS coordinates - * and first get the gps coordinates (reading a V2 file) and happen to get back "the other" name, - * this function allows us to verify if a very specific name/GPS combination already exists */ -uint32_t get_dive_site_uuid_by_gps_and_name(char *name, degrees_t latitude, degrees_t longitude) -{ - int i; - struct dive_site *ds; - for_each_dive_site (i, ds) { - if (ds->latitude.udeg == latitude.udeg && ds->longitude.udeg == longitude.udeg && same_string(ds->name, name)) - return ds->uuid; - } - return 0; -} - -// Calculate the distance in meters between two coordinates. -unsigned int get_distance(degrees_t lat1, degrees_t lon1, degrees_t lat2, degrees_t lon2) -{ - double lat2_r = udeg_to_radians(lat2.udeg); - double lat_d_r = udeg_to_radians(lat2.udeg-lat1.udeg); - double lon_d_r = udeg_to_radians(lon2.udeg-lon1.udeg); - - double a = sin(lat_d_r/2) * sin(lat_d_r/2) + - cos(lat2_r) * cos(lat2_r) * sin(lon_d_r/2) * sin(lon_d_r/2); - double c = 2 * atan2(sqrt(a), sqrt(1.0 - a)); - - // Earth radious in metres - return 6371000 * c; -} - -/* find the closest one, no more than distance meters away - if more than one at same distance, pick the first */ -uint32_t get_dive_site_uuid_by_gps_proximity(degrees_t latitude, degrees_t longitude, int distance, struct dive_site **dsp) -{ - int i; - int uuid = 0; - struct dive_site *ds; - unsigned int cur_distance, min_distance = distance; - for_each_dive_site (i, ds) { - if (dive_site_has_gps_location(ds) && - (cur_distance = get_distance(ds->latitude, ds->longitude, latitude, longitude)) < min_distance) { - min_distance = cur_distance; - uuid = ds->uuid; - if (dsp) - *dsp = ds; - } - } - return uuid; -} - -/* try to create a uniqe ID - fingers crossed */ -static uint32_t dive_site_getUniqId() -{ - uint32_t id = 0; - - while (id == 0 || get_dive_site_by_uuid(id)) { - id = rand() & 0xff; - id |= (rand() & 0xff) << 8; - id |= (rand() & 0xff) << 16; - id |= (rand() & 0xff) << 24; - } - - return id; -} - -/* we never allow a second dive site with the same uuid */ -struct dive_site *alloc_or_get_dive_site(uint32_t uuid) -{ - struct dive_site *ds; - if (uuid) { - if ((ds = get_dive_site_by_uuid(uuid)) != NULL) { - fprintf(stderr, "PROBLEM: refusing to create dive site with the same uuid %08x\n", uuid); - return ds; - } - } - int nr = dive_site_table.nr; - int allocated = dive_site_table.allocated; - struct dive_site **sites = dive_site_table.dive_sites; - - if (nr >= allocated) { - allocated = (nr + 32) * 3 / 2; - sites = realloc(sites, allocated * sizeof(struct dive_site *)); - if (!sites) - exit(1); - dive_site_table.dive_sites = sites; - dive_site_table.allocated = allocated; - } - ds = calloc(1, sizeof(*ds)); - if (!ds) - exit(1); - sites[nr] = ds; - dive_site_table.nr = nr + 1; - // we should always be called with a valid uuid except in the special - // case where we want to copy a dive site into the memory we allocated - // here - then we need to pass in 0 and create a temporary uuid here - // (just so things are always consistent) - if (uuid) - ds->uuid = uuid; - else - ds->uuid = dive_site_getUniqId(); - return ds; -} - -int nr_of_dives_at_dive_site(uint32_t uuid, bool select_only) -{ - int j; - int nr = 0; - struct dive *d; - for_each_dive(j, d) { - if (d->dive_site_uuid == uuid && (!select_only || d->selected)) { - nr++; - } - } - return nr; -} - -bool is_dive_site_used(uint32_t uuid, bool select_only) -{ - int j; - bool found = false; - struct dive *d; - for_each_dive(j, d) { - if (d->dive_site_uuid == uuid && (!select_only || d->selected)) { - found = true; - break; - } - } - return found; -} - -void delete_dive_site(uint32_t id) -{ - int nr = dive_site_table.nr; - for (int i = 0; i < nr; i++) { - struct dive_site *ds = get_dive_site(i); - if (ds->uuid == id) { - free(ds->name); - free(ds->notes); - free(ds); - if (nr - 1 > i) - memmove(&dive_site_table.dive_sites[i], - &dive_site_table.dive_sites[i+1], - (nr - 1 - i) * sizeof(dive_site_table.dive_sites[0])); - dive_site_table.nr = nr - 1; - break; - } - } -} - -uint32_t create_divesite_uuid(const char *name, timestamp_t divetime) -{ - if (name == NULL) - name =""; - unsigned char hash[20]; - SHA_CTX ctx; - SHA1_Init(&ctx); - SHA1_Update(&ctx, &divetime, sizeof(timestamp_t)); - SHA1_Update(&ctx, name, strlen(name)); - SHA1_Final(hash, &ctx); - // now return the first 32 of the 160 bit hash - return *(uint32_t *)hash; -} - -/* allocate a new site and add it to the table */ -uint32_t create_dive_site(const char *name, timestamp_t divetime) -{ - uint32_t uuid = create_divesite_uuid(name, divetime); - struct dive_site *ds = alloc_or_get_dive_site(uuid); - ds->name = copy_string(name); - - return uuid; -} - -/* same as before, but with current time if no current_dive is present */ -uint32_t create_dive_site_from_current_dive(const char *name) -{ - if (current_dive != NULL) { - return create_dive_site(name, current_dive->when); - } else { - timestamp_t when; - time_t now = time(0); - when = utc_mktime(localtime(&now)); - return create_dive_site(name, when); - } -} - -/* same as before, but with GPS data */ -uint32_t create_dive_site_with_gps(const char *name, degrees_t latitude, degrees_t longitude, timestamp_t divetime) -{ - uint32_t uuid = create_divesite_uuid(name, divetime); - struct dive_site *ds = alloc_or_get_dive_site(uuid); - ds->name = copy_string(name); - ds->latitude = latitude; - ds->longitude = longitude; - - return ds->uuid; -} - -/* a uuid is always present - but if all the other fields are empty, the dive site is pointless */ -bool dive_site_is_empty(struct dive_site *ds) -{ - return same_string(ds->name, "") && - same_string(ds->description, "") && - same_string(ds->notes, "") && - ds->latitude.udeg == 0 && - ds->longitude.udeg == 0; -} - -void copy_dive_site(struct dive_site *orig, struct dive_site *copy) -{ - free(copy->name); - free(copy->notes); - free(copy->description); - - copy->latitude = orig->latitude; - copy->longitude = orig->longitude; - copy->name = copy_string(orig->name); - copy->notes = copy_string(orig->notes); - copy->description = copy_string(orig->description); - copy->uuid = orig->uuid; - if (orig->taxonomy.category == NULL) { - free_taxonomy(©->taxonomy); - } else { - if (copy->taxonomy.category == NULL) - copy->taxonomy.category = alloc_taxonomy(); - for (int i = 0; i < TC_NR_CATEGORIES; i++) { - if (i < copy->taxonomy.nr) - free((void *)copy->taxonomy.category[i].value); - if (i < orig->taxonomy.nr) { - copy->taxonomy.category[i] = orig->taxonomy.category[i]; - copy->taxonomy.category[i].value = copy_string(orig->taxonomy.category[i].value); - } - } - copy->taxonomy.nr = orig->taxonomy.nr; - } -} - -void clear_dive_site(struct dive_site *ds) -{ - free(ds->name); - free(ds->notes); - free(ds->description); - ds->name = 0; - ds->notes = 0; - ds->description = 0; - ds->latitude.udeg = 0; - ds->longitude.udeg = 0; - ds->uuid = 0; - ds->taxonomy.nr = 0; - free_taxonomy(&ds->taxonomy); -} - -void merge_dive_sites(uint32_t ref, uint32_t* uuids, int count) -{ - int curr_dive, i; - struct dive *d; - for(i = 0; i < count; i++){ - if (uuids[i] == ref) - continue; - - for_each_dive(curr_dive, d) { - if (d->dive_site_uuid != uuids[i] ) - continue; - d->dive_site_uuid = ref; - } - } - - for(i = 0; i < count; i++) { - if (uuids[i] == ref) - continue; - delete_dive_site(uuids[i]); - } - mark_divelist_changed(true); -} - -uint32_t find_or_create_dive_site_with_name(const char *name, timestamp_t divetime) -{ - int i; - struct dive_site *ds; - for_each_dive_site(i,ds) { - if (same_string(name, ds->name)) - break; - } - if (ds) - return ds->uuid; - return create_dive_site(name, divetime); -} - -static int compare_sites(const void *_a, const void *_b) -{ - const struct dive_site *a = (const struct dive_site *)*(void **)_a; - const struct dive_site *b = (const struct dive_site *)*(void **)_b; - return a->uuid > b->uuid ? 1 : a->uuid == b->uuid ? 0 : -1; -} - -void dive_site_table_sort() -{ - qsort(dive_site_table.dive_sites, dive_site_table.nr, sizeof(struct dive_site *), compare_sites); -} diff --git a/divesite.cpp b/divesite.cpp deleted file mode 100644 index ae102a14b..000000000 --- a/divesite.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "divesite.h" -#include "pref.h" - -QString constructLocationTags(uint32_t ds_uuid) -{ - QString locationTag; - struct dive_site *ds = get_dive_site_by_uuid(ds_uuid); - - if (!ds || !ds->taxonomy.nr) - return locationTag; - - locationTag = "(tags: "; - QString connector; - for (int i = 0; i < 3; i++) { - if (prefs.geocoding.category[i] == TC_NONE) - continue; - for (int j = 0; j < TC_NR_CATEGORIES; j++) { - if (ds->taxonomy.category[j].category == prefs.geocoding.category[i]) { - QString tag = ds->taxonomy.category[j].value; - if (!tag.isEmpty()) { - locationTag += connector + tag; - connector = " / "; - } - break; - } - } - } - - locationTag += ")"; - return locationTag; -} diff --git a/divesite.h b/divesite.h deleted file mode 100644 index f18b2e8e8..000000000 --- a/divesite.h +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef DIVESITE_H -#define DIVESITE_H - -#include "units.h" -#include "taxonomy.h" -#include - -#ifdef __cplusplus -#include -extern "C" { -#else -#include -#endif - -struct dive_site -{ - uint32_t uuid; - char *name; - degrees_t latitude, longitude; - char *description; - char *notes; - struct taxonomy_data taxonomy; -}; - -struct dive_site_table { - int nr, allocated; - struct dive_site **dive_sites; -}; - -extern struct dive_site_table dive_site_table; - -static inline struct dive_site *get_dive_site(int nr) -{ - if (nr >= dive_site_table.nr || nr < 0) - return NULL; - return dive_site_table.dive_sites[nr]; -} - -/* iterate over each dive site */ -#define for_each_dive_site(_i, _x) \ - for ((_i) = 0; ((_x) = get_dive_site(_i)) != NULL; (_i)++) - -static inline struct dive_site *get_dive_site_by_uuid(uint32_t uuid) -{ - int i; - struct dive_site *ds; - for_each_dive_site (i, ds) - if (ds->uuid == uuid) - return get_dive_site(i); - return NULL; -} - -void dive_site_table_sort(); -struct dive_site *alloc_or_get_dive_site(uint32_t uuid); -int nr_of_dives_at_dive_site(uint32_t uuid, bool select_only); -bool is_dive_site_used(uint32_t uuid, bool select_only); -void delete_dive_site(uint32_t id); -uint32_t create_dive_site(const char *name, timestamp_t divetime); -uint32_t create_dive_site_from_current_dive(const char *name); -uint32_t create_dive_site_with_gps(const char *name, degrees_t latitude, degrees_t longitude, timestamp_t divetime); -uint32_t get_dive_site_uuid_by_name(const char *name, struct dive_site **dsp); -uint32_t get_dive_site_uuid_by_gps(degrees_t latitude, degrees_t longitude, struct dive_site **dsp); -uint32_t get_dive_site_uuid_by_gps_and_name(char *name, degrees_t latitude, degrees_t longitude); -uint32_t get_dive_site_uuid_by_gps_proximity(degrees_t latitude, degrees_t longitude, int distance, struct dive_site **dsp); -bool dive_site_is_empty(struct dive_site *ds); -void copy_dive_site(struct dive_site *orig, struct dive_site *copy); -void clear_dive_site(struct dive_site *ds); -unsigned int get_distance(degrees_t lat1, degrees_t lon1, degrees_t lat2, degrees_t lon2); -uint32_t find_or_create_dive_site_with_name(const char *name, timestamp_t divetime); -void merge_dive_sites(uint32_t ref, uint32_t *uuids, int count); - -#define INVALID_DIVE_SITE_NAME "development use only - not a valid dive site name" - -#ifdef __cplusplus -} -QString constructLocationTags(uint32_t ds_uuid); - -#endif - -#endif // DIVESITE_H diff --git a/divesitehelpers.cpp b/divesitehelpers.cpp deleted file mode 100644 index 3542f96fa..000000000 --- a/divesitehelpers.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// -// infrastructure to deal with dive sites -// - -#include "divesitehelpers.h" - -#include "divesite.h" -#include "helpers.h" -#include "membuffer.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct GeoLookupInfo { - degrees_t lat; - degrees_t lon; - uint32_t uuid; -}; - -QVector geo_lookup_data; - -ReverseGeoLookupThread* ReverseGeoLookupThread::instance() { - static ReverseGeoLookupThread* self = new ReverseGeoLookupThread(); - return self; -} - -ReverseGeoLookupThread::ReverseGeoLookupThread(QObject *obj) : QThread(obj) -{ -} - -void ReverseGeoLookupThread::run() { - if (geo_lookup_data.isEmpty()) - return; - - QNetworkRequest request; - QNetworkAccessManager *rgl = new QNetworkAccessManager(); - QEventLoop loop; - QString mapquestURL("http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&accept-language=%1&lat=%2&lon=%3"); - QString geonamesURL("http://api.geonames.org/findNearbyPlaceNameJSON?language=%1&lat=%2&lng=%3&radius=50&username=dirkhh"); - QString geonamesOceanURL("http://api.geonames.org/oceanJSON?language=%1&lat=%2&lng=%3&radius=50&username=dirkhh"); - QString divelogsURL("https://www.divelogs.de/mapsearch_divespotnames.php?lat=%1&lng=%2&radius=50"); - QTimer timer; - - request.setRawHeader("Accept", "text/json"); - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); - connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); - - Q_FOREACH (const GeoLookupInfo& info, geo_lookup_data ) { - struct dive_site *ds = info.uuid ? get_dive_site_by_uuid(info.uuid) : &displayed_dive_site; - - // first check the findNearbyPlaces API from geonames - that should give us country, state, city - request.setUrl(geonamesURL.arg(uiLanguage(NULL)).arg(info.lat.udeg / 1000000.0).arg(info.lon.udeg / 1000000.0)); - - QNetworkReply *reply = rgl->get(request); - timer.setSingleShot(true); - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - timer.start(5000); // 5 secs. timeout - loop.exec(); - - if(timer.isActive()) { - timer.stop(); - if(reply->error() > 0) { - report_error("got error accessing geonames.org: %s", qPrintable(reply->errorString())); - goto clear_reply; - } - int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (v < 200 || v >= 300) - goto clear_reply; - QByteArray fullReply = reply->readAll(); - QJsonParseError errorObject; - QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject); - if (errorObject.error != QJsonParseError::NoError) { - report_error("error parsing geonames.org response: %s", qPrintable(errorObject.errorString())); - goto clear_reply; - } - QJsonObject obj = jsonDoc.object(); - QVariant geoNamesObject = obj.value("geonames").toVariant(); - QVariantList geoNames = geoNamesObject.toList(); - if (geoNames.count() > 0) { - QVariantMap firstData = geoNames.at(0).toMap(); - int ri = 0, l3 = -1, lt = -1; - if (ds->taxonomy.category == NULL) { - ds->taxonomy.category = alloc_taxonomy(); - } else { - // clear out the data (except for the ocean data) - int ocean; - if ((ocean = taxonomy_index_for_category(&ds->taxonomy, TC_OCEAN)) > 0) { - ds->taxonomy.category[0] = ds->taxonomy.category[ocean]; - ds->taxonomy.nr = 1; - } else { - // ocean is -1 if there is no such entry, and we didn't copy above - // if ocean is 0, so the following gets us the correct count - ds->taxonomy.nr = ocean + 1; - } - } - // get all the data - OCEAN is special, so start at COUNTRY - for (int j = TC_COUNTRY; j < TC_NR_CATEGORIES; j++) { - if (firstData[taxonomy_api_names[j]].isValid()) { - ds->taxonomy.category[ri].category = j; - ds->taxonomy.category[ri].origin = taxonomy::GEOCODED; - free((void*)ds->taxonomy.category[ri].value); - ds->taxonomy.category[ri].value = copy_string(qPrintable(firstData[taxonomy_api_names[j]].toString())); - ri++; - } - } - ds->taxonomy.nr = ri; - l3 = taxonomy_index_for_category(&ds->taxonomy, TC_ADMIN_L3); - lt = taxonomy_index_for_category(&ds->taxonomy, TC_LOCALNAME); - if (l3 == -1 && lt != -1) { - // basically this means we did get a local name (what we call town), but just like most places - // we didn't get an adminName_3 - which in some regions is the actual city that town belongs to, - // then we copy the town into the city - ds->taxonomy.category[ri].value = copy_string(ds->taxonomy.category[lt].value); - ds->taxonomy.category[ri].origin = taxonomy::COPIED; - ds->taxonomy.category[ri].category = TC_ADMIN_L3; - ds->taxonomy.nr++; - } - mark_divelist_changed(true); - } else { - report_error("geonames.org did not provide reverse lookup information"); - qDebug() << "no reverse geo lookup; geonames returned\n" << fullReply; - } - } else { - report_error("timeout accessing geonames.org"); - disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - reply->abort(); - } - // next check the oceans API to figure out the body of water - request.setUrl(geonamesOceanURL.arg(uiLanguage(NULL)).arg(info.lat.udeg / 1000000.0).arg(info.lon.udeg / 1000000.0)); - reply = rgl->get(request); - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - timer.start(5000); // 5 secs. timeout - loop.exec(); - if(timer.isActive()) { - timer.stop(); - if(reply->error() > 0) { - report_error("got error accessing oceans API of geonames.org: %s", qPrintable(reply->errorString())); - goto clear_reply; - } - int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (v < 200 || v >= 300) - goto clear_reply; - QByteArray fullReply = reply->readAll(); - QJsonParseError errorObject; - QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject); - if (errorObject.error != QJsonParseError::NoError) { - report_error("error parsing geonames.org response: %s", qPrintable(errorObject.errorString())); - goto clear_reply; - } - QJsonObject obj = jsonDoc.object(); - QVariant oceanObject = obj.value("ocean").toVariant(); - QVariantMap oceanName = oceanObject.toMap(); - if (oceanName["name"].isValid()) { - int idx; - if (ds->taxonomy.category == NULL) - ds->taxonomy.category = alloc_taxonomy(); - idx = taxonomy_index_for_category(&ds->taxonomy, TC_OCEAN); - if (idx == -1) - idx = ds->taxonomy.nr; - if (idx < TC_NR_CATEGORIES) { - ds->taxonomy.category[idx].category = TC_OCEAN; - ds->taxonomy.category[idx].origin = taxonomy::GEOCODED; - ds->taxonomy.category[idx].value = copy_string(qPrintable(oceanName["name"].toString())); - if (idx == ds->taxonomy.nr) - ds->taxonomy.nr++; - } - mark_divelist_changed(true); - } - } else { - report_error("timeout accessing geonames.org"); - disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - reply->abort(); - } - -clear_reply: - reply->deleteLater(); - } - rgl->deleteLater(); -} - -void ReverseGeoLookupThread::lookup(dive_site *ds) -{ - if (!ds) - return; - GeoLookupInfo info; - info.lat = ds->latitude; - info.lon = ds->longitude; - info.uuid = ds->uuid; - - geo_lookup_data.clear(); - geo_lookup_data.append(info); - run(); -} - -extern "C" void add_geo_information_for_lookup(degrees_t latitude, degrees_t longitude, uint32_t uuid) { - GeoLookupInfo info; - info.lat = latitude; - info.lon = longitude; - info.uuid = uuid; - - geo_lookup_data.append(info); -} diff --git a/divesitehelpers.h b/divesitehelpers.h deleted file mode 100644 index a08069bc0..000000000 --- a/divesitehelpers.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef DIVESITEHELPERS_H -#define DIVESITEHELPERS_H - -#include "units.h" -#include - -class ReverseGeoLookupThread : public QThread { -Q_OBJECT -public: - static ReverseGeoLookupThread *instance(); - void lookup(struct dive_site *ds); - void run() Q_DECL_OVERRIDE; - -private: - ReverseGeoLookupThread(QObject *parent = 0); -}; - -#endif // DIVESITEHELPERS_H diff --git a/equipment.c b/equipment.c deleted file mode 100644 index 47c439735..000000000 --- a/equipment.c +++ /dev/null @@ -1,235 +0,0 @@ -/* equipment.c */ -#include -#include -#include -#include -#include -#include "gettext.h" -#include "dive.h" -#include "display.h" -#include "divelist.h" - -/* placeholders for a few functions that we need to redesign for the Qt UI */ -void add_cylinder_description(cylinder_type_t *type) -{ - const char *desc; - int i; - - desc = type->description; - if (!desc) - return; - for (i = 0; i < 100 && tank_info[i].name != NULL; i++) { - if (strcmp(tank_info[i].name, desc) == 0) - return; - } - if (i < 100) { - // FIXME: leaked on exit - tank_info[i].name = strdup(desc); - tank_info[i].ml = type->size.mliter; - tank_info[i].bar = type->workingpressure.mbar / 1000; - } -} -void add_weightsystem_description(weightsystem_t *weightsystem) -{ - const char *desc; - int i; - - desc = weightsystem->description; - if (!desc) - return; - for (i = 0; i < 100 && ws_info[i].name != NULL; i++) { - if (strcmp(ws_info[i].name, desc) == 0) { - ws_info[i].grams = weightsystem->weight.grams; - return; - } - } - if (i < 100) { - // FIXME: leaked on exit - ws_info[i].name = strdup(desc); - ws_info[i].grams = weightsystem->weight.grams; - } -} - -bool cylinder_nodata(cylinder_t *cyl) -{ - return !cyl->type.size.mliter && - !cyl->type.workingpressure.mbar && - !cyl->type.description && - !cyl->gasmix.o2.permille && - !cyl->gasmix.he.permille && - !cyl->start.mbar && - !cyl->end.mbar && - !cyl->gas_used.mliter && - !cyl->deco_gas_used.mliter; -} - -static bool cylinder_nosamples(cylinder_t *cyl) -{ - return !cyl->sample_start.mbar && - !cyl->sample_end.mbar; -} - -bool cylinder_none(void *_data) -{ - cylinder_t *cyl = _data; - return cylinder_nodata(cyl) && cylinder_nosamples(cyl); -} - -void get_gas_string(const struct gasmix *gasmix, char *text, int len) -{ - if (gasmix_is_air(gasmix)) - snprintf(text, len, "%s", translate("gettextFromC", "air")); - else if (get_he(gasmix) == 0 && get_o2(gasmix) < 1000) - snprintf(text, len, translate("gettextFromC", "EAN%d"), (get_o2(gasmix) + 5) / 10); - else if (get_he(gasmix) == 0 && get_o2(gasmix) == 1000) - snprintf(text, len, "%s", translate("gettextFromC", "oxygen")); - else - snprintf(text, len, "(%d/%d)", (get_o2(gasmix) + 5) / 10, (get_he(gasmix) + 5) / 10); -} - -/* Returns a static char buffer - only good for immediate use by printf etc */ -const char *gasname(const struct gasmix *gasmix) -{ - static char gas[64]; - get_gas_string(gasmix, gas, sizeof(gas)); - return gas; -} - -bool weightsystem_none(void *_data) -{ - weightsystem_t *ws = _data; - return !ws->weight.grams && !ws->description; -} - -bool no_weightsystems(weightsystem_t *ws) -{ - int i; - - for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) - if (!weightsystem_none(ws + i)) - return false; - return true; -} - -static bool one_weightsystem_equal(weightsystem_t *ws1, weightsystem_t *ws2) -{ - return ws1->weight.grams == ws2->weight.grams && - same_string(ws1->description, ws2->description); -} - -bool weightsystems_equal(weightsystem_t *ws1, weightsystem_t *ws2) -{ - int i; - - for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) - if (!one_weightsystem_equal(ws1 + i, ws2 + i)) - return false; - return true; -} - -/* - * We hardcode the most common standard cylinders, - * we should pick up any other names from the dive - * logs directly. - */ -struct tank_info_t tank_info[100] = { - /* Need an empty entry for the no-cylinder case */ - { "", }, - - /* Size-only metric cylinders */ - { "10.0â„“", .ml = 10000 }, - { "11.1â„“", .ml = 11100 }, - - /* Most common AL cylinders */ - { "AL40", .cuft = 40, .psi = 3000 }, - { "AL50", .cuft = 50, .psi = 3000 }, - { "AL63", .cuft = 63, .psi = 3000 }, - { "AL72", .cuft = 72, .psi = 3000 }, - { "AL80", .cuft = 80, .psi = 3000 }, - { "AL100", .cuft = 100, .psi = 3300 }, - - /* Metric AL cylinders */ - { "ALU7", .ml = 7000, .bar = 200 }, - - /* Somewhat common LP steel cylinders */ - { "LP85", .cuft = 85, .psi = 2640 }, - { "LP95", .cuft = 95, .psi = 2640 }, - { "LP108", .cuft = 108, .psi = 2640 }, - { "LP121", .cuft = 121, .psi = 2640 }, - - /* Somewhat common HP steel cylinders */ - { "HP65", .cuft = 65, .psi = 3442 }, - { "HP80", .cuft = 80, .psi = 3442 }, - { "HP100", .cuft = 100, .psi = 3442 }, - { "HP119", .cuft = 119, .psi = 3442 }, - { "HP130", .cuft = 130, .psi = 3442 }, - - /* Common European steel cylinders */ - { "3â„“ 232 bar", .ml = 3000, .bar = 232 }, - { "3â„“ 300 bar", .ml = 3000, .bar = 300 }, - { "10â„“ 300 bar", .ml = 10000, .bar = 300 }, - { "12â„“ 200 bar", .ml = 12000, .bar = 200 }, - { "12â„“ 232 bar", .ml = 12000, .bar = 232 }, - { "12â„“ 300 bar", .ml = 12000, .bar = 300 }, - { "15â„“ 200 bar", .ml = 15000, .bar = 200 }, - { "15â„“ 232 bar", .ml = 15000, .bar = 232 }, - { "D7 300 bar", .ml = 14000, .bar = 300 }, - { "D8.5 232 bar", .ml = 17000, .bar = 232 }, - { "D12 232 bar", .ml = 24000, .bar = 232 }, - { "D13 232 bar", .ml = 26000, .bar = 232 }, - { "D15 232 bar", .ml = 30000, .bar = 232 }, - { "D16 232 bar", .ml = 32000, .bar = 232 }, - { "D18 232 bar", .ml = 36000, .bar = 232 }, - { "D20 232 bar", .ml = 40000, .bar = 232 }, - - /* We'll fill in more from the dive log dynamically */ - { NULL, } -}; - -/* - * We hardcode the most common weight system types - * This is a bit odd as the weight system types don't usually encode weight - */ -struct ws_info_t ws_info[100] = { - { QT_TRANSLATE_NOOP("gettextFromC", "integrated"), 0 }, - { QT_TRANSLATE_NOOP("gettextFromC", "belt"), 0 }, - { QT_TRANSLATE_NOOP("gettextFromC", "ankle"), 0 }, - { QT_TRANSLATE_NOOP("gettextFromC", "backplate weight"), 0 }, - { QT_TRANSLATE_NOOP("gettextFromC", "clip-on"), 0 }, -}; - -void remove_cylinder(struct dive *dive, int idx) -{ - cylinder_t *cyl = dive->cylinder + idx; - int nr = MAX_CYLINDERS - idx - 1; - memmove(cyl, cyl + 1, nr * sizeof(*cyl)); - memset(cyl + nr, 0, sizeof(*cyl)); -} - -void remove_weightsystem(struct dive *dive, int idx) -{ - weightsystem_t *ws = dive->weightsystem + idx; - int nr = MAX_WEIGHTSYSTEMS - idx - 1; - memmove(ws, ws + 1, nr * sizeof(*ws)); - memset(ws + nr, 0, sizeof(*ws)); -} - -/* when planning a dive we need to make sure that all cylinders have a sane depth assigned - * and if we are tracking gas consumption the pressures need to be reset to start = end = workingpressure */ -void reset_cylinders(struct dive *dive, bool track_gas) -{ - int i; - pressure_t decopo2 = {.mbar = prefs.decopo2}; - - for (i = 0; i < MAX_CYLINDERS; i++) { - cylinder_t *cyl = &dive->cylinder[i]; - if (cylinder_none(cyl)) - continue; - if (cyl->depth.mm == 0) /* if the gas doesn't give a mod, calculate based on prefs */ - cyl->depth = gas_mod(&cyl->gasmix, decopo2, dive, M_OR_FT(3,10)); - if (track_gas) - cyl->start.mbar = cyl->end.mbar = cyl->type.workingpressure.mbar; - cyl->gas_used.mliter = 0; - cyl->deco_gas_used.mliter = 0; - } -} diff --git a/exif.cpp b/exif.cpp deleted file mode 100644 index 0b1cda2bc..000000000 --- a/exif.cpp +++ /dev/null @@ -1,587 +0,0 @@ -#include -/************************************************************************** - exif.cpp -- A simple ISO C++ library to parse basic EXIF - information from a JPEG file. - - Copyright (c) 2010-2013 Mayank Lahiri - mlahiri@gmail.com - All rights reserved (BSD License). - - See exif.h for version history. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - -- Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS - OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN - NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -#include -#include "dive.h" -#include "exif.h" - -using std::string; - -namespace { - // IF Entry - struct IFEntry { - // Raw fields - unsigned short tag; - unsigned short format; - unsigned data; - unsigned length; - - // Parsed fields - string val_string; - unsigned short val_16; - unsigned val_32; - double val_rational; - unsigned char val_byte; - }; - - // Helper functions - unsigned int parse32(const unsigned char *buf, bool intel) - { - if (intel) - return ((unsigned)buf[3] << 24) | - ((unsigned)buf[2] << 16) | - ((unsigned)buf[1] << 8) | - buf[0]; - - return ((unsigned)buf[0] << 24) | - ((unsigned)buf[1] << 16) | - ((unsigned)buf[2] << 8) | - buf[3]; - } - - unsigned short parse16(const unsigned char *buf, bool intel) - { - if (intel) - return ((unsigned)buf[1] << 8) | buf[0]; - return ((unsigned)buf[0] << 8) | buf[1]; - } - - string parseEXIFString(const unsigned char *buf, - const unsigned num_components, - const unsigned data, - const unsigned base, - const unsigned len) - { - string value; - if (num_components <= 4) - value.assign((const char *)&data, num_components); - else { - if (base + data + num_components <= len) - value.assign((const char *)(buf + base + data), num_components); - } - return value; - } - - double parseEXIFRational(const unsigned char *buf, bool intel) - { - double numerator = 0; - double denominator = 1; - - numerator = (double)parse32(buf, intel); - denominator = (double)parse32(buf + 4, intel); - if (denominator < 1e-20) - return 0; - return numerator / denominator; - } - - IFEntry parseIFEntry(const unsigned char *buf, - const unsigned offs, - const bool alignIntel, - const unsigned base, - const unsigned len) - { - IFEntry result; - - // Each directory entry is composed of: - // 2 bytes: tag number (data field) - // 2 bytes: data format - // 4 bytes: number of components - // 4 bytes: data value or offset to data value - result.tag = parse16(buf + offs, alignIntel); - result.format = parse16(buf + offs + 2, alignIntel); - result.length = parse32(buf + offs + 4, alignIntel); - result.data = parse32(buf + offs + 8, alignIntel); - - // Parse value in specified format - switch (result.format) { - case 1: - result.val_byte = (unsigned char)*(buf + offs + 8); - break; - case 2: - result.val_string = parseEXIFString(buf, result.length, result.data, base, len); - break; - case 3: - result.val_16 = parse16((const unsigned char *)buf + offs + 8, alignIntel); - break; - case 4: - result.val_32 = result.data; - break; - case 5: - if (base + result.data + 8 <= len) - result.val_rational = parseEXIFRational(buf + base + result.data, alignIntel); - break; - case 7: - case 9: - case 10: - break; - default: - result.tag = 0xFF; - } - return result; - } -} - -// -// Locates the EXIF segment and parses it using parseFromEXIFSegment -// -int EXIFInfo::parseFrom(const unsigned char *buf, unsigned len) -{ - // Sanity check: all JPEG files start with 0xFFD8 and end with 0xFFD9 - // This check also ensures that the user has supplied a correct value for len. - if (!buf || len < 4) - return PARSE_EXIF_ERROR_NO_EXIF; - if (buf[0] != 0xFF || buf[1] != 0xD8) - return PARSE_EXIF_ERROR_NO_JPEG; - if (buf[len - 2] != 0xFF || buf[len - 1] != 0xD9) - return PARSE_EXIF_ERROR_NO_JPEG; - clear(); - - // Scan for EXIF header (bytes 0xFF 0xE1) and do a sanity check by - // looking for bytes "Exif\0\0". The marker length data is in Motorola - // byte order, which results in the 'false' parameter to parse16(). - // The marker has to contain at least the TIFF header, otherwise the - // EXIF data is corrupt. So the minimum length specified here has to be: - // 2 bytes: section size - // 6 bytes: "Exif\0\0" string - // 2 bytes: TIFF header (either "II" or "MM" string) - // 2 bytes: TIFF magic (short 0x2a00 in Motorola byte order) - // 4 bytes: Offset to first IFD - // ========= - // 16 bytes - unsigned offs = 0; // current offset into buffer - for (offs = 0; offs < len - 1; offs++) - if (buf[offs] == 0xFF && buf[offs + 1] == 0xE1) - break; - if (offs + 4 > len) - return PARSE_EXIF_ERROR_NO_EXIF; - offs += 2; - unsigned short section_length = parse16(buf + offs, false); - if (offs + section_length > len || section_length < 16) - return PARSE_EXIF_ERROR_CORRUPT; - offs += 2; - - return parseFromEXIFSegment(buf + offs, len - offs); -} - -int EXIFInfo::parseFrom(const string &data) -{ - return parseFrom((const unsigned char *)data.data(), data.length()); -} - -// -// Main parsing function for an EXIF segment. -// -// PARAM: 'buf' start of the EXIF TIFF, which must be the bytes "Exif\0\0". -// PARAM: 'len' length of buffer -// -int EXIFInfo::parseFromEXIFSegment(const unsigned char *buf, unsigned len) -{ - bool alignIntel = true; // byte alignment (defined in EXIF header) - unsigned offs = 0; // current offset into buffer - if (!buf || len < 6) - return PARSE_EXIF_ERROR_NO_EXIF; - - if (!std::equal(buf, buf + 6, "Exif\0\0")) - return PARSE_EXIF_ERROR_NO_EXIF; - offs += 6; - - // Now parsing the TIFF header. The first two bytes are either "II" or - // "MM" for Intel or Motorola byte alignment. Sanity check by parsing - // the unsigned short that follows, making sure it equals 0x2a. The - // last 4 bytes are an offset into the first IFD, which are added to - // the global offset counter. For this block, we expect the following - // minimum size: - // 2 bytes: 'II' or 'MM' - // 2 bytes: 0x002a - // 4 bytes: offset to first IDF - // ----------------------------- - // 8 bytes - if (offs + 8 > len) - return PARSE_EXIF_ERROR_CORRUPT; - unsigned tiff_header_start = offs; - if (buf[offs] == 'I' && buf[offs + 1] == 'I') - alignIntel = true; - else { - if (buf[offs] == 'M' && buf[offs + 1] == 'M') - alignIntel = false; - else - return PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN; - } - this->ByteAlign = alignIntel; - offs += 2; - if (0x2a != parse16(buf + offs, alignIntel)) - return PARSE_EXIF_ERROR_CORRUPT; - offs += 2; - unsigned first_ifd_offset = parse32(buf + offs, alignIntel); - offs += first_ifd_offset - 4; - if (offs >= len) - return PARSE_EXIF_ERROR_CORRUPT; - - // Now parsing the first Image File Directory (IFD0, for the main image). - // An IFD consists of a variable number of 12-byte directory entries. The - // first two bytes of the IFD section contain the number of directory - // entries in the section. The last 4 bytes of the IFD contain an offset - // to the next IFD, which means this IFD must contain exactly 6 + 12 * num - // bytes of data. - if (offs + 2 > len) - return PARSE_EXIF_ERROR_CORRUPT; - int num_entries = parse16(buf + offs, alignIntel); - if (offs + 6 + 12 * num_entries > len) - return PARSE_EXIF_ERROR_CORRUPT; - offs += 2; - unsigned exif_sub_ifd_offset = len; - unsigned gps_sub_ifd_offset = len; - while (--num_entries >= 0) { - IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); - offs += 12; - switch (result.tag) { - case 0x102: - // Bits per sample - if (result.format == 3) - this->BitsPerSample = result.val_16; - break; - - case 0x10E: - // Image description - if (result.format == 2) - this->ImageDescription = result.val_string; - break; - - case 0x10F: - // Digicam make - if (result.format == 2) - this->Make = result.val_string; - break; - - case 0x110: - // Digicam model - if (result.format == 2) - this->Model = result.val_string; - break; - - case 0x112: - // Orientation of image - if (result.format == 3) - this->Orientation = result.val_16; - break; - - case 0x131: - // Software used for image - if (result.format == 2) - this->Software = result.val_string; - break; - - case 0x132: - // EXIF/TIFF date/time of image modification - if (result.format == 2) - this->DateTime = result.val_string; - break; - - case 0x8298: - // Copyright information - if (result.format == 2) - this->Copyright = result.val_string; - break; - - case 0x8825: - // GPS IFS offset - gps_sub_ifd_offset = tiff_header_start + result.data; - break; - - case 0x8769: - // EXIF SubIFD offset - exif_sub_ifd_offset = tiff_header_start + result.data; - break; - } - } - - // Jump to the EXIF SubIFD if it exists and parse all the information - // there. Note that it's possible that the EXIF SubIFD doesn't exist. - // The EXIF SubIFD contains most of the interesting information that a - // typical user might want. - if (exif_sub_ifd_offset + 4 <= len) { - offs = exif_sub_ifd_offset; - int num_entries = parse16(buf + offs, alignIntel); - if (offs + 6 + 12 * num_entries > len) - return PARSE_EXIF_ERROR_CORRUPT; - offs += 2; - while (--num_entries >= 0) { - IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); - switch (result.tag) { - case 0x829a: - // Exposure time in seconds - if (result.format == 5) - this->ExposureTime = result.val_rational; - break; - - case 0x829d: - // FNumber - if (result.format == 5) - this->FNumber = result.val_rational; - break; - - case 0x8827: - // ISO Speed Rating - if (result.format == 3) - this->ISOSpeedRatings = result.val_16; - break; - - case 0x9003: - // Original date and time - if (result.format == 2) - this->DateTimeOriginal = result.val_string; - break; - - case 0x9004: - // Digitization date and time - if (result.format == 2) - this->DateTimeDigitized = result.val_string; - break; - - case 0x9201: - // Shutter speed value - if (result.format == 5) - this->ShutterSpeedValue = result.val_rational; - break; - - case 0x9204: - // Exposure bias value - if (result.format == 5) - this->ExposureBiasValue = result.val_rational; - break; - - case 0x9206: - // Subject distance - if (result.format == 5) - this->SubjectDistance = result.val_rational; - break; - - case 0x9209: - // Flash used - if (result.format == 3) - this->Flash = result.data ? 1 : 0; - break; - - case 0x920a: - // Focal length - if (result.format == 5) - this->FocalLength = result.val_rational; - break; - - case 0x9207: - // Metering mode - if (result.format == 3) - this->MeteringMode = result.val_16; - break; - - case 0x9291: - // Subsecond original time - if (result.format == 2) - this->SubSecTimeOriginal = result.val_string; - break; - - case 0xa002: - // EXIF Image width - if (result.format == 4) - this->ImageWidth = result.val_32; - if (result.format == 3) - this->ImageWidth = result.val_16; - break; - - case 0xa003: - // EXIF Image height - if (result.format == 4) - this->ImageHeight = result.val_32; - if (result.format == 3) - this->ImageHeight = result.val_16; - break; - - case 0xa405: - // Focal length in 35mm film - if (result.format == 3) - this->FocalLengthIn35mm = result.val_16; - break; - } - offs += 12; - } - } - - // Jump to the GPS SubIFD if it exists and parse all the information - // there. Note that it's possible that the GPS SubIFD doesn't exist. - if (gps_sub_ifd_offset + 4 <= len) { - offs = gps_sub_ifd_offset; - int num_entries = parse16(buf + offs, alignIntel); - if (offs + 6 + 12 * num_entries > len) - return PARSE_EXIF_ERROR_CORRUPT; - offs += 2; - while (--num_entries >= 0) { - unsigned short tag = parse16(buf + offs, alignIntel); - unsigned short format = parse16(buf + offs + 2, alignIntel); - unsigned length = parse32(buf + offs + 4, alignIntel); - unsigned data = parse32(buf + offs + 8, alignIntel); - switch (tag) { - case 1: - // GPS north or south - this->GeoLocation.LatComponents.direction = *(buf + offs + 8); - if ('S' == this->GeoLocation.LatComponents.direction) - this->GeoLocation.Latitude = -this->GeoLocation.Latitude; - break; - - case 2: - // GPS latitude - if (format == 5 && length == 3) { - this->GeoLocation.LatComponents.degrees = - parseEXIFRational(buf + data + tiff_header_start, alignIntel); - this->GeoLocation.LatComponents.minutes = - parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); - this->GeoLocation.LatComponents.seconds = - parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); - this->GeoLocation.Latitude = - this->GeoLocation.LatComponents.degrees + - this->GeoLocation.LatComponents.minutes / 60 + - this->GeoLocation.LatComponents.seconds / 3600; - if ('S' == this->GeoLocation.LatComponents.direction) - this->GeoLocation.Latitude = -this->GeoLocation.Latitude; - } - break; - - case 3: - // GPS east or west - this->GeoLocation.LonComponents.direction = *(buf + offs + 8); - if ('W' == this->GeoLocation.LonComponents.direction) - this->GeoLocation.Longitude = -this->GeoLocation.Longitude; - break; - - case 4: - // GPS longitude - if (format == 5 && length == 3) { - this->GeoLocation.LonComponents.degrees = - parseEXIFRational(buf + data + tiff_header_start, alignIntel); - this->GeoLocation.LonComponents.minutes = - parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); - this->GeoLocation.LonComponents.seconds = - parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); - this->GeoLocation.Longitude = - this->GeoLocation.LonComponents.degrees + - this->GeoLocation.LonComponents.minutes / 60 + - this->GeoLocation.LonComponents.seconds / 3600; - if ('W' == this->GeoLocation.LonComponents.direction) - this->GeoLocation.Longitude = -this->GeoLocation.Longitude; - } - break; - - case 5: - // GPS altitude reference (below or above sea level) - this->GeoLocation.AltitudeRef = *(buf + offs + 8); - if (1 == this->GeoLocation.AltitudeRef) - this->GeoLocation.Altitude = -this->GeoLocation.Altitude; - break; - - case 6: - // GPS altitude reference - if (format == 5) { - this->GeoLocation.Altitude = - parseEXIFRational(buf + data + tiff_header_start, alignIntel); - if (1 == this->GeoLocation.AltitudeRef) - this->GeoLocation.Altitude = -this->GeoLocation.Altitude; - } - break; - } - offs += 12; - } - } - - return PARSE_EXIF_SUCCESS; -} - -void EXIFInfo::clear() -{ - // Strings - ImageDescription.clear(); - Make.clear(); - Model.clear(); - Software.clear(); - DateTime.clear(); - DateTimeOriginal.clear(); - DateTimeDigitized.clear(); - SubSecTimeOriginal.clear(); - Copyright.clear(); - - // Shorts / unsigned / double - ByteAlign = 0; - Orientation = 0; - - BitsPerSample = 0; - ExposureTime = 0; - FNumber = 0; - ISOSpeedRatings = 0; - ShutterSpeedValue = 0; - ExposureBiasValue = 0; - SubjectDistance = 0; - FocalLength = 0; - FocalLengthIn35mm = 0; - Flash = 0; - MeteringMode = 0; - ImageWidth = 0; - ImageHeight = 0; - - // Geolocation - GeoLocation.Latitude = 0; - GeoLocation.Longitude = 0; - GeoLocation.Altitude = 0; - GeoLocation.AltitudeRef = 0; - GeoLocation.LatComponents.degrees = 0; - GeoLocation.LatComponents.minutes = 0; - GeoLocation.LatComponents.seconds = 0; - GeoLocation.LatComponents.direction = 0; - GeoLocation.LonComponents.degrees = 0; - GeoLocation.LonComponents.minutes = 0; - GeoLocation.LonComponents.seconds = 0; - GeoLocation.LonComponents.direction = 0; -} - -time_t EXIFInfo::epoch() -{ - struct tm tm; - int year, month, day, hour, min, sec; - - if (DateTimeOriginal.size()) - sscanf(DateTimeOriginal.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); - else - sscanf(DateTime.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); - tm.tm_year = year; - tm.tm_mon = month - 1; - tm.tm_mday = day; - tm.tm_hour = hour; - tm.tm_min = min; - tm.tm_sec = sec; - return (utc_mktime(&tm)); -} diff --git a/exif.h b/exif.h deleted file mode 100644 index 0fb3a7d4a..000000000 --- a/exif.h +++ /dev/null @@ -1,147 +0,0 @@ -/************************************************************************** - exif.h -- A simple ISO C++ library to parse basic EXIF - information from a JPEG file. - - Based on the description of the EXIF file format at: - -- http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html - -- http://www.media.mit.edu/pia/Research/deepview/exif.html - -- http://www.exif.org/Exif2-2.PDF - - Copyright (c) 2010-2013 Mayank Lahiri - mlahiri@gmail.com - All rights reserved. - - VERSION HISTORY: - ================ - - 2.1: Released July 2013 - -- fixed a bug where JPEGs without an EXIF SubIFD would not be parsed - -- fixed a bug in parsing GPS coordinate seconds - -- fixed makefile bug - -- added two pathological test images from Matt Galloway - http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/ - -- split main parsing routine for easier integration into Firefox - - 2.0: Released February 2013 - -- complete rewrite - -- no new/delete - -- added GPS support - - 1.0: Released 2010 - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - -- Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS - OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN - NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -#ifndef EXIF_H -#define EXIF_H - -#include - -// -// Class responsible for storing and parsing EXIF information from a JPEG blob -// -class EXIFInfo { -public: - // Parsing function for an entire JPEG image buffer. - // - // PARAM 'data': A pointer to a JPEG image. - // PARAM 'length': The length of the JPEG image. - // RETURN: PARSE_EXIF_SUCCESS (0) on succes with 'result' filled out - // error code otherwise, as defined by the PARSE_EXIF_ERROR_* macros - int parseFrom(const unsigned char *data, unsigned length); - int parseFrom(const std::string &data); - - // Parsing function for an EXIF segment. This is used internally by parseFrom() - // but can be called for special cases where only the EXIF section is - // available (i.e., a blob starting with the bytes "Exif\0\0"). - int parseFromEXIFSegment(const unsigned char *buf, unsigned len); - - // Set all data members to default values. - void clear(); - - // Data fields filled out by parseFrom() - char ByteAlign; // 0 = Motorola byte alignment, 1 = Intel - std::string ImageDescription; // Image description - std::string Make; // Camera manufacturer's name - std::string Model; // Camera model - unsigned short Orientation; // Image orientation, start of data corresponds to - // 0: unspecified in EXIF data - // 1: upper left of image - // 3: lower right of image - // 6: upper right of image - // 8: lower left of image - // 9: undefined - unsigned short BitsPerSample; // Number of bits per component - std::string Software; // Software used - std::string DateTime; // File change date and time - std::string DateTimeOriginal; // Original file date and time (may not exist) - std::string DateTimeDigitized; // Digitization date and time (may not exist) - std::string SubSecTimeOriginal; // Sub-second time that original picture was taken - std::string Copyright; // File copyright information - double ExposureTime; // Exposure time in seconds - double FNumber; // F/stop - unsigned short ISOSpeedRatings; // ISO speed - double ShutterSpeedValue; // Shutter speed (reciprocal of exposure time) - double ExposureBiasValue; // Exposure bias value in EV - double SubjectDistance; // Distance to focus point in meters - double FocalLength; // Focal length of lens in millimeters - unsigned short FocalLengthIn35mm; // Focal length in 35mm film - char Flash; // 0 = no flash, 1 = flash used - unsigned short MeteringMode; // Metering mode - // 1: average - // 2: center weighted average - // 3: spot - // 4: multi-spot - // 5: multi-segment - unsigned ImageWidth; // Image width reported in EXIF data - unsigned ImageHeight; // Image height reported in EXIF data - struct Geolocation_t - { // GPS information embedded in file - double Latitude; // Image latitude expressed as decimal - double Longitude; // Image longitude expressed as decimal - double Altitude; // Altitude in meters, relative to sea level - char AltitudeRef; // 0 = above sea level, -1 = below sea level - struct Coord_t { - double degrees; - double minutes; - double seconds; - char direction; - } LatComponents, LonComponents; // Latitude, Longitude expressed in deg/min/sec - } GeoLocation; - EXIFInfo() - { - clear(); - } - - time_t epoch(); -}; - -// Parse was successful -#define PARSE_EXIF_SUCCESS 0 -// No JPEG markers found in buffer, possibly invalid JPEG file -#define PARSE_EXIF_ERROR_NO_JPEG 1982 -// No EXIF header found in JPEG file. -#define PARSE_EXIF_ERROR_NO_EXIF 1983 -// Byte alignment specified in EXIF file was unknown (not Motorola or Intel). -#define PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN 1984 -// EXIF header was found, but data was corrupted. -#define PARSE_EXIF_ERROR_CORRUPT 1985 - -#endif // EXIF_H diff --git a/file.c b/file.c deleted file mode 100644 index c4032c1f2..000000000 --- a/file.c +++ /dev/null @@ -1,1066 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "gettext.h" -#include -#include - -#include "dive.h" -#include "file.h" -#include "git-access.h" -#include "qthelperfromc.h" - -/* For SAMPLE_* */ -#include - -/* to check XSLT version number */ -#include - -/* Crazy windows sh*t */ -#ifndef O_BINARY -#define O_BINARY 0 -#endif - -int readfile(const char *filename, struct memblock *mem) -{ - int ret, fd; - struct stat st; - char *buf; - - mem->buffer = NULL; - mem->size = 0; - - fd = subsurface_open(filename, O_RDONLY | O_BINARY, 0); - if (fd < 0) - return fd; - ret = fstat(fd, &st); - if (ret < 0) - goto out; - ret = -EINVAL; - if (!S_ISREG(st.st_mode)) - goto out; - ret = 0; - if (!st.st_size) - goto out; - buf = malloc(st.st_size + 1); - ret = -1; - errno = ENOMEM; - if (!buf) - goto out; - mem->buffer = buf; - mem->size = st.st_size; - ret = read(fd, buf, mem->size); - if (ret < 0) - goto free; - buf[ret] = 0; - if (ret == mem->size) - goto out; - errno = EIO; - ret = -1; -free: - free(mem->buffer); - mem->buffer = NULL; - mem->size = 0; -out: - close(fd); - return ret; -} - - -static void zip_read(struct zip_file *file, const char *filename) -{ - int size = 1024, n, read = 0; - char *mem = malloc(size); - - while ((n = zip_fread(file, mem + read, size - read)) > 0) { - read += n; - size = read * 3 / 2; - mem = realloc(mem, size); - } - mem[read] = 0; - (void) parse_xml_buffer(filename, mem, read, &dive_table, NULL); - free(mem); -} - -int try_to_open_zip(const char *filename, struct memblock *mem) -{ - int success = 0; - /* Grr. libzip needs to re-open the file, it can't take a buffer */ - struct zip *zip = subsurface_zip_open_readonly(filename, ZIP_CHECKCONS, NULL); - - if (zip) { - int index; - for (index = 0;; index++) { - struct zip_file *file = zip_fopen_index(zip, index, 0); - if (!file) - break; - /* skip parsing the divelogs.de pictures */ - if (strstr(zip_get_name(zip, index, 0), "pictures/")) - continue; - zip_read(file, filename); - zip_fclose(file); - success++; - } - subsurface_zip_close(zip); - } - return success; -} - -static int try_to_xslt_open_csv(const char *filename, struct memblock *mem, const char *tag) -{ - char *buf; - - if (mem->size == 0 && readfile(filename, mem) < 0) - return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); - - /* Surround the CSV file content with XML tags to enable XSLT - * parsing - * - * Tag markers take: strlen("<>") = 5 - */ - buf = realloc(mem->buffer, mem->size + 7 + strlen(tag) * 2); - if (buf != NULL) { - char *starttag = NULL; - char *endtag = NULL; - - starttag = malloc(3 + strlen(tag)); - endtag = malloc(5 + strlen(tag)); - - if (starttag == NULL || endtag == NULL) { - /* this is fairly silly - so the malloc fails, but we strdup the error? - * let's complete the silliness by freeing the two pointers in case one malloc succeeded - * and the other one failed - this will make static analysis tools happy */ - free(starttag); - free(endtag); - free(buf); - return report_error("Memory allocation failed in %s", __func__); - } - - sprintf(starttag, "<%s>", tag); - sprintf(endtag, "\n", tag); - - memmove(buf + 2 + strlen(tag), buf, mem->size); - memcpy(buf, starttag, 2 + strlen(tag)); - memcpy(buf + mem->size + 2 + strlen(tag), endtag, 5 + strlen(tag)); - mem->size += (6 + 2 * strlen(tag)); - mem->buffer = buf; - - free(starttag); - free(endtag); - } else { - free(mem->buffer); - return report_error("realloc failed in %s", __func__); - } - - return 0; -} - -int db_test_func(void *param, int columns, char **data, char **column) -{ - return *data[0] == '0'; -} - - -static int try_to_open_db(const char *filename, struct memblock *mem) -{ - sqlite3 *handle; - char dm4_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%ProfileBlob%'"; - char dm5_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%SampleBlob%'"; - char shearwater_test[] = "select count(*) from sqlite_master where type='table' and name='system' and sql like '%dbVersion%'"; - char cobalt_test[] = "select count(*) from sqlite_master where type='table' and name='TrackPoints' and sql like '%DepthPressure%'"; - char divinglog_test[] = "select count(*) from sqlite_master where type='table' and name='DBInfo' and sql like '%PrgName%'"; - int retval; - - retval = sqlite3_open(filename, &handle); - - if (retval) { - fprintf(stderr, "Database connection failed '%s'.\n", filename); - return 1; - } - - /* Testing if DB schema resembles Suunto DM5 database format */ - retval = sqlite3_exec(handle, dm5_test, &db_test_func, 0, NULL); - if (!retval) { - retval = parse_dm5_buffer(handle, filename, mem->buffer, mem->size, &dive_table); - sqlite3_close(handle); - return retval; - } - - /* Testing if DB schema resembles Suunto DM4 database format */ - retval = sqlite3_exec(handle, dm4_test, &db_test_func, 0, NULL); - if (!retval) { - retval = parse_dm4_buffer(handle, filename, mem->buffer, mem->size, &dive_table); - sqlite3_close(handle); - return retval; - } - - /* Testing if DB schema resembles Shearwater database format */ - retval = sqlite3_exec(handle, shearwater_test, &db_test_func, 0, NULL); - if (!retval) { - retval = parse_shearwater_buffer(handle, filename, mem->buffer, mem->size, &dive_table); - sqlite3_close(handle); - return retval; - } - - /* Testing if DB schema resembles Atomic Cobalt database format */ - retval = sqlite3_exec(handle, cobalt_test, &db_test_func, 0, NULL); - if (!retval) { - retval = parse_cobalt_buffer(handle, filename, mem->buffer, mem->size, &dive_table); - sqlite3_close(handle); - return retval; - } - - /* Testing if DB schema resembles Divinglog database format */ - retval = sqlite3_exec(handle, divinglog_test, &db_test_func, 0, NULL); - if (!retval) { - retval = parse_divinglog_buffer(handle, filename, mem->buffer, mem->size, &dive_table); - sqlite3_close(handle); - return retval; - } - - sqlite3_close(handle); - - return retval; -} - -timestamp_t parse_date(const char *date) -{ - int hour, min, sec; - struct tm tm; - char *p; - - memset(&tm, 0, sizeof(tm)); - tm.tm_mday = strtol(date, &p, 10); - if (tm.tm_mday < 1 || tm.tm_mday > 31) - return 0; - for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) { - if (!memcmp(p, monthname(tm.tm_mon), 3)) - break; - } - if (tm.tm_mon > 11) - return 0; - date = p + 3; - tm.tm_year = strtol(date, &p, 10); - if (date == p) - return 0; - if (tm.tm_year < 70) - tm.tm_year += 2000; - if (tm.tm_year < 100) - tm.tm_year += 1900; - if (sscanf(p, "%d:%d:%d", &hour, &min, &sec) != 3) - return 0; - tm.tm_hour = hour; - tm.tm_min = min; - tm.tm_sec = sec; - return utc_mktime(&tm); -} - -enum csv_format { - CSV_DEPTH, - CSV_TEMP, - CSV_PRESSURE, - POSEIDON_DEPTH, - POSEIDON_TEMP, - POSEIDON_SETPOINT, - POSEIDON_SENSOR1, - POSEIDON_SENSOR2, - POSEIDON_PRESSURE, - POSEIDON_O2CYLINDER, - POSEIDON_NDL, - POSEIDON_CEILING -}; - -static void add_sample_data(struct sample *sample, enum csv_format type, double val) -{ - switch (type) { - case CSV_DEPTH: - sample->depth.mm = feet_to_mm(val); - break; - case CSV_TEMP: - sample->temperature.mkelvin = F_to_mkelvin(val); - break; - case CSV_PRESSURE: - sample->cylinderpressure.mbar = psi_to_mbar(val * 4); - break; - case POSEIDON_DEPTH: - sample->depth.mm = val * 0.5 *1000; - break; - case POSEIDON_TEMP: - sample->temperature.mkelvin = C_to_mkelvin(val * 0.2); - break; - case POSEIDON_SETPOINT: - sample->setpoint.mbar = val * 10; - break; - case POSEIDON_SENSOR1: - sample->o2sensor[0].mbar = val * 10; - break; - case POSEIDON_SENSOR2: - sample->o2sensor[1].mbar = val * 10; - break; - case POSEIDON_PRESSURE: - sample->cylinderpressure.mbar = val * 1000; - break; - case POSEIDON_O2CYLINDER: - sample->o2cylinderpressure.mbar = val * 1000; - break; - case POSEIDON_NDL: - sample->ndl.seconds = val * 60; - break; - case POSEIDON_CEILING: - sample->stopdepth.mm = val * 1000; - break; - } -} - -/* - * Cochran comma-separated values: depth in feet, temperature in F, pressure in psi. - * - * They start with eight comma-separated fields like: - * - * filename: {C:\Analyst4\can\T036785.can},{C:\Analyst4\can\K031892.can} - * divenr: %d - * datetime: {03Sep11 16:37:22},{15Dec11 18:27:02} - * ??: 1 - * serialnr??: {CCI134},{CCI207} - * computer??: {GeminiII},{CommanderIII} - * computer??: {GeminiII},{CommanderIII} - * ??: 1 - * - * Followed by the data values (all comma-separated, all one long line). - */ -static int try_to_open_csv(const char *filename, struct memblock *mem, enum csv_format type) -{ - char *p = mem->buffer; - char *header[8]; - int i, time; - timestamp_t date; - struct dive *dive; - struct divecomputer *dc; - - for (i = 0; i < 8; i++) { - header[i] = p; - p = strchr(p, ','); - if (!p) - return 0; - p++; - } - - date = parse_date(header[2]); - if (!date) - return 0; - - dive = alloc_dive(); - dive->when = date; - dive->number = atoi(header[1]); - dc = &dive->dc; - - time = 0; - for (;;) { - char *end; - double val; - struct sample *sample; - - errno = 0; - val = strtod(p, &end); // FIXME == localization issue - if (end == p) - break; - if (errno) - break; - - sample = prepare_sample(dc); - sample->time.seconds = time; - add_sample_data(sample, type, val); - finish_sample(dc); - - time++; - dc->duration.seconds = time; - if (*end != ',') - break; - p = end + 1; - } - record_dive(dive); - return 1; -} - -static int open_by_filename(const char *filename, const char *fmt, struct memblock *mem) -{ - // hack to be able to provide a comment for the translated string - static char *csv_warning = QT_TRANSLATE_NOOP3("gettextFromC", - "Cannot open CSV file %s; please use Import log file dialog", - "'Import log file' should be the same text as corresponding label in Import menu"); - - /* Suunto Dive Manager files: SDE, ZIP; divelogs.de files: DLD */ - if (!strcasecmp(fmt, "SDE") || !strcasecmp(fmt, "ZIP") || !strcasecmp(fmt, "DLD")) - return try_to_open_zip(filename, mem); - - /* CSV files */ - if (!strcasecmp(fmt, "CSV")) - return report_error(translate("gettextFromC", csv_warning), filename); - /* Truly nasty intentionally obfuscated Cochran Anal software */ - if (!strcasecmp(fmt, "CAN")) - return try_to_open_cochran(filename, mem); - /* Cochran export comma-separated-value files */ - if (!strcasecmp(fmt, "DPT")) - return try_to_open_csv(filename, mem, CSV_DEPTH); - if (!strcasecmp(fmt, "LVD")) - return try_to_open_liquivision(filename, mem); - if (!strcasecmp(fmt, "TMP")) - return try_to_open_csv(filename, mem, CSV_TEMP); - if (!strcasecmp(fmt, "HP1")) - return try_to_open_csv(filename, mem, CSV_PRESSURE); - - return 0; -} - -static int parse_file_buffer(const char *filename, struct memblock *mem) -{ - int ret; - char *fmt = strrchr(filename, '.'); - if (fmt && (ret = open_by_filename(filename, fmt + 1, mem)) != 0) - return ret; - - if (!mem->size || !mem->buffer) - return report_error("Out of memory parsing file %s\n", filename); - - return parse_xml_buffer(filename, mem->buffer, mem->size, &dive_table, NULL); -} - -int parse_file(const char *filename) -{ - struct git_repository *git; - const char *branch; - struct memblock mem; - char *fmt; - int ret; - - git = is_git_repository(filename, &branch, NULL, false); - if (prefs.cloud_git_url && - strstr(filename, prefs.cloud_git_url) - && git == dummy_git_repository) - /* opening the cloud storage repository failed for some reason - * give up here and don't send errors about git repositories */ - return 0; - - if (git && !git_load_dives(git, branch)) - return 0; - - if ((ret = readfile(filename, &mem)) < 0) { - /* we don't want to display an error if this was the default file or the cloud storage */ - if ((prefs.default_filename && !strcmp(filename, prefs.default_filename)) || - isCloudUrl(filename)) - return 0; - - return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); - } else if (ret == 0) { - return report_error(translate("gettextFromC", "Empty file '%s'"), filename); - } - - fmt = strrchr(filename, '.'); - if (fmt && (!strcasecmp(fmt + 1, "DB") || !strcasecmp(fmt + 1, "BAK") || !strcasecmp(fmt + 1, "SQL"))) { - if (!try_to_open_db(filename, &mem)) { - free(mem.buffer); - return 0; - } - } - - /* Divesoft Freedom */ - if (fmt && (!strcasecmp(fmt + 1, "DLF"))) { - if (!parse_dlf_buffer(mem.buffer, mem.size)) { - free(mem.buffer); - return 0; - } - return -1; - } - - /* DataTrak/Wlog */ - if (fmt && !strcasecmp(fmt + 1, "LOG")) { - datatrak_import(filename, &dive_table); - return 0; - } - - /* OSTCtools */ - if (fmt && (!strcasecmp(fmt + 1, "DIVE"))) { - ostctools_import(filename, &dive_table); - return 0; - } - - ret = parse_file_buffer(filename, &mem); - free(mem.buffer); - return ret; -} - -#define MATCH(buffer, pattern) \ - memcmp(buffer, pattern, strlen(pattern)) - -char *parse_mkvi_value(const char *haystack, const char *needle) -{ - char *lineptr, *valueptr, *endptr, *ret = NULL; - - if ((lineptr = strstr(haystack, needle)) != NULL) { - if ((valueptr = strstr(lineptr, ": ")) != NULL) { - valueptr += 2; - } - if ((endptr = strstr(lineptr, "\n")) != NULL) { - char terminator = '\n'; - if (*(endptr - 1) == '\r') { - --endptr; - terminator = '\r'; - } - *endptr = 0; - ret = copy_string(valueptr); - *endptr = terminator; - - } - } - return ret; -} - -char *next_mkvi_key(const char *haystack) -{ - char *valueptr, *endptr, *ret = NULL; - - if ((valueptr = strstr(haystack, "\n")) != NULL) { - valueptr += 1; - if ((endptr = strstr(valueptr, ": ")) != NULL) { - *endptr = 0; - ret = strdup(valueptr); - *endptr = ':'; - } - } - return ret; -} - -int parse_txt_file(const char *filename, const char *csv) -{ - struct memblock memtxt, memcsv; - - if (readfile(filename, &memtxt) < 0) { - return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); - } - - /* - * MkVI stores some information in .txt file but the whole profile and events are stored in .csv file. First - * make sure the input .txt looks like proper MkVI file, then start parsing the .csv. - */ - if (MATCH(memtxt.buffer, "MkVI_Config") == 0) { - int d, m, y, he; - int hh = 0, mm = 0, ss = 0; - int prev_depth = 0, cur_sampletime = 0, prev_setpoint = -1, prev_ndl = -1; - bool has_depth = false, has_setpoint = false, has_ndl = false; - char *lineptr, *key, *value; - int o2cylinder_pressure = 0, cylinder_pressure = 0, cur_cylinder_index = 0; - unsigned int prev_time = 0; - - struct dive *dive; - struct divecomputer *dc; - struct tm cur_tm; - - value = parse_mkvi_value(memtxt.buffer, "Dive started at"); - if (sscanf(value, "%d-%d-%d %d:%d:%d", &y, &m, &d, &hh, &mm, &ss) != 6) { - free(value); - return -1; - } - free(value); - cur_tm.tm_year = y; - cur_tm.tm_mon = m - 1; - cur_tm.tm_mday = d; - cur_tm.tm_hour = hh; - cur_tm.tm_min = mm; - cur_tm.tm_sec = ss; - - dive = alloc_dive(); - dive->when = utc_mktime(&cur_tm);; - dive->dc.model = strdup("Poseidon MkVI Discovery"); - value = parse_mkvi_value(memtxt.buffer, "Rig Serial number"); - dive->dc.deviceid = atoi(value); - free(value); - dive->dc.divemode = CCR; - dive->dc.no_o2sensors = 2; - - dive->cylinder[cur_cylinder_index].cylinder_use = OXYGEN; - dive->cylinder[cur_cylinder_index].type.size.mliter = 3000; - dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000; - dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6"); - dive->cylinder[cur_cylinder_index].gasmix.o2.permille = 1000; - cur_cylinder_index++; - - dive->cylinder[cur_cylinder_index].cylinder_use = DILUENT; - dive->cylinder[cur_cylinder_index].type.size.mliter = 3000; - dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000; - dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6"); - value = parse_mkvi_value(memtxt.buffer, "Helium percentage"); - he = atoi(value); - free(value); - value = parse_mkvi_value(memtxt.buffer, "Nitrogen percentage"); - dive->cylinder[cur_cylinder_index].gasmix.o2.permille = (100 - atoi(value) - he) * 10; - free(value); - dive->cylinder[cur_cylinder_index].gasmix.he.permille = he * 10; - cur_cylinder_index++; - - lineptr = strstr(memtxt.buffer, "Dive started at"); - while (lineptr && *lineptr && (lineptr = strchr(lineptr, '\n')) && ++lineptr) { - key = next_mkvi_key(lineptr); - if (!key) - break; - value = parse_mkvi_value(lineptr, key); - if (!value) { - free(key); - break; - } - add_extra_data(&dive->dc, key, value); - free(key); - free(value); - } - dc = &dive->dc; - - /* - * Read samples from the CSV file. A sample contains all the lines with same timestamp. The CSV file has - * the following format: - * - * timestamp, type, value - * - * And following fields are of interest to us: - * - * 6 sensor1 - * 7 sensor2 - * 8 depth - * 13 o2 tank pressure - * 14 diluent tank pressure - * 20 o2 setpoint - * 39 water temp - */ - - if (readfile(csv, &memcsv) < 0) { - free(dive); - return report_error(translate("gettextFromC", "Poseidon import failed: unable to read '%s'"), csv); - } - lineptr = memcsv.buffer; - for (;;) { - struct sample *sample; - int type; - int value; - int sampletime; - int gaschange = 0; - - /* Collect all the information for one sample */ - sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value); - - has_depth = false; - has_setpoint = false; - has_ndl = false; - sample = prepare_sample(dc); - - /* - * There was a bug in MKVI download tool that resulted in erroneous sample - * times. This fix should work similarly as the vendor's own. - */ - - sample->time.seconds = cur_sampletime < 0xFFFF * 3 / 4 ? cur_sampletime : prev_time; - prev_time = sample->time.seconds; - - do { - int i = sscanf(lineptr, "%d,%d,%d", &sampletime, &type, &value); - switch (i) { - case 3: - switch (type) { - case 0: - //Mouth piece position event: 0=OC, 1=CC, 2=UN, 3=NC - switch (value) { - case 0: - add_event(dc, cur_sampletime, 0, 0, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position OC")); - break; - case 1: - add_event(dc, cur_sampletime, 0, 0, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position CC")); - break; - case 2: - add_event(dc, cur_sampletime, 0, 0, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position unknown")); - break; - case 3: - add_event(dc, cur_sampletime, 0, 0, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position not connected")); - break; - } - break; - case 3: - //Power Off event - add_event(dc, cur_sampletime, 0, 0, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Power off")); - break; - case 4: - //Battery State of Charge in % -#ifdef SAMPLE_EVENT_BATTERY - add_event(dc, cur_sampletime, SAMPLE_EVENT_BATTERY, 0, - value, QT_TRANSLATE_NOOP("gettextFromC", "battery")); -#endif - break; - case 6: - //PO2 Cell 1 Average - add_sample_data(sample, POSEIDON_SENSOR1, value); - break; - case 7: - //PO2 Cell 2 Average - add_sample_data(sample, POSEIDON_SENSOR2, value); - break; - case 8: - //Depth * 2 - has_depth = true; - prev_depth = value; - add_sample_data(sample, POSEIDON_DEPTH, value); - break; - //9 Max Depth * 2 - //10 Ascent/Descent Rate * 2 - case 11: - //Ascent Rate Alert >10 m/s - add_event(dc, cur_sampletime, SAMPLE_EVENT_ASCENT, 0, 0, - QT_TRANSLATE_NOOP("gettextFromC", "ascent")); - break; - case 13: - //O2 Tank Pressure - add_sample_data(sample, POSEIDON_O2CYLINDER, value); - if (!o2cylinder_pressure) { - dive->cylinder[0].sample_start.mbar = value * 1000; - o2cylinder_pressure = value; - } else - o2cylinder_pressure = value; - break; - case 14: - //Diluent Tank Pressure - add_sample_data(sample, POSEIDON_PRESSURE, value); - if (!cylinder_pressure) { - dive->cylinder[1].sample_start.mbar = value * 1000; - cylinder_pressure = value; - } else - cylinder_pressure = value; - break; - //16 Remaining dive time #1? - //17 related to O2 injection - case 20: - //PO2 Setpoint - has_setpoint = true; - prev_setpoint = value; - add_sample_data(sample, POSEIDON_SETPOINT, value); - break; - case 22: - //End of O2 calibration Event: 0 = OK, 2 = Failed, rest of dive setpoint 1.0 - if (value == 2) - add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration failed")); - add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_END, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration")); - break; - case 25: - //25 Max Ascent depth - add_sample_data(sample, POSEIDON_CEILING, value); - break; - case 31: - //Start of O2 calibration Event - add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_BEGIN, 0, - QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration")); - break; - case 37: - //Remaining dive time #2? - has_ndl = true; - prev_ndl = value; - add_sample_data(sample, POSEIDON_NDL, value); - break; - case 39: - // Water Temperature in Celcius - add_sample_data(sample, POSEIDON_TEMP, value); - break; - case 85: - //He diluent part in % - gaschange += value << 16; - break; - case 86: - //O2 diluent part in % - gaschange += value; - break; - //239 Unknown, maybe PO2 at sensor validation? - //240 Unknown, maybe PO2 at sensor validation? - //247 Unknown, maybe PO2 Cell 1 during pressure test - //248 Unknown, maybe PO2 Cell 2 during pressure test - //250 PO2 Cell 1 - //251 PO2 Cell 2 - default: - break; - } /* sample types */ - break; - case EOF: - break; - default: - printf("Unable to parse input: %s\n", lineptr); - break; - } - - lineptr = strchr(lineptr, '\n'); - if (!lineptr || !*lineptr) - break; - lineptr++; - - /* Grabbing next sample time */ - sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value); - } while (sampletime == cur_sampletime); - - if (gaschange) - add_event(dc, cur_sampletime, SAMPLE_EVENT_GASCHANGE2, 0, gaschange, - QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); - if (!has_depth) - add_sample_data(sample, POSEIDON_DEPTH, prev_depth); - if (!has_setpoint && prev_setpoint >= 0) - add_sample_data(sample, POSEIDON_SETPOINT, prev_setpoint); - if (!has_ndl && prev_ndl >= 0) - add_sample_data(sample, POSEIDON_NDL, prev_ndl); - if (cylinder_pressure) - dive->cylinder[1].sample_end.mbar = cylinder_pressure * 1000; - if (o2cylinder_pressure) - dive->cylinder[0].sample_end.mbar = o2cylinder_pressure * 1000; - finish_sample(dc); - - if (!lineptr || !*lineptr) - break; - } - record_dive(dive); - return 1; - } else { - return report_error(translate("gettextFromC", "No matching DC found for file '%s'"), csv); - } - - return 0; -} - -#define MAXCOLDIGITS 10 -#define DATESTR 9 -#define TIMESTR 6 - -int parse_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate) -{ - int ret, i; - struct memblock mem; - time_t now; - struct tm *timep = NULL; - char tmpbuf[MAXCOLDIGITS]; - - /* Increase the limits for recursion and variables on XSLT - * parsing */ - xsltMaxDepth = 30000; -#if LIBXSLT_VERSION > 10126 - xsltMaxVars = 150000; -#endif - - if (filename == NULL) - return report_error("No CSV filename"); - - time(&now); - timep = localtime(&now); - - strftime(tmpbuf, MAXCOLDIGITS, "%Y%m%d", timep); - params[pnr++] = "date"; - params[pnr++] = strdup(tmpbuf); - - /* As the parameter is numeric, we need to ensure that the leading zero - * is not discarded during the transform, thus prepend time with 1 */ - - strftime(tmpbuf, MAXCOLDIGITS, "1%H%M", timep); - params[pnr++] = "time"; - params[pnr++] = strdup(tmpbuf); - params[pnr++] = NULL; - - mem.size = 0; - if (try_to_xslt_open_csv(filename, &mem, csvtemplate)) - return -1; - - /* - * Lets print command line for manual testing with xsltproc if - * verbosity level is high enough. The printed line needs the - * input file added as last parameter. - */ - - if (verbose >= 2) { - fprintf(stderr, "(echo ''; cat %s;echo '') | xsltproc ", filename); - for (i=0; params[i]; i+=2) - fprintf(stderr, "--stringparam %s %s ", params[i], params[i+1]); - fprintf(stderr, "%s/xslt/csv2xml.xslt -\n", SUBSURFACE_SOURCE); - } - - ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); - - free(mem.buffer); - for (i = 0; params[i]; i += 2) - free(params[i + 1]); - - return ret; -} - -#define SBPARAMS 40 -int parse_seabear_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate) -{ - int ret, i; - struct memblock mem; - time_t now; - struct tm *timep = NULL; - char *ptr, *ptr_old = NULL; - char *NL = NULL; - char tmpbuf[MAXCOLDIGITS]; - - /* Increase the limits for recursion and variables on XSLT - * parsing */ - xsltMaxDepth = 30000; -#if LIBXSLT_VERSION > 10126 - xsltMaxVars = 150000; -#endif - - time(&now); - timep = localtime(&now); - - strftime(tmpbuf, MAXCOLDIGITS, "%Y%m%d", timep); - params[pnr++] = "date"; - params[pnr++] = strdup(tmpbuf); - - /* As the parameter is numeric, we need to ensure that the leading zero - * is not discarded during the transform, thus prepend time with 1 */ - strftime(tmpbuf, MAXCOLDIGITS, "1%H%M", timep); - params[pnr++] = "time"; - params[pnr++] = strdup(tmpbuf); - - - if (filename == NULL) - return report_error("No CSV filename"); - - if (readfile(filename, &mem) < 0) - return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); - - /* Determine NL (new line) character and the start of CSV data */ - ptr = mem.buffer; - while ((ptr = strstr(ptr, "\r\n\r\n")) != NULL) { - ptr_old = ptr; - ptr += 1; - NL = "\r\n"; - } - - if (!ptr_old) { - ptr = mem.buffer; - while ((ptr = strstr(ptr, "\n\n")) != NULL) { - ptr_old = ptr; - ptr += 1; - NL = "\n"; - } - ptr_old += 2; - } else - ptr_old += 4; - - /* - * If file does not contain empty lines, it is not a valid - * Seabear CSV file. - */ - if (NL == NULL) - return -1; - - /* - * On my current sample of Seabear DC log file, the date is - * without any identifier. Thus we must search for the previous - * line and step through from there. That is the line after - * Serial number. - */ - ptr = strstr(mem.buffer, "Serial number:"); - if (ptr) - ptr = strstr(ptr, NL); - - /* - * Write date and time values to params array, if available in - * the CSV header - */ - - if (ptr) { - ptr += strlen(NL) + 2; - /* - * pnr is the index of NULL on the params as filled by - * the init function. The two last entries should be - * date and time. Here we overwrite them with the data - * from the CSV header. - */ - - memcpy(params[pnr - 3], ptr, 4); - memcpy(params[pnr - 3] + 4, ptr + 5, 2); - memcpy(params[pnr - 3] + 6, ptr + 8, 2); - params[pnr - 3][8] = 0; - - memcpy(params[pnr - 1] + 1, ptr + 11, 2); - memcpy(params[pnr - 1] + 3, ptr + 14, 2); - params[pnr - 1][5] = 0; - } - - params[pnr++] = NULL; - - /* Move the CSV data to the start of mem buffer */ - memmove(mem.buffer, ptr_old, mem.size - (ptr_old - (char*)mem.buffer)); - mem.size = (int)mem.size - (ptr_old - (char*)mem.buffer); - - if (try_to_xslt_open_csv(filename, &mem, csvtemplate)) - return -1; - - /* - * Lets print command line for manual testing with xsltproc if - * verbosity level is high enough. The printed line needs the - * input file added as last parameter. - */ - - if (verbose >= 2) { - fprintf(stderr, "xsltproc "); - for (i=0; params[i]; i+=2) - fprintf(stderr, "--stringparam %s %s ", params[i], params[i+1]); - fprintf(stderr, "xslt/csv2xml.xslt\n"); - } - - ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); - free(mem.buffer); - for (i = 0; params[i]; i += 2) - free(params[i + 1]); - - return ret; -} - -int parse_manual_file(const char *filename, char **params, int pnr) -{ - struct memblock mem; - time_t now; - struct tm *timep; - char curdate[9]; - char curtime[6]; - int ret, i; - - - time(&now); - timep = localtime(&now); - strftime(curdate, DATESTR, "%Y%m%d", timep); - - /* As the parameter is numeric, we need to ensure that the leading zero - * is not discarded during the transform, thus prepend time with 1 */ - strftime(curtime, TIMESTR, "1%H%M", timep); - - - params[pnr++] = strdup("date"); - params[pnr++] = strdup(curdate); - params[pnr++] = strdup("time"); - params[pnr++] = strdup(curtime); - params[pnr++] = NULL; - - if (filename == NULL) - return report_error("No manual CSV filename"); - - mem.size = 0; - if (try_to_xslt_open_csv(filename, &mem, "manualCSV")) - return -1; - - ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); - - free(mem.buffer); - for (i = 0; i < pnr - 2; ++i) - free(params[i]); - return ret; -} diff --git a/file.h b/file.h deleted file mode 100644 index 855109960..000000000 --- a/file.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef FILE_H -#define FILE_H - -struct memblock { - void *buffer; - size_t size; -}; - -extern int try_to_open_cochran(const char *filename, struct memblock *mem); -extern int try_to_open_liquivision(const char *filename, struct memblock *mem); -extern void datatrak_import(const char *file, struct dive_table *table); -extern void ostctools_import(const char *file, struct dive_table *table); - -#ifdef __cplusplus -extern "C" { -#endif -extern int readfile(const char *filename, struct memblock *mem); -extern timestamp_t parse_date(const char *date); -extern int try_to_open_zip(const char *filename, struct memblock *mem); -#ifdef __cplusplus -} -#endif - -#endif // FILE_H diff --git a/gaspressures.c b/gaspressures.c deleted file mode 100644 index 5f46d6080..000000000 --- a/gaspressures.c +++ /dev/null @@ -1,435 +0,0 @@ -/* gaspressures.c - * --------------- - * This file contains the routines to calculate the gas pressures in the cylinders. - * The functions below support the code in profile.c. - * The high-level function is populate_pressure_information(), called by function - * create_plot_info_new() in profile.c. The other functions below are, in turn, - * called by populate_pressure_information(). The calling sequence is as follows: - * - * populate_pressure_information() -> calc_pressure_time() - * -> fill_missing_tank_pressures() -> fill_missing_segment_pressures() - * -> get_pr_interpolate_data() - * - * The pr_track_t related functions below implement a linked list that is used by - * the majority of the functions below. The linked list covers a part of the dive profile - * for which there are no cylinder pressure data. Each element in the linked list - * represents a segment between two consecutive points on the dive profile. - * pr_track_t is defined in gaspressures.h - */ - -#include "dive.h" -#include "display.h" -#include "profile.h" -#include "gaspressures.h" - -static pr_track_t *pr_track_alloc(int start, int t_start) -{ - pr_track_t *pt = malloc(sizeof(pr_track_t)); - pt->start = start; - pt->end = 0; - pt->t_start = pt->t_end = t_start; - pt->pressure_time = 0; - pt->next = NULL; - return pt; -} - -/* poor man's linked list */ -static pr_track_t *list_last(pr_track_t *list) -{ - pr_track_t *tail = list; - if (!tail) - return NULL; - while (tail->next) { - tail = tail->next; - } - return tail; -} - -static pr_track_t *list_add(pr_track_t *list, pr_track_t *element) -{ - pr_track_t *tail = list_last(list); - if (!tail) - return element; - tail->next = element; - return list; -} - -static void list_free(pr_track_t *list) -{ - if (!list) - return; - list_free(list->next); - free(list); -} - -#ifdef DEBUG_PR_TRACK -static void dump_pr_track(pr_track_t **track_pr) -{ - int cyl; - pr_track_t *list; - - for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { - list = track_pr[cyl]; - while (list) { - printf("cyl%d: start %d end %d t_start %d t_end %d pt %d\n", cyl, - list->start, list->end, list->t_start, list->t_end, list->pressure_time); - list = list->next; - } - } -} -#endif - -/* - * This looks at the pressures for one cylinder, and - * calculates any missing beginning/end pressures for - * each segment by taking the over-all SAC-rate into - * account for that cylinder. - * - * NOTE! Many segments have full pressure information - * (both beginning and ending pressure). But if we have - * switched away from a cylinder, we will have the - * beginning pressure for the first segment with a - * missing end pressure. We may then have one or more - * segments without beginning or end pressures, until - * we finally have a segment with an end pressure. - * - * We want to spread out the pressure over these missing - * segments according to how big of a time_pressure area - * they have. - */ -static void fill_missing_segment_pressures(pr_track_t *list, enum interpolation_strategy strategy) -{ - double magic; - - while (list) { - int start = list->start, end; - pr_track_t *tmp = list; - int pt_sum = 0, pt = 0; - - for (;;) { - pt_sum += tmp->pressure_time; - end = tmp->end; - if (end) - break; - end = start; - if (!tmp->next) - break; - tmp = tmp->next; - } - - if (!start) - start = end; - - /* - * Now 'start' and 'end' contain the pressure values - * for the set of segments described by 'list'..'tmp'. - * pt_sum is the sum of all the pressure-times of the - * segments. - * - * Now dole out the pressures relative to pressure-time. - */ - list->start = start; - tmp->end = end; - switch (strategy) { - case SAC: - for (;;) { - int pressure; - pt += list->pressure_time; - pressure = start; - if (pt_sum) - pressure -= (start - end) * (double)pt / pt_sum; - list->end = pressure; - if (list == tmp) - break; - list = list->next; - list->start = pressure; - } - break; - case TIME: - if (list->t_end && (tmp->t_start - tmp->t_end)) { - magic = (list->t_start - tmp->t_end) / (tmp->t_start - tmp->t_end); - list->end = rint(start - (start - end) * magic); - } else { - list->end = start; - } - break; - case CONSTANT: - list->end = start; - } - - /* Ok, we've done that set of segments */ - list = list->next; - } -} - -#ifdef DEBUG_PR_INTERPOLATE -void dump_pr_interpolate(int i, pr_interpolate_t interpolate_pr) -{ - printf("Interpolate for entry %d: start %d - end %d - pt %d - acc_pt %d\n", i, - interpolate_pr.start, interpolate_pr.end, interpolate_pr.pressure_time, interpolate_pr.acc_pressure_time); -} -#endif - - -static struct pr_interpolate_struct get_pr_interpolate_data(pr_track_t *segment, struct plot_info *pi, int cur, int pressure) -{ // cur = index to pi->entry corresponding to t_end of segment; - struct pr_interpolate_struct interpolate; - int i; - struct plot_data *entry; - - interpolate.start = segment->start; - interpolate.end = segment->end; - interpolate.acc_pressure_time = 0; - interpolate.pressure_time = 0; - - for (i = 0; i < pi->nr; i++) { - entry = pi->entry + i; - - if (entry->sec < segment->t_start) - continue; - if (entry->sec >= segment->t_end) { - interpolate.pressure_time += entry->pressure_time; - break; - } - if (entry->sec == segment->t_start) { - interpolate.acc_pressure_time = 0; - interpolate.pressure_time = 0; - if (pressure) - interpolate.start = pressure; - continue; - } - if (i < cur) { - if (pressure) { - interpolate.start = pressure; - interpolate.acc_pressure_time = 0; - interpolate.pressure_time = 0; - } else { - interpolate.acc_pressure_time += entry->pressure_time; - interpolate.pressure_time += entry->pressure_time; - } - continue; - } - if (i == cur) { - interpolate.acc_pressure_time += entry->pressure_time; - interpolate.pressure_time += entry->pressure_time; - continue; - } - interpolate.pressure_time += entry->pressure_time; - if (pressure) { - interpolate.end = pressure; - break; - } - } - return interpolate; -} - -static void fill_missing_tank_pressures(struct dive *dive, struct plot_info *pi, pr_track_t **track_pr, bool o2_flag) -{ - int cyl, i; - struct plot_data *entry; - int cur_pr[MAX_CYLINDERS]; // cur_pr[MAX_CYLINDERS] is the CCR diluent cylinder - - for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { - enum interpolation_strategy strategy; - if (!track_pr[cyl]) { - /* no segment where this cylinder is used */ - cur_pr[cyl] = -1; - continue; - } - if (dive->cylinder[cyl].cylinder_use == OC_GAS) - strategy = SAC; - else - strategy = TIME; - fill_missing_segment_pressures(track_pr[cyl], strategy); // Interpolate the missing tank pressure values .. - cur_pr[cyl] = track_pr[cyl]->start; // in the pr_track_t lists of structures - } // and keep the starting pressure for each cylinder. - -#ifdef DEBUG_PR_TRACK - /* another great debugging tool */ - dump_pr_track(track_pr); -#endif - - /* Transfer interpolated cylinder pressures from pr_track strucktures to plotdata - * Go down the list of tank pressures in plot_info. Align them with the start & - * end times of each profile segment represented by a pr_track_t structure. Get - * the accumulated pressure_depths from the pr_track_t structures and then - * interpolate the pressure where these do not exist in the plot_info pressure - * variables. Pressure values are transferred from the pr_track_t structures - * to the plot_info structure, allowing us to plot the tank pressure. - * - * The first two pi structures are "fillers", but in case we don't have a sample - * at time 0 we need to process the second of them here, therefore i=1 */ - for (i = 1; i < pi->nr; i++) { // For each point on the profile: - double magic; - pr_track_t *segment; - pr_interpolate_t interpolate; - int pressure; - int *save_pressure, *save_interpolated; - - entry = pi->entry + i; - - if (o2_flag) { - // Find the cylinder index (cyl) and pressure - cyl = dive->oxygen_cylinder_index; - if (cyl < 0) - return; // Can we do this?!? - pressure = O2CYLINDER_PRESSURE(entry); - save_pressure = &(entry->o2cylinderpressure[SENSOR_PR]); - save_interpolated = &(entry->o2cylinderpressure[INTERPOLATED_PR]); - } else { - pressure = SENSOR_PRESSURE(entry); - save_pressure = &(entry->pressure[SENSOR_PR]); - save_interpolated = &(entry->pressure[INTERPOLATED_PR]); - cyl = entry->cylinderindex; - } - - if (pressure) { // If there is a valid pressure value, - cur_pr[cyl] = pressure; // set current pressure - continue; // and skip to next point. - } - // If there is NO valid pressure value.. - // Find the pressure segment corresponding to this entry.. - segment = track_pr[cyl]; - while (segment && segment->t_end < entry->sec) // Find the track_pr with end time.. - segment = segment->next; // ..that matches the plot_info time (entry->sec) - - if (!segment || !segment->pressure_time) { // No (or empty) segment? - *save_pressure = cur_pr[cyl]; // Just use our current pressure - continue; // and skip to next point. - } - - // If there is a valid segment but no tank pressure .. - interpolate = get_pr_interpolate_data(segment, pi, i, pressure); // Set up an interpolation structure - if(dive->cylinder[cyl].cylinder_use == OC_GAS) { - - /* if this segment has pressure_time, then calculate a new interpolated pressure */ - if (interpolate.pressure_time) { - /* Overall pressure change over total pressure-time for this segment*/ - magic = (interpolate.end - interpolate.start) / (double)interpolate.pressure_time; - - /* Use that overall pressure change to update the current pressure */ - cur_pr[cyl] = rint(interpolate.start + magic * interpolate.acc_pressure_time); - } - } else { - magic = (interpolate.end - interpolate.start) / (segment->t_end - segment->t_start); - cur_pr[cyl] = rint(segment->start + magic * (entry->sec - segment->t_start)); - } - *save_interpolated = cur_pr[cyl]; // and store the interpolated data in plot_info - } -} - - -/* - * What's the pressure-time between two plot data entries? - * We're calculating the integral of pressure over time by - * adding these up. - * - * The units won't matter as long as everybody agrees about - * them, since they'll cancel out - we use this to calculate - * a constant SAC-rate-equivalent, but we only use it to - * scale pressures, so it ends up being a unitless scaling - * factor. - */ -static inline int calc_pressure_time(struct dive *dive, struct divecomputer *dc, struct plot_data *a, struct plot_data *b) -{ - int time = b->sec - a->sec; - int depth = (a->depth + b->depth) / 2; - - if (depth <= SURFACE_THRESHOLD) - return 0; - - return depth_to_mbar(depth, dive) * time; -} - -#ifdef PRINT_PRESSURES_DEBUG -// A CCR debugging tool that prints the gas pressures in cylinder 0 and in the diluent cylinder, used in populate_pressure_information(): -static void debug_print_pressures(struct plot_info *pi) -{ - int i; - for (i = 0; i < pi->nr; i++) { - struct plot_data *entry = pi->entry + i; - printf("%5d |%9d | %9d || %9d | %9d |\n", i, SENSOR_PRESSURE(entry), INTERPOLATED_PRESSURE(entry), DILUENT_PRESSURE(entry), INTERPOLATED_DILUENT_PRESSURE(entry)); - } -} -#endif - -/* This function goes through the list of tank pressures, either SENSOR_PRESSURE(entry) or O2CYLINDER_PRESSURE(entry), - * of structure plot_info for the dive profile where each item in the list corresponds to one point (node) of the - * profile. It finds values for which there are no tank pressures (pressure==0). For each missing item (node) of - * tank pressure it creates a pr_track_alloc structure that represents a segment on the dive profile and that - * contains tank pressures. There is a linked list of pr_track_alloc structures for each cylinder. These pr_track_alloc - * structures ultimately allow for filling the missing tank pressure values on the dive profile using the depth_pressure - * of the dive. To do this, it calculates the summed pressure-time value for the duration of the dive and stores these - * in the pr_track_alloc structures. If diluent_flag = 1, then DILUENT_PRESSURE(entry) is used instead of SENSOR_PRESSURE. - * This function is called by create_plot_info_new() in profile.c - */ -void populate_pressure_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, int o2_flag) -{ - int i, cylinderid, cylinderindex = -1; - pr_track_t *track_pr[MAX_CYLINDERS] = { NULL, }; - pr_track_t *current = NULL; - bool missing_pr = false; - - for (i = 0; i < pi->nr; i++) { - struct plot_data *entry = pi->entry + i; - unsigned pressure; - if (o2_flag) { // if this is a diluent cylinder: - pressure = O2CYLINDER_PRESSURE(entry); - cylinderid = dive->oxygen_cylinder_index; - if (cylinderid < 0) - goto GIVE_UP; - } else { - pressure = SENSOR_PRESSURE(entry); - cylinderid = entry->cylinderindex; - } - /* If track_pr structure already exists, then update it: */ - /* discrete integration of pressure over time to get the SAC rate equivalent */ - if (current) { - entry->pressure_time = calc_pressure_time(dive, dc, entry - 1, entry); - current->pressure_time += entry->pressure_time; - current->t_end = entry->sec; - } - - /* If 1st record or different cylinder: Create a new track_pr structure: */ - /* track the segments per cylinder and their pressure/time integral */ - if (cylinderid != cylinderindex) { - if (o2_flag) // For CCR dives: - cylinderindex = dive->oxygen_cylinder_index; // indicate o2 cylinder - else - cylinderindex = entry->cylinderindex; - current = pr_track_alloc(pressure, entry->sec); - track_pr[cylinderindex] = list_add(track_pr[cylinderindex], current); - continue; - } - - if (!pressure) { - missing_pr = 1; - continue; - } - if (current) - current->end = pressure; - - /* Was it continuous? */ - if ((o2_flag) && (O2CYLINDER_PRESSURE(entry - 1))) // in the case of CCR o2 pressure - continue; - else if (SENSOR_PRESSURE(entry - 1)) // for all other cylinders - continue; - - /* transmitter stopped transmitting cylinder pressure data */ - current = pr_track_alloc(pressure, entry->sec); - if (cylinderindex >= 0) - track_pr[cylinderindex] = list_add(track_pr[cylinderindex], current); - } - - if (missing_pr) { - fill_missing_tank_pressures(dive, pi, track_pr, o2_flag); - } - -#ifdef PRINT_PRESSURES_DEBUG - debug_print_pressures(pi); -#endif - -GIVE_UP: - for (i = 0; i < MAX_CYLINDERS; i++) - list_free(track_pr[i]); -} diff --git a/gaspressures.h b/gaspressures.h deleted file mode 100644 index 420c117a2..000000000 --- a/gaspressures.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef GASPRESSURES_H -#define GASPRESSURES_H - -#ifdef __cplusplus -extern "C" { -#endif - -/* - * simple structure to track the beginning and end tank pressure as - * well as the integral of depth over time spent while we have no - * pressure reading from the tank */ -typedef struct pr_track_struct pr_track_t; -struct pr_track_struct { - int start; - int end; - int t_start; - int t_end; - int pressure_time; - pr_track_t *next; -}; - -typedef struct pr_interpolate_struct pr_interpolate_t; -struct pr_interpolate_struct { - int start; - int end; - int pressure_time; - int acc_pressure_time; -}; - -enum interpolation_strategy {SAC, TIME, CONSTANT}; - -#ifdef __cplusplus -} -#endif -#endif // GASPRESSURES_H diff --git a/gettext.h b/gettext.h deleted file mode 100644 index 43ff023c7..000000000 --- a/gettext.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef MYGETTEXT_H -#define MYGETTEXT_H - -/* this is for the Qt based translations */ -extern const char *trGettext(const char *); -#define translate(_context, arg) trGettext(arg) -#define QT_TRANSLATE_NOOP(_context, arg) arg -#define QT_TRANSLATE_NOOP3(_context, arg, _comment) arg - -#endif // MYGETTEXT_H diff --git a/gettextfromc.cpp b/gettextfromc.cpp deleted file mode 100644 index c579e3c3c..000000000 --- a/gettextfromc.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include - -const char *gettextFromC::trGettext(const char *text) -{ - QByteArray &result = translationCache[QByteArray(text)]; - if (result.isEmpty()) - result = translationCache[QByteArray(text)] = trUtf8(text).toUtf8(); - return result.constData(); -} - -void gettextFromC::reset(void) -{ - translationCache.clear(); -} - -gettextFromC *gettextFromC::instance() -{ - static QScopedPointer self(new gettextFromC()); - return self.data(); -} - -extern "C" const char *trGettext(const char *text) -{ - return gettextFromC::instance()->trGettext(text); -} diff --git a/gettextfromc.h b/gettextfromc.h deleted file mode 100644 index 53df18365..000000000 --- a/gettextfromc.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef GETTEXTFROMC_H -#define GETTEXTFROMC_H - -#include -#include - -extern "C" const char *trGettext(const char *text); - -class gettextFromC { - Q_DECLARE_TR_FUNCTIONS(gettextFromC) -public: - static gettextFromC *instance(); - const char *trGettext(const char *text); - void reset(void); - QHash translationCache; -}; - -#endif // GETTEXTFROMC_H diff --git a/git-access.c b/git-access.c deleted file mode 100644 index 607789f98..000000000 --- a/git-access.c +++ /dev/null @@ -1,891 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dive.h" -#include "membuffer.h" -#include "strndup.h" -#include "qthelperfromc.h" -#include "git-access.h" -#include "gettext.h" - -/* - * The libgit2 people are incompetent at making libraries. They randomly change - * the interfaces, often just renaming things without any sane way to know which - * version you should check for etc etc. It's a disgrace. - */ -#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR < 22 - #define git_remote_lookup(res, repo, name) git_remote_load(res, repo, name) - #if LIBGIT2_VER_MINOR <= 20 - #define git_remote_fetch(remote, refspecs, signature, reflog) git_remote_fetch(remote) - #else - #define git_remote_fetch(remote, refspecs, signature, reflog) git_remote_fetch(remote, signature, reflog) - #endif -#endif - -#if !USE_LIBGIT23_API && !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR == 22 - #define git_remote_push(remote,refspecs,opts) git_remote_push(remote,refspecs,opts,NULL,NULL) - #define git_reference_set_target(out,ref,id,log_message) git_reference_set_target(out,ref,id,NULL,log_message) - #define git_reset(repo,target,reset_type,checkout_opts) git_reset(repo,target,reset_type,checkout_opts,NULL,NULL) -#endif - -/* - * api break introduced in libgit2 master after 0.22 - let's guess this is the v0.23 API - */ -#if USE_LIBGIT23_API || (!LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR >= 23) - #define git_branch_create(out, repo, branch_name, target, force, signature, log_message) \ - git_branch_create(out, repo, branch_name, target, force) -#endif - -bool is_subsurface_cloud = false; - -int (*update_progress_cb)(int) = NULL; - -void set_git_update_cb(int(*cb)(int)) -{ - update_progress_cb = cb; -} - -static int update_progress(int percent) -{ - static int last_percent = -10; - int ret = 0; - if (update_progress_cb) - ret = (*update_progress_cb)(percent); - if (verbose && percent - 10 >= last_percent) { - last_percent = percent; - fprintf(stderr, "git progress %d%%\n", percent); - } - return ret; -} - -// the checkout_progress_cb doesn't allow canceling of the operation -static void progress_cb(const char *path, size_t completed_steps, size_t total_steps, void *payload) -{ - int percent = 0; - if (total_steps) - percent = 100 * completed_steps / total_steps; - (void)update_progress(percent); -} - -// this randomly assumes that 80% of the time is spent on the objects and 20% on the deltas -// if the user cancels the dialog this is passed back to libgit2 -static int transfer_progress_cb(const git_transfer_progress *stats, void *payload) -{ - int percent = 0; - if (stats->total_objects) - percent = 80 * stats->received_objects / stats->total_objects; - if (stats->total_deltas) - percent += 20 * stats->indexed_deltas / stats->total_deltas; - return update_progress(percent); -} - -static int push_transfer_progress_cb(unsigned int current, unsigned int total, size_t bytes, void *payload) -{ - int percent = 0; - if (total != 0) - percent = 100 * current / total; - return update_progress(percent); -} - -char *get_local_dir(const char *remote, const char *branch) -{ - SHA_CTX ctx; - unsigned char hash[20]; - - // That zero-byte update is so that we don't get hash - // collisions for "repo1 branch" vs "repo 1branch". - SHA1_Init(&ctx); - SHA1_Update(&ctx, remote, strlen(remote)); - SHA1_Update(&ctx, "", 1); - SHA1_Update(&ctx, branch, strlen(branch)); - SHA1_Final(hash, &ctx); - - return format_string("%s/cloudstorage/%02x%02x%02x%02x%02x%02x%02x%02x", - system_default_directory(), - hash[0], hash[1], hash[2], hash[3], - hash[4], hash[5], hash[6], hash[7]); -} - -static char *move_local_cache(const char *remote, const char *branch) -{ - char *old_path = get_local_dir(remote, branch); - return move_away(old_path); -} - -static int check_clean(const char *path, unsigned int status, void *payload) -{ - status &= ~GIT_STATUS_CURRENT | GIT_STATUS_IGNORED; - if (!status) - return 0; - if (is_subsurface_cloud) - report_error(translate("gettextFromC", "Local cache directory %s corrupted - can't sync with Subsurface cloud storage"), path); - else - report_error("WARNING: Git cache directory modified (path %s) status %0x", path, status); - return 1; -} - -/* - * The remote is strictly newer than the local branch. - */ -static int reset_to_remote(git_repository *repo, git_reference *local, const git_oid *new_id) -{ - git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; - opts.progress_cb = &progress_cb; - git_object *target; - - if (verbose) - fprintf(stderr, "git storage: reset to remote\n"); - - // If it's not checked out (bare or not HEAD), just update the reference */ - if (git_repository_is_bare(repo) || git_branch_is_head(local) != 1) { - git_reference *out; - - if (git_reference_set_target(&out, local, new_id, "Update to remote")) - return report_error(translate("gettextFromC", "Could not update local cache to newer remote data")); - - git_reference_free(out); - -#ifdef DEBUG - // Not really an error, just informational - report_error("Updated local branch from remote"); -#endif - return 0; - } - - if (git_object_lookup(&target, repo, new_id, GIT_OBJ_COMMIT)) { - if (is_subsurface_cloud) - return report_error(translate("gettextFromC", "Subsurface cloud storage corrupted")); - else - return report_error("Could not look up remote commit"); - } - opts.checkout_strategy = GIT_CHECKOUT_SAFE; - if (git_reset(repo, target, GIT_RESET_HARD, &opts)) { - if (is_subsurface_cloud) - return report_error(translate("gettextFromC", "Could not update local cache to newer remote data")); - else - return report_error("Local head checkout failed after update"); - } - // Not really an error, just informational -#ifdef DEBUG - report_error("Updated local information from remote"); -#endif - return 0; -} - -#if USE_LIBGIT23_API -int credential_ssh_cb(git_cred **out, - const char *url, - const char *username_from_url, - unsigned int allowed_types, - void *payload) -{ - const char *priv_key = format_string("%s/%s", system_default_directory(), "ssrf_remote.key"); - const char *passphrase = prefs.cloud_storage_password ? strdup(prefs.cloud_storage_password) : strdup(""); - return git_cred_ssh_key_new(out, username_from_url, NULL, priv_key, passphrase); -} - -int credential_https_cb(git_cred **out, - const char *url, - const char *username_from_url, - unsigned int allowed_types, - void *payload) -{ - const char *username = prefs.cloud_storage_email_encoded; - const char *password = prefs.cloud_storage_password ? strdup(prefs.cloud_storage_password) : strdup(""); - return git_cred_userpass_plaintext_new(out, username, password); -} - -#define KNOWN_CERT "\xfd\xb8\xf7\x73\x76\xe2\x75\x53\x93\x37\xdc\xfe\x1e\x55\x43\x3d\xf2\x2c\x18\x2c" -int certificate_check_cb(git_cert *cert, int valid, const char *host, void *payload) -{ - if (same_string(host, "cloud.subsurface-divelog.org") && cert->cert_type == GIT_CERT_X509) { - SHA_CTX ctx; - unsigned char hash[21]; - git_cert_x509 *cert509 = (git_cert_x509 *)cert; - SHA1_Init(&ctx); - SHA1_Update(&ctx, cert509->data, cert509->len); - SHA1_Final(hash, &ctx); - hash[20] = 0; - if (verbose > 1) - if (same_string((char *)hash, KNOWN_CERT)) { - fprintf(stderr, "cloud certificate considered %s, forcing it valid\n", - valid ? "valid" : "not valid"); - return 1; - } - } - return valid; -} - -#endif - -static int update_remote(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, enum remote_transport rt) -{ - git_push_options opts = GIT_PUSH_OPTIONS_INIT; - git_strarray refspec; - const char *name = git_reference_name(local); - - if (verbose) - fprintf(stderr, "git storage: update remote\n"); - - refspec.count = 1; - refspec.strings = (char **)&name; - -#if USE_LIBGIT23_API - opts.callbacks.push_transfer_progress = &push_transfer_progress_cb; - if (rt == RT_SSH) - opts.callbacks.credentials = credential_ssh_cb; - else if (rt == RT_HTTPS) - opts.callbacks.credentials = credential_https_cb; - opts.callbacks.certificate_check = certificate_check_cb; -#endif - if (git_remote_push(origin, &refspec, &opts)) { - if (is_subsurface_cloud) - return report_error(translate("gettextFromC", "Could not update Subsurface cloud storage, try again later")); - else - return report_error("Unable to update remote with current local cache state (%s)", giterr_last()->message); - } - return 0; -} - -extern int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree); - -static int try_to_git_merge(git_repository *repo, git_reference *local, git_reference *remote, git_oid *base, const git_oid *local_id, const git_oid *remote_id) -{ - git_tree *local_tree, *remote_tree, *base_tree; - git_commit *local_commit, *remote_commit, *base_commit; - git_index *merged_index; - git_merge_options merge_options; - - if (verbose) { - char outlocal[41], outremote[41]; - outlocal[40] = outremote[40] = 0; - git_oid_fmt(outlocal, local_id); - git_oid_fmt(outremote, remote_id); - fprintf(stderr, "trying to merge local SHA %s remote SHA %s\n", outlocal, outremote); - } - - git_merge_init_options(&merge_options, GIT_MERGE_OPTIONS_VERSION); -#ifdef USE_LIBGIT23_API - merge_options.tree_flags = GIT_MERGE_TREE_FIND_RENAMES; -#else - merge_options.flags = GIT_MERGE_TREE_FIND_RENAMES; -#endif - merge_options.file_favor = GIT_MERGE_FILE_FAVOR_UNION; - merge_options.rename_threshold = 100; - if (git_commit_lookup(&local_commit, repo, local_id)) { - fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); - goto diverged_error; - } - if (git_commit_tree(&local_tree, local_commit)) { - fprintf(stderr, "Remote storage and local data diverged. Error: failed local tree lookup (%s)", giterr_last()->message); - goto diverged_error; - } - if (git_commit_lookup(&remote_commit, repo, remote_id)) { - fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); - goto diverged_error; - } - if (git_commit_tree(&remote_tree, remote_commit)) { - fprintf(stderr, "Remote storage and local data diverged. Error: failed local tree lookup (%s)", giterr_last()->message); - goto diverged_error; - } - if (git_commit_lookup(&base_commit, repo, base)) { - fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); - goto diverged_error; - } - if (git_commit_tree(&base_tree, base_commit)) { - fprintf(stderr, "Remote storage and local data diverged. Error: failed base tree lookup (%s)", giterr_last()->message); - goto diverged_error; - } - if (git_merge_trees(&merged_index, repo, base_tree, local_tree, remote_tree, &merge_options)) { - fprintf(stderr, "Remote storage and local data diverged. Error: merge failed (%s)", giterr_last()->message); - // this is the one where I want to report more detail to the user - can't quite explain why - return report_error(translate("gettextFromC", "Remote storage and local data diverged. Error: merge failed (%s)"), giterr_last()->message); - } - if (git_index_has_conflicts(merged_index)) { - int error; - const git_index_entry *ancestor = NULL, - *ours = NULL, - *theirs = NULL; - git_index_conflict_iterator *iter = NULL; - error = git_index_conflict_iterator_new(&iter, merged_index); - while (git_index_conflict_next(&ancestor, &ours, &theirs, iter) - != GIT_ITEROVER) { - /* Mark this conflict as resolved */ - fprintf(stderr, "conflict in %s / %s / %s -- ", - ours ? ours->path : "-", - theirs ? theirs->path : "-", - ancestor ? ancestor->path : "-"); - if ((!ours && theirs && ancestor) || - (ours && !theirs && ancestor)) { - // the file was removed on one side or the other - just remove it - fprintf(stderr, "looks like a delete on one side; removing the file from the index\n"); - error = git_index_remove(merged_index, ours ? ours->path : theirs->path, GIT_INDEX_STAGE_ANY); - } else if (ancestor) { - error = git_index_conflict_remove(merged_index, ours ? ours->path : theirs ? theirs->path : ancestor->path); - } - if (error) { - fprintf(stderr, "error at conflict resplution (%s)", giterr_last()->message); - } - } - git_index_conflict_cleanup(merged_index); - git_index_conflict_iterator_free(iter); - report_error(translate("gettextFromC", "Remote storage and local data diverged. Cannot combine local and remote changes")); - } - git_oid merge_oid, commit_oid; - git_tree *merged_tree; - git_signature *author; - git_commit *commit; - - if (git_index_write_tree_to(&merge_oid, merged_index, repo)) - goto write_error; - if (git_tree_lookup(&merged_tree, repo, &merge_oid)) - goto write_error; - if (git_signature_default(&author, repo) < 0) - if (git_signature_now(&author, "Subsurface", "noemail@given") < 0) - goto write_error; - if (git_commit_create_v(&commit_oid, repo, NULL, author, author, NULL, "automatic merge", merged_tree, 2, local_commit, remote_commit)) - goto write_error; - if (git_commit_lookup(&commit, repo, &commit_oid)) - goto write_error; - if (git_branch_is_head(local) && !git_repository_is_bare(repo)) { - git_object *parent; - git_reference_peel(&parent, local, GIT_OBJ_COMMIT); - if (update_git_checkout(repo, parent, merged_tree)) { - goto write_error; - } - } - if (git_reference_set_target(&local, local, &commit_oid, "Subsurface merge event")) - goto write_error; - set_git_id(&commit_oid); - git_signature_free(author); - - return 0; - -diverged_error: - return report_error(translate("gettextFromC", "Remote storage and local data diverged")); - -write_error: - return report_error(translate("gettextFromC", "Remote storage and local data diverged. Error: writing the data failed (%s)"), giterr_last()->message); -} - -// if accessing the local cache of Subsurface cloud storage fails, we simplify things -// for the user and simply move the cache away (in case they want to try and extract data) -// and ask them to retry the operation (which will then refresh the data from the cloud server) -static int cleanup_local_cache(const char *remote_url, const char *branch) -{ - char *backup_path = move_local_cache(remote_url, branch); - report_error(translate("gettextFromC", "Problems with local cache of Subsurface cloud data")); - report_error(translate("gettextFromC", "Moved cache data to %s. Please try the operation again."), backup_path); - free(backup_path); - return -1; -} -static int try_to_update(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, - const char *remote_url, const char *branch, enum remote_transport rt) -{ - git_oid base; - const git_oid *local_id, *remote_id; - - if (verbose) - fprintf(stderr, "git storage: try to update\n"); - - if (!git_reference_cmp(local, remote)) - return 0; - - // Dirty modified state in the working tree? We're not going - // to update either way - if (git_status_foreach(repo, check_clean, NULL)) { - if (is_subsurface_cloud) - goto cloud_data_error; - else - return report_error("local cached copy is dirty, skipping update"); - } - local_id = git_reference_target(local); - remote_id = git_reference_target(remote); - - if (!local_id || !remote_id) { - if (is_subsurface_cloud) - goto cloud_data_error; - else - return report_error("Unable to get local or remote SHA1"); - } - if (git_merge_base(&base, repo, local_id, remote_id)) { - if (is_subsurface_cloud) - goto cloud_data_error; - else - return report_error("Unable to find common commit of local and remote branches"); - } - /* Is the remote strictly newer? Use it */ - if (git_oid_equal(&base, local_id)) - return reset_to_remote(repo, local, remote_id); - - /* Is the local repo the more recent one? See if we can update upstream */ - if (git_oid_equal(&base, remote_id)) - return update_remote(repo, origin, local, remote, rt); - - /* Merging a bare repository always needs user action */ - if (git_repository_is_bare(repo)) { - if (is_subsurface_cloud) - goto cloud_data_error; - else - return report_error("Local and remote have diverged, merge of bare branch needed"); - } - /* Merging will definitely need the head branch too */ - if (git_branch_is_head(local) != 1) { - if (is_subsurface_cloud) - goto cloud_data_error; - else - return report_error("Local and remote do not match, local branch not HEAD - cannot update"); - } - /* Ok, let's try to merge these */ - return try_to_git_merge(repo, local, remote, &base, local_id, remote_id); - -cloud_data_error: - // since we are working with Subsurface cloud storage we want to make the user interaction - // as painless as possible. So if something went wrong with the local cache, tell the user - // about it an move it away - return cleanup_local_cache(remote_url, branch); -} - -static int check_remote_status(git_repository *repo, git_remote *origin, const char *remote, const char *branch, enum remote_transport rt) -{ - int error = 0; - - git_reference *local_ref, *remote_ref; - - if (verbose) - fprintf(stderr, "git storage: check remote status\n"); - - if (git_branch_lookup(&local_ref, repo, branch, GIT_BRANCH_LOCAL)) { - if (is_subsurface_cloud) - return cleanup_local_cache(remote, branch); - else - return report_error("Git cache branch %s no longer exists", branch); - } - if (git_branch_upstream(&remote_ref, local_ref)) { - /* so there is no upstream branch for our branch; that's a problem. - * let's push our branch */ - git_strarray refspec; - git_reference_list(&refspec, repo); -#if USE_LIBGIT23_API - git_push_options opts = GIT_PUSH_OPTIONS_INIT; - opts.callbacks.transfer_progress = &transfer_progress_cb; - if (rt == RT_SSH) - opts.callbacks.credentials = credential_ssh_cb; - else if (rt == RT_HTTPS) - opts.callbacks.credentials = credential_https_cb; - opts.callbacks.certificate_check = certificate_check_cb; - error = git_remote_push(origin, &refspec, &opts); -#else - error = git_remote_push(origin, &refspec, NULL); -#endif - } else { - error = try_to_update(repo, origin, local_ref, remote_ref, remote, branch, rt); - git_reference_free(remote_ref); - } - git_reference_free(local_ref); - return error; -} - -int sync_with_remote(git_repository *repo, const char *remote, const char *branch, enum remote_transport rt) -{ - int error; - git_remote *origin; - char *proxy_string; - git_config *conf; - - if (verbose) - fprintf(stderr, "sync with remote %s[%s]\n", remote, branch); - - git_repository_config(&conf, repo); - if (rt == RT_HTTPS && getProxyString(&proxy_string)) { - if (verbose) - fprintf(stderr, "set proxy to \"%s\"\n", proxy_string); - git_config_set_string(conf, "http.proxy", proxy_string); - free(proxy_string); - } else { - if (verbose) - fprintf(stderr, "delete proxy setting\n"); - git_config_delete_entry(conf, "http.proxy"); - } - - /* - * NOTE! Remote errors are reported, but are nonfatal: - * we still successfully return the local repository. - */ - error = git_remote_lookup(&origin, repo, "origin"); - if (error) { - if (!is_subsurface_cloud) - report_error("Repository '%s' origin lookup failed (%s)", remote, giterr_last()->message); - return 0; - } - - if (rt == RT_HTTPS && !canReachCloudServer()) { - // this is not an error, just a warning message, so return 0 - report_error("Cannot connect to cloud server, working with local copy"); - return 0; - } - if (verbose) - fprintf(stderr, "git storage: fetch remote\n"); -#if USE_LIBGIT23_API - git_fetch_options opts = GIT_FETCH_OPTIONS_INIT; - opts.callbacks.transfer_progress = &transfer_progress_cb; - if (rt == RT_SSH) - opts.callbacks.credentials = credential_ssh_cb; - else if (rt == RT_HTTPS) - opts.callbacks.credentials = credential_https_cb; - opts.callbacks.certificate_check = certificate_check_cb; - error = git_remote_fetch(origin, NULL, &opts, NULL); -#else - error = git_remote_fetch(origin, NULL, NULL, NULL); -#endif - // NOTE! A fetch error is not fatal, we just report it - if (error) { - if (is_subsurface_cloud) - report_error("Cannot sync with cloud server, working with offline copy"); - else - report_error("Unable to fetch remote '%s'", remote); - if (verbose) - fprintf(stderr, "remote fetch failed (%s)\n", giterr_last()->message); - error = 0; - } else { - error = check_remote_status(repo, origin, remote, branch, rt); - } - git_remote_free(origin); - return error; -} - -static git_repository *update_local_repo(const char *localdir, const char *remote, const char *branch, enum remote_transport rt) -{ - int error; - git_repository *repo = NULL; - - if (verbose) - fprintf(stderr, "git storage: update local repo\n"); - - error = git_repository_open(&repo, localdir); - if (error) { - if (is_subsurface_cloud) - (void)cleanup_local_cache(remote, branch); - else - report_error("Unable to open git cache repository at %s: %s", localdir, giterr_last()->message); - return NULL; - } - sync_with_remote(repo, remote, branch, rt); - return repo; -} - -static int repository_create_cb(git_repository **out, const char *path, int bare, void *payload) -{ - char *proxy_string; - git_config *conf; - - int ret = git_repository_init(out, path, bare); - - git_repository_config(&conf, *out); - if (getProxyString(&proxy_string)) { - if (verbose) - fprintf(stderr, "set proxy to \"%s\"\n", proxy_string); - git_config_set_string(conf, "http.proxy", proxy_string); - free(proxy_string); - } else { - if (verbose) - fprintf(stderr, "delete proxy setting\n"); - git_config_delete_entry(conf, "http.proxy"); - } - return ret; -} - -/* this should correctly initialize both the local and remote - * repository for the Subsurface cloud storage */ -static git_repository *create_and_push_remote(const char *localdir, const char *remote, const char *branch) -{ - git_repository *repo; - git_config *conf; - int len; - char *variable_name, *merge_head; - - if (verbose) - fprintf(stderr, "git storage: create and push remote\n"); - - /* first make sure the directory for the local cache exists */ - subsurface_mkdir(localdir); - - /* set up the origin to point to our remote */ - git_repository_init_options init_opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; - init_opts.origin_url = remote; - - /* now initialize the repository with */ - git_repository_init_ext(&repo, localdir, &init_opts); - - /* create a config so we can set the remote tracking branch */ - git_repository_config(&conf, repo); - len = sizeof("branch..remote") + strlen(branch); - variable_name = malloc(len); - snprintf(variable_name, len, "branch.%s.remote", branch); - git_config_set_string(conf, variable_name, "origin"); - /* we know this is shorter than the previous one, so we reuse the variable*/ - snprintf(variable_name, len, "branch.%s.merge", branch); - len = sizeof("refs/heads/") + strlen(branch); - merge_head = malloc(len); - snprintf(merge_head, len, "refs/heads/%s", branch); - git_config_set_string(conf, variable_name, merge_head); - - /* finally create an empty commit and push it to the remote */ - if (do_git_save(repo, branch, remote, false, true)) - return NULL; - return(repo); -} - -static git_repository *create_local_repo(const char *localdir, const char *remote, const char *branch, enum remote_transport rt) -{ - int error; - git_repository *cloned_repo = NULL; - git_clone_options opts = GIT_CLONE_OPTIONS_INIT; - - if (verbose) - fprintf(stderr, "git storage: create_local_repo\n"); - -#if USE_LIBGIT23_API - opts.fetch_opts.callbacks.transfer_progress = &transfer_progress_cb; - if (rt == RT_SSH) - opts.fetch_opts.callbacks.credentials = credential_ssh_cb; - else if (rt == RT_HTTPS) - opts.fetch_opts.callbacks.credentials = credential_https_cb; - opts.repository_cb = repository_create_cb; - opts.fetch_opts.callbacks.certificate_check = certificate_check_cb; -#endif - opts.checkout_branch = branch; - if (rt == RT_HTTPS && !canReachCloudServer()) - return 0; - if (verbose > 1) - fprintf(stderr, "git storage: calling git_clone()\n"); - error = git_clone(&cloned_repo, remote, localdir, &opts); - if (verbose > 1) - fprintf(stderr, "git storage: returned from git_clone() with error %d\n", error); - if (error) { - char *msg = giterr_last()->message; - int len = sizeof("Reference 'refs/remotes/origin/' not found") + strlen(branch); - char *pattern = malloc(len); - snprintf(pattern, len, "Reference 'refs/remotes/origin/%s' not found", branch); - if (strstr(remote, prefs.cloud_git_url) && strstr(msg, pattern)) { - /* we're trying to open the remote branch that corresponds - * to our cloud storage and the branch doesn't exist. - * So we need to create the branch and push it to the remote */ - cloned_repo = create_and_push_remote(localdir, remote, branch); -#if !defined(DEBUG) - } else if (is_subsurface_cloud) { - report_error(translate("gettextFromC", "Error connecting to Subsurface cloud storage")); -#endif - } else { - report_error(translate("gettextFromC", "git clone of %s failed (%s)"), remote, msg); - } - free(pattern); - } - return cloned_repo; -} - -static struct git_repository *get_remote_repo(const char *localdir, const char *remote, const char *branch) -{ - struct stat st; - enum remote_transport rt; - - /* figure out the remote transport */ - if (strncmp(remote, "ssh://", 6) == 0) - rt = RT_SSH; - else if (strncmp(remote, "https://", 8) == 0) - rt = RT_HTTPS; - else - rt = RT_OTHER; - - if (verbose > 1) { - fprintf(stderr, "git storage: accessing %s\n", remote); - } - /* Do we already have a local cache? */ - if (!stat(localdir, &st)) { - if (!S_ISDIR(st.st_mode)) { - if (is_subsurface_cloud) - (void)cleanup_local_cache(remote, branch); - else - report_error("local git cache at '%s' is corrupt"); - return NULL; - } - return update_local_repo(localdir, remote, branch, rt); - } - return create_local_repo(localdir, remote, branch, rt); -} - -/* - * This turns a remote repository into a local one if possible. - * - * The recognized formats are - * git://host/repo[branch] - * ssh://host/repo[branch] - * http://host/repo[branch] - * https://host/repo[branch] - * file://repo[branch] - */ -static struct git_repository *is_remote_git_repository(char *remote, const char *branch) -{ - char c, *localdir; - const char *p = remote; - - while ((c = *p++) >= 'a' && c <= 'z') - /* nothing */; - if (c != ':') - return NULL; - if (*p++ != '/' || *p++ != '/') - return NULL; - - /* Special-case "file://", since it's already local */ - if (!strncmp(remote, "file://", 7)) - remote += 7; - - /* - * Ok, we found "[a-z]*://", we've simplified the - * local repo case (because libgit2 is insanely slow - * for that), and we think we have a real "remote - * git" format. - * - * We now create the SHA1 hash of the whole thing, - * including the branch name. That will be our unique - * unique local repository name. - * - * NOTE! We will create a local repository per branch, - * because - * - * (a) libgit2 remote tracking branch support seems to - * be a bit lacking - * (b) we'll actually check the branch out so that we - * can do merges etc too. - * - * so even if you have a single remote git repo with - * multiple branches for different people, the local - * caches will sadly force that to split into multiple - * individual repositories. - */ - - /* - * next we need to make sure that any encoded username - * has been extracted from an https:// based URL - */ - if (!strncmp(remote, "https://", 8)) { - char *at = strchr(remote, '@'); - if (at) { - /* was this the @ that denotes an account? that means it was before the - * first '/' after the https:// - so let's find a '/' after that and compare */ - char *slash = strchr(remote + 8, '/'); - if (slash && slash > at) { - /* grab the part between "https://" and "@" as encoded email address - * (that's our username) and move the rest of the URL forward, remembering - * to copy the closing NUL as well */ - prefs.cloud_storage_email_encoded = strndup(remote + 8, at - remote - 8); - memmove(remote + 8, at + 1, strlen(at + 1) + 1); - } - } - } - localdir = get_local_dir(remote, branch); - if (!localdir) - return NULL; - - /* remember if the current git storage we are working on is our cloud storage - * this is used to create more user friendly error message and warnings */ - is_subsurface_cloud = strstr(remote, prefs.cloud_git_url) != NULL; - - return get_remote_repo(localdir, remote, branch); -} - -/* - * If it's not a git repo, return NULL. Be very conservative. - */ -struct git_repository *is_git_repository(const char *filename, const char **branchp, const char **remote, bool dry_run) -{ - int flen, blen, ret; - int offset = 1; - struct stat st; - git_repository *repo; - char *loc, *branch; - - flen = strlen(filename); - if (!flen || filename[--flen] != ']') - return NULL; - - /* Find the matching '[' */ - blen = 0; - while (flen && filename[--flen] != '[') - blen++; - - /* Ignore slashes at the end of the repo name */ - while (flen && filename[flen-1] == '/') { - flen--; - offset++; - } - - if (!flen) - return NULL; - - /* - * This is the "point of no return": the name matches - * the git repository name rules, and we will no longer - * return NULL. - * - * We will either return "dummy_git_repository" and the - * branch pointer will have the _whole_ filename in it, - * or we will return a real git repository with the - * branch pointer being filled in with just the branch - * name. - * - * The actual git reading/writing routines can use this - * to generate proper error messages. - */ - *branchp = filename; - loc = format_string("%.*s", flen, filename); - if (!loc) - return dummy_git_repository; - - branch = format_string("%.*s", blen, filename + flen + offset); - if (!branch) { - free(loc); - return dummy_git_repository; - } - - if (dry_run) { - *branchp = branch; - *remote = loc; - return dummy_git_repository; - } - repo = is_remote_git_repository(loc, branch); - if (repo) { - if (remote) - *remote = loc; - else - free(loc); - *branchp = branch; - return repo; - } - - if (stat(loc, &st) < 0 || !S_ISDIR(st.st_mode)) { - free(loc); - free(branch); - return dummy_git_repository; - } - - ret = git_repository_open(&repo, loc); - free(loc); - if (ret < 0) { - free(branch); - return dummy_git_repository; - } - if (remote) - *remote = NULL; - *branchp = branch; - return repo; -} diff --git a/git-access.h b/git-access.h deleted file mode 100644 index a2a9ba3ae..000000000 --- a/git-access.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef GITACCESS_H -#define GITACCESS_H - -#include "git2.h" - -#ifdef __cplusplus -extern "C" { -#else -#include -#endif - -enum remote_transport { RT_OTHER, RT_HTTPS, RT_SSH }; - -struct git_oid; -struct git_repository; -#define dummy_git_repository ((git_repository *)3ul) /* Random bogus pointer, not NULL */ -extern struct git_repository *is_git_repository(const char *filename, const char **branchp, const char **remote, bool dry_run); -extern int sync_with_remote(struct git_repository *repo, const char *remote, const char *branch, enum remote_transport rt); -extern int git_save_dives(struct git_repository *, const char *, const char *remote, bool select_only); -extern int git_load_dives(struct git_repository *, const char *); -extern int do_git_save(git_repository *repo, const char *branch, const char *remote, bool select_only, bool create_empty); -extern const char *saved_git_id; -extern void clear_git_id(void); -extern void set_git_id(const struct git_oid *); -void set_git_update_cb(int(*cb)(int)); -char *get_local_dir(const char *remote, const char *branch); -#ifdef __cplusplus -} -#endif -#endif // GITACCESS_H - diff --git a/helpers.h b/helpers.h deleted file mode 100644 index 5f2f2f2c5..000000000 --- a/helpers.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * helpers.h - * - * header file for random helper functions of Subsurface - * - */ -#ifndef HELPERS_H -#define HELPERS_H - -#include -#include "dive.h" -#include "qthelper.h" - -QString get_depth_string(depth_t depth, bool showunit = false, bool showdecimal = true); -QString get_depth_string(int mm, bool showunit = false, bool showdecimal = true); -QString get_depth_unit(); -QString get_weight_string(weight_t weight, bool showunit = false); -QString get_weight_unit(); -QString get_cylinder_used_gas_string(cylinder_t *cyl, bool showunit = false); -QString get_temperature_string(temperature_t temp, bool showunit = false); -QString get_temp_unit(); -QString get_volume_string(volume_t volume, bool showunit = false, int mbar = 0); -QString get_volume_unit(); -QString get_pressure_string(pressure_t pressure, bool showunit = false); -QString get_pressure_unit(); -void set_default_dive_computer(const char *vendor, const char *product); -void set_default_dive_computer_device(const char *name); -void set_default_dive_computer_download_mode(int downloadMode); -QString getSubsurfaceDataPath(QString folderToFind); -QString getPrintingTemplatePathUser(); -QString getPrintingTemplatePathBundle(); -void copyPath(QString src, QString dst); -extern const QString get_dc_nickname(const char *model, uint32_t deviceid); -int gettimezoneoffset(timestamp_t when = 0); -int parseTemperatureToMkelvin(const QString &text); -QString get_dive_duration_string(timestamp_t when, QString hourText, QString minutesText); -QString get_dive_date_string(timestamp_t when); -QString get_short_dive_date_string(timestamp_t when); -bool is_same_day (timestamp_t trip_when, timestamp_t dive_when); -QString get_trip_date_string(timestamp_t when, int nr, bool getday); -QString uiLanguage(QLocale *callerLoc); -QLocale getLocale(); -QString getDateFormat(); -void selectedDivesGasUsed(QVector > &gasUsed); -QString getUserAgent(); - -#if defined __APPLE__ -#define TITLE_OR_TEXT(_t, _m) "", _t + "\n" + _m -#else -#define TITLE_OR_TEXT(_t, _m) _t, _m -#endif -#endif // HELPERS_H diff --git a/libdivecomputer.c b/libdivecomputer.c deleted file mode 100644 index ca8378379..000000000 --- a/libdivecomputer.c +++ /dev/null @@ -1,1083 +0,0 @@ -#include -#include -#include -#include -#include "gettext.h" -#include "dive.h" -#include "device.h" -#include "divelist.h" -#include "display.h" - -#include "libdivecomputer.h" -#include -#include - - -/* Christ. Libdivecomputer has the worst configuration system ever. */ -#ifdef HW_FROG_H -#define NOT_FROG , 0 -#define LIBDIVECOMPUTER_SUPPORTS_FROG -#else -#define NOT_FROG -#endif - -char *dumpfile_name; -char *logfile_name; -const char *progress_bar_text = ""; -double progress_bar_fraction = 0.0; - -static int stoptime, stopdepth, ndl, po2, cns; -static bool in_deco, first_temp_is_air; - -/* - * Directly taken from libdivecomputer's examples/common.c to improve - * the error messages resulting from libdc's return codes - */ -const char *errmsg (dc_status_t rc) -{ - switch (rc) { - case DC_STATUS_SUCCESS: - return "Success"; - case DC_STATUS_UNSUPPORTED: - return "Unsupported operation"; - case DC_STATUS_INVALIDARGS: - return "Invalid arguments"; - case DC_STATUS_NOMEMORY: - return "Out of memory"; - case DC_STATUS_NODEVICE: - return "No device found"; - case DC_STATUS_NOACCESS: - return "Access denied"; - case DC_STATUS_IO: - return "Input/output error"; - case DC_STATUS_TIMEOUT: - return "Timeout"; - case DC_STATUS_PROTOCOL: - return "Protocol error"; - case DC_STATUS_DATAFORMAT: - return "Data format error"; - case DC_STATUS_CANCELLED: - return "Cancelled"; - default: - return "Unknown error"; - } -} - -static dc_status_t create_parser(device_data_t *devdata, dc_parser_t **parser) -{ - return dc_parser_new(parser, devdata->device); -} - -static int parse_gasmixes(device_data_t *devdata, struct dive *dive, dc_parser_t *parser, int ngases) -{ - static bool shown_warning = false; - int i, rc; - -#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) - int ntanks = 0; - rc = dc_parser_get_field(parser, DC_FIELD_TANK_COUNT, 0, &ntanks); - if (rc == DC_STATUS_SUCCESS) { - if (ntanks != ngases) { - shown_warning = true; - report_error("different number of gases (%d) and tanks (%d)", ngases, ntanks); - } - } - dc_tank_t tank = { 0 }; -#endif - - for (i = 0; i < ngases; i++) { - dc_gasmix_t gasmix = { 0 }; - int o2, he; - bool no_volume = true; - - rc = dc_parser_get_field(parser, DC_FIELD_GASMIX, i, &gasmix); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) - return rc; - - if (i >= MAX_CYLINDERS) - continue; - - o2 = rint(gasmix.oxygen * 1000); - he = rint(gasmix.helium * 1000); - - /* Ignore bogus data - libdivecomputer does some crazy stuff */ - if (o2 + he <= O2_IN_AIR || o2 > 1000) { - if (!shown_warning) { - shown_warning = true; - report_error("unlikely dive gas data from libdivecomputer: o2 = %d he = %d", o2, he); - } - o2 = 0; - } - if (he < 0 || o2 + he > 1000) { - if (!shown_warning) { - shown_warning = true; - report_error("unlikely dive gas data from libdivecomputer: o2 = %d he = %d", o2, he); - } - he = 0; - } - dive->cylinder[i].gasmix.o2.permille = o2; - dive->cylinder[i].gasmix.he.permille = he; - -#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) - tank.volume = 0.0; - if (i < ntanks) { - rc = dc_parser_get_field(parser, DC_FIELD_TANK, i, &tank); - if (rc == DC_STATUS_SUCCESS) { - if (tank.type == DC_TANKVOLUME_IMPERIAL) { - dive->cylinder[i].type.size.mliter = rint(tank.volume * 1000); - dive->cylinder[i].type.workingpressure.mbar = rint(tank.workpressure * 1000); - if (same_string(devdata->model, "Suunto EON Steel")) { - /* Suunto EON Steele gets this wrong. Badly. - * but on the plus side it only supports a few imperial sizes, - * so let's try and guess at least the most common ones. - * First, the pressures are off by a constant factor. WTF? - * Then we can round the wet sizes so we get to multiples of 10 - * for cuft sizes (as that's all that you can enter) */ - dive->cylinder[i].type.workingpressure.mbar *= 206.843 / 206.7; - char name_buffer[9]; - int rounded_size = ml_to_cuft(gas_volume(&dive->cylinder[i], - dive->cylinder[i].type.workingpressure)); - rounded_size = (int)((rounded_size + 5) / 10) * 10; - switch (dive->cylinder[i].type.workingpressure.mbar) { - case 206843: - snprintf(name_buffer, 9, "AL%d", rounded_size); - break; - case 234422: /* this is wrong - HP tanks tend to be 3440, but Suunto only allows 3400 */ - snprintf(name_buffer, 9, "HP%d", rounded_size); - break; - case 179263: - snprintf(name_buffer, 9, "LP+%d", rounded_size); - break; - case 165474: - snprintf(name_buffer, 9, "LP%d", rounded_size); - break; - default: - snprintf(name_buffer, 9, "%d cuft", rounded_size); - break; - } - dive->cylinder[i].type.description = copy_string(name_buffer); - dive->cylinder[i].type.size.mliter = cuft_to_l(rounded_size) * 1000 / - mbar_to_atm(dive->cylinder[i].type.workingpressure.mbar); - } - } else if (tank.type == DC_TANKVOLUME_METRIC) { - dive->cylinder[i].type.size.mliter = rint(tank.volume * 1000); - } - if (tank.gasmix != i) { // we don't handle this, yet - shown_warning = true; - report_error("gasmix %d for tank %d doesn't match", tank.gasmix, i); - } - } - } - if (!IS_FP_SAME(tank.volume, 0.0)) - no_volume = false; - - // this new API also gives us the beginning and end pressure for the tank - if (!IS_FP_SAME(tank.beginpressure, 0.0) && !IS_FP_SAME(tank.endpressure, 0.0)) { - dive->cylinder[i].start.mbar = tank.beginpressure * 1000; - dive->cylinder[i].end.mbar = tank.endpressure * 1000; - } -#endif - if (no_volume) { - /* for the first tank, if there is no tanksize available from the - * dive computer, fill in the default tank information (if set) */ - fill_default_cylinder(&dive->cylinder[i]); - } - /* whatever happens, make sure there is a name for the cylinder */ - if (same_string(dive->cylinder[i].type.description, "")) - dive->cylinder[i].type.description = strdup(translate("gettextFromC", "unknown")); - } - return DC_STATUS_SUCCESS; -} - -static void handle_event(struct divecomputer *dc, struct sample *sample, dc_sample_value_t value) -{ - int type, time; - /* we mark these for translation here, but we store the untranslated strings - * and only translate them when they are displayed on screen */ - static const char *events[] = { - QT_TRANSLATE_NOOP("gettextFromC", "none"), QT_TRANSLATE_NOOP("gettextFromC", "deco stop"), QT_TRANSLATE_NOOP("gettextFromC", "rbt"), QT_TRANSLATE_NOOP("gettextFromC", "ascent"), QT_TRANSLATE_NOOP("gettextFromC", "ceiling"), QT_TRANSLATE_NOOP("gettextFromC", "workload"), - QT_TRANSLATE_NOOP("gettextFromC", "transmitter"), QT_TRANSLATE_NOOP("gettextFromC", "violation"), QT_TRANSLATE_NOOP("gettextFromC", "bookmark"), QT_TRANSLATE_NOOP("gettextFromC", "surface"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop"), - QT_TRANSLATE_NOOP("gettextFromC", "gaschange"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop (voluntary)"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop (mandatory)"), - QT_TRANSLATE_NOOP("gettextFromC", "deepstop"), QT_TRANSLATE_NOOP("gettextFromC", "ceiling (safety stop)"), QT_TRANSLATE_NOOP3("gettextFromC", "below floor", "event showing dive is below deco floor and adding deco time"), QT_TRANSLATE_NOOP("gettextFromC", "divetime"), - QT_TRANSLATE_NOOP("gettextFromC", "maxdepth"), QT_TRANSLATE_NOOP("gettextFromC", "OLF"), QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚"), QT_TRANSLATE_NOOP("gettextFromC", "airtime"), QT_TRANSLATE_NOOP("gettextFromC", "rgbm"), QT_TRANSLATE_NOOP("gettextFromC", "heading"), - QT_TRANSLATE_NOOP("gettextFromC", "tissue level warning"), QT_TRANSLATE_NOOP("gettextFromC", "gaschange"), QT_TRANSLATE_NOOP("gettextFromC", "non stop time") - }; - const int nr_events = sizeof(events) / sizeof(const char *); - const char *name; - /* - * Just ignore surface events. They are pointless. What "surface" - * means depends on the dive computer (and possibly even settings - * in the dive computer). It does *not* necessarily mean "depth 0", - * so don't even turn it into that. - */ - if (value.event.type == SAMPLE_EVENT_SURFACE) - return; - - /* - * Other evens might be more interesting, but for now we just print them out. - */ - type = value.event.type; - name = QT_TRANSLATE_NOOP("gettextFromC", "invalid event number"); - if (type < nr_events) - name = events[type]; - - time = value.event.time; - if (sample) - time += sample->time.seconds; - - add_event(dc, time, type, value.event.flags, value.event.value, name); -} - -void -sample_cb(dc_sample_type_t type, dc_sample_value_t value, void *userdata) -{ - static unsigned int nsensor = 0; - struct divecomputer *dc = userdata; - struct sample *sample; - - /* - * We fill in the "previous" sample - except for DC_SAMPLE_TIME, - * which creates a new one. - */ - sample = dc->samples ? dc->sample + dc->samples - 1 : NULL; - - /* - * Ok, sanity check. - * If first sample is not a DC_SAMPLE_TIME, Allocate a sample for us - */ - if (sample == NULL && type != DC_SAMPLE_TIME) - sample = prepare_sample(dc); - - switch (type) { - case DC_SAMPLE_TIME: - nsensor = 0; - - // The previous sample gets some sticky values - // that may have been around from before, even - // if there was no new data - if (sample) { - sample->in_deco = in_deco; - sample->ndl.seconds = ndl; - sample->stoptime.seconds = stoptime; - sample->stopdepth.mm = stopdepth; - sample->setpoint.mbar = po2; - sample->cns = cns; - } - // Create a new sample. - // Mark depth as negative - sample = prepare_sample(dc); - sample->time.seconds = value.time; - sample->depth.mm = -1; - finish_sample(dc); - break; - case DC_SAMPLE_DEPTH: - sample->depth.mm = rint(value.depth * 1000); - break; - case DC_SAMPLE_PRESSURE: - sample->sensor = value.pressure.tank; - sample->cylinderpressure.mbar = rint(value.pressure.value * 1000); - break; - case DC_SAMPLE_TEMPERATURE: - sample->temperature.mkelvin = C_to_mkelvin(value.temperature); - break; - case DC_SAMPLE_EVENT: - handle_event(dc, sample, value); - break; - case DC_SAMPLE_RBT: - sample->rbt.seconds = (!strncasecmp(dc->model, "suunto", 6)) ? value.rbt : value.rbt * 60; - break; - case DC_SAMPLE_HEARTBEAT: - sample->heartbeat = value.heartbeat; - break; - case DC_SAMPLE_BEARING: - sample->bearing.degrees = value.bearing; - break; -#ifdef DEBUG_DC_VENDOR - case DC_SAMPLE_VENDOR: - printf(" ", FRACTION(sample->time.seconds, 60), - value.vendor.type, value.vendor.size); - for (int i = 0; i < value.vendor.size; ++i) - printf("%02X", ((unsigned char *)value.vendor.data)[i]); - printf("\n"); - break; -#endif -#if DC_VERSION_CHECK(0, 3, 0) - case DC_SAMPLE_SETPOINT: - /* for us a setpoint means constant pO2 from here */ - sample->setpoint.mbar = po2 = rint(value.setpoint * 1000); - break; - case DC_SAMPLE_PPO2: - if (nsensor < 3) - sample->o2sensor[nsensor].mbar = rint(value.ppo2 * 1000); - else - report_error("%d is more o2 sensors than we can handle", nsensor); - nsensor++; - // Set the amount of detected o2 sensors - if (nsensor > dc->no_o2sensors) - dc->no_o2sensors = nsensor; - break; - case DC_SAMPLE_CNS: - sample->cns = cns = rint(value.cns * 100); - break; - case DC_SAMPLE_DECO: - if (value.deco.type == DC_DECO_NDL) { - sample->ndl.seconds = ndl = value.deco.time; - sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); - sample->in_deco = in_deco = false; - } else if (value.deco.type == DC_DECO_DECOSTOP || - value.deco.type == DC_DECO_DEEPSTOP) { - sample->in_deco = in_deco = true; - sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); - sample->stoptime.seconds = stoptime = value.deco.time; - ndl = 0; - } else if (value.deco.type == DC_DECO_SAFETYSTOP) { - sample->in_deco = in_deco = false; - sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); - sample->stoptime.seconds = stoptime = value.deco.time; - } -#endif - default: - break; - } -} - -static void dev_info(device_data_t *devdata, const char *fmt, ...) -{ - static char buffer[1024]; - va_list ap; - - va_start(ap, fmt); - vsnprintf(buffer, sizeof(buffer), fmt, ap); - va_end(ap); - progress_bar_text = buffer; -} - -static int import_dive_number = 0; - -static int parse_samples(device_data_t *devdata, struct divecomputer *dc, dc_parser_t *parser) -{ - // Parse the sample data. - return dc_parser_samples_foreach(parser, sample_cb, dc); -} - -static int might_be_same_dc(struct divecomputer *a, struct divecomputer *b) -{ - if (!a->model || !b->model) - return 1; - if (strcasecmp(a->model, b->model)) - return 0; - if (!a->deviceid || !b->deviceid) - return 1; - return a->deviceid == b->deviceid; -} - -static int match_one_dive(struct divecomputer *a, struct dive *dive) -{ - struct divecomputer *b = &dive->dc; - - /* - * Walk the existing dive computer data, - * see if we have a match (or an anti-match: - * the same dive computer but a different - * dive ID). - */ - do { - int match = match_one_dc(a, b); - if (match) - return match > 0; - b = b->next; - } while (b); - - /* Ok, no exact dive computer match. Does the date match? */ - b = &dive->dc; - do { - if (a->when == b->when && might_be_same_dc(a, b)) - return 1; - b = b->next; - } while (b); - - return 0; -} - -/* - * Check if this dive already existed before the import - */ -static int find_dive(struct divecomputer *match) -{ - int i; - - for (i = 0; i < dive_table.preexisting; i++) { - struct dive *old = dive_table.dives[i]; - - if (match_one_dive(match, old)) - return 1; - } - return 0; -} - -static inline int year(int year) -{ - if (year < 70) - return year + 2000; - if (year < 100) - return year + 1900; - return year; -} - -/* - * Like g_strdup_printf(), but without the stupid g_malloc/g_free confusion. - * And we limit the string to some arbitrary size. - */ -static char *str_printf(const char *fmt, ...) -{ - va_list args; - char buf[1024]; - - va_start(args, fmt); - vsnprintf(buf, sizeof(buf) - 1, fmt, args); - va_end(args); - buf[sizeof(buf) - 1] = 0; - return strdup(buf); -} - -/* - * The dive ID for libdivecomputer dives is the first word of the - * SHA1 of the fingerprint, if it exists. - * - * NOTE! This is byte-order dependent, and I don't care. - */ -static uint32_t calculate_diveid(const unsigned char *fingerprint, unsigned int fsize) -{ - uint32_t csum[5]; - - if (!fingerprint || !fsize) - return 0; - - SHA1(fingerprint, fsize, (unsigned char *)csum); - return csum[0]; -} - -#ifdef DC_FIELD_STRING -static uint32_t calculate_string_hash(const char *str) -{ - return calculate_diveid((const unsigned char *)str, strlen(str)); -} - -static void parse_string_field(struct dive *dive, dc_field_string_t *str) -{ - // Our dive ID is the string hash of the "Dive ID" string - if (!strcmp(str->desc, "Dive ID")) { - if (!dive->dc.diveid) - dive->dc.diveid = calculate_string_hash(str->value); - return; - } - add_extra_data(&dive->dc, str->desc, str->value); - if (!strcmp(str->desc, "Serial")) { - dive->dc.serial = strdup(str->value); - /* should we just overwrite this whenever we have the "Serial" field? - * It's a much better deviceid then what we have so far... for now I'm leaving it as is */ - if (!dive->dc.deviceid) - dive->dc.deviceid = calculate_string_hash(str->value); - return; - } - if (!strcmp(str->desc, "FW Version")) { - dive->dc.fw_version = strdup(str->value); - return; - } -} -#endif - -static dc_status_t libdc_header_parser(dc_parser_t *parser, struct device_data_t *devdata, struct dive *dive) -{ - dc_status_t rc = 0; - dc_datetime_t dt = { 0 }; - struct tm tm; - - rc = dc_parser_get_datetime(parser, &dt); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error parsing the datetime")); - return rc; - } - - dive->dc.deviceid = devdata->deviceid; - - if (rc == DC_STATUS_SUCCESS) { - tm.tm_year = dt.year; - tm.tm_mon = dt.month - 1; - tm.tm_mday = dt.day; - tm.tm_hour = dt.hour; - tm.tm_min = dt.minute; - tm.tm_sec = dt.second; - dive->when = dive->dc.when = utc_mktime(&tm); - } - - // Parse the divetime. - const char *date_string = get_dive_date_c_string(dive->when); - dev_info(devdata, translate("gettextFromC", "Dive %d: %s"), import_dive_number, date_string); - free((void *)date_string); - - unsigned int divetime = 0; - rc = dc_parser_get_field(parser, DC_FIELD_DIVETIME, 0, &divetime); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error parsing the divetime")); - return rc; - } - if (rc == DC_STATUS_SUCCESS) - dive->dc.duration.seconds = divetime; - - // Parse the maxdepth. - double maxdepth = 0.0; - rc = dc_parser_get_field(parser, DC_FIELD_MAXDEPTH, 0, &maxdepth); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error parsing the maxdepth")); - return rc; - } - if (rc == DC_STATUS_SUCCESS) - dive->dc.maxdepth.mm = rint(maxdepth * 1000); - -#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) - // if this is defined then we have a fairly late version of libdivecomputer - // from the 0.5 development cylcle - most likely temperatures and tank sizes - // are supported - - // Parse temperatures - double temperature; - dc_field_type_t temp_fields[] = {DC_FIELD_TEMPERATURE_SURFACE, - DC_FIELD_TEMPERATURE_MAXIMUM, - DC_FIELD_TEMPERATURE_MINIMUM}; - for (int i = 0; i < 3; i++) { - rc = dc_parser_get_field(parser, temp_fields[i], 0, &temperature); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error parsing temperature")); - return rc; - } - if (rc == DC_STATUS_SUCCESS) - switch(i) { - case 0: - dive->dc.airtemp.mkelvin = C_to_mkelvin(temperature); - break; - case 1: // we don't distinguish min and max water temp here, so take min if given, max otherwise - case 2: - dive->dc.watertemp.mkelvin = C_to_mkelvin(temperature); - break; - } - } -#endif - - // Parse the gas mixes. - unsigned int ngases = 0; - rc = dc_parser_get_field(parser, DC_FIELD_GASMIX_COUNT, 0, &ngases); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error parsing the gas mix count")); - return rc; - } - -#if DC_VERSION_CHECK(0, 3, 0) - // Check if the libdivecomputer version already supports salinity & atmospheric - dc_salinity_t salinity = { - .type = DC_WATER_SALT, - .density = SEAWATER_SALINITY / 10.0 - }; - rc = dc_parser_get_field(parser, DC_FIELD_SALINITY, 0, &salinity); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error obtaining water salinity")); - return rc; - } - if (rc == DC_STATUS_SUCCESS) - dive->dc.salinity = rint(salinity.density * 10.0); - - double surface_pressure = 0; - rc = dc_parser_get_field(parser, DC_FIELD_ATMOSPHERIC, 0, &surface_pressure); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error obtaining surface pressure")); - return rc; - } - if (rc == DC_STATUS_SUCCESS) - dive->dc.surface_pressure.mbar = rint(surface_pressure * 1000.0); -#endif - -#ifdef DC_FIELD_STRING - // The dive parsing may give us more device information - int idx; - for (idx = 0; idx < 100; idx++) { - dc_field_string_t str = { NULL }; - rc = dc_parser_get_field(parser, DC_FIELD_STRING, idx, &str); - if (rc != DC_STATUS_SUCCESS) - break; - if (!str.desc || !str.value) - break; - parse_string_field(dive, &str); - } -#endif - -#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) - dc_divemode_t divemode; - rc = dc_parser_get_field(parser, DC_FIELD_DIVEMODE, 0, &divemode); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error obtaining divemode")); - return rc; - } - if (rc == DC_STATUS_SUCCESS) - switch(divemode) { - case DC_DIVEMODE_FREEDIVE: - dive->dc.divemode = FREEDIVE; - break; - case DC_DIVEMODE_GAUGE: - case DC_DIVEMODE_OC: /* Open circuit */ - dive->dc.divemode = OC; - break; - case DC_DIVEMODE_CC: /* Closed circuit */ - dive->dc.divemode = CCR; - break; - } -#endif - - rc = parse_gasmixes(devdata, dive, parser, ngases); - if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { - dev_info(devdata, translate("gettextFromC", "Error parsing the gas mix")); - return rc; - } - - return DC_STATUS_SUCCESS; -} - -/* returns true if we want libdivecomputer's dc_device_foreach() to continue, - * false otherwise */ -static int dive_cb(const unsigned char *data, unsigned int size, - const unsigned char *fingerprint, unsigned int fsize, - void *userdata) -{ - int rc; - dc_parser_t *parser = NULL; - device_data_t *devdata = userdata; - struct dive *dive = NULL; - - /* reset the deco / ndl data */ - ndl = stoptime = stopdepth = 0; - in_deco = false; - - rc = create_parser(devdata, &parser); - if (rc != DC_STATUS_SUCCESS) { - dev_info(devdata, translate("gettextFromC", "Unable to create parser for %s %s"), devdata->vendor, devdata->product); - return false; - } - - rc = dc_parser_set_data(parser, data, size); - if (rc != DC_STATUS_SUCCESS) { - dev_info(devdata, translate("gettextFromC", "Error registering the data")); - goto error_exit; - } - - import_dive_number++; - dive = alloc_dive(); - - // Parse the dive's header data - rc = libdc_header_parser (parser, devdata, dive); - if (rc != DC_STATUS_SUCCESS) { - dev_info(devdata, translate("getextFromC", "Error parsing the header")); - goto error_exit; - } - - dive->dc.model = strdup(devdata->model); - dive->dc.diveid = calculate_diveid(fingerprint, fsize); - - // Initialize the sample data. - rc = parse_samples(devdata, &dive->dc, parser); - if (rc != DC_STATUS_SUCCESS) { - dev_info(devdata, translate("gettextFromC", "Error parsing the samples")); - goto error_exit; - } - - /* If we already saw this dive, abort. */ - if (!devdata->force_download && find_dive(&dive->dc)) - goto error_exit; - - dc_parser_destroy(parser); - - /* Various libdivecomputer interface fixups */ - if (first_temp_is_air && dive->dc.samples) { - dive->dc.airtemp = dive->dc.sample[0].temperature; - dive->dc.sample[0].temperature.mkelvin = 0; - } - - if (devdata->create_new_trip) { - if (!devdata->trip) - devdata->trip = create_and_hookup_trip_from_dive(dive); - else - add_dive_to_trip(dive, devdata->trip); - } - - dive->downloaded = true; - record_dive_to_table(dive, devdata->download_table); - mark_divelist_changed(true); - return true; - -error_exit: - dc_parser_destroy(parser); - free(dive); - return false; - -} - -/* - * The device ID for libdivecomputer devices is the first 32-bit word - * of the SHA1 hash of the model/firmware/serial numbers. - * - * NOTE! This is byte-order-dependent. And I can't find it in myself to - * care. - */ -static uint32_t calculate_sha1(unsigned int model, unsigned int firmware, unsigned int serial) -{ - SHA_CTX ctx; - uint32_t csum[5]; - - SHA1_Init(&ctx); - SHA1_Update(&ctx, &model, sizeof(model)); - SHA1_Update(&ctx, &firmware, sizeof(firmware)); - SHA1_Update(&ctx, &serial, sizeof(serial)); - SHA1_Final((unsigned char *)csum, &ctx); - return csum[0]; -} - -/* - * libdivecomputer has returned two different serial numbers for the - * same device in different versions. First it used to just do the four - * bytes as one 32-bit number, then it turned it into a decimal number - * with each byte giving two digits (0-99). - * - * The only way we can tell is by looking at the format of the number, - * so we'll just fix it to the first format. - */ -static unsigned int undo_libdivecomputer_suunto_nr_changes(unsigned int serial) -{ - unsigned char b0, b1, b2, b3; - - /* - * The second format will never have more than 8 decimal - * digits, so do a cheap check first - */ - if (serial >= 100000000) - return serial; - - /* The original format seems to be four bytes of values 00-99 */ - b0 = (serial >> 0) & 0xff; - b1 = (serial >> 8) & 0xff; - b2 = (serial >> 16) & 0xff; - b3 = (serial >> 24) & 0xff; - - /* Looks like an old-style libdivecomputer serial number */ - if ((b0 < 100) && (b1 < 100) && (b2 < 100) && (b3 < 100)) - return serial; - - /* Nope, it was converted. */ - b0 = serial % 100; - serial /= 100; - b1 = serial % 100; - serial /= 100; - b2 = serial % 100; - serial /= 100; - b3 = serial % 100; - - serial = b0 + (b1 << 8) + (b2 << 16) + (b3 << 24); - return serial; -} - -static unsigned int fixup_suunto_versions(device_data_t *devdata, const dc_event_devinfo_t *devinfo) -{ - unsigned int serial = devinfo->serial; - char serial_nr[13] = ""; - char firmware[13] = ""; - - first_temp_is_air = 1; - - serial = undo_libdivecomputer_suunto_nr_changes(serial); - - if (serial) { - snprintf(serial_nr, sizeof(serial_nr), "%02d%02d%02d%02d", - (devinfo->serial >> 24) & 0xff, - (devinfo->serial >> 16) & 0xff, - (devinfo->serial >> 8) & 0xff, - (devinfo->serial >> 0) & 0xff); - } - if (devinfo->firmware) { - snprintf(firmware, sizeof(firmware), "%d.%d.%d", - (devinfo->firmware >> 16) & 0xff, - (devinfo->firmware >> 8) & 0xff, - (devinfo->firmware >> 0) & 0xff); - } - create_device_node(devdata->model, devdata->deviceid, serial_nr, firmware, ""); - - return serial; -} - -static void event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata) -{ - const dc_event_progress_t *progress = data; - const dc_event_devinfo_t *devinfo = data; - const dc_event_clock_t *clock = data; - const dc_event_vendor_t *vendor = data; - device_data_t *devdata = userdata; - unsigned int serial; - - switch (event) { - case DC_EVENT_WAITING: - dev_info(devdata, translate("gettextFromC", "Event: waiting for user action")); - break; - case DC_EVENT_PROGRESS: - if (!progress->maximum) - break; - progress_bar_fraction = (double)progress->current / (double)progress->maximum; - break; - case DC_EVENT_DEVINFO: - dev_info(devdata, translate("gettextFromC", "model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)"), - devinfo->model, devinfo->model, - devinfo->firmware, devinfo->firmware, - devinfo->serial, devinfo->serial); - if (devdata->libdc_logfile) { - fprintf(devdata->libdc_logfile, "Event: model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)\n", - devinfo->model, devinfo->model, - devinfo->firmware, devinfo->firmware, - devinfo->serial, devinfo->serial); - } - /* - * libdivecomputer doesn't give serial numbers in the proper string form, - * so we have to see if we can do some vendor-specific munging. - */ - serial = devinfo->serial; - if (!strcmp(devdata->vendor, "Suunto")) - serial = fixup_suunto_versions(devdata, devinfo); - devdata->deviceid = calculate_sha1(devinfo->model, devinfo->firmware, serial); - /* really, serial and firmware version are NOT numbers. We'll try to save them here - * in something that might work, but this really needs to be handled with the - * DC_FIELD_STRING interface instead */ - devdata->libdc_serial = devinfo->serial; - devdata->libdc_firmware = devinfo->firmware; - break; - case DC_EVENT_CLOCK: - dev_info(devdata, translate("gettextFromC", "Event: systime=%" PRId64 ", devtime=%u\n"), - (uint64_t)clock->systime, clock->devtime); - if (devdata->libdc_logfile) { - fprintf(devdata->libdc_logfile, "Event: systime=%" PRId64 ", devtime=%u\n", - (uint64_t)clock->systime, clock->devtime); - } - break; - case DC_EVENT_VENDOR: - if (devdata->libdc_logfile) { - fprintf(devdata->libdc_logfile, "Event: vendor="); - for (unsigned int i = 0; i < vendor->size; ++i) - fprintf(devdata->libdc_logfile, "%02X", vendor->data[i]); - fprintf(devdata->libdc_logfile, "\n"); - } - break; - default: - break; - } -} - -int import_thread_cancelled; - -static int -cancel_cb(void *userdata) -{ - return import_thread_cancelled; -} - -static const char *do_device_import(device_data_t *data) -{ - dc_status_t rc; - dc_device_t *device = data->device; - - data->model = str_printf("%s %s", data->vendor, data->product); - - // Register the event handler. - int events = DC_EVENT_WAITING | DC_EVENT_PROGRESS | DC_EVENT_DEVINFO | DC_EVENT_CLOCK | DC_EVENT_VENDOR; - rc = dc_device_set_events(device, events, event_cb, data); - if (rc != DC_STATUS_SUCCESS) - return translate("gettextFromC", "Error registering the event handler."); - - // Register the cancellation handler. - rc = dc_device_set_cancel(device, cancel_cb, data); - if (rc != DC_STATUS_SUCCESS) - return translate("gettextFromC", "Error registering the cancellation handler."); - - if (data->libdc_dump) { - dc_buffer_t *buffer = dc_buffer_new(0); - - rc = dc_device_dump(device, buffer); - if (rc == DC_STATUS_SUCCESS && dumpfile_name) { - FILE *fp = subsurface_fopen(dumpfile_name, "wb"); - if (fp != NULL) { - fwrite(dc_buffer_get_data(buffer), 1, dc_buffer_get_size(buffer), fp); - fclose(fp); - } - } - - dc_buffer_free(buffer); - } else { - rc = dc_device_foreach(device, dive_cb, data); - } - - if (rc != DC_STATUS_SUCCESS) { - progress_bar_fraction = 0.0; - return translate("gettextFromC", "Dive data import error"); - } - - /* All good */ - return NULL; -} - -void -logfunc(dc_context_t *context, dc_loglevel_t loglevel, const char *file, unsigned int line, const char *function, const char *msg, void *userdata) -{ - const char *loglevels[] = { "NONE", "ERROR", "WARNING", "INFO", "DEBUG", "ALL" }; - - FILE *fp = (FILE *)userdata; - - if (loglevel == DC_LOGLEVEL_ERROR || loglevel == DC_LOGLEVEL_WARNING) { - fprintf(fp, "%s: %s [in %s:%d (%s)]\n", loglevels[loglevel], msg, file, line, function); - } else { - fprintf(fp, "%s: %s\n", loglevels[loglevel], msg); - } -} - -const char *do_libdivecomputer_import(device_data_t *data) -{ - dc_status_t rc; - const char *err; - FILE *fp = NULL; - - import_dive_number = 0; - first_temp_is_air = 0; - data->device = NULL; - data->context = NULL; - - if (data->libdc_log && logfile_name) - fp = subsurface_fopen(logfile_name, "w"); - - data->libdc_logfile = fp; - - rc = dc_context_new(&data->context); - if (rc != DC_STATUS_SUCCESS) - return translate("gettextFromC", "Unable to create libdivecomputer context"); - - if (fp) { - dc_context_set_loglevel(data->context, DC_LOGLEVEL_ALL); - dc_context_set_logfunc(data->context, logfunc, fp); - } - - err = translate("gettextFromC", "Unable to open %s %s (%s)"); - -#if defined(SSRF_CUSTOM_SERIAL) - dc_serial_t *serial_device = NULL; - - if (data->bluetooth_mode) { -#ifdef BT_SUPPORT - rc = dc_serial_qt_open(&serial_device, data->context, data->devname); -#endif -#ifdef SERIAL_FTDI - } else if (!strcmp(data->devname, "ftdi")) { - rc = dc_serial_ftdi_open(&serial_device, data->context); -#endif - } - - if (rc != DC_STATUS_SUCCESS) { - report_error(errmsg(rc)); - } else if (serial_device) { - rc = dc_device_custom_open(&data->device, data->context, data->descriptor, serial_device); - } else { -#else - { -#endif - rc = dc_device_open(&data->device, data->context, data->descriptor, data->devname); - - if (rc != DC_STATUS_SUCCESS && subsurface_access(data->devname, R_OK | W_OK) != 0) - err = translate("gettextFromC", "Insufficient privileges to open the device %s %s (%s)"); - } - - if (rc == DC_STATUS_SUCCESS) { - err = do_device_import(data); - /* TODO: Show the logfile to the user on error. */ - dc_device_close(data->device); - data->device = NULL; - } - - dc_context_free(data->context); - data->context = NULL; - - if (fp) { - fclose(fp); - } - - return err; -} - -/* - * Parse data buffers instead of dc devices downloaded data. - * Intended to be used to parse profile data from binary files during import tasks. - * Actually included Uwatec families because of works on datatrak and smartrak logs - * and OSTC families for OSTCTools logs import. - * For others, simply include them in the switch (check parameters). - * Note that dc_descriptor_t in data *must* have been filled using dc_descriptor_iterator() - * calls. - */ -dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size) -{ - dc_status_t rc; - dc_parser_t *parser = NULL; - - switch (data->descriptor->type) { - case DC_FAMILY_UWATEC_ALADIN: - case DC_FAMILY_UWATEC_MEMOMOUSE: - rc = uwatec_memomouse_parser_create(&parser, data->context, 0, 0); - break; - case DC_FAMILY_UWATEC_SMART: - case DC_FAMILY_UWATEC_MERIDIAN: - rc = uwatec_smart_parser_create (&parser, data->context, data->descriptor->model, 0, 0); - break; - case DC_FAMILY_HW_OSTC: -#if defined(SSRF_CUSTOM_SERIAL) - rc = hw_ostc_parser_create (&parser, data->context, data->deviceid, 0); -#else - rc = hw_ostc_parser_create (&parser, data->context, data->deviceid); -#endif - break; - case DC_FAMILY_HW_FROG: - case DC_FAMILY_HW_OSTC3: -#if defined(SSRF_CUSTOM_SERIAL) - rc = hw_ostc_parser_create (&parser, data->context, data->deviceid, 1); -#else - rc = hw_ostc_parser_create (&parser, data->context, data->deviceid); -#endif - break; - default: - report_error("Device type not handled!"); - return DC_STATUS_UNSUPPORTED; - } - if (rc != DC_STATUS_SUCCESS) { - report_error("Error creating parser."); - dc_parser_destroy (parser); - return rc; - } - rc = dc_parser_set_data(parser, buffer, size); - if (rc != DC_STATUS_SUCCESS) { - report_error("Error registering the data."); - dc_parser_destroy (parser); - return rc; - } - // Do not parse Aladin/Memomouse headers as they are fakes - // Do not return on error, we can still parse the samples - if (data->descriptor->type != DC_FAMILY_UWATEC_ALADIN && data->descriptor->type != DC_FAMILY_UWATEC_MEMOMOUSE) { - rc = libdc_header_parser (parser, data, dive); - if (rc != DC_STATUS_SUCCESS) { - report_error("Error parsing the dive header data. Dive # %d\nStatus = %s", dive->number, errmsg(rc)); - } - } - rc = dc_parser_samples_foreach (parser, sample_cb, &dive->dc); - if (rc != DC_STATUS_SUCCESS) { - report_error("Error parsing the sample data. Dive # %d\nStatus = %s", dive->number, errmsg(rc)); - dc_parser_destroy (parser); - return rc; - } - dc_parser_destroy(parser); - return(DC_STATUS_SUCCESS); -} diff --git a/libdivecomputer.h b/libdivecomputer.h deleted file mode 100644 index 79817e509..000000000 --- a/libdivecomputer.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef LIBDIVECOMPUTER_H -#define LIBDIVECOMPUTER_H - - -/* libdivecomputer */ -#include -#include -#include - -#include "dive.h" - -#ifdef __cplusplus -extern "C" { -#endif - -struct dc_descriptor_t { - const char *vendor; - const char *product; - dc_family_t type; - unsigned int model; -}; - -/* don't forget to include the UI toolkit specific display-XXX.h first - to get the definition of progressbar_t */ -typedef struct device_data_t -{ - dc_descriptor_t *descriptor; - const char *vendor, *product, *devname; - const char *model; - uint32_t libdc_firmware, libdc_serial; - uint32_t deviceid, diveid; - dc_device_t *device; - dc_context_t *context; - struct dive_trip *trip; - int preexisting; - bool force_download; - bool create_new_trip; - bool libdc_log; - bool libdc_dump; - bool bluetooth_mode; - FILE *libdc_logfile; - struct dive_table *download_table; -} device_data_t; - -const char *errmsg (dc_status_t rc); -const char *do_libdivecomputer_import(device_data_t *data); -const char *do_uemis_import(device_data_t *data); -dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size); -void logfunc(dc_context_t *context, dc_loglevel_t loglevel, const char *file, unsigned int line, const char *function, const char *msg, void *userdata); - -extern int import_thread_cancelled; -extern const char *progress_bar_text; -extern double progress_bar_fraction; -extern char *logfile_name; -extern char *dumpfile_name; - -#if SSRF_CUSTOM_SERIAL -extern dc_status_t dc_serial_qt_open(dc_serial_t **out, dc_context_t *context, const char *devaddr); -extern dc_status_t dc_serial_ftdi_open(dc_serial_t **out, dc_context_t *context); -#endif - -#ifdef __cplusplus -} -#endif - -#endif // LIBDIVECOMPUTER_H diff --git a/linux.c b/linux.c deleted file mode 100644 index d4131c7ea..000000000 --- a/linux.c +++ /dev/null @@ -1,225 +0,0 @@ -/* linux.c */ -/* implements Linux specific functions */ -#include "dive.h" -#include "display.h" -#include "membuffer.h" -#include -#include -#include -#include -#include -#include -#include -#include - -// the DE should provide us with a default font and font size... -const char linux_system_divelist_default_font[] = "Sans"; -const char *system_divelist_default_font = linux_system_divelist_default_font; -double system_divelist_default_font_size = -1.0; - -void subsurface_OS_pref_setup(void) -{ - // nothing -} - -bool subsurface_ignore_font(const char *font) -{ - // there are no old default fonts to ignore - return false; -} - -void subsurface_user_info(struct user_info *user) -{ - struct passwd *pwd = getpwuid(getuid()); - const char *username = getenv("USER"); - - if (pwd) { - if (pwd->pw_gecos && *pwd->pw_gecos) - user->name = pwd->pw_gecos; - if (!username) - username = pwd->pw_name; - } - if (username && *username) { - char hostname[64]; - struct membuffer mb = { 0 }; - gethostname(hostname, sizeof(hostname)); - put_format(&mb, "%s@%s", username, hostname); - user->email = mb_cstring(&mb); - } -} - -static const char *system_default_path_append(const char *append) -{ - const char *home = getenv("HOME"); - if (!home) - home = "~"; - const char *path = "/.subsurface"; - - int len = strlen(home) + strlen(path) + 1; - if (append) - len += strlen(append) + 1; - - char *buffer = (char *)malloc(len); - memset(buffer, 0, len); - strcat(buffer, home); - strcat(buffer, path); - if (append) { - strcat(buffer, "/"); - strcat(buffer, append); - } - - return buffer; -} - -const char *system_default_directory(void) -{ - static const char *path = NULL; - if (!path) - path = system_default_path_append(NULL); - return path; -} - -const char *system_default_filename(void) -{ - static char *filename = NULL; - if (!filename) { - const char *user = getenv("LOGNAME"); - if (same_string(user, "")) - user = "username"; - filename = calloc(strlen(user) + 5, 1); - strcat(filename, user); - strcat(filename, ".xml"); - } - static const char *path = NULL; - if (!path) - path = system_default_path_append(filename); - return path; -} - -int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) -{ - int index = -1, entries = 0; - DIR *dp = NULL; - struct dirent *ep = NULL; - size_t i; - FILE *file; - char *line = NULL; - char *fname; - size_t len; - if (dc_type != DC_TYPE_UEMIS) { - const char *dirname = "/dev"; - const char *patterns[] = { - "ttyUSB*", - "ttyS*", - "ttyACM*", - "rfcomm*", - NULL - }; - - dp = opendir(dirname); - if (dp == NULL) { - return -1; - } - - while ((ep = readdir(dp)) != NULL) { - for (i = 0; patterns[i] != NULL; ++i) { - if (fnmatch(patterns[i], ep->d_name, 0) == 0) { - char filename[1024]; - int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); - if (n >= sizeof(filename)) { - closedir(dp); - return -1; - } - callback(filename, userdata); - if (is_default_dive_computer_device(filename)) - index = entries; - entries++; - break; - } - } - } - closedir(dp); - } - if (dc_type != DC_TYPE_SERIAL) { - int num_uemis = 0; - file = fopen("/proc/mounts", "r"); - if (file == NULL) - return index; - - while ((getline(&line, &len, file)) != -1) { - char *ptr = strstr(line, "UEMISSDA"); - if (ptr) { - char *end = ptr, *start = ptr; - while (start > line && *start != ' ') - start--; - if (*start == ' ') - start++; - while (*end != ' ' && *end != '\0') - end++; - - *end = '\0'; - fname = strdup(start); - - callback(fname, userdata); - - if (is_default_dive_computer_device(fname)) - index = entries; - entries++; - num_uemis++; - free((void *)fname); - } - } - free(line); - fclose(file); - if (num_uemis == 1 && entries == 1) /* if we found only one and it's a mounted Uemis, pick it */ - index = 0; - } - return index; -} - -/* NOP wrappers to comform with windows.c */ -int subsurface_rename(const char *path, const char *newpath) -{ - return rename(path, newpath); -} - -int subsurface_open(const char *path, int oflags, mode_t mode) -{ - return open(path, oflags, mode); -} - -FILE *subsurface_fopen(const char *path, const char *mode) -{ - return fopen(path, mode); -} - -void *subsurface_opendir(const char *path) -{ - return (void *)opendir(path); -} - -int subsurface_access(const char *path, int mode) -{ - return access(path, mode); -} - -struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) -{ - return zip_open(path, flags, errorp); -} - -int subsurface_zip_close(struct zip *zip) -{ - return zip_close(zip); -} - -/* win32 console */ -void subsurface_console_init(bool dedicated) -{ - /* NOP */ -} - -void subsurface_console_exit(void) -{ - /* NOP */ -} diff --git a/liquivision.c b/liquivision.c deleted file mode 100644 index 295287c15..000000000 --- a/liquivision.c +++ /dev/null @@ -1,414 +0,0 @@ -#include - -#include "dive.h" -#include "divelist.h" -#include "file.h" -#include "strndup.h" - -// Convert bytes into an INT -#define array_uint16_le(p) ((unsigned int) (p)[0] \ - + ((p)[1]<<8) ) -#define array_uint32_le(p) ((unsigned int) (p)[0] \ - + ((p)[1]<<8) + ((p)[2]<<16) \ - + ((p)[3]<<24)) - -struct lv_event { - time_t time; - struct pressure { - int sensor; - int mbar; - } pressure; -}; - -uint16_t primary_sensor; - -static int handle_event_ver2(int code, const unsigned char *ps, unsigned int ps_ptr, struct lv_event *event) -{ - // Skip 4 bytes - return 4; -} - - -static int handle_event_ver3(int code, const unsigned char *ps, unsigned int ps_ptr, struct lv_event *event) -{ - int skip = 4; - uint16_t current_sensor; - - switch (code) { - case 0x0002: // Unknown - case 0x0004: // Unknown - skip = 4; - break; - case 0x0005: // Unknown - skip = 6; - break; - case 0x0007: // Gas - // 4 byte time - // 1 byte O2, 1 bye He - skip = 6; - break; - case 0x0008: - // 4 byte time - // 2 byte gas set point 2 - skip = 6; - break; - case 0x000f: - // Tank pressure - event->time = array_uint32_le(ps + ps_ptr); - - /* As far as I know, Liquivision supports 2 sensors, own and buddie's. This is my - * best guess how it is represented. */ - - current_sensor = array_uint16_le(ps + ps_ptr + 4); - if (primary_sensor == 0) { - primary_sensor = current_sensor; - } - if (current_sensor == primary_sensor) { - event->pressure.sensor = 0; - event->pressure.mbar = array_uint16_le(ps + ps_ptr + 6) * 10; // cb->mb - } else { - /* Ignoring the buddy sensor for no as we cannot draw it on the profile. - event->pressure.sensor = 1; - event->pressure.mbar = array_uint16_le(ps + ps_ptr + 6) * 10; // cb->mb - */ - } - // 1 byte PSR - // 1 byte ST - skip = 10; - break; - case 0x0010: - skip = 26; - break; - case 0x0015: // Unknown - skip = 2; - break; - default: - skip = 4; - break; - } - - return skip; -} - -static void parse_dives (int log_version, const unsigned char *buf, unsigned int buf_size) -{ - unsigned int ptr = 0; - unsigned char model; - - struct dive *dive; - struct divecomputer *dc; - struct sample *sample; - - while (ptr < buf_size) { - int i; - bool found_divesite = false; - dive = alloc_dive(); - primary_sensor = 0; - dc = &dive->dc; - - /* Just the main cylinder until we can handle the buddy cylinder porperly */ - for (i = 0; i < 1; i++) - fill_default_cylinder(&dive->cylinder[i]); - - // Model 0=Xen, 1,2=Xeo, 4=Lynx, other=Liquivision - model = *(buf + ptr); - switch (model) { - case 0: - dc->model = "Xen"; - break; - case 1: - case 2: - dc->model = "Xeo"; - break; - case 4: - dc->model = "Lynx"; - break; - default: - dc->model = "Liquivision"; - break; - } - ptr++; - - // Dive location, assemble Location and Place - unsigned int len, place_len; - char *location; - len = array_uint32_le(buf + ptr); - ptr += 4; - place_len = array_uint32_le(buf + ptr + len); - - if (len && place_len) { - location = malloc(len + place_len + 4); - memset(location, 0, len + place_len + 4); - memcpy(location, buf + ptr, len); - memcpy(location + len, ", ", 2); - memcpy(location + len + 2, buf + ptr + len + 4, place_len); - } else if (len) { - location = strndup((char *)buf + ptr, len); - } else if (place_len) { - location = strndup((char *)buf + ptr + len + 4, place_len); - } - - /* Store the location only if we have one */ - if (len || place_len) - found_divesite = true; - - ptr += len + 4 + place_len; - - // Dive comment - len = array_uint32_le(buf + ptr); - ptr += 4; - - // Blank notes are better than the default text - if (len && strncmp((char *)buf + ptr, "Comment ...", 11)) { - dive->notes = strndup((char *)buf + ptr, len); - } - ptr += len; - - dive->id = array_uint32_le(buf + ptr); - ptr += 4; - - dive->number = array_uint16_le(buf + ptr) + 1; - ptr += 2; - - dive->duration.seconds = array_uint32_le(buf + ptr); // seconds - ptr += 4; - - dive->maxdepth.mm = array_uint16_le(buf + ptr) * 10; // cm->mm - ptr += 2; - - dive->meandepth.mm = array_uint16_le(buf + ptr) * 10; // cm->mm - ptr += 2; - - dive->when = array_uint32_le(buf + ptr); - ptr += 4; - - // now that we have the dive time we can store the divesite - // (we need the dive time to create deterministic uuids) - if (found_divesite) { - dive->dive_site_uuid = find_or_create_dive_site_with_name(location, dive->when); - free(location); - } - //unsigned int end_time = array_uint32_le(buf + ptr); - ptr += 4; - - //unsigned int sit = array_uint32_le(buf + ptr); - ptr += 4; - //if (sit == 0xffffffff) { - //} - - dive->surface_pressure.mbar = array_uint16_le(buf + ptr); // ??? - ptr += 2; - - //unsigned int rep_dive = array_uint16_le(buf + ptr); - ptr += 2; - - dive->mintemp.mkelvin = C_to_mkelvin((float)array_uint16_le(buf + ptr)/10);// C->mK - ptr += 2; - - dive->maxtemp.mkelvin = C_to_mkelvin((float)array_uint16_le(buf + ptr)/10);// C->mK - ptr += 2; - - dive->salinity = *(buf + ptr); // ??? - ptr += 1; - - unsigned int sample_count = array_uint32_le(buf + ptr); - ptr += 4; - - // Sample interval - unsigned char sample_interval; - sample_interval = 1; - - unsigned char intervals[6] = {1,2,5,10,30,60}; - if (*(buf + ptr) < 6) - sample_interval = intervals[*(buf + ptr)]; - ptr += 1; - - float start_cns = 0; - unsigned char dive_mode = 0, algorithm = 0; - if (array_uint32_le(buf + ptr) != sample_count) { - // Xeo, with CNS and OTU - start_cns = *(float *) (buf + ptr); - ptr += 4; - dive->cns = *(float *) (buf + ptr); // end cns - ptr += 4; - dive->otu = *(float *) (buf + ptr); - ptr += 4; - dive_mode = *(buf + ptr++); // 0=Deco, 1=Gauge, 2=None - algorithm = *(buf + ptr++); // 0=ZH-L16C+GF - sample_count = array_uint32_le(buf + ptr); - } - // we aren't using the start_cns, dive_mode, and algorithm, yet - (void)start_cns; - (void)dive_mode; - (void)algorithm; - - ptr += 4; - - // Parse dive samples - const unsigned char *ds = buf + ptr; - const unsigned char *ts = buf + ptr + sample_count * 2 + 4; - const unsigned char *ps = buf + ptr + sample_count * 4 + 4; - unsigned int ps_count = array_uint32_le(ps); - ps += 4; - - // Bump ptr - ptr += sample_count * 4 + 4; - - // Handle events - unsigned int ps_ptr; - ps_ptr = 0; - - unsigned int event_code, d = 0, e; - struct lv_event event; - - // Loop through events - for (e = 0; e < ps_count; e++) { - // Get event - event_code = array_uint16_le(ps + ps_ptr); - ps_ptr += 2; - - if (log_version == 3) { - ps_ptr += handle_event_ver3(event_code, ps, ps_ptr, &event); - if (event_code != 0xf) - continue; // ignore all by pressure sensor event - } else { // version 2 - ps_ptr += handle_event_ver2(event_code, ps, ps_ptr, &event); - continue; // ignore all events - } - int sample_time, last_time; - int depth_mm, last_depth, temp_mk, last_temp; - - while (true) { - sample = prepare_sample(dc); - - // Get sample times - sample_time = d * sample_interval; - depth_mm = array_uint16_le(ds + d * 2) * 10; // cm->mm - temp_mk = C_to_mkelvin((float)array_uint16_le(ts + d * 2) / 10); // dC->mK - last_time = (d ? (d - 1) * sample_interval : 0); - - if (d == sample_count) { - // We still have events to record - sample->time.seconds = event.time; - sample->depth.mm = array_uint16_le(ds + (d - 1) * 2) * 10; // cm->mm - sample->temperature.mkelvin = C_to_mkelvin((float) array_uint16_le(ts + (d - 1) * 2) / 10); // dC->mK - sample->sensor = event.pressure.sensor; - sample->cylinderpressure.mbar = event.pressure.mbar; - finish_sample(dc); - - break; - } else if (event.time > sample_time) { - // Record sample and loop - sample->time.seconds = sample_time; - sample->depth.mm = depth_mm; - sample->temperature.mkelvin = temp_mk; - finish_sample(dc); - d++; - - continue; - } else if (event.time == sample_time) { - sample->time.seconds = sample_time; - sample->depth.mm = depth_mm; - sample->temperature.mkelvin = temp_mk; - sample->sensor = event.pressure.sensor; - sample->cylinderpressure.mbar = event.pressure.mbar; - finish_sample(dc); - - break; - } else { // Event is prior to sample - sample->time.seconds = event.time; - sample->sensor = event.pressure.sensor; - sample->cylinderpressure.mbar = event.pressure.mbar; - if (last_time == sample_time) { - sample->depth.mm = depth_mm; - sample->temperature.mkelvin = temp_mk; - } else { - // Extrapolate - last_depth = array_uint16_le(ds + (d - 1) * 2) * 10; // cm->mm - last_temp = C_to_mkelvin((float) array_uint16_le(ts + (d - 1) * 2) / 10); // dC->mK - sample->depth.mm = last_depth + (depth_mm - last_depth) - * (event.time - last_time) / sample_interval; - sample->temperature.mkelvin = last_temp + (temp_mk - last_temp) - * (event.time - last_time) / sample_interval; - } - finish_sample(dc); - - break; - } - } // while (true); - } // for each event sample - - // record trailing depth samples - for ( ;d < sample_count; d++) { - sample = prepare_sample(dc); - sample->time.seconds = d * sample_interval; - - sample->depth.mm = array_uint16_le(ds + d * 2) * 10; // cm->mm - sample->temperature.mkelvin = - C_to_mkelvin((float)array_uint16_le(ts + d * 2) / 10); - finish_sample(dc); - } - - if (log_version == 3 && model == 4) { - // Advance to begin of next dive - switch (array_uint16_le(ps + ps_ptr)) { - case 0x0000: - ps_ptr += 5; - break; - case 0x0100: - ps_ptr += 7; - break; - case 0x0200: - ps_ptr += 9; - break; - case 0x0300: - ps_ptr += 11; - break; - case 0x0b0b: - ps_ptr += 27; - break; - } - - while (*(ps + ps_ptr) != 0x04) - ps_ptr++; - } - - // End dive - dive->downloaded = true; - record_dive(dive); - mark_divelist_changed(true); - - // Advance ptr for next dive - ptr += ps_ptr + 4; - } // while - - //DEBUG save_dives("/tmp/test.xml"); -} - -int try_to_open_liquivision(const char *filename, struct memblock *mem) -{ - const unsigned char *buf = mem->buffer; - unsigned int buf_size = mem->size; - unsigned int ptr; - int log_version; - - // Get name length - unsigned int len = array_uint32_le(buf); - // Ignore length field and the name - ptr = 4 + len; - - unsigned int dive_count = array_uint32_le(buf + ptr); - if (dive_count == 0xffffffff) { - // File version 3.0 - log_version = 3; - ptr += 6; - dive_count = array_uint32_le(buf + ptr); - } else { - log_version = 2; - } - ptr += 4; - - parse_dives(log_version, buf + ptr, buf_size - ptr); - - return 1; -} diff --git a/load-git.c b/load-git.c deleted file mode 100644 index 39dab4367..000000000 --- a/load-git.c +++ /dev/null @@ -1,1638 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gettext.h" - -#include "dive.h" -#include "divelist.h" -#include "device.h" -#include "membuffer.h" -#include "git-access.h" -#include "qthelperfromc.h" - -const char *saved_git_id = NULL; - -struct picture_entry_list { - void *data; - int len; - const char *hash; - struct picture_entry_list *next; -}; -struct picture_entry_list *pel = NULL; - -struct keyword_action { - const char *keyword; - void (*fn)(char *, struct membuffer *, void *); -}; -#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0])) - -extern degrees_t parse_degrees(char *buf, char **end); -git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry); - -static void save_picture_from_git(struct picture *picture) -{ - struct picture_entry_list *pic_entry = pel; - - while (pic_entry) { - if (same_string(pic_entry->hash, picture->hash)) { - savePictureLocal(picture, pic_entry->data, pic_entry->len); - return; - } - pic_entry = pic_entry->next; - } - fprintf(stderr, "didn't find picture entry for %s\n", picture->filename); -} - -static char *get_utf8(struct membuffer *b) -{ - int len = b->len; - char *res; - - if (!len) - return NULL; - res = malloc(len+1); - if (res) { - memcpy(res, b->buffer, len); - res[len] = 0; - } - return res; -} - -static temperature_t get_temperature(const char *line) -{ - temperature_t t; - t.mkelvin = C_to_mkelvin(ascii_strtod(line, NULL)); - return t; -} - -static depth_t get_depth(const char *line) -{ - depth_t d; - d.mm = rint(1000*ascii_strtod(line, NULL)); - return d; -} - -static volume_t get_volume(const char *line) -{ - volume_t v; - v.mliter = rint(1000*ascii_strtod(line, NULL)); - return v; -} - -static weight_t get_weight(const char *line) -{ - weight_t w; - w.grams = rint(1000*ascii_strtod(line, NULL)); - return w; -} - -static pressure_t get_pressure(const char *line) -{ - pressure_t p; - p.mbar = rint(1000*ascii_strtod(line, NULL)); - return p; -} - -static int get_salinity(const char *line) -{ - return rint(10*ascii_strtod(line, NULL)); -} - -static fraction_t get_fraction(const char *line) -{ - fraction_t f; - f.permille = rint(10*ascii_strtod(line, NULL)); - return f; -} - -static void update_date(timestamp_t *when, const char *line) -{ - unsigned yyyy, mm, dd; - struct tm tm; - - if (sscanf(line, "%04u-%02u-%02u", &yyyy, &mm, &dd) != 3) - return; - utc_mkdate(*when, &tm); - tm.tm_year = yyyy - 1900; - tm.tm_mon = mm - 1; - tm.tm_mday = dd; - *when = utc_mktime(&tm); -} - -static void update_time(timestamp_t *when, const char *line) -{ - unsigned h, m, s = 0; - struct tm tm; - - if (sscanf(line, "%02u:%02u:%02u", &h, &m, &s) < 2) - return; - utc_mkdate(*when, &tm); - tm.tm_hour = h; - tm.tm_min = m; - tm.tm_sec = s; - *when = utc_mktime(&tm); -} - -static duration_t get_duration(const char *line) -{ - int m = 0, s = 0; - duration_t d; - sscanf(line, "%d:%d", &m, &s); - d.seconds = m*60+s; - return d; -} - -static enum dive_comp_type get_dctype(const char *line) -{ - for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) { - if (strcmp(line, divemode_text[i]) == 0) - return i; - } - return 0; -} - -static int get_index(const char *line) -{ return atoi(line); } - -static int get_hex(const char *line) -{ return strtoul(line, NULL, 16); } - -/* this is in qthelper.cpp, so including the .h file is a pain */ -extern const char *printGPSCoords(int lat, int lon); - -static void parse_dive_gps(char *line, struct membuffer *str, void *_dive) -{ - uint32_t uuid; - degrees_t latitude = parse_degrees(line, &line); - degrees_t longitude = parse_degrees(line, &line); - struct dive *dive = _dive; - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!ds) { - uuid = get_dive_site_uuid_by_gps(latitude, longitude, NULL); - if (!uuid) - uuid = create_dive_site_with_gps("", latitude, longitude, dive->when); - dive->dive_site_uuid = uuid; - } else { - if (dive_site_has_gps_location(ds) && - (ds->latitude.udeg != latitude.udeg || ds->longitude.udeg != longitude.udeg)) { - const char *coords = printGPSCoords(latitude.udeg, longitude.udeg); - // we have a dive site that already has GPS coordinates - ds->notes = add_to_string(ds->notes, translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords); - free((void *)coords); - } - ds->latitude = latitude; - ds->longitude = longitude; - } - -} - -static void parse_dive_location(char *line, struct membuffer *str, void *_dive) -{ - uint32_t uuid; - char *name = get_utf8(str); - struct dive *dive = _dive; - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!ds) { - uuid = get_dive_site_uuid_by_name(name, NULL); - if (!uuid) - uuid = create_dive_site(name, dive->when); - dive->dive_site_uuid = uuid; - } else { - // we already had a dive site linked to the dive - if (same_string(ds->name, "")) { - ds->name = strdup(name); - } else { - // and that dive site had a name. that's weird - if our name is different, add it to the notes - if (!same_string(ds->name, name)) - ds->notes = add_to_string(ds->notes, translate("gettextFromC", "additional name for site: %s\n"), name); - } - } - free(name); -} - -static void parse_dive_divemaster(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->divemaster = get_utf8(str); } - -static void parse_dive_buddy(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->buddy = get_utf8(str); } - -static void parse_dive_suit(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->suit = get_utf8(str); } - -static void parse_dive_notes(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->notes = get_utf8(str); } - -static void parse_dive_divesiteid(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->dive_site_uuid = get_hex(line); } - -/* - * We can have multiple tags in the membuffer. They are separated by - * NUL bytes. - */ -static void parse_dive_tags(char *line, struct membuffer *str, void *_dive) -{ - struct dive *dive = _dive; - const char *tag; - int len = str->len; - - if (!len) - return; - - /* Make sure there is a NUL at the end too */ - tag = mb_cstring(str); - for (;;) { - int taglen = strlen(tag); - if (taglen) - taglist_add_tag(&dive->tag_list, tag); - len -= taglen; - if (!len) - return; - tag += taglen+1; - len--; - } -} - -static void parse_dive_airtemp(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->airtemp = get_temperature(line); } - -static void parse_dive_watertemp(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->watertemp = get_temperature(line); } - -static void parse_dive_duration(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->duration = get_duration(line); } - -static void parse_dive_rating(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->rating = get_index(line); } - -static void parse_dive_visibility(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->visibility = get_index(line); } - -static void parse_dive_notrip(char *line, struct membuffer *str, void *_dive) -{ struct dive *dive = _dive; dive->tripflag = NO_TRIP; } - -static void parse_site_description(char *line, struct membuffer *str, void *_ds) -{ struct dive_site *ds = _ds; ds->description = strdup(mb_cstring(str)); } - -static void parse_site_name(char *line, struct membuffer *str, void *_ds) -{ struct dive_site *ds = _ds; ds->name = strdup(mb_cstring(str)); } - -static void parse_site_notes(char *line, struct membuffer *str, void *_ds) -{ struct dive_site *ds = _ds; ds->notes = strdup(mb_cstring(str)); } - -extern degrees_t parse_degrees(char *buf, char **end); -static void parse_site_gps(char *line, struct membuffer *str, void *_ds) -{ - struct dive_site *ds = _ds; - - ds->latitude = parse_degrees(line, &line); - ds->longitude = parse_degrees(line, &line); -} - -static void parse_site_geo(char *line, struct membuffer *str, void *_ds) -{ - struct dive_site *ds = _ds; - if (ds->taxonomy.category == NULL) - ds->taxonomy.category = alloc_taxonomy(); - int nr = ds->taxonomy.nr; - if (nr < TC_NR_CATEGORIES) { - struct taxonomy *t = &ds->taxonomy.category[nr]; - t->value = strdup(mb_cstring(str)); - sscanf(line, "cat %d origin %d \"", &t->category, (int *)&t->origin); - ds->taxonomy.nr++; - } -} - -/* Parse key=val parts of samples and cylinders etc */ -static char *parse_keyvalue_entry(void (*fn)(void *, const char *, const char *), void *fndata, char *line) -{ - char *key = line, *val, c; - - while ((c = *line) != 0) { - if (isspace(c) || c == '=') - break; - line++; - } - - if (c == '=') - *line++ = 0; - val = line; - - while ((c = *line) != 0) { - if (isspace(c)) - break; - line++; - } - if (c) - *line++ = 0; - - fn(fndata, key, val); - return line; -} - -static int cylinder_index, weightsystem_index; - -static void parse_cylinder_keyvalue(void *_cylinder, const char *key, const char *value) -{ - cylinder_t *cylinder = _cylinder; - if (!strcmp(key, "vol")) { - cylinder->type.size = get_volume(value); - return; - } - if (!strcmp(key, "workpressure")) { - cylinder->type.workingpressure = get_pressure(value); - return; - } - /* This is handled by the "get_utf8()" */ - if (!strcmp(key, "description")) - return; - if (!strcmp(key, "o2")) { - cylinder->gasmix.o2 = get_fraction(value); - return; - } - if (!strcmp(key, "he")) { - cylinder->gasmix.he = get_fraction(value); - return; - } - if (!strcmp(key, "start")) { - cylinder->start = get_pressure(value); - return; - } - if (!strcmp(key, "end")) { - cylinder->end = get_pressure(value); - return; - } - if (!strcmp(key, "use")) { - cylinder->cylinder_use = cylinderuse_from_text(value); - return; - } - report_error("Unknown cylinder key/value pair (%s/%s)", key, value); -} - -static void parse_dive_cylinder(char *line, struct membuffer *str, void *_dive) -{ - struct dive *dive = _dive; - cylinder_t *cylinder = dive->cylinder + cylinder_index; - - cylinder_index++; - cylinder->type.description = get_utf8(str); - for (;;) { - char c; - while (isspace(c = *line)) - line++; - if (!c) - break; - line = parse_keyvalue_entry(parse_cylinder_keyvalue, cylinder, line); - } -} - -static void parse_weightsystem_keyvalue(void *_ws, const char *key, const char *value) -{ - weightsystem_t *ws = _ws; - if (!strcmp(key, "weight")) { - ws->weight = get_weight(value); - return; - } - /* This is handled by the "get_utf8()" */ - if (!strcmp(key, "description")) - return; - report_error("Unknown weightsystem key/value pair (%s/%s)", key, value); -} - -static void parse_dive_weightsystem(char *line, struct membuffer *str, void *_dive) -{ - struct dive *dive = _dive; - weightsystem_t *ws = dive->weightsystem + weightsystem_index; - - weightsystem_index++; - ws->description = get_utf8(str); - for (;;) { - char c; - while (isspace(c = *line)) - line++; - if (!c) - break; - line = parse_keyvalue_entry(parse_weightsystem_keyvalue, ws, line); - } -} - -static int match_action(char *line, struct membuffer *str, void *data, - struct keyword_action *action, unsigned nr_action) -{ - char *p = line, c; - unsigned low, high; - - while ((c = *p) >= 'a' && c <= 'z') // skip over 1st word - p++; // Extract the second word from the line: - if (p == line) - return -1; - switch (c) { - case 0: // if 2nd word is C-terminated - break; - case ' ': // =end of 2nd word? - *p++ = 0; // then C-terminate that word - break; - default: - return -1; - } - - /* Standard binary search in a table */ - low = 0; - high = nr_action; - while (low < high) { - unsigned mid = (low+high)/2; - struct keyword_action *a = action + mid; - int cmp = strcmp(line, a->keyword); - if (!cmp) { // attribute found: - a->fn(p, str, data); // Execute appropriate function, - return 0; // .. passing 2n word from above - } // (p) as a function argument. - if (cmp < 0) - high = mid; - else - low = mid+1; - } -report_error("Unmatched action '%s'", line); - return -1; -} - -/* FIXME! We should do the array thing here too. */ -static void parse_sample_keyvalue(void *_sample, const char *key, const char *value) -{ - struct sample *sample = _sample; - - if (!strcmp(key, "sensor")) { - sample->sensor = atoi(value); - return; - } - if (!strcmp(key, "ndl")) { - sample->ndl = get_duration(value); - return; - } - if (!strcmp(key, "tts")) { - sample->tts = get_duration(value); - return; - } - if (!strcmp(key, "in_deco")) { - sample->in_deco = atoi(value); - return; - } - if (!strcmp(key, "stoptime")) { - sample->stoptime = get_duration(value); - return; - } - if (!strcmp(key, "stopdepth")) { - sample->stopdepth = get_depth(value); - return; - } - if (!strcmp(key, "cns")) { - sample->cns = atoi(value); - return; - } - - if (!strcmp(key, "rbt")) { - sample->rbt = get_duration(value); - return; - } - - if (!strcmp(key, "po2")) { - pressure_t p = get_pressure(value); - sample->setpoint.mbar = p.mbar; - return; - } - if (!strcmp(key, "sensor1")) { - pressure_t p = get_pressure(value); - sample->o2sensor[0].mbar = p.mbar; - return; - } - if (!strcmp(key, "sensor2")) { - pressure_t p = get_pressure(value); - sample->o2sensor[1].mbar = p.mbar; - return; - } - if (!strcmp(key, "sensor3")) { - pressure_t p = get_pressure(value); - sample->o2sensor[2].mbar = p.mbar; - return; - } - if (!strcmp(key, "o2pressure")) { - pressure_t p = get_pressure(value); - sample->o2cylinderpressure.mbar = p.mbar; - return; - } - if (!strcmp(key, "heartbeat")) { - sample->heartbeat = atoi(value); - return; - } - if (!strcmp(key, "bearing")) { - sample->bearing.degrees = atoi(value); - return; - } - - report_error("Unexpected sample key/value pair (%s/%s)", key, value); -} - -static char *parse_sample_unit(struct sample *sample, double val, char *unit) -{ - char *end = unit, c; - - /* Skip over the unit */ - while ((c = *end) != 0) { - if (isspace(c)) { - *end++ = 0; - break; - } - end++; - } - - /* The units are "°C", "m" or "bar", so let's just look at the first character */ - switch (*unit) { - case 'm': - sample->depth.mm = rint(1000*val); - break; - case 'b': - sample->cylinderpressure.mbar = rint(1000*val); - break; - default: - sample->temperature.mkelvin = C_to_mkelvin(val); - break; - } - - return end; -} - -/* - * By default the sample data does not change unless the - * save-file gives an explicit new value. So we copy the - * data from the previous sample if one exists, and then - * the parsing will update it as necessary. - * - * There are a few exceptions, like the sample pressure: - * missing sample pressure doesn't mean "same as last - * time", but "interpolate". We clear those ones - * explicitly. - */ -static struct sample *new_sample(struct divecomputer *dc) -{ - struct sample *sample = prepare_sample(dc); - if (sample != dc->sample) { - memcpy(sample, sample-1, sizeof(struct sample)); - sample->cylinderpressure.mbar = 0; - } - return sample; -} - -static void sample_parser(char *line, struct divecomputer *dc) -{ - int m, s = 0; - struct sample *sample = new_sample(dc); - - m = strtol(line, &line, 10); - if (*line == ':') - s = strtol(line+1, &line, 10); - sample->time.seconds = m*60+s; - - for (;;) { - char c; - - while (isspace(c = *line)) - line++; - if (!c) - break; - /* Less common sample entries have a name */ - if (c >= 'a' && c <= 'z') { - line = parse_keyvalue_entry(parse_sample_keyvalue, sample, line); - } else { - const char *end; - double val = ascii_strtod(line, &end); - if (end == line) { - report_error("Odd sample data: %s", line); - break; - } - line = (char *)end; - line = parse_sample_unit(sample, val, line); - } - } - finish_sample(dc); -} - -static void parse_dc_airtemp(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->airtemp = get_temperature(line); } - -static void parse_dc_date(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; update_date(&dc->when, line); } - -static void parse_dc_deviceid(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->deviceid = get_hex(line); } - -static void parse_dc_diveid(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->diveid = get_hex(line); } - -static void parse_dc_duration(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->duration = get_duration(line); } - -static void parse_dc_dctype(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->divemode = get_dctype(line); } - -static void parse_dc_maxdepth(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->maxdepth = get_depth(line); } - -static void parse_dc_meandepth(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->meandepth = get_depth(line); } - -static void parse_dc_model(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->model = get_utf8(str); } - -static void parse_dc_numberofoxygensensors(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->no_o2sensors = get_index(line); } - -static void parse_dc_surfacepressure(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->surface_pressure = get_pressure(line); } - -static void parse_dc_salinity(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->salinity = get_salinity(line); } - -static void parse_dc_surfacetime(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->surfacetime = get_duration(line); } - -static void parse_dc_time(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; update_time(&dc->when, line); } - -static void parse_dc_watertemp(char *line, struct membuffer *str, void *_dc) -{ struct divecomputer *dc = _dc; dc->watertemp = get_temperature(line); } - -static void parse_event_keyvalue(void *_event, const char *key, const char *value) -{ - struct event *event = _event; - int val = atoi(value); - - if (!strcmp(key, "type")) { - event->type = val; - } else if (!strcmp(key, "flags")) { - event->flags = val; - } else if (!strcmp(key, "value")) { - event->value = val; - } else if (!strcmp(key, "name")) { - /* We get the name from the string handling */ - } else if (!strcmp(key, "cylinder")) { - /* NOTE! We add one here as a marker that "yes, we got a cylinder index" */ - event->gas.index = 1+get_index(value); - } else if (!strcmp(key, "o2")) { - event->gas.mix.o2 = get_fraction(value); - } else if (!strcmp(key, "he")) { - event->gas.mix.he = get_fraction(value); - } else - report_error("Unexpected event key/value pair (%s/%s)", key, value); -} - -/* keyvalue "key" "value" - * so we have two strings (possibly empty) in the membuffer, separated by a '\0' */ -static void parse_dc_keyvalue(char *line, struct membuffer *str, void *_dc) -{ - const char *key, *value; - struct divecomputer *dc = _dc; - - // Let's make sure we have two strings... - int string_counter = 0; - while(*line) { - if (*line == '"') - string_counter++; - line++; - } - if (string_counter != 2) - return; - - // stupidly the second string in the membuffer isn't NUL terminated; - // asking for a cstring fixes that; interestingly enough, given that there are two - // strings in the mb, the next command at the same time assigns a pointer to the - // first string to 'key' and NUL terminates the second string (which then goes to 'value') - key = mb_cstring(str); - value = key + strlen(key) + 1; - add_extra_data(dc, key, value); -} - -static void parse_dc_event(char *line, struct membuffer *str, void *_dc) -{ - int m, s = 0; - const char *name; - struct divecomputer *dc = _dc; - struct event event = { 0 }, *ev; - - m = strtol(line, &line, 10); - if (*line == ':') - s = strtol(line+1, &line, 10); - event.time.seconds = m*60+s; - - for (;;) { - char c; - while (isspace(c = *line)) - line++; - if (!c) - break; - line = parse_keyvalue_entry(parse_event_keyvalue, &event, line); - } - - name = ""; - if (str->len) - name = mb_cstring(str); - ev = add_event(dc, event.time.seconds, event.type, event.flags, event.value, name); - if (ev && event_is_gaschange(ev)) { - /* - * We subtract one here because "0" is "no index", - * and the parsing will add one for actual cylinder - * index data (see parse_event_keyvalue) - */ - ev->gas.index = event.gas.index-1; - if (event.gas.mix.o2.permille || event.gas.mix.he.permille) - ev->gas.mix = event.gas.mix; - } -} - -static void parse_trip_date(char *line, struct membuffer *str, void *_trip) -{ dive_trip_t *trip = _trip; update_date(&trip->when, line); } - -static void parse_trip_time(char *line, struct membuffer *str, void *_trip) -{ dive_trip_t *trip = _trip; update_time(&trip->when, line); } - -static void parse_trip_location(char *line, struct membuffer *str, void *_trip) -{ dive_trip_t *trip = _trip; trip->location = get_utf8(str); } - -static void parse_trip_notes(char *line, struct membuffer *str, void *_trip) -{ dive_trip_t *trip = _trip; trip->notes = get_utf8(str); } - -static void parse_settings_autogroup(char *line, struct membuffer *str, void *_unused) -{ set_autogroup(1); } - -static void parse_settings_units(char *line, struct membuffer *str, void *unused) -{ - if (line) - set_informational_units(line); -} - -static void parse_settings_userid(char *line, struct membuffer *str, void *_unused) -{ - if (line) { - set_save_userid_local(true); - set_userid(line); - } -} - -/* - * Our versioning is a joke right now, but this is more of an example of what we - * *can* do some day. And if we do change the version, this warning will show if - * you read with a version of subsurface that doesn't know about it. - * We MUST keep this in sync with the XML version (so we can report a consistent - * minimum datafile version) - */ -static void parse_settings_version(char *line, struct membuffer *str, void *_unused) -{ - int version = atoi(line); - report_datafile_version(version); - if (version > DATAFORMAT_VERSION) - report_error("Git save file version %d is newer than version %d I know about", version, DATAFORMAT_VERSION); -} - -/* The string in the membuffer is the version string of subsurface that saved things, just FYI */ -static void parse_settings_subsurface(char *line, struct membuffer *str, void *_unused) -{ } - -struct divecomputerid { - const char *model; - const char *nickname; - const char *firmware; - const char *serial; - const char *cstr; - unsigned int deviceid; -}; - -static void parse_divecomputerid_keyvalue(void *_cid, const char *key, const char *value) -{ - struct divecomputerid *cid = _cid; - - if (*value == '"') { - value = cid->cstr; - cid->cstr += strlen(cid->cstr)+1; - } - if (!strcmp(key, "deviceid")) { - cid->deviceid = get_hex(value); - return; - } - if (!strcmp(key, "serial")) { - cid->serial = value; - return; - } - if (!strcmp(key, "firmware")) { - cid->firmware = value; - return; - } - if (!strcmp(key, "nickname")) { - cid->nickname = value; - return; - } - report_error("Unknow divecomputerid key/value pair (%s/%s)", key, value); -} - -/* - * The 'divecomputerid' is a bit harder to parse than some other things, because - * it can have multiple strings (but see the tag parsing for another example of - * that) in addition to the non-string entries. - * - * We keep the "next" string in "id.cstr" and update it as we use it. - */ -static void parse_settings_divecomputerid(char *line, struct membuffer *str, void *_unused) -{ - struct divecomputerid id = { mb_cstring(str) }; - - id.cstr = id.model + strlen(id.model) + 1; - - /* Skip the '"' that stood for the model string */ - line++; - - for (;;) { - char c; - while (isspace(c = *line)) - line++; - if (!c) - break; - line = parse_keyvalue_entry(parse_divecomputerid_keyvalue, &id, line); - } - create_device_node(id.model, id.deviceid, id.serial, id.firmware, id.nickname); -} - -static void parse_picture_filename(char *line, struct membuffer *str, void *_pic) -{ - struct picture *pic = _pic; - pic->filename = get_utf8(str); -} - -static void parse_picture_gps(char *line, struct membuffer *str, void *_pic) -{ - struct picture *pic = _pic; - - pic->latitude = parse_degrees(line, &line); - pic->longitude = parse_degrees(line, &line); -} - -static void parse_picture_hash(char *line, struct membuffer *str, void *_pic) -{ - struct picture *pic = _pic; - pic->hash = get_utf8(str); -} - -/* These need to be sorted! */ -struct keyword_action dc_action[] = { -#undef D -#define D(x) { #x, parse_dc_ ## x } - D(airtemp), D(date), D(dctype), D(deviceid), D(diveid), D(duration), - D(event), D(keyvalue), D(maxdepth), D(meandepth), D(model), D(numberofoxygensensors), - D(salinity), D(surfacepressure), D(surfacetime), D(time), D(watertemp) -}; - -/* Sample lines start with a space or a number */ -static void divecomputer_parser(char *line, struct membuffer *str, void *_dc) -{ - char c = *line; - if (c < 'a' || c > 'z') - sample_parser(line, _dc); - match_action(line, str, _dc, dc_action, ARRAY_SIZE(dc_action)); -} - -/* These need to be sorted! */ -struct keyword_action dive_action[] = { -#undef D -#define D(x) { #x, parse_dive_ ## x } - D(airtemp), D(buddy), D(cylinder), D(divemaster), D(divesiteid), D(duration), - D(gps), D(location), D(notes), D(notrip), D(rating), D(suit), - D(tags), D(visibility), D(watertemp), D(weightsystem) -}; - -static void dive_parser(char *line, struct membuffer *str, void *_dive) -{ - match_action(line, str, _dive, dive_action, ARRAY_SIZE(dive_action)); -} - -/* These need to be sorted! */ -struct keyword_action site_action[] = { -#undef D -#define D(x) { #x, parse_site_ ## x } - D(description), D(geo), D(gps), D(name), D(notes) -}; - -static void site_parser(char *line, struct membuffer *str, void *_ds) -{ - match_action(line, str, _ds, site_action, ARRAY_SIZE(site_action)); -} - -/* These need to be sorted! */ -struct keyword_action trip_action[] = { -#undef D -#define D(x) { #x, parse_trip_ ## x } - D(date), D(location), D(notes), D(time), -}; - -static void trip_parser(char *line, struct membuffer *str, void *_trip) -{ - match_action(line, str, _trip, trip_action, ARRAY_SIZE(trip_action)); -} - -/* These need to be sorted! */ -static struct keyword_action settings_action[] = { -#undef D -#define D(x) { #x, parse_settings_ ## x } - D(autogroup), D(divecomputerid), D(subsurface), D(units), D(userid), D(version), -}; - -static void settings_parser(char *line, struct membuffer *str, void *_unused) -{ - match_action(line, str, NULL, settings_action, ARRAY_SIZE(settings_action)); -} - -/* These need to be sorted! */ -static struct keyword_action picture_action[] = { -#undef D -#define D(x) { #x, parse_picture_ ## x } - D(filename), D(gps), D(hash) -}; - -static void picture_parser(char *line, struct membuffer *str, void *_pic) -{ - match_action(line, str, _pic, picture_action, ARRAY_SIZE(picture_action)); -} - -/* - * We have a very simple line-based interface, with the small - * complication that lines can have strings in the middle, and - * a string can be multiple lines. - * - * The UTF-8 string escaping is *very* simple, though: - * - * - a string starts and ends with double quotes (") - * - * - inside the string we escape: - * (a) double quotes with '\"' - * (b) backslash (\) with '\\' - * - * - additionally, for human readability, we escape - * newlines with '\n\t', with the exception that - * consecutive newlines are left unescaped (so an - * empty line doesn't become a line with just a tab - * on it). - * - * Also, while the UTF-8 string can have arbitrarily - * long lines, the non-string parts of the lines are - * never long, so we can use a small temporary buffer - * on stack for that part. - * - * Also, note that if a line has one or more strings - * in it: - * - * - each string will be represented as a single '"' - * character in the output. - * - * - all string will exist in the same 'membuffer', - * separated by NUL characters (that cannot exist - * in a string, not even quoted). - */ -static const char *parse_one_string(const char *buf, const char *end, struct membuffer *b) -{ - const char *p = buf; - - /* - * We turn multiple strings one one line (think dive tags) into one - * membuffer that has NUL characters in between strings. - */ - if (b->len) - put_bytes(b, "", 1); - - while (p < end) { - char replace; - - switch (*p++) { - default: - continue; - case '\n': - if (p < end && *p == '\t') { - replace = '\n'; - break; - } - continue; - case '\\': - if (p < end) { - replace = *p; - break; - } - continue; - case '"': - replace = 0; - break; - } - put_bytes(b, buf, p - buf - 1); - if (!replace) - break; - put_bytes(b, &replace, 1); - buf = ++p; - } - return p; -} - -typedef void (line_fn_t)(char *, struct membuffer *, void *); -#define MAXLINE 500 -static unsigned parse_one_line(const char *buf, unsigned size, line_fn_t *fn, void *fndata, struct membuffer *b) -{ - const char *end = buf + size; - const char *p = buf; - char line[MAXLINE+1]; - int off = 0; - - while (p < end) { - char c = *p++; - if (c == '\n') - break; - line[off] = c; - off++; - if (off > MAXLINE) - off = MAXLINE; - if (c == '"') - p = parse_one_string(p, end, b); - } - line[off] = 0; - fn(line, b, fndata); - return p - buf; -} - -/* - * We keep on re-using the membuffer that we use for - * strings, but the callback function can "steal" it by - * saving its value and just clear the original. - */ -static void for_each_line(git_blob *blob, line_fn_t *fn, void *fndata) -{ - const char *content = git_blob_rawcontent(blob); - unsigned int size = git_blob_rawsize(blob); - struct membuffer str = { 0 }; - - while (size) { - unsigned int n = parse_one_line(content, size, fn, fndata, &str); - content += n; - size -= n; - - /* Re-use the allocation, but forget the data */ - str.len = 0; - } - free_buffer(&str); -} - -#define GIT_WALK_OK 0 -#define GIT_WALK_SKIP 1 - -static struct divecomputer *active_dc; -static struct dive *active_dive; -static dive_trip_t *active_trip; - -static void finish_active_trip(void) -{ - dive_trip_t *trip = active_trip; - - if (trip) { - active_trip = NULL; - insert_trip(&trip); - } -} - -static void finish_active_dive(void) -{ - struct dive *dive = active_dive; - - if (dive) { - /* check if we need to save pictures */ - FOR_EACH_PICTURE(dive) { - if (!picture_exists(picture)) - save_picture_from_git(picture); - } - /* free any memory we allocated to track pictures */ - while (pel) { - free(pel->data); - void *lastone = pel; - pel = pel->next; - free(lastone); - } - active_dive = NULL; - record_dive(dive); - } -} - -static struct dive *create_new_dive(timestamp_t when) -{ - struct dive *dive = alloc_dive(); - - /* We'll fill in more data from the dive file */ - dive->when = when; - - if (active_trip) - add_dive_to_trip(dive, active_trip); - return dive; -} - -static dive_trip_t *create_new_trip(int yyyy, int mm, int dd) -{ - dive_trip_t *trip = calloc(1, sizeof(dive_trip_t)); - struct tm tm = { 0 }; - - /* We'll fill in the real data from the trip descriptor file */ - tm.tm_year = yyyy; - tm.tm_mon = mm-1; - tm.tm_mday = dd; - trip->when = utc_mktime(&tm); - - return trip; -} - -static bool validate_date(int yyyy, int mm, int dd) -{ - return yyyy > 1970 && yyyy < 3000 && - mm > 0 && mm < 13 && - dd > 0 && dd < 32; -} - -static bool validate_time(int h, int m, int s) -{ - return h >= 0 && h < 24 && - m >= 0 && m < 60 && - s >=0 && s <= 60; -} - -/* - * Dive trip directory, name is 'nn-alphabetic[~hex]' - */ -static int dive_trip_directory(const char *root, const char *name) -{ - int yyyy = -1, mm = -1, dd = -1; - - if (sscanf(root, "%d/%d", &yyyy, &mm) != 2) - return GIT_WALK_SKIP; - dd = atoi(name); - if (!validate_date(yyyy, mm, dd)) - return GIT_WALK_SKIP; - finish_active_trip(); - active_trip = create_new_trip(yyyy, mm, dd); - return GIT_WALK_OK; -} - -/* - * Dive directory, name is [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] in older git repositories - * but [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] in newer repos as ':' is an illegal character for Windows files - * and 'timeoff' points to what should be the time part of - * the name (the first digit of the hour). - * - * The root path will be of the form yyyy/mm[/tripdir], - */ -static int dive_directory(const char *root, const char *name, int timeoff) -{ - int yyyy = -1, mm = -1, dd = -1; - int h, m, s; - int mday_off, month_off, year_off; - struct tm tm; - - /* Skip the '-' before the time */ - mday_off = timeoff; - if (!mday_off || name[--mday_off] != '-') - return GIT_WALK_SKIP; - /* Skip the day name */ - while (mday_off > 0 && name[--mday_off] != '-') - /* nothing */; - - mday_off = mday_off - 2; - month_off = mday_off - 3; - year_off = month_off - 5; - if (mday_off < 0) - return GIT_WALK_SKIP; - - /* Get the time of day -- parse both time formats so we can read old repos when not on Windows */ - if (sscanf(name+timeoff, "%d:%d:%d", &h, &m, &s) != 3 && sscanf(name+timeoff, "%d=%d=%d", &h, &m, &s) != 3) - return GIT_WALK_SKIP; - if (!validate_time(h, m, s)) - return GIT_WALK_SKIP; - - /* - * Using the "git_tree_walk()" interface is simple, but - * it kind of sucks as an interface because there is - * no sane way to pass the hierarchy to the callbacks. - * The "payload" is a fixed one-time thing: we'd like - * the "current trip" to be passed down to the dives - * that get parsed under that trip, but we can't. - * - * So "active_trip" is not the trip that is in the hierarchy - * _above_ us, it's just the trip that was _before_ us. But - * if a dive is not in a trip at all, we can't tell. - * - * We could just do a better walker that passes the - * return value around, but we hack around this by - * instead looking at the one hierarchical piece of - * data we have: the pathname to the current entry. - * - * This is pretty hacky. The magic '8' is the length - * of a pathname of the form 'yyyy/mm/'. - */ - if (strlen(root) == 8) - finish_active_trip(); - - /* - * Get the date. The day of the month is in the dive directory - * name, the year and month might be in the path leading up - * to it. - */ - dd = atoi(name + mday_off); - if (year_off < 0) { - if (sscanf(root, "%d/%d", &yyyy, &mm) != 2) - return GIT_WALK_SKIP; - } else - yyyy = atoi(name + year_off); - if (month_off >= 0) - mm = atoi(name + month_off); - - if (!validate_date(yyyy, mm, dd)) - return GIT_WALK_SKIP; - - /* Ok, close enough. We've gotten sufficient information */ - memset(&tm, 0, sizeof(tm)); - tm.tm_hour = h; - tm.tm_min = m; - tm.tm_sec = s; - tm.tm_year = yyyy - 1900; - tm.tm_mon = mm-1; - tm.tm_mday = dd; - - finish_active_dive(); - active_dive = create_new_dive(utc_mktime(&tm)); - return GIT_WALK_OK; -} - -static int picture_directory(const char *root, const char *name) -{ - if (!active_dive) - return GIT_WALK_SKIP; - return GIT_WALK_OK; -} - -/* - * Return the length of the string without the unique part. - */ -static int nonunique_length(const char *str) -{ - int len = 0; - - for (;;) { - char c = *str++; - if (!c || c == '~') - return len; - len++; - } -} - -/* - * When hitting a directory node, we have a couple of cases: - * - * - It's just a date entry - all numeric (either year or month): - * - * [yyyy|mm] - * - * We don't do anything with these, we just traverse into them. - * The numeric data will show up as part of the full path when - * we hit more interesting entries. - * - * - It's a trip directory. The name will be of the form - * - * nn-alphabetic[~hex] - * - * where 'nn' is the day of the month (year and month will be - * encoded in the path leading up to this). - * - * - It's a dive directory. The name will be of the form - * - * [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] - * - * (older versions had this as [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] - * but that faile on Windows) - * - * which describes the date and time of a dive (yyyy and mm - * are optional, and may be encoded in the path leading up to - * the dive). - * - * - It is a per-dive picture directory ("Pictures") - * - * - It's some random non-dive-data directory. - * - * If it doesn't match the above patterns, we'll ignore them - * for dive loading purposes, and not even recurse into them. - */ -static int walk_tree_directory(const char *root, const git_tree_entry *entry) -{ - const char *name = git_tree_entry_name(entry); - int digits = 0, len; - char c; - - if (!strcmp(name, "Pictures")) - return picture_directory(root, name); - - if (!strcmp(name, "01-Divesites")) - return GIT_WALK_OK; - - while (isdigit(c = name[digits])) - digits++; - - /* Doesn't start with two or four digits? Skip */ - if (digits != 4 && digits != 2) - return GIT_WALK_SKIP; - - /* Only digits? Do nothing, but recurse into it */ - if (!c) - return GIT_WALK_OK; - - /* All valid cases need to have a slash following */ - if (c != '-') - return GIT_WALK_SKIP; - - /* Do a quick check for a common dive case */ - len = nonunique_length(name); - - /* - * We know the len is at least 3, because we had at least - * two digits and a dash - */ - if (name[len-3] == ':' || name[len-3] == '=') - return dive_directory(root, name, len-8); - - if (digits != 2) - return GIT_WALK_SKIP; - - return dive_trip_directory(root, name); -} - -git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry) -{ - const git_oid *id = git_tree_entry_id(entry); - git_blob *blob; - - if (git_blob_lookup(&blob, repo, id)) - return NULL; - return blob; -} - -static struct divecomputer *create_new_dc(struct dive *dive) -{ - struct divecomputer *dc = &dive->dc; - - while (dc->next) - dc = dc->next; - /* Did we already fill that in? */ - if (dc->samples || dc->model || dc->when) { - struct divecomputer *newdc = calloc(1, sizeof(*newdc)); - if (!newdc) - return NULL; - dc->next = newdc; - dc = newdc; - } - dc->when = dive->when; - dc->duration = dive->duration; - return dc; -} - -/* - * We should *really* try to delay the dive computer data parsing - * until necessary, in order to reduce load-time. The parsing is - * cheap, but the loading of the git blob into memory can be pretty - * costly. - */ -static int parse_divecomputer_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) -{ - git_blob *blob = git_tree_entry_blob(repo, entry); - - if (!blob) - return report_error("Unable to read divecomputer file"); - - active_dc = create_new_dc(active_dive); - for_each_line(blob, divecomputer_parser, active_dc); - git_blob_free(blob); - active_dc = NULL; - return 0; -} - -static int parse_dive_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) -{ - struct dive *dive = active_dive; - git_blob *blob = git_tree_entry_blob(repo, entry); - if (!blob) - return report_error("Unable to read dive file"); - if (*suffix) - dive->number = atoi(suffix+1); - cylinder_index = weightsystem_index = 0; - for_each_line(blob, dive_parser, active_dive); - git_blob_free(blob); - return 0; -} - -static int parse_site_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) -{ - if (*suffix == '\0') - return report_error("Dive site without uuid"); - uint32_t uuid = strtoul(suffix, NULL, 16); - struct dive_site *ds = alloc_or_get_dive_site(uuid); - git_blob *blob = git_tree_entry_blob(repo, entry); - if (!blob) - return report_error("Unable to read dive site file"); - for_each_line(blob, site_parser, ds); - git_blob_free(blob); - return 0; -} - -static int parse_trip_entry(git_repository *repo, const git_tree_entry *entry) -{ - git_blob *blob = git_tree_entry_blob(repo, entry); - if (!blob) - return report_error("Unable to read trip file"); - for_each_line(blob, trip_parser, active_trip); - git_blob_free(blob); - return 0; -} - -static int parse_settings_entry(git_repository *repo, const git_tree_entry *entry) -{ - git_blob *blob = git_tree_entry_blob(repo, entry); - if (!blob) - return report_error("Unable to read settings file"); - set_save_userid_local(false); - for_each_line(blob, settings_parser, NULL); - git_blob_free(blob); - return 0; -} - -static int parse_picture_file(git_repository *repo, const git_tree_entry *entry, const char *name) -{ - /* remember the picture data so we can handle it when all dive data has been loaded - * the name of the git file is PIC- */ - git_blob *blob = git_tree_entry_blob(repo, entry); - const void *rawdata = git_blob_rawcontent(blob); - int len = git_blob_rawsize(blob); - struct picture_entry_list *new_pel = malloc(sizeof(struct picture_entry_list)); - new_pel->next = pel; - pel = new_pel; - pel->data = malloc(len); - memcpy(pel->data, rawdata, len); - pel->len = len; - pel->hash = strdup(name + 4); - git_blob_free(blob); - return 0; -} - -static int parse_picture_entry(git_repository *repo, const git_tree_entry *entry, const char *name) -{ - git_blob *blob; - struct picture *pic; - int hh, mm, ss, offset; - char sign; - - /* - * The format of the picture name files is just the offset within - * the dive in form [[+-]hh=mm=ss (previously [[+-]hh:mm:ss, but - * that didn't work on Windows), possibly followed by a hash to - * make the filename unique (which we can just ignore). - */ - if (sscanf(name, "%c%d:%d:%d", &sign, &hh, &mm, &ss) != 4 && - sscanf(name, "%c%d=%d=%d", &sign, &hh, &mm, &ss) != 4) - return report_error("Unknown file name %s", name); - offset = ss + 60*(mm + 60*hh); - if (sign == '-') - offset = -offset; - - blob = git_tree_entry_blob(repo, entry); - if (!blob) - return report_error("Unable to read picture file"); - - pic = alloc_picture(); - pic->offset.seconds = offset; - - for_each_line(blob, picture_parser, pic); - dive_add_picture(active_dive, pic); - git_blob_free(blob); - return 0; -} - -static int walk_tree_file(const char *root, const git_tree_entry *entry, git_repository *repo) -{ - struct dive *dive = active_dive; - dive_trip_t *trip = active_trip; - const char *name = git_tree_entry_name(entry); - if (verbose > 1) - fprintf(stderr, "git load handling file %s\n", name); - switch (*name) { - /* Picture file? They are saved as time offsets in the dive */ - case '-': case '+': - if (dive) - return parse_picture_entry(repo, entry, name); - break; - case 'D': - if (dive && !strncmp(name, "Divecomputer", 12)) - return parse_divecomputer_entry(repo, entry, name+12); - if (dive && !strncmp(name, "Dive", 4)) - return parse_dive_entry(repo, entry, name+4); - break; - case 'S': - if (!strncmp(name, "Site", 4)) - return parse_site_entry(repo, entry, name + 5); - break; - case '0': - if (trip && !strcmp(name, "00-Trip")) - return parse_trip_entry(repo, entry); - if (!strcmp(name, "00-Subsurface")) - return parse_settings_entry(repo, entry); - break; - case 'P': - if (dive && !strncmp(name, "PIC-", 4)) - return parse_picture_file(repo, entry, name); - break; - } - report_error("Unknown file %s%s (%p %p)", root, name, dive, trip); - return GIT_WALK_SKIP; -} - -static int walk_tree_cb(const char *root, const git_tree_entry *entry, void *payload) -{ - git_repository *repo = payload; - git_filemode_t mode = git_tree_entry_filemode(entry); - - if (mode == GIT_FILEMODE_TREE) - return walk_tree_directory(root, entry); - - walk_tree_file(root, entry, repo); - /* Ignore failed blob loads */ - return GIT_WALK_OK; -} - -static int load_dives_from_tree(git_repository *repo, git_tree *tree) -{ - git_tree_walk(tree, GIT_TREEWALK_PRE, walk_tree_cb, repo); - return 0; -} - -void clear_git_id(void) -{ - saved_git_id = NULL; -} - -void set_git_id(const struct git_oid * id) -{ - static char git_id_buffer[GIT_OID_HEXSZ+1]; - - git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), id); - saved_git_id = git_id_buffer; -} - -static int do_git_load(git_repository *repo, const char *branch) -{ - int ret; - git_object *object; - git_commit *commit; - git_tree *tree; - - if (git_revparse_single(&object, repo, branch)) - return report_error("Unable to look up revision '%s'", branch); - if (git_object_peel((git_object **)&commit, object, GIT_OBJ_COMMIT)) - return report_error("Revision '%s' is not a valid commit", branch); - if (git_commit_tree(&tree, commit)) - return report_error("Could not look up tree of commit in branch '%s'", branch); - ret = load_dives_from_tree(repo, tree); - if (!ret) - set_git_id(git_commit_id(commit)); - git_object_free((git_object *)tree); - return ret; -} - -/* - * Like git_save_dives(), this silently returns a negative - * value if it's not a git repository at all (so that you - * can try to load it some other way. - * - * If it is a git repository, we return zero for success, - * or report an error and return 1 if the load failed. - */ -int git_load_dives(struct git_repository *repo, const char *branch) -{ - int ret; - - if (repo == dummy_git_repository) - return report_error("Unable to open git repository at '%s'", branch); - ret = do_git_load(repo, branch); - git_repository_free(repo); - free((void *)branch); - finish_active_dive(); - finish_active_trip(); - return ret; -} diff --git a/macos.c b/macos.c deleted file mode 100644 index aa2be4b3b..000000000 --- a/macos.c +++ /dev/null @@ -1,206 +0,0 @@ -/* macos.c */ -/* implements Mac OS X specific functions */ -#include -#include -#include -#include "dive.h" -#include "display.h" -#include -#include -#include -#include -#include -#include -#include - -void subsurface_user_info(struct user_info *info) -{ /* Nothing, let's use libgit2-20 on MacOS */ } - -/* macos defines CFSTR to create a CFString object from a constant, - * but no similar macros if a C string variable is supposed to be - * the argument. We add this here (hardcoding the default allocator - * and MacRoman encoding */ -#define CFSTR_VAR(_var) CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, \ - (_var), kCFStringEncodingMacRoman, \ - kCFAllocatorNull) - -#define SUBSURFACE_PREFERENCES CFSTR("org.hohndel.subsurface") -#define ICON_NAME "Subsurface.icns" -#define UI_FONT "Arial 12" - -const char mac_system_divelist_default_font[] = "Arial"; -const char *system_divelist_default_font = mac_system_divelist_default_font; -double system_divelist_default_font_size = -1.0; - -void subsurface_OS_pref_setup(void) -{ - // nothing -} - -bool subsurface_ignore_font(const char *font) -{ - // there are no old default fonts to ignore - return false; -} - -static const char *system_default_path_append(const char *append) -{ - const char *home = getenv("HOME"); - const char *path = "/Library/Application Support/Subsurface"; - - int len = strlen(home) + strlen(path) + 1; - if (append) - len += strlen(append) + 1; - - char *buffer = (char *)malloc(len); - memset(buffer, 0, len); - strcat(buffer, home); - strcat(buffer, path); - if (append) { - strcat(buffer, "/"); - strcat(buffer, append); - } - - return buffer; -} - -const char *system_default_directory(void) -{ - static const char *path = NULL; - if (!path) - path = system_default_path_append(NULL); - return path; -} - -const char *system_default_filename(void) -{ - static char *filename = NULL; - if (!filename) { - const char *user = getenv("LOGNAME"); - if (same_string(user, "")) - user = "username"; - filename = calloc(strlen(user) + 5, 1); - strcat(filename, user); - strcat(filename, ".xml"); - } - static const char *path = NULL; - if (!path) - path = system_default_path_append(filename); - return path; -} - -int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) -{ - int index = -1, entries = 0; - DIR *dp = NULL; - struct dirent *ep = NULL; - size_t i; - if (dc_type != DC_TYPE_UEMIS) { - const char *dirname = "/dev"; - const char *patterns[] = { - "tty.*", - "usbserial", - NULL - }; - - dp = opendir(dirname); - if (dp == NULL) { - return -1; - } - - while ((ep = readdir(dp)) != NULL) { - for (i = 0; patterns[i] != NULL; ++i) { - if (fnmatch(patterns[i], ep->d_name, 0) == 0) { - char filename[1024]; - int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); - if (n >= sizeof(filename)) { - closedir(dp); - return -1; - } - callback(filename, userdata); - if (is_default_dive_computer_device(filename)) - index = entries; - entries++; - break; - } - } - } - closedir(dp); - } - if (dc_type != DC_TYPE_SERIAL) { - const char *dirname = "/Volumes"; - int num_uemis = 0; - dp = opendir(dirname); - if (dp == NULL) { - return -1; - } - - while ((ep = readdir(dp)) != NULL) { - if (fnmatch("UEMISSDA", ep->d_name, 0) == 0) { - char filename[1024]; - int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); - if (n >= sizeof(filename)) { - closedir(dp); - return -1; - } - callback(filename, userdata); - if (is_default_dive_computer_device(filename)) - index = entries; - entries++; - num_uemis++; - break; - } - } - closedir(dp); - if (num_uemis == 1 && entries == 1) /* if we find exactly one entry and that's a Uemis, select it */ - index = 0; - } - return index; -} - -/* NOP wrappers to comform with windows.c */ -int subsurface_rename(const char *path, const char *newpath) -{ - return rename(path, newpath); -} - -int subsurface_open(const char *path, int oflags, mode_t mode) -{ - return open(path, oflags, mode); -} - -FILE *subsurface_fopen(const char *path, const char *mode) -{ - return fopen(path, mode); -} - -void *subsurface_opendir(const char *path) -{ - return (void *)opendir(path); -} - -int subsurface_access(const char *path, int mode) -{ - return access(path, mode); -} - -struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) -{ - return zip_open(path, flags, errorp); -} - -int subsurface_zip_close(struct zip *zip) -{ - return zip_close(zip); -} - -/* win32 console */ -void subsurface_console_init(bool dedicated) -{ - /* NOP */ -} - -void subsurface_console_exit(void) -{ - /* NOP */ -} diff --git a/membuffer.c b/membuffer.c deleted file mode 100644 index 2889a0cdc..000000000 --- a/membuffer.c +++ /dev/null @@ -1,285 +0,0 @@ -#include -#include -#include -#include - -#include "dive.h" -#include "membuffer.h" - -char *detach_buffer(struct membuffer *b) -{ - char *result = b->buffer; - b->buffer = NULL; - b->len = 0; - b->alloc = 0; - return result; -} - -void free_buffer(struct membuffer *b) -{ - free(detach_buffer(b)); -} - -void flush_buffer(struct membuffer *b, FILE *f) -{ - if (b->len) { - fwrite(b->buffer, 1, b->len, f); - free_buffer(b); - } -} - -void strip_mb(struct membuffer *b) -{ - while (b->len && isspace(b->buffer[b->len - 1])) - b->len--; -} - -/* - * Running out of memory isn't really an issue these days. - * So rather than do insane error handling and making the - * interface very complex, we'll just die. It won't happen - * unless you're running on a potato. - */ -static void oom(void) -{ - fprintf(stderr, "Out of memory\n"); - exit(1); -} - -static void make_room(struct membuffer *b, unsigned int size) -{ - unsigned int needed = b->len + size; - if (needed > b->alloc) { - char *n; - /* round it up to not reallocate all the time.. */ - needed = needed * 9 / 8 + 1024; - n = realloc(b->buffer, needed); - if (!n) - oom(); - b->buffer = n; - b->alloc = needed; - } -} - -const char *mb_cstring(struct membuffer *b) -{ - make_room(b, 1); - b->buffer[b->len] = 0; - return b->buffer; -} - -void put_bytes(struct membuffer *b, const char *str, int len) -{ - make_room(b, len); - memcpy(b->buffer + b->len, str, len); - b->len += len; -} - -void put_string(struct membuffer *b, const char *str) -{ - put_bytes(b, str, strlen(str)); -} - -void put_vformat(struct membuffer *b, const char *fmt, va_list args) -{ - int room = 128; - - for (;;) { - int len; - va_list copy; - char *target; - - make_room(b, room); - room = b->alloc - b->len; - target = b->buffer + b->len; - - va_copy(copy, args); - len = vsnprintf(target, room, fmt, copy); - va_end(copy); - - if (len < room) { - b->len += len; - return; - } - - room = len + 1; - } -} - -/* Silly helper using membuffer */ -char *vformat_string(const char *fmt, va_list args) -{ - struct membuffer mb = { 0 }; - put_vformat(&mb, fmt, args); - mb_cstring(&mb); - return detach_buffer(&mb); -} - -char *format_string(const char *fmt, ...) -{ - va_list args; - char *result; - - va_start(args, fmt); - result = vformat_string(fmt, args); - va_end(args); - return result; -} - -void put_format(struct membuffer *b, const char *fmt, ...) -{ - va_list args; - - va_start(args, fmt); - put_vformat(b, fmt, args); - va_end(args); -} - -void put_milli(struct membuffer *b, const char *pre, int value, const char *post) -{ - int i; - char buf[4]; - const char *sign = ""; - unsigned v; - - v = value; - if (value < 0) { - sign = "-"; - v = -value; - } - for (i = 2; i >= 0; i--) { - buf[i] = (v % 10) + '0'; - v /= 10; - } - buf[3] = 0; - if (buf[2] == '0') { - buf[2] = 0; - if (buf[1] == '0') - buf[1] = 0; - } - - put_format(b, "%s%s%u.%s%s", pre, sign, v, buf, post); -} - -void put_temperature(struct membuffer *b, temperature_t temp, const char *pre, const char *post) -{ - if (temp.mkelvin) - put_milli(b, pre, temp.mkelvin - ZERO_C_IN_MKELVIN, post); -} - -void put_depth(struct membuffer *b, depth_t depth, const char *pre, const char *post) -{ - if (depth.mm) - put_milli(b, pre, depth.mm, post); -} - -void put_duration(struct membuffer *b, duration_t duration, const char *pre, const char *post) -{ - if (duration.seconds) - put_format(b, "%s%u:%02u%s", pre, FRACTION(duration.seconds, 60), post); -} - -void put_pressure(struct membuffer *b, pressure_t pressure, const char *pre, const char *post) -{ - if (pressure.mbar) - put_milli(b, pre, pressure.mbar, post); -} - -void put_salinity(struct membuffer *b, int salinity, const char *pre, const char *post) -{ - if (salinity) - put_format(b, "%s%d%s", pre, salinity / 10, post); -} - -void put_degrees(struct membuffer *b, degrees_t value, const char *pre, const char *post) -{ - int udeg = value.udeg; - const char *sign = ""; - - if (udeg < 0) { - udeg = -udeg; - sign = "-"; - } - put_format(b, "%s%s%u.%06u%s", pre, sign, FRACTION(udeg, 1000000), post); -} - -void put_quoted(struct membuffer *b, const char *text, int is_attribute, int is_html) -{ - const char *p = text; - - for (;;) { - const char *escape; - - switch (*p++) { - default: - continue; - case 0: - escape = NULL; - break; - case 1 ... 8: - case 11: - case 12: - case 14 ... 31: - escape = "?"; - break; - case '<': - escape = "<"; - break; - case '>': - escape = ">"; - break; - case '&': - escape = "&"; - break; - case '\'': - if (!is_attribute) - continue; - escape = "'"; - break; - case '\"': - if (!is_attribute) - continue; - escape = """; - break; - case '\n': - if (!is_html) - continue; - else - escape = "
"; - } - put_bytes(b, text, (p - text - 1)); - if (!escape) - break; - put_string(b, escape); - text = p; - } -} - -char *add_to_string_va(const char *old, const char *fmt, va_list args) -{ - char *res; - struct membuffer o = { 0 }, n = { 0 }; - put_vformat(&n, fmt, args); - put_format(&o, "%s\n%s", old ?: "", mb_cstring(&n)); - res = strdup(mb_cstring(&o)); - free_buffer(&o); - free_buffer(&n); - free((void *)old); - return res; -} - -/* this is a convenience function that cleverly adds text to a string, using our membuffer - * infrastructure. - * WARNING - this will free(old), the intended pattern is - * string = add_to_string(string, fmt, ...) - */ -char *add_to_string(const char *old, const char *fmt, ...) -{ - char *res; - va_list args; - - va_start(args, fmt); - res = add_to_string_va(old, fmt, args); - va_end(args); - return res; -} diff --git a/membuffer.h b/membuffer.h deleted file mode 100644 index 434b34c71..000000000 --- a/membuffer.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef MEMBUFFER_H -#define MEMBUFFER_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include - -struct membuffer { - unsigned int len, alloc; - char *buffer; -}; - -#ifdef __GNUC__ -#define __printf(x, y) __attribute__((__format__(__printf__, x, y))) -#else -#define __printf(x, y) -#endif - -extern char *detach_buffer(struct membuffer *b); -extern void free_buffer(struct membuffer *); -extern void flush_buffer(struct membuffer *, FILE *); -extern void put_bytes(struct membuffer *, const char *, int); -extern void put_string(struct membuffer *, const char *); -extern void put_quoted(struct membuffer *, const char *, int, int); -extern void strip_mb(struct membuffer *); -extern const char *mb_cstring(struct membuffer *); -extern __printf(2, 0) void put_vformat(struct membuffer *, const char *, va_list); -extern __printf(2, 3) void put_format(struct membuffer *, const char *fmt, ...); -extern __printf(2, 0) char *add_to_string_va(const char *old, const char *fmt, va_list args); -extern __printf(2, 3) char *add_to_string(const char *old, const char *fmt, ...); - -/* Helpers that use membuffers internally */ -extern __printf(1, 0) char *vformat_string(const char *, va_list); -extern __printf(1, 2) char *format_string(const char *, ...); - - -/* Output one of our "milli" values with type and pre/post data */ -extern void put_milli(struct membuffer *, const char *, int, const char *); - -/* - * Helper functions for showing particular types. If the type - * is empty, nothing is done, and the function returns false. - * Otherwise, it returns true. - * - * The two "const char *" at the end are pre/post data. - * - * The reason for the pre/post data is so that you can easily - * prepend and append a string without having to test whether the - * type is empty. So - * - * put_temperature(b, temp, "Temp=", " C\n"); - * - * writes nothing to the buffer if there is no temperature data, - * but otherwise would a string that looks something like - * - * "Temp=28.1 C\n" - * - * to the memory buffer (typically the post/pre will be some XML - * pattern and unit string or whatever). - */ -extern void put_temperature(struct membuffer *, temperature_t, const char *, const char *); -extern void put_depth(struct membuffer *, depth_t, const char *, const char *); -extern void put_duration(struct membuffer *, duration_t, const char *, const char *); -extern void put_pressure(struct membuffer *, pressure_t, const char *, const char *); -extern void put_salinity(struct membuffer *, int, const char *, const char *); -extern void put_degrees(struct membuffer *b, degrees_t value, const char *, const char *); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/ostctools.c b/ostctools.c deleted file mode 100644 index 4b4cff241..000000000 --- a/ostctools.c +++ /dev/null @@ -1,193 +0,0 @@ -#include -#include -#include - -#include "dive.h" -#include "gettext.h" -#include "divelist.h" -#include "libdivecomputer.h" - -/* - * Returns a dc_descriptor_t structure based on dc model's number and family. - */ - -static dc_descriptor_t *ostc_get_data_descriptor(int data_model, dc_family_t data_fam) -{ - dc_descriptor_t *descriptor = NULL, *current = NULL; - ; - dc_iterator_t *iterator = NULL; - dc_status_t rc; - - rc = dc_descriptor_iterator(&iterator); - if (rc != DC_STATUS_SUCCESS) { - fprintf(stderr, "Error creating the device descriptor iterator.\n"); - return current; - } - while ((dc_iterator_next(iterator, &descriptor)) == DC_STATUS_SUCCESS) { - int desc_model = dc_descriptor_get_model(descriptor); - dc_family_t desc_fam = dc_descriptor_get_type(descriptor); - if (data_model == desc_model && data_fam == desc_fam) { - current = descriptor; - break; - } - dc_descriptor_free(descriptor); - } - dc_iterator_free(iterator); - return current; -} - -/* - * Fills a device_data_t structure with known dc data and a descriptor. - */ -static int ostc_prepare_data(int data_model, dc_family_t dc_fam, device_data_t *dev_data) -{ - dc_descriptor_t *data_descriptor; - - dev_data->device = NULL; - dev_data->context = NULL; - - data_descriptor = ostc_get_data_descriptor(data_model, dc_fam); - if (data_descriptor) { - dev_data->descriptor = data_descriptor; - dev_data->vendor = copy_string(data_descriptor->vendor); - dev_data->model = copy_string(data_descriptor->product); - } else { - return 0; - } - return 1; -} - -/* - * OSTCTools stores the raw dive data in heavily padded files, one dive - * each file. So it's not necesary to iterate once and again on a parsing - * function. Actually there's only one kind of archive for every DC model. - */ -void ostctools_import(const char *file, struct dive_table *divetable) -{ - FILE *archive; - device_data_t *devdata = calloc(1, sizeof(device_data_t)); - dc_family_t dc_fam; - unsigned char *buffer = calloc(65536, 1), *uc_tmp; - char *tmp; - struct dive *ostcdive = alloc_dive(); - dc_status_t rc = 0; - int model, ret, i = 0; - unsigned int serial; - struct extra_data *ptr; - - // Open the archive - if ((archive = subsurface_fopen(file, "rb")) == NULL) { - report_error(translate("gettextFromC", "Failed to read '%s'"), file); - free(ostcdive); - goto out; - } - - // Read dive number from the log - uc_tmp = calloc(2, 1); - fseek(archive, 258, 0); - fread(uc_tmp, 1, 2, archive); - ostcdive->number = uc_tmp[0] + (uc_tmp[1] << 8); - free(uc_tmp); - - // Read device's serial number - uc_tmp = calloc(2, 1); - fseek(archive, 265, 0); - fread(uc_tmp, 1, 2, archive); - serial = uc_tmp[0] + (uc_tmp[1] << 8); - free(uc_tmp); - - // Read dive's raw data, header + profile - fseek(archive, 456, 0); - while (!feof(archive)) { - fread(buffer + i, 1, 1, archive); - if (buffer[i] == 0xFD && buffer[i - 1] == 0xFD) - break; - i++; - } - - // Try to determine the dc family based on the header type - if (buffer[2] == 0x20 || buffer[2] == 0x21) { - dc_fam = DC_FAMILY_HW_OSTC; - } else { - switch (buffer[8]) { - case 0x22: - dc_fam = DC_FAMILY_HW_FROG; - break; - case 0x23: - dc_fam = DC_FAMILY_HW_OSTC3; - break; - default: - report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number); - free(ostcdive); - fclose(archive); - goto out; - } - } - - // Try to determine the model based on serial number - switch (dc_fam) { - case DC_FAMILY_HW_OSTC: - if (serial > 7000) - model = 3; //2C - else if (serial > 2048) - model = 2; //2N - else if (serial > 300) - model = 1; //MK2 - else - model = 0; //OSTC - break; - case DC_FAMILY_HW_FROG: - model = 0; - break; - default: - if (serial > 10000) - model = 0x12; //Sport - else - model = 0x0A; //OSTC3 - } - - // Prepare data to pass to libdivecomputer. - ret = ostc_prepare_data(model, dc_fam, devdata); - if (ret == 0) { - report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number); - free(ostcdive); - fclose(archive); - goto out; - } - tmp = calloc(strlen(devdata->vendor) + strlen(devdata->model) + 28, 1); - sprintf(tmp, "%s %s (Imported from OSTCTools)", devdata->vendor, devdata->model); - ostcdive->dc.model = copy_string(tmp); - free(tmp); - - // Parse the dive data - rc = libdc_buffer_parser(ostcdive, devdata, buffer, i + 1); - if (rc != DC_STATUS_SUCCESS) - report_error(translate("gettextFromC", "Error - %s - parsing dive %d"), errmsg(rc), ostcdive->number); - - // Serial number is not part of the header nor the profile, so libdc won't - // catch it. If Serial is part of the extra_data, and set to zero, remove - // it from the list and add again. - tmp = calloc(12, 1); - sprintf(tmp, "%d", serial); - ostcdive->dc.serial = copy_string(tmp); - free(tmp); - - if (ostcdive->dc.extra_data) { - ptr = ostcdive->dc.extra_data; - while (strcmp(ptr->key, "Serial")) - ptr = ptr->next; - if (!strcmp(ptr->value, "0")) { - add_extra_data(&ostcdive->dc, "Serial", ostcdive->dc.serial); - *ptr = *(ptr)->next; - } - } else { - add_extra_data(&ostcdive->dc, "Serial", ostcdive->dc.serial); - } - record_dive_to_table(ostcdive, divetable); - mark_divelist_changed(true); - sort_table(divetable); - fclose(archive); -out: - free(devdata); - free(buffer); -} diff --git a/parse-xml.c b/parse-xml.c deleted file mode 100644 index 3d86222b9..000000000 --- a/parse-xml.c +++ /dev/null @@ -1,3641 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#define __USE_XOPEN -#include -#include -#include -#include -#include -#include - -#include "gettext.h" - -#include "dive.h" -#include "divelist.h" -#include "device.h" -#include "membuffer.h" - -int verbose, quit; -int metric = 1; -int last_xml_version = -1; -int diveid = -1; - -static xmlDoc *test_xslt_transforms(xmlDoc *doc, const char **params); - -/* the dive table holds the overall dive list; target table points at - * the table we are currently filling */ -struct dive_table dive_table; -struct dive_table *target_table = NULL; - -/* Trim a character string by removing leading and trailing white space characters. - * Parameter: a pointer to a null-terminated character string (buffer); - * Return value: length of the trimmed string, excluding the terminal 0x0 byte - * The original pointer (buffer) remains valid after this function has been called - * and points to the trimmed string */ -int trimspace(char *buffer) { - int i, size, start, end; - size = strlen(buffer); - for(start = 0; isspace(buffer[start]); start++) - if (start >= size) return 0; // Find 1st character following leading whitespace - for(end = size - 1; isspace(buffer[end]); end--) // Find last character before trailing whitespace - if (end <= 0) return 0; - for(i = start; i <= end; i++) // Move the nonspace characters to the start of the string - buffer[i-start] = buffer[i]; - size = end - start + 1; - buffer[size] = 0x0; // then terminate the string - return size; // return string length -} - -/* - * Clear a dive_table - */ -void clear_table(struct dive_table *table) -{ - for (int i = 0; i < table->nr; i++) - free(table->dives[i]); - table->nr = 0; -} - -/* - * Add a dive into the dive_table array - */ -void record_dive_to_table(struct dive *dive, struct dive_table *table) -{ - assert(table != NULL); - struct dive **dives = grow_dive_table(table); - int nr = table->nr; - - dives[nr] = fixup_dive(dive); - table->nr = nr + 1; -} - -void record_dive(struct dive *dive) -{ - record_dive_to_table(dive, &dive_table); -} - -static void start_match(const char *type, const char *name, char *buffer) -{ - if (verbose > 2) - printf("Matching %s '%s' (%s)\n", - type, name, buffer); -} - -static void nonmatch(const char *type, const char *name, char *buffer) -{ - if (verbose > 1) - printf("Unable to match %s '%s' (%s)\n", - type, name, buffer); -} - -typedef void (*matchfn_t)(char *buffer, void *); - -static int match(const char *pattern, int plen, - const char *name, - matchfn_t fn, char *buf, void *data) -{ - switch (name[plen]) { - case '\0': - case '.': - break; - default: - return 0; - } - if (memcmp(pattern, name, plen)) - return 0; - fn(buf, data); - return 1; -} - - -struct units xml_parsing_units; -const struct units SI_units = SI_UNITS; -const struct units IMPERIAL_units = IMPERIAL_UNITS; - -/* - * Dive info as it is being built up.. - */ -#define MAX_EVENT_NAME 128 -static struct divecomputer *cur_dc; -static struct dive *cur_dive; -static struct dive_site *cur_dive_site; -degrees_t cur_latitude, cur_longitude; -static dive_trip_t *cur_trip = NULL; -static struct sample *cur_sample; -static struct picture *cur_picture; -static union { - struct event event; - char allocation[sizeof(struct event)+MAX_EVENT_NAME]; -} event_allocation = { .event.deleted = 1 }; -#define cur_event event_allocation.event -static struct { - struct { - const char *model; - uint32_t deviceid; - const char *nickname, *serial_nr, *firmware; - } dc; -} cur_settings; -static bool in_settings = false; -static bool in_userid = false; -static struct tm cur_tm; -static int cur_cylinder_index, cur_ws_index; -static int lastndl, laststoptime, laststopdepth, lastcns, lastpo2, lastindeco; -static int lastcylinderindex, lastsensor, next_o2_sensor; -static struct extra_data cur_extra_data; - -/* - * If we don't have an explicit dive computer, - * we use the implicit one that every dive has.. - */ -static struct divecomputer *get_dc(void) -{ - return cur_dc ?: &cur_dive->dc; -} - -static enum import_source { - UNKNOWN, - LIBDIVECOMPUTER, - DIVINGLOG, - UDDF, - SSRF_WS, -} import_source; - -static void divedate(const char *buffer, timestamp_t *when) -{ - int d, m, y; - int hh, mm, ss; - - hh = 0; - mm = 0; - ss = 0; - if (sscanf(buffer, "%d.%d.%d %d:%d:%d", &d, &m, &y, &hh, &mm, &ss) >= 3) { - /* This is ok, and we got at least the date */ - } else if (sscanf(buffer, "%d-%d-%d %d:%d:%d", &y, &m, &d, &hh, &mm, &ss) >= 3) { - /* This is also ok */ - } else { - fprintf(stderr, "Unable to parse date '%s'\n", buffer); - return; - } - cur_tm.tm_year = y; - cur_tm.tm_mon = m - 1; - cur_tm.tm_mday = d; - cur_tm.tm_hour = hh; - cur_tm.tm_min = mm; - cur_tm.tm_sec = ss; - - *when = utc_mktime(&cur_tm); -} - -static void divetime(const char *buffer, timestamp_t *when) -{ - int h, m, s = 0; - - if (sscanf(buffer, "%d:%d:%d", &h, &m, &s) >= 2) { - cur_tm.tm_hour = h; - cur_tm.tm_min = m; - cur_tm.tm_sec = s; - *when = utc_mktime(&cur_tm); - } -} - -/* Libdivecomputer: "2011-03-20 10:22:38" */ -static void divedatetime(char *buffer, timestamp_t *when) -{ - int y, m, d; - int hr, min, sec; - - if (sscanf(buffer, "%d-%d-%d %d:%d:%d", - &y, &m, &d, &hr, &min, &sec) == 6) { - cur_tm.tm_year = y; - cur_tm.tm_mon = m - 1; - cur_tm.tm_mday = d; - cur_tm.tm_hour = hr; - cur_tm.tm_min = min; - cur_tm.tm_sec = sec; - *when = utc_mktime(&cur_tm); - } -} - -enum ParseState { - FINDSTART, - FINDEND -}; -static void divetags(char *buffer, struct tag_entry **tags) -{ - int i = 0, start = 0, end = 0; - enum ParseState state = FINDEND; - int len = buffer ? strlen(buffer) : 0; - - while (i < len) { - if (buffer[i] == ',') { - if (state == FINDSTART) { - /* Detect empty tags */ - } else if (state == FINDEND) { - /* Found end of tag */ - if (i > 0 && buffer[i - 1] != '\\') { - buffer[i] = '\0'; - state = FINDSTART; - taglist_add_tag(tags, buffer + start); - } else { - state = FINDSTART; - } - } - } else if (buffer[i] == ' ') { - /* Handled */ - } else { - /* Found start of tag */ - if (state == FINDSTART) { - state = FINDEND; - start = i; - } else if (state == FINDEND) { - end = i; - } - } - i++; - } - if (state == FINDEND) { - if (end < start) - end = len - 1; - if (len > 0) { - buffer[end + 1] = '\0'; - taglist_add_tag(tags, buffer + start); - } - } -} - -enum number_type { - NEITHER, - FLOAT -}; - -static enum number_type parse_float(const char *buffer, double *res, const char **endp) -{ - double val; - static bool first_time = true; - - errno = 0; - val = ascii_strtod(buffer, endp); - if (errno || *endp == buffer) - return NEITHER; - if (**endp == ',') { - if (IS_FP_SAME(val, rint(val))) { - /* we really want to send an error if this is a Subsurface native file - * as this is likely indication of a bug - but right now we don't have - * that information available */ - if (first_time) { - fprintf(stderr, "Floating point value with decimal comma (%s)?\n", buffer); - first_time = false; - } - /* Try again in permissive mode*/ - val = strtod_flags(buffer, endp, 0); - } - } - - *res = val; - return FLOAT; -} - -union int_or_float { - double fp; -}; - -static enum number_type integer_or_float(char *buffer, union int_or_float *res) -{ - const char *end; - return parse_float(buffer, &res->fp, &end); -} - -static void pressure(char *buffer, pressure_t *pressure) -{ - double mbar = 0.0; - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - /* Just ignore zero values */ - if (!val.fp) - break; - switch (xml_parsing_units.pressure) { - case PASCAL: - mbar = val.fp / 100; - break; - case BAR: - /* Assume mbar, but if it's really small, it's bar */ - mbar = val.fp; - if (fabs(mbar) < 5000) - mbar = mbar * 1000; - break; - case PSI: - mbar = psi_to_mbar(val.fp); - break; - } - if (fabs(mbar) > 5 && fabs(mbar) < 5000000) { - pressure->mbar = rint(mbar); - break; - } - /* fallthrough */ - default: - printf("Strange pressure reading %s\n", buffer); - } -} - -static void cylinder_use(char *buffer, enum cylinderuse *cyl_use) -{ - if (trimspace(buffer)) - *cyl_use = cylinderuse_from_text(buffer); -} - -static void salinity(char *buffer, int *salinity) -{ - union int_or_float val; - switch (integer_or_float(buffer, &val)) { - case FLOAT: - *salinity = rint(val.fp * 10.0); - break; - default: - printf("Strange salinity reading %s\n", buffer); - } -} - -static void depth(char *buffer, depth_t *depth) -{ - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - switch (xml_parsing_units.length) { - case METERS: - depth->mm = rint(val.fp * 1000); - break; - case FEET: - depth->mm = feet_to_mm(val.fp); - break; - } - break; - default: - printf("Strange depth reading %s\n", buffer); - } -} - -static void extra_data_start(void) -{ - memset(&cur_extra_data, 0, sizeof(struct extra_data)); -} - -static void extra_data_end(void) -{ - // don't save partial structures - we must have both key and value - if (cur_extra_data.key && cur_extra_data.value) - add_extra_data(cur_dc, cur_extra_data.key, cur_extra_data.value); -} - -static void weight(char *buffer, weight_t *weight) -{ - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - switch (xml_parsing_units.weight) { - case KG: - weight->grams = rint(val.fp * 1000); - break; - case LBS: - weight->grams = lbs_to_grams(val.fp); - break; - } - break; - default: - printf("Strange weight reading %s\n", buffer); - } -} - -static void temperature(char *buffer, temperature_t *temperature) -{ - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - switch (xml_parsing_units.temperature) { - case KELVIN: - temperature->mkelvin = val.fp * 1000; - break; - case CELSIUS: - temperature->mkelvin = C_to_mkelvin(val.fp); - break; - case FAHRENHEIT: - temperature->mkelvin = F_to_mkelvin(val.fp); - break; - } - break; - default: - printf("Strange temperature reading %s\n", buffer); - } - /* temperatures outside -40C .. +70C should be ignored */ - if (temperature->mkelvin < ZERO_C_IN_MKELVIN - 40000 || - temperature->mkelvin > ZERO_C_IN_MKELVIN + 70000) - temperature->mkelvin = 0; -} - -static void sampletime(char *buffer, duration_t *time) -{ - int i; - int min, sec; - - i = sscanf(buffer, "%d:%d", &min, &sec); - switch (i) { - case 1: - sec = min; - min = 0; - /* fallthrough */ - case 2: - time->seconds = sec + min * 60; - break; - default: - printf("Strange sample time reading %s\n", buffer); - } -} - -static void offsettime(char *buffer, offset_t *time) -{ - duration_t uoffset; - int sign = 1; - if (*buffer == '-') { - sign = -1; - buffer++; - } - /* yes, this could indeed fail if we have an offset > 34yrs - * - too bad */ - sampletime(buffer, &uoffset); - time->seconds = sign * uoffset.seconds; -} - -static void duration(char *buffer, duration_t *time) -{ - /* DivingLog 5.08 (and maybe other versions) appear to sometimes - * store the dive time as 44.00 instead of 44:00; - * This attempts to parse this in a fairly robust way */ - if (!strchr(buffer, ':') && strchr(buffer, '.')) { - char *mybuffer = strdup(buffer); - char *dot = strchr(mybuffer, '.'); - *dot = ':'; - sampletime(mybuffer, time); - free(mybuffer); - } else { - sampletime(buffer, time); - } -} - -static void percent(char *buffer, fraction_t *fraction) -{ - double val; - const char *end; - - switch (parse_float(buffer, &val, &end)) { - case FLOAT: - /* Turn fractions into percent unless explicit.. */ - if (val <= 1.0) { - while (isspace(*end)) - end++; - if (*end != '%') - val *= 100; - } - - /* Then turn percent into our integer permille format */ - if (val >= 0 && val <= 100.0) { - fraction->permille = rint(val * 10); - break; - } - default: - printf(translate("gettextFromC", "Strange percentage reading %s\n"), buffer); - break; - } -} - -static void gasmix(char *buffer, fraction_t *fraction) -{ - /* libdivecomputer does negative percentages. */ - if (*buffer == '-') - return; - if (cur_cylinder_index < MAX_CYLINDERS) - percent(buffer, fraction); -} - -static void gasmix_nitrogen(char *buffer, struct gasmix *gasmix) -{ - /* Ignore n2 percentages. There's no value in them. */ -} - -static void cylindersize(char *buffer, volume_t *volume) -{ - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - volume->mliter = rint(val.fp * 1000); - break; - - default: - printf("Strange volume reading %s\n", buffer); - break; - } -} - -static void utf8_string(char *buffer, void *_res) -{ - char **res = _res; - int size; - size = trimspace(buffer); - if(size) - *res = strdup(buffer); -} - -static void event_name(char *buffer, char *name) -{ - int size = trimspace(buffer); - if (size >= MAX_EVENT_NAME) - size = MAX_EVENT_NAME-1; - memcpy(name, buffer, size); - name[size] = 0; -} - -// We don't use gauge as a mode, and pscr doesn't exist as a libdc divemode -const char *libdc_divemode_text[] = { "oc", "cc", "pscr", "freedive", "gauge"}; - -/* Extract the dive computer type from the xml text buffer */ -static void get_dc_type(char *buffer, enum dive_comp_type *dct) -{ - if (trimspace(buffer)) { - for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) { - if (strcmp(buffer, divemode_text[i]) == 0) - *dct = i; - else if (strcmp(buffer, libdc_divemode_text[i]) == 0) - *dct = i; - } - } -} - -#define MATCH(pattern, fn, dest) ({ \ - /* Silly type compatibility test */ \ - if (0) (fn)("test", dest); \ - match(pattern, strlen(pattern), name, (matchfn_t) (fn), buf, dest); }) - -static void get_index(char *buffer, int *i) -{ - *i = atoi(buffer); -} - -static void get_uint8(char *buffer, uint8_t *i) -{ - *i = atoi(buffer); -} - -static void get_bearing(char *buffer, bearing_t *bearing) -{ - bearing->degrees = atoi(buffer); -} - -static void get_rating(char *buffer, int *i) -{ - int j = atoi(buffer); - if (j >= 0 && j <= 5) { - *i = j; - } -} - -static void double_to_o2pressure(char *buffer, o2pressure_t *i) -{ - i->mbar = rint(ascii_strtod(buffer, NULL) * 1000.0); -} - -static void hex_value(char *buffer, uint32_t *i) -{ - *i = strtoul(buffer, NULL, 16); -} - -static void get_tripflag(char *buffer, tripflag_t *tf) -{ - *tf = strcmp(buffer, "NOTRIP") ? TF_NONE : NO_TRIP; -} - -/* - * Divinglog is crazy. The temperatures are in celsius. EXCEPT - * for the sample temperatures, that are in Fahrenheit. - * WTF? - * - * Oh, and I think Diving Log *internally* probably kept them - * in celsius, because I'm seeing entries like - * - * 32.0 - * - * in there. Which is freezing, aka 0 degC. I bet the "0" is - * what Diving Log uses for "no temperature". - * - * So throw away crap like that. - * - * It gets worse. Sometimes the sample temperatures are in - * Celsius, which apparently happens if you are in a SI - * locale. So we now do: - * - * - temperatures < 32.0 == Celsius - * - temperature == 32.0 -> garbage, it's a missing temperature (zero converted from C to F) - * - temperatures > 32.0 == Fahrenheit - */ -static void fahrenheit(char *buffer, temperature_t *temperature) -{ - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - if (IS_FP_SAME(val.fp, 32.0)) - break; - if (val.fp < 32.0) - temperature->mkelvin = C_to_mkelvin(val.fp); - else - temperature->mkelvin = F_to_mkelvin(val.fp); - break; - default: - fprintf(stderr, "Crazy Diving Log temperature reading %s\n", buffer); - } -} - -/* - * Did I mention how bat-shit crazy divinglog is? The sample - * pressures are in PSI. But the tank working pressure is in - * bar. WTF^2? - * - * Crazy stuff like this is why subsurface has everything in - * these inconvenient typed structures, and you have to say - * "pressure->mbar" to get the actual value. Exactly so that - * you can never have unit confusion. - * - * It gets worse: sometimes apparently the pressures are in - * bar, sometimes in psi. Dirk suspects that this may be a - * DivingLog Uemis importer bug, and that they are always - * supposed to be in bar, but that the importer got the - * sample importing wrong. - * - * Sadly, there's no way to really tell. So I think we just - * have to have some arbitrary cut-off point where we assume - * that smaller values mean bar.. Not good. - */ -static void psi_or_bar(char *buffer, pressure_t *pressure) -{ - union int_or_float val; - - switch (integer_or_float(buffer, &val)) { - case FLOAT: - if (val.fp > 400) - pressure->mbar = psi_to_mbar(val.fp); - else - pressure->mbar = rint(val.fp * 1000); - break; - default: - fprintf(stderr, "Crazy Diving Log PSI reading %s\n", buffer); - } -} - -static int divinglog_fill_sample(struct sample *sample, const char *name, char *buf) -{ - return MATCH("time.p", sampletime, &sample->time) || - MATCH("depth.p", depth, &sample->depth) || - MATCH("temp.p", fahrenheit, &sample->temperature) || - MATCH("press1.p", psi_or_bar, &sample->cylinderpressure) || - 0; -} - -static void uddf_gasswitch(char *buffer, struct sample *sample) -{ - int idx = atoi(buffer); - int seconds = sample->time.seconds; - struct dive *dive = cur_dive; - struct divecomputer *dc = get_dc(); - - add_gas_switch_event(dive, dc, seconds, idx); -} - -static int uddf_fill_sample(struct sample *sample, const char *name, char *buf) -{ - return MATCH("divetime", sampletime, &sample->time) || - MATCH("depth", depth, &sample->depth) || - MATCH("temperature", temperature, &sample->temperature) || - MATCH("tankpressure", pressure, &sample->cylinderpressure) || - MATCH("ref.switchmix", uddf_gasswitch, sample) || - 0; -} - -static void eventtime(char *buffer, duration_t *duration) -{ - sampletime(buffer, duration); - if (cur_sample) - duration->seconds += cur_sample->time.seconds; -} - -static void try_to_match_autogroup(const char *name, char *buf) -{ - int autogroupvalue; - - start_match("autogroup", name, buf); - if (MATCH("state.autogroup", get_index, &autogroupvalue)) { - set_autogroup(autogroupvalue); - return; - } - nonmatch("autogroup", name, buf); -} - -void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int seconds, int idx) -{ - /* sanity check so we don't crash */ - if (idx < 0 || idx >= MAX_CYLINDERS) - return; - /* The gas switch event format is insane for historical reasons */ - struct gasmix *mix = &dive->cylinder[idx].gasmix; - int o2 = get_o2(mix); - int he = get_he(mix); - struct event *ev; - int value; - - o2 = (o2 + 5) / 10; - he = (he + 5) / 10; - value = o2 + (he << 16); - - ev = add_event(dc, seconds, he ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE, 0, value, "gaschange"); - if (ev) { - ev->gas.index = idx; - ev->gas.mix = *mix; - } -} - -static void get_cylinderindex(char *buffer, uint8_t *i) -{ - *i = atoi(buffer); - if (lastcylinderindex != *i) { - add_gas_switch_event(cur_dive, get_dc(), cur_sample->time.seconds, *i); - lastcylinderindex = *i; - } -} - -static void get_sensor(char *buffer, uint8_t *i) -{ - *i = atoi(buffer); - lastsensor = *i; -} - -static void parse_libdc_deco(char *buffer, struct sample *s) -{ - if (strcmp(buffer, "deco") == 0) { - s->in_deco = true; - } else if (strcmp(buffer, "ndl") == 0) { - s->in_deco = false; - // The time wasn't stoptime, it was ndl - s->ndl = s->stoptime; - s->stoptime.seconds = 0; - } -} - -static void try_to_fill_dc_settings(const char *name, char *buf) -{ - start_match("divecomputerid", name, buf); - if (MATCH("model.divecomputerid", utf8_string, &cur_settings.dc.model)) - return; - if (MATCH("deviceid.divecomputerid", hex_value, &cur_settings.dc.deviceid)) - return; - if (MATCH("nickname.divecomputerid", utf8_string, &cur_settings.dc.nickname)) - return; - if (MATCH("serial.divecomputerid", utf8_string, &cur_settings.dc.serial_nr)) - return; - if (MATCH("firmware.divecomputerid", utf8_string, &cur_settings.dc.firmware)) - return; - - nonmatch("divecomputerid", name, buf); -} - -static void try_to_fill_event(const char *name, char *buf) -{ - start_match("event", name, buf); - if (MATCH("event", event_name, cur_event.name)) - return; - if (MATCH("name", event_name, cur_event.name)) - return; - if (MATCH("time", eventtime, &cur_event.time)) - return; - if (MATCH("type", get_index, &cur_event.type)) - return; - if (MATCH("flags", get_index, &cur_event.flags)) - return; - if (MATCH("value", get_index, &cur_event.value)) - return; - if (MATCH("cylinder", get_index, &cur_event.gas.index)) { - /* We add one to indicate that we got an actual cylinder index value */ - cur_event.gas.index++; - return; - } - if (MATCH("o2", percent, &cur_event.gas.mix.o2)) - return; - if (MATCH("he", percent, &cur_event.gas.mix.he)) - return; - nonmatch("event", name, buf); -} - -static int match_dc_data_fields(struct divecomputer *dc, const char *name, char *buf) -{ - if (MATCH("maxdepth", depth, &dc->maxdepth)) - return 1; - if (MATCH("meandepth", depth, &dc->meandepth)) - return 1; - if (MATCH("max.depth", depth, &dc->maxdepth)) - return 1; - if (MATCH("mean.depth", depth, &dc->meandepth)) - return 1; - if (MATCH("duration", duration, &dc->duration)) - return 1; - if (MATCH("divetime", duration, &dc->duration)) - return 1; - if (MATCH("divetimesec", duration, &dc->duration)) - return 1; - if (MATCH("surfacetime", duration, &dc->surfacetime)) - return 1; - if (MATCH("airtemp", temperature, &dc->airtemp)) - return 1; - if (MATCH("watertemp", temperature, &dc->watertemp)) - return 1; - if (MATCH("air.temperature", temperature, &dc->airtemp)) - return 1; - if (MATCH("water.temperature", temperature, &dc->watertemp)) - return 1; - if (MATCH("pressure.surface", pressure, &dc->surface_pressure)) - return 1; - if (MATCH("salinity.water", salinity, &dc->salinity)) - return 1; - if (MATCH("key.extradata", utf8_string, &cur_extra_data.key)) - return 1; - if (MATCH("value.extradata", utf8_string, &cur_extra_data.value)) - return 1; - if (MATCH("divemode", get_dc_type, &dc->divemode)) - return 1; - if (MATCH("salinity", salinity, &dc->salinity)) - return 1; - if (MATCH("atmospheric", pressure, &dc->surface_pressure)) - return 1; - return 0; -} - -/* We're in the top-level dive xml. Try to convert whatever value to a dive value */ -static void try_to_fill_dc(struct divecomputer *dc, const char *name, char *buf) -{ - start_match("divecomputer", name, buf); - - if (MATCH("date", divedate, &dc->when)) - return; - if (MATCH("time", divetime, &dc->when)) - return; - if (MATCH("model", utf8_string, &dc->model)) - return; - if (MATCH("deviceid", hex_value, &dc->deviceid)) - return; - if (MATCH("diveid", hex_value, &dc->diveid)) - return; - if (MATCH("dctype", get_dc_type, &dc->divemode)) - return; - if (MATCH("no_o2sensors", get_sensor, &dc->no_o2sensors)) - return; - if (match_dc_data_fields(dc, name, buf)) - return; - - nonmatch("divecomputer", name, buf); -} - -/* We're in samples - try to convert the random xml value to something useful */ -static void try_to_fill_sample(struct sample *sample, const char *name, char *buf) -{ - int in_deco; - - start_match("sample", name, buf); - if (MATCH("pressure.sample", pressure, &sample->cylinderpressure)) - return; - if (MATCH("cylpress.sample", pressure, &sample->cylinderpressure)) - return; - if (MATCH("pdiluent.sample", pressure, &sample->cylinderpressure)) - return; - if (MATCH("o2pressure.sample", pressure, &sample->o2cylinderpressure)) - return; - if (MATCH("cylinderindex.sample", get_cylinderindex, &sample->sensor)) - return; - if (MATCH("sensor.sample", get_sensor, &sample->sensor)) - return; - if (MATCH("depth.sample", depth, &sample->depth)) - return; - if (MATCH("temp.sample", temperature, &sample->temperature)) - return; - if (MATCH("temperature.sample", temperature, &sample->temperature)) - return; - if (MATCH("sampletime.sample", sampletime, &sample->time)) - return; - if (MATCH("time.sample", sampletime, &sample->time)) - return; - if (MATCH("ndl.sample", sampletime, &sample->ndl)) - return; - if (MATCH("tts.sample", sampletime, &sample->tts)) - return; - if (MATCH("in_deco.sample", get_index, &in_deco)) { - sample->in_deco = (in_deco == 1); - return; - } - if (MATCH("stoptime.sample", sampletime, &sample->stoptime)) - return; - if (MATCH("stopdepth.sample", depth, &sample->stopdepth)) - return; - if (MATCH("cns.sample", get_uint8, &sample->cns)) - return; - if (MATCH("rbt.sample", sampletime, &sample->rbt)) - return; - if (MATCH("sensor1.sample", double_to_o2pressure, &sample->o2sensor[0])) // CCR O2 sensor data - return; - if (MATCH("sensor2.sample", double_to_o2pressure, &sample->o2sensor[1])) - return; - if (MATCH("sensor3.sample", double_to_o2pressure, &sample->o2sensor[2])) // up to 3 CCR sensors - return; - if (MATCH("po2.sample", double_to_o2pressure, &sample->setpoint)) - return; - if (MATCH("heartbeat", get_uint8, &sample->heartbeat)) - return; - if (MATCH("bearing", get_bearing, &sample->bearing)) - return; - if (MATCH("setpoint.sample", double_to_o2pressure, &sample->setpoint)) - return; - if (MATCH("ppo2.sample", double_to_o2pressure, &sample->o2sensor[next_o2_sensor])) { - next_o2_sensor++; - return; - } - if (MATCH("deco.sample", parse_libdc_deco, sample)) - return; - if (MATCH("time.deco", sampletime, &sample->stoptime)) - return; - if (MATCH("depth.deco", depth, &sample->stopdepth)) - return; - - switch (import_source) { - case DIVINGLOG: - if (divinglog_fill_sample(sample, name, buf)) - return; - break; - - case UDDF: - if (uddf_fill_sample(sample, name, buf)) - return; - break; - - default: - break; - } - - nonmatch("sample", name, buf); -} - -void try_to_fill_userid(const char *name, char *buf) -{ - if (prefs.save_userid_local) - set_userid(buf); -} - -static const char *country, *city; - -static void divinglog_place(char *place, uint32_t *uuid) -{ - char buffer[1024]; - - snprintf(buffer, sizeof(buffer), - "%s%s%s%s%s", - place, - city ? ", " : "", - city ? city : "", - country ? ", " : "", - country ? country : ""); - *uuid = get_dive_site_uuid_by_name(buffer, NULL); - if (*uuid == 0) - *uuid = create_dive_site(buffer, cur_dive->when); - - city = NULL; - country = NULL; -} - -static int divinglog_dive_match(struct dive *dive, const char *name, char *buf) -{ - return MATCH("divedate", divedate, &dive->when) || - MATCH("entrytime", divetime, &dive->when) || - MATCH("divetime", duration, &dive->dc.duration) || - MATCH("depth", depth, &dive->dc.maxdepth) || - MATCH("depthavg", depth, &dive->dc.meandepth) || - MATCH("tanktype", utf8_string, &dive->cylinder[0].type.description) || - MATCH("tanksize", cylindersize, &dive->cylinder[0].type.size) || - MATCH("presw", pressure, &dive->cylinder[0].type.workingpressure) || - MATCH("press", pressure, &dive->cylinder[0].start) || - MATCH("prese", pressure, &dive->cylinder[0].end) || - MATCH("comments", utf8_string, &dive->notes) || - MATCH("names.buddy", utf8_string, &dive->buddy) || - MATCH("name.country", utf8_string, &country) || - MATCH("name.city", utf8_string, &city) || - MATCH("name.place", divinglog_place, &dive->dive_site_uuid) || - 0; -} - -/* - * Uddf specifies ISO 8601 time format. - * - * There are many variations on that. This handles the useful cases. - */ -static void uddf_datetime(char *buffer, timestamp_t *when) -{ - char c; - int y, m, d, hh, mm, ss; - struct tm tm = { 0 }; - int i; - - i = sscanf(buffer, "%d-%d-%d%c%d:%d:%d", &y, &m, &d, &c, &hh, &mm, &ss); - if (i == 7) - goto success; - ss = 0; - if (i == 6) - goto success; - - i = sscanf(buffer, "%04d%02d%02d%c%02d%02d%02d", &y, &m, &d, &c, &hh, &mm, &ss); - if (i == 7) - goto success; - ss = 0; - if (i == 6) - goto success; -bad_date: - printf("Bad date time %s\n", buffer); - return; - -success: - if (c != 'T' && c != ' ') - goto bad_date; - tm.tm_year = y; - tm.tm_mon = m - 1; - tm.tm_mday = d; - tm.tm_hour = hh; - tm.tm_min = mm; - tm.tm_sec = ss; - *when = utc_mktime(&tm); -} - -#define uddf_datedata(name, offset) \ - static void uddf_##name(char *buffer, timestamp_t *when) \ - { \ - cur_tm.tm_##name = atoi(buffer) + offset; \ - *when = utc_mktime(&cur_tm); \ - } - -uddf_datedata(year, 0) -uddf_datedata(mon, -1) -uddf_datedata(mday, 0) -uddf_datedata(hour, 0) -uddf_datedata(min, 0) - -static int uddf_dive_match(struct dive *dive, const char *name, char *buf) -{ - return MATCH("datetime", uddf_datetime, &dive->when) || - MATCH("diveduration", duration, &dive->dc.duration) || - MATCH("greatestdepth", depth, &dive->dc.maxdepth) || - MATCH("year.date", uddf_year, &dive->when) || - MATCH("month.date", uddf_mon, &dive->when) || - MATCH("day.date", uddf_mday, &dive->when) || - MATCH("hour.time", uddf_hour, &dive->when) || - MATCH("minute.time", uddf_min, &dive->when) || - 0; -} - -/* - * This parses "floating point" into micro-degrees. - * We don't do exponentials etc, if somebody does - * GPS locations in that format, they are insane. - */ -degrees_t parse_degrees(char *buf, char **end) -{ - int sign = 1, decimals = 6, value = 0; - degrees_t ret; - - while (isspace(*buf)) - buf++; - switch (*buf) { - case '-': - sign = -1; - /* fallthrough */ - case '+': - buf++; - } - while (isdigit(*buf)) { - value = 10 * value + *buf - '0'; - buf++; - } - - /* Get the first six decimals if they exist */ - if (*buf == '.') - buf++; - do { - value *= 10; - if (isdigit(*buf)) { - value += *buf - '0'; - buf++; - } - } while (--decimals); - - /* Rounding */ - switch (*buf) { - case '5' ... '9': - value++; - } - while (isdigit(*buf)) - buf++; - - *end = buf; - ret.udeg = value * sign; - return ret; -} - -static void gps_lat(char *buffer, struct dive *dive) -{ - char *end; - degrees_t latitude = parse_degrees(buffer, &end); - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!ds) { - dive->dive_site_uuid = create_dive_site_with_gps(NULL, latitude, (degrees_t){0}, dive->when); - } else { - if (ds->latitude.udeg && ds->latitude.udeg != latitude.udeg) - fprintf(stderr, "Oops, changing the latitude of existing dive site id %8x name %s; not good\n", ds->uuid, ds->name ?: "(unknown)"); - ds->latitude = latitude; - } -} - -static void gps_long(char *buffer, struct dive *dive) -{ - char *end; - degrees_t longitude = parse_degrees(buffer, &end); - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!ds) { - dive->dive_site_uuid = create_dive_site_with_gps(NULL, (degrees_t){0}, longitude, dive->when); - } else { - if (ds->longitude.udeg && ds->longitude.udeg != longitude.udeg) - fprintf(stderr, "Oops, changing the longitude of existing dive site id %8x name %s; not good\n", ds->uuid, ds->name ?: "(unknown)"); - ds->longitude = longitude; - } - -} - -static void gps_location(char *buffer, struct dive_site *ds) -{ - char *end; - - ds->latitude = parse_degrees(buffer, &end); - ds->longitude = parse_degrees(end, &end); -} - -/* this is in qthelper.cpp, so including the .h file is a pain */ -extern const char *printGPSCoords(int lat, int lon); - -static void gps_in_dive(char *buffer, struct dive *dive) -{ - char *end; - struct dive_site *ds = NULL; - degrees_t latitude = parse_degrees(buffer, &end); - degrees_t longitude = parse_degrees(end, &end); - uint32_t uuid = dive->dive_site_uuid; - if (uuid == 0) { - // check if we have a dive site within 20 meters of that gps fix - uuid = get_dive_site_uuid_by_gps_proximity(latitude, longitude, 20, &ds); - - if (ds) { - // found a site nearby; in case it turns out this one had a different name let's - // remember the original coordinates so we can create the correct dive site later - cur_latitude = latitude; - cur_longitude = longitude; - dive->dive_site_uuid = uuid; - } else { - dive->dive_site_uuid = create_dive_site_with_gps("", latitude, longitude, dive->when); - ds = get_dive_site_by_uuid(dive->dive_site_uuid); - } - } else { - ds = get_dive_site_by_uuid(uuid); - if (dive_site_has_gps_location(ds) && - (latitude.udeg != 0 || longitude.udeg != 0) && - (ds->latitude.udeg != latitude.udeg || ds->longitude.udeg != longitude.udeg)) { - // Houston, we have a problem - fprintf(stderr, "dive site uuid in dive, but gps location (%10.6f/%10.6f) different from dive location (%10.6f/%10.6f)\n", - ds->latitude.udeg / 1000000.0, ds->longitude.udeg / 1000000.0, - latitude.udeg / 1000000.0, longitude.udeg / 1000000.0); - const char *coords = printGPSCoords(latitude.udeg, longitude.udeg); - ds->notes = add_to_string(ds->notes, translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords); - free((void *)coords); - } else { - ds->latitude = latitude; - ds->longitude = longitude; - } - } -} - -static void add_dive_site(char *ds_name, struct dive *dive) -{ - static int suffix = 1; - char *buffer = ds_name; - char *to_free = NULL; - int size = trimspace(buffer); - if(size) { - uint32_t uuid = dive->dive_site_uuid; - struct dive_site *ds = get_dive_site_by_uuid(uuid); - if (uuid && !ds) { - // that's strange - we have a uuid but it doesn't exist - let's just ignore it - fprintf(stderr, "dive contains a non-existing dive site uuid %x\n", dive->dive_site_uuid); - uuid = 0; - } - if (!uuid) { - // if the dive doesn't have a uuid, check if there's already a dive site by this name - uuid = get_dive_site_uuid_by_name(buffer, &ds); - if (uuid && import_source == SSRF_WS) { - // when downloading GPS fixes from the Subsurface webservice we will often - // get a lot of dives with identical names (the autogenerated fixes). - // So in this case modify the name to make it unique - int name_size = strlen(buffer) + 10; // 8 digits - enough for 100 million sites - to_free = buffer = malloc(name_size); - do { - suffix++; - snprintf(buffer, name_size, "%s %8d", ds_name, suffix); - } while (get_dive_site_uuid_by_name(buffer, NULL) != 0); - ds = NULL; - } - } - if (ds) { - // we have a uuid, let's hope there isn't a different name - if (same_string(ds->name, "")) { - ds->name = copy_string(buffer); - } else if (!same_string(ds->name, buffer)) { - // if it's not the same name, it's not the same dive site - // but wait, we could have gotten this one based on GPS coords and could - // have had two different names for the same site... so let's search the other - // way around - uint32_t exact_match_uuid = get_dive_site_uuid_by_gps_and_name(buffer, ds->latitude, ds->longitude); - if (exact_match_uuid) { - dive->dive_site_uuid = exact_match_uuid; - } else { - dive->dive_site_uuid = create_dive_site(buffer, dive->when); - struct dive_site *newds = get_dive_site_by_uuid(dive->dive_site_uuid); - if (cur_latitude.udeg || cur_longitude.udeg) { - // we started this uuid with GPS data, so lets use those - newds->latitude = cur_latitude; - newds->longitude = cur_longitude; - } else { - newds->latitude = ds->latitude; - newds->longitude = ds->longitude; - } - newds->notes = add_to_string(newds->notes, translate("gettextFromC", "additional name for site: %s\n"), ds->name); - } - } else { - // add the existing dive site to the current dive - dive->dive_site_uuid = uuid; - } - } else { - dive->dive_site_uuid = create_dive_site(buffer, dive->when); - } - } - free(to_free); -} - -static void gps_picture_location(char *buffer, struct picture *pic) -{ - char *end; - - pic->latitude = parse_degrees(buffer, &end); - pic->longitude = parse_degrees(end, &end); -} - -/* We're in the top-level dive xml. Try to convert whatever value to a dive value */ -static void try_to_fill_dive(struct dive *dive, const char *name, char *buf) -{ - start_match("dive", name, buf); - - switch (import_source) { - case DIVINGLOG: - if (divinglog_dive_match(dive, name, buf)) - return; - break; - - case UDDF: - if (uddf_dive_match(dive, name, buf)) - return; - break; - - default: - break; - } - if (MATCH("divesiteid", hex_value, &dive->dive_site_uuid)) - return; - if (MATCH("number", get_index, &dive->number)) - return; - if (MATCH("tags", divetags, &dive->tag_list)) - return; - if (MATCH("tripflag", get_tripflag, &dive->tripflag)) - return; - if (MATCH("date", divedate, &dive->when)) - return; - if (MATCH("time", divetime, &dive->when)) - return; - if (MATCH("datetime", divedatetime, &dive->when)) - return; - /* - * Legacy format note: per-dive depths and duration get saved - * in the first dive computer entry - */ - if (match_dc_data_fields(&dive->dc, name, buf)) - return; - - if (MATCH("filename.picture", utf8_string, &cur_picture->filename)) - return; - if (MATCH("offset.picture", offsettime, &cur_picture->offset)) - return; - if (MATCH("gps.picture", gps_picture_location, cur_picture)) - return; - if (MATCH("hash.picture", utf8_string, &cur_picture->hash)) - return; - if (MATCH("cylinderstartpressure", pressure, &dive->cylinder[0].start)) - return; - if (MATCH("cylinderendpressure", pressure, &dive->cylinder[0].end)) - return; - if (MATCH("gps", gps_in_dive, dive)) - return; - if (MATCH("Place", gps_in_dive, dive)) - return; - if (MATCH("latitude", gps_lat, dive)) - return; - if (MATCH("sitelat", gps_lat, dive)) - return; - if (MATCH("lat", gps_lat, dive)) - return; - if (MATCH("longitude", gps_long, dive)) - return; - if (MATCH("sitelon", gps_long, dive)) - return; - if (MATCH("lon", gps_long, dive)) - return; - if (MATCH("location", add_dive_site, dive)) - return; - if (MATCH("name.dive", add_dive_site, dive)) - return; - if (MATCH("suit", utf8_string, &dive->suit)) - return; - if (MATCH("divesuit", utf8_string, &dive->suit)) - return; - if (MATCH("notes", utf8_string, &dive->notes)) - return; - if (MATCH("divemaster", utf8_string, &dive->divemaster)) - return; - if (MATCH("buddy", utf8_string, &dive->buddy)) - return; - if (MATCH("rating.dive", get_rating, &dive->rating)) - return; - if (MATCH("visibility.dive", get_rating, &dive->visibility)) - return; - if (MATCH("size.cylinder", cylindersize, &dive->cylinder[cur_cylinder_index].type.size)) - return; - if (MATCH("workpressure.cylinder", pressure, &dive->cylinder[cur_cylinder_index].type.workingpressure)) - return; - if (MATCH("description.cylinder", utf8_string, &dive->cylinder[cur_cylinder_index].type.description)) - return; - if (MATCH("start.cylinder", pressure, &dive->cylinder[cur_cylinder_index].start)) - return; - if (MATCH("end.cylinder", pressure, &dive->cylinder[cur_cylinder_index].end)) - return; - if (MATCH("use.cylinder", cylinder_use, &dive->cylinder[cur_cylinder_index].cylinder_use)) - return; - if (MATCH("description.weightsystem", utf8_string, &dive->weightsystem[cur_ws_index].description)) - return; - if (MATCH("weight.weightsystem", weight, &dive->weightsystem[cur_ws_index].weight)) - return; - if (MATCH("weight", weight, &dive->weightsystem[cur_ws_index].weight)) - return; - if (MATCH("o2", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.o2)) - return; - if (MATCH("o2percent", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.o2)) - return; - if (MATCH("n2", gasmix_nitrogen, &dive->cylinder[cur_cylinder_index].gasmix)) - return; - if (MATCH("he", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.he)) - return; - if (MATCH("air.divetemperature", temperature, &dive->airtemp)) - return; - if (MATCH("water.divetemperature", temperature, &dive->watertemp)) - return; - - nonmatch("dive", name, buf); -} - -/* We're in the top-level trip xml. Try to convert whatever value to a trip value */ -static void try_to_fill_trip(dive_trip_t **dive_trip_p, const char *name, char *buf) -{ - start_match("trip", name, buf); - - dive_trip_t *dive_trip = *dive_trip_p; - - if (MATCH("date", divedate, &dive_trip->when)) - return; - if (MATCH("time", divetime, &dive_trip->when)) - return; - if (MATCH("location", utf8_string, &dive_trip->location)) - return; - if (MATCH("notes", utf8_string, &dive_trip->notes)) - return; - - nonmatch("trip", name, buf); -} - -/* We're processing a divesite entry - try to fill the components */ -static void try_to_fill_dive_site(struct dive_site **ds_p, const char *name, char *buf) -{ - start_match("divesite", name, buf); - - struct dive_site *ds = *ds_p; - if (ds->taxonomy.category == NULL) - ds->taxonomy.category = alloc_taxonomy(); - - if (MATCH("uuid", hex_value, &ds->uuid)) - return; - if (MATCH("name", utf8_string, &ds->name)) - return; - if (MATCH("description", utf8_string, &ds->description)) - return; - if (MATCH("notes", utf8_string, &ds->notes)) - return; - if (MATCH("gps", gps_location, ds)) - return; - if (MATCH("cat.geo", get_index, (int *)&ds->taxonomy.category[ds->taxonomy.nr].category)) - return; - if (MATCH("origin.geo", get_index, (int *)&ds->taxonomy.category[ds->taxonomy.nr].origin)) - return; - if (MATCH("value.geo", utf8_string, &ds->taxonomy.category[ds->taxonomy.nr].value)) { - if (ds->taxonomy.nr < TC_NR_CATEGORIES) - ds->taxonomy.nr++; - return; - } - - nonmatch("divesite", name, buf); -} - -/* - * While in some formats file boundaries are dive boundaries, in many - * others (as for example in our native format) there are - * multiple dives per file, so there can be other events too that - * trigger a "new dive" marker and you may get some nesting due - * to that. Just ignore nesting levels. - * On the flipside it is possible that we start an XML file that ends - * up having no dives in it at all - don't create a bogus empty dive - * for those. It's not entirely clear what is the minimum set of data - * to make a dive valid, but if it has no location, no date and no - * samples I'm pretty sure it's useless. - */ -static bool is_dive(void) -{ - return (cur_dive && - (cur_dive->dive_site_uuid || cur_dive->when || cur_dive->dc.samples)); -} - -static void reset_dc_info(struct divecomputer *dc) -{ - lastcns = lastpo2 = lastndl = laststoptime = laststopdepth = lastindeco = 0; - lastsensor = lastcylinderindex = 0; -} - -static void reset_dc_settings(void) -{ - free((void *)cur_settings.dc.model); - free((void *)cur_settings.dc.nickname); - free((void *)cur_settings.dc.serial_nr); - free((void *)cur_settings.dc.firmware); - cur_settings.dc.model = NULL; - cur_settings.dc.nickname = NULL; - cur_settings.dc.serial_nr = NULL; - cur_settings.dc.firmware = NULL; - cur_settings.dc.deviceid = 0; -} - -static void settings_start(void) -{ - in_settings = true; -} - -static void settings_end(void) -{ - in_settings = false; -} - -static void dc_settings_start(void) -{ - reset_dc_settings(); -} - -static void dc_settings_end(void) -{ - create_device_node(cur_settings.dc.model, cur_settings.dc.deviceid, cur_settings.dc.serial_nr, - cur_settings.dc.firmware, cur_settings.dc.nickname); - reset_dc_settings(); -} - -static void dive_site_start(void) -{ - if (cur_dive_site) - return; - cur_dive_site = calloc(1, sizeof(struct dive_site)); -} - -static void dive_site_end(void) -{ - if (!cur_dive_site) - return; - if (cur_dive_site->uuid) { - // we intentionally call this with '0' to ensure we get - // a new structure and then copy things into that new - // structure a few lines below (which sets the correct - // uuid) - struct dive_site *ds = alloc_or_get_dive_site(0); - if (cur_dive_site->taxonomy.nr == 0) { - free(cur_dive_site->taxonomy.category); - cur_dive_site->taxonomy.category = NULL; - } - copy_dive_site(cur_dive_site, ds); - - if (verbose > 3) - printf("completed dive site uuid %x8 name {%s}\n", ds->uuid, ds->name); - } - free_taxonomy(&cur_dive_site->taxonomy); - free(cur_dive_site); - cur_dive_site = NULL; -} - -// now we need to add the code to parse the parts of the divesite enry - -static void dive_start(void) -{ - if (cur_dive) - return; - cur_dive = alloc_dive(); - reset_dc_info(&cur_dive->dc); - memset(&cur_tm, 0, sizeof(cur_tm)); - if (cur_trip) { - add_dive_to_trip(cur_dive, cur_trip); - cur_dive->tripflag = IN_TRIP; - } -} - -static void dive_end(void) -{ - if (!cur_dive) - return; - if (!is_dive()) - free(cur_dive); - else - record_dive_to_table(cur_dive, target_table); - cur_dive = NULL; - cur_dc = NULL; - cur_latitude.udeg = 0; - cur_longitude.udeg = 0; - cur_cylinder_index = 0; - cur_ws_index = 0; -} - -static void trip_start(void) -{ - if (cur_trip) - return; - dive_end(); - cur_trip = calloc(1, sizeof(dive_trip_t)); - memset(&cur_tm, 0, sizeof(cur_tm)); -} - -static void trip_end(void) -{ - if (!cur_trip) - return; - insert_trip(&cur_trip); - cur_trip = NULL; -} - -static void event_start(void) -{ - memset(&cur_event, 0, sizeof(cur_event)); - cur_event.deleted = 0; /* Active */ -} - -static void event_end(void) -{ - struct divecomputer *dc = get_dc(); - if (strcmp(cur_event.name, "surface") != 0) { /* 123 is a magic event that we used for a while to encode images in dives */ - if (cur_event.type == 123) { - struct picture *pic = alloc_picture(); - pic->filename = strdup(cur_event.name); - /* theoretically this could fail - but we didn't support multi year offsets */ - pic->offset.seconds = cur_event.time.seconds; - dive_add_picture(cur_dive, pic); - } else { - struct event *ev; - /* At some point gas change events did not have any type. Thus we need to add - * one on import, if we encounter the type one missing. - */ - if (cur_event.type == 0 && strcmp(cur_event.name, "gaschange") == 0) - cur_event.type = cur_event.value >> 16 > 0 ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE; - ev = add_event(dc, cur_event.time.seconds, - cur_event.type, cur_event.flags, - cur_event.value, cur_event.name); - if (ev && event_is_gaschange(ev)) { - /* See try_to_fill_event() on why the filled-in index is one too big */ - ev->gas.index = cur_event.gas.index-1; - if (cur_event.gas.mix.o2.permille || cur_event.gas.mix.he.permille) - ev->gas.mix = cur_event.gas.mix; - } - } - } - cur_event.deleted = 1; /* No longer active */ -} - -static void picture_start(void) -{ - cur_picture = alloc_picture(); -} - -static void picture_end(void) -{ - dive_add_picture(cur_dive, cur_picture); - cur_picture = NULL; -} - -static void cylinder_start(void) -{ -} - -static void cylinder_end(void) -{ - cur_cylinder_index++; -} - -static void ws_start(void) -{ -} - -static void ws_end(void) -{ - cur_ws_index++; -} - -static void sample_start(void) -{ - cur_sample = prepare_sample(get_dc()); - cur_sample->ndl.seconds = lastndl; - cur_sample->in_deco = lastindeco; - cur_sample->stoptime.seconds = laststoptime; - cur_sample->stopdepth.mm = laststopdepth; - cur_sample->cns = lastcns; - cur_sample->setpoint.mbar = lastpo2; - cur_sample->sensor = lastsensor; - next_o2_sensor = 0; -} - -static void sample_end(void) -{ - if (!cur_dive) - return; - - finish_sample(get_dc()); - lastndl = cur_sample->ndl.seconds; - lastindeco = cur_sample->in_deco; - laststoptime = cur_sample->stoptime.seconds; - laststopdepth = cur_sample->stopdepth.mm; - lastcns = cur_sample->cns; - lastpo2 = cur_sample->setpoint.mbar; - cur_sample = NULL; -} - -static void divecomputer_start(void) -{ - struct divecomputer *dc; - - /* Start from the previous dive computer */ - dc = &cur_dive->dc; - while (dc->next) - dc = dc->next; - - /* Did we already fill that in? */ - if (dc->samples || dc->model || dc->when) { - struct divecomputer *newdc = calloc(1, sizeof(*newdc)); - if (newdc) { - dc->next = newdc; - dc = newdc; - } - } - - /* .. this is the one we'll use */ - cur_dc = dc; - reset_dc_info(dc); -} - -static void divecomputer_end(void) -{ - if (!cur_dc->when) - cur_dc->when = cur_dive->when; - cur_dc = NULL; -} - -static void userid_start(void) -{ - in_userid = true; - set_save_userid_local(true); //if the xml contains userid, keep saving it. -} - -static void userid_stop(void) -{ - in_userid = false; -} - -static bool entry(const char *name, char *buf) -{ - if (!strncmp(name, "version.program", sizeof("version.program") - 1) || - !strncmp(name, "version.divelog", sizeof("version.divelog") - 1)) { - last_xml_version = atoi(buf); - report_datafile_version(last_xml_version); - } - if (in_userid) { - try_to_fill_userid(name, buf); - return true; - } - if (in_settings) { - try_to_fill_dc_settings(name, buf); - try_to_match_autogroup(name, buf); - return true; - } - if (cur_dive_site) { - try_to_fill_dive_site(&cur_dive_site, name, buf); - return true; - } - if (!cur_event.deleted) { - try_to_fill_event(name, buf); - return true; - } - if (cur_sample) { - try_to_fill_sample(cur_sample, name, buf); - return true; - } - if (cur_dc) { - try_to_fill_dc(cur_dc, name, buf); - return true; - } - if (cur_dive) { - try_to_fill_dive(cur_dive, name, buf); - return true; - } - if (cur_trip) { - try_to_fill_trip(&cur_trip, name, buf); - return true; - } - return true; -} - -static const char *nodename(xmlNode *node, char *buf, int len) -{ - int levels = 2; - char *p = buf; - - if (!node || (node->type != XML_CDATA_SECTION_NODE && !node->name)) { - return "root"; - } - - if (node->type == XML_CDATA_SECTION_NODE || (node->parent && !strcmp((const char *)node->name, "text"))) - node = node->parent; - - /* Make sure it's always NUL-terminated */ - p[--len] = 0; - - for (;;) { - const char *name = (const char *)node->name; - char c; - while ((c = *name++) != 0) { - /* Cheaper 'tolower()' for ASCII */ - c = (c >= 'A' && c <= 'Z') ? c - 'A' + 'a' : c; - *p++ = c; - if (!--len) - return buf; - } - *p = 0; - node = node->parent; - if (!node || !node->name) - return buf; - *p++ = '.'; - if (!--len) - return buf; - if (!--levels) - return buf; - } -} - -#define MAXNAME 32 - -static bool visit_one_node(xmlNode *node) -{ - xmlChar *content; - static char buffer[MAXNAME]; - const char *name; - - content = node->content; - if (!content || xmlIsBlankNode(node)) - return true; - - name = nodename(node, buffer, sizeof(buffer)); - - return entry(name, (char *)content); -} - -static bool traverse(xmlNode *root); - -static bool traverse_properties(xmlNode *node) -{ - xmlAttr *p; - bool ret = true; - - for (p = node->properties; p; p = p->next) - if ((ret = traverse(p->children)) == false) - break; - return ret; -} - -static bool visit(xmlNode *n) -{ - return visit_one_node(n) && traverse_properties(n) && traverse(n->children); -} - -static void DivingLog_importer(void) -{ - import_source = DIVINGLOG; - - /* - * Diving Log units are really strange. - * - * Temperatures are in C, except in samples, - * when they are in Fahrenheit. Depths are in - * meters, an dpressure is in PSI in the samples, - * but in bar when it comes to working pressure. - * - * Crazy f*%^ morons. - */ - xml_parsing_units = SI_units; -} - -static void uddf_importer(void) -{ - import_source = UDDF; - xml_parsing_units = SI_units; - xml_parsing_units.pressure = PASCAL; - xml_parsing_units.temperature = KELVIN; -} - -static void subsurface_webservice(void) -{ - import_source = SSRF_WS; -} - -/* - * I'm sure this could be done as some fancy DTD rules. - * It's just not worth the headache. - */ -static struct nesting { - const char *name; - void (*start)(void), (*end)(void); -} nesting[] = { - { "divecomputerid", dc_settings_start, dc_settings_end }, - { "settings", settings_start, settings_end }, - { "site", dive_site_start, dive_site_end }, - { "dive", dive_start, dive_end }, - { "Dive", dive_start, dive_end }, - { "trip", trip_start, trip_end }, - { "sample", sample_start, sample_end }, - { "waypoint", sample_start, sample_end }, - { "SAMPLE", sample_start, sample_end }, - { "reading", sample_start, sample_end }, - { "event", event_start, event_end }, - { "mix", cylinder_start, cylinder_end }, - { "gasmix", cylinder_start, cylinder_end }, - { "cylinder", cylinder_start, cylinder_end }, - { "weightsystem", ws_start, ws_end }, - { "divecomputer", divecomputer_start, divecomputer_end }, - { "P", sample_start, sample_end }, - { "userid", userid_start, userid_stop}, - { "picture", picture_start, picture_end }, - { "extradata", extra_data_start, extra_data_end }, - - /* Import type recognition */ - { "Divinglog", DivingLog_importer }, - { "uddf", uddf_importer }, - { "output", subsurface_webservice }, - { NULL, } - }; - -static bool traverse(xmlNode *root) -{ - xmlNode *n; - bool ret = true; - - for (n = root; n; n = n->next) { - struct nesting *rule = nesting; - - if (!n->name) { - if ((ret = visit(n)) == false) - break; - continue; - } - - do { - if (!strcmp(rule->name, (const char *)n->name)) - break; - rule++; - } while (rule->name); - - if (rule->start) - rule->start(); - if ((ret = visit(n)) == false) - break; - if (rule->end) - rule->end(); - } - return ret; -} - -/* Per-file reset */ -static void reset_all(void) -{ - /* - * We reset the units for each file. You'd think it was - * a per-dive property, but I'm not going to trust people - * to do per-dive setup. If the xml does have per-dive - * data within one file, we might have to reset it per - * dive for that format. - */ - xml_parsing_units = SI_units; - import_source = UNKNOWN; -} - -/* divelog.de sends us xml files that claim to be iso-8859-1 - * but once we decode the HTML encoded characters they turn - * into UTF-8 instead. So skip the incorrect encoding - * declaration and decode the HTML encoded characters */ -const char *preprocess_divelog_de(const char *buffer) -{ - char *ret = strstr(buffer, ""); - - if (ret) { - xmlParserCtxtPtr ctx; - char buf[] = ""; - int i; - - for (i = 0; i < strlen(ret); ++i) - if (!isascii(ret[i])) - return buffer; - - ctx = xmlCreateMemoryParserCtxt(buf, sizeof(buf)); - ret = (char *)xmlStringLenDecodeEntities(ctx, (xmlChar *)ret, strlen(ret), XML_SUBSTITUTE_REF, 0, 0, 0); - - return ret; - } - return buffer; -} - -int parse_xml_buffer(const char *url, const char *buffer, int size, - struct dive_table *table, const char **params) -{ - xmlDoc *doc; - const char *res = preprocess_divelog_de(buffer); - int ret = 0; - - target_table = table; - doc = xmlReadMemory(res, strlen(res), url, NULL, 0); - if (res != buffer) - free((char *)res); - - if (!doc) - return report_error(translate("gettextFromC", "Failed to parse '%s'"), url); - - set_save_userid_local(false); - reset_all(); - dive_start(); - doc = test_xslt_transforms(doc, params); - if (!traverse(xmlDocGetRootElement(doc))) { - // we decided to give up on parsing... why? - ret = -1; - } - dive_end(); - xmlFreeDoc(doc); - return ret; -} - -void parse_mkvi_buffer(struct membuffer *txt, struct membuffer *csv, const char *starttime) -{ - dive_start(); - divedate(starttime, &cur_dive->when); - dive_end(); -} - -extern int dm4_events(void *handle, int columns, char **data, char **column) -{ - event_start(); - if (data[1]) - cur_event.time.seconds = atoi(data[1]); - - if (data[2]) { - switch (atoi(data[2])) { - case 1: - /* 1 Mandatory Safety Stop */ - strcpy(cur_event.name, "safety stop (mandatory)"); - break; - case 3: - /* 3 Deco */ - /* What is Subsurface's term for going to - * deco? */ - strcpy(cur_event.name, "deco"); - break; - case 4: - /* 4 Ascent warning */ - strcpy(cur_event.name, "ascent"); - break; - case 5: - /* 5 Ceiling broken */ - strcpy(cur_event.name, "violation"); - break; - case 6: - /* 6 Mandatory safety stop ceiling error */ - strcpy(cur_event.name, "violation"); - break; - case 7: - /* 7 Below deco floor */ - strcpy(cur_event.name, "below floor"); - break; - case 8: - /* 8 Dive time alarm */ - strcpy(cur_event.name, "divetime"); - break; - case 9: - /* 9 Depth alarm */ - strcpy(cur_event.name, "maxdepth"); - break; - case 10: - /* 10 OLF 80% */ - case 11: - /* 11 OLF 100% */ - strcpy(cur_event.name, "OLF"); - break; - case 12: - /* 12 High pO₂ */ - strcpy(cur_event.name, "PO2"); - break; - case 13: - /* 13 Air time */ - strcpy(cur_event.name, "airtime"); - break; - case 17: - /* 17 Ascent warning */ - strcpy(cur_event.name, "ascent"); - break; - case 18: - /* 18 Ceiling error */ - strcpy(cur_event.name, "ceiling"); - break; - case 19: - /* 19 Surfaced */ - strcpy(cur_event.name, "surface"); - break; - case 20: - /* 20 Deco */ - strcpy(cur_event.name, "deco"); - break; - case 22: - case 32: - /* 22 Mandatory safety stop violation */ - /* 32 Deep stop violation */ - strcpy(cur_event.name, "violation"); - break; - case 30: - /* Tissue level warning */ - strcpy(cur_event.name, "tissue warning"); - break; - case 37: - /* Tank pressure alarm */ - strcpy(cur_event.name, "tank pressure"); - break; - case 257: - /* 257 Dive active */ - /* This seems to be given after surface when - * descending again. */ - strcpy(cur_event.name, "surface"); - break; - case 258: - /* 258 Bookmark */ - if (data[3]) { - strcpy(cur_event.name, "heading"); - cur_event.value = atoi(data[3]); - } else { - strcpy(cur_event.name, "bookmark"); - } - break; - case 259: - /* Deep stop */ - strcpy(cur_event.name, "Deep stop"); - break; - case 260: - /* Deep stop */ - strcpy(cur_event.name, "Deep stop cleared"); - break; - case 266: - /* Mandatory safety stop activated */ - strcpy(cur_event.name, "safety stop (mandatory)"); - break; - case 267: - /* Mandatory safety stop deactivated */ - /* DM5 shows this only on event list, not on the - * profile so skipping as well for now */ - break; - default: - strcpy(cur_event.name, "unknown"); - cur_event.value = atoi(data[2]); - break; - } - } - event_end(); - - return 0; -} - -extern int dm5_cylinders(void *handle, int columns, char **data, char **column) -{ - cylinder_start(); - if (data[7] && atoi(data[7]) > 0 && atoi(data[7]) < 350000) - cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[7]); - if (data[8] && atoi(data[8]) > 0 && atoi(data[8]) < 350000) - cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[8])); - if (data[6]) { - /* DM5 shows tank size of 12 liters when the actual - * value is 0 (and using metric units). So we just use - * the same 12 liters when size is not available */ - if (atof(data[6]) == 0.0 && cur_dive->cylinder[cur_cylinder_index].start.mbar) - cur_dive->cylinder[cur_cylinder_index].type.size.mliter = 12000; - else - cur_dive->cylinder[cur_cylinder_index].type.size.mliter = (atof(data[6])) * 1000; - } - if (data[2]) - cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[2]) * 10; - if (data[3]) - cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[3]) * 10; - cylinder_end(); - return 0; -} - -extern int dm5_gaschange(void *handle, int columns, char **data, char **column) -{ - event_start(); - if (data[0]) - cur_event.time.seconds = atoi(data[0]); - if (data[1]) { - strcpy(cur_event.name, "gaschange"); - cur_event.value = atof(data[1]); - } - event_end(); - - return 0; -} - -extern int dm4_tags(void *handle, int columns, char **data, char **column) -{ - if (data[0]) - taglist_add_tag(&cur_dive->tag_list, data[0]); - - return 0; -} - -extern int dm4_dive(void *param, int columns, char **data, char **column) -{ - int i, interval, retval = 0; - sqlite3 *handle = (sqlite3 *)param; - float *profileBlob; - unsigned char *tempBlob; - int *pressureBlob; - char *err = NULL; - char get_events_template[] = "select * from Mark where DiveId = %d"; - char get_tags_template[] = "select Text from DiveTag where DiveId = %d"; - char get_events[64]; - - dive_start(); - cur_dive->number = atoi(data[0]); - - cur_dive->when = (time_t)(atol(data[1])); - if (data[2]) - utf8_string(data[2], &cur_dive->notes); - - /* - * DM4 stores Duration and DiveTime. It looks like DiveTime is - * 10 to 60 seconds shorter than Duration. However, I have no - * idea what is the difference and which one should be used. - * Duration = data[3] - * DiveTime = data[15] - */ - if (data[3]) - cur_dive->duration.seconds = atoi(data[3]); - if (data[15]) - cur_dive->dc.duration.seconds = atoi(data[15]); - - /* - * TODO: the deviceid hash should be calculated here. - */ - settings_start(); - dc_settings_start(); - if (data[4]) - utf8_string(data[4], &cur_settings.dc.serial_nr); - if (data[5]) - utf8_string(data[5], &cur_settings.dc.model); - - cur_settings.dc.deviceid = 0xffffffff; - dc_settings_end(); - settings_end(); - - if (data[6]) - cur_dive->dc.maxdepth.mm = atof(data[6]) * 1000; - if (data[8]) - cur_dive->dc.airtemp.mkelvin = C_to_mkelvin(atoi(data[8])); - if (data[9]) - cur_dive->dc.watertemp.mkelvin = C_to_mkelvin(atoi(data[9])); - - /* - * TODO: handle multiple cylinders - */ - cylinder_start(); - if (data[22] && atoi(data[22]) > 0) - cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[22]); - else if (data[10] && atoi(data[10]) > 0) - cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[10]); - if (data[23] && atoi(data[23]) > 0) - cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[23])); - if (data[11] && atoi(data[11]) > 0) - cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[11])); - if (data[12]) - cur_dive->cylinder[cur_cylinder_index].type.size.mliter = (atof(data[12])) * 1000; - if (data[13]) - cur_dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = (atoi(data[13])); - if (data[20]) - cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[20]) * 10; - if (data[21]) - cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[21]) * 10; - cylinder_end(); - - if (data[14]) - cur_dive->dc.surface_pressure.mbar = (atoi(data[14]) * 1000); - - interval = data[16] ? atoi(data[16]) : 0; - profileBlob = (float *)data[17]; - tempBlob = (unsigned char *)data[18]; - pressureBlob = (int *)data[19]; - for (i = 0; interval && i * interval < cur_dive->duration.seconds; i++) { - sample_start(); - cur_sample->time.seconds = i * interval; - if (profileBlob) - cur_sample->depth.mm = profileBlob[i] * 1000; - else - cur_sample->depth.mm = cur_dive->dc.maxdepth.mm; - - if (data[18] && data[18][0]) - cur_sample->temperature.mkelvin = C_to_mkelvin(tempBlob[i]); - if (data[19] && data[19][0]) - cur_sample->cylinderpressure.mbar = pressureBlob[i]; - sample_end(); - } - - snprintf(get_events, sizeof(get_events) - 1, get_events_template, cur_dive->number); - retval = sqlite3_exec(handle, get_events, &dm4_events, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query dm4_events failed.\n"); - return 1; - } - - snprintf(get_events, sizeof(get_events) - 1, get_tags_template, cur_dive->number); - retval = sqlite3_exec(handle, get_events, &dm4_tags, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query dm4_tags failed.\n"); - return 1; - } - - dive_end(); - - /* - for (i=0; inumber = atoi(data[0]); - - cur_dive->when = (time_t)(atol(data[1])); - if (data[2]) - utf8_string(data[2], &cur_dive->notes); - - if (data[3]) - cur_dive->duration.seconds = atoi(data[3]); - if (data[15]) - cur_dive->dc.duration.seconds = atoi(data[15]); - - /* - * TODO: the deviceid hash should be calculated here. - */ - settings_start(); - dc_settings_start(); - if (data[4]) { - utf8_string(data[4], &cur_settings.dc.serial_nr); - cur_settings.dc.deviceid = atoi(data[4]); - } - if (data[5]) - utf8_string(data[5], &cur_settings.dc.model); - - dc_settings_end(); - settings_end(); - - if (data[6]) - cur_dive->dc.maxdepth.mm = atof(data[6]) * 1000; - if (data[8]) - cur_dive->dc.airtemp.mkelvin = C_to_mkelvin(atoi(data[8])); - if (data[9]) - cur_dive->dc.watertemp.mkelvin = C_to_mkelvin(atoi(data[9])); - - if (data[4]) { - cur_dive->dc.deviceid = atoi(data[4]); - } - if (data[5]) - utf8_string(data[5], &cur_dive->dc.model); - - snprintf(get_events, sizeof(get_events) - 1, get_cylinders_template, cur_dive->number); - retval = sqlite3_exec(handle, get_events, &dm5_cylinders, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query dm5_cylinders failed.\n"); - return 1; - } - - if (data[14]) - cur_dive->dc.surface_pressure.mbar = (atoi(data[14]) / 100); - - interval = data[16] ? atoi(data[16]) : 0; - sampleBlob = (unsigned const char *)data[24]; - - if (sampleBlob) { - switch (sampleBlob[0]) { - case 2: - block_size = 19; - break; - case 3: - block_size = 23; - break; - default: - block_size = 16; - break; - } - } - - for (i = 0; interval && sampleBlob && i * interval < cur_dive->duration.seconds; i++) { - float *depth = (float *)&sampleBlob[i * block_size + 3]; - int32_t temp = (sampleBlob[i * block_size + 10] << 8) + sampleBlob[i * block_size + 11]; - int32_t pressure = (sampleBlob[i * block_size + 9] << 16) + (sampleBlob[i * block_size + 8] << 8) + sampleBlob[i * block_size + 7]; - - sample_start(); - cur_sample->time.seconds = i * interval; - cur_sample->depth.mm = depth[0] * 1000; - /* - * Limit temperatures and cylinder pressures to somewhat - * sensible values - */ - if (temp >= -10 && temp < 50) - cur_sample->temperature.mkelvin = C_to_mkelvin(temp); - if (pressure >= 0 && pressure < 350000) - cur_sample->cylinderpressure.mbar = pressure; - sample_end(); - } - - /* - * Log was converted from DM4, thus we need to parse the profile - * from DM4 format - */ - - if (i == 0) { - float *profileBlob; - unsigned char *tempBlob; - int *pressureBlob; - - profileBlob = (float *)data[17]; - tempBlob = (unsigned char *)data[18]; - pressureBlob = (int *)data[19]; - for (i = 0; interval && i * interval < cur_dive->duration.seconds; i++) { - sample_start(); - cur_sample->time.seconds = i * interval; - if (profileBlob) - cur_sample->depth.mm = profileBlob[i] * 1000; - else - cur_sample->depth.mm = cur_dive->dc.maxdepth.mm; - - if (data[18] && data[18][0]) - cur_sample->temperature.mkelvin = C_to_mkelvin(tempBlob[i]); - if (data[19] && data[19][0]) - cur_sample->cylinderpressure.mbar = pressureBlob[i]; - sample_end(); - } - } - - snprintf(get_events, sizeof(get_events) - 1, get_gaschange_template, cur_dive->number); - retval = sqlite3_exec(handle, get_events, &dm5_gaschange, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query dm5_gaschange failed.\n"); - return 1; - } - - snprintf(get_events, sizeof(get_events) - 1, get_events_template, cur_dive->number); - retval = sqlite3_exec(handle, get_events, &dm4_events, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query dm4_events failed.\n"); - return 1; - } - - snprintf(get_events, sizeof(get_events) - 1, get_tags_template, cur_dive->number); - retval = sqlite3_exec(handle, get_events, &dm4_tags, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query dm4_tags failed.\n"); - return 1; - } - - dive_end(); - - return SQLITE_OK; -} - - -int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, - struct dive_table *table) -{ - int retval; - char *err = NULL; - target_table = table; - - /* StartTime is converted from Suunto's nano seconds to standard - * time. We also need epoch, not seconds since year 1. */ - char get_dives[] = "select D.DiveId,StartTime/10000000-62135596800,Note,Duration,SourceSerialNumber,Source,MaxDepth,SampleInterval,StartTemperature,BottomTemperature,D.StartPressure,D.EndPressure,Size,CylinderWorkPressure,SurfacePressure,DiveTime,SampleInterval,ProfileBlob,TemperatureBlob,PressureBlob,Oxygen,Helium,MIX.StartPressure,MIX.EndPressure FROM Dive AS D JOIN DiveMixture AS MIX ON D.DiveId=MIX.DiveId"; - - retval = sqlite3_exec(handle, get_dives, &dm4_dive, handle, &err); - - if (retval != SQLITE_OK) { - fprintf(stderr, "Database query failed '%s'.\n", url); - return 1; - } - - return 0; -} - -int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, - struct dive_table *table) -{ - int retval; - char *err = NULL; - target_table = table; - - /* StartTime is converted from Suunto's nano seconds to standard - * time. We also need epoch, not seconds since year 1. */ - char get_dives[] = "select DiveId,StartTime/10000000-62135596800,Note,Duration,coalesce(SourceSerialNumber,SerialNumber),Source,MaxDepth,SampleInterval,StartTemperature,BottomTemperature,StartPressure,EndPressure,'','',SurfacePressure,DiveTime,SampleInterval,ProfileBlob,TemperatureBlob,PressureBlob,'','','','',SampleBlob FROM Dive where Deleted is null"; - - retval = sqlite3_exec(handle, get_dives, &dm5_dive, handle, &err); - - if (retval != SQLITE_OK) { - fprintf(stderr, "Database query failed '%s'.\n", url); - return 1; - } - - return 0; -} - -extern int shearwater_cylinders(void *handle, int columns, char **data, char **column) -{ - cylinder_start(); - if (data[0]) - cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atof(data[0]) * 1000; - if (data[1]) - cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atof(data[1]) * 1000; - cylinder_end(); - - return 0; -} - -extern int shearwater_changes(void *handle, int columns, char **data, char **column) -{ - event_start(); - if (data[0]) - cur_event.time.seconds = atoi(data[0]); - if (data[1]) { - strcpy(cur_event.name, "gaschange"); - cur_event.value = atof(data[1]) * 100; - } - event_end(); - - return 0; -} - - -extern int cobalt_profile_sample(void *handle, int columns, char **data, char **column) -{ - sample_start(); - if (data[0]) - cur_sample->time.seconds = atoi(data[0]); - if (data[1]) - cur_sample->depth.mm = atoi(data[1]); - if (data[2]) - cur_sample->temperature.mkelvin = metric ? C_to_mkelvin(atof(data[2])) : F_to_mkelvin(atof(data[2])); - sample_end(); - - return 0; -} - - -extern int shearwater_profile_sample(void *handle, int columns, char **data, char **column) -{ - sample_start(); - if (data[0]) - cur_sample->time.seconds = atoi(data[0]); - if (data[1]) - cur_sample->depth.mm = metric ? atof(data[1]) * 1000 : feet_to_mm(atof(data[1])); - if (data[2]) - cur_sample->temperature.mkelvin = metric ? C_to_mkelvin(atof(data[2])) : F_to_mkelvin(atof(data[2])); - if (data[3]) { - cur_sample->setpoint.mbar = atof(data[3]) * 1000; - cur_dive->dc.divemode = CCR; - } - if (data[4]) - cur_sample->ndl.seconds = atoi(data[4]) * 60; - if (data[5]) - cur_sample->cns = atoi(data[5]); - if (data[6]) - cur_sample->stopdepth.mm = metric ? atoi(data[6]) * 1000 : feet_to_mm(atoi(data[6])); - - /* We don't actually have data[3], but it should appear in the - * SQL query at some point. - if (data[3]) - cur_sample->cylinderpressure.mbar = metric ? atoi(data[3]) * 1000 : psi_to_mbar(atoi(data[3])); - */ - sample_end(); - - return 0; -} - -extern int shearwater_dive(void *param, int columns, char **data, char **column) -{ - int retval = 0; - sqlite3 *handle = (sqlite3 *)param; - char *err = NULL; - char get_profile_template[] = "select currentTime,currentDepth,waterTemp,averagePPO2,currentNdl,CNSPercent,decoCeiling from dive_log_records where diveLogId = %d"; - char get_cylinder_template[] = "select fractionO2,fractionHe from dive_log_records where diveLogId = %d group by fractionO2,fractionHe"; - char get_changes_template[] = "select a.currentTime,a.fractionO2,a.fractionHe from dive_log_records as a,dive_log_records as b where a.diveLogId = %d and b.diveLogId = %d and (a.id - 1) = b.id and (a.fractionO2 != b.fractionO2 or a.fractionHe != b.fractionHe) union select min(currentTime),fractionO2,fractionHe from dive_log_records"; - char get_buffer[1024]; - - dive_start(); - cur_dive->number = atoi(data[0]); - - cur_dive->when = (time_t)(atol(data[1])); - - if (data[2]) - add_dive_site(data[2], cur_dive); - if (data[3]) - utf8_string(data[3], &cur_dive->buddy); - if (data[4]) - utf8_string(data[4], &cur_dive->notes); - - metric = atoi(data[5]) == 1 ? 0 : 1; - - /* TODO: verify that metric calculation is correct */ - if (data[6]) - cur_dive->dc.maxdepth.mm = metric ? atof(data[6]) * 1000 : feet_to_mm(atof(data[6])); - - if (data[7]) - cur_dive->dc.duration.seconds = atoi(data[7]) * 60; - - if (data[8]) - cur_dive->dc.surface_pressure.mbar = atoi(data[8]); - /* - * TODO: the deviceid hash should be calculated here. - */ - settings_start(); - dc_settings_start(); - if (data[9]) - utf8_string(data[9], &cur_settings.dc.serial_nr); - if (data[10]) - utf8_string(data[10], &cur_settings.dc.model); - - cur_settings.dc.deviceid = 0xffffffff; - dc_settings_end(); - settings_end(); - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &shearwater_cylinders, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query shearwater_cylinders failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_changes_template, cur_dive->number, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &shearwater_changes, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query shearwater_changes failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &shearwater_profile_sample, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query shearwater_profile_sample failed.\n"); - return 1; - } - - dive_end(); - - return SQLITE_OK; -} - -extern int cobalt_cylinders(void *handle, int columns, char **data, char **column) -{ - cylinder_start(); - if (data[0]) - cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[0]) * 10; - if (data[1]) - cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[1]) * 10; - if (data[2]) - cur_dive->cylinder[cur_cylinder_index].start.mbar = psi_to_mbar(atoi(data[2])); - if (data[3]) - cur_dive->cylinder[cur_cylinder_index].end.mbar = psi_to_mbar(atoi(data[3])); - if (data[4]) - cur_dive->cylinder[cur_cylinder_index].type.size.mliter = atoi(data[4]) * 100; - if (data[5]) - cur_dive->cylinder[cur_cylinder_index].gas_used.mliter = atoi(data[5]) * 1000; - cylinder_end(); - - return 0; -} - -extern int cobalt_buddies(void *handle, int columns, char **data, char **column) -{ - if (data[0]) - utf8_string(data[0], &cur_dive->buddy); - - return 0; -} - -/* - * We still need to figure out how to map free text visibility to - * Subsurface star rating. - */ - -extern int cobalt_visibility(void *handle, int columns, char **data, char **column) -{ - return 0; -} - -extern int cobalt_location(void *handle, int columns, char **data, char **column) -{ - static char *location = NULL; - if (data[0]) { - if (location) { - char *tmp = malloc(strlen(location) + strlen(data[0]) + 4); - if (!tmp) - return -1; - sprintf(tmp, "%s / %s", location, data[0]); - free(location); - location = NULL; - cur_dive->dive_site_uuid = find_or_create_dive_site_with_name(tmp, cur_dive->when); - free(tmp); - } else { - location = strdup(data[0]); - } - } - return 0; -} - - -extern int cobalt_dive(void *param, int columns, char **data, char **column) -{ - int retval = 0; - sqlite3 *handle = (sqlite3 *)param; - char *err = NULL; - char get_profile_template[] = "select runtime*60,(DepthPressure*10000/SurfacePressure)-10000,p.Temperature from Dive AS d JOIN TrackPoints AS p ON d.Id=p.DiveId where d.Id=%d"; - char get_cylinder_template[] = "select FO2,FHe,StartingPressure,EndingPressure,TankSize,TankPressure,TotalConsumption from GasMixes where DiveID=%d and StartingPressure>0 group by FO2,FHe"; - char get_buddy_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=4"; - char get_visibility_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=3"; - char get_location_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=0"; - char get_site_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=1"; - char get_buffer[1024]; - - dive_start(); - cur_dive->number = atoi(data[0]); - - cur_dive->when = (time_t)(atol(data[1])); - - if (data[4]) - utf8_string(data[4], &cur_dive->notes); - - /* data[5] should have information on Units used, but I cannot - * parse it at all based on the sample log I have received. The - * temperatures in the samples are all Imperial, so let's go by - * that. - */ - - metric = 0; - - /* Cobalt stores the pressures, not the depth */ - if (data[6]) - cur_dive->dc.maxdepth.mm = atoi(data[6]); - - if (data[7]) - cur_dive->dc.duration.seconds = atoi(data[7]); - - if (data[8]) - cur_dive->dc.surface_pressure.mbar = atoi(data[8]); - /* - * TODO: the deviceid hash should be calculated here. - */ - settings_start(); - dc_settings_start(); - if (data[9]) { - utf8_string(data[9], &cur_settings.dc.serial_nr); - cur_settings.dc.deviceid = atoi(data[9]); - cur_settings.dc.model = strdup("Cobalt import"); - } - - dc_settings_end(); - settings_end(); - - if (data[9]) { - cur_dive->dc.deviceid = atoi(data[9]); - cur_dive->dc.model = strdup("Cobalt import"); - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &cobalt_cylinders, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query cobalt_cylinders failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_buddy_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &cobalt_buddies, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query cobalt_buddies failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_visibility_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &cobalt_visibility, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query cobalt_visibility failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_location_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &cobalt_location, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query cobalt_location failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_site_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &cobalt_location, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query cobalt_location (site) failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, cur_dive->number); - retval = sqlite3_exec(handle, get_buffer, &cobalt_profile_sample, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query cobalt_profile_sample failed.\n"); - return 1; - } - - dive_end(); - - return SQLITE_OK; -} - - -int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, - struct dive_table *table) -{ - int retval; - char *err = NULL; - target_table = table; - - char get_dives[] = "select i.diveId,timestamp,location||' / '||site,buddy,notes,imperialUnits,maxDepth,maxTime,startSurfacePressure,computerSerial,computerModel FROM dive_info AS i JOIN dive_logs AS l ON i.diveId=l.diveId"; - - retval = sqlite3_exec(handle, get_dives, &shearwater_dive, handle, &err); - - if (retval != SQLITE_OK) { - fprintf(stderr, "Database query failed '%s'.\n", url); - return 1; - } - - return 0; -} - -int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, - struct dive_table *table) -{ - int retval; - char *err = NULL; - target_table = table; - - char get_dives[] = "select Id,strftime('%s',DiveStartTime),LocationId,'buddy','notes',Units,(MaxDepthPressure*10000/SurfacePressure)-10000,DiveMinutes,SurfacePressure,SerialNumber,'model' from Dive where IsViewDeleted = 0"; - - retval = sqlite3_exec(handle, get_dives, &cobalt_dive, handle, &err); - - if (retval != SQLITE_OK) { - fprintf(stderr, "Database query failed '%s'.\n", url); - return 1; - } - - return 0; -} - -extern int divinglog_cylinder(void *handle, int columns, char **data, char **column) -{ - short dbl = 1; - //char get_cylinder_template[] = "select TankID,TankSize,PresS,PresE,PresW,O2,He,DblTank from Tank where LogID = %d"; - - /* - * Divinglog might have more cylinders than what we support. So - * better to ignore those. - */ - - if (cur_cylinder_index >= MAX_CYLINDERS) - return 0; - - if (data[7] && atoi(data[7]) > 0) - dbl = 2; - - cylinder_start(); - - /* - * Assuming that we have to double the cylinder size, if double - * is set - */ - - if (data[1] && atoi(data[1]) > 0) - cur_dive->cylinder[cur_cylinder_index].type.size.mliter = atol(data[1]) * 1000 * dbl; - - if (data[2] && atoi(data[2]) > 0) - cur_dive->cylinder[cur_cylinder_index].start.mbar = atol(data[2]) * 1000; - if (data[3] && atoi(data[3]) > 0) - cur_dive->cylinder[cur_cylinder_index].end.mbar = atol(data[3]) * 1000; - if (data[4] && atoi(data[4]) > 0) - cur_dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = atol(data[4]) * 1000; - if (data[5] && atoi(data[5]) > 0) - cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atol(data[5]) * 10; - if (data[6] && atoi(data[6]) > 0) - cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atol(data[6]) * 10; - - cylinder_end(); - - return 0; -} - -extern int divinglog_profile(void *handle, int columns, char **data, char **column) -{ - int sinterval = 0; - unsigned long i, len, lenprofile2 = 0; - char *ptr, temp[4], pres[5], hbeat[4], stop[4], stime[4], ndl[4], ppo2_1[4], ppo2_2[4], ppo2_3[4], cns[5], setpoint[3]; - short oldcyl = -1; - - /* We do not have samples */ - if (!data[1]) - return 0; - - if (data[0]) - sinterval = atoi(data[0]); - - /* - * Profile - * - * DDDDDCRASWEE - * D: Depth (in meter with two decimals) - * C: Deco (1 = yes, 0 = no) - * R: RBT (Remaining Bottom Time warning) - * A: Ascent warning - * S: Decostop ignored - * W: Work warning - * E: Extra info (different for every computer) - * - * Example: 004500010000 - * 4.5 m, no deco, no RBT warning, ascanding too fast, no decostop ignored, no work, no extra info - * - * - * Profile2 - * - * TTTFFFFIRRR - * - * T: Temperature (in °C with one decimal) - * F: Tank pressure 1 (in bar with one decimal) - * I: Tank ID (0, 1, 2 ... 9) - * R: RBT (in min) - * - * Example: 25518051099 - * 25.5 °C, 180.5 bar, Tank 1, 99 min RBT - * - */ - - len = strlen(data[1]); - - if (data[2]) - lenprofile2 = strlen(data[2]); - - for (i = 0, ptr = data[1]; i * 12 < len; ++i) { - sample_start(); - - cur_sample->time.seconds = sinterval * i; - cur_sample->in_deco = ptr[5] - '0' ? true : false; - ptr[5] = 0; - cur_sample->depth.mm = atoi(ptr) * 10; - - if (i * 11 < lenprofile2) { - memcpy(temp, &data[2][i * 11], 3); - cur_sample->temperature.mkelvin = C_to_mkelvin(atoi(temp) / 10); - } - - if (data[2]) { - memcpy(pres, &data[2][i * 11 + 3], 4); - cur_sample->cylinderpressure.mbar = atoi(pres) * 100; - } - - if (data[3] && strlen(data[3])) { - memcpy(hbeat, &data[3][i * 14 + 8], 3); - cur_sample->heartbeat = atoi(hbeat); - } - - if (data[4] && strlen(data[4])) { - memcpy(stop, &data[4][i * 9 + 6], 3); - cur_sample->stopdepth.mm = atoi(stop) * 1000; - - memcpy(stime, &data[4][i * 9 + 3], 3); - cur_sample->stoptime.seconds = atoi(stime) * 60; - - /* - * Following value is NDL when not in deco, and - * either 0 or TTS when in deco. - */ - - memcpy(ndl, &data[4][i * 9 + 0], 3); - if (cur_sample->in_deco == false) - cur_sample->ndl.seconds = atoi(ndl) * 60; - else if (atoi(ndl)) - cur_sample->tts.seconds = atoi(ndl) * 60; - - if (cur_sample->in_deco == true) - cur_sample->ndl.seconds = 0; - } - - /* - * AAABBBCCCOOOONNNNSS - * - * A = ppO2 cell 1 (measured) - * B = ppO2 cell 2 (measured) - * C = ppO2 cell 3 (measured) - * O = OTU - * N = CNS - * S = Setpoint - * - * Example: 1121131141548026411 - * 1.12 bar, 1.13 bar, 1.14 bar, OTU = 154.8, CNS = 26.4, Setpoint = 1.1 - */ - - if (data[5] && strlen(data[5])) { - memcpy(ppo2_1, &data[5][i * 19 + 0], 3); - memcpy(ppo2_2, &data[5][i * 19 + 3], 3); - memcpy(ppo2_3, &data[5][i * 19 + 6], 3); - memcpy(cns, &data[5][i * 19 + 13], 4); - memcpy(setpoint, &data[5][i * 19 + 17], 2); - - if (atoi(ppo2_1) > 0) - cur_sample->o2sensor[0].mbar = atoi(ppo2_1) * 100; - if (atoi(ppo2_2) > 0) - cur_sample->o2sensor[1].mbar = atoi(ppo2_2) * 100; - if (atoi(ppo2_3) > 0) - cur_sample->o2sensor[2].mbar = atoi(ppo2_3) * 100; - if (atoi(cns) > 0) - cur_sample->cns = rint(atoi(cns) / 10); - if (atoi(setpoint) > 0) - cur_sample->setpoint.mbar = atoi(setpoint) * 100; - - } - - /* - * My best guess is that if we have o2sensors, then it - * is either CCR or PSCR dive. And the first time we - * have O2 sensor readings, we can count them to get - * the amount O2 sensors. - */ - - if (!cur_dive->dc.no_o2sensors) { - cur_dive->dc.no_o2sensors = cur_sample->o2sensor[0].mbar ? 1 : 0 + - cur_sample->o2sensor[1].mbar ? 1 : 0 + - cur_sample->o2sensor[2].mbar ? 1 : 0; - cur_dive->dc.divemode = CCR; - } - - ptr += 12; - sample_end(); - } - - for (i = 0, ptr = data[1]; i * 12 < len; ++i) { - /* Remaining bottom time warning */ - if (ptr[6] - '0') { - event_start(); - cur_event.time.seconds = sinterval * i; - strcpy(cur_event.name, "rbt"); - event_end(); - } - - /* Ascent warning */ - if (ptr[7] - '0') { - event_start(); - cur_event.time.seconds = sinterval * i; - strcpy(cur_event.name, "ascent"); - event_end(); - } - - /* Deco stop ignored */ - if (ptr[8] - '0') { - event_start(); - cur_event.time.seconds = sinterval * i; - strcpy(cur_event.name, "violation"); - event_end(); - } - - /* Workload warning */ - if (ptr[9] - '0') { - event_start(); - cur_event.time.seconds = sinterval * i; - strcpy(cur_event.name, "workload"); - event_end(); - } - ptr += 12; - } - - for (i = 0; i * 11 < lenprofile2; ++i) { - short tank = data[2][i * 11 + 7] - '0'; - if (oldcyl != tank) { - struct gasmix *mix = &cur_dive->cylinder[tank].gasmix; - int o2 = get_o2(mix); - int he = get_he(mix); - - event_start(); - cur_event.time.seconds = sinterval * i; - strcpy(cur_event.name, "gaschange"); - - o2 = (o2 + 5) / 10; - he = (he + 5) / 10; - cur_event.value = o2 + (he << 16); - - event_end(); - oldcyl = tank; - } - } - - return 0; -} - - -extern int divinglog_dive(void *param, int columns, char **data, char **column) -{ - int retval = 0; - sqlite3 *handle = (sqlite3 *)param; - char *err = NULL; - char get_profile_template[] = "select ProfileInt,Profile,Profile2,Profile3,Profile4,Profile5 from Logbook where ID = %d"; - char get_cylinder0_template[] = "select 0,TankSize,PresS,PresE,PresW,O2,He,DblTank from Logbook where ID = %d"; - char get_cylinder_template[] = "select TankID,TankSize,PresS,PresE,PresW,O2,He,DblTank from Tank where LogID = %d order by TankID"; - char get_buffer[1024]; - - dive_start(); - diveid = atoi(data[13]); - cur_dive->number = atoi(data[0]); - - cur_dive->when = (time_t)(atol(data[1])); - - if (data[2]) - cur_dive->dive_site_uuid = find_or_create_dive_site_with_name(data[2], cur_dive->when); - - if (data[3]) - utf8_string(data[3], &cur_dive->buddy); - - if (data[4]) - utf8_string(data[4], &cur_dive->notes); - - if (data[5]) - cur_dive->dc.maxdepth.mm = atof(data[5]) * 1000; - - if (data[6]) - cur_dive->dc.duration.seconds = atoi(data[6]) * 60; - - if (data[7]) - utf8_string(data[7], &cur_dive->divemaster); - - if (data[8]) - cur_dive->airtemp.mkelvin = C_to_mkelvin(atol(data[8])); - - if (data[9]) - cur_dive->watertemp.mkelvin = C_to_mkelvin(atol(data[9])); - - if (data[10]) { - cur_dive->weightsystem[0].weight.grams = atol(data[10]) * 1000; - cur_dive->weightsystem[0].description = strdup(translate("gettextFromC", "unknown")); - } - - if (data[11]) - cur_dive->suit = strdup(data[11]); - - settings_start(); - dc_settings_start(); - - if (data[12]) { - cur_dive->dc.model = strdup(data[12]); - } else { - cur_settings.dc.model = strdup("Divinglog import"); - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder0_template, diveid); - retval = sqlite3_exec(handle, get_buffer, &divinglog_cylinder, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query divinglog_cylinder0 failed.\n"); - return 1; - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, diveid); - retval = sqlite3_exec(handle, get_buffer, &divinglog_cylinder, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query divinglog_cylinder failed.\n"); - return 1; - } - - - dc_settings_end(); - settings_end(); - - if (data[12]) { - cur_dive->dc.model = strdup(data[12]); - } else { - cur_dive->dc.model = strdup("Divinglog import"); - } - - snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, diveid); - retval = sqlite3_exec(handle, get_buffer, &divinglog_profile, 0, &err); - if (retval != SQLITE_OK) { - fprintf(stderr, "%s", "Database query divinglog_profile failed.\n"); - return 1; - } - - dive_end(); - - return SQLITE_OK; -} - - -int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, - struct dive_table *table) -{ - int retval; - char *err = NULL; - target_table = table; - - char get_dives[] = "select Number,strftime('%s',Divedate || ' ' || ifnull(Entrytime,'00:00')),Country || ' - ' || City || ' - ' || Place,Buddy,Comments,Depth,Divetime,Divemaster,Airtemp,Watertemp,Weight,Divesuit,Computer,ID from Logbook where UUID not in (select UUID from DeletedRecords)"; - - retval = sqlite3_exec(handle, get_dives, &divinglog_dive, handle, &err); - - if (retval != SQLITE_OK) { - fprintf(stderr, "Database query failed '%s'.\n", url); - return 1; - } - - return 0; -} - -/* - * Parse a unsigned 32-bit integer in little-endian mode, - * that is seconds since Jan 1, 2000. - */ -static timestamp_t parse_dlf_timestamp(unsigned char *buffer) -{ - timestamp_t offset; - - offset = buffer[3]; - offset = (offset << 8) + buffer[2]; - offset = (offset << 8) + buffer[1]; - offset = (offset << 8) + buffer[0]; - - // Jan 1, 2000 is 946684800 seconds after Jan 1, 1970, which is - // the Unix epoch date that "timestamp_t" uses. - return offset + 946684800; -} - -int parse_dlf_buffer(unsigned char *buffer, size_t size) -{ - unsigned char *ptr = buffer; - unsigned char event; - bool found; - unsigned int time = 0; - int i; - char serial[6]; - - target_table = &dive_table; - - // Check for the correct file magic - if (ptr[0] != 'D' || ptr[1] != 'i' || ptr[2] != 'v' || ptr[3] != 'E') - return -1; - - dive_start(); - divecomputer_start(); - - cur_dc->model = strdup("DLF import"); - // (ptr[7] << 8) + ptr[6] Is "Serial" - snprintf(serial, sizeof(serial), "%d", (ptr[7] << 8) + ptr[6]); - cur_dc->serial = strdup(serial); - cur_dc->when = parse_dlf_timestamp(ptr + 8); - cur_dive->when = cur_dc->when; - - cur_dc->duration.seconds = ((ptr[14] & 0xFE) << 16) + (ptr[13] << 8) + ptr[12]; - - // ptr[14] >> 1 is scrubber used in % - - // 3 bit dive type - switch((ptr[15] & 0x30) >> 3) { - case 0: // unknown - case 1: - cur_dc->divemode = OC; - break; - case 2: - cur_dc->divemode = CCR; - break; - case 3: - cur_dc->divemode = CCR; // mCCR - break; - case 4: - cur_dc->divemode = FREEDIVE; - break; - case 5: - cur_dc->divemode = OC; // Gauge - break; - case 6: - cur_dc->divemode = PSCR; // ASCR - break; - case 7: - cur_dc->divemode = PSCR; - break; - } - - cur_dc->maxdepth.mm = ((ptr[21] << 8) + ptr[20]) * 10; - cur_dc->surface_pressure.mbar = ((ptr[25] << 8) + ptr[24]) / 10; - - /* Done with parsing what we know about the dive header */ - ptr += 32; - - // We're going to interpret ppO2 saved as a sensor value in these modes. - if (cur_dc->divemode == CCR || cur_dc->divemode == PSCR) - cur_dc->no_o2sensors = 1; - - while (ptr < buffer + size) { - time = ((ptr[0] >> 4) & 0x0f) + - ((ptr[1] << 4) & 0xff0) + - (ptr[2] & 0x0f) * 3600; /* hours */ - event = ptr[0] & 0x0f; - switch (event) { - case 0: - /* Regular sample */ - sample_start(); - cur_sample->time.seconds = time; - cur_sample->depth.mm = ((ptr[5] << 8) + ptr[4]) * 10; - // Crazy precision on these stored values... - // Only store value if we're in CCR/PSCR mode, - // because we rather calculate ppo2 our selfs. - if (cur_dc->divemode == CCR || cur_dc->divemode == PSCR) - cur_sample->o2sensor[0].mbar = ((ptr[7] << 8) + ptr[6]) / 10; - // NDL in minutes, 10 bit - cur_sample->ndl.seconds = (((ptr[9] & 0x03) << 8) + ptr[8]) * 60; - // TTS in minutes, 10 bit - cur_sample->tts.seconds = (((ptr[10] & 0x0F) << 6) + (ptr[9] >> 2)) * 60; - // Temperature in 1/10 C, 10 bit signed - cur_sample->temperature.mkelvin = ((ptr[11] & 0x20) ? -1 : 1) * (((ptr[11] & 0x1F) << 4) + (ptr[10] >> 4)) * 100 + ZERO_C_IN_MKELVIN; - // ptr[11] & 0xF0 is unknown, and always 0xC in all checked files - cur_sample->stopdepth.mm = ((ptr[13] << 8) + ptr[12]) * 10; - if (cur_sample->stopdepth.mm) - cur_sample->in_deco = true; - //ptr[14] is helium content, always zero? - //ptr[15] is setpoint, always zero? - sample_end(); - break; - case 1: /* dive event */ - case 2: /* automatic parameter change */ - case 3: /* diver error */ - case 4: /* internal error */ - case 5: /* device activity log */ - event_start(); - cur_event.time.seconds = time; - switch (ptr[4]) { - case 1: - strcpy(cur_event.name, "Setpoint Manual"); - // There is a setpoint value somewhere... - break; - case 2: - strcpy(cur_event.name, "Setpoint Auto"); - // There is a setpoint value somewhere... - switch (ptr[7]) { - case 0: - strcat(cur_event.name, " Manual"); - break; - case 1: - strcat(cur_event.name, " Auto Start"); - break; - case 2: - strcat(cur_event.name, " Auto Hypox"); - break; - case 3: - strcat(cur_event.name, " Auto Timeout"); - break; - case 4: - strcat(cur_event.name, " Auto Ascent"); - break; - case 5: - strcat(cur_event.name, " Auto Stall"); - break; - case 6: - strcat(cur_event.name, " Auto SP Low"); - break; - default: - break; - } - break; - case 3: - // obsolete - strcpy(cur_event.name, "OC"); - break; - case 4: - // obsolete - strcpy(cur_event.name, "CCR"); - break; - case 5: - strcpy(cur_event.name, "gaschange"); - cur_event.type = SAMPLE_EVENT_GASCHANGE2; - cur_event.value = ptr[7] << 8 ^ ptr[6]; - - found = false; - for (i = 0; i < cur_cylinder_index; ++i) { - if (cur_dive->cylinder[i].gasmix.o2.permille == ptr[6] * 10 && cur_dive->cylinder[i].gasmix.he.permille == ptr[7] * 10) { - found = true; - break; - } - } - if (!found) { - cylinder_start(); - cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = ptr[6] * 10; - cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = ptr[7] * 10; - cylinder_end(); - cur_event.gas.index = cur_cylinder_index; - } else { - cur_event.gas.index = i; - } - break; - case 6: - strcpy(cur_event.name, "Start"); - break; - case 7: - strcpy(cur_event.name, "Too Fast"); - break; - case 8: - strcpy(cur_event.name, "Above Ceiling"); - break; - case 9: - strcpy(cur_event.name, "Toxic"); - break; - case 10: - strcpy(cur_event.name, "Hypox"); - break; - case 11: - strcpy(cur_event.name, "Critical"); - break; - case 12: - strcpy(cur_event.name, "Sensor Disabled"); - break; - case 13: - strcpy(cur_event.name, "Sensor Enabled"); - break; - case 14: - strcpy(cur_event.name, "O2 Backup"); - break; - case 15: - strcpy(cur_event.name, "Peer Down"); - break; - case 16: - strcpy(cur_event.name, "HS Down"); - break; - case 17: - strcpy(cur_event.name, "Inconsistent"); - break; - case 18: - // key pressed - probably not - // interesting to view on profile - break; - case 19: - // obsolete - strcpy(cur_event.name, "SCR"); - break; - case 20: - strcpy(cur_event.name, "Above Stop"); - break; - case 21: - strcpy(cur_event.name, "Safety Miss"); - break; - case 22: - strcpy(cur_event.name, "Fatal"); - break; - case 23: - strcpy(cur_event.name, "Diluent"); - break; - case 24: - strcpy(cur_event.name, "gaschange"); - cur_event.type = SAMPLE_EVENT_GASCHANGE2; - cur_event.value = ptr[7] << 8 ^ ptr[6]; - event_end(); - // This is both a mode change and a gas change event - // so we encode it as two separate events. - event_start(); - strcpy(cur_event.name, "Change Mode"); - switch (ptr[8]) { - case 1: - strcat(cur_event.name, ": OC"); - break; - case 2: - strcat(cur_event.name, ": CCR"); - break; - case 3: - strcat(cur_event.name, ": mCCR"); - break; - case 4: - strcat(cur_event.name, ": Free"); - break; - case 5: - strcat(cur_event.name, ": Gauge"); - break; - case 6: - strcat(cur_event.name, ": ASCR"); - break; - case 7: - strcat(cur_event.name, ": PSCR"); - break; - default: - break; - } - event_end(); - break; - case 25: - strcpy(cur_event.name, "CCR O2 solenoid opened/closed"); - break; - case 26: - strcpy(cur_event.name, "User mark"); - break; - case 27: - snprintf(cur_event.name, MAX_EVENT_NAME, "%sGF Switch (%d/%d)", ptr[6] ? "Bailout, ": "", ptr[7], ptr[8]); - break; - case 28: - strcpy(cur_event.name, "Peer Up"); - break; - case 29: - strcpy(cur_event.name, "HS Up"); - break; - case 30: - snprintf(cur_event.name, MAX_EVENT_NAME, "CNS %d%%", ptr[6]); - break; - default: - // No values above 30 had any description - break; - } - event_end(); - break; - case 6: - /* device configuration */ - break; - case 7: - /* measure record */ - /* Po2 sample? Solenoid inject? */ - //fprintf(stderr, "%02X %02X%02X %02X%02X\n", ptr[5], ptr[6], ptr[7], ptr[8], ptr[9]); - break; - default: - /* Unknown... */ - break; - } - ptr += 16; - } - divecomputer_end(); - dive_end(); - return 0; -} - - -void parse_xml_init(void) -{ - LIBXML_TEST_VERSION -} - -void parse_xml_exit(void) -{ - xmlCleanupParser(); -} - -static struct xslt_files { - const char *root; - const char *file; - const char *attribute; -} xslt_files[] = { - { "SUUNTO", "SuuntoSDM.xslt", NULL }, - { "Dive", "SuuntoDM4.xslt", "xmlns" }, - { "Dive", "shearwater.xslt", "version" }, - { "JDiveLog", "jdivelog2subsurface.xslt", NULL }, - { "dives", "MacDive.xslt", NULL }, - { "DIVELOGSDATA", "divelogs.xslt", NULL }, - { "uddf", "uddf.xslt", NULL }, - { "UDDF", "uddf.xslt", NULL }, - { "profile", "udcf.xslt", NULL }, - { "Divinglog", "DivingLog.xslt", NULL }, - { "csv", "csv2xml.xslt", NULL }, - { "sensuscsv", "sensuscsv.xslt", NULL }, - { "SubsurfaceCSV", "subsurfacecsv.xslt", NULL }, - { "manualcsv", "manualcsv2xml.xslt", NULL }, - { "logbook", "DiveLog.xslt", NULL }, - { NULL, } - }; - -static xmlDoc *test_xslt_transforms(xmlDoc *doc, const char **params) -{ - struct xslt_files *info = xslt_files; - xmlDoc *transformed; - xsltStylesheetPtr xslt = NULL; - xmlNode *root_element = xmlDocGetRootElement(doc); - char *attribute; - - while (info->root) { - if ((strcasecmp((const char *)root_element->name, info->root) == 0)) { - if (info->attribute == NULL) - break; - else if (xmlGetProp(root_element, (const xmlChar *)info->attribute) != NULL) - break; - } - info++; - } - - if (info->root) { - attribute = (char *)xmlGetProp(xmlFirstElementChild(root_element), (const xmlChar *)"name"); - if (attribute) { - if (strcasecmp(attribute, "subsurface") == 0) { - free((void *)attribute); - return doc; - } - free((void *)attribute); - } - xmlSubstituteEntitiesDefault(1); - xslt = get_stylesheet(info->file); - if (xslt == NULL) { - report_error(translate("gettextFromC", "Can't open stylesheet %s"), info->file); - return doc; - } - transformed = xsltApplyStylesheet(xslt, doc, params); - xmlFreeDoc(doc); - xsltFreeStylesheet(xslt); - - return transformed; - } - return doc; -} diff --git a/planner.c b/planner.c deleted file mode 100644 index 22d37165a..000000000 --- a/planner.c +++ /dev/null @@ -1,1455 +0,0 @@ -/* planner.c - * - * code that allows us to plan future dives - * - * (c) Dirk Hohndel 2013 - */ -#include -#include -#include -#include -#include "dive.h" -#include "divelist.h" -#include "planner.h" -#include "gettext.h" -#include "libdivecomputer/parser.h" - -#define TIMESTEP 2 /* second */ -#define DECOTIMESTEP 60 /* seconds. Unit of deco stop times */ - -int decostoplevels_metric[] = { 0, 3000, 6000, 9000, 12000, 15000, 18000, 21000, 24000, 27000, - 30000, 33000, 36000, 39000, 42000, 45000, 48000, 51000, 54000, 57000, - 60000, 63000, 66000, 69000, 72000, 75000, 78000, 81000, 84000, 87000, - 90000, 100000, 110000, 120000, 130000, 140000, 150000, 160000, 170000, - 180000, 190000, 200000, 220000, 240000, 260000, 280000, 300000, - 320000, 340000, 360000, 380000 }; -int decostoplevels_imperial[] = { 0, 3048, 6096, 9144, 12192, 15240, 18288, 21336, 24384, 27432, - 30480, 33528, 36576, 39624, 42672, 45720, 48768, 51816, 54864, 57912, - 60960, 64008, 67056, 70104, 73152, 76200, 79248, 82296, 85344, 88392, - 91440, 101600, 111760, 121920, 132080, 142240, 152400, 162560, 172720, - 182880, 193040, 203200, 223520, 243840, 264160, 284480, 304800, - 325120, 345440, 365760, 386080 }; - -double plangflow, plangfhigh; -bool plan_verbatim, plan_display_runtime, plan_display_duration, plan_display_transitions; - -pressure_t first_ceiling_pressure, max_bottom_ceiling_pressure = {}; - -const char *disclaimer; - -#if DEBUG_PLAN -void dump_plan(struct diveplan *diveplan) -{ - struct divedatapoint *dp; - struct tm tm; - - if (!diveplan) { - printf("Diveplan NULL\n"); - return; - } - utc_mkdate(diveplan->when, &tm); - - printf("\nDiveplan @ %04d-%02d-%02d %02d:%02d:%02d (surfpres %dmbar):\n", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec, - diveplan->surface_pressure); - dp = diveplan->dp; - while (dp) { - printf("\t%3u:%02u: %dmm gas: %d o2 %d h2\n", FRACTION(dp->time, 60), dp->depth, get_o2(&dp->gasmix), get_he(&dp->gasmix)); - dp = dp->next; - } -} -#endif - -bool diveplan_empty(struct diveplan *diveplan) -{ - struct divedatapoint *dp; - if (!diveplan || !diveplan->dp) - return true; - dp = diveplan->dp; - while (dp) { - if (dp->time) - return false; - dp = dp->next; - } - return true; -} - -/* get the gas at a certain time during the dive */ -void get_gas_at_time(struct dive *dive, struct divecomputer *dc, duration_t time, struct gasmix *gas) -{ - // we always start with the first gas, so that's our gas - // unless an event tells us otherwise - struct event *event = dc->events; - *gas = dive->cylinder[0].gasmix; - while (event && event->time.seconds <= time.seconds) { - if (!strcmp(event->name, "gaschange")) { - int cylinder_idx = get_cylinder_index(dive, event); - *gas = dive->cylinder[cylinder_idx].gasmix; - } - event = event->next; - } -} - -int get_gasidx(struct dive *dive, struct gasmix *mix) -{ - int gasidx = -1; - - while (++gasidx < MAX_CYLINDERS) - if (gasmix_distance(&dive->cylinder[gasidx].gasmix, mix) < 100) - return gasidx; - return -1; -} - -void interpolate_transition(struct dive *dive, duration_t t0, duration_t t1, depth_t d0, depth_t d1, const struct gasmix *gasmix, o2pressure_t po2) -{ - int j; - - for (j = t0.seconds; j < t1.seconds; j++) { - int depth = interpolate(d0.mm, d1.mm, j - t0.seconds, t1.seconds - t0.seconds); - add_segment(depth_to_bar(depth, dive), gasmix, 1, po2.mbar, dive, prefs.bottomsac); - } - if (d1.mm > d0.mm) - calc_crushing_pressure(depth_to_bar(d1.mm, &displayed_dive)); -} - -/* returns the tissue tolerance at the end of this (partial) dive */ -void tissue_at_end(struct dive *dive, char **cached_datap) -{ - struct divecomputer *dc; - struct sample *sample, *psample; - int i; - depth_t lastdepth = {}; - duration_t t0 = {}, t1 = {}; - struct gasmix gas; - - if (!dive) - return; - if (*cached_datap) { - restore_deco_state(*cached_datap); - } else { - init_decompression(dive); - cache_deco_state(cached_datap); - } - dc = &dive->dc; - if (!dc->samples) - return; - psample = sample = dc->sample; - - for (i = 0; i < dc->samples; i++, sample++) { - o2pressure_t setpoint; - - if (i) - setpoint = sample[-1].setpoint; - else - setpoint = sample[0].setpoint; - - t1 = sample->time; - get_gas_at_time(dive, dc, t0, &gas); - if (i > 0) - lastdepth = psample->depth; - - /* The ceiling in the deeper portion of a multilevel dive is sometimes critical for the VPM-B - * Boyle's law compensation. We should check the ceiling prior to ascending during the bottom - * portion of the dive. The maximum ceiling might be reached while ascending, but testing indicates - * that it is only marginally deeper than the ceiling at the start of ascent. - * Do not set the first_ceiling_pressure variable (used for the Boyle's law compensation calculation) - * at this stage, because it would interfere with calculating the ceiling at the end of the bottom - * portion of the dive. - * Remember the value for later. - */ - if ((prefs.deco_mode == VPMB) && (lastdepth.mm > sample->depth.mm)) { - pressure_t ceiling_pressure; - nuclear_regeneration(t0.seconds); - vpmb_start_gradient(); - ceiling_pressure.mbar = depth_to_mbar(deco_allowed_depth(tissue_tolerance_calc(dive, - depth_to_bar(lastdepth.mm, dive)), - dive->surface_pressure.mbar / 1000.0, - dive, - 1), - dive); - if (ceiling_pressure.mbar > max_bottom_ceiling_pressure.mbar) - max_bottom_ceiling_pressure.mbar = ceiling_pressure.mbar; - } - - interpolate_transition(dive, t0, t1, lastdepth, sample->depth, &gas, setpoint); - psample = sample; - t0 = t1; - } -} - - -/* if a default cylinder is set, use that */ -void fill_default_cylinder(cylinder_t *cyl) -{ - const char *cyl_name = prefs.default_cylinder; - struct tank_info_t *ti = tank_info; - pressure_t pO2 = {.mbar = 1600}; - - if (!cyl_name) - return; - while (ti->name != NULL) { - if (strcmp(ti->name, cyl_name) == 0) - break; - ti++; - } - if (ti->name == NULL) - /* didn't find it */ - return; - cyl->type.description = strdup(ti->name); - if (ti->ml) { - cyl->type.size.mliter = ti->ml; - cyl->type.workingpressure.mbar = ti->bar * 1000; - } else { - cyl->type.workingpressure.mbar = psi_to_mbar(ti->psi); - if (ti->psi) - cyl->type.size.mliter = cuft_to_l(ti->cuft) * 1000 / bar_to_atm(psi_to_bar(ti->psi)); - } - // MOD of air - cyl->depth = gas_mod(&cyl->gasmix, pO2, &displayed_dive, 1); -} - -/* make sure that the gas we are switching to is represented in our - * list of cylinders */ -static int verify_gas_exists(struct gasmix mix_in) -{ - int i; - cylinder_t *cyl; - - for (i = 0; i < MAX_CYLINDERS; i++) { - cyl = displayed_dive.cylinder + i; - if (cylinder_nodata(cyl)) - continue; - if (gasmix_distance(&cyl->gasmix, &mix_in) < 100) - return i; - } - fprintf(stderr, "this gas %s should have been on the cylinder list\nThings will fail now\n", gasname(&mix_in)); - return -1; -} - -/* calculate the new end pressure of the cylinder, based on its current end pressure and the - * latest segment. */ -static void update_cylinder_pressure(struct dive *d, int old_depth, int new_depth, int duration, int sac, cylinder_t *cyl, bool in_deco) -{ - volume_t gas_used; - pressure_t delta_p; - depth_t mean_depth; - int factor = 1000; - - if (d->dc.divemode == PSCR) - factor = prefs.pscr_ratio; - - if (!cyl) - return; - mean_depth.mm = (old_depth + new_depth) / 2; - gas_used.mliter = depth_to_atm(mean_depth.mm, d) * sac / 60 * duration * factor / 1000; - cyl->gas_used.mliter += gas_used.mliter; - if (in_deco) - cyl->deco_gas_used.mliter += gas_used.mliter; - if (cyl->type.size.mliter) { - delta_p.mbar = gas_used.mliter * 1000.0 / cyl->type.size.mliter; - cyl->end.mbar -= delta_p.mbar; - } -} - -/* simply overwrite the data in the displayed_dive - * return false if something goes wrong */ -static void create_dive_from_plan(struct diveplan *diveplan, bool track_gas) -{ - struct divedatapoint *dp; - struct divecomputer *dc; - struct sample *sample; - struct gasmix oldgasmix; - struct event *ev; - cylinder_t *cyl; - int oldpo2 = 0; - int lasttime = 0; - int lastdepth = 0; - enum dive_comp_type type = displayed_dive.dc.divemode; - - if (!diveplan || !diveplan->dp) - return; -#if DEBUG_PLAN & 4 - printf("in create_dive_from_plan\n"); - dump_plan(diveplan); -#endif - displayed_dive.salinity = diveplan->salinity; - // reset the cylinders and clear out the samples and events of the - // displayed dive so we can restart - reset_cylinders(&displayed_dive, track_gas); - dc = &displayed_dive.dc; - dc->when = displayed_dive.when = diveplan->when; - free(dc->sample); - dc->sample = NULL; - dc->samples = 0; - dc->alloc_samples = 0; - while ((ev = dc->events)) { - dc->events = dc->events->next; - free(ev); - } - dp = diveplan->dp; - cyl = &displayed_dive.cylinder[0]; - oldgasmix = cyl->gasmix; - sample = prepare_sample(dc); - sample->setpoint.mbar = dp->setpoint; - sample->sac.mliter = prefs.bottomsac; - oldpo2 = dp->setpoint; - if (track_gas && cyl->type.workingpressure.mbar) - sample->cylinderpressure.mbar = cyl->end.mbar; - sample->manually_entered = true; - finish_sample(dc); - while (dp) { - struct gasmix gasmix = dp->gasmix; - int po2 = dp->setpoint; - if (dp->setpoint) - type = CCR; - int time = dp->time; - int depth = dp->depth; - - if (time == 0) { - /* special entries that just inform the algorithm about - * additional gases that are available */ - if (verify_gas_exists(gasmix) < 0) - goto gas_error_exit; - dp = dp->next; - continue; - } - - /* Check for SetPoint change */ - if (oldpo2 != po2) { - /* this is a bad idea - we should get a different SAMPLE_EVENT type - * reserved for this in libdivecomputer... overloading SMAPLE_EVENT_PO2 - * with a different meaning will only cause confusion elsewhere in the code */ - add_event(dc, lasttime, SAMPLE_EVENT_PO2, 0, po2, "SP change"); - oldpo2 = po2; - } - - /* Make sure we have the new gas, and create a gas change event */ - if (gasmix_distance(&gasmix, &oldgasmix) > 0) { - int idx; - if ((idx = verify_gas_exists(gasmix)) < 0) - goto gas_error_exit; - /* need to insert a first sample for the new gas */ - add_gas_switch_event(&displayed_dive, dc, lasttime + 1, idx); - cyl = &displayed_dive.cylinder[idx]; - sample = prepare_sample(dc); - sample[-1].setpoint.mbar = po2; - sample->time.seconds = lasttime + 1; - sample->depth.mm = lastdepth; - sample->manually_entered = dp->entered; - sample->sac.mliter = dp->entered ? prefs.bottomsac : prefs.decosac; - if (track_gas && cyl->type.workingpressure.mbar) - sample->cylinderpressure.mbar = cyl->sample_end.mbar; - finish_sample(dc); - oldgasmix = gasmix; - } - /* Create sample */ - sample = prepare_sample(dc); - /* set po2 at beginning of this segment */ - /* and keep it valid for last sample - where it likely doesn't matter */ - sample[-1].setpoint.mbar = po2; - sample->setpoint.mbar = po2; - sample->time.seconds = lasttime = time; - sample->depth.mm = lastdepth = depth; - sample->manually_entered = dp->entered; - sample->sac.mliter = dp->entered ? prefs.bottomsac : prefs.decosac; - if (track_gas && !sample[-1].setpoint.mbar) { /* Don't track gas usage for CCR legs of dive */ - update_cylinder_pressure(&displayed_dive, sample[-1].depth.mm, depth, time - sample[-1].time.seconds, - dp->entered ? diveplan->bottomsac : diveplan->decosac, cyl, !dp->entered); - if (cyl->type.workingpressure.mbar) - sample->cylinderpressure.mbar = cyl->end.mbar; - } - finish_sample(dc); - dp = dp->next; - } - dc->divemode = type; -#if DEBUG_PLAN & 32 - save_dive(stdout, &displayed_dive); -#endif - return; - -gas_error_exit: - report_error(translate("gettextFromC", "Too many gas mixes")); - return; -} - -void free_dps(struct diveplan *diveplan) -{ - if (!diveplan) - return; - struct divedatapoint *dp = diveplan->dp; - while (dp) { - struct divedatapoint *ndp = dp->next; - free(dp); - dp = ndp; - } - diveplan->dp = NULL; -} - -struct divedatapoint *create_dp(int time_incr, int depth, struct gasmix gasmix, int po2) -{ - struct divedatapoint *dp; - - dp = malloc(sizeof(struct divedatapoint)); - dp->time = time_incr; - dp->depth = depth; - dp->gasmix = gasmix; - dp->setpoint = po2; - dp->entered = false; - dp->next = NULL; - return dp; -} - -void add_to_end_of_diveplan(struct diveplan *diveplan, struct divedatapoint *dp) -{ - struct divedatapoint **lastdp = &diveplan->dp; - struct divedatapoint *ldp = *lastdp; - int lasttime = 0; - while (*lastdp) { - ldp = *lastdp; - if (ldp->time > lasttime) - lasttime = ldp->time; - lastdp = &(*lastdp)->next; - } - *lastdp = dp; - if (ldp && dp->time != 0) - dp->time += lasttime; -} - -struct divedatapoint *plan_add_segment(struct diveplan *diveplan, int duration, int depth, struct gasmix gasmix, int po2, bool entered) -{ - struct divedatapoint *dp = create_dp(duration, depth, gasmix, po2); - dp->entered = entered; - add_to_end_of_diveplan(diveplan, dp); - return (dp); -} - -struct gaschanges { - int depth; - int gasidx; -}; - - -static struct gaschanges *analyze_gaslist(struct diveplan *diveplan, int *gaschangenr, int depth, int *asc_cylinder) -{ - struct gasmix gas; - int nr = 0; - struct gaschanges *gaschanges = NULL; - struct divedatapoint *dp = diveplan->dp; - int best_depth = displayed_dive.cylinder[*asc_cylinder].depth.mm; - while (dp) { - if (dp->time == 0) { - gas = dp->gasmix; - if (dp->depth <= depth) { - int i = 0; - nr++; - gaschanges = realloc(gaschanges, nr * sizeof(struct gaschanges)); - while (i < nr - 1) { - if (dp->depth < gaschanges[i].depth) { - memmove(gaschanges + i + 1, gaschanges + i, (nr - i - 1) * sizeof(struct gaschanges)); - break; - } - i++; - } - gaschanges[i].depth = dp->depth; - gaschanges[i].gasidx = get_gasidx(&displayed_dive, &gas); - assert(gaschanges[i].gasidx != -1); - } else { - /* is there a better mix to start deco? */ - if (dp->depth < best_depth) { - best_depth = dp->depth; - *asc_cylinder = get_gasidx(&displayed_dive, &gas); - } - } - } - dp = dp->next; - } - *gaschangenr = nr; -#if DEBUG_PLAN & 16 - for (nr = 0; nr < *gaschangenr; nr++) { - int idx = gaschanges[nr].gasidx; - printf("gaschange nr %d: @ %5.2lfm gasidx %d (%s)\n", nr, gaschanges[nr].depth / 1000.0, - idx, gasname(&displayed_dive.cylinder[idx].gasmix)); - } -#endif - return gaschanges; -} - -/* sort all the stops into one ordered list */ -static unsigned int *sort_stops(int *dstops, int dnr, struct gaschanges *gstops, int gnr) -{ - int i, gi, di; - int total = dnr + gnr; - unsigned int *stoplevels = malloc(total * sizeof(int)); - - /* no gaschanges */ - if (gnr == 0) { - memcpy(stoplevels, dstops, dnr * sizeof(int)); - return stoplevels; - } - i = total - 1; - gi = gnr - 1; - di = dnr - 1; - while (i >= 0) { - if (dstops[di] > gstops[gi].depth) { - stoplevels[i] = dstops[di]; - di--; - } else if (dstops[di] == gstops[gi].depth) { - stoplevels[i] = dstops[di]; - di--; - gi--; - } else { - stoplevels[i] = gstops[gi].depth; - gi--; - } - i--; - if (di < 0) { - while (gi >= 0) - stoplevels[i--] = gstops[gi--].depth; - break; - } - if (gi < 0) { - while (di >= 0) - stoplevels[i--] = dstops[di--]; - break; - } - } - while (i >= 0) - stoplevels[i--] = 0; - -#if DEBUG_PLAN & 16 - int k; - for (k = gnr + dnr - 1; k >= 0; k--) { - printf("stoplevel[%d]: %5.2lfm\n", k, stoplevels[k] / 1000.0); - if (stoplevels[k] == 0) - break; - } -#endif - return stoplevels; -} - -static void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, int error) -{ - const unsigned int sz_buffer = 2000000; - const unsigned int sz_temp = 100000; - char *buffer = (char *)malloc(sz_buffer); - char *temp = (char *)malloc(sz_temp); - char buf[1000], *deco; - int len, lastdepth = 0, lasttime = 0, lastsetpoint = -1, newdepth = 0, lastprintdepth = 0, lastprintsetpoint = -1; - struct gasmix lastprintgasmix = {{ -1 }, { -1 }}; - struct divedatapoint *dp = diveplan->dp; - bool gaschange_after = !plan_verbatim; - bool gaschange_before; - bool lastentered = true; - struct divedatapoint *nextdp = NULL; - - plan_verbatim = prefs.verbatim_plan; - plan_display_runtime = prefs.display_runtime; - plan_display_duration = prefs.display_duration; - plan_display_transitions = prefs.display_transitions; - - if (prefs.deco_mode == VPMB) { - deco = "VPM-B"; - } else { - deco = "BUHLMANN"; - } - - snprintf(buf, sizeof(buf), translate("gettextFromC", "DISCLAIMER / WARNING: THIS IS A NEW IMPLEMENTATION OF THE %s " - "ALGORITHM AND A DIVE PLANNER IMPLEMENTATION BASED ON THAT WHICH HAS " - "RECEIVED ONLY A LIMITED AMOUNT OF TESTING. WE STRONGLY RECOMMEND NOT TO " - "PLAN DIVES SIMPLY BASED ON THE RESULTS GIVEN HERE."), deco); - disclaimer = buf; - - if (!dp) { - free((void *)buffer); - free((void *)temp); - return; - } - - if (error) { - snprintf(temp, sz_temp, "%s", - translate("gettextFromC", "Decompression calculation aborted due to excessive time")); - snprintf(buffer, sz_buffer, "%s %s
", - translate("gettextFromC", "Warning:"), temp); - dive->notes = strdup(buffer); - - free((void *)buffer); - free((void *)temp); - return; - } - - len = show_disclaimer ? snprintf(buffer, sz_buffer, "
%s

", disclaimer) : 0; - if (prefs.deco_mode == BUEHLMANN){ - snprintf(temp, sz_temp, translate("gettextFromC", "based on Bühlmann ZHL-16B with GFlow = %d and GFhigh = %d"), - diveplan->gflow, diveplan->gfhigh); - } else if (prefs.deco_mode == VPMB){ - if (prefs.conservatism_level == 0) - snprintf(temp, sz_temp, "%s", translate("gettextFromC", "based on VPM-B at nominal conservatism")); - else - snprintf(temp, sz_temp, translate("gettextFromC", "based on VPM-B at +%d conservatism"), prefs.conservatism_level); - } else if (prefs.deco_mode == RECREATIONAL){ - snprintf(temp, sz_temp, translate("gettextFromC", "recreational mode based on Bühlmann ZHL-16B with GFlow = %d and GFhigh = %d"), - diveplan->gflow, diveplan->gfhigh); - } - len += snprintf(buffer + len, sz_buffer - len, "
%s
%s

", - translate("gettextFromC", "Subsurface dive plan"), temp); - - if (!plan_verbatim) { - len += snprintf(buffer + len, sz_buffer - len, "
", - translate("gettextFromC", "depth")); - if (plan_display_duration) - len += snprintf(buffer + len, sz_buffer - len, "", - translate("gettextFromC", "duration")); - if (plan_display_runtime) - len += snprintf(buffer + len, sz_buffer - len, "", - translate("gettextFromC", "runtime")); - len += snprintf(buffer + len, sz_buffer - len, - "", - translate("gettextFromC", "gas")); - } - do { - struct gasmix gasmix, newgasmix = {}; - const char *depth_unit; - double depthvalue; - int decimals; - bool isascent = (dp->depth < lastdepth); - - nextdp = dp->next; - if (dp->time == 0) - continue; - gasmix = dp->gasmix; - depthvalue = get_depth_units(dp->depth, &decimals, &depth_unit); - /* analyze the dive points ahead */ - while (nextdp && nextdp->time == 0) - nextdp = nextdp->next; - if (nextdp) - newgasmix = nextdp->gasmix; - gaschange_after = (nextdp && (gasmix_distance(&gasmix, &newgasmix) || dp->setpoint != nextdp->setpoint)); - gaschange_before = (gasmix_distance(&lastprintgasmix, &gasmix) || lastprintsetpoint != dp->setpoint); - /* do we want to skip this leg as it is devoid of anything useful? */ - if (!dp->entered && - nextdp && - dp->depth != lastdepth && - nextdp->depth != dp->depth && - !gaschange_before && - !gaschange_after) - continue; - if (dp->time - lasttime < 10 && !(gaschange_after && dp->next && dp->depth != dp->next->depth)) - continue; - - len = strlen(buffer); - if (plan_verbatim) { - /* When displaying a verbatim plan, we output a waypoint for every gas change. - * Therefore, we do not need to test for difficult cases that mean we need to - * print a segment just so we don't miss a gas change. This makes the logic - * to determine whether or not to print a segment much simpler than with the - * non-verbatim plan. - */ - if (dp->depth != lastprintdepth) { - if (plan_display_transitions || dp->entered || !dp->next || (gaschange_after && dp->next && dp->depth != nextdp->depth)) { - if (dp->setpoint) - snprintf(temp, sz_temp, translate("gettextFromC", "Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s (SP = %.1fbar)"), - decimals, depthvalue, depth_unit, - FRACTION(dp->time - lasttime, 60), - FRACTION(dp->time, 60), - gasname(&gasmix), - (double) dp->setpoint / 1000.0); - - else - snprintf(temp, sz_temp, translate("gettextFromC", "Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s"), - decimals, depthvalue, depth_unit, - FRACTION(dp->time - lasttime, 60), - FRACTION(dp->time, 60), - gasname(&gasmix)); - - len += snprintf(buffer + len, sz_buffer - len, "%s
", temp); - } - newdepth = dp->depth; - lasttime = dp->time; - } else { - if ((nextdp && dp->depth != nextdp->depth) || gaschange_after) { - if (dp->setpoint) - snprintf(temp, sz_temp, translate("gettextFromC", "Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s (SP = %.1fbar)"), - decimals, depthvalue, depth_unit, - FRACTION(dp->time - lasttime, 60), - FRACTION(dp->time, 60), - gasname(&gasmix), - (double) dp->setpoint / 1000.0); - else - snprintf(temp, sz_temp, translate("gettextFromC", "Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s"), - decimals, depthvalue, depth_unit, - FRACTION(dp->time - lasttime, 60), - FRACTION(dp->time, 60), - gasname(&gasmix)); - - len += snprintf(buffer + len, sz_buffer - len, "%s
", temp); - newdepth = dp->depth; - lasttime = dp->time; - } - } - } else { - /* When not displaying the verbatim dive plan, we typically ignore ascents between deco stops, - * unless the display transitions option has been selected. We output a segment if any of the - * following conditions are met. - * 1) Display transitions is selected - * 2) The segment was manually entered - * 3) It is the last segment of the dive - * 4) The segment is not an ascent, there was a gas change at the start of the segment and the next segment - * is a change in depth (typical deco stop) - * 5) There is a gas change at the end of the segment and the last segment was entered (first calculated - * segment if it ends in a gas change) - * 6) There is a gaschange after but no ascent. This should only occur when backgas breaks option is selected - * 7) It is an ascent ending with a gas change, but is not followed by a stop. As case 5 already matches - * the first calculated ascent if it ends with a gas change, this should only occur if a travel gas is - * used for a calculated ascent, there is a subsequent gas change before the first deco stop, and zero - * time has been allowed for a gas switch. - */ - if (plan_display_transitions || dp->entered || !dp->next || - (nextdp && dp->depth != nextdp->depth) || - (!isascent && gaschange_before && nextdp && dp->depth != nextdp->depth) || - (gaschange_after && lastentered) || (gaschange_after && !isascent) || - (isascent && gaschange_after && nextdp && dp->depth != nextdp->depth )) { - snprintf(temp, sz_temp, translate("gettextFromC", "%3.0f%s"), depthvalue, depth_unit); - len += snprintf(buffer + len, sz_buffer - len, "", temp); - if (plan_display_duration) { - snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time - lasttime + 30) / 60); - len += snprintf(buffer + len, sz_buffer - len, "", temp); - } - if (plan_display_runtime) { - snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time + 30) / 60); - len += snprintf(buffer + len, sz_buffer - len, "", temp); - } - - /* Normally a gas change is displayed on the stopping segment, so only display a gas change at the end of - * an ascent segment if it is not followed by a stop - */ - if (isascent && gaschange_after && dp->next && nextdp && dp->depth != nextdp->depth) { - if (dp->setpoint) { - snprintf(temp, sz_temp, translate("gettextFromC", "(SP = %.1fbar)"), (double) nextdp->setpoint / 1000.0); - len += snprintf(buffer + len, sz_buffer - len, "", gasname(&newgasmix), - temp); - } else { - len += snprintf(buffer + len, sz_buffer - len, "", gasname(&newgasmix)); - } - lastprintsetpoint = nextdp->setpoint; - lastprintgasmix = newgasmix; - gaschange_after = false; - } else if (gaschange_before) { - // If a new gas has been used for this segment, now is the time to show it - if (dp->setpoint) { - snprintf(temp, sz_temp, translate("gettextFromC", "(SP = %.1fbar)"), (double) dp->setpoint / 1000.0); - len += snprintf(buffer + len, sz_buffer - len, "", gasname(&gasmix), - temp); - } else { - len += snprintf(buffer + len, sz_buffer - len, "", gasname(&gasmix)); - } - // Set variables so subsequent iterations can test against the last gas printed - lastprintsetpoint = dp->setpoint; - lastprintgasmix = gasmix; - gaschange_after = false; - } else { - len += snprintf(buffer + len, sz_buffer - len, ""); - } - len += snprintf(buffer + len, sz_buffer - len, ""); - newdepth = dp->depth; - lasttime = dp->time; - } - } - if (gaschange_after) { - // gas switch at this waypoint - if (plan_verbatim) { - if (lastsetpoint >= 0) { - if (nextdp && nextdp->setpoint) - snprintf(temp, sz_temp, translate("gettextFromC", "Switch gas to %s (SP = %.1fbar)"), gasname(&newgasmix), (double) nextdp->setpoint / 1000.0); - else - snprintf(temp, sz_temp, translate("gettextFromC", "Switch gas to %s"), gasname(&newgasmix)); - - len += snprintf(buffer + len, sz_buffer - len, "%s
", temp); - } - gaschange_after = false; - gasmix = newgasmix; - } - } - lastprintdepth = newdepth; - lastdepth = dp->depth; - lastsetpoint = dp->setpoint; - lastentered = dp->entered; - } while ((dp = nextdp) != NULL); - len += snprintf(buffer + len, sz_buffer - len, "
%s%s%s%s
%s%s%s%s %s%s%s %s%s 
"); - - dive->cns = 0; - dive->maxcns = 0; - update_cylinder_related_info(dive); - snprintf(temp, sz_temp, "%s", translate("gettextFromC", "CNS")); - len += snprintf(buffer + len, sz_buffer - len, "

%s: %i%%", temp, dive->cns); - snprintf(temp, sz_temp, "%s", translate("gettextFromC", "OTU")); - len += snprintf(buffer + len, sz_buffer - len, "
%s: %i
", temp, dive->otu); - - if (dive->dc.divemode == CCR) - snprintf(temp, sz_temp, "%s", translate("gettextFromC", "Gas consumption (CCR legs excluded):")); - else - snprintf(temp, sz_temp, "%s", translate("gettextFromC", "Gas consumption:")); - len += snprintf(buffer + len, sz_buffer - len, "

%s
", temp); - for (int gasidx = 0; gasidx < MAX_CYLINDERS; gasidx++) { - double volume, pressure, deco_volume, deco_pressure; - const char *unit, *pressure_unit; - char warning[1000] = ""; - cylinder_t *cyl = &dive->cylinder[gasidx]; - if (cylinder_none(cyl)) - break; - - volume = get_volume_units(cyl->gas_used.mliter, NULL, &unit); - deco_volume = get_volume_units(cyl->deco_gas_used.mliter, NULL, &unit); - if (cyl->type.size.mliter) { - deco_pressure = get_pressure_units(1000.0 * cyl->deco_gas_used.mliter / cyl->type.size.mliter, &pressure_unit); - pressure = get_pressure_units(1000.0 * cyl->gas_used.mliter / cyl->type.size.mliter, &pressure_unit); - /* Warn if the plan uses more gas than is available in a cylinder - * This only works if we have working pressure for the cylinder - * 10bar is a made up number - but it seemed silly to pretend you could breathe cylinder down to 0 */ - if (cyl->end.mbar < 10000) - snprintf(warning, sizeof(warning), " — %s %s", - translate("gettextFromC", "Warning:"), - translate("gettextFromC", "this is more gas than available in the specified cylinder!")); - else - if ((float) cyl->end.mbar * cyl->type.size.mliter / 1000.0 < (float) cyl->deco_gas_used.mliter) - snprintf(warning, sizeof(warning), " — %s %s", - translate("gettextFromC", "Warning:"), - translate("gettextFromC", "not enough reserve for gas sharing on ascent!")); - - snprintf(temp, sz_temp, translate("gettextFromC", "%.0f%s/%.0f%s of %s (%.0f%s/%.0f%s in planned ascent)"), volume, unit, pressure, pressure_unit, gasname(&cyl->gasmix), deco_volume, unit, deco_pressure, pressure_unit); - } else { - snprintf(temp, sz_temp, translate("gettextFromC", "%.0f%s (%.0f%s during planned ascent) of %s"), volume, unit, deco_volume, unit, gasname(&cyl->gasmix)); - } - len += snprintf(buffer + len, sz_buffer - len, "%s%s
", temp, warning); - } - dp = diveplan->dp; - if (dive->dc.divemode != CCR) { - while (dp) { - if (dp->time != 0) { - struct gas_pressures pressures; - fill_pressures(&pressures, depth_to_atm(dp->depth, dive), &dp->gasmix, 0.0, dive->dc.divemode); - - if (pressures.o2 > (dp->entered ? prefs.bottompo2 : prefs.decopo2) / 1000.0) { - const char *depth_unit; - int decimals; - double depth_value = get_depth_units(dp->depth, &decimals, &depth_unit); - len = strlen(buffer); - snprintf(temp, sz_temp, - translate("gettextFromC", "high pOâ‚‚ value %.2f at %d:%02u with gas %s at depth %.*f %s"), - pressures.o2, FRACTION(dp->time, 60), gasname(&dp->gasmix), decimals, depth_value, depth_unit); - len += snprintf(buffer + len, sz_buffer - len, "%s %s
", - translate("gettextFromC", "Warning:"), temp); - } else if (pressures.o2 < 0.16) { - const char *depth_unit; - int decimals; - double depth_value = get_depth_units(dp->depth, &decimals, &depth_unit); - len = strlen(buffer); - snprintf(temp, sz_temp, - translate("gettextFromC", "low pOâ‚‚ value %.2f at %d:%02u with gas %s at depth %.*f %s"), - pressures.o2, FRACTION(dp->time, 60), gasname(&dp->gasmix), decimals, depth_value, depth_unit); - len += snprintf(buffer + len, sz_buffer - len, "%s %s
", - translate("gettextFromC", "Warning:"), temp); - - } - } - dp = dp->next; - } - } - snprintf(buffer + len, sz_buffer - len, "
"); - dive->notes = strdup(buffer); - - free((void *)buffer); - free((void *)temp); -} - -int ascent_velocity(int depth, int avg_depth, int bottom_time) -{ - /* We need to make this configurable */ - - /* As an example (and possibly reasonable default) this is the Tech 1 provedure according - * to http://www.globalunderwaterexplorers.org/files/Standards_and_Procedures/SOP_Manual_Ver2.0.2.pdf */ - - if (depth * 4 > avg_depth * 3) { - return prefs.ascrate75; - } else { - if (depth * 2 > avg_depth) { - return prefs.ascrate50; - } else { - if (depth > 6000) - return prefs.ascratestops; - else - return prefs.ascratelast6m; - } - } -} - -void track_ascent_gas(int depth, cylinder_t *cylinder, int avg_depth, int bottom_time, bool safety_stop) -{ - while (depth > 0) { - int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; - if (deltad > depth) - deltad = depth; - update_cylinder_pressure(&displayed_dive, depth, depth - deltad, TIMESTEP, prefs.decosac, cylinder, true); - if (depth <= 5000 && depth >= (5000 - deltad) && safety_stop) { - update_cylinder_pressure(&displayed_dive, 5000, 5000, 180, prefs.decosac, cylinder, true); - safety_stop = false; - } - depth -= deltad; - } -} - -// Determine whether ascending to the next stop will break the ceiling. Return true if the ascent is ok, false if it isn't. -bool trial_ascent(int trial_depth, int stoplevel, int avg_depth, int bottom_time, struct gasmix *gasmix, int po2, double surface_pressure) -{ - - bool clear_to_ascend = true; - char *trial_cache = NULL; - - // For consistency with other VPM-B implementations, we should not start the ascent while the ceiling is - // deeper than the next stop (thus the offgasing during the ascent is ignored). - // However, we still need to make sure we don't break the ceiling due to on-gassing during ascent. - if (prefs.deco_mode == VPMB && (deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, - depth_to_bar(stoplevel, &displayed_dive)), - surface_pressure, &displayed_dive, 1) > stoplevel)) - return false; - - cache_deco_state(&trial_cache); - while (trial_depth > stoplevel) { - int deltad = ascent_velocity(trial_depth, avg_depth, bottom_time) * TIMESTEP; - if (deltad > trial_depth) /* don't test against depth above surface */ - deltad = trial_depth; - add_segment(depth_to_bar(trial_depth, &displayed_dive), - gasmix, - TIMESTEP, po2, &displayed_dive, prefs.decosac); - if (deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, depth_to_bar(trial_depth, &displayed_dive)), - surface_pressure, &displayed_dive, 1) > trial_depth - deltad) { - /* We should have stopped */ - clear_to_ascend = false; - break; - } - trial_depth -= deltad; - } - restore_deco_state(trial_cache); - free(trial_cache); - return clear_to_ascend; -} - -/* Determine if there is enough gas for the dive. Return true if there is enough. - * Also return true if this cannot be calculated because the cylinder doesn't have - * size or a starting pressure. - */ -bool enough_gas(int current_cylinder) -{ - cylinder_t *cyl; - cyl = &displayed_dive.cylinder[current_cylinder]; - - if (!cyl->start.mbar) - return true; - if (cyl->type.size.mliter) - return (float) (cyl->end.mbar - prefs.reserve_gas) * cyl->type.size.mliter / 1000.0 > (float) cyl->deco_gas_used.mliter; - else - return true; -} - -// Work out the stops. Return value is if there were any mandatory stops. - -bool plan(struct diveplan *diveplan, char **cached_datap, bool is_planner, bool show_disclaimer) -{ - int bottom_depth; - int bottom_gi; - int bottom_stopidx; - bool is_final_plan = true; - int deco_time; - int previous_deco_time; - char *bottom_cache = NULL; - struct sample *sample; - int po2; - int transitiontime, gi; - int current_cylinder; - unsigned int stopidx; - int depth; - struct gaschanges *gaschanges = NULL; - int gaschangenr; - int *decostoplevels; - int decostoplevelcount; - unsigned int *stoplevels = NULL; - bool stopping = false; - bool pendinggaschange = false; - int clock, previous_point_time; - int avg_depth, max_depth, bottom_time = 0; - int last_ascend_rate; - int best_first_ascend_cylinder; - struct gasmix gas, bottom_gas; - int o2time = 0; - int breaktime = -1; - int breakcylinder = 0; - int error = 0; - bool decodive = false; - - set_gf(diveplan->gflow, diveplan->gfhigh, prefs.gf_low_at_maxdepth); - if (!diveplan->surface_pressure) - diveplan->surface_pressure = SURFACE_PRESSURE; - displayed_dive.surface_pressure.mbar = diveplan->surface_pressure; - clear_deco(displayed_dive.surface_pressure.mbar / 1000.0); - max_bottom_ceiling_pressure.mbar = first_ceiling_pressure.mbar = 0; - create_dive_from_plan(diveplan, is_planner); - - // Do we want deco stop array in metres or feet? - if (prefs.units.length == METERS ) { - decostoplevels = decostoplevels_metric; - decostoplevelcount = sizeof(decostoplevels_metric) / sizeof(int); - } else { - decostoplevels = decostoplevels_imperial; - decostoplevelcount = sizeof(decostoplevels_imperial) / sizeof(int); - } - - /* If the user has selected last stop to be at 6m/20', we need to get rid of the 3m/10' stop. - * Otherwise reinstate the last stop 3m/10' stop. - */ - if (prefs.last_stop) - *(decostoplevels + 1) = 0; - else - *(decostoplevels + 1) = M_OR_FT(3,10); - - /* Let's start at the last 'sample', i.e. the last manually entered waypoint. */ - sample = &displayed_dive.dc.sample[displayed_dive.dc.samples - 1]; - - get_gas_at_time(&displayed_dive, &displayed_dive.dc, sample->time, &gas); - - po2 = sample->setpoint.mbar; - if ((current_cylinder = get_gasidx(&displayed_dive, &gas)) == -1) { - report_error(translate("gettextFromC", "Can't find gas %s"), gasname(&gas)); - current_cylinder = 0; - } - depth = displayed_dive.dc.sample[displayed_dive.dc.samples - 1].depth.mm; - average_max_depth(diveplan, &avg_depth, &max_depth); - last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); - - /* if all we wanted was the dive just get us back to the surface */ - if (!is_planner) { - transitiontime = depth / 75; /* this still needs to be made configurable */ - plan_add_segment(diveplan, transitiontime, 0, gas, po2, false); - create_dive_from_plan(diveplan, is_planner); - return(false); - } - -#if DEBUG_PLAN & 4 - printf("gas %s\n", gasname(&gas)); - printf("depth %5.2lfm \n", depth / 1000.0); -#endif - - best_first_ascend_cylinder = current_cylinder; - /* Find the gases available for deco */ - - if (po2) { // Don't change gas in CCR mode - gaschanges = NULL; - gaschangenr = 0; - } else { - gaschanges = analyze_gaslist(diveplan, &gaschangenr, depth, &best_first_ascend_cylinder); - } - /* Find the first potential decostopdepth above current depth */ - for (stopidx = 0; stopidx < decostoplevelcount; stopidx++) - if (*(decostoplevels + stopidx) >= depth) - break; - if (stopidx > 0) - stopidx--; - /* Stoplevels are either depths of gas changes or potential deco stop depths. */ - stoplevels = sort_stops(decostoplevels, stopidx + 1, gaschanges, gaschangenr); - stopidx += gaschangenr; - - /* Keep time during the ascend */ - bottom_time = clock = previous_point_time = displayed_dive.dc.sample[displayed_dive.dc.samples - 1].time.seconds; - gi = gaschangenr - 1; - - /* Set tissue tolerance and initial vpmb gradient at start of ascent phase */ - tissue_at_end(&displayed_dive, cached_datap); - nuclear_regeneration(clock); - vpmb_start_gradient(); - - if(prefs.deco_mode == RECREATIONAL) { - bool safety_stop = prefs.safetystop && max_depth >= 10000; - track_ascent_gas(depth, &displayed_dive.cylinder[current_cylinder], avg_depth, bottom_time, safety_stop); - // How long can we stay at the current depth and still directly ascent to the surface? - do { - add_segment(depth_to_bar(depth, &displayed_dive), - &displayed_dive.cylinder[current_cylinder].gasmix, - DECOTIMESTEP, po2, &displayed_dive, prefs.bottomsac); - update_cylinder_pressure(&displayed_dive, depth, depth, DECOTIMESTEP, prefs.bottomsac, &displayed_dive.cylinder[current_cylinder], false); - clock += DECOTIMESTEP; - } while (trial_ascent(depth, 0, avg_depth, bottom_time, &displayed_dive.cylinder[current_cylinder].gasmix, - po2, diveplan->surface_pressure / 1000.0) && - enough_gas(current_cylinder)); - - // We did stay one DECOTIMESTEP too many. - // In the best of all worlds, we would roll back also the last add_segment in terms of caching deco state, but - // let's ignore that since for the eventual ascent in recreational mode, nobody looks at the ceiling anymore, - // so we don't really have to compute the deco state. - update_cylinder_pressure(&displayed_dive, depth, depth, -DECOTIMESTEP, prefs.bottomsac, &displayed_dive.cylinder[current_cylinder], false); - clock -= DECOTIMESTEP; - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, true); - previous_point_time = clock; - do { - /* Ascend to surface */ - int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; - if (ascent_velocity(depth, avg_depth, bottom_time) != last_ascend_rate) { - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); - } - if (depth - deltad < 0) - deltad = depth; - - clock += TIMESTEP; - depth -= deltad; - if (depth <= 5000 && depth >= (5000 - deltad) && safety_stop) { - plan_add_segment(diveplan, clock - previous_point_time, 5000, gas, po2, false); - previous_point_time = clock; - clock += 180; - plan_add_segment(diveplan, clock - previous_point_time, 5000, gas, po2, false); - previous_point_time = clock; - safety_stop = false; - } - } while (depth > 0); - plan_add_segment(diveplan, clock - previous_point_time, 0, gas, po2, false); - create_dive_from_plan(diveplan, is_planner); - add_plan_to_notes(diveplan, &displayed_dive, show_disclaimer, error); - fixup_dc_duration(&displayed_dive.dc); - - free(stoplevels); - free(gaschanges); - return(false); - } - - if (best_first_ascend_cylinder != current_cylinder) { - current_cylinder = best_first_ascend_cylinder; - gas = displayed_dive.cylinder[current_cylinder].gasmix; - -#if DEBUG_PLAN & 16 - printf("switch to gas %d (%d/%d) @ %5.2lfm\n", best_first_ascend_cylinder, - (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[best_first_ascend_cylinder].depth / 1000.0); -#endif - } - - // VPM-B or Buehlmann Deco - tissue_at_end(&displayed_dive, cached_datap); - previous_deco_time = 100000000; - deco_time = 10000000; - cache_deco_state(&bottom_cache); // Lets us make several iterations - bottom_depth = depth; - bottom_gi = gi; - bottom_gas = gas; - bottom_stopidx = stopidx; - - //CVA - do { - is_final_plan = (prefs.deco_mode == BUEHLMANN) || (previous_deco_time - deco_time < 10); // CVA time converges - if (deco_time != 10000000) - vpmb_next_gradient(deco_time, diveplan->surface_pressure / 1000.0); - - previous_deco_time = deco_time; - restore_deco_state(bottom_cache); - - depth = bottom_depth; - gi = bottom_gi; - clock = previous_point_time = bottom_time; - gas = bottom_gas; - stopping = false; - decodive = false; - stopidx = bottom_stopidx; - breaktime = -1; - breakcylinder = 0; - o2time = 0; - first_ceiling_pressure.mbar = depth_to_mbar(deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, - depth_to_bar(depth, &displayed_dive)), - diveplan->surface_pressure / 1000.0, - &displayed_dive, - 1), - &displayed_dive); - if (max_bottom_ceiling_pressure.mbar > first_ceiling_pressure.mbar) - first_ceiling_pressure.mbar = max_bottom_ceiling_pressure.mbar; - - last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); - if ((current_cylinder = get_gasidx(&displayed_dive, &gas)) == -1) { - report_error(translate("gettextFromC", "Can't find gas %s"), gasname(&gas)); - current_cylinder = 0; - } - - while (1) { - /* We will break out when we hit the surface */ - do { - /* Ascend to next stop depth */ - int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; - if (ascent_velocity(depth, avg_depth, bottom_time) != last_ascend_rate) { - if (is_final_plan) - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - stopping = false; - last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); - } - if (depth - deltad < stoplevels[stopidx]) - deltad = depth - stoplevels[stopidx]; - - add_segment(depth_to_bar(depth, &displayed_dive), - &displayed_dive.cylinder[current_cylinder].gasmix, - TIMESTEP, po2, &displayed_dive, prefs.decosac); - clock += TIMESTEP; - depth -= deltad; - } while (depth > 0 && depth > stoplevels[stopidx]); - - if (depth <= 0) - break; /* We are at the surface */ - - if (gi >= 0 && stoplevels[stopidx] <= gaschanges[gi].depth) { - /* We have reached a gas change. - * Record this in the dive plan */ - if (is_final_plan) - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - stopping = true; - - /* Check we need to change cylinder. - * We might not if the cylinder was chosen by the user - * or user has selected only to switch only at required stops. - * If current gas is hypoxic, we want to switch asap */ - - if (current_cylinder != gaschanges[gi].gasidx) { - if (!prefs.switch_at_req_stop || - !trial_ascent(depth, stoplevels[stopidx - 1], avg_depth, bottom_time, - &displayed_dive.cylinder[current_cylinder].gasmix, po2, diveplan->surface_pressure / 1000.0) || get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) < 160) { - current_cylinder = gaschanges[gi].gasidx; - gas = displayed_dive.cylinder[current_cylinder].gasmix; -#if DEBUG_PLAN & 16 - printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi].gasidx, - (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[gi].depth / 1000.0); -#endif - /* Stop for the minimum duration to switch gas */ - add_segment(depth_to_bar(depth, &displayed_dive), - &displayed_dive.cylinder[current_cylinder].gasmix, - prefs.min_switch_duration, po2, &displayed_dive, prefs.decosac); - clock += prefs.min_switch_duration; - if (prefs.doo2breaks && get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) - o2time += prefs.min_switch_duration; - } else { - /* The user has selected the option to switch gas only at required stops. - * Remember that we are waiting to switch gas - */ - pendinggaschange = true; - } - } - gi--; - } - --stopidx; - - /* Save the current state and try to ascend to the next stopdepth */ - while (1) { - /* Check if ascending to next stop is clear, go back and wait if we hit the ceiling on the way */ - if (trial_ascent(depth, stoplevels[stopidx], avg_depth, bottom_time, - &displayed_dive.cylinder[current_cylinder].gasmix, po2, diveplan->surface_pressure / 1000.0)) - break; /* We did not hit the ceiling */ - - /* Add a minute of deco time and then try again */ - decodive = true; - if (!stopping) { - /* The last segment was an ascend segment. - * Add a waypoint for start of this deco stop */ - if (is_final_plan) - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - stopping = true; - } - - /* Are we waiting to switch gas? - * Occurs when the user has selected the option to switch only at required stops - */ - if (pendinggaschange) { - current_cylinder = gaschanges[gi + 1].gasidx; - gas = displayed_dive.cylinder[current_cylinder].gasmix; -#if DEBUG_PLAN & 16 - printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi + 1].gasidx, - (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[gi + 1].depth / 1000.0); -#endif - /* Stop for the minimum duration to switch gas */ - add_segment(depth_to_bar(depth, &displayed_dive), - &displayed_dive.cylinder[current_cylinder].gasmix, - prefs.min_switch_duration, po2, &displayed_dive, prefs.decosac); - clock += prefs.min_switch_duration; - if (prefs.doo2breaks && get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) - o2time += prefs.min_switch_duration; - pendinggaschange = false; - } - - /* Deco stop should end when runtime is at a whole minute */ - int this_decotimestep; - this_decotimestep = DECOTIMESTEP - clock % DECOTIMESTEP; - - add_segment(depth_to_bar(depth, &displayed_dive), - &displayed_dive.cylinder[current_cylinder].gasmix, - this_decotimestep, po2, &displayed_dive, prefs.decosac); - clock += this_decotimestep; - /* Finish infinite deco */ - if(clock >= 48 * 3600 && depth >= 6000) { - error = LONGDECO; - break; - } - if (prefs.doo2breaks) { - /* The backgas breaks option limits time on oxygen to 12 minutes, followed by 6 minutes on - * backgas (first defined gas). This could be customized if there were demand. - */ - if (get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) { - o2time += DECOTIMESTEP; - if (o2time >= 12 * 60) { - breaktime = 0; - breakcylinder = current_cylinder; - if (is_final_plan) - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - current_cylinder = 0; - gas = displayed_dive.cylinder[current_cylinder].gasmix; - } - } else { - if (breaktime >= 0) { - breaktime += DECOTIMESTEP; - if (breaktime >= 6 * 60) { - o2time = 0; - if (is_final_plan) - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - current_cylinder = breakcylinder; - gas = displayed_dive.cylinder[current_cylinder].gasmix; - breaktime = -1; - } - } - } - } - } - if (stopping) { - /* Next we will ascend again. Add a waypoint if we have spend deco time */ - if (is_final_plan) - plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); - previous_point_time = clock; - stopping = false; - } - } - - deco_time = clock - bottom_time; - } while (!is_final_plan); - - plan_add_segment(diveplan, clock - previous_point_time, 0, gas, po2, false); - create_dive_from_plan(diveplan, is_planner); - add_plan_to_notes(diveplan, &displayed_dive, show_disclaimer, error); - fixup_dc_duration(&displayed_dive.dc); - - free(stoplevels); - free(gaschanges); - free(bottom_cache); - return decodive; -} - -/* - * Get a value in tenths (so "10.2" == 102, "9" = 90) - * - * Return negative for errors. - */ -static int get_tenths(const char *begin, const char **endp) -{ - char *end; - int value = strtol(begin, &end, 10); - - if (begin == end) - return -1; - value *= 10; - - /* Fraction? We only look at the first digit */ - if (*end == '.') { - end++; - if (!isdigit(*end)) - return -1; - value += *end - '0'; - do { - end++; - } while (isdigit(*end)); - } - *endp = end; - return value; -} - -static int get_permille(const char *begin, const char **end) -{ - int value = get_tenths(begin, end); - if (value >= 0) { - /* Allow a percentage sign */ - if (**end == '%') - ++*end; - } - return value; -} - -int validate_gas(const char *text, struct gasmix *gas) -{ - int o2, he; - - if (!text) - return 0; - - while (isspace(*text)) - text++; - - if (!*text) - return 0; - - if (!strcasecmp(text, translate("gettextFromC", "air"))) { - o2 = O2_IN_AIR; - he = 0; - text += strlen(translate("gettextFromC", "air")); - } else if (!strcasecmp(text, translate("gettextFromC", "oxygen"))) { - o2 = 1000; - he = 0; - text += strlen(translate("gettextFromC", "oxygen")); - } else if (!strncasecmp(text, translate("gettextFromC", "ean"), 3)) { - o2 = get_permille(text + 3, &text); - he = 0; - } else { - o2 = get_permille(text, &text); - he = 0; - if (*text == '/') - he = get_permille(text + 1, &text); - } - - /* We don't want any extra crud */ - while (isspace(*text)) - text++; - if (*text) - return 0; - - /* Validate the gas mix */ - if (*text || o2 < 1 || o2 > 1000 || he < 0 || o2 + he > 1000) - return 0; - - /* Let it rip */ - gas->o2.permille = o2; - gas->he.permille = he; - return 1; -} - -int validate_po2(const char *text, int *mbar_po2) -{ - int po2; - - if (!text) - return 0; - - po2 = get_tenths(text, &text); - if (po2 < 0) - return 0; - - while (isspace(*text)) - text++; - - while (isspace(*text)) - text++; - if (*text) - return 0; - - *mbar_po2 = po2 * 100; - return 1; -} diff --git a/planner.h b/planner.h deleted file mode 100644 index a675989e0..000000000 --- a/planner.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef PLANNER_H -#define PLANNER_H - -#define LONGDECO 1 -#define NOT_RECREATIONAL 2 - -#ifdef __cplusplus -extern "C" { -#endif - -extern int validate_gas(const char *text, struct gasmix *gas); -extern int validate_po2(const char *text, int *mbar_po2); -extern timestamp_t current_time_notz(void); -extern void set_last_stop(bool last_stop_6m); -extern void set_verbatim(bool verbatim); -extern void set_display_runtime(bool display); -extern void set_display_duration(bool display); -extern void set_display_transitions(bool display); -extern void get_gas_at_time(struct dive *dive, struct divecomputer *dc, duration_t time, struct gasmix *gas); -extern int get_gasidx(struct dive *dive, struct gasmix *mix); -extern bool diveplan_empty(struct diveplan *diveplan); - -extern void free_dps(struct diveplan *diveplan); -extern struct dive *planned_dive; -extern char *cache_data; -extern const char *disclaimer; -extern double plangflow, plangfhigh; - -#ifdef __cplusplus -} -#endif -#endif // PLANNER_H diff --git a/pref.h b/pref.h deleted file mode 100644 index 84975aaaa..000000000 --- a/pref.h +++ /dev/null @@ -1,158 +0,0 @@ -#ifndef PREF_H -#define PREF_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "units.h" -#include "taxonomy.h" - -/* can't use 'bool' for the boolean values - different size in C and C++ */ -typedef struct -{ - short po2; - short pn2; - short phe; - double po2_threshold; - double pn2_threshold; - double phe_threshold; -} partial_pressure_graphs_t; - -typedef struct { - char *access_token; - char *user_id; - char *album_id; -} facebook_prefs_t; - -typedef struct { - bool enable_geocoding; - bool parse_dive_without_gps; - bool tag_existing_dives; - enum taxonomy_category category[3]; -} geocoding_prefs_t; - -enum deco_mode { - BUEHLMANN, - RECREATIONAL, - VPMB -}; - -struct preferences { - const char *divelist_font; - const char *default_filename; - const char *default_cylinder; - const char *cloud_base_url; - const char *cloud_git_url; - double font_size; - partial_pressure_graphs_t pp_graphs; - short mod; - double modpO2; - short ead; - short dcceiling; - short redceiling; - short calcceiling; - short calcceiling3m; - short calcalltissues; - short calcndltts; - short gflow; - short gfhigh; - int animation_speed; - bool gf_low_at_maxdepth; - bool show_ccr_setpoint; - bool show_ccr_sensors; - short display_invalid_dives; - short unit_system; - struct units units; - bool coordinates_traditional; - short show_sac; - short display_unused_tanks; - short show_average_depth; - short zoomed_plot; - short hrgraph; - short percentagegraph; - short rulergraph; - short tankbar; - short save_userid_local; - char *userid; - int ascrate75; // All rates in mm / sec - int ascrate50; - int ascratestops; - int ascratelast6m; - int descrate; - int bottompo2; - int decopo2; - int proxy_type; - char *proxy_host; - int proxy_port; - short proxy_auth; - char *proxy_user; - char *proxy_pass; - bool doo2breaks; - bool drop_stone_mode; - bool last_stop; // At 6m? - bool verbatim_plan; - bool display_runtime; - bool display_duration; - bool display_transitions; - bool safetystop; - bool switch_at_req_stop; - int reserve_gas; - int min_switch_duration; // seconds - int bottomsac; - int decosac; - int o2consumption; // ml per min - int pscr_ratio; // dump ratio times 1000 - int defaultsetpoint; // default setpoint in mbar - bool show_pictures_in_profile; - bool use_default_file; - short default_file_behavior; - facebook_prefs_t facebook; - char *cloud_storage_password; - char *cloud_storage_newpassword; - char *cloud_storage_email; - char *cloud_storage_email_encoded; - bool save_password_local; - short cloud_verification_status; - bool cloud_background_sync; - geocoding_prefs_t geocoding; - enum deco_mode deco_mode; - short conservatism_level; -}; -enum unit_system_values { - METRIC, - IMPERIAL, - PERSONALIZE -}; - -enum def_file_behavior { - UNDEFINED_DEFAULT_FILE, - LOCAL_DEFAULT_FILE, - NO_DEFAULT_FILE, - CLOUD_DEFAULT_FILE -}; - -enum cloud_status { - CS_UNKNOWN, - CS_INCORRECT_USER_PASSWD, - CS_NEED_TO_VERIFY, - CS_VERIFIED -}; - -extern struct preferences prefs, default_prefs, informational_prefs; - -#define PP_GRAPHS_ENABLED (prefs.pp_graphs.po2 || prefs.pp_graphs.pn2 || prefs.pp_graphs.phe) - -extern const char *system_divelist_default_font; -extern double system_divelist_default_font_size; - -extern const char *system_default_directory(void); -extern const char *system_default_filename(); -extern bool subsurface_ignore_font(const char *font); -extern void subsurface_OS_pref_setup(); - -#ifdef __cplusplus -} -#endif - -#endif // PREF_H diff --git a/prefs-macros.h b/prefs-macros.h deleted file mode 100644 index fe459d3da..000000000 --- a/prefs-macros.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef PREFSMACROS_H -#define PREFSMACROS_H - -#define SB(V, B) s.setValue(V, (int)(B->isChecked() ? 1 : 0)) - -#define GET_UNIT(name, field, f, t) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.units.field = (v.toInt() == (t)) ? (t) : (f); \ - else \ - prefs.units.field = default_prefs.units.field - -#define GET_BOOL(name, field) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = v.toBool(); \ - else \ - prefs.field = default_prefs.field - -#define GET_DOUBLE(name, field) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = v.toDouble(); \ - else \ - prefs.field = default_prefs.field - -#define GET_INT(name, field) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = v.toInt(); \ - else \ - prefs.field = default_prefs.field - -#define GET_ENUM(name, type, field) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = (enum type)v.toInt(); \ - else \ - prefs.field = default_prefs.field - -#define GET_INT_DEF(name, field, defval) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = v.toInt(); \ - else \ - prefs.field = defval - -#define GET_TXT(name, field) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = strdup(v.toString().toUtf8().constData()); \ - else \ - prefs.field = default_prefs.field - -#define SAVE_OR_REMOVE_SPECIAL(_setting, _default, _compare, _value) \ - if (_compare != _default) \ - s.setValue(_setting, _value); \ - else \ - s.remove(_setting) - -#define SAVE_OR_REMOVE(_setting, _default, _value) \ - if (_value != _default) \ - s.setValue(_setting, _value); \ - else \ - s.remove(_setting) - -#endif // PREFSMACROS_H - diff --git a/profile.c b/profile.c deleted file mode 100644 index d39133c21..000000000 --- a/profile.c +++ /dev/null @@ -1,1463 +0,0 @@ -/* profile.c */ -/* creates all the necessary data for drawing the dive profile - */ -#include "gettext.h" -#include -#include -#include - -#include "dive.h" -#include "display.h" -#include "divelist.h" - -#include "profile.h" -#include "gaspressures.h" -#include "deco.h" -#include "libdivecomputer/parser.h" -#include "libdivecomputer/version.h" -#include "membuffer.h" - -//#define DEBUG_GAS 1 - -#define MAX_PROFILE_DECO 7200 - - -int selected_dive = -1; /* careful: 0 is a valid value */ -unsigned int dc_number = 0; - -static struct plot_data *last_pi_entry_new = NULL; -void populate_pressure_information(struct dive *, struct divecomputer *, struct plot_info *, int); - -#ifdef DEBUG_PI -/* debugging tool - not normally used */ -static void dump_pi(struct plot_info *pi) -{ - int i; - - printf("pi:{nr:%d maxtime:%d meandepth:%d maxdepth:%d \n" - " maxpressure:%d mintemp:%d maxtemp:%d\n", - pi->nr, pi->maxtime, pi->meandepth, pi->maxdepth, - pi->maxpressure, pi->mintemp, pi->maxtemp); - for (i = 0; i < pi->nr; i++) { - struct plot_data *entry = &pi->entry[i]; - printf(" entry[%d]:{cylinderindex:%d sec:%d pressure:{%d,%d}\n" - " time:%d:%02d temperature:%d depth:%d stopdepth:%d stoptime:%d ndl:%d smoothed:%d po2:%lf phe:%lf pn2:%lf sum-pp %lf}\n", - i, entry->cylinderindex, entry->sec, - entry->pressure[0], entry->pressure[1], - entry->sec / 60, entry->sec % 60, - entry->temperature, entry->depth, entry->stopdepth, entry->stoptime, entry->ndl, entry->smoothed, - entry->pressures.o2, entry->pressures.he, entry->pressures.n2, - entry->pressures.o2 + entry->pressures.he + entry->pressures.n2); - } - printf(" }\n"); -} -#endif - -#define ROUND_UP(x, y) ((((x) + (y) - 1) / (y)) * (y)) -#define DIV_UP(x, y) (((x) + (y) - 1) / (y)) - -/* - * When showing dive profiles, we scale things to the - * current dive. However, we don't scale past less than - * 30 minutes or 90 ft, just so that small dives show - * up as such unless zoom is enabled. - * We also need to add 180 seconds at the end so the min/max - * plots correctly - */ -int get_maxtime(struct plot_info *pi) -{ - int seconds = pi->maxtime; - - int DURATION_THR = (pi->dive_type == FREEDIVING ? 60 : 600); - int CEILING = (pi->dive_type == FREEDIVING ? 30 : 60); - - if (prefs.zoomed_plot) { - /* Rounded up to one minute, with at least 2.5 minutes to - * spare. - * For dive times shorter than 10 minutes, we use seconds/4 to - * calculate the space dynamically. - * This is seamless since 600/4 = 150. - */ - if (seconds < DURATION_THR) - return ROUND_UP(seconds + seconds / 4, CEILING); - else - return ROUND_UP(seconds + DURATION_THR/4, CEILING); - } else { - /* min 30 minutes, rounded up to 5 minutes, with at least 2.5 minutes to spare */ - return MAX(30 * 60, ROUND_UP(seconds + DURATION_THR/4, CEILING * 5)); - } -} - -/* get the maximum depth to which we want to plot - * take into account the additional vertical space needed to plot - * partial pressure graphs */ -int get_maxdepth(struct plot_info *pi) -{ - unsigned mm = pi->maxdepth; - int md; - - if (prefs.zoomed_plot) { - /* Rounded up to 10m, with at least 3m to spare */ - md = ROUND_UP(mm + 3000, 10000); - } else { - /* Minimum 30m, rounded up to 10m, with at least 3m to spare */ - md = MAX((unsigned)30000, ROUND_UP(mm + 3000, 10000)); - } - md += pi->maxpp * 9000; - return md; -} - -/* collect all event names and whether we display them */ -struct ev_select *ev_namelist; -int evn_allocated; -int evn_used; - -#if WE_DONT_USE_THIS /* we need to implement event filters in Qt */ -int evn_foreach (void (*callback)(const char *, bool *, void *), void *data) { - int i; - - for (i = 0; i < evn_used; i++) { - /* here we display an event name on screen - so translate */ - callback(translate("gettextFromC", ev_namelist[i].ev_name), &ev_namelist[i].plot_ev, data); - } - return i; -} -#endif /* WE_DONT_USE_THIS */ - -void clear_events(void) -{ - for (int i = 0; i < evn_used; i++) - free(ev_namelist[i].ev_name); - evn_used = 0; -} - -void remember_event(const char *eventname) -{ - int i = 0, len; - - if (!eventname || (len = strlen(eventname)) == 0) - return; - while (i < evn_used) { - if (!strncmp(eventname, ev_namelist[i].ev_name, len)) - return; - i++; - } - if (evn_used == evn_allocated) { - evn_allocated += 10; - ev_namelist = realloc(ev_namelist, evn_allocated * sizeof(struct ev_select)); - if (!ev_namelist) - /* we are screwed, but let's just bail out */ - return; - } - ev_namelist[evn_used].ev_name = strdup(eventname); - ev_namelist[evn_used].plot_ev = true; - evn_used++; -} - -/* Get local sac-rate (in ml/min) between entry1 and entry2 */ -static int get_local_sac(struct plot_data *entry1, struct plot_data *entry2, struct dive *dive) -{ - int index = entry1->cylinderindex; - cylinder_t *cyl; - int duration = entry2->sec - entry1->sec; - int depth, airuse; - pressure_t a, b; - double atm; - - if (entry2->cylinderindex != index) - return 0; - if (duration <= 0) - return 0; - a.mbar = GET_PRESSURE(entry1); - b.mbar = GET_PRESSURE(entry2); - if (!b.mbar || a.mbar <= b.mbar) - return 0; - - /* Mean pressure in ATM */ - depth = (entry1->depth + entry2->depth) / 2; - atm = depth_to_atm(depth, dive); - - cyl = dive->cylinder + index; - - airuse = gas_volume(cyl, a) - gas_volume(cyl, b); - - /* milliliters per minute */ - return airuse / atm * 60 / duration; -} - -static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index) -{ - struct plot_data *p = entry; - int time = entry->sec; - int seconds = 90 * (index + 1); - struct plot_data *min, *max; - int avg, nr; - - /* Go back 'seconds' in time */ - while (p > first) { - if (p[-1].sec < time - seconds) - break; - p--; - } - - /* Then go forward until we hit an entry past the time */ - min = max = p; - avg = p->depth; - nr = 1; - while (++p < last) { - int depth = p->depth; - if (p->sec > time + seconds) - break; - avg += depth; - nr++; - if (depth < min->depth) - min = p; - if (depth > max->depth) - max = p; - } - entry->min[index] = min; - entry->max[index] = max; - entry->avg[index] = (avg + nr / 2) / nr; -} - -static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last) -{ - analyze_plot_info_minmax_minute(entry, first, last, 0); - analyze_plot_info_minmax_minute(entry, first, last, 1); - analyze_plot_info_minmax_minute(entry, first, last, 2); -} - -static velocity_t velocity(int speed) -{ - velocity_t v; - - if (speed < -304) /* ascent faster than -60ft/min */ - v = CRAZY; - else if (speed < -152) /* above -30ft/min */ - v = FAST; - else if (speed < -76) /* -15ft/min */ - v = MODERATE; - else if (speed < -25) /* -5ft/min */ - v = SLOW; - else if (speed < 25) /* very hard to find data, but it appears that the recommendations - for descent are usually about 2x ascent rate; still, we want - stable to mean stable */ - v = STABLE; - else if (speed < 152) /* between 5 and 30ft/min is considered slow */ - v = SLOW; - else if (speed < 304) /* up to 60ft/min is moderate */ - v = MODERATE; - else if (speed < 507) /* up to 100ft/min is fast */ - v = FAST; - else /* more than that is just crazy - you'll blow your ears out */ - v = CRAZY; - - return v; -} - -struct plot_info *analyze_plot_info(struct plot_info *pi) -{ - int i; - int nr = pi->nr; - - /* Smoothing function: 5-point triangular smooth */ - for (i = 2; i < nr; i++) { - struct plot_data *entry = pi->entry + i; - int depth; - - if (i < nr - 2) { - depth = entry[-2].depth + 2 * entry[-1].depth + 3 * entry[0].depth + 2 * entry[1].depth + entry[2].depth; - entry->smoothed = (depth + 4) / 9; - } - /* vertical velocity in mm/sec */ - /* Linus wants to smooth this - let's at least look at the samples that aren't FAST or CRAZY */ - if (entry[0].sec - entry[-1].sec) { - entry->speed = (entry[0].depth - entry[-1].depth) / (entry[0].sec - entry[-1].sec); - entry->velocity = velocity(entry->speed); - /* if our samples are short and we aren't too FAST*/ - if (entry[0].sec - entry[-1].sec < 15 && entry->velocity < FAST) { - int past = -2; - while (i + past > 0 && entry[0].sec - entry[past].sec < 15) - past--; - entry->velocity = velocity((entry[0].depth - entry[past].depth) / - (entry[0].sec - entry[past].sec)); - } - } else { - entry->velocity = STABLE; - entry->speed = 0; - } - } - - /* One-, two- and three-minute minmax data */ - for (i = 0; i < nr; i++) { - struct plot_data *entry = pi->entry + i; - analyze_plot_info_minmax(entry, pi->entry, pi->entry + nr); - } - - return pi; -} - -/* - * If the event has an explicit cylinder index, - * we return that. If it doesn't, we return the best - * match based on the gasmix. - * - * Some dive computers give cylinder indexes, some - * give just the gas mix. - */ -int get_cylinder_index(struct dive *dive, struct event *ev) -{ - int i; - int best = 0, score = INT_MAX; - int target_o2, target_he; - struct gasmix *g; - - if (ev->gas.index >= 0) - return ev->gas.index; - - g = get_gasmix_from_event(ev); - target_o2 = get_o2(g); - target_he = get_he(g); - - /* - * Try to find a cylinder that best matches the target gas - * mix. - */ - for (i = 0; i < MAX_CYLINDERS; i++) { - cylinder_t *cyl = dive->cylinder + i; - int delta_o2, delta_he, distance; - - if (cylinder_nodata(cyl)) - continue; - - delta_o2 = get_o2(&cyl->gasmix) - target_o2; - delta_he = get_he(&cyl->gasmix) - target_he; - distance = delta_o2 * delta_o2; - distance += delta_he * delta_he; - - if (distance >= score) - continue; - score = distance; - best = i; - } - return best; -} - -struct event *get_next_event(struct event *event, const char *name) -{ - if (!name || !*name) - return NULL; - while (event) { - if (!strcmp(event->name, name)) - return event; - event = event->next; - } - return event; -} - -static int count_events(struct divecomputer *dc) -{ - int result = 0; - struct event *ev = dc->events; - while (ev != NULL) { - result++; - ev = ev->next; - } - return result; -} - -static int set_cylinder_index(struct plot_info *pi, int i, int cylinderindex, unsigned int end) -{ - while (i < pi->nr) { - struct plot_data *entry = pi->entry + i; - if (entry->sec > end) - break; - if (entry->cylinderindex != cylinderindex) { - entry->cylinderindex = cylinderindex; - entry->pressure[0] = 0; - } - i++; - } - return i; -} - -static int set_setpoint(struct plot_info *pi, int i, int setpoint, unsigned int end) -{ - while (i < pi->nr) { - struct plot_data *entry = pi->entry + i; - if (entry->sec > end) - break; - entry->o2pressure.mbar = setpoint; - i++; - } - return i; -} - -/* normally the first cylinder has index 0... if not, we need to fix this up here */ -static int set_first_cylinder_index(struct plot_info *pi, int i, int cylinderindex, unsigned int end) -{ - while (i < pi->nr) { - struct plot_data *entry = pi->entry + i; - if (entry->sec > end) - break; - entry->cylinderindex = cylinderindex; - i++; - } - return i; -} - -static void check_gas_change_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) -{ - int i = 0, cylinderindex = 0; - struct event *ev = get_next_event(dc->events, "gaschange"); - - // for dive computers that tell us their first gas as an event on the first sample - // we need to make sure things are setup correctly - cylinderindex = explicit_first_cylinder(dive, dc); - set_first_cylinder_index(pi, 0, cylinderindex, ~0u); - - if (!ev) - return; - - do { - i = set_cylinder_index(pi, i, cylinderindex, ev->time.seconds); - cylinderindex = get_cylinder_index(dive, ev); - ev = get_next_event(ev->next, "gaschange"); - } while (ev); - set_cylinder_index(pi, i, cylinderindex, ~0u); -} - -static void check_setpoint_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) -{ - int i = 0; - pressure_t setpoint; - - setpoint.mbar = 0; - struct event *ev = get_next_event(dc->events, "SP change"); - - if (!ev) - return; - - do { - i = set_setpoint(pi, i, setpoint.mbar, ev->time.seconds); - setpoint.mbar = ev->value; - if (setpoint.mbar) - dc->divemode = CCR; - ev = get_next_event(ev->next, "SP change"); - } while (ev); - set_setpoint(pi, i, setpoint.mbar, ~0u); -} - - -struct plot_info calculate_max_limits_new(struct dive *dive, struct divecomputer *given_dc) -{ - struct divecomputer *dc = &(dive->dc); - bool seen = false; - static struct plot_info pi; - int maxdepth = dive->maxdepth.mm; - int maxtime = 0; - int maxpressure = 0, minpressure = INT_MAX; - int maxhr = 0, minhr = INT_MAX; - int mintemp = dive->mintemp.mkelvin; - int maxtemp = dive->maxtemp.mkelvin; - int cyl; - - /* Get the per-cylinder maximum pressure if they are manual */ - for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { - int mbar = dive->cylinder[cyl].start.mbar; - if (mbar > maxpressure) - maxpressure = mbar; - if (mbar < minpressure) - minpressure = mbar; - } - - /* Then do all the samples from all the dive computers */ - do { - if (dc == given_dc) - seen = true; - int i = dc->samples; - int lastdepth = 0; - struct sample *s = dc->sample; - - while (--i >= 0) { - int depth = s->depth.mm; - int pressure = s->cylinderpressure.mbar; - int temperature = s->temperature.mkelvin; - int heartbeat = s->heartbeat; - - if (!mintemp && temperature < mintemp) - mintemp = temperature; - if (temperature > maxtemp) - maxtemp = temperature; - - if (pressure && pressure < minpressure) - minpressure = pressure; - if (pressure > maxpressure) - maxpressure = pressure; - if (heartbeat > maxhr) - maxhr = heartbeat; - if (heartbeat < minhr) - minhr = heartbeat; - - if (depth > maxdepth) - maxdepth = s->depth.mm; - if ((depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) && - s->time.seconds > maxtime) - maxtime = s->time.seconds; - lastdepth = depth; - s++; - } - dc = dc->next; - if (dc == NULL && !seen) { - dc = given_dc; - seen = true; - } - } while (dc != NULL); - - if (minpressure > maxpressure) - minpressure = 0; - if (minhr > maxhr) - minhr = 0; - - memset(&pi, 0, sizeof(pi)); - pi.maxdepth = maxdepth; - pi.maxtime = maxtime; - pi.maxpressure = maxpressure; - pi.minpressure = minpressure; - pi.minhr = minhr; - pi.maxhr = maxhr; - pi.mintemp = mintemp; - pi.maxtemp = maxtemp; - return pi; -} - -/* copy the previous entry (we know this exists), update time and depth - * and zero out the sensor pressure (since this is a synthetic entry) - * increment the entry pointer and the count of synthetic entries. */ -#define INSERT_ENTRY(_time, _depth, _sac) \ - *entry = entry[-1]; \ - entry->sec = _time; \ - entry->depth = _depth; \ - entry->running_sum = (entry - 1)->running_sum + (_time - (entry - 1)->sec) * (_depth + (entry - 1)->depth) / 2; \ - SENSOR_PRESSURE(entry) = 0; \ - entry->sac = _sac; \ - entry++; \ - idx++ - -struct plot_data *populate_plot_entries(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) -{ - int idx, maxtime, nr, i; - int lastdepth, lasttime, lasttemp = 0; - struct plot_data *plot_data; - struct event *ev = dc->events; - - maxtime = pi->maxtime; - - /* - * We want to have a plot_info event at least every 10s (so "maxtime/10+1"), - * but samples could be more dense than that (so add in dc->samples). We also - * need to have one for every event (so count events and add that) and - * additionally we want two surface events around the whole thing (thus the - * additional 4). There is also one extra space for a final entry - * that has time > maxtime (because there can be surface samples - * past "maxtime" in the original sample data) - */ - nr = dc->samples + 6 + maxtime / 10 + count_events(dc); - plot_data = calloc(nr, sizeof(struct plot_data)); - pi->entry = plot_data; - if (!plot_data) - return NULL; - pi->nr = nr; - idx = 2; /* the two extra events at the start */ - - lastdepth = 0; - lasttime = 0; - /* skip events at time = 0 */ - while (ev && ev->time.seconds == 0) - ev = ev->next; - for (i = 0; i < dc->samples; i++) { - struct plot_data *entry = plot_data + idx; - struct sample *sample = dc->sample + i; - int time = sample->time.seconds; - int offset, delta; - int depth = sample->depth.mm; - int sac = sample->sac.mliter; - - /* Add intermediate plot entries if required */ - delta = time - lasttime; - if (delta <= 0) { - time = lasttime; - delta = 1; // avoid divide by 0 - } - for (offset = 10; offset < delta; offset += 10) { - if (lasttime + offset > maxtime) - break; - - /* Add events if they are between plot entries */ - while (ev && ev->time.seconds < lasttime + offset) { - INSERT_ENTRY(ev->time.seconds, interpolate(lastdepth, depth, ev->time.seconds - lasttime, delta), sac); - ev = ev->next; - } - - /* now insert the time interpolated entry */ - INSERT_ENTRY(lasttime + offset, interpolate(lastdepth, depth, offset, delta), sac); - - /* skip events that happened at this time */ - while (ev && ev->time.seconds == lasttime + offset) - ev = ev->next; - } - - /* Add events if they are between plot entries */ - while (ev && ev->time.seconds < time) { - INSERT_ENTRY(ev->time.seconds, interpolate(lastdepth, depth, ev->time.seconds - lasttime, delta), sac); - ev = ev->next; - } - - - entry->sec = time; - entry->depth = depth; - - entry->running_sum = (entry - 1)->running_sum + (time - (entry - 1)->sec) * (depth + (entry - 1)->depth) / 2; - entry->stopdepth = sample->stopdepth.mm; - entry->stoptime = sample->stoptime.seconds; - entry->ndl = sample->ndl.seconds; - entry->tts = sample->tts.seconds; - pi->has_ndl |= sample->ndl.seconds; - entry->in_deco = sample->in_deco; - entry->cns = sample->cns; - if (dc->divemode == CCR) { - entry->o2pressure.mbar = entry->o2setpoint.mbar = sample->setpoint.mbar; // for rebreathers - entry->o2sensor[0].mbar = sample->o2sensor[0].mbar; // for up to three rebreather O2 sensors - entry->o2sensor[1].mbar = sample->o2sensor[1].mbar; - entry->o2sensor[2].mbar = sample->o2sensor[2].mbar; - } else { - entry->pressures.o2 = sample->setpoint.mbar / 1000.0; - } - /* FIXME! sensor index -> cylinder index translation! */ - // entry->cylinderindex = sample->sensor; - SENSOR_PRESSURE(entry) = sample->cylinderpressure.mbar; - O2CYLINDER_PRESSURE(entry) = sample->o2cylinderpressure.mbar; - if (sample->temperature.mkelvin) - entry->temperature = lasttemp = sample->temperature.mkelvin; - else - entry->temperature = lasttemp; - entry->heartbeat = sample->heartbeat; - entry->bearing = sample->bearing.degrees; - entry->sac = sample->sac.mliter; - if (sample->rbt.seconds) - entry->rbt = sample->rbt.seconds; - /* skip events that happened at this time */ - while (ev && ev->time.seconds == time) - ev = ev->next; - lasttime = time; - lastdepth = depth; - idx++; - - if (time > maxtime) - break; - } - - /* Add two final surface events */ - plot_data[idx++].sec = lasttime + 1; - plot_data[idx++].sec = lasttime + 2; - pi->nr = idx; - - return plot_data; -} - -#undef INSERT_ENTRY - -static void populate_cylinder_pressure_data(int idx, int start, int end, struct plot_info *pi, bool o2flag) -{ - int i; - - /* First: check that none of the entries has sensor pressure for this cylinder index */ - for (i = 0; i < pi->nr; i++) { - struct plot_data *entry = pi->entry + i; - if (entry->cylinderindex != idx && !o2flag) - continue; - if (CYLINDER_PRESSURE(o2flag, entry)) - return; - } - - /* Then: populate the first entry with the beginning cylinder pressure */ - for (i = 0; i < pi->nr; i++) { - struct plot_data *entry = pi->entry + i; - if (entry->cylinderindex != idx && !o2flag) - continue; - if (o2flag) - O2CYLINDER_PRESSURE(entry) = start; - else - SENSOR_PRESSURE(entry) = start; - break; - } - - /* .. and the last entry with the ending cylinder pressure */ - for (i = pi->nr; --i >= 0; /* nothing */) { - struct plot_data *entry = pi->entry + i; - if (entry->cylinderindex != idx && !o2flag) - continue; - if (o2flag) - O2CYLINDER_PRESSURE(entry) = end; - else - SENSOR_PRESSURE(entry) = end; - break; - } -} - -/* - * Calculate the sac rate between the two plot entries 'first' and 'last'. - * - * Everything in between has a cylinder pressure, and it's all the same - * cylinder. - */ -static int sac_between(struct dive *dive, struct plot_data *first, struct plot_data *last) -{ - int airuse; - double pressuretime; - pressure_t a, b; - cylinder_t *cyl; - int duration; - - if (first == last) - return 0; - - /* Calculate air use - trivial */ - a.mbar = GET_PRESSURE(first); - b.mbar = GET_PRESSURE(last); - cyl = dive->cylinder + first->cylinderindex; - airuse = gas_volume(cyl, a) - gas_volume(cyl, b); - if (airuse <= 0) - return 0; - - /* Calculate depthpressure integrated over time */ - pressuretime = 0.0; - do { - int depth = (first[0].depth + first[1].depth) / 2; - int time = first[1].sec - first[0].sec; - double atm = depth_to_atm(depth, dive); - - pressuretime += atm * time; - } while (++first < last); - - /* Turn "atmseconds" into "atmminutes" */ - pressuretime /= 60; - - /* SAC = mliter per minute */ - return rint(airuse / pressuretime); -} - -/* - * Try to do the momentary sac rate for this entry, averaging over one - * minute. - */ -static void fill_sac(struct dive *dive, struct plot_info *pi, int idx) -{ - struct plot_data *entry = pi->entry + idx; - struct plot_data *first, *last; - int time; - - if (entry->sac) - return; - - if (!GET_PRESSURE(entry)) - return; - - /* - * Try to go back 30 seconds to get 'first'. - * Stop if the sensor changed, or if we went back too far. - */ - first = entry; - time = entry->sec - 30; - while (idx > 0) { - struct plot_data *prev = first-1; - if (prev->cylinderindex != first->cylinderindex) - break; - if (prev->depth < SURFACE_THRESHOLD && first->depth < SURFACE_THRESHOLD) - break; - if (prev->sec < time) - break; - if (!GET_PRESSURE(prev)) - break; - idx--; - first = prev; - } - - /* Now find an entry a minute after the first one */ - last = first; - time = first->sec + 60; - while (++idx < pi->nr) { - struct plot_data *next = last+1; - if (next->cylinderindex != last->cylinderindex) - break; - if (next->depth < SURFACE_THRESHOLD && last->depth < SURFACE_THRESHOLD) - break; - if (next->sec > time) - break; - if (!GET_PRESSURE(next)) - break; - last = next; - } - - /* Ok, now calculate the SAC between 'first' and 'last' */ - entry->sac = sac_between(dive, first, last); -} - -static void calculate_sac(struct dive *dive, struct plot_info *pi) -{ - int i = 0, last = 0; - struct plot_data *last_entry = NULL; - - for (i = 0; i < pi->nr; i++) - fill_sac(dive, pi, i); -} - -static void populate_secondary_sensor_data(struct divecomputer *dc, struct plot_info *pi) -{ - /* We should try to see if it has interesting pressure data here */ -} - -static void setup_gas_sensor_pressure(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) -{ - int i; - struct divecomputer *secondary; - - /* First, populate the pressures with the manual cylinder data.. */ - for (i = 0; i < MAX_CYLINDERS; i++) { - cylinder_t *cyl = dive->cylinder + i; - int start = cyl->start.mbar ?: cyl->sample_start.mbar; - int end = cyl->end.mbar ?: cyl->sample_end.mbar; - - if (!start || !end) - continue; - - populate_cylinder_pressure_data(i, start, end, pi, dive->cylinder[i].cylinder_use == OXYGEN); - } - - /* - * Here, we should try to walk through all the dive computers, - * and try to see if they have sensor data different from the - * primary dive computer (dc). - */ - secondary = &dive->dc; - do { - if (secondary == dc) - continue; - populate_secondary_sensor_data(dc, pi); - } while ((secondary = secondary->next) != NULL); -} - -/* calculate DECO STOP / TTS / NDL */ -static void calculate_ndl_tts(struct plot_data *entry, struct dive *dive, double surface_pressure) -{ - /* FIXME: This should be configurable */ - /* ascent speed up to first deco stop */ - const int ascent_s_per_step = 1; - const int ascent_mm_per_step = 200; /* 12 m/min */ - /* ascent speed between deco stops */ - const int ascent_s_per_deco_step = 1; - const int ascent_mm_per_deco_step = 16; /* 1 m/min */ - /* how long time steps in deco calculations? */ - const int time_stepsize = 60; - const int deco_stepsize = 3000; - /* at what depth is the current deco-step? */ - int next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), - surface_pressure, dive, 1), deco_stepsize); - int ascent_depth = entry->depth; - /* at what time should we give up and say that we got enuff NDL? */ - int cylinderindex = entry->cylinderindex; - - /* If we don't have a ceiling yet, calculate ndl. Don't try to calculate - * a ndl for lower values than 3m it would take forever */ - if (next_stop == 0) { - if (entry->depth < 3000) { - entry->ndl = MAX_PROFILE_DECO; - return; - } - /* stop if the ndl is above max_ndl seconds, and call it plenty of time */ - while (entry->ndl_calc < MAX_PROFILE_DECO && deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, 1) <= 0) { - entry->ndl_calc += time_stepsize; - add_segment(depth_to_bar(entry->depth, dive), - &dive->cylinder[cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, prefs.bottomsac); - } - /* we don't need to calculate anything else */ - return; - } - - /* We are in deco */ - entry->in_deco_calc = true; - - /* Add segments for movement to stopdepth */ - for (; ascent_depth > next_stop; ascent_depth -= ascent_mm_per_step, entry->tts_calc += ascent_s_per_step) { - add_segment(depth_to_bar(ascent_depth, dive), - &dive->cylinder[cylinderindex].gasmix, ascent_s_per_step, entry->o2pressure.mbar, dive, prefs.decosac); - next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(ascent_depth, dive)), surface_pressure, dive, 1), deco_stepsize); - } - ascent_depth = next_stop; - - /* And how long is the current deco-step? */ - entry->stoptime_calc = 0; - entry->stopdepth_calc = next_stop; - next_stop -= deco_stepsize; - - /* And how long is the total TTS */ - while (next_stop >= 0) { - /* save the time for the first stop to show in the graph */ - if (ascent_depth == entry->stopdepth_calc) - entry->stoptime_calc += time_stepsize; - - entry->tts_calc += time_stepsize; - if (entry->tts_calc > MAX_PROFILE_DECO) - break; - add_segment(depth_to_bar(ascent_depth, dive), - &dive->cylinder[cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, prefs.decosac); - - if (deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(ascent_depth,dive)), surface_pressure, dive, 1) <= next_stop) { - /* move to the next stop and add the travel between stops */ - for (; ascent_depth > next_stop; ascent_depth -= ascent_mm_per_deco_step, entry->tts_calc += ascent_s_per_deco_step) - add_segment(depth_to_bar(ascent_depth, dive), - &dive->cylinder[cylinderindex].gasmix, ascent_s_per_deco_step, entry->o2pressure.mbar, dive, prefs.decosac); - ascent_depth = next_stop; - next_stop -= deco_stepsize; - } - } -} - -/* Let's try to do some deco calculations. - */ -void calculate_deco_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool print_mode) -{ - int i; - double surface_pressure = (dc->surface_pressure.mbar ? dc->surface_pressure.mbar : get_surface_pressure_in_mbar(dive, true)) / 1000.0; - int last_ndl_tts_calc_time = 0; - for (i = 1; i < pi->nr; i++) { - struct plot_data *entry = pi->entry + i; - int j, t0 = (entry - 1)->sec, t1 = entry->sec; - int time_stepsize = 20; - - entry->ambpressure = depth_to_bar(entry->depth, dive); - entry->gfline = MAX((double)prefs.gflow, (entry->ambpressure - surface_pressure) / (gf_low_pressure_this_dive - surface_pressure) * - (prefs.gflow - prefs.gfhigh) + - prefs.gfhigh) * - (100.0 - AMB_PERCENTAGE) / 100.0 + AMB_PERCENTAGE; - if (t0 > t1) { - fprintf(stderr, "non-monotonous dive stamps %d %d\n", t0, t1); - int xchg = t1; - t1 = t0; - t0 = xchg; - } - if (t0 != t1 && t1 - t0 < time_stepsize) - time_stepsize = t1 - t0; - for (j = t0 + time_stepsize; j <= t1; j += time_stepsize) { - int depth = interpolate(entry[-1].depth, entry[0].depth, j - t0, t1 - t0); - add_segment(depth_to_bar(depth, dive), - &dive->cylinder[entry->cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, entry->sac); - if ((t1 - j < time_stepsize) && (j < t1)) - time_stepsize = t1 - j; - } - if (t0 == t1) - entry->ceiling = (entry - 1)->ceiling; - else - entry->ceiling = deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, !prefs.calcceiling3m); - for (j = 0; j < 16; j++) { - double m_value = buehlmann_inertgas_a[j] + entry->ambpressure / buehlmann_inertgas_b[j]; - entry->ceilings[j] = deco_allowed_depth(tolerated_by_tissue[j], surface_pressure, dive, 1); - entry->percentages[j] = tissue_inertgas_saturation[j] < entry->ambpressure ? - tissue_inertgas_saturation[j] / entry->ambpressure * AMB_PERCENTAGE : - AMB_PERCENTAGE + (tissue_inertgas_saturation[j] - entry->ambpressure) / (m_value - entry->ambpressure) * (100.0 - AMB_PERCENTAGE); - } - - /* should we do more calculations? - * We don't for print-mode because this info doesn't show up there */ - if (prefs.calcndltts && !print_mode) { - /* only calculate ndl/tts on every 30 seconds */ - if ((entry->sec - last_ndl_tts_calc_time) < 30) { - struct plot_data *prev_entry = (entry - 1); - entry->stoptime_calc = prev_entry->stoptime_calc; - entry->stopdepth_calc = prev_entry->stopdepth_calc; - entry->tts_calc = prev_entry->tts_calc; - entry->ndl_calc = prev_entry->ndl_calc; - continue; - } - last_ndl_tts_calc_time = entry->sec; - - /* We are going to mess up deco state, so store it for later restore */ - char *cache_data = NULL; - cache_deco_state(&cache_data); - calculate_ndl_tts(entry, dive, surface_pressure); - /* Restore "real" deco state for next real time step */ - restore_deco_state(cache_data); - free(cache_data); - } - } -#if DECO_CALC_DEBUG & 1 - dump_tissues(); -#endif -} - - -/* Function calculate_ccr_po2: This function takes information from one plot_data structure (i.e. one point on - * the dive profile), containing the oxygen sensor values of a CCR system and, for that plot_data structure, - * calculates the po2 value from the sensor data. Several rules are applied, depending on how many o2 sensors - * there are and the differences among the readings from these sensors. - */ -static int calculate_ccr_po2(struct plot_data *entry, struct divecomputer *dc) -{ - int sump = 0, minp = 999999, maxp = -999999; - int diff_limit = 100; // The limit beyond which O2 sensor differences are considered significant (default = 100 mbar) - int i, np = 0; - - for (i = 0; i < dc->no_o2sensors; i++) - if (entry->o2sensor[i].mbar) { // Valid reading - ++np; - sump += entry->o2sensor[i].mbar; - minp = MIN(minp, entry->o2sensor[i].mbar); - maxp = MAX(maxp, entry->o2sensor[i].mbar); - } - switch (np) { - case 0: // Uhoh - return entry->o2pressure.mbar; - case 1: // Return what we have - return sump; - case 2: // Take the average - return sump / 2; - case 3: // Voting logic - if (2 * maxp - sump + minp < diff_limit) { // Upper difference acceptable... - if (2 * minp - sump + maxp) // ...and lower difference acceptable - return sump / 3; - else - return (sump - minp) / 2; - } else { - if (2 * minp - sump + maxp) // ...but lower difference acceptable - return (sump - maxp) / 2; - else - return sump / 3; - } - default: // This should not happen - assert(np <= 3); - return 0; - } -} - -static void calculate_gas_information_new(struct dive *dive, struct plot_info *pi) -{ - int i; - double amb_pressure; - - for (i = 1; i < pi->nr; i++) { - int fn2, fhe; - struct plot_data *entry = pi->entry + i; - int cylinderindex = entry->cylinderindex; - - amb_pressure = depth_to_bar(entry->depth, dive); - - fill_pressures(&entry->pressures, amb_pressure, &dive->cylinder[cylinderindex].gasmix, entry->o2pressure.mbar / 1000.0, dive->dc.divemode); - fn2 = (int)(1000.0 * entry->pressures.n2 / amb_pressure); - fhe = (int)(1000.0 * entry->pressures.he / amb_pressure); - - /* Calculate MOD, EAD, END and EADD based on partial pressures calculated before - * so there is no difference in calculating between OC and CC - * END takes O₂ + N₂ (air) into account ("Narcotic" for trimix dives) - * EAD just uses N₂ ("Air" for nitrox dives) */ - pressure_t modpO2 = { .mbar = (int)(prefs.modpO2 * 1000) }; - entry->mod = (double)gas_mod(&dive->cylinder[cylinderindex].gasmix, modpO2, dive, 1).mm; - entry->end = (entry->depth + 10000) * (1000 - fhe) / 1000.0 - 10000; - entry->ead = (entry->depth + 10000) * fn2 / (double)N2_IN_AIR - 10000; - entry->eadd = (entry->depth + 10000) * - (entry->pressures.o2 / amb_pressure * O2_DENSITY + - entry->pressures.n2 / amb_pressure * N2_DENSITY + - entry->pressures.he / amb_pressure * HE_DENSITY) / - (O2_IN_AIR * O2_DENSITY + N2_IN_AIR * N2_DENSITY) * 1000 - 10000; - if (entry->mod < 0) - entry->mod = 0; - if (entry->ead < 0) - entry->ead = 0; - if (entry->end < 0) - entry->end = 0; - if (entry->eadd < 0) - entry->eadd = 0; - } -} - -void fill_o2_values(struct divecomputer *dc, struct plot_info *pi, struct dive *dive) -/* In the samples from each dive computer, there may be uninitialised oxygen - * sensor or setpoint values, e.g. when events were inserted into the dive log - * or if the dive computer does not report o2 values with every sample. But - * for drawing the profile a complete series of valid o2 pressure values is - * required. This function takes the oxygen sensor data and setpoint values - * from the structures of plotinfo and replaces the zero values with their - * last known values so that the oxygen sensor data are complete and ready - * for plotting. This function called by: create_plot_info_new() */ -{ - int i, j; - pressure_t last_sensor[3], o2pressure; - pressure_t amb_pressure; - - for (i = 0; i < pi->nr; i++) { - struct plot_data *entry = pi->entry + i; - - if (dc->divemode == CCR) { - if (i == 0) { // For 1st iteration, initialise the last_sensor values - for (j = 0; j < dc->no_o2sensors; j++) - last_sensor[j].mbar = pi->entry->o2sensor[j].mbar; - } else { // Now re-insert the missing oxygen pressure values - for (j = 0; j < dc->no_o2sensors; j++) - if (entry->o2sensor[j].mbar) - last_sensor[j].mbar = entry->o2sensor[j].mbar; - else - entry->o2sensor[j].mbar = last_sensor[j].mbar; - } // having initialised the empty o2 sensor values for this point on the profile, - amb_pressure.mbar = depth_to_mbar(entry->depth, dive); - o2pressure.mbar = calculate_ccr_po2(entry, dc); // ...calculate the po2 based on the sensor data - entry->o2pressure.mbar = MIN(o2pressure.mbar, amb_pressure.mbar); - } else { - entry->o2pressure.mbar = 0; // initialise po2 to zero for dctype = OC - } - } -} - -#ifdef DEBUG_GAS -/* A CCR debug function that writes the cylinder pressure and the oxygen values to the file debug_print_profiledata.dat: - * Called in create_plot_info_new() - */ -static void debug_print_profiledata(struct plot_info *pi) -{ - FILE *f1; - struct plot_data *entry; - int i; - if (!(f1 = fopen("debug_print_profiledata.dat", "w"))) { - printf("File open error for: debug_print_profiledata.dat\n"); - } else { - fprintf(f1, "id t1 gas gasint t2 t3 dil dilint t4 t5 setpoint sensor1 sensor2 sensor3 t6 po2 fo2\n"); - for (i = 0; i < pi->nr; i++) { - entry = pi->entry + i; - fprintf(f1, "%d gas=%8d %8d ; dil=%8d %8d ; o2_sp= %d %d %d %d PO2= %f\n", i, SENSOR_PRESSURE(entry), - INTERPOLATED_PRESSURE(entry), O2CYLINDER_PRESSURE(entry), INTERPOLATED_O2CYLINDER_PRESSURE(entry), - entry->o2pressure.mbar, entry->o2sensor[0].mbar, entry->o2sensor[1].mbar, entry->o2sensor[2].mbar, entry->pressures.o2); - } - fclose(f1); - } -} -#endif - -/* - * Create a plot-info with smoothing and ranged min/max - * - * This also makes sure that we have extra empty events on both - * sides, so that you can do end-points without having to worry - * about it. - */ -void create_plot_info_new(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool fast) -{ - int o2, he, o2max; - init_decompression(dive); - /* Create the new plot data */ - free((void *)last_pi_entry_new); - - get_dive_gas(dive, &o2, &he, &o2max); - if (dc->divemode == FREEDIVE){ - pi->dive_type = FREEDIVE; - } else if (he > 0) { - pi->dive_type = TRIMIX; - } else { - if (o2) - pi->dive_type = NITROX; - else - pi->dive_type = AIR; - } - - last_pi_entry_new = populate_plot_entries(dive, dc, pi); - - check_gas_change_events(dive, dc, pi); /* Populate the gas index from the gas change events */ - check_setpoint_events(dive, dc, pi); /* Populate setpoints */ - setup_gas_sensor_pressure(dive, dc, pi); /* Try to populate our gas pressure knowledge */ - if (!fast) { - populate_pressure_information(dive, dc, pi, false); /* .. calculate missing pressure entries for all gasses except o2 */ - if (dc->divemode == CCR) /* For CCR dives.. */ - populate_pressure_information(dive, dc, pi, true); /* .. calculate missing o2 gas pressure entries */ - } - fill_o2_values(dc, pi, dive); /* .. and insert the O2 sensor data having 0 values. */ - calculate_sac(dive, pi); /* Calculate sac */ - calculate_deco_information(dive, dc, pi, false); /* and ceiling information, using gradient factor values in Preferences) */ - calculate_gas_information_new(dive, pi); /* Calculate gas partial pressures */ - -#ifdef DEBUG_GAS - debug_print_profiledata(pi); -#endif - - pi->meandepth = dive->dc.meandepth.mm; - analyze_plot_info(pi); -} - -struct divecomputer *select_dc(struct dive *dive) -{ - unsigned int max = number_of_computers(dive); - unsigned int i = dc_number; - - /* Reset 'dc_number' if we've switched dives and it is now out of range */ - if (i >= max) - dc_number = i = 0; - - return get_dive_dc(dive, i); -} - -static void plot_string(struct plot_info *pi, struct plot_data *entry, struct membuffer *b, bool has_ndl) -{ - int pressurevalue, mod, ead, end, eadd; - const char *depth_unit, *pressure_unit, *temp_unit, *vertical_speed_unit; - double depthvalue, tempvalue, speedvalue, sacvalue; - int decimals; - const char *unit; - - depthvalue = get_depth_units(entry->depth, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "@: %d:%02d\nD: %.1f%s\n"), FRACTION(entry->sec, 60), depthvalue, depth_unit); - if (GET_PRESSURE(entry)) { - pressurevalue = get_pressure_units(GET_PRESSURE(entry), &pressure_unit); - put_format(b, translate("gettextFromC", "P: %d%s\n"), pressurevalue, pressure_unit); - } - if (entry->temperature) { - tempvalue = get_temp_units(entry->temperature, &temp_unit); - put_format(b, translate("gettextFromC", "T: %.1f%s\n"), tempvalue, temp_unit); - } - speedvalue = get_vertical_speed_units(abs(entry->speed), NULL, &vertical_speed_unit); - /* Ascending speeds are positive, descending are negative */ - if (entry->speed > 0) - speedvalue *= -1; - put_format(b, translate("gettextFromC", "V: %.1f%s\n"), speedvalue, vertical_speed_unit); - sacvalue = get_volume_units(entry->sac, &decimals, &unit); - if (entry->sac && prefs.show_sac) - put_format(b, translate("gettextFromC", "SAC: %.*f%s/min\n"), decimals, sacvalue, unit); - if (entry->cns) - put_format(b, translate("gettextFromC", "CNS: %u%%\n"), entry->cns); - if (prefs.pp_graphs.po2) - put_format(b, translate("gettextFromC", "pO%s: %.2fbar\n"), UTF8_SUBSCRIPT_2, entry->pressures.o2); - if (prefs.pp_graphs.pn2) - put_format(b, translate("gettextFromC", "pN%s: %.2fbar\n"), UTF8_SUBSCRIPT_2, entry->pressures.n2); - if (prefs.pp_graphs.phe) - put_format(b, translate("gettextFromC", "pHe: %.2fbar\n"), entry->pressures.he); - if (prefs.mod) { - mod = (int)get_depth_units(entry->mod, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "MOD: %d%s\n"), mod, depth_unit); - } - eadd = (int)get_depth_units(entry->eadd, NULL, &depth_unit); - if (prefs.ead) { - switch (pi->dive_type) { - case NITROX: - ead = (int)get_depth_units(entry->ead, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "EAD: %d%s\nEADD: %d%s\n"), ead, depth_unit, eadd, depth_unit); - break; - case TRIMIX: - end = (int)get_depth_units(entry->end, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "END: %d%s\nEADD: %d%s\n"), end, depth_unit, eadd, depth_unit); - break; - case AIR: - case FREEDIVING: - /* nothing */ - break; - } - } - if (entry->stopdepth) { - depthvalue = get_depth_units(entry->stopdepth, NULL, &depth_unit); - if (entry->ndl) { - /* this is a safety stop as we still have ndl */ - if (entry->stoptime) - put_format(b, translate("gettextFromC", "Safetystop: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), - depthvalue, depth_unit); - else - put_format(b, translate("gettextFromC", "Safetystop: unkn time @ %.0f%s\n"), - depthvalue, depth_unit); - } else { - /* actual deco stop */ - if (entry->stoptime) - put_format(b, translate("gettextFromC", "Deco: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), - depthvalue, depth_unit); - else - put_format(b, translate("gettextFromC", "Deco: unkn time @ %.0f%s\n"), - depthvalue, depth_unit); - } - } else if (entry->in_deco) { - put_string(b, translate("gettextFromC", "In deco\n")); - } else if (has_ndl) { - put_format(b, translate("gettextFromC", "NDL: %umin\n"), DIV_UP(entry->ndl, 60)); - } - if (entry->tts) - put_format(b, translate("gettextFromC", "TTS: %umin\n"), DIV_UP(entry->tts, 60)); - if (entry->stopdepth_calc && entry->stoptime_calc) { - depthvalue = get_depth_units(entry->stopdepth_calc, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "Deco: %umin @ %.0f%s (calc)\n"), DIV_UP(entry->stoptime_calc, 60), - depthvalue, depth_unit); - } else if (entry->in_deco_calc) { - /* This means that we have no NDL left, - * and we have no deco stop, - * so if we just accend to the surface slowly - * (ascent_mm_per_step / ascent_s_per_step) - * everything will be ok. */ - put_string(b, translate("gettextFromC", "In deco (calc)\n")); - } else if (prefs.calcndltts && entry->ndl_calc != 0) { - if(entry->ndl_calc < MAX_PROFILE_DECO) - put_format(b, translate("gettextFromC", "NDL: %umin (calc)\n"), DIV_UP(entry->ndl_calc, 60)); - else - put_format(b, "%s", translate("gettextFromC", "NDL: >2h (calc)\n")); - } - if (entry->tts_calc) { - if (entry->tts_calc < MAX_PROFILE_DECO) - put_format(b, translate("gettextFromC", "TTS: %umin (calc)\n"), DIV_UP(entry->tts_calc, 60)); - else - put_format(b, "%s", translate("gettextFromC", "TTS: >2h (calc)\n")); - } - if (entry->rbt) - put_format(b, translate("gettextFromC", "RBT: %umin\n"), DIV_UP(entry->rbt, 60)); - if (entry->ceiling) { - depthvalue = get_depth_units(entry->ceiling, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "Calculated ceiling %.0f%s\n"), depthvalue, depth_unit); - if (prefs.calcalltissues) { - int k; - for (k = 0; k < 16; k++) { - if (entry->ceilings[k]) { - depthvalue = get_depth_units(entry->ceilings[k], NULL, &depth_unit); - put_format(b, translate("gettextFromC", "Tissue %.0fmin: %.1f%s\n"), buehlmann_N2_t_halflife[k], depthvalue, depth_unit); - } - } - } - } - if (entry->heartbeat && prefs.hrgraph) - put_format(b, translate("gettextFromC", "heartbeat: %d\n"), entry->heartbeat); - if (entry->bearing) - put_format(b, translate("gettextFromC", "bearing: %d\n"), entry->bearing); - if (entry->running_sum) { - depthvalue = get_depth_units(entry->running_sum / entry->sec, NULL, &depth_unit); - put_format(b, translate("gettextFromC", "mean depth to here %.1f%s\n"), depthvalue, depth_unit); - } - - strip_mb(b); -} - -struct plot_data *get_plot_details_new(struct plot_info *pi, int time, struct membuffer *mb) -{ - struct plot_data *entry = NULL; - int i; - - for (i = 0; i < pi->nr; i++) { - entry = pi->entry + i; - if (entry->sec >= time) - break; - } - if (entry) - plot_string(pi, entry, mb, pi->has_ndl); - return (entry); -} - -/* Compare two plot_data entries and writes the results into a string */ -void compare_samples(struct plot_data *e1, struct plot_data *e2, char *buf, int bufsize, int sum) -{ - struct plot_data *start, *stop, *data; - const char *depth_unit, *pressure_unit, *vertical_speed_unit; - char *buf2 = malloc(bufsize); - int avg_speed, max_asc_speed, max_desc_speed; - int delta_depth, avg_depth, max_depth, min_depth; - int bar_used, last_pressure, pressurevalue; - int count, last_sec, delta_time; - - double depthvalue, speedvalue; - - if (bufsize > 0) - buf[0] = '\0'; - if (e1 == NULL || e2 == NULL) { - free(buf2); - return; - } - - if (e1->sec < e2->sec) { - start = e1; - stop = e2; - } else if (e1->sec > e2->sec) { - start = e2; - stop = e1; - } else { - free(buf2); - return; - } - count = 0; - avg_speed = 0; - max_asc_speed = 0; - max_desc_speed = 0; - - delta_depth = abs(start->depth - stop->depth); - delta_time = abs(start->sec - stop->sec); - avg_depth = 0; - max_depth = 0; - min_depth = INT_MAX; - bar_used = 0; - - last_sec = start->sec; - last_pressure = GET_PRESSURE(start); - - data = start; - while (data != stop) { - data = start + count; - if (sum) - avg_speed += abs(data->speed) * (data->sec - last_sec); - else - avg_speed += data->speed * (data->sec - last_sec); - avg_depth += data->depth * (data->sec - last_sec); - - if (data->speed > max_desc_speed) - max_desc_speed = data->speed; - if (data->speed < max_asc_speed) - max_asc_speed = data->speed; - - if (data->depth < min_depth) - min_depth = data->depth; - if (data->depth > max_depth) - max_depth = data->depth; - /* Try to detect gas changes */ - if (GET_PRESSURE(data) < last_pressure + 2000) - bar_used += last_pressure - GET_PRESSURE(data); - - count += 1; - last_sec = data->sec; - last_pressure = GET_PRESSURE(data); - } - avg_depth /= stop->sec - start->sec; - avg_speed /= stop->sec - start->sec; - - snprintf(buf, bufsize, translate("gettextFromC", "%sT: %d:%02d min"), UTF8_DELTA, delta_time / 60, delta_time % 60); - memcpy(buf2, buf, bufsize); - - depthvalue = get_depth_units(delta_depth, NULL, &depth_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_DELTA, depthvalue, depth_unit); - memcpy(buf2, buf, bufsize); - - depthvalue = get_depth_units(min_depth, NULL, &depth_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_DOWNWARDS_ARROW, depthvalue, depth_unit); - memcpy(buf2, buf, bufsize); - - depthvalue = get_depth_units(max_depth, NULL, &depth_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_UPWARDS_ARROW, depthvalue, depth_unit); - memcpy(buf2, buf, bufsize); - - depthvalue = get_depth_units(avg_depth, NULL, &depth_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s\n"), buf2, UTF8_AVERAGE, depthvalue, depth_unit); - memcpy(buf2, buf, bufsize); - - speedvalue = get_vertical_speed_units(abs(max_desc_speed), NULL, &vertical_speed_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s%sV:%.2f%s"), buf2, UTF8_DOWNWARDS_ARROW, speedvalue, vertical_speed_unit); - memcpy(buf2, buf, bufsize); - - speedvalue = get_vertical_speed_units(abs(max_asc_speed), NULL, &vertical_speed_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sV:%.2f%s"), buf2, UTF8_UPWARDS_ARROW, speedvalue, vertical_speed_unit); - memcpy(buf2, buf, bufsize); - - speedvalue = get_vertical_speed_units(abs(avg_speed), NULL, &vertical_speed_unit); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sV:%.2f%s"), buf2, UTF8_AVERAGE, speedvalue, vertical_speed_unit); - memcpy(buf2, buf, bufsize); - - /* Only print if gas has been used */ - if (bar_used) { - pressurevalue = get_pressure_units(bar_used, &pressure_unit); - memcpy(buf2, buf, bufsize); - snprintf(buf, bufsize, translate("gettextFromC", "%s %sP:%d %s"), buf2, UTF8_DELTA, pressurevalue, pressure_unit); - } - - free(buf2); -} diff --git a/profile.h b/profile.h deleted file mode 100644 index a6dbfcf5f..000000000 --- a/profile.h +++ /dev/null @@ -1,109 +0,0 @@ -#ifndef PROFILE_H -#define PROFILE_H - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - STABLE, - SLOW, - MODERATE, - FAST, - CRAZY -} velocity_t; - -struct membuffer; -struct divecomputer; -struct plot_info; -struct plot_data { - unsigned int in_deco : 1; - int cylinderindex; - int sec; - /* pressure[0] is sensor cylinder pressure [when CCR, the pressure of the diluent cylinder] - * pressure[1] is interpolated cylinder pressure */ - int pressure[2]; - /* o2pressure[0] is o2 cylinder pressure [CCR] - * o2pressure[1] is interpolated o2 cylinder pressure [CCR] */ - int o2cylinderpressure[2]; - int temperature; - /* Depth info */ - int depth; - int ceiling; - int ceilings[16]; - int percentages[16]; - int ndl; - int tts; - int rbt; - int stoptime; - int stopdepth; - int cns; - int smoothed; - int sac; - int running_sum; - struct gas_pressures pressures; - pressure_t o2pressure; // for rebreathers, this is consensus measured po2, or setpoint otherwise. 0 for OC. - pressure_t o2sensor[3]; //for rebreathers with up to 3 PO2 sensors - pressure_t o2setpoint; - double mod, ead, end, eadd; - velocity_t velocity; - int speed; - struct plot_data *min[3]; - struct plot_data *max[3]; - int avg[3]; - /* values calculated by us */ - unsigned int in_deco_calc : 1; - int ndl_calc; - int tts_calc; - int stoptime_calc; - int stopdepth_calc; - int pressure_time; - int heartbeat; - int bearing; - double ambpressure; - double gfline; -}; - -struct ev_select { - char *ev_name; - bool plot_ev; -}; - -struct plot_info calculate_max_limits_new(struct dive *dive, struct divecomputer *given_dc); -void compare_samples(struct plot_data *e1, struct plot_data *e2, char *buf, int bufsize, int sum); -struct plot_data *populate_plot_entries(struct dive *dive, struct divecomputer *dc, struct plot_info *pi); -struct plot_info *analyze_plot_info(struct plot_info *pi); -void create_plot_info_new(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool fast); -void calculate_deco_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool print_mode); -struct plot_data *get_plot_details_new(struct plot_info *pi, int time, struct membuffer *); - -/* - * When showing dive profiles, we scale things to the - * current dive. However, we don't scale past less than - * 30 minutes or 90 ft, just so that small dives show - * up as such unless zoom is enabled. - * We also need to add 180 seconds at the end so the min/max - * plots correctly - */ -int get_maxtime(struct plot_info *pi); - -/* get the maximum depth to which we want to plot - * take into account the additional verical space needed to plot - * partial pressure graphs */ -int get_maxdepth(struct plot_info *pi); - -#define SENSOR_PR 0 -#define INTERPOLATED_PR 1 -#define SENSOR_PRESSURE(_entry) (_entry)->pressure[SENSOR_PR] -#define O2CYLINDER_PRESSURE(_entry) (_entry)->o2cylinderpressure[SENSOR_PR] -#define CYLINDER_PRESSURE(_o2, _entry) (_o2 ? O2CYLINDER_PRESSURE(_entry) : SENSOR_PRESSURE(_entry)) -#define INTERPOLATED_PRESSURE(_entry) (_entry)->pressure[INTERPOLATED_PR] -#define INTERPOLATED_O2CYLINDER_PRESSURE(_entry) (_entry)->o2cylinderpressure[INTERPOLATED_PR] -#define GET_PRESSURE(_entry) (SENSOR_PRESSURE(_entry) ? SENSOR_PRESSURE(_entry) : INTERPOLATED_PRESSURE(_entry)) -#define GET_O2CYLINDER_PRESSURE(_entry) (O2CYLINDER_PRESSURE(_entry) ? O2CYLINDER_PRESSURE(_entry) : INTERPOLATED_O2CYLINDER_PRESSURE(_entry)) -#define SAC_WINDOW 45 /* sliding window in seconds for current SAC calculation */ - -#ifdef __cplusplus -} -#endif -#endif // PROFILE_H diff --git a/qt-gui.h b/qt-gui.h deleted file mode 100644 index ca038b145..000000000 --- a/qt-gui.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef QT_GUI_H -#define QT_GUI_H - -#include - -void init_qt_late(); -void init_ui(); - -void run_ui(); -void exit_ui(); - -#if defined(SUBSURFACE_MOBILE) -#include -extern QObject *qqWindowObject; -#endif - -#endif // QT_GUI_H diff --git a/qt-init.cpp b/qt-init.cpp deleted file mode 100644 index b52dfd970..000000000 --- a/qt-init.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include -#include -#include -#include "helpers.h" - -void init_qt_late() -{ - QApplication *application = qApp; - // tell Qt to use system proxies - // note: on Linux, "system" == "environment variables" - QNetworkProxyFactory::setUseSystemConfiguration(true); - - // for Win32 and Qt5 we try to set the locale codec to UTF-8. - // this makes QFile::encodeName() work. -#ifdef Q_OS_WIN - QTextCodec::setCodecForLocale(QTextCodec::codecForMib(106)); -#endif - - QCoreApplication::setOrganizationName("Subsurface"); - QCoreApplication::setOrganizationDomain("subsurface.hohndel.org"); - QCoreApplication::setApplicationName("Subsurface"); - // find plugins installed in the application directory (without this SVGs don't work on Windows) - QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); - QLocale loc; - QString uiLang = uiLanguage(&loc); - QLocale::setDefault(loc); - - // we don't have translations for English - if we don't check for this - // Qt will proceed to load the second language in preference order - not what we want - // on Linux this tends to be en-US, but on the Mac it's just en - if (!uiLang.startsWith("en") || uiLang.startsWith("en-GB")) { - qtTranslator = new QTranslator; - if (qtTranslator->load(loc, "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { - application->installTranslator(qtTranslator); - } else { - qDebug() << "can't find Qt localization for locale" << uiLang << "searching in" << QLibraryInfo::location(QLibraryInfo::TranslationsPath); - } - ssrfTranslator = new QTranslator; - if (ssrfTranslator->load(loc, "subsurface", "_") || - ssrfTranslator->load(loc, "subsurface", "_", getSubsurfaceDataPath("translations")) || - ssrfTranslator->load(loc, "subsurface", "_", getSubsurfaceDataPath("../translations"))) { - application->installTranslator(ssrfTranslator); - } else { - qDebug() << "can't find Subsurface localization for locale" << uiLang; - } - } -} diff --git a/qt-models/models.h b/qt-models/models.h index c9212195e..f152af469 100644 --- a/qt-models/models.h +++ b/qt-models/models.h @@ -15,9 +15,9 @@ #include "metrics.h" -#include "../dive.h" -#include "../divelist.h" -#include "../divecomputer.h" +#include "subsurface-core/dive.h" +#include "subsurface-core/divelist.h" +#include "subsurface-core/divecomputer.h" #include "cleanertablemodel.h" #include "treemodel.h" diff --git a/qt-ui/CMakeLists.txt b/qt-ui/CMakeLists.txt index 9def39ff3..a860faed5 100644 --- a/qt-ui/CMakeLists.txt +++ b/qt-ui/CMakeLists.txt @@ -4,6 +4,10 @@ qt5_wrap_ui(SUBSURFACE_UI_HDRS ${SUBSURFACE_UI}) qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc) source_group("Subsurface Interface Files" FILES ${SUBSURFACE_UI}) +if(BTSUPPORT) + set(BT_SRC_FILES btdeviceselectiondialog.cpp) +endif() + # the interface, in C++ set(SUBSURFACE_INTERFACE updatemanager.cpp diff --git a/qt-ui/configuredivecomputerdialog.h b/qt-ui/configuredivecomputerdialog.h index be76644a9..9ad30ac67 100644 --- a/qt-ui/configuredivecomputerdialog.h +++ b/qt-ui/configuredivecomputerdialog.h @@ -4,7 +4,7 @@ #include #include #include "ui_configuredivecomputerdialog.h" -#include "../libdivecomputer.h" +#include "subsurface-core/libdivecomputer.h" #include "configuredivecomputer.h" #include #include diff --git a/qt-ui/divelogimportdialog.h b/qt-ui/divelogimportdialog.h index 03bb14029..2d12c7cac 100644 --- a/qt-ui/divelogimportdialog.h +++ b/qt-ui/divelogimportdialog.h @@ -9,8 +9,8 @@ #include #include -#include "../dive.h" -#include "../divelist.h" +#include "subsurface-core/dive.h" +#include "subsurface-core/divelist.h" namespace Ui { class DiveLogImportDialog; diff --git a/qt-ui/graphicsview-common.h b/qt-ui/graphicsview-common.h index 3c1cb75a0..2a757b2ae 100644 --- a/qt-ui/graphicsview-common.h +++ b/qt-ui/graphicsview-common.h @@ -1,7 +1,7 @@ #ifndef GRAPHICSVIEW_COMMON_H #define GRAPHICSVIEW_COMMON_H -#include "../color.h" +#include "subsurface-core/color.h" #include #include #include diff --git a/qthelper.cpp b/qthelper.cpp deleted file mode 100644 index a12d25333..000000000 --- a/qthelper.cpp +++ /dev/null @@ -1,1653 +0,0 @@ -#include "qthelper.h" -#include "helpers.h" -#include "gettextfromc.h" -#include "statistics.h" -#include "membuffer.h" -#include "subsurfacesysinfo.h" -#include "version.h" -#include "divecomputer.h" -#include "time.h" -#include "gettextfromc.h" -#include -#include -#include "file.h" -#include "prefs-macros.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -const char *existing_filename; -static QString shortDateFormat; -static QString dateFormat; -static QString timeFormat; -static QLocale loc; - -#define translate(_context, arg) trGettext(arg) -static const QString DEGREE_SIGNS("dD" UTF8_DEGREE); - -Dive::Dive() : - m_number(-1), - dive(NULL) -{ -} - -Dive::~Dive() -{ -} - -int Dive::number() const -{ - return m_number; -} - -int Dive::id() const -{ - return m_id; -} - -QString Dive::date() const -{ - return m_date; -} - -timestamp_t Dive::timestamp() const -{ - return m_timestamp; -} - -QString Dive::time() const -{ - return m_time; -} - -QString Dive::location() const -{ - return m_location; -} - -QString Dive::duration() const -{ - return m_duration; -} - -QString Dive::depth() const -{ - return m_depth; -} - -QString Dive::divemaster() const -{ - return m_divemaster; -} - -QString Dive::buddy() const -{ - return m_buddy; -} - -QString Dive::airTemp() const -{ - return m_airTemp; -} - -QString Dive::waterTemp() const -{ - return m_waterTemp; -} - -QString Dive::notes() const -{ - return m_notes; -} - -QString Dive::tags() const -{ - return m_tags; -} - -QString Dive::gas() const -{ - return m_gas; -} - -QString Dive::sac() const -{ - return m_sac; -} - -QString Dive::weight() const -{ - return m_weight; -} - -QString Dive::suit() const -{ - return m_suit; -} - -QString Dive::cylinder() const -{ - return m_cylinder; -} - -QString Dive::trip() const -{ - return m_trip; -} - -int Dive::rating() const -{ - return m_rating; -} - -void Dive::put_divemaster() -{ - if (!dive->divemaster) - m_divemaster = "--"; - else - m_divemaster = dive->divemaster; -} - -void Dive::put_date_time() -{ - QDateTime localTime = QDateTime::fromTime_t(dive->when - gettimezoneoffset(displayed_dive.when)); - localTime.setTimeSpec(Qt::UTC); - m_date = localTime.date().toString(dateFormat); - m_time = localTime.time().toString(timeFormat); -} - -void Dive::put_timestamp() -{ - m_timestamp = dive->when; -} - -void Dive::put_location() -{ - m_location = QString::fromUtf8(get_dive_location(dive)); - if (m_location.isEmpty()) { - m_location = "--"; - } -} - -void Dive::put_depth() -{ - m_depth = get_depth_string(dive->dc.maxdepth.mm, true, true); -} - -void Dive::put_duration() -{ - m_duration = get_dive_duration_string(dive->duration.seconds, QObject::tr("h:"), QObject::tr("min")); -} - -void Dive::put_buddy() -{ - if (!dive->buddy) - m_buddy = "--"; - else - m_buddy = dive->buddy; -} - -void Dive::put_temp() -{ - m_airTemp = get_temperature_string(dive->airtemp, true); - m_waterTemp = get_temperature_string(dive->watertemp, true); - if (m_airTemp.isEmpty()) { - m_airTemp = "--"; - } - if (m_waterTemp.isEmpty()) { - m_waterTemp = "--"; - } -} - -void Dive::put_notes() -{ - if (same_string(dive->dc.model, "planned dive")) { - QTextDocument notes; - notes.setHtml(QString::fromUtf8(dive->notes)); - m_notes = notes.toPlainText(); - } else { - m_notes = QString::fromUtf8(dive->notes); - } - if (m_notes.isEmpty()) { - m_notes = "--"; - } -} - -void Dive::put_tags() -{ - char buffer[256]; - taglist_get_tagstring(dive->tag_list, buffer, 256); - m_tags = QString(buffer); -} - -void Dive::put_gas() -{ - int added = 0; - QString gas, gases; - for (int i = 0; i < MAX_CYLINDERS; i++) { - if (!is_cylinder_used(dive, i)) - continue; - gas = dive->cylinder[i].type.description; - gas += QString(!gas.isEmpty() ? " " : "") + gasname(&dive->cylinder[i].gasmix); - // if has a description and if such gas is not already present - if (!gas.isEmpty() && gases.indexOf(gas) == -1) { - if (added > 0) - gases += QString(" / "); - gases += gas; - added++; - } - } - m_gas = gases; -} - -void Dive::put_sac() -{ - if (dive->sac) { - const char *unit; - int decimal; - double value = get_volume_units(dive->sac, &decimal, &unit); - m_sac = QString::number(value, 'f', decimal).append(unit); - } -} - -void Dive::put_weight() -{ - weight_t tw = { total_weight(dive) }; - m_weight = weight_string(tw.grams); -} - -void Dive::put_suit() -{ - m_suit = QString(dive->suit); -} - -void Dive::put_cylinder() -{ - m_cylinder = QString(dive->cylinder[0].type.description); -} - -void Dive::put_trip() -{ - dive_trip *trip = dive->divetrip; - if (trip) { - m_trip = QString(trip->location); - } -} - -QString weight_string(int weight_in_grams) -{ - QString str; - if (get_units()->weight == units::KG) { - int gr = weight_in_grams % 1000; - int kg = weight_in_grams / 1000; - if (kg >= 20.0) { - str = QString("0"); - } else { - str = QString("%1.%2").arg(kg).arg((unsigned)(gr) / 100); - } - } else { - double lbs = grams_to_lbs(weight_in_grams); - str = QString("%1").arg(lbs, 0, 'f', lbs >= 40.0 ? 0 : 1); - } - return (str); -} - -QString distance_string(int distanceInMeters) -{ - QString str; - if(get_units()->length == units::METERS) { - if (distanceInMeters >= 1000) - str = QString(translate("gettextFromC", "%1km")).arg(distanceInMeters / 1000); - else - str = QString(translate("gettextFromC", "%1m")).arg(distanceInMeters); - } else { - double miles = m_to_mile(distanceInMeters); - if (miles >= 1.0) - str = QString(translate("gettextFromC", "%1mi")).arg((int)miles); - else - str = QString(translate("gettextFromC", "%1yd")).arg((int)(miles * 1760)); - } - return str; -} - -extern "C" const char *printGPSCoords(int lat, int lon) -{ - unsigned int latdeg, londeg; - unsigned int latmin, lonmin; - double latsec, lonsec; - QString lath, lonh, result; - - if (!lat && !lon) - return strdup(""); - - if (prefs.coordinates_traditional) { - lath = lat >= 0 ? translate("gettextFromC", "N") : translate("gettextFromC", "S"); - lonh = lon >= 0 ? translate("gettextFromC", "E") : translate("gettextFromC", "W"); - lat = abs(lat); - lon = abs(lon); - latdeg = lat / 1000000U; - londeg = lon / 1000000U; - latmin = (lat % 1000000U) * 60U; - lonmin = (lon % 1000000U) * 60U; - latsec = (latmin % 1000000) * 60; - lonsec = (lonmin % 1000000) * 60; - result.sprintf("%u%s%02d\'%06.3f\"%s %u%s%02d\'%06.3f\"%s", - latdeg, UTF8_DEGREE, latmin / 1000000, latsec / 1000000, lath.toUtf8().data(), - londeg, UTF8_DEGREE, lonmin / 1000000, lonsec / 1000000, lonh.toUtf8().data()); - } else { - result.sprintf("%f %f", (double) lat / 1000000.0, (double) lon / 1000000.0); - } - return strdup(result.toUtf8().data()); -} - -/** -* Try to parse in a generic manner a coordinate. -*/ -static bool parseCoord(const QString& txt, int& pos, const QString& positives, - const QString& negatives, const QString& others, - double& value) -{ - bool numberDefined = false, degreesDefined = false, - minutesDefined = false, secondsDefined = false; - double number = 0.0; - int posBeforeNumber = pos; - int sign = 0; - value = 0.0; - while (pos < txt.size()) { - if (txt[pos].isDigit()) { - if (numberDefined) - return false; - QRegExp numberRe("(\\d+(?:[\\.,]\\d+)?).*"); - if (!numberRe.exactMatch(txt.mid(pos))) - return false; - number = numberRe.cap(1).toDouble(); - numberDefined = true; - posBeforeNumber = pos; - pos += numberRe.cap(1).size() - 1; - } else if (positives.indexOf(txt[pos]) >= 0) { - if (sign != 0) - return false; - sign = 1; - if (degreesDefined || numberDefined) { - //sign after the degrees => - //at the end of the coordinate - ++pos; - break; - } - } else if (negatives.indexOf(txt[pos]) >= 0) { - if (sign != 0) { - if (others.indexOf(txt[pos]) >= 0) - //special case for the '-' sign => next coordinate - break; - return false; - } - sign = -1; - if (degreesDefined || numberDefined) { - //sign after the degrees => - //at the end of the coordinate - ++pos; - break; - } - } else if (others.indexOf(txt[pos]) >= 0) { - //we are at the next coordinate. - break; - } else if (DEGREE_SIGNS.indexOf(txt[pos]) >= 0 || - (txt[pos].isSpace() && !degreesDefined && numberDefined)) { - if (!numberDefined) - return false; - if (degreesDefined) { - //next coordinate => need to put back the number - pos = posBeforeNumber; - numberDefined = false; - break; - } - value += number; - numberDefined = false; - degreesDefined = true; - } else if (txt[pos] == '\'' || (txt[pos].isSpace() && !minutesDefined && numberDefined)) { - if (!numberDefined || minutesDefined) - return false; - value += number / 60.0; - numberDefined = false; - minutesDefined = true; - } else if (txt[pos] == '"' || (txt[pos].isSpace() && !secondsDefined && numberDefined)) { - if (!numberDefined || secondsDefined) - return false; - value += number / 3600.0; - numberDefined = false; - secondsDefined = true; - } else if ((numberDefined || minutesDefined || secondsDefined) && - (txt[pos] == ',' || txt[pos] == ';')) { - // next coordinate coming up - // eat the ',' and any subsequent white space - while (txt[++pos].isSpace()) - /* nothing */ ; - break; - } else { - return false; - } - ++pos; - } - if (!degreesDefined && numberDefined) { - value = number; //just a single number => degrees - } else if (!minutesDefined && numberDefined) { - value += number / 60.0; - } else if (!secondsDefined && numberDefined) { - value += number / 3600.0; - } else if (numberDefined) { - return false; - } - if (sign == -1) value *= -1.0; - return true; -} - -/** -* Parse special coordinate formats that cannot be handled by parseCoord. -*/ -static bool parseSpecialCoords(const QString& txt, double& latitude, double& longitude) { - QRegExp xmlFormat("(-?\\d+(?:\\.\\d+)?),?\\s+(-?\\d+(?:\\.\\d+)?)"); - if (xmlFormat.exactMatch(txt)) { - latitude = xmlFormat.cap(1).toDouble(); - longitude = xmlFormat.cap(2).toDouble(); - return true; - } - return false; -} - -bool parseGpsText(const QString &gps_text, double *latitude, double *longitude) -{ - static const QString POS_LAT = QString("+N") + translate("gettextFromC", "N"); - static const QString NEG_LAT = QString("-S") + translate("gettextFromC", "S"); - static const QString POS_LON = QString("+E") + translate("gettextFromC", "E"); - static const QString NEG_LON = QString("-W") + translate("gettextFromC", "W"); - - //remove the useless spaces (but keep the ones separating numbers) - static const QRegExp SPACE_CLEANER("\\s*([" + POS_LAT + NEG_LAT + POS_LON + - NEG_LON + DEGREE_SIGNS + "'\"\\s])\\s*"); - const QString normalized = gps_text.trimmed().toUpper().replace(SPACE_CLEANER, "\\1"); - - if (normalized.isEmpty()) { - *latitude = 0.0; - *longitude = 0.0; - return true; - } - if (parseSpecialCoords(normalized, *latitude, *longitude)) - return true; - int pos = 0; - return parseCoord(normalized, pos, POS_LAT, NEG_LAT, POS_LON + NEG_LON, *latitude) && - parseCoord(normalized, pos, POS_LON, NEG_LON, "", *longitude) && - pos == normalized.size(); -} - -#if 0 // we'll need something like this for the dive site management, eventually -bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out) -{ - double latitude, longitude; - int latudeg, longudeg; - bool ignore; - bool *parsed = parsed_out ?: &ignore; - *parsed = true; - - /* if we have a master and the dive's gps address is different from it, - * don't change the dive */ - if (master && (master->latitude.udeg != dive->latitude.udeg || - master->longitude.udeg != dive->longitude.udeg)) - return false; - - if (!(*parsed = parseGpsText(gps_text, &latitude, &longitude))) - return false; - - latudeg = rint(1000000 * latitude); - longudeg = rint(1000000 * longitude); - - /* if dive gps didn't change, nothing changed */ - if (dive->latitude.udeg == latudeg && dive->longitude.udeg == longudeg) - return false; - /* ok, update the dive and mark things changed */ - dive->latitude.udeg = latudeg; - dive->longitude.udeg = longudeg; - return true; -} -#endif - -QList getDivesInTrip(dive_trip_t *trip) -{ - QList ret; - int i; - struct dive *d; - for_each_dive (i, d) { - if (d->divetrip == trip) { - ret.push_back(get_divenr(d)); - } - } - return ret; -} - -// we need this to be uniq, but also make sure -// it doesn't change during the life time of a Subsurface session -// oh, and it has no meaning whatsoever - that's why we have the -// silly initial number and increment by 3 :-) -int dive_getUniqID(struct dive *d) -{ - static QSet ids; - static int maxId = 83529; - - int id = d->id; - if (id) { - if (!ids.contains(id)) { - qDebug() << "WTF - only I am allowed to create IDs"; - ids.insert(id); - } - return id; - } - maxId += 3; - id = maxId; - Q_ASSERT(!ids.contains(id)); - ids.insert(id); - return id; -} - - -static xmlDocPtr get_stylesheet_doc(const xmlChar *uri, xmlDictPtr, int, void *, xsltLoadType) -{ - QFile f(QLatin1String(":/xslt/") + (const char *)uri); - if (!f.open(QIODevice::ReadOnly)) { - if (verbose > 0) { - qDebug() << "cannot open stylesheet" << QLatin1String(":/xslt/") + (const char *)uri; - return NULL; - } - } - /* Load and parse the data */ - QByteArray source = f.readAll(); - - xmlDocPtr doc = xmlParseMemory(source, source.size()); - return doc; -} - -extern "C" xsltStylesheetPtr get_stylesheet(const char *name) -{ - // this needs to be done only once, but doesn't hurt to run every time - xsltSetLoaderFunc(get_stylesheet_doc); - - // get main document: - xmlDocPtr doc = get_stylesheet_doc((const xmlChar *)name, NULL, 0, NULL, XSLT_LOAD_START); - if (!doc) - return NULL; - - // xsltSetGenericErrorFunc(stderr, NULL); - xsltStylesheetPtr xslt = xsltParseStylesheetDoc(doc); - if (!xslt) { - xmlFreeDoc(doc); - return NULL; - } - - return xslt; -} - - -extern "C" timestamp_t picture_get_timestamp(char *filename) -{ - EXIFInfo exif; - memblock mem; - int retval; - - // filename might not be the actual filename, so let's go via the hash. - if (readfile(localFilePath(QString(filename)).toUtf8().data(), &mem) <= 0) - return 0; - retval = exif.parseFrom((const unsigned char *)mem.buffer, (unsigned)mem.size); - free(mem.buffer); - if (retval != PARSE_EXIF_SUCCESS) - return 0; - return exif.epoch(); -} - -extern "C" char *move_away(const char *old_path) -{ - if (verbose > 1) - qDebug() << "move away" << old_path; - QDir oldDir(old_path); - QDir newDir; - QString newPath; - int i = 0; - do { - newPath = QString(old_path) + QString(".%1").arg(++i); - newDir.setPath(newPath); - } while(newDir.exists()); - if (verbose > 1) - qDebug() << "renaming to" << newPath; - if (!oldDir.rename(old_path, newPath)) { - if (verbose) - qDebug() << "rename of" << old_path << "to" << newPath << "failed"; - // this next one we only try on Windows... if we are on a different platform - // we simply give up and return an empty string -#ifdef WIN32 - if (subsurface_dir_rename(old_path, qPrintable(newPath)) == 0) -#endif - return strdup(""); - } - return strdup(qPrintable(newPath)); -} - -extern "C" char *get_file_name(const char *fileName) -{ - QFileInfo fileInfo(fileName); - return strdup(fileInfo.fileName().toUtf8()); -} - -extern "C" void copy_image_and_overwrite(const char *cfileName, const char *path, const char *cnewName) -{ - QString fileName(cfileName); - QString newName(path); - newName += cnewName; - QFile file(newName); - if (file.exists()) - file.remove(); - if (!QFile::copy(fileName, newName)) - qDebug() << "copy of" << fileName << "to" << newName << "failed"; -} - -extern "C" bool string_sequence_contains(const char *string_sequence, const char *text) -{ - if (same_string(text, "") || same_string(string_sequence, "")) - return false; - - QString stringSequence(string_sequence); - QStringList strings = stringSequence.split(",", QString::SkipEmptyParts); - Q_FOREACH (const QString& string, strings) { - if (string.trimmed().compare(QString(text).trimmed(), Qt::CaseInsensitive) == 0) - return true; - } - return false; -} - -static bool lessThan(const QPair &a, const QPair &b) -{ - return a.second < b.second; -} - -void selectedDivesGasUsed(QVector > &gasUsedOrdered) -{ - int i, j; - struct dive *d; - QMap gasUsed; - for_each_dive (i, d) { - if (!d->selected) - continue; - volume_t diveGases[MAX_CYLINDERS] = {}; - get_gas_used(d, diveGases); - for (j = 0; j < MAX_CYLINDERS; j++) - if (diveGases[j].mliter) { - QString gasName = gasname(&d->cylinder[j].gasmix); - gasUsed[gasName] += diveGases[j].mliter; - } - } - Q_FOREACH(const QString& gas, gasUsed.keys()) { - gasUsedOrdered.append(qMakePair(gas, gasUsed[gas])); - } - qSort(gasUsedOrdered.begin(), gasUsedOrdered.end(), lessThan); -} - -QString getUserAgent() -{ - QString arch; - // fill in the system data - use ':' as separator - // replace all other ':' with ' ' so that this is easy to parse - QString userAgent = QString("Subsurface:%1:").arg(subsurface_version()); - userAgent.append(SubsurfaceSysInfo::prettyOsName().replace(':', ' ') + ":"); - arch = SubsurfaceSysInfo::buildCpuArchitecture().replace(':', ' '); - userAgent.append(arch); - if (arch == "i386") - userAgent.append("/" + SubsurfaceSysInfo::currentCpuArchitecture()); - userAgent.append(":" + uiLanguage(NULL)); - return userAgent; - -} - -QString uiLanguage(QLocale *callerLoc) -{ - QSettings s; - s.beginGroup("Language"); - - if (!s.value("UseSystemLanguage", true).toBool()) { - loc = QLocale(s.value("UiLanguage", QLocale().uiLanguages().first()).toString()); - } else { - loc = QLocale(QLocale().uiLanguages().first()); - } - - QString uiLang = loc.uiLanguages().first(); - s.endGroup(); - - // there's a stupid Qt bug on MacOS where uiLanguages doesn't give us the country info - if (!uiLang.contains('-') && uiLang != loc.bcp47Name()) { - QLocale loc2(loc.bcp47Name()); - loc = loc2; - uiLang = loc2.uiLanguages().first(); - } - if (callerLoc) - *callerLoc = loc; - - // the short format is fine - // the long format uses long weekday and month names, so replace those with the short ones - // for time we don't want the time zone designator and don't want leading zeroes on the hours - shortDateFormat = loc.dateFormat(QLocale::ShortFormat); - dateFormat = loc.dateFormat(QLocale::LongFormat); - dateFormat.replace("dddd,", "ddd").replace("dddd", "ddd").replace("MMMM", "MMM"); - // special hack for Swedish as our switching from long weekday names to short weekday names - // messes things up there - dateFormat.replace("'en' 'den' d:'e'", " d"); - timeFormat = loc.timeFormat(); - timeFormat.replace("(t)", "").replace(" t", "").replace("t", "").replace("hh", "h").replace("HH", "H").replace("'kl'.", ""); - timeFormat.replace(".ss", "").replace(":ss", "").replace("ss", ""); - return uiLang; -} - -QLocale getLocale() -{ - return loc; -} - -QString getDateFormat() -{ - return dateFormat; -} -void set_filename(const char *filename, bool force) -{ - if (!force && existing_filename) - return; - free((void *)existing_filename); - if (filename) - existing_filename = strdup(filename); - else - existing_filename = NULL; -} - -const QString get_dc_nickname(const char *model, uint32_t deviceid) -{ - const DiveComputerNode *existNode = dcList.getExact(model, deviceid); - - if (existNode && !existNode->nickName.isEmpty()) - return existNode->nickName; - else - return model; -} - -QString get_depth_string(int mm, bool showunit, bool showdecimal) -{ - if (prefs.units.length == units::METERS) { - double meters = mm / 1000.0; - return QString("%1%2").arg(meters, 0, 'f', (showdecimal && meters < 20.0) ? 1 : 0).arg(showunit ? translate("gettextFromC", "m") : ""); - } else { - double feet = mm_to_feet(mm); - return QString("%1%2").arg(feet, 0, 'f', 0).arg(showunit ? translate("gettextFromC", "ft") : ""); - } -} - -QString get_depth_string(depth_t depth, bool showunit, bool showdecimal) -{ - return get_depth_string(depth.mm, showunit, showdecimal); -} - -QString get_depth_unit() -{ - if (prefs.units.length == units::METERS) - return QString("%1").arg(translate("gettextFromC", "m")); - else - return QString("%1").arg(translate("gettextFromC", "ft")); -} - -QString get_weight_string(weight_t weight, bool showunit) -{ - QString str = weight_string(weight.grams); - if (get_units()->weight == units::KG) { - str = QString("%1%2").arg(str).arg(showunit ? translate("gettextFromC", "kg") : ""); - } else { - str = QString("%1%2").arg(str).arg(showunit ? translate("gettextFromC", "lbs") : ""); - } - return (str); -} - -QString get_weight_unit() -{ - if (prefs.units.weight == units::KG) - return QString("%1").arg(translate("gettextFromC", "kg")); - else - return QString("%1").arg(translate("gettextFromC", "lbs")); -} - -/* these methods retrieve used gas per cylinder */ -static unsigned start_pressure(cylinder_t *cyl) -{ - return cyl->start.mbar ?: cyl->sample_start.mbar; -} - -static unsigned end_pressure(cylinder_t *cyl) -{ - return cyl->end.mbar ?: cyl->sample_end.mbar; -} - -QString get_cylinder_used_gas_string(cylinder_t *cyl, bool showunit) -{ - int decimals; - const char *unit; - double gas_usage; - /* Get the cylinder gas use in mbar */ - gas_usage = start_pressure(cyl) - end_pressure(cyl); - /* Can we turn it into a volume? */ - if (cyl->type.size.mliter) { - gas_usage = bar_to_atm(gas_usage / 1000); - gas_usage *= cyl->type.size.mliter; - gas_usage = get_volume_units(gas_usage, &decimals, &unit); - } else { - gas_usage = get_pressure_units(gas_usage, &unit); - decimals = 0; - } - // translate("gettextFromC","%.*f %s" - return QString("%1 %2").arg(gas_usage, 0, 'f', decimals).arg(showunit ? unit : ""); -} - -QString get_temperature_string(temperature_t temp, bool showunit) -{ - if (temp.mkelvin == 0) { - return ""; //temperature not defined - } else if (prefs.units.temperature == units::CELSIUS) { - double celsius = mkelvin_to_C(temp.mkelvin); - return QString("%1%2%3").arg(celsius, 0, 'f', 1).arg(showunit ? (UTF8_DEGREE) : "").arg(showunit ? translate("gettextFromC", "C") : ""); - } else { - double fahrenheit = mkelvin_to_F(temp.mkelvin); - return QString("%1%2%3").arg(fahrenheit, 0, 'f', 1).arg(showunit ? (UTF8_DEGREE) : "").arg(showunit ? translate("gettextFromC", "F") : ""); - } -} - -QString get_temp_unit() -{ - if (prefs.units.temperature == units::CELSIUS) - return QString(UTF8_DEGREE "C"); - else - return QString(UTF8_DEGREE "F"); -} - -QString get_volume_string(volume_t volume, bool showunit, int mbar) -{ - const char *unit; - int decimals; - double value = get_volume_units(volume.mliter, &decimals, &unit); - if (mbar) { - // we are showing a tank size - // fix the weird imperial way of denominating size and provide - // reasonable number of decimals - if (prefs.units.volume == units::CUFT) - value *= bar_to_atm(mbar / 1000.0); - decimals = (value > 20.0) ? 0 : (value > 2.0) ? 1 : 2; - } - return QString("%1%2").arg(value, 0, 'f', decimals).arg(showunit ? unit : ""); -} - -QString get_volume_unit() -{ - const char *unit; - (void) get_volume_units(0, NULL, &unit); - return QString(unit); -} - -QString get_pressure_string(pressure_t pressure, bool showunit) -{ - if (prefs.units.pressure == units::BAR) { - double bar = pressure.mbar / 1000.0; - return QString("%1%2").arg(bar, 0, 'f', 1).arg(showunit ? translate("gettextFromC", "bar") : ""); - } else { - double psi = mbar_to_PSI(pressure.mbar); - return QString("%1%2").arg(psi, 0, 'f', 0).arg(showunit ? translate("gettextFromC", "psi") : ""); - } -} - -QString getSubsurfaceDataPath(QString folderToFind) -{ - QString execdir; - QDir folder; - - // first check if we are running in the build dir, so the path that we - // are looking for is just a subdirectory of the execution path; - // this also works on Windows as there we install the dirs - // under the application path - execdir = QCoreApplication::applicationDirPath(); - folder = QDir(execdir.append(QDir::separator()).append(folderToFind)); - if (folder.exists()) - return folder.absolutePath(); - - // next check for the Linux typical $(prefix)/share/subsurface - execdir = QCoreApplication::applicationDirPath(); - if (execdir.contains("bin")) { - folder = QDir(execdir.replace("bin", "share/subsurface/").append(folderToFind)); - if (folder.exists()) - return folder.absolutePath(); - } - // then look for the usual locations on a Mac - execdir = QCoreApplication::applicationDirPath(); - folder = QDir(execdir.append("/../Resources/share/").append(folderToFind)); - if (folder.exists()) - return folder.absolutePath(); - execdir = QCoreApplication::applicationDirPath(); - folder = QDir(execdir.append("/../Resources/").append(folderToFind)); - if (folder.exists()) - return folder.absolutePath(); - return QString(""); -} - -static const char *printing_templates = "printing_templates"; - -QString getPrintingTemplatePathUser() -{ - static QString path = QString(); - if (path.isEmpty()) - path = QString(system_default_directory()) + QDir::separator() + QString(printing_templates); - return path; -} - -QString getPrintingTemplatePathBundle() -{ - static QString path = QString(); - if (path.isEmpty()) - path = getSubsurfaceDataPath(printing_templates); - return path; -} - -void copyPath(QString src, QString dst) -{ - QDir dir(src); - if (!dir.exists()) - return; - foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { - QString dst_path = dst + QDir::separator() + d; - dir.mkpath(dst_path); - copyPath(src + QDir::separator() + d, dst_path); - } - foreach (QString f, dir.entryList(QDir::Files)) - QFile::copy(src + QDir::separator() + f, dst + QDir::separator() + f); -} - -int gettimezoneoffset(timestamp_t when) -{ - QDateTime dt1, dt2; - if (when == 0) - dt1 = QDateTime::currentDateTime(); - else - dt1 = QDateTime::fromMSecsSinceEpoch(when * 1000); - dt2 = dt1.toUTC(); - dt1.setTimeSpec(Qt::UTC); - return dt2.secsTo(dt1); -} - -int parseTemperatureToMkelvin(const QString &text) -{ - int mkelvin; - QString numOnly = text; - numOnly.replace(",", ".").remove(QRegExp("[^-0-9.]")); - if (numOnly.isEmpty()) - return 0; - double number = numOnly.toDouble(); - switch (prefs.units.temperature) { - case units::CELSIUS: - mkelvin = C_to_mkelvin(number); - break; - case units::FAHRENHEIT: - mkelvin = F_to_mkelvin(number); - break; - default: - mkelvin = 0; - } - return mkelvin; -} - -QString get_dive_duration_string(timestamp_t when, QString hourText, QString minutesText) -{ - int hrs, mins; - mins = (when + 59) / 60; - hrs = mins / 60; - mins -= hrs * 60; - - QString displayTime; - if (hrs) - displayTime = QString("%1%2%3%4").arg(hrs).arg(hourText).arg(mins, 2, 10, QChar('0')).arg(minutesText); - else - displayTime = QString("%1%2").arg(mins).arg(minutesText); - - return displayTime; -} - -QString get_dive_date_string(timestamp_t when) -{ - QDateTime ts; - ts.setMSecsSinceEpoch(when * 1000L); - return loc.toString(ts.toUTC(), dateFormat + " " + timeFormat); -} - -QString get_short_dive_date_string(timestamp_t when) -{ - QDateTime ts; - ts.setMSecsSinceEpoch(when * 1000L); - return loc.toString(ts.toUTC(), shortDateFormat + " " + timeFormat); -} - -const char *get_dive_date_c_string(timestamp_t when) -{ - QString text = get_dive_date_string(when); - return strdup(text.toUtf8().data()); -} - -bool is_same_day(timestamp_t trip_when, timestamp_t dive_when) -{ - static timestamp_t twhen = (timestamp_t) 0; - static struct tm tmt; - struct tm tmd; - - utc_mkdate(dive_when, &tmd); - - if (twhen != trip_when) { - twhen = trip_when; - utc_mkdate(twhen, &tmt); - } - - return ((tmd.tm_mday == tmt.tm_mday) && (tmd.tm_mon == tmt.tm_mon) && (tmd.tm_year == tmt.tm_year)); -} - -QString get_trip_date_string(timestamp_t when, int nr, bool getday) -{ - struct tm tm; - utc_mkdate(when, &tm); - QDateTime localTime = QDateTime::fromTime_t(when); - localTime.setTimeSpec(Qt::UTC); - QString ret ; - - QString suffix = " " + QObject::tr("(%n dive(s))", "", nr); - if (getday) { - ret = localTime.date().toString(dateFormat) + suffix; - } else { - ret = localTime.date().toString("MMM yy") + suffix; - } - return ret; - -} - -extern "C" void reverseGeoLookup(degrees_t latitude, degrees_t longitude, uint32_t uuid) -{ - QNetworkRequest request; - QNetworkAccessManager *rgl = new QNetworkAccessManager(); - request.setUrl(QString("http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&accept-language=%1&lat=%2&lon=%3") - .arg(uiLanguage(NULL)).arg(latitude.udeg / 1000000.0).arg(longitude.udeg / 1000000.0)); - request.setRawHeader("Accept", "text/json"); - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); - QNetworkReply *reply = rgl->get(request); - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - QJsonParseError errorObject; - QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &errorObject); - if (errorObject.error != QJsonParseError::NoError) { - qDebug() << errorObject.errorString(); - } else { - QJsonObject obj = jsonDoc.object(); - QJsonObject address = obj.value("address").toObject(); - qDebug() << "found country:" << address.value("country").toString(); - struct dive_site *ds = get_dive_site_by_uuid(uuid); - ds->notes = add_to_string(ds->notes, "countrytag: %s", address.value("country").toString().toUtf8().data()); - } -} - -QHash hashOf; -QMutex hashOfMutex; -QHash localFilenameOf; - -extern "C" char * hashstring(char * filename) -{ - return hashOf[QString(filename)].toHex().data(); -} - -const QString hashfile_name() -{ - return QString(system_default_directory()).append("/hashes"); -} - -extern "C" char *hashfile_name_string() -{ - return strdup(hashfile_name().toUtf8().data()); -} - -void read_hashes() -{ - QFile hashfile(hashfile_name()); - if (hashfile.open(QIODevice::ReadOnly)) { - QDataStream stream(&hashfile); - stream >> localFilenameOf; - stream >> hashOf; - hashfile.close(); - } -} - -void write_hashes() -{ - QSaveFile hashfile(hashfile_name()); - if (hashfile.open(QIODevice::WriteOnly)) { - QDataStream stream(&hashfile); - stream << localFilenameOf; - stream << hashOf; - hashfile.commit(); - } else { - qDebug() << "cannot open" << hashfile.fileName(); - } -} - -void add_hash(const QString filename, QByteArray hash) -{ - QMutexLocker locker(&hashOfMutex); - hashOf[filename] = hash; - localFilenameOf[hash] = filename; -} - -QByteArray hashFile(const QString filename) -{ - QCryptographicHash hash(QCryptographicHash::Sha1); - QFile imagefile(filename); - if (imagefile.exists() && imagefile.open(QIODevice::ReadOnly)) { - hash.addData(&imagefile); - add_hash(filename, hash.result()); - return hash.result(); - } else { - return QByteArray(); - } -} - -void learnHash(struct picture *picture, QByteArray hash) -{ - if (picture->hash) - free(picture->hash); - QMutexLocker locker(&hashOfMutex); - hashOf[QString(picture->filename)] = hash; - picture->hash = strdup(hash.toHex()); -} - -QString localFilePath(const QString originalFilename) -{ - if (hashOf.contains(originalFilename) && localFilenameOf.contains(hashOf[originalFilename])) - return localFilenameOf[hashOf[originalFilename]]; - else - return originalFilename; -} - -QString fileFromHash(char *hash) -{ - return localFilenameOf[QByteArray::fromHex(hash)]; -} - -void updateHash(struct picture *picture) { - QByteArray hash = hashFile(fileFromHash(picture->hash)); - QMutexLocker locker(&hashOfMutex); - hashOf[QString(picture->filename)] = hash; - char *old = picture->hash; - picture->hash = strdup(hash.toHex()); - free(old); -} - -void hashPicture(struct picture *picture) -{ - char *oldHash = copy_string(picture->hash); - learnHash(picture, hashFile(QString(picture->filename))); - if (!same_string(picture->hash, "") && !same_string(picture->hash, oldHash)) - mark_divelist_changed((true)); - free(oldHash); -} - -extern "C" void cache_picture(struct picture *picture) -{ - QString filename = picture->filename; - if (!hashOf.contains(filename)) - QtConcurrent::run(hashPicture, picture); -} - -void learnImages(const QDir dir, int max_recursions, bool recursed) -{ - QDir current(dir); - QStringList filters, files; - - if (max_recursions) { - foreach (QString dirname, dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs)) { - learnImages(QDir(dir.filePath(dirname)), max_recursions - 1, true); - } - } - - foreach (QString format, QImageReader::supportedImageFormats()) { - filters.append(QString("*.").append(format)); - } - - foreach (QString file, dir.entryList(filters, QDir::Files)) { - files.append(dir.absoluteFilePath(file)); - } - - QtConcurrent::blockingMap(files, hashFile); -} - -extern "C" const char *local_file_path(struct picture *picture) -{ - QString hashString = picture->hash; - if (hashString.isEmpty()) { - QByteArray hash = hashFile(picture->filename); - free(picture->hash); - picture->hash = strdup(hash.toHex().data()); - } - QString localFileName = fileFromHash(picture->hash); - if (localFileName.isEmpty()) - localFileName = picture->filename; - return strdup(qPrintable(localFileName)); -} - -extern "C" bool picture_exists(struct picture *picture) -{ - QString localFilename = fileFromHash(picture->hash); - QByteArray hash = hashFile(localFilename); - return same_string(hash.toHex().data(), picture->hash); -} - -const QString picturedir() -{ - return QString(system_default_directory()).append("/picturedata/"); -} - -extern "C" char *picturedir_string() -{ - return strdup(picturedir().toUtf8().data()); -} - -/* when we get a picture from git storage (local or remote) and can't find the picture - * based on its hash, we create a local copy with the hash as filename and the appropriate - * suffix */ -extern "C" void savePictureLocal(struct picture *picture, const char *data, int len) -{ - QString dirname = picturedir(); - QDir localPictureDir(dirname); - localPictureDir.mkpath(dirname); - QString suffix(picture->filename); - suffix.replace(QRegularExpression(".*\\."), ""); - QString filename(dirname + picture->hash + "." + suffix); - QSaveFile out(filename); - if (out.open(QIODevice::WriteOnly)) { - out.write(data, len); - out.commit(); - add_hash(filename, QByteArray::fromHex(picture->hash)); - } -} - -extern "C" void picture_load_exif_data(struct picture *p) -{ - EXIFInfo exif; - memblock mem; - - if (readfile(localFilePath(QString(p->filename)).toUtf8().data(), &mem) <= 0) - goto picture_load_exit; - if (exif.parseFrom((const unsigned char *)mem.buffer, (unsigned)mem.size) != PARSE_EXIF_SUCCESS) - goto picture_load_exit; - p->longitude.udeg= lrint(1000000.0 * exif.GeoLocation.Longitude); - p->latitude.udeg = lrint(1000000.0 * exif.GeoLocation.Latitude); - -picture_load_exit: - free(mem.buffer); - return; -} - -QString get_gas_string(struct gasmix gas) -{ - uint o2 = (get_o2(&gas) + 5) / 10, he = (get_he(&gas) + 5) / 10; - QString result = gasmix_is_air(&gas) ? QObject::tr("AIR") : he == 0 ? (o2 == 100 ? QObject::tr("OXYGEN") : QString("EAN%1").arg(o2, 2, 10, QChar('0'))) : QString("%1/%2").arg(o2).arg(he); - return result; -} - -QString get_divepoint_gas_string(const divedatapoint &p) -{ - return get_gas_string(p.gasmix); -} - -weight_t string_to_weight(const char *str) -{ - const char *end; - double value = strtod_flags(str, &end, 0); - QString rest = QString(end).trimmed(); - QString local_kg = QObject::tr("kg"); - QString local_lbs = QObject::tr("lbs"); - weight_t weight; - - if (rest.startsWith("kg") || rest.startsWith(local_kg)) - goto kg; - // using just "lb" instead of "lbs" is intentional - some people might enter the singular - if (rest.startsWith("lb") || rest.startsWith(local_lbs)) - goto lbs; - if (prefs.units.weight == prefs.units.LBS) - goto lbs; -kg: - weight.grams = rint(value * 1000); - return weight; -lbs: - weight.grams = lbs_to_grams(value); - return weight; -} - -depth_t string_to_depth(const char *str) -{ - const char *end; - double value = strtod_flags(str, &end, 0); - QString rest = QString(end).trimmed(); - QString local_ft = QObject::tr("ft"); - QString local_m = QObject::tr("m"); - depth_t depth; - - if (rest.startsWith("m") || rest.startsWith(local_m)) - goto m; - if (rest.startsWith("ft") || rest.startsWith(local_ft)) - goto ft; - if (prefs.units.length == prefs.units.FEET) - goto ft; -m: - depth.mm = rint(value * 1000); - return depth; -ft: - depth.mm = feet_to_mm(value); - return depth; -} - -pressure_t string_to_pressure(const char *str) -{ - const char *end; - double value = strtod_flags(str, &end, 0); - QString rest = QString(end).trimmed(); - QString local_psi = QObject::tr("psi"); - QString local_bar = QObject::tr("bar"); - pressure_t pressure; - - if (rest.startsWith("bar") || rest.startsWith(local_bar)) - goto bar; - if (rest.startsWith("psi") || rest.startsWith(local_psi)) - goto psi; - if (prefs.units.pressure == prefs.units.PSI) - goto psi; -bar: - pressure.mbar = rint(value * 1000); - return pressure; -psi: - pressure.mbar = psi_to_mbar(value); - return pressure; -} - -volume_t string_to_volume(const char *str, pressure_t workp) -{ - const char *end; - double value = strtod_flags(str, &end, 0); - QString rest = QString(end).trimmed(); - QString local_l = QObject::tr("l"); - QString local_cuft = QObject::tr("cuft"); - volume_t volume; - - if (rest.startsWith("l") || rest.startsWith("ℓ") || rest.startsWith(local_l)) - goto l; - if (rest.startsWith("cuft") || rest.startsWith(local_cuft)) - goto cuft; - /* - * If we don't have explicit units, and there is no working - * pressure, we're going to assume "liter" even in imperial - * measurements. - */ - if (!workp.mbar) - goto l; - if (prefs.units.volume == prefs.units.LITER) - goto l; -cuft: - if (workp.mbar) - value /= bar_to_atm(workp.mbar / 1000.0); - value = cuft_to_l(value); -l: - volume.mliter = rint(value * 1000); - return volume; -} - -fraction_t string_to_fraction(const char *str) -{ - const char *end; - double value = strtod_flags(str, &end, 0); - fraction_t fraction; - - fraction.permille = rint(value * 10); - return fraction; -} - -int getCloudURL(QString &filename) -{ - QString email = QString(prefs.cloud_storage_email); - email.replace(QRegularExpression("[^a-zA-Z0-9@._+-]"), ""); - if (email.isEmpty() || same_string(prefs.cloud_storage_password, "")) - return report_error("Please configure Cloud storage email and password in the preferences"); - if (email != prefs.cloud_storage_email_encoded) { - free(prefs.cloud_storage_email_encoded); - prefs.cloud_storage_email_encoded = strdup(qPrintable(email)); - } - filename = QString(QString(prefs.cloud_git_url) + "/%1[%1]").arg(email); - if (verbose) - qDebug() << "cloud URL set as" << filename; - return 0; -} - -extern "C" char *cloud_url() -{ - QString filename; - getCloudURL(filename); - return strdup(filename.toUtf8().data()); -} - -void loadPreferences() -{ - QSettings s; - QVariant v; - - s.beginGroup("Units"); - if (s.value("unit_system").toString() == "metric") { - prefs.unit_system = METRIC; - prefs.units = SI_units; - } else if (s.value("unit_system").toString() == "imperial") { - prefs.unit_system = IMPERIAL; - prefs.units = IMPERIAL_units; - } else { - prefs.unit_system = PERSONALIZE; - GET_UNIT("length", length, units::FEET, units::METERS); - GET_UNIT("pressure", pressure, units::PSI, units::BAR); - GET_UNIT("volume", volume, units::CUFT, units::LITER); - GET_UNIT("temperature", temperature, units::FAHRENHEIT, units::CELSIUS); - GET_UNIT("weight", weight, units::LBS, units::KG); - } - GET_UNIT("vertical_speed_time", vertical_speed_time, units::MINUTES, units::SECONDS); - GET_BOOL("coordinates", coordinates_traditional); - s.endGroup(); - s.beginGroup("TecDetails"); - GET_BOOL("po2graph", pp_graphs.po2); - GET_BOOL("pn2graph", pp_graphs.pn2); - GET_BOOL("phegraph", pp_graphs.phe); - GET_DOUBLE("po2threshold", pp_graphs.po2_threshold); - GET_DOUBLE("pn2threshold", pp_graphs.pn2_threshold); - GET_DOUBLE("phethreshold", pp_graphs.phe_threshold); - GET_BOOL("mod", mod); - GET_DOUBLE("modpO2", modpO2); - GET_BOOL("ead", ead); - GET_BOOL("redceiling", redceiling); - GET_BOOL("dcceiling", dcceiling); - GET_BOOL("calcceiling", calcceiling); - GET_BOOL("calcceiling3m", calcceiling3m); - GET_BOOL("calcndltts", calcndltts); - GET_BOOL("calcalltissues", calcalltissues); - GET_BOOL("hrgraph", hrgraph); - GET_BOOL("tankbar", tankbar); - GET_BOOL("percentagegraph", percentagegraph); - GET_INT("gflow", gflow); - GET_INT("gfhigh", gfhigh); - GET_BOOL("gf_low_at_maxdepth", gf_low_at_maxdepth); - GET_BOOL("show_ccr_setpoint",show_ccr_setpoint); - GET_BOOL("show_ccr_sensors",show_ccr_sensors); - GET_BOOL("zoomed_plot", zoomed_plot); - set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth); - GET_BOOL("show_sac", show_sac); - GET_BOOL("display_unused_tanks", display_unused_tanks); - GET_BOOL("show_average_depth", show_average_depth); - s.endGroup(); - - s.beginGroup("GeneralSettings"); - GET_TXT("default_filename", default_filename); - GET_INT("default_file_behavior", default_file_behavior); - if (prefs.default_file_behavior == UNDEFINED_DEFAULT_FILE) { - // undefined, so check if there's a filename set and - // use that, otherwise go with no default file - if (QString(prefs.default_filename).isEmpty()) - prefs.default_file_behavior = NO_DEFAULT_FILE; - else - prefs.default_file_behavior = LOCAL_DEFAULT_FILE; - } - GET_TXT("default_cylinder", default_cylinder); - GET_BOOL("use_default_file", use_default_file); - GET_INT("defaultsetpoint", defaultsetpoint); - GET_INT("o2consumption", o2consumption); - GET_INT("pscr_ratio", pscr_ratio); - s.endGroup(); - - s.beginGroup("Display"); - // get the font from the settings or our defaults - // respect the system default font size if none is explicitly set - QFont defaultFont = s.value("divelist_font", prefs.divelist_font).value(); - if (IS_FP_SAME(system_divelist_default_font_size, -1.0)) { - prefs.font_size = qApp->font().pointSizeF(); - system_divelist_default_font_size = prefs.font_size; // this way we don't save it on exit - } - prefs.font_size = s.value("font_size", prefs.font_size).toFloat(); - // painful effort to ignore previous default fonts on Windows - ridiculous - QString fontName = defaultFont.toString(); - if (fontName.contains(",")) - fontName = fontName.left(fontName.indexOf(",")); - if (subsurface_ignore_font(fontName.toUtf8().constData())) { - defaultFont = QFont(prefs.divelist_font); - } else { - free((void *)prefs.divelist_font); - prefs.divelist_font = strdup(fontName.toUtf8().constData()); - } - defaultFont.setPointSizeF(prefs.font_size); - qApp->setFont(defaultFont); - GET_INT("displayinvalid", display_invalid_dives); - s.endGroup(); - - s.beginGroup("Animations"); - GET_INT("animation_speed", animation_speed); - s.endGroup(); - - s.beginGroup("Network"); - GET_INT_DEF("proxy_type", proxy_type, QNetworkProxy::DefaultProxy); - GET_TXT("proxy_host", proxy_host); - GET_INT("proxy_port", proxy_port); - GET_BOOL("proxy_auth", proxy_auth); - GET_TXT("proxy_user", proxy_user); - GET_TXT("proxy_pass", proxy_pass); - s.endGroup(); - - s.beginGroup("CloudStorage"); - GET_TXT("email", cloud_storage_email); - GET_BOOL("save_password_local", save_password_local); - if (prefs.save_password_local) { // GET_TEXT macro is not a single statement - GET_TXT("password", cloud_storage_password); - } - GET_INT("cloud_verification_status", cloud_verification_status); - GET_BOOL("cloud_background_sync", cloud_background_sync); - - // creating the git url here is simply a convenience when C code wants - // to compare against that git URL - it's always derived from the base URL - GET_TXT("cloud_base_url", cloud_base_url); - prefs.cloud_git_url = strdup(qPrintable(QString(prefs.cloud_base_url) + "/git")); - s.endGroup(); - - // Subsurface webservice id is stored outside of the groups - GET_TXT("subsurface_webservice_uid", userid); - - // GeoManagement - s.beginGroup("geocoding"); -#ifdef DISABLED - GET_BOOL("enable_geocoding", geocoding.enable_geocoding); - GET_BOOL("parse_dive_without_gps", geocoding.parse_dive_without_gps); - GET_BOOL("tag_existing_dives", geocoding.tag_existing_dives); -#else - prefs.geocoding.enable_geocoding = true; -#endif - GET_ENUM("cat0", taxonomy_category, geocoding.category[0]); - GET_ENUM("cat1", taxonomy_category, geocoding.category[1]); - GET_ENUM("cat2", taxonomy_category, geocoding.category[2]); - s.endGroup(); - -} - -extern "C" bool isCloudUrl(const char *filename) -{ - QString email = QString(prefs.cloud_storage_email); - email.replace(QRegularExpression("[^a-zA-Z0-9@._+-]"), ""); - if (!email.isEmpty() && - QString(QString(prefs.cloud_git_url) + "/%1[%1]").arg(email) == filename) - return true; - return false; -} - -extern "C" bool getProxyString(char **buffer) -{ - if (prefs.proxy_type == QNetworkProxy::HttpProxy) { - QString proxy; - if (prefs.proxy_auth) - proxy = QString("http://%1:%2@%3:%4").arg(prefs.proxy_user).arg(prefs.proxy_pass) - .arg(prefs.proxy_host).arg(prefs.proxy_port); - else - proxy = QString("http://%1:%2").arg(prefs.proxy_host).arg(prefs.proxy_port); - if (buffer) - *buffer = strdup(qPrintable(proxy)); - return true; - } - return false; -} - -extern "C" void subsurface_mkdir(const char *dir) -{ - QDir directory; - if (!directory.mkpath(QString(dir))) - qDebug() << "failed to create path" << dir; -} - -extern "C" void parse_display_units(char *line) -{ - qDebug() << line; -} - -static QByteArray currentApplicationState; - -QByteArray getCurrentAppState() -{ - return currentApplicationState; -} - -void setCurrentAppState(QByteArray state) -{ - currentApplicationState = state; -} - -extern "C" bool in_planner() -{ - return (currentApplicationState == "PlanDive" || currentApplicationState == "EditPlannedDive"); -} diff --git a/qthelper.h b/qthelper.h deleted file mode 100644 index a2b7b6c39..000000000 --- a/qthelper.h +++ /dev/null @@ -1,135 +0,0 @@ -#ifndef QTHELPER_H -#define QTHELPER_H - -#include -#include -#include -#include "dive.h" -#include "divelist.h" -#include -#include - -class Dive { -private: - int m_number; - int m_id; - int m_rating; - QString m_date; - timestamp_t m_timestamp; - QString m_time; - QString m_location; - QString m_duration; - QString m_depth; - QString m_divemaster; - QString m_buddy; - QString m_airTemp; - QString m_waterTemp; - QString m_notes; - QString m_tags; - QString m_gas; - QString m_sac; - QString m_weight; - QString m_suit; - QString m_cylinder; - QString m_trip; - struct dive *dive; - void put_date_time(); - void put_timestamp(); - void put_location(); - void put_duration(); - void put_depth(); - void put_divemaster(); - void put_buddy(); - void put_temp(); - void put_notes(); - void put_tags(); - void put_gas(); - void put_sac(); - void put_weight(); - void put_suit(); - void put_cylinder(); - void put_trip(); - -public: - Dive(struct dive *dive) - : dive(dive) - { - m_number = dive->number; - m_id = dive->id; - m_rating = dive->rating; - put_date_time(); - put_location(); - put_duration(); - put_depth(); - put_divemaster(); - put_buddy(); - put_temp(); - put_notes(); - put_tags(); - put_gas(); - put_sac(); - put_timestamp(); - put_weight(); - put_suit(); - put_cylinder(); - put_trip(); - } - Dive(); - ~Dive(); - int number() const; - int id() const; - int rating() const; - QString date() const; - timestamp_t timestamp() const; - QString time() const; - QString location() const; - QString duration() const; - QString depth() const; - QString divemaster() const; - QString buddy() const; - QString airTemp() const; - QString waterTemp() const; - QString notes() const; - QString tags() const; - QString gas() const; - QString sac() const; - QString weight() const; - QString suit() const; - QString cylinder() const; - QString trip() const; -}; - -// global pointers for our translation -extern QTranslator *qtTranslator, *ssrfTranslator; - -QString weight_string(int weight_in_grams); -QString distance_string(int distanceInMeters); -bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out = 0); -extern "C" const char *printGPSCoords(int lat, int lon); -QList getDivesInTrip(dive_trip_t *trip); -QString get_gas_string(struct gasmix gas); -QString get_divepoint_gas_string(const divedatapoint& dp); -void read_hashes(); -void write_hashes(); -void updateHash(struct picture *picture); -QByteArray hashFile(const QString filename); -void learnImages(const QDir dir, int max_recursions, bool recursed); -void add_hash(const QString filename, QByteArray hash); -QString localFilePath(const QString originalFilename); -QString fileFromHash(char *hash); -void learnHash(struct picture *picture, QByteArray hash); -extern "C" void cache_picture(struct picture *picture); -weight_t string_to_weight(const char *str); -depth_t string_to_depth(const char *str); -pressure_t string_to_pressure(const char *str); -volume_t string_to_volume(const char *str, pressure_t workp); -fraction_t string_to_fraction(const char *str); -int getCloudURL(QString &filename); -void loadPreferences(); -bool parseGpsText(const QString &gps_text, double *latitude, double *longitude); -QByteArray getCurrentAppState(); -void setCurrentAppState(QByteArray state); -extern "C" bool in_planner(); -extern "C" void subsurface_mkdir(const char *dir); - -#endif // QTHELPER_H diff --git a/qthelperfromc.h b/qthelperfromc.h deleted file mode 100644 index d2e80144c..000000000 --- a/qthelperfromc.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef QTHELPERFROMC_H -#define QTHELPERFROMC_H - -bool getProxyString(char **buffer); -bool canReachCloudServer(); -void updateWindowTitle(); -bool isCloudUrl(const char *filename); -void subsurface_mkdir(const char *dir); -char *get_file_name(const char *fileName); -void copy_image_and_overwrite(const char *cfileName, const char *path, const char *cnewName); -char *hashstring(char *filename); -bool picture_exists(struct picture *picture); -char *move_away(const char *path); -const char *local_file_path(struct picture *picture); -void savePictureLocal(struct picture *picture, const char *data, int len); -void cache_picture(struct picture *picture); -char *cloud_url(); -char *hashfile_name_string(); -char *picturedir_string(); - -#endif // QTHELPERFROMC_H diff --git a/qtserialbluetooth.cpp b/qtserialbluetooth.cpp deleted file mode 100644 index 025ab8c34..000000000 --- a/qtserialbluetooth.cpp +++ /dev/null @@ -1,415 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include - -#if defined(SSRF_CUSTOM_SERIAL) - -#if defined(Q_OS_WIN) - #include - #include - #include -#endif - -#include - -extern "C" { -typedef struct serial_t { - /* Library context. */ - dc_context_t *context; - /* - * RFCOMM socket used for Bluetooth Serial communication. - */ -#if defined(Q_OS_WIN) - SOCKET socket; -#else - QBluetoothSocket *socket; -#endif - long timeout; -} serial_t; - -static int qt_serial_open(serial_t **out, dc_context_t *context, const char* devaddr) -{ - if (out == NULL) - return DC_STATUS_INVALIDARGS; - - // Allocate memory. - serial_t *serial_port = (serial_t *) malloc (sizeof (serial_t)); - if (serial_port == NULL) { - return DC_STATUS_NOMEMORY; - } - - // Library context. - serial_port->context = context; - - // Default to blocking reads. - serial_port->timeout = -1; - -#if defined(Q_OS_WIN) - // Create a RFCOMM socket - serial_port->socket = ::socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM); - - if (serial_port->socket == INVALID_SOCKET) { - free(serial_port); - return DC_STATUS_IO; - } - - SOCKADDR_BTH socketBthAddress; - int socketBthAddressBth = sizeof (socketBthAddress); - char *address = strdup(devaddr); - - ZeroMemory(&socketBthAddress, socketBthAddressBth); - qDebug() << "Trying to connect to address " << devaddr; - - if (WSAStringToAddressA(address, - AF_BTH, - NULL, - (LPSOCKADDR) &socketBthAddress, - &socketBthAddressBth - ) != 0) { - qDebug() << "FAiled to convert the address " << address; - free(address); - - return DC_STATUS_IO; - } - - free(address); - - socketBthAddress.addressFamily = AF_BTH; - socketBthAddress.port = BT_PORT_ANY; - memset(&socketBthAddress.serviceClassId, 0, sizeof(socketBthAddress.serviceClassId)); - socketBthAddress.serviceClassId = SerialPortServiceClass_UUID; - - // Try to connect to the device - if (::connect(serial_port->socket, - (struct sockaddr *) &socketBthAddress, - socketBthAddressBth - ) != 0) { - qDebug() << "Failed to connect to device"; - - return DC_STATUS_NODEVICE; - } - - qDebug() << "Succesfully connected to device"; -#else - // Create a RFCOMM socket - serial_port->socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); - - // Wait until the connection succeeds or until an error occurs - QEventLoop loop; - loop.connect(serial_port->socket, SIGNAL(connected()), SLOT(quit())); - loop.connect(serial_port->socket, SIGNAL(error(QBluetoothSocket::SocketError)), SLOT(quit())); - - // Create a timer. If the connection doesn't succeed after five seconds or no error occurs then stop the opening step - QTimer timer; - int msec = 5000; - timer.setSingleShot(true); - loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); - -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - // First try to connect on RFCOMM channel 1. This is the default channel for most devices - QBluetoothAddress remoteDeviceAddress(devaddr); - serial_port->socket->connectToService(remoteDeviceAddress, 1, QIODevice::ReadWrite | QIODevice::Unbuffered); - timer.start(msec); - loop.exec(); - - if (serial_port->socket->state() == QBluetoothSocket::ConnectingState) { - // It seems that the connection on channel 1 took more than expected. Wait another 15 seconds - qDebug() << "The connection on RFCOMM channel number 1 took more than expected. Wait another 15 seconds."; - timer.start(3 * msec); - loop.exec(); - } else if (serial_port->socket->state() == QBluetoothSocket::UnconnectedState) { - // Try to connect on channel number 5. Maybe this is a Shearwater Petrel2 device. - qDebug() << "Connection on channel 1 failed. Trying on channel number 5."; - serial_port->socket->connectToService(remoteDeviceAddress, 5, QIODevice::ReadWrite | QIODevice::Unbuffered); - timer.start(msec); - loop.exec(); - - if (serial_port->socket->state() == QBluetoothSocket::ConnectingState) { - // It seems that the connection on channel 5 took more than expected. Wait another 15 seconds - qDebug() << "The connection on RFCOMM channel number 5 took more than expected. Wait another 15 seconds."; - timer.start(3 * msec); - loop.exec(); - } - } -#elif defined(Q_OS_ANDROID) || (QT_VERSION >= 0x050500 && defined(Q_OS_MAC)) - // Try to connect to the device using the uuid of the Serial Port Profile service - QBluetoothAddress remoteDeviceAddress(devaddr); - serial_port->socket->connectToService(remoteDeviceAddress, QBluetoothUuid(QBluetoothUuid::SerialPort)); - timer.start(msec); - loop.exec(); - - if (serial_port->socket->state() == QBluetoothSocket::ConnectingState || - serial_port->socket->state() == QBluetoothSocket::ServiceLookupState) { - // It seems that the connection step took more than expected. Wait another 20 seconds. - qDebug() << "The connection step took more than expected. Wait another 20 seconds"; - timer.start(4 * msec); - loop.exec(); - } -#endif - if (serial_port->socket->state() != QBluetoothSocket::ConnectedState) { - - // Get the latest error and try to match it with one from libdivecomputer - QBluetoothSocket::SocketError err = serial_port->socket->error(); - qDebug() << "Failed to connect to device " << devaddr << ". Device state " << serial_port->socket->state() << ". Error: " << err; - - free (serial_port); - switch(err) { - case QBluetoothSocket::HostNotFoundError: - case QBluetoothSocket::ServiceNotFoundError: - return DC_STATUS_NODEVICE; - case QBluetoothSocket::UnsupportedProtocolError: - return DC_STATUS_PROTOCOL; -#if QT_VERSION >= 0x050400 - case QBluetoothSocket::OperationError: - return DC_STATUS_UNSUPPORTED; -#endif - case QBluetoothSocket::NetworkError: - return DC_STATUS_IO; - default: - return QBluetoothSocket::UnknownSocketError; - } - } -#endif - *out = serial_port; - - return DC_STATUS_SUCCESS; -} - -static int qt_serial_close(serial_t *device) -{ - if (device == NULL) - return DC_STATUS_SUCCESS; - -#if defined(Q_OS_WIN) - // Cleanup - closesocket(device->socket); - free(device); -#else - if (device->socket == NULL) { - free(device); - return DC_STATUS_SUCCESS; - } - - device->socket->close(); - - delete device->socket; - free(device); -#endif - - return DC_STATUS_SUCCESS; -} - -static int qt_serial_read(serial_t *device, void* data, unsigned int size) -{ -#if defined(Q_OS_WIN) - if (device == NULL) - return DC_STATUS_INVALIDARGS; - - unsigned int nbytes = 0; - int rc; - - while (nbytes < size) { - rc = recv (device->socket, (char *) data + nbytes, size - nbytes, 0); - - if (rc < 0) { - return -1; // Error during recv call. - } else if (rc == 0) { - break; // EOF reached. - } - - nbytes += rc; - } - - return nbytes; -#else - if (device == NULL || device->socket == NULL) - return DC_STATUS_INVALIDARGS; - - unsigned int nbytes = 0; - int rc; - - while(nbytes < size && device->socket->state() == QBluetoothSocket::ConnectedState) - { - rc = device->socket->read((char *) data + nbytes, size - nbytes); - - if (rc < 0) { - if (errno == EINTR || errno == EAGAIN) - continue; // Retry. - - return -1; // Something really bad happened :-( - } else if (rc == 0) { - // Wait until the device is available for read operations - QEventLoop loop; - QTimer timer; - timer.setSingleShot(true); - loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); - loop.connect(device->socket, SIGNAL(readyRead()), SLOT(quit())); - timer.start(device->timeout); - loop.exec(); - - if (!timer.isActive()) - return nbytes; - } - - nbytes += rc; - } - - return nbytes; -#endif -} - -static int qt_serial_write(serial_t *device, const void* data, unsigned int size) -{ -#if defined(Q_OS_WIN) - if (device == NULL) - return DC_STATUS_INVALIDARGS; - - unsigned int nbytes = 0; - int rc; - - while (nbytes < size) { - rc = send(device->socket, (char *) data + nbytes, size - nbytes, 0); - - if (rc < 0) { - return -1; // Error during send call. - } - - nbytes += rc; - } - - return nbytes; -#else - if (device == NULL || device->socket == NULL) - return DC_STATUS_INVALIDARGS; - - unsigned int nbytes = 0; - int rc; - - while(nbytes < size && device->socket->state() == QBluetoothSocket::ConnectedState) - { - rc = device->socket->write((char *) data + nbytes, size - nbytes); - - if (rc < 0) { - if (errno == EINTR || errno == EAGAIN) - continue; // Retry. - - return -1; // Something really bad happened :-( - } else if (rc == 0) { - break; - } - - nbytes += rc; - } - - return nbytes; -#endif -} - -static int qt_serial_flush(serial_t *device, int queue) -{ - if (device == NULL) - return DC_STATUS_INVALIDARGS; -#if !defined(Q_OS_WIN) - if (device->socket == NULL) - return DC_STATUS_INVALIDARGS; -#endif - // TODO: add implementation - - return DC_STATUS_SUCCESS; -} - -static int qt_serial_get_received(serial_t *device) -{ -#if defined(Q_OS_WIN) - if (device == NULL) - return DC_STATUS_INVALIDARGS; - - // TODO use WSAIoctl to get the information - - return 0; -#else - if (device == NULL || device->socket == NULL) - return DC_STATUS_INVALIDARGS; - - return device->socket->bytesAvailable(); -#endif -} - -static int qt_serial_get_transmitted(serial_t *device) -{ -#if defined(Q_OS_WIN) - if (device == NULL) - return DC_STATUS_INVALIDARGS; - - // TODO add implementation - - return 0; -#else - if (device == NULL || device->socket == NULL) - return DC_STATUS_INVALIDARGS; - - return device->socket->bytesToWrite(); -#endif -} - -static int qt_serial_set_timeout(serial_t *device, long timeout) -{ - if (device == NULL) - return DC_STATUS_INVALIDARGS; - - device->timeout = timeout; - - return DC_STATUS_SUCCESS; -} - - -const dc_serial_operations_t qt_serial_ops = { - .open = qt_serial_open, - .close = qt_serial_close, - .read = qt_serial_read, - .write = qt_serial_write, - .flush = qt_serial_flush, - .get_received = qt_serial_get_received, - .get_transmitted = qt_serial_get_transmitted, - .set_timeout = qt_serial_set_timeout -}; - -extern void dc_serial_init (dc_serial_t *serial, void *data, const dc_serial_operations_t *ops); - -dc_status_t dc_serial_qt_open(dc_serial_t **out, dc_context_t *context, const char *devaddr) -{ - if (out == NULL) - return DC_STATUS_INVALIDARGS; - - // Allocate memory. - dc_serial_t *serial_device = (dc_serial_t *) malloc (sizeof (dc_serial_t)); - - if (serial_device == NULL) { - return DC_STATUS_NOMEMORY; - } - - // Initialize data and function pointers - dc_serial_init(serial_device, NULL, &qt_serial_ops); - - // Open the serial device. - dc_status_t rc = (dc_status_t)qt_serial_open (&serial_device->port, context, devaddr); - if (rc != DC_STATUS_SUCCESS) { - free (serial_device); - return rc; - } - - // Set the type of the device - serial_device->type = DC_TRANSPORT_BLUETOOTH; - - *out = serial_device; - - return DC_STATUS_SUCCESS; -} -} -#endif diff --git a/save-git.c b/save-git.c deleted file mode 100644 index 69ad0726d..000000000 --- a/save-git.c +++ /dev/null @@ -1,1235 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dive.h" -#include "divelist.h" -#include "device.h" -#include "membuffer.h" -#include "git-access.h" -#include "version.h" -#include "qthelperfromc.h" - -/* - * handle libgit2 revision 0.20 and earlier - */ -#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR <= 20 && !defined(USE_LIBGIT21_API) - #define GIT_CHECKOUT_OPTIONS_INIT GIT_CHECKOUT_OPTS_INIT - #define git_checkout_options git_checkout_opts - #define git_branch_create(out,repo,branch_name,target,force,sig,msg) \ - git_branch_create(out,repo,branch_name,target,force) - #define git_reference_set_target(out,ref,target,signature,log_message) \ - git_reference_set_target(out,ref,target) -#endif -/* - * api break in libgit2 revision 0.22 - */ -#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR < 22 - #define git_treebuilder_new(out, repo, source) git_treebuilder_create(out, source) -#else - #define git_treebuilder_write(id, repo, bld) git_treebuilder_write(id, bld) -#endif -/* - * api break introduced in libgit2 master after 0.22 - let's guess this is the v0.23 API - */ -#if USE_LIBGIT23_API || (!LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR >= 23) - #define git_branch_create(out, repo, branch_name, target, force, signature, log_message) \ - git_branch_create(out, repo, branch_name, target, force) - #define git_reference_set_target(out, ref, id, author, log_message) \ - git_reference_set_target(out, ref, id, log_message) -#endif - -#define VA_BUF(b, fmt) do { va_list args; va_start(args, fmt); put_vformat(b, fmt, args); va_end(args); } while (0) - -static void cond_put_format(int cond, struct membuffer *b, const char *fmt, ...) -{ - if (cond) { - VA_BUF(b, fmt); - } -} - -#define SAVE(str, x) cond_put_format(dive->x, b, str " %d\n", dive->x) - -static void show_gps(struct membuffer *b, degrees_t latitude, degrees_t longitude) -{ - if (latitude.udeg || longitude.udeg) { - put_degrees(b, latitude, "gps ", " "); - put_degrees(b, longitude, "", "\n"); - } -} - -static void quote(struct membuffer *b, const char *text) -{ - const char *p = text; - - for (;;) { - const char *escape; - - switch (*p++) { - default: - continue; - case 0: - escape = NULL; - break; - case 1 ... 8: - case 11: - case 12: - case 14 ... 31: - escape = "?"; - break; - case '\\': - escape = "\\\\"; - break; - case '"': - escape = "\\\""; - break; - case '\n': - escape = "\n\t"; - if (*p == '\n') - escape = "\n"; - break; - } - put_bytes(b, text, (p - text - 1)); - if (!escape) - break; - put_string(b, escape); - text = p; - } -} - -static void show_utf8(struct membuffer *b, const char *prefix, const char *value, const char *postfix) -{ - if (value) { - put_format(b, "%s\"", prefix); - quote(b, value); - put_format(b, "\"%s", postfix); - } -} - -static void save_overview(struct membuffer *b, struct dive *dive) -{ - show_utf8(b, "divemaster ", dive->divemaster, "\n"); - show_utf8(b, "buddy ", dive->buddy, "\n"); - show_utf8(b, "suit ", dive->suit, "\n"); - show_utf8(b, "notes ", dive->notes, "\n"); -} - -static void save_tags(struct membuffer *b, struct tag_entry *tags) -{ - const char *sep = " "; - - if (!tags) - return; - put_string(b, "tags"); - while (tags) { - show_utf8(b, sep, tags->tag->source ? : tags->tag->name, ""); - sep = ", "; - tags = tags->next; - } - put_string(b, "\n"); -} - -static void save_extra_data(struct membuffer *b, struct extra_data *ed) -{ - while (ed) { - if (ed->key && ed->value) - put_format(b, "keyvalue \"%s\" \"%s\"\n", ed->key ? : "", ed->value ? : ""); - ed = ed->next; - } -} - -static void put_gasmix(struct membuffer *b, struct gasmix *mix) -{ - int o2 = mix->o2.permille; - int he = mix->he.permille; - - if (o2) { - put_format(b, " o2=%u.%u%%", FRACTION(o2, 10)); - if (he) - put_format(b, " he=%u.%u%%", FRACTION(he, 10)); - } -} - -static void save_cylinder_info(struct membuffer *b, struct dive *dive) -{ - int i, nr; - - nr = nr_cylinders(dive); - for (i = 0; i < nr; i++) { - cylinder_t *cylinder = dive->cylinder + i; - int volume = cylinder->type.size.mliter; - const char *description = cylinder->type.description; - - put_string(b, "cylinder"); - if (volume) - put_milli(b, " vol=", volume, "l"); - put_pressure(b, cylinder->type.workingpressure, " workpressure=", "bar"); - show_utf8(b, " description=", description, ""); - strip_mb(b); - put_gasmix(b, &cylinder->gasmix); - put_pressure(b, cylinder->start, " start=", "bar"); - put_pressure(b, cylinder->end, " end=", "bar"); - if (cylinder->cylinder_use != OC_GAS) - put_format(b, " use=%s", cylinderuse_text[cylinder->cylinder_use]); - - put_string(b, "\n"); - } -} - -static void save_weightsystem_info(struct membuffer *b, struct dive *dive) -{ - int i, nr; - - nr = nr_weightsystems(dive); - for (i = 0; i < nr; i++) { - weightsystem_t *ws = dive->weightsystem + i; - int grams = ws->weight.grams; - const char *description = ws->description; - - put_string(b, "weightsystem"); - put_milli(b, " weight=", grams, "kg"); - show_utf8(b, " description=", description, ""); - put_string(b, "\n"); - } -} - -static void save_dive_temperature(struct membuffer *b, struct dive *dive) -{ - if (dive->airtemp.mkelvin != dc_airtemp(&dive->dc)) - put_temperature(b, dive->airtemp, "airtemp ", "°C\n"); - if (dive->watertemp.mkelvin != dc_watertemp(&dive->dc)) - put_temperature(b, dive->watertemp, "watertemp ", "°C\n"); -} - -static void save_depths(struct membuffer *b, struct divecomputer *dc) -{ - put_depth(b, dc->maxdepth, "maxdepth ", "m\n"); - put_depth(b, dc->meandepth, "meandepth ", "m\n"); -} - -static void save_temperatures(struct membuffer *b, struct divecomputer *dc) -{ - put_temperature(b, dc->airtemp, "airtemp ", "°C\n"); - put_temperature(b, dc->watertemp, "watertemp ", "°C\n"); -} - -static void save_airpressure(struct membuffer *b, struct divecomputer *dc) -{ - put_pressure(b, dc->surface_pressure, "surfacepressure ", "bar\n"); -} - -static void save_salinity(struct membuffer *b, struct divecomputer *dc) -{ - /* only save if we have a value that isn't the default of sea water */ - if (!dc->salinity || dc->salinity == SEAWATER_SALINITY) - return; - put_salinity(b, dc->salinity, "salinity ", "g/l\n"); -} - -static void show_date(struct membuffer *b, timestamp_t when) -{ - struct tm tm; - - utc_mkdate(when, &tm); - - put_format(b, "date %04u-%02u-%02u\n", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); - put_format(b, "time %02u:%02u:%02u\n", - tm.tm_hour, tm.tm_min, tm.tm_sec); -} - -static void show_index(struct membuffer *b, int value, const char *pre, const char *post) -{ - if (value) - put_format(b, " %s%d%s", pre, value, post); -} - -/* - * Samples are saved as densely as possible while still being readable, - * since they are the bulk of the data. - * - * For parsing, look at the units to figure out what the numbers are. - */ -static void save_sample(struct membuffer *b, struct sample *sample, struct sample *old) -{ - put_format(b, "%3u:%02u", FRACTION(sample->time.seconds, 60)); - put_milli(b, " ", sample->depth.mm, "m"); - put_temperature(b, sample->temperature, " ", "°C"); - put_pressure(b, sample->cylinderpressure, " ", "bar"); - put_pressure(b, sample->o2cylinderpressure," o2pressure=","bar"); - - /* - * We only show sensor information for samples with pressure, and only if it - * changed from the previous sensor we showed. - */ - if (sample->cylinderpressure.mbar && sample->sensor != old->sensor) { - put_format(b, " sensor=%d", sample->sensor); - old->sensor = sample->sensor; - } - - /* the deco/ndl values are stored whenever they change */ - if (sample->ndl.seconds != old->ndl.seconds) { - put_format(b, " ndl=%u:%02u", FRACTION(sample->ndl.seconds, 60)); - old->ndl = sample->ndl; - } - if (sample->tts.seconds != old->tts.seconds) { - put_format(b, " tts=%u:%02u", FRACTION(sample->tts.seconds, 60)); - old->tts = sample->tts; - } - if (sample->in_deco != old->in_deco) { - put_format(b, " in_deco=%d", sample->in_deco ? 1 : 0); - old->in_deco = sample->in_deco; - } - if (sample->stoptime.seconds != old->stoptime.seconds) { - put_format(b, " stoptime=%u:%02u", FRACTION(sample->stoptime.seconds, 60)); - old->stoptime = sample->stoptime; - } - - if (sample->stopdepth.mm != old->stopdepth.mm) { - put_milli(b, " stopdepth=", sample->stopdepth.mm, "m"); - old->stopdepth = sample->stopdepth; - } - - if (sample->cns != old->cns) { - put_format(b, " cns=%u%%", sample->cns); - old->cns = sample->cns; - } - - if (sample->rbt.seconds) - put_format(b, " rbt=%u:%02u", FRACTION(sample->rbt.seconds, 60)); - - if (sample->o2sensor[0].mbar != old->o2sensor[0].mbar) { - put_milli(b, " sensor1=", sample->o2sensor[0].mbar, "bar"); - old->o2sensor[0] = sample->o2sensor[0]; - } - - if ((sample->o2sensor[1].mbar) && (sample->o2sensor[1].mbar != old->o2sensor[1].mbar)) { - put_milli(b, " sensor2=", sample->o2sensor[1].mbar, "bar"); - old->o2sensor[1] = sample->o2sensor[1]; - } - - if ((sample->o2sensor[2].mbar) && (sample->o2sensor[2].mbar != old->o2sensor[2].mbar)) { - put_milli(b, " sensor3=", sample->o2sensor[2].mbar, "bar"); - old->o2sensor[2] = sample->o2sensor[2]; - } - - if (sample->setpoint.mbar != old->setpoint.mbar) { - put_milli(b, " po2=", sample->setpoint.mbar, "bar"); - old->setpoint = sample->setpoint; - } - show_index(b, sample->heartbeat, "heartbeat=", ""); - show_index(b, sample->bearing.degrees, "bearing=", "°"); - put_format(b, "\n"); -} - -static void save_samples(struct membuffer *b, int nr, struct sample *s) -{ - struct sample dummy = {}; - - while (--nr >= 0) { - save_sample(b, s, &dummy); - s++; - } -} - -static void save_one_event(struct membuffer *b, struct event *ev) -{ - put_format(b, "event %d:%02d", FRACTION(ev->time.seconds, 60)); - show_index(b, ev->type, "type=", ""); - show_index(b, ev->flags, "flags=", ""); - show_index(b, ev->value, "value=", ""); - show_utf8(b, " name=", ev->name, ""); - if (event_is_gaschange(ev)) { - if (ev->gas.index >= 0) { - show_index(b, ev->gas.index, "cylinder=", ""); - put_gasmix(b, &ev->gas.mix); - } else if (!event_gasmix_redundant(ev)) - put_gasmix(b, &ev->gas.mix); - } - put_string(b, "\n"); -} - -static void save_events(struct membuffer *b, struct event *ev) -{ - while (ev) { - save_one_event(b, ev); - ev = ev->next; - } -} - -static void save_dc(struct membuffer *b, struct dive *dive, struct divecomputer *dc) -{ - show_utf8(b, "model ", dc->model, "\n"); - if (dc->deviceid) - put_format(b, "deviceid %08x\n", dc->deviceid); - if (dc->diveid) - put_format(b, "diveid %08x\n", dc->diveid); - if (dc->when && dc->when != dive->when) - show_date(b, dc->when); - if (dc->duration.seconds && dc->duration.seconds != dive->dc.duration.seconds) - put_duration(b, dc->duration, "duration ", "min\n"); - if (dc->divemode != OC) { - put_format(b, "dctype %s\n", divemode_text[dc->divemode]); - put_format(b, "numberofoxygensensors %d\n",dc->no_o2sensors); - } - - save_depths(b, dc); - save_temperatures(b, dc); - save_airpressure(b, dc); - save_salinity(b, dc); - put_duration(b, dc->surfacetime, "surfacetime ", "min\n"); - - save_extra_data(b, dc->extra_data); - save_events(b, dc->events); - save_samples(b, dc->samples, dc->sample); -} - -/* - * Note that we don't save the date and time or dive - * number: they are encoded in the filename. - */ -static void create_dive_buffer(struct dive *dive, struct membuffer *b) -{ - put_format(b, "duration %u:%02u min\n", FRACTION(dive->dc.duration.seconds, 60)); - SAVE("rating", rating); - SAVE("visibility", visibility); - cond_put_format(dive->tripflag == NO_TRIP, b, "notrip\n"); - save_tags(b, dive->tag_list); - cond_put_format(dive->dive_site_uuid && get_dive_site_by_uuid(dive->dive_site_uuid), - b, "divesiteid %08x\n", dive->dive_site_uuid); - if (verbose && dive->dive_site_uuid && !get_dive_site_by_uuid(dive->dive_site_uuid)) - fprintf(stderr, "removed reference to non-existant dive site with uuid %08x\n", dive->dive_site_uuid); - save_overview(b, dive); - save_cylinder_info(b, dive); - save_weightsystem_info(b, dive); - save_dive_temperature(b, dive); -} - -static struct membuffer error_string_buffer = { 0 }; - -/* - * Note that the act of "getting" the error string - * buffer doesn't de-allocate the buffer, but it does - * set the buffer length to zero, so that any future - * error reports will overwrite the string rather than - * append to it. - */ -const char *get_error_string(void) -{ - const char *str; - - if (!error_string_buffer.len) - return ""; - str = mb_cstring(&error_string_buffer); - error_string_buffer.len = 0; - return str; -} - -int report_error(const char *fmt, ...) -{ - struct membuffer *buf = &error_string_buffer; - - /* Previous unprinted errors? Add a newline in between */ - if (buf->len) - put_bytes(buf, "\n", 1); - VA_BUF(buf, fmt); - mb_cstring(buf); - return -1; -} - -/* - * libgit2 has a "git_treebuilder" concept, but it's broken, and can not - * be used to do a flat tree (like the git "index") nor a recursive tree. - * Stupid. - * - * So we have to do that "keep track of recursive treebuilder entries" - * ourselves. We use 'git_treebuilder' for any regular files, and our own - * data structures for recursive trees. - * - * When finally writing it out, we traverse the subdirectories depth- - * first, writing them out, and then adding the written-out trees to - * the git_treebuilder they existed in. - */ -struct dir { - git_treebuilder *files; - struct dir *subdirs, *sibling; - char unique, name[1]; -}; - -static int tree_insert(git_treebuilder *dir, const char *name, int mkunique, git_oid *id, unsigned mode) -{ - int ret; - struct membuffer uniquename = { 0 }; - - if (mkunique && git_treebuilder_get(dir, name)) { - char hex[8]; - git_oid_nfmt(hex, 7, id); - hex[7] = 0; - put_format(&uniquename, "%s~%s", name, hex); - name = mb_cstring(&uniquename); - } - ret = git_treebuilder_insert(NULL, dir, name, id, mode); - free_buffer(&uniquename); - return ret; -} - -/* - * This does *not* make sure the new subdirectory doesn't - * alias some existing name. That is actually useful: you - * can create multiple directories with the same name, and - * set the "unique" flag, which will then append the SHA1 - * of the directory to the name when it is written. - */ -static struct dir *new_directory(git_repository *repo, struct dir *parent, struct membuffer *namebuf) -{ - struct dir *subdir; - const char *name = mb_cstring(namebuf); - int len = namebuf->len; - - subdir = malloc(sizeof(*subdir)+len); - - /* - * It starts out empty: no subdirectories of its own, - * and an empty treebuilder list of files. - */ - subdir->subdirs = NULL; - git_treebuilder_new(&subdir->files, repo, NULL); - memcpy(subdir->name, name, len); - subdir->unique = 0; - subdir->name[len] = 0; - - /* Add it to the list of subdirs of the parent */ - subdir->sibling = parent->subdirs; - parent->subdirs = subdir; - - return subdir; -} - -static struct dir *mktree(git_repository *repo, struct dir *dir, const char *fmt, ...) -{ - struct membuffer buf = { 0 }; - struct dir *subdir; - - VA_BUF(&buf, fmt); - for (subdir = dir->subdirs; subdir; subdir = subdir->sibling) { - if (subdir->unique) - continue; - if (strncmp(subdir->name, buf.buffer, buf.len)) - continue; - if (!subdir->name[buf.len]) - break; - } - if (!subdir) - subdir = new_directory(repo, dir, &buf); - free_buffer(&buf); - return subdir; -} - -/* - * The name of a dive is the date and the dive number (and possibly - * the uniqueness suffix). - * - * Note that the time of the dive may not be the same as the - * time of the directory structure it is created in: the dive - * might be part of a trip that straddles a month (or even a - * year). - * - * We do *not* want to use localized weekdays and cause peoples save - * formats to depend on their locale. - */ -static void create_dive_name(struct dive *dive, struct membuffer *name, struct tm *dirtm) -{ - struct tm tm; - static const char weekday[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; - - utc_mkdate(dive->when, &tm); - if (tm.tm_year != dirtm->tm_year) - put_format(name, "%04u-", tm.tm_year + 1900); - if (tm.tm_mon != dirtm->tm_mon) - put_format(name, "%02u-", tm.tm_mon+1); - - /* a colon is an illegal char in a file name on Windows - use an '=' instead */ - put_format(name, "%02u-%s-%02u=%02u=%02u", - tm.tm_mday, weekday[tm.tm_wday], - tm.tm_hour, tm.tm_min, tm.tm_sec); -} - -/* Write file at filepath to the git repo with given filename */ -static int blob_insert_fromdisk(git_repository *repo, struct dir *tree, const char *filepath, const char *filename) -{ - int ret; - git_oid blob_id; - - ret = git_blob_create_fromdisk(&blob_id, repo, filepath); - if (ret) - return ret; - return tree_insert(tree->files, filename, 1, &blob_id, GIT_FILEMODE_BLOB); -} - -/* - * Write a membuffer to the git repo, and free it - */ -static int blob_insert(git_repository *repo, struct dir *tree, struct membuffer *b, const char *fmt, ...) -{ - int ret; - git_oid blob_id; - struct membuffer name = { 0 }; - - ret = git_blob_create_frombuffer(&blob_id, repo, b->buffer, b->len); - free_buffer(b); - if (ret) - return ret; - - VA_BUF(&name, fmt); - ret = tree_insert(tree->files, mb_cstring(&name), 1, &blob_id, GIT_FILEMODE_BLOB); - free_buffer(&name); - return ret; -} - -static int save_one_divecomputer(git_repository *repo, struct dir *tree, struct dive *dive, struct divecomputer *dc, int idx) -{ - int ret; - struct membuffer buf = { 0 }; - - save_dc(&buf, dive, dc); - ret = blob_insert(repo, tree, &buf, "Divecomputer%c%03u", idx ? '-' : 0, idx); - if (ret) - report_error("divecomputer tree insert failed"); - return ret; -} - -static int save_one_picture(git_repository *repo, struct dir *dir, struct picture *pic) -{ - int offset = pic->offset.seconds; - struct membuffer buf = { 0 }; - char sign = '+'; - unsigned h; - int error; - - show_utf8(&buf, "filename ", pic->filename, "\n"); - show_gps(&buf, pic->latitude, pic->longitude); - show_utf8(&buf, "hash ", pic->hash, "\n"); - - /* Picture loading will load even negative offsets.. */ - if (offset < 0) { - offset = -offset; - sign = '-'; - } - - /* Use full hh:mm:ss format to make it all sort nicely */ - h = offset / 3600; - offset -= h *3600; - error = blob_insert(repo, dir, &buf, "%c%02u=%02u=%02u", - sign, h, FRACTION(offset, 60)); - if (!error) { - /* next store the actual picture; we prefix all picture names - * with "PIC-" to make things easier on the parsing side */ - struct membuffer namebuf = { 0 }; - const char *localfn = local_file_path(pic); - put_format(&namebuf, "PIC-%s", pic->hash); - error = blob_insert_fromdisk(repo, dir, localfn, mb_cstring(&namebuf)); - free((void *)localfn); - } - return error; -} - -static int save_pictures(git_repository *repo, struct dir *dir, struct dive *dive) -{ - if (dive->picture_list) { - dir = mktree(repo, dir, "Pictures"); - FOR_EACH_PICTURE(dive) { - save_one_picture(repo, dir, picture); - } - } - return 0; -} - -static int save_one_dive(git_repository *repo, struct dir *tree, struct dive *dive, struct tm *tm) -{ - struct divecomputer *dc; - struct membuffer buf = { 0 }, name = { 0 }; - struct dir *subdir; - int ret, nr; - - /* Create dive directory */ - create_dive_name(dive, &name, tm); - subdir = new_directory(repo, tree, &name); - subdir->unique = 1; - free_buffer(&name); - - create_dive_buffer(dive, &buf); - nr = dive->number; - ret = blob_insert(repo, subdir, &buf, - "Dive%c%d", nr ? '-' : 0, nr); - if (ret) - return report_error("dive save-file tree insert failed"); - - /* - * Save the dive computer data. If there is only one dive - * computer, use index 0 for that (which disables the index - * generation when naming it). - */ - dc = &dive->dc; - nr = dc->next ? 1 : 0; - do { - save_one_divecomputer(repo, subdir, dive, dc, nr++); - dc = dc->next; - } while (dc); - - /* Save the picture data, if any */ - save_pictures(repo, subdir, dive); - return 0; -} - -/* - * We'll mark the trip directories unique, so this does not - * need to be unique per se. It could be just "trip". But - * to make things a bit more user-friendly, we try to take - * the trip location into account. - * - * But no special characters, and no numbers (numbers in the - * name could be construed as a date). - * - * So we might end up with "02-Maui", and then the unique - * flag will make us write it out as "02-Maui~54b4" or - * similar. - */ -#define MAXTRIPNAME 15 -static void create_trip_name(dive_trip_t *trip, struct membuffer *name, struct tm *tm) -{ - put_format(name, "%02u-", tm->tm_mday); - if (trip->location) { - char ascii_loc[MAXTRIPNAME+1], *p = trip->location; - int i; - - for (i = 0; i < MAXTRIPNAME; ) { - char c = *p++; - switch (c) { - case 0: - case ',': - case '.': - break; - - case 'a' ... 'z': - case 'A' ... 'Z': - ascii_loc[i++] = c; - continue; - default: - continue; - } - break; - } - if (i > 1) { - put_bytes(name, ascii_loc, i); - return; - } - } - - /* No useful name? */ - put_string(name, "trip"); -} - -static int save_trip_description(git_repository *repo, struct dir *dir, dive_trip_t *trip, struct tm *tm) -{ - int ret; - git_oid blob_id; - struct membuffer desc = { 0 }; - - put_format(&desc, "date %04u-%02u-%02u\n", - tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); - put_format(&desc, "time %02u:%02u:%02u\n", - tm->tm_hour, tm->tm_min, tm->tm_sec); - - show_utf8(&desc, "location ", trip->location, "\n"); - show_utf8(&desc, "notes ", trip->notes, "\n"); - - ret = git_blob_create_frombuffer(&blob_id, repo, desc.buffer, desc.len); - free_buffer(&desc); - if (ret) - return report_error("trip blob creation failed"); - ret = tree_insert(dir->files, "00-Trip", 0, &blob_id, GIT_FILEMODE_BLOB); - if (ret) - return report_error("trip description tree insert failed"); - return 0; -} - -static void verify_shared_date(timestamp_t when, struct tm *tm) -{ - struct tm tmp_tm; - - utc_mkdate(when, &tmp_tm); - if (tmp_tm.tm_year != tm->tm_year) { - tm->tm_year = -1; - tm->tm_mon = -1; - } - if (tmp_tm.tm_mon != tm->tm_mon) - tm->tm_mon = -1; -} - -#define MIN_TIMESTAMP (0) -#define MAX_TIMESTAMP (0x7fffffffffffffff) - -static int save_one_trip(git_repository *repo, struct dir *tree, dive_trip_t *trip, struct tm *tm) -{ - int i; - struct dive *dive; - struct dir *subdir; - struct membuffer name = { 0 }; - timestamp_t first, last; - - /* Create trip directory */ - create_trip_name(trip, &name, tm); - subdir = new_directory(repo, tree, &name); - subdir->unique = 1; - free_buffer(&name); - - /* Trip description file */ - save_trip_description(repo, subdir, trip, tm); - - /* Make sure we write out the dates to the dives consistently */ - first = MAX_TIMESTAMP; - last = MIN_TIMESTAMP; - for_each_dive(i, dive) { - if (dive->divetrip != trip) - continue; - if (dive->when < first) - first = dive->when; - if (dive->when > last) - last = dive->when; - } - verify_shared_date(first, tm); - verify_shared_date(last, tm); - - /* Save each dive in the directory */ - for_each_dive(i, dive) { - if (dive->divetrip == trip) - save_one_dive(repo, subdir, dive, tm); - } - - return 0; -} - -static void save_units(void *_b) -{ - struct membuffer *b =_b; - if (prefs.unit_system == METRIC) - put_string(b, "units METRIC\n"); - else if (prefs.unit_system == IMPERIAL) - put_string(b, "units IMPERIAL\n"); - else - put_format(b, "units PERSONALIZE %s %s %s %s %s %s", - prefs.units.length == METERS ? "METERS" : "FEET", - prefs.units.volume == LITER ? "LITER" : "CUFT", - prefs.units.pressure == BAR ? "BAR" : prefs.units.pressure == PSI ? "PSI" : "PASCAL", - prefs.units.temperature == CELSIUS ? "CELSIUS" : prefs.units.temperature == FAHRENHEIT ? "FAHRENHEIT" : "KELVIN", - prefs.units.weight == KG ? "KG" : "LBS", - prefs.units.vertical_speed_time == SECONDS ? "SECONDS" : "MINUTES"); -} - -static void save_userid(void *_b) -{ - struct membuffer *b = _b; - if (prefs.save_userid_local) - put_format(b, "userid %30s\n", prefs.userid); -} - -static void save_one_device(void *_b, const char *model, uint32_t deviceid, - const char *nickname, const char *serial, const char *firmware) -{ - struct membuffer *b = _b; - - if (nickname && !strcmp(model, nickname)) - nickname = NULL; - if (serial && !*serial) serial = NULL; - if (firmware && !*firmware) firmware = NULL; - if (nickname && !*nickname) nickname = NULL; - if (!nickname && !serial && !firmware) - return; - - show_utf8(b, "divecomputerid ", model, ""); - put_format(b, " deviceid=%08x", deviceid); - show_utf8(b, " serial=", serial, ""); - show_utf8(b, " firmware=", firmware, ""); - show_utf8(b, " nickname=", nickname, ""); - put_string(b, "\n"); -} - -static void save_settings(git_repository *repo, struct dir *tree) -{ - struct membuffer b = { 0 }; - - put_format(&b, "version %d\n", DATAFORMAT_VERSION); - save_userid(&b); - call_for_each_dc(&b, save_one_device, false); - cond_put_format(autogroup, &b, "autogroup\n"); - save_units(&b); - - blob_insert(repo, tree, &b, "00-Subsurface"); -} - -static void save_divesites(git_repository *repo, struct dir *tree) -{ - struct dir *subdir; - struct membuffer dirname = { 0 }; - put_format(&dirname, "01-Divesites"); - subdir = new_directory(repo, tree, &dirname); - - for (int i = 0; i < dive_site_table.nr; i++) { - struct membuffer b = { 0 }; - struct dive_site *ds = get_dive_site(i); - if (dive_site_is_empty(ds)) { - int j; - struct dive *d; - for_each_dive(j, d) { - if (d->dive_site_uuid == ds->uuid) - d->dive_site_uuid = 0; - } - delete_dive_site(ds->uuid); - i--; // since we just deleted that one - continue; - } else if (ds->name && - (strncmp(ds->name, "Auto-created dive", 17) == 0 || - strncmp(ds->name, "New Dive", 8) == 0)) { - fprintf(stderr, "found an auto divesite %s\n", ds->name); - // these are the two default names for sites from - // the web service; if the site isn't used in any - // dive (really? you didn't rename it?), delete it - if (!is_dive_site_used(ds->uuid, false)) { - if (verbose) - fprintf(stderr, "Deleted unused auto-created dive site %s\n", ds->name); - delete_dive_site(ds->uuid); - i--; // since we just deleted that one - continue; - } - } - struct membuffer site_file_name = { 0 }; - put_format(&site_file_name, "Site-%08x", ds->uuid); - show_utf8(&b, "name ", ds->name, "\n"); - show_utf8(&b, "description ", ds->description, "\n"); - show_utf8(&b, "notes ", ds->notes, "\n"); - show_gps(&b, ds->latitude, ds->longitude); - for (int j = 0; j < ds->taxonomy.nr; j++) { - struct taxonomy *t = &ds->taxonomy.category[j]; - if (t->category != TC_NONE && t->value) { - put_format(&b, "geo cat %d origin %d ", t->category, t->origin); - show_utf8(&b, "", t->value, "\n" ); - } - } - blob_insert(repo, subdir, &b, mb_cstring(&site_file_name)); - } -} - -static int create_git_tree(git_repository *repo, struct dir *root, bool select_only) -{ - int i; - struct dive *dive; - dive_trip_t *trip; - - save_settings(repo, root); - - save_divesites(repo, root); - - for (trip = dive_trip_list; trip != NULL; trip = trip->next) - trip->index = 0; - - /* save the dives */ - for_each_dive(i, dive) { - struct tm tm; - struct dir *tree; - - trip = dive->divetrip; - - if (select_only) { - if (!dive->selected) - continue; - /* We don't save trips when doing selected dive saves */ - trip = NULL; - } - - /* Create the date-based hierarchy */ - utc_mkdate(trip ? trip->when : dive->when, &tm); - tree = mktree(repo, root, "%04d", tm.tm_year + 1900); - tree = mktree(repo, tree, "%02d", tm.tm_mon + 1); - - if (trip) { - /* Did we already save this trip? */ - if (trip->index) - continue; - trip->index = 1; - - /* Pass that new subdirectory in for save-trip */ - save_one_trip(repo, tree, trip, &tm); - continue; - } - - save_one_dive(repo, tree, dive, &tm); - } - return 0; -} - -/* - * See if we can find the parent ID that the git data came from - */ -static git_object *try_to_find_parent(const char *hex_id, git_repository *repo) -{ - git_oid object_id; - git_commit *commit; - - if (!hex_id) - return NULL; - if (git_oid_fromstr(&object_id, hex_id)) - return NULL; - if (git_commit_lookup(&commit, repo, &object_id)) - return NULL; - return (git_object *)commit; -} - -static int notify_cb(git_checkout_notify_t why, - const char *path, - const git_diff_file *baseline, - const git_diff_file *target, - const git_diff_file *workdir, - void *payload) -{ - report_error("File '%s' does not match in working tree", path); - return 0; /* Continue with checkout */ -} - -static git_tree *get_git_tree(git_repository *repo, git_object *parent) -{ - git_tree *tree; - if (!parent) - return NULL; - if (git_tree_lookup(&tree, repo, git_commit_tree_id((const git_commit *) parent))) - return NULL; - return tree; -} - -int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree) -{ - git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; - - opts.checkout_strategy = GIT_CHECKOUT_SAFE; - opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT | GIT_CHECKOUT_NOTIFY_DIRTY; - opts.notify_cb = notify_cb; - opts.baseline = get_git_tree(repo, parent); - return git_checkout_tree(repo, (git_object *) tree, &opts); -} - -static int get_authorship(git_repository *repo, git_signature **authorp) -{ -#if LIBGIT2_VER_MAJOR || LIBGIT2_VER_MINOR >= 20 - if (git_signature_default(authorp, repo) == 0) - return 0; -#endif - /* Default name information, with potential OS overrides */ - struct user_info user = { - .name = "Subsurface", - .email = "subsurace@subsurface-divelog.org" - }; - - subsurface_user_info(&user); - - /* git_signature_default() is too recent */ - return git_signature_now(authorp, user.name, user.email); -} - -static void create_commit_message(struct membuffer *msg) -{ - int nr = dive_table.nr; - struct dive *dive = get_dive(nr-1); - - if (dive) { - dive_trip_t *trip = dive->divetrip; - const char *location = get_dive_location(dive) ? : "no location"; - struct divecomputer *dc = &dive->dc; - const char *sep = "\n"; - - if (dive->number) - nr = dive->number; - - put_format(msg, "dive %d: %s", nr, location); - if (trip && trip->location && *trip->location && strcmp(trip->location, location)) - put_format(msg, " (%s)", trip->location); - put_format(msg, "\n"); - do { - if (dc->model && *dc->model) { - put_format(msg, "%s%s", sep, dc->model); - sep = ", "; - } - } while ((dc = dc->next) != NULL); - put_format(msg, "\n"); - } - put_format(msg, "Created by subsurface %s\n", subsurface_version()); -} - -static int create_new_commit(git_repository *repo, const char *remote, const char *branch, git_oid *tree_id) -{ - int ret; - git_reference *ref; - git_object *parent; - git_oid commit_id; - git_signature *author; - git_commit *commit; - git_tree *tree; - - ret = git_branch_lookup(&ref, repo, branch, GIT_BRANCH_LOCAL); - switch (ret) { - default: - return report_error("Bad branch '%s' (%s)", branch, strerror(errno)); - case GIT_EINVALIDSPEC: - return report_error("Invalid branch name '%s'", branch); - case GIT_ENOTFOUND: /* We'll happily create it */ - ref = NULL; - parent = try_to_find_parent(saved_git_id, repo); - break; - case 0: - if (git_reference_peel(&parent, ref, GIT_OBJ_COMMIT)) - return report_error("Unable to look up parent in branch '%s'", branch); - - if (saved_git_id) { - if (existing_filename) - fprintf(stderr, "existing filename %s\n", existing_filename); - const git_oid *id = git_commit_id((const git_commit *) parent); - /* if we are saving to the same git tree we got this from, let's make - * sure there is no confusion */ - if (same_string(existing_filename, remote) && git_oid_strcmp(id, saved_git_id)) - return report_error("The git branch does not match the git parent of the source"); - } - - /* all good */ - break; - } - - if (git_tree_lookup(&tree, repo, tree_id)) - return report_error("Could not look up newly created tree"); - - if (get_authorship(repo, &author)) - return report_error("No user name configuration in git repo"); - - /* If the parent commit has the same tree ID, do not create a new commit */ - if (parent && git_oid_equal(tree_id, git_commit_tree_id((const git_commit *) parent))) { - /* If the parent already came from the ref, the commit is already there */ - if (ref) - return 0; - /* Else we do want to create the new branch, but with the old commit */ - commit = (git_commit *) parent; - } else { - struct membuffer commit_msg = { 0 }; - - create_commit_message(&commit_msg); - if (git_commit_create_v(&commit_id, repo, NULL, author, author, NULL, mb_cstring(&commit_msg), tree, parent != NULL, parent)) - return report_error("Git commit create failed (%s)", strerror(errno)); - free_buffer(&commit_msg); - - if (git_commit_lookup(&commit, repo, &commit_id)) - return report_error("Could not look up newly created commit"); - } - - if (!ref) { - if (git_branch_create(&ref, repo, branch, commit, 0, author, "Create branch")) - return report_error("Failed to create branch '%s'", branch); - } - /* - * If it's a checked-out branch, try to also update the working - * tree and index. If that fails (dirty working tree or whatever), - * this is not technically a save error (we did save things in - * the object database), but it can cause extreme confusion, so - * warn about it. - */ - if (git_branch_is_head(ref) && !git_repository_is_bare(repo)) { - if (update_git_checkout(repo, parent, tree)) { - const git_error *err = giterr_last(); - const char *errstr = err ? err->message : strerror(errno); - report_error("Git branch '%s' is checked out, but worktree is dirty (%s)", - branch, errstr); - } - } - - if (git_reference_set_target(&ref, ref, &commit_id, author, "Subsurface save event")) - return report_error("Failed to update branch '%s'", branch); - set_git_id(&commit_id); - - git_signature_free(author); - - return 0; -} - -static int write_git_tree(git_repository *repo, struct dir *tree, git_oid *result) -{ - int ret; - struct dir *subdir; - - /* Write out our subdirectories, add them to the treebuilder, and free them */ - while ((subdir = tree->subdirs) != NULL) { - git_oid id; - - if (!write_git_tree(repo, subdir, &id)) - tree_insert(tree->files, subdir->name, subdir->unique, &id, GIT_FILEMODE_TREE); - tree->subdirs = subdir->sibling; - free(subdir); - }; - - /* .. write out the resulting treebuilder */ - ret = git_treebuilder_write(result, repo, tree->files); - - /* .. and free the now useless treebuilder */ - git_treebuilder_free(tree->files); - - return ret; -} - -int do_git_save(git_repository *repo, const char *branch, const char *remote, bool select_only, bool create_empty) -{ - struct dir tree; - git_oid id; - - if (verbose) - fprintf(stderr, "git storage: do git save\n"); - - /* Start with an empty tree: no subdirectories, no files */ - tree.name[0] = 0; - tree.subdirs = NULL; - if (git_treebuilder_new(&tree.files, repo, NULL)) - return report_error("git treebuilder failed"); - - if (!create_empty) - /* Populate our tree data structure */ - if (create_git_tree(repo, &tree, select_only)) - return -1; - - if (write_git_tree(repo, &tree, &id)) - return report_error("git tree write failed"); - - /* And save the tree! */ - if (create_new_commit(repo, remote, branch, &id)) - return report_error("creating commit failed"); - - if (remote && prefs.cloud_background_sync) { - /* now sync the tree with the cloud server */ - if (strstr(remote, prefs.cloud_git_url)) { - return sync_with_remote(repo, remote, branch, RT_HTTPS); - } - } - return 0; -} - -int git_save_dives(struct git_repository *repo, const char *branch, const char *remote, bool select_only) -{ - int ret; - - if (repo == dummy_git_repository) - return report_error("Unable to open git repository '%s'", branch); - ret = do_git_save(repo, branch, remote, select_only, false); - git_repository_free(repo); - free((void *)branch); - return ret; -} diff --git a/save-html.c b/save-html.c deleted file mode 100644 index 64ce94f66..000000000 --- a/save-html.c +++ /dev/null @@ -1,533 +0,0 @@ -#include "save-html.h" -#include "qthelperfromc.h" -#include "gettext.h" -#include "stdio.h" - -void write_attribute(struct membuffer *b, const char *att_name, const char *value, const char *separator) -{ - if (!value) - value = "--"; - put_format(b, "\"%s\":\"", att_name); - put_HTML_quoted(b, value); - put_format(b, "\"%s", separator); -} - -void save_photos(struct membuffer *b, const char *photos_dir, struct dive *dive) -{ - struct picture *pic = dive->picture_list; - - if (!pic) - return; - - char *separator = "\"photos\":["; - do { - put_string(b, separator); - separator = ", "; - char *fname = get_file_name(local_file_path(pic)); - put_format(b, "{\"filename\":\"%s\"}", fname); - copy_image_and_overwrite(local_file_path(pic), photos_dir, fname); - free(fname); - pic = pic->next; - } while (pic); - put_string(b, "],"); -} - -void write_divecomputers(struct membuffer *b, struct dive *dive) -{ - put_string(b, "\"divecomputers\":["); - struct divecomputer *dc; - char *separator = ""; - for_each_dc (dive, dc) { - put_string(b, separator); - separator = ", "; - put_format(b, "{"); - write_attribute(b, "model", dc->model, ", "); - if (dc->deviceid) - put_format(b, "\"deviceid\":\"%08x\", ", dc->deviceid); - else - put_string(b, "\"deviceid\":\"--\", "); - if (dc->diveid) - put_format(b, "\"diveid\":\"%08x\" ", dc->diveid); - else - put_string(b, "\"diveid\":\"--\" "); - put_format(b, "}"); - } - put_string(b, "],"); -} - -void write_dive_status(struct membuffer *b, struct dive *dive) -{ - put_format(b, "\"sac\":\"%d\",", dive->sac); - put_format(b, "\"otu\":\"%d\",", dive->otu); - put_format(b, "\"cns\":\"%d\",", dive->cns); -} - -void put_HTML_bookmarks(struct membuffer *b, struct dive *dive) -{ - struct event *ev = dive->dc.events; - - if (!ev) - return; - - char *separator = "\"events\":["; - do { - put_string(b, separator); - separator = ", "; - put_format(b, "{\"name\":\"%s\",", ev->name); - put_format(b, "\"value\":\"%d\",", ev->value); - put_format(b, "\"type\":\"%d\",", ev->type); - put_format(b, "\"time\":\"%d\"}", ev->time.seconds); - ev = ev->next; - } while (ev); - put_string(b, "],"); -} - -static void put_weightsystem_HTML(struct membuffer *b, struct dive *dive) -{ - int i, nr; - - nr = nr_weightsystems(dive); - - put_string(b, "\"Weights\":["); - - char *separator = ""; - - for (i = 0; i < nr; i++) { - weightsystem_t *ws = dive->weightsystem + i; - int grams = ws->weight.grams; - const char *description = ws->description; - - put_string(b, separator); - separator = ", "; - put_string(b, "{"); - put_HTML_weight_units(b, grams, "\"weight\":\"", "\","); - write_attribute(b, "description", description, " "); - put_string(b, "}"); - } - put_string(b, "],"); -} - -static void put_cylinder_HTML(struct membuffer *b, struct dive *dive) -{ - int i, nr; - char *separator = "\"Cylinders\":["; - nr = nr_cylinders(dive); - - if (!nr) - put_string(b, separator); - - for (i = 0; i < nr; i++) { - cylinder_t *cylinder = dive->cylinder + i; - put_format(b, "%s{", separator); - separator = ", "; - write_attribute(b, "Type", cylinder->type.description, ", "); - if (cylinder->type.size.mliter) { - int volume = cylinder->type.size.mliter; - if (prefs.units.volume == CUFT && cylinder->type.workingpressure.mbar) - volume *= bar_to_atm(cylinder->type.workingpressure.mbar / 1000.0); - put_HTML_volume_units(b, volume, "\"Size\":\"", " \", "); - } else { - write_attribute(b, "Size", "--", ", "); - } - put_HTML_pressure_units(b, cylinder->type.workingpressure, "\"WPressure\":\"", " \", "); - - if (cylinder->start.mbar) { - put_HTML_pressure_units(b, cylinder->start, "\"SPressure\":\"", " \", "); - } else { - write_attribute(b, "SPressure", "--", ", "); - } - - if (cylinder->end.mbar) { - put_HTML_pressure_units(b, cylinder->end, "\"EPressure\":\"", " \", "); - } else { - write_attribute(b, "EPressure", "--", ", "); - } - - if (cylinder->gasmix.o2.permille) { - put_format(b, "\"O2\":\"%u.%u%%\",", FRACTION(cylinder->gasmix.o2.permille, 10)); - put_format(b, "\"He\":\"%u.%u%%\"", FRACTION(cylinder->gasmix.he.permille, 10)); - } else { - write_attribute(b, "O2", "Air", ""); - } - - put_string(b, "}"); - } - - put_string(b, "],"); -} - - -void put_HTML_samples(struct membuffer *b, struct dive *dive) -{ - int i; - put_format(b, "\"maxdepth\":%d,", dive->dc.maxdepth.mm); - put_format(b, "\"duration\":%d,", dive->dc.duration.seconds); - struct sample *s = dive->dc.sample; - - if (!dive->dc.samples) - return; - - char *separator = "\"samples\":["; - for (i = 0; i < dive->dc.samples; i++) { - put_format(b, "%s[%d,%d,%d,%d]", separator, s->time.seconds, s->depth.mm, s->cylinderpressure.mbar, s->temperature.mkelvin); - separator = ", "; - s++; - } - put_string(b, "],"); -} - -void put_HTML_coordinates(struct membuffer *b, struct dive *dive) -{ - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!ds) - return; - degrees_t latitude = ds->latitude; - degrees_t longitude = ds->longitude; - - //don't put coordinates if in (0,0) - if (!latitude.udeg && !longitude.udeg) - return; - - put_string(b, "\"coordinates\":{"); - put_degrees(b, latitude, "\"lat\":\"", "\","); - put_degrees(b, longitude, "\"lon\":\"", "\""); - put_string(b, "},"); -} - -void put_HTML_date(struct membuffer *b, struct dive *dive, const char *pre, const char *post) -{ - struct tm tm; - utc_mkdate(dive->when, &tm); - put_format(b, "%s%04u-%02u-%02u%s", pre, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, post); -} - -void put_HTML_quoted(struct membuffer *b, const char *text) -{ - int is_html = 1, is_attribute = 1; - put_quoted(b, text, is_attribute, is_html); -} - -void put_HTML_notes(struct membuffer *b, struct dive *dive, const char *pre, const char *post) -{ - put_string(b, pre); - if (dive->notes) { - put_HTML_quoted(b, dive->notes); - } else { - put_string(b, "--"); - } - put_string(b, post); -} - -void put_HTML_pressure_units(struct membuffer *b, pressure_t pressure, const char *pre, const char *post) -{ - const char *unit; - double value; - - if (!pressure.mbar) { - put_format(b, "%s%s", pre, post); - return; - } - - value = get_pressure_units(pressure.mbar, &unit); - put_format(b, "%s%.1f %s%s", pre, value, unit, post); -} - -void put_HTML_volume_units(struct membuffer *b, unsigned int ml, const char *pre, const char *post) -{ - const char *unit; - double value; - int frac; - - value = get_volume_units(ml, &frac, &unit); - put_format(b, "%s%.1f %s%s", pre, value, unit, post); -} - -void put_HTML_weight_units(struct membuffer *b, unsigned int grams, const char *pre, const char *post) -{ - const char *unit; - double value; - int frac; - - value = get_weight_units(grams, &frac, &unit); - put_format(b, "%s%.1f %s%s", pre, value, unit, post); -} - -void put_HTML_time(struct membuffer *b, struct dive *dive, const char *pre, const char *post) -{ - struct tm tm; - utc_mkdate(dive->when, &tm); - put_format(b, "%s%02u:%02u:%02u%s", pre, tm.tm_hour, tm.tm_min, tm.tm_sec, post); -} - -void put_HTML_airtemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post) -{ - const char *unit; - double value; - - if (!dive->airtemp.mkelvin) { - put_format(b, "%s--%s", pre, post); - return; - } - value = get_temp_units(dive->airtemp.mkelvin, &unit); - put_format(b, "%s%.1f %s%s", pre, value, unit, post); -} - -void put_HTML_watertemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post) -{ - const char *unit; - double value; - - if (!dive->watertemp.mkelvin) { - put_format(b, "%s--%s", pre, post); - return; - } - value = get_temp_units(dive->watertemp.mkelvin, &unit); - put_format(b, "%s%.1f %s%s", pre, value, unit, post); -} - -void put_HTML_tags(struct membuffer *b, struct dive *dive, const char *pre, const char *post) -{ - put_string(b, pre); - struct tag_entry *tag = dive->tag_list; - - if (!tag) - put_string(b, "[\"--\""); - - char *separator = "["; - while (tag) { - put_format(b, "%s\"", separator); - separator = ", "; - put_HTML_quoted(b, tag->tag->name); - put_string(b, "\""); - tag = tag->next; - } - put_string(b, "]"); - put_string(b, post); -} - -/* if exporting list_only mode, we neglect exporting the samples, bookmarks and cylinders */ -void write_one_dive(struct membuffer *b, struct dive *dive, const char *photos_dir, int *dive_no, const bool list_only) -{ - put_string(b, "{"); - put_format(b, "\"number\":%d,", *dive_no); - put_format(b, "\"subsurface_number\":%d,", dive->number); - put_HTML_date(b, dive, "\"date\":\"", "\","); - put_HTML_time(b, dive, "\"time\":\"", "\","); - write_attribute(b, "location", get_dive_location(dive), ", "); - put_HTML_coordinates(b, dive); - put_format(b, "\"rating\":%d,", dive->rating); - put_format(b, "\"visibility\":%d,", dive->visibility); - put_format(b, "\"dive_duration\":\"%u:%02u min\",", - FRACTION(dive->duration.seconds, 60)); - put_string(b, "\"temperature\":{"); - put_HTML_airtemp(b, dive, "\"air\":\"", "\","); - put_HTML_watertemp(b, dive, "\"water\":\"", "\""); - put_string(b, " },"); - write_attribute(b, "buddy", dive->buddy, ", "); - write_attribute(b, "divemaster", dive->divemaster, ", "); - write_attribute(b, "suit", dive->suit, ", "); - put_HTML_tags(b, dive, "\"tags\":", ","); - if (!list_only) { - put_cylinder_HTML(b, dive); - put_weightsystem_HTML(b, dive); - put_HTML_samples(b, dive); - put_HTML_bookmarks(b, dive); - write_dive_status(b, dive); - if (photos_dir && strcmp(photos_dir, "")) - save_photos(b, photos_dir, dive); - write_divecomputers(b, dive); - } - put_HTML_notes(b, dive, "\"notes\":\"", "\""); - put_string(b, "}\n"); - (*dive_no)++; -} - -void write_no_trip(struct membuffer *b, int *dive_no, bool selected_only, const char *photos_dir, const bool list_only, char *sep) -{ - int i; - struct dive *dive; - char *separator = ""; - bool found_sel_dive = 0; - - for_each_dive (i, dive) { - // write dive if it doesn't belong to any trip and the dive is selected - // or we are in exporting all dives mode. - if (!dive->divetrip && (dive->selected || !selected_only)) { - if (!found_sel_dive) { - put_format(b, "%c{", *sep); - (*sep) = ','; - put_format(b, "\"name\":\"Other\","); - put_format(b, "\"dives\":["); - found_sel_dive = 1; - } - put_string(b, separator); - separator = ", "; - write_one_dive(b, dive, photos_dir, dive_no, list_only); - } - } - if (found_sel_dive) - put_format(b, "]}\n\n"); -} - -void write_trip(struct membuffer *b, dive_trip_t *trip, int *dive_no, bool selected_only, const char *photos_dir, const bool list_only, char *sep) -{ - struct dive *dive; - char *separator = ""; - bool found_sel_dive = 0; - - for (dive = trip->dives; dive != NULL; dive = dive->next) { - if (!dive->selected && selected_only) - continue; - - // save trip if found at least one selected dive. - if (!found_sel_dive) { - found_sel_dive = 1; - put_format(b, "%c {", *sep); - (*sep) = ','; - put_format(b, "\"name\":\"%s\",", trip->location); - put_format(b, "\"dives\":["); - } - put_string(b, separator); - separator = ", "; - write_one_dive(b, dive, photos_dir, dive_no, list_only); - } - - // close the trip object if contain dives. - if (found_sel_dive) - put_format(b, "]}\n\n"); -} - -void write_trips(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only) -{ - int i, dive_no = 0; - struct dive *dive; - dive_trip_t *trip; - char sep_ = ' '; - char *sep = &sep_; - - for (trip = dive_trip_list; trip != NULL; trip = trip->next) - trip->index = 0; - - for_each_dive (i, dive) { - trip = dive->divetrip; - - /*Continue if the dive have no trips or we have seen this trip before*/ - if (!trip || trip->index) - continue; - - /* We haven't seen this trip before - save it and all dives */ - trip->index = 1; - write_trip(b, trip, &dive_no, selected_only, photos_dir, list_only, sep); - } - - /*Save all remaining trips into Others*/ - write_no_trip(b, &dive_no, selected_only, photos_dir, list_only, sep); -} - -void export_list(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only) -{ - put_string(b, "trips=["); - write_trips(b, photos_dir, selected_only, list_only); - put_string(b, "]"); -} - -void export_HTML(const char *file_name, const char *photos_dir, const bool selected_only, const bool list_only) -{ - FILE *f; - - struct membuffer buf = { 0 }; - export_list(&buf, photos_dir, selected_only, list_only); - - f = subsurface_fopen(file_name, "w+"); - if (!f) { - report_error(translate("gettextFromC", "Can't open file %s"), file_name); - } else { - flush_buffer(&buf, f); /*check for writing errors? */ - fclose(f); - } - free_buffer(&buf); -} - -void export_translation(const char *file_name) -{ - FILE *f; - - struct membuffer buf = { 0 }; - struct membuffer *b = &buf; - - //export translated words here - put_format(b, "translate={"); - - //Dive list view - write_attribute(b, "Number", translate("gettextFromC", "Number"), ", "); - write_attribute(b, "Date", translate("gettextFromC", "Date"), ", "); - write_attribute(b, "Time", translate("gettextFromC", "Time"), ", "); - write_attribute(b, "Location", translate("gettextFromC", "Location"), ", "); - write_attribute(b, "Air_Temp", translate("gettextFromC", "Air temp."), ", "); - write_attribute(b, "Water_Temp", translate("gettextFromC", "Water temp."), ", "); - write_attribute(b, "dives", translate("gettextFromC", "Dives"), ", "); - write_attribute(b, "Expand_All", translate("gettextFromC", "Expand all"), ", "); - write_attribute(b, "Collapse_All", translate("gettextFromC", "Collapse all"), ", "); - write_attribute(b, "trips", translate("gettextFromC", "Trips"), ", "); - write_attribute(b, "Statistics", translate("gettextFromC", "Statistics"), ", "); - write_attribute(b, "Advanced_Search", translate("gettextFromC", "Advanced search"), ", "); - - //Dive expanded view - write_attribute(b, "Rating", translate("gettextFromC", "Rating"), ", "); - write_attribute(b, "Visibility", translate("gettextFromC", "Visibility"), ", "); - write_attribute(b, "Duration", translate("gettextFromC", "Duration"), ", "); - write_attribute(b, "DiveMaster", translate("gettextFromC", "Divemaster"), ", "); - write_attribute(b, "Buddy", translate("gettextFromC", "Buddy"), ", "); - write_attribute(b, "Suit", translate("gettextFromC", "Suit"), ", "); - write_attribute(b, "Tags", translate("gettextFromC", "Tags"), ", "); - write_attribute(b, "Notes", translate("gettextFromC", "Notes"), ", "); - write_attribute(b, "Show_more_details", translate("gettextFromC", "Show more details"), ", "); - - //Yearly statistics view - write_attribute(b, "Yearly_statistics", translate("gettextFromC", "Yearly statistics"), ", "); - write_attribute(b, "Year", translate("gettextFromC", "Year"), ", "); - write_attribute(b, "Total_Time", translate("gettextFromC", "Total time"), ", "); - write_attribute(b, "Average_Time", translate("gettextFromC", "Average time"), ", "); - write_attribute(b, "Shortest_Time", translate("gettextFromC", "Shortest time"), ", "); - write_attribute(b, "Longest_Time", translate("gettextFromC", "Longest time"), ", "); - write_attribute(b, "Average_Depth", translate("gettextFromC", "Average depth"), ", "); - write_attribute(b, "Min_Depth", translate("gettextFromC", "Min. depth"), ", "); - write_attribute(b, "Max_Depth", translate("gettextFromC", "Max. depth"), ", "); - write_attribute(b, "Average_SAC", translate("gettextFromC", "Average SAC"), ", "); - write_attribute(b, "Min_SAC", translate("gettextFromC", "Min. SAC"), ", "); - write_attribute(b, "Max_SAC", translate("gettextFromC", "Max. SAC"), ", "); - write_attribute(b, "Average_Temp", translate("gettextFromC", "Average temp."), ", "); - write_attribute(b, "Min_Temp", translate("gettextFromC", "Min. temp."), ", "); - write_attribute(b, "Max_Temp", translate("gettextFromC", "Max. temp."), ", "); - write_attribute(b, "Back_to_List", translate("gettextFromC", "Back to list"), ", "); - - //dive detailed view - write_attribute(b, "Dive_No", translate("gettextFromC", "Dive No."), ", "); - write_attribute(b, "Dive_profile", translate("gettextFromC", "Dive profile"), ", "); - write_attribute(b, "Dive_information", translate("gettextFromC", "Dive information"), ", "); - write_attribute(b, "Dive_equipment", translate("gettextFromC", "Dive equipment"), ", "); - write_attribute(b, "Type", translate("gettextFromC", "Type"), ", "); - write_attribute(b, "Size", translate("gettextFromC", "Size"), ", "); - write_attribute(b, "Work_Pressure", translate("gettextFromC", "Work pressure"), ", "); - write_attribute(b, "Start_Pressure", translate("gettextFromC", "Start pressure"), ", "); - write_attribute(b, "End_Pressure", translate("gettextFromC", "End pressure"), ", "); - write_attribute(b, "Gas", translate("gettextFromC", "Gas"), ", "); - write_attribute(b, "Weight", translate("gettextFromC", "Weight"), ", "); - write_attribute(b, "Type", translate("gettextFromC", "Type"), ", "); - write_attribute(b, "Events", translate("gettextFromC", "Events"), ", "); - write_attribute(b, "Name", translate("gettextFromC", "Name"), ", "); - write_attribute(b, "Value", translate("gettextFromC", "Value"), ", "); - write_attribute(b, "Coordinates", translate("gettextFromC", "Coordinates"), ", "); - write_attribute(b, "Dive_Status", translate("gettextFromC", "Dive status"), " "); - - put_format(b, "}"); - - f = subsurface_fopen(file_name, "w+"); - if (!f) { - report_error(translate("gettextFromC", "Can't open file %s"), file_name); - } else { - flush_buffer(&buf, f); /*check for writing errors? */ - fclose(f); - } - free_buffer(&buf); -} diff --git a/save-html.h b/save-html.h deleted file mode 100644 index 20743e90a..000000000 --- a/save-html.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef HTML_SAVE_H -#define HTML_SAVE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "dive.h" -#include "membuffer.h" - -void put_HTML_date(struct membuffer *b, struct dive *dive, const char *pre, const char *post); -void put_HTML_airtemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post); -void put_HTML_watertemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post); -void put_HTML_time(struct membuffer *b, struct dive *dive, const char *pre, const char *post); -void put_HTML_notes(struct membuffer *b, struct dive *dive, const char *pre, const char *post); -void put_HTML_quoted(struct membuffer *b, const char *text); -void put_HTML_pressure_units(struct membuffer *b, pressure_t pressure, const char *pre, const char *post); -void put_HTML_weight_units(struct membuffer *b, unsigned int grams, const char *pre, const char *post); -void put_HTML_volume_units(struct membuffer *b, unsigned int ml, const char *pre, const char *post); - -void export_HTML(const char *file_name, const char *photos_dir, const bool selected_only, const bool list_only); -void export_list(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only); - -void export_translation(const char *file_name); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/save-xml.c b/save-xml.c deleted file mode 100644 index 166885861..000000000 --- a/save-xml.c +++ /dev/null @@ -1,743 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dive.h" -#include "divelist.h" -#include "device.h" -#include "membuffer.h" -#include "strndup.h" -#include "git-access.h" -#include "qthelperfromc.h" - -/* - * We're outputting utf8 in xml. - * We need to quote the characters <, >, &. - * - * Technically I don't think we'd necessarily need to quote the control - * characters, but at least libxml2 doesn't like them. It doesn't even - * allow them quoted. So we just skip them and replace them with '?'. - * - * If we do this for attributes, we need to quote the quotes we use too. - */ -static void quote(struct membuffer *b, const char *text, int is_attribute) -{ - int is_html = 0; - put_quoted(b, text, is_attribute, is_html); -} - -static void show_utf8(struct membuffer *b, const char *text, const char *pre, const char *post, int is_attribute) -{ - int len; - char *cleaned; - - if (!text) - return; - /* remove leading and trailing space */ - /* We need to combine isascii() with isspace(), - * because we can only trust isspace() with 7-bit ascii, - * on windows for example */ - while (isascii(*text) && isspace(*text)) - text++; - len = strlen(text); - if (!len) - return; - while (len && isascii(text[len - 1]) && isspace(text[len - 1])) - len--; - cleaned = strndup(text, len); - put_string(b, pre); - quote(b, cleaned, is_attribute); - put_string(b, post); - free(cleaned); -} - -static void save_depths(struct membuffer *b, struct divecomputer *dc) -{ - /* What's the point of this dive entry again? */ - if (!dc->maxdepth.mm && !dc->meandepth.mm) - return; - - put_string(b, " maxdepth, " max='", " m'"); - put_depth(b, dc->meandepth, " mean='", " m'"); - put_string(b, " />\n"); -} - -static void save_dive_temperature(struct membuffer *b, struct dive *dive) -{ - if (!dive->airtemp.mkelvin && !dive->watertemp.mkelvin) - return; - if (dive->airtemp.mkelvin == dc_airtemp(&dive->dc) && dive->watertemp.mkelvin == dc_watertemp(&dive->dc)) - return; - - put_string(b, " airtemp.mkelvin != dc_airtemp(&dive->dc)) - put_temperature(b, dive->airtemp, " air='", " C'"); - if (dive->watertemp.mkelvin != dc_watertemp(&dive->dc)) - put_temperature(b, dive->watertemp, " water='", " C'"); - put_string(b, "/>\n"); -} - -static void save_temperatures(struct membuffer *b, struct divecomputer *dc) -{ - if (!dc->airtemp.mkelvin && !dc->watertemp.mkelvin) - return; - put_string(b, " airtemp, " air='", " C'"); - put_temperature(b, dc->watertemp, " water='", " C'"); - put_string(b, " />\n"); -} - -static void save_airpressure(struct membuffer *b, struct divecomputer *dc) -{ - if (!dc->surface_pressure.mbar) - return; - put_string(b, " surface_pressure, " pressure='", " bar'"); - put_string(b, " />\n"); -} - -static void save_salinity(struct membuffer *b, struct divecomputer *dc) -{ - /* only save if we have a value that isn't the default of sea water */ - if (!dc->salinity || dc->salinity == SEAWATER_SALINITY) - return; - put_string(b, " salinity, " salinity='", " g/l'"); - put_string(b, " />\n"); -} - -static void save_overview(struct membuffer *b, struct dive *dive) -{ - show_utf8(b, dive->divemaster, " ", "\n", 0); - show_utf8(b, dive->buddy, " ", "\n", 0); - show_utf8(b, dive->notes, " ", "\n", 0); - show_utf8(b, dive->suit, " ", "\n", 0); -} - -static void put_gasmix(struct membuffer *b, struct gasmix *mix) -{ - int o2 = mix->o2.permille; - int he = mix->he.permille; - - if (o2) { - put_format(b, " o2='%u.%u%%'", FRACTION(o2, 10)); - if (he) - put_format(b, " he='%u.%u%%'", FRACTION(he, 10)); - } -} - -static void save_cylinder_info(struct membuffer *b, struct dive *dive) -{ - int i, nr; - - nr = nr_cylinders(dive); - - for (i = 0; i < nr; i++) { - cylinder_t *cylinder = dive->cylinder + i; - int volume = cylinder->type.size.mliter; - const char *description = cylinder->type.description; - - put_format(b, " type.workingpressure, " workpressure='", " bar'"); - show_utf8(b, description, " description='", "'", 1); - put_gasmix(b, &cylinder->gasmix); - put_pressure(b, cylinder->start, " start='", " bar'"); - put_pressure(b, cylinder->end, " end='", " bar'"); - if (cylinder->cylinder_use != OC_GAS) - show_utf8(b, cylinderuse_text[cylinder->cylinder_use], " use='", "'", 1); - put_format(b, " />\n"); - } -} - -static void save_weightsystem_info(struct membuffer *b, struct dive *dive) -{ - int i, nr; - - nr = nr_weightsystems(dive); - - for (i = 0; i < nr; i++) { - weightsystem_t *ws = dive->weightsystem + i; - int grams = ws->weight.grams; - const char *description = ws->description; - - put_format(b, " \n"); - } -} - -static void show_index(struct membuffer *b, int value, const char *pre, const char *post) -{ - if (value) - put_format(b, " %s%d%s", pre, value, post); -} - -static void save_sample(struct membuffer *b, struct sample *sample, struct sample *old) -{ - put_format(b, " time.seconds, 60)); - put_milli(b, " depth='", sample->depth.mm, " m'"); - if (sample->temperature.mkelvin && sample->temperature.mkelvin != old->temperature.mkelvin) { - put_temperature(b, sample->temperature, " temp='", " C'"); - old->temperature = sample->temperature; - } - put_pressure(b, sample->cylinderpressure, " pressure='", " bar'"); - put_pressure(b, sample->o2cylinderpressure, " o2pressure='", " bar'"); - - /* - * We only show sensor information for samples with pressure, and only if it - * changed from the previous sensor we showed. - */ - if (sample->cylinderpressure.mbar && sample->sensor != old->sensor) { - put_format(b, " sensor='%d'", sample->sensor); - old->sensor = sample->sensor; - } - - /* the deco/ndl values are stored whenever they change */ - if (sample->ndl.seconds != old->ndl.seconds) { - put_format(b, " ndl='%u:%02u min'", FRACTION(sample->ndl.seconds, 60)); - old->ndl = sample->ndl; - } - if (sample->tts.seconds != old->tts.seconds) { - put_format(b, " tts='%u:%02u min'", FRACTION(sample->tts.seconds, 60)); - old->tts = sample->tts; - } - if (sample->rbt.seconds) - put_format(b, " rbt='%u:%02u min'", FRACTION(sample->rbt.seconds, 60)); - if (sample->in_deco != old->in_deco) { - put_format(b, " in_deco='%d'", sample->in_deco ? 1 : 0); - old->in_deco = sample->in_deco; - } - if (sample->stoptime.seconds != old->stoptime.seconds) { - put_format(b, " stoptime='%u:%02u min'", FRACTION(sample->stoptime.seconds, 60)); - old->stoptime = sample->stoptime; - } - - if (sample->stopdepth.mm != old->stopdepth.mm) { - put_milli(b, " stopdepth='", sample->stopdepth.mm, " m'"); - old->stopdepth = sample->stopdepth; - } - - if (sample->cns != old->cns) { - put_format(b, " cns='%u%%'", sample->cns); - old->cns = sample->cns; - } - - if ((sample->o2sensor[0].mbar) && (sample->o2sensor[0].mbar != old->o2sensor[0].mbar)) { - put_milli(b, " sensor1='", sample->o2sensor[0].mbar, " bar'"); - old->o2sensor[0] = sample->o2sensor[0]; - } - - if ((sample->o2sensor[1].mbar) && (sample->o2sensor[1].mbar != old->o2sensor[1].mbar)) { - put_milli(b, " sensor2='", sample->o2sensor[1].mbar, " bar'"); - old->o2sensor[1] = sample->o2sensor[1]; - } - - if ((sample->o2sensor[2].mbar) && (sample->o2sensor[2].mbar != old->o2sensor[2].mbar)) { - put_milli(b, " sensor3='", sample->o2sensor[2].mbar, " bar'"); - old->o2sensor[2] = sample->o2sensor[2]; - } - - if (sample->setpoint.mbar != old->setpoint.mbar) { - put_milli(b, " po2='", sample->setpoint.mbar, " bar'"); - old->setpoint = sample->setpoint; - } - show_index(b, sample->heartbeat, "heartbeat='", "'"); - show_index(b, sample->bearing.degrees, "bearing='", "'"); - put_format(b, " />\n"); -} - -static void save_one_event(struct membuffer *b, struct event *ev) -{ - put_format(b, " time.seconds, 60)); - show_index(b, ev->type, "type='", "'"); - show_index(b, ev->flags, "flags='", "'"); - show_index(b, ev->value, "value='", "'"); - show_utf8(b, ev->name, " name='", "'", 1); - if (event_is_gaschange(ev)) { - if (ev->gas.index >= 0) { - show_index(b, ev->gas.index, "cylinder='", "'"); - put_gasmix(b, &ev->gas.mix); - } else if (!event_gasmix_redundant(ev)) - put_gasmix(b, &ev->gas.mix); - } - put_format(b, " />\n"); -} - - -static void save_events(struct membuffer *b, struct event *ev) -{ - while (ev) { - save_one_event(b, ev); - ev = ev->next; - } -} - -static void save_tags(struct membuffer *b, struct tag_entry *entry) -{ - if (entry) { - const char *sep = " tags='"; - do { - struct divetag *tag = entry->tag; - put_string(b, sep); - /* If the tag has been translated, write the source to the xml file */ - quote(b, tag->source ?: tag->name, 1); - sep = ", "; - } while ((entry = entry->next) != NULL); - put_string(b, "'"); - } -} - -static void save_extra_data(struct membuffer *b, struct extra_data *ed) -{ - while (ed) { - if (ed->key && ed->value) { - put_string(b, " key, " key='", "'", 1); - show_utf8(b, ed->value, " value='", "'", 1); - put_string(b, " />\n"); - } - ed = ed->next; - } -} - -static void show_date(struct membuffer *b, timestamp_t when) -{ - struct tm tm; - - utc_mkdate(when, &tm); - - put_format(b, " date='%04u-%02u-%02u'", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); - put_format(b, " time='%02u:%02u:%02u'", - tm.tm_hour, tm.tm_min, tm.tm_sec); -} - -static void save_samples(struct membuffer *b, int nr, struct sample *s) -{ - struct sample dummy = {}; - - while (--nr >= 0) { - save_sample(b, s, &dummy); - s++; - } -} - -static void save_dc(struct membuffer *b, struct dive *dive, struct divecomputer *dc) -{ - put_format(b, " model, " model='", "'", 1); - if (dc->deviceid) - put_format(b, " deviceid='%08x'", dc->deviceid); - if (dc->diveid) - put_format(b, " diveid='%08x'", dc->diveid); - if (dc->when && dc->when != dive->when) - show_date(b, dc->when); - if (dc->duration.seconds && dc->duration.seconds != dive->dc.duration.seconds) - put_duration(b, dc->duration, " duration='", " min'"); - if (dc->divemode != OC) { - for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) - if (dc->divemode == i) - show_utf8(b, divemode_text[i], " dctype='", "'", 1); - if (dc->no_o2sensors) - put_format(b," no_o2sensors='%d'", dc->no_o2sensors); - } - put_format(b, ">\n"); - save_depths(b, dc); - save_temperatures(b, dc); - save_airpressure(b, dc); - save_salinity(b, dc); - put_duration(b, dc->surfacetime, " ", " min\n"); - save_extra_data(b, dc->extra_data); - save_events(b, dc->events); - save_samples(b, dc->samples, dc->sample); - - put_format(b, " \n"); -} - -static void save_picture(struct membuffer *b, struct picture *pic) -{ - put_string(b, " offset.seconds) { - int offset = pic->offset.seconds; - char sign = '+'; - if (offset < 0) { - sign = '-'; - offset = -offset; - } - put_format(b, " offset='%c%u:%02u min'", sign, FRACTION(offset, 60)); - } - if (pic->latitude.udeg || pic->longitude.udeg) { - put_degrees(b, pic->latitude, " gps='", " "); - put_degrees(b, pic->longitude, "", "'"); - } - if (hashstring(pic->filename)) - put_format(b, " hash='%s'", hashstring(pic->filename)); - - put_string(b, "/>\n"); -} - -void save_one_dive_to_mb(struct membuffer *b, struct dive *dive) -{ - struct divecomputer *dc; - - put_string(b, "number) - put_format(b, " number='%d'", dive->number); - if (dive->tripflag == NO_TRIP) - put_format(b, " tripflag='NOTRIP'"); - if (dive->rating) - put_format(b, " rating='%d'", dive->rating); - if (dive->visibility) - put_format(b, " visibility='%d'", dive->visibility); - save_tags(b, dive->tag_list); - if (dive->dive_site_uuid) { - if (get_dive_site_by_uuid(dive->dive_site_uuid) != NULL) - put_format(b, " divesiteid='%8x'", dive->dive_site_uuid); - else if (verbose) - fprintf(stderr, "removed reference to non-existant dive site with uuid %08x\n", dive->dive_site_uuid); - } - show_date(b, dive->when); - put_format(b, " duration='%u:%02u min'>\n", - FRACTION(dive->dc.duration.seconds, 60)); - save_overview(b, dive); - save_cylinder_info(b, dive); - save_weightsystem_info(b, dive); - save_dive_temperature(b, dive); - /* Save the dive computer data */ - for_each_dc(dive, dc) - save_dc(b, dive, dc); - FOR_EACH_PICTURE(dive) - save_picture(b, picture); - put_format(b, "
\n"); -} - -int save_dive(FILE *f, struct dive *dive) -{ - struct membuffer buf = { 0 }; - - save_one_dive_to_mb(&buf, dive); - flush_buffer(&buf, f); - /* Error handling? */ - return 0; -} - -static void save_trip(struct membuffer *b, dive_trip_t *trip) -{ - int i; - struct dive *dive; - - put_format(b, "when); - show_utf8(b, trip->location, " location=\'", "\'", 1); - put_format(b, ">\n"); - show_utf8(b, trip->notes, "", "\n", 0); - - /* - * Incredibly cheesy: we want to save the dives sorted, and they - * are sorted in the dive array.. So instead of using the dive - * list in the trip, we just traverse the global dive array and - * check the divetrip pointer.. - */ - for_each_dive(i, dive) { - if (dive->divetrip == trip) - save_one_dive_to_mb(b, dive); - } - - put_format(b, "\n"); -} - -static void save_one_device(void *_f, const char *model, uint32_t deviceid, - const char *nickname, const char *serial_nr, const char *firmware) -{ - struct membuffer *b = _f; - - /* Nicknames that are empty or the same as the device model are not interesting */ - if (nickname) { - if (!*nickname || !strcmp(model, nickname)) - nickname = NULL; - } - - /* Serial numbers that are empty are not interesting */ - if (serial_nr && !*serial_nr) - serial_nr = NULL; - - /* Firmware strings that are empty are not interesting */ - if (firmware && !*firmware) - firmware = NULL; - - /* Do we have anything interesting about this dive computer to save? */ - if (!serial_nr && !nickname && !firmware) - return; - - put_format(b, "\n"); -} - -int save_dives(const char *filename) -{ - return save_dives_logic(filename, false); -} - -void save_dives_buffer(struct membuffer *b, const bool select_only) -{ - int i; - struct dive *dive; - dive_trip_t *trip; - - put_format(b, "\n\n", DATAFORMAT_VERSION); - - if (prefs.save_userid_local) - put_format(b, " %30s\n", prefs.userid); - - /* save the dive computer nicknames, if any */ - call_for_each_dc(b, save_one_device, select_only); - if (autogroup) - put_format(b, " \n"); - put_format(b, "\n"); - - /* save the dive sites - to make the output consistent let's sort the table, first */ - dive_site_table_sort(); - put_format(b, "\n"); - for (i = 0; i < dive_site_table.nr; i++) { - int j; - struct dive *d; - struct dive_site *ds = get_dive_site(i); - if (dive_site_is_empty(ds)) { - for_each_dive(j, d) { - if (d->dive_site_uuid == ds->uuid) - d->dive_site_uuid = 0; - } - delete_dive_site(ds->uuid); - i--; // since we just deleted that one - continue; - } else if (ds->name && - (strncmp(ds->name, "Auto-created dive", 17) == 0 || - strncmp(ds->name, "New Dive", 8) == 0)) { - // these are the two default names for sites from - // the web service; if the site isn't used in any - // dive (really? you didn't rename it?), delete it - if (!is_dive_site_used(ds->uuid, false)) { - if (verbose) - fprintf(stderr, "Deleted unused auto-created dive site %s\n", ds->name); - delete_dive_site(ds->uuid); - i--; // since we just deleted that one - continue; - } - } - if (select_only && !is_dive_site_used(ds->uuid, true)) - continue; - - put_format(b, "uuid); - show_utf8(b, ds->name, " name='", "'", 1); - if (ds->latitude.udeg || ds->longitude.udeg) { - put_degrees(b, ds->latitude, " gps='", " "); - put_degrees(b, ds->longitude, "", "'"); - } - show_utf8(b, ds->description, " description='", "'", 1); - put_format(b, ">\n"); - show_utf8(b, ds->notes, " ", " \n", 0); - if (ds->taxonomy.nr) { - for (int j = 0; j < ds->taxonomy.nr; j++) { - struct taxonomy *t = &ds->taxonomy.category[j]; - if (t->category != TC_NONE && t->value) { - put_format(b, " category); - put_format(b, " origin='%d'", t->origin); - show_utf8(b, t->value, " value='", "'/>\n", 1); - } - } - } - put_format(b, "\n"); - } - put_format(b, "\n\n"); - for (trip = dive_trip_list; trip != NULL; trip = trip->next) - trip->index = 0; - - /* save the dives */ - for_each_dive(i, dive) { - if (select_only) { - - if (!dive->selected) - continue; - save_one_dive_to_mb(b, dive); - - } else { - trip = dive->divetrip; - - /* Bare dive without a trip? */ - if (!trip) { - save_one_dive_to_mb(b, dive); - continue; - } - - /* Have we already seen this trip (and thus saved this dive?) */ - if (trip->index) - continue; - - /* We haven't seen this trip before - save it and all dives */ - trip->index = 1; - save_trip(b, trip); - } - } - put_format(b, "\n\n"); -} - -static void save_backup(const char *name, const char *ext, const char *new_ext) -{ - int len = strlen(name); - int a = strlen(ext), b = strlen(new_ext); - char *newname; - - /* len up to and including the final '.' */ - len -= a; - if (len <= 1) - return; - if (name[len - 1] != '.') - return; - /* msvc doesn't have strncasecmp, has _strnicmp instead - crazy */ - if (strncasecmp(name + len, ext, a)) - return; - - newname = malloc(len + b + 1); - if (!newname) - return; - - memcpy(newname, name, len); - memcpy(newname + len, new_ext, b + 1); - - /* - * Ignore errors. Maybe we can't create the backup file, - * maybe no old file existed. Regardless, we'll write the - * new file. - */ - (void) subsurface_rename(name, newname); - free(newname); -} - -static void try_to_backup(const char *filename) -{ - char extension[][5] = { "xml", "ssrf", "" }; - int i = 0; - int flen = strlen(filename); - - /* Maybe we might want to make this configurable? */ - while (extension[i][0] != '\0') { - int elen = strlen(extension[i]); - if (strcasecmp(filename + flen - elen, extension[i]) == 0) { - if (last_xml_version < DATAFORMAT_VERSION) { - int se_len = strlen(extension[i]) + 5; - char *special_ext = malloc(se_len); - snprintf(special_ext, se_len, "%s.v%d", extension[i], last_xml_version); - save_backup(filename, extension[i], special_ext); - free(special_ext); - } else { - save_backup(filename, extension[i], "bak"); - } - break; - } - i++; - } -} - -int save_dives_logic(const char *filename, const bool select_only) -{ - struct membuffer buf = { 0 }; - FILE *f; - void *git; - const char *branch, *remote; - int error; - - git = is_git_repository(filename, &branch, &remote, false); - if (git) - return git_save_dives(git, branch, remote, select_only); - - try_to_backup(filename); - - save_dives_buffer(&buf, select_only); - - error = -1; - f = subsurface_fopen(filename, "w"); - if (f) { - flush_buffer(&buf, f); - error = fclose(f); - } - if (error) - report_error("Save failed (%s)", strerror(errno)); - - free_buffer(&buf); - return error; -} - -int export_dives_xslt(const char *filename, const bool selected, const int units, const char *export_xslt) -{ - FILE *f; - struct membuffer buf = { 0 }; - xmlDoc *doc; - xsltStylesheetPtr xslt = NULL; - xmlDoc *transformed; - int res = 0; - char *params[3]; - int pnr = 0; - char unitstr[3]; - - if (verbose) - fprintf(stderr, "export_dives_xslt with stylesheet %s\n", export_xslt); - - if (!filename) - return report_error("No filename for export"); - - /* Save XML to file and convert it into a memory buffer */ - save_dives_buffer(&buf, selected); - - /* - * Parse the memory buffer into XML document and - * transform it to selected export format, finally dumping - * the XML into a character buffer. - */ - doc = xmlReadMemory(buf.buffer, buf.len, "divelog", NULL, 0); - free_buffer(&buf); - if (!doc) - return report_error("Failed to read XML memory"); - - /* Convert to export format */ - xslt = get_stylesheet(export_xslt); - if (!xslt) - return report_error("Failed to open export conversion stylesheet"); - - snprintf(unitstr, 3, "%d", units); - params[pnr++] = "units"; - params[pnr++] = unitstr; - params[pnr++] = NULL; - - transformed = xsltApplyStylesheet(xslt, doc, (const char **)params); - xmlFreeDoc(doc); - - /* Write the transformed export to file */ - f = subsurface_fopen(filename, "w"); - if (f) { - xsltSaveResultToFile(f, transformed, xslt); - fclose(f); - /* Check write errors? */ - } else { - res = report_error("Failed to open %s for writing (%s)", filename, strerror(errno)); - } - xsltFreeStylesheet(xslt); - xmlFreeDoc(transformed); - - return res; -} diff --git a/serial_ftdi.c b/serial_ftdi.c deleted file mode 100644 index cbac026cf..000000000 --- a/serial_ftdi.c +++ /dev/null @@ -1,664 +0,0 @@ -/* - * libdivecomputer - * - * Copyright (C) 2008 Jef Driesen - * Copyright (C) 2014 Venkatesh Shukla - * Copyright (C) 2015 Anton Lundin - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301 USA - */ - -#include // malloc, free -#include // strerror -#include // errno -#include // gettimeofday -#include // nanosleep -#include - -#include -#include - -#ifndef __ANDROID__ -#define INFO(context, fmt, ...) fprintf(stderr, "INFO: " fmt "\n", ##__VA_ARGS__) -#define ERROR(context, fmt, ...) fprintf(stderr, "ERROR: " fmt "\n", ##__VA_ARGS__) -#else -#include -#define INFO(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "INFO: " fmt "\n", ##__VA_ARGS__) -#define ERROR(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "ERROR: " fmt "\n", ##__VA_ARGS__) -#endif -//#define SYSERROR(context, errcode) ERROR(__FILE__ ":" __LINE__ ": %s", strerror(errcode)) -#define SYSERROR(context, errcode) ; - -#include - -/* Verbatim copied libdivecomputer enums to support configure */ -typedef enum serial_parity_t { - SERIAL_PARITY_NONE, - SERIAL_PARITY_EVEN, - SERIAL_PARITY_ODD -} serial_parity_t; - -typedef enum serial_flowcontrol_t { - SERIAL_FLOWCONTROL_NONE, - SERIAL_FLOWCONTROL_HARDWARE, - SERIAL_FLOWCONTROL_SOFTWARE -} serial_flowcontrol_t; - -typedef enum serial_queue_t { - SERIAL_QUEUE_INPUT = 0x01, - SERIAL_QUEUE_OUTPUT = 0x02, - SERIAL_QUEUE_BOTH = SERIAL_QUEUE_INPUT | SERIAL_QUEUE_OUTPUT -} serial_queue_t; - -typedef enum serial_line_t { - SERIAL_LINE_DCD, // Data carrier detect - SERIAL_LINE_CTS, // Clear to send - SERIAL_LINE_DSR, // Data set ready - SERIAL_LINE_RNG, // Ring indicator -} serial_line_t; - -#define VID 0x0403 // Vendor ID of FTDI - -#define MAX_BACKOFF 500 // Max milliseconds to wait before timing out. - -typedef struct serial_t { - /* Library context. */ - dc_context_t *context; - /* - * The file descriptor corresponding to the serial port. - * Also a libftdi_ftdi_ctx could be used? - */ - struct ftdi_context *ftdi_ctx; - long timeout; - /* - * Serial port settings are saved into this variable immediately - * after the port is opened. These settings are restored when the - * serial port is closed. - * Saving this using libftdi context or libusb. Search further. - * Custom implementation using libftdi functions could be done. - */ - - /* Half-duplex settings */ - int halfduplex; - unsigned int baudrate; - unsigned int nbits; -} serial_t; - -static int serial_ftdi_get_received (serial_t *device) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - // Direct access is not encouraged. But function implementation - // is not available. The return quantity might be anything. - // Find out further about its possible values and correct way of - // access. - int bytes = device->ftdi_ctx->readbuffer_remaining; - - return bytes; -} - -static int serial_ftdi_get_transmitted (serial_t *device) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - // This is not possible using libftdi. Look further into it. - return -1; -} - -static int serial_ftdi_sleep (serial_t *device, unsigned long timeout) -{ - if (device == NULL) - return -1; - - INFO (device->context, "Sleep: value=%lu", timeout); - - struct timespec ts; - ts.tv_sec = (timeout / 1000); - ts.tv_nsec = (timeout % 1000) * 1000000; - - while (nanosleep (&ts, &ts) != 0) { - if (errno != EINTR ) { - SYSERROR (device->context, errno); - return -1; - } - } - - return 0; -} - - -// Used internally for opening ftdi devices -static int serial_ftdi_open_device (struct ftdi_context *ftdi_ctx) -{ - int accepted_pids[] = { 0x6001, 0x6010, 0x6011, // Suunto (Smart Interface), Heinrichs Weikamp - 0xF460, // Oceanic - 0xF680, // Suunto - 0x87D0, // Cressi (Leonardo) - }; - int num_accepted_pids = 6; - int i, pid, ret; - for (i = 0; i < num_accepted_pids; i++) { - pid = accepted_pids[i]; - ret = ftdi_usb_open (ftdi_ctx, VID, pid); - if (ret == -3) // Device not found - continue; - else - return ret; - } - // No supported devices are attached. - return ret; -} - -// -// Open the serial port. -// Initialise ftdi_context and use it to open the device -// -//FIXME: ugly forward declaration of serial_ftdi_configure, util we support configure for real... -static dc_status_t serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol); -static dc_status_t serial_ftdi_open (serial_t **out, dc_context_t *context, const char* name) -{ - if (out == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (context, "Open: name=%s", name ? name : ""); - - // Allocate memory. - serial_t *device = (serial_t *) malloc (sizeof (serial_t)); - if (device == NULL) { - SYSERROR (context, errno); - return DC_STATUS_NOMEMORY; - } - - struct ftdi_context *ftdi_ctx = ftdi_new(); - if (ftdi_ctx == NULL) { - free(device); - SYSERROR (context, errno); - return DC_STATUS_NOMEMORY; - } - - // Library context. - device->context = context; - - // Default to blocking reads. - device->timeout = -1; - - // Default to full-duplex. - device->halfduplex = 0; - device->baudrate = 0; - device->nbits = 0; - - // Initialize device ftdi context - ftdi_init(ftdi_ctx); - - if (ftdi_set_interface(ftdi_ctx,INTERFACE_ANY)) { - free(device); - ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); - return DC_STATUS_IO; - } - - if (serial_ftdi_open_device(ftdi_ctx) < 0) { - free(device); - ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); - return DC_STATUS_IO; - } - - if (ftdi_usb_reset(ftdi_ctx)) { - ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); - return DC_STATUS_IO; - } - - if (ftdi_usb_purge_buffers(ftdi_ctx)) { - free(device); - ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); - return DC_STATUS_IO; - } - - device->ftdi_ctx = ftdi_ctx; - - //FIXME: remove this when custom-serial have support for configure calls - serial_ftdi_configure (device, 115200, 8, 0, 1, 0); - - *out = device; - - return DC_STATUS_SUCCESS; -} - -// -// Close the serial port. -// -static int serial_ftdi_close (serial_t *device) -{ - if (device == NULL) - return 0; - - // Restore the initial terminal attributes. - // See if it is possible using libusb or libftdi - - int ret = ftdi_usb_close(device->ftdi_ctx); - if (ret < 0) { - ERROR (device->context, "Unable to close the ftdi device : %d (%s)\n", - ret, ftdi_get_error_string(device->ftdi_ctx)); - return ret; - } - - ftdi_free(device->ftdi_ctx); - - // Free memory. - free (device); - - return 0; -} - -// -// Configure the serial port (baudrate, databits, parity, stopbits and flowcontrol). -// -static dc_status_t serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (device->context, "Configure: baudrate=%i, databits=%i, parity=%i, stopbits=%i, flowcontrol=%i", - baudrate, databits, parity, stopbits, flowcontrol); - - enum ftdi_bits_type ft_bits; - enum ftdi_stopbits_type ft_stopbits; - enum ftdi_parity_type ft_parity; - - if (ftdi_set_baudrate(device->ftdi_ctx, baudrate) < 0) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; - } - - // Set the character size. - switch (databits) { - case 7: - ft_bits = BITS_7; - break; - case 8: - ft_bits = BITS_8; - break; - default: - return DC_STATUS_INVALIDARGS; - } - - // Set the parity type. - switch (parity) { - case SERIAL_PARITY_NONE: // No parity - ft_parity = NONE; - break; - case SERIAL_PARITY_EVEN: // Even parity - ft_parity = EVEN; - break; - case SERIAL_PARITY_ODD: // Odd parity - ft_parity = ODD; - break; - default: - return DC_STATUS_INVALIDARGS; - } - - // Set the number of stop bits. - switch (stopbits) { - case 1: // One stopbit - ft_stopbits = STOP_BIT_1; - break; - case 2: // Two stopbits - ft_stopbits = STOP_BIT_2; - break; - default: - return DC_STATUS_INVALIDARGS; - } - - // Set the attributes - if (ftdi_set_line_property(device->ftdi_ctx, ft_bits, ft_stopbits, ft_parity)) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return DC_STATUS_IO; - } - - // Set the flow control. - switch (flowcontrol) { - case SERIAL_FLOWCONTROL_NONE: // No flow control. - if (ftdi_setflowctrl(device->ftdi_ctx, SIO_DISABLE_FLOW_CTRL) < 0) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return DC_STATUS_IO; - } - break; - case SERIAL_FLOWCONTROL_HARDWARE: // Hardware (RTS/CTS) flow control. - if (ftdi_setflowctrl(device->ftdi_ctx, SIO_RTS_CTS_HS) < 0) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return DC_STATUS_IO; - } - break; - case SERIAL_FLOWCONTROL_SOFTWARE: // Software (XON/XOFF) flow control. - if (ftdi_setflowctrl(device->ftdi_ctx, SIO_XON_XOFF_HS) < 0) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return DC_STATUS_IO; - } - break; - default: - return DC_STATUS_INVALIDARGS; - } - - device->baudrate = baudrate; - device->nbits = 1 + databits + stopbits + (parity ? 1 : 0); - - return DC_STATUS_SUCCESS; -} - -// -// Configure the serial port (timeouts). -// -static int serial_ftdi_set_timeout (serial_t *device, long timeout) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (device->context, "Timeout: value=%li", timeout); - - device->timeout = timeout; - - return 0; -} - -static int serial_ftdi_set_halfduplex (serial_t *device, int value) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - // Most ftdi chips support full duplex operation. ft232rl does. - // Crosscheck other chips. - - device->halfduplex = value; - - return 0; -} - -static int serial_ftdi_read (serial_t *device, void *data, unsigned int size) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - // The total timeout. - long timeout = device->timeout; - - // The absolute target time. - struct timeval tve; - - static int backoff = 1; - int init = 1; - unsigned int nbytes = 0; - while (nbytes < size) { - struct timeval tvt; - if (timeout > 0) { - struct timeval now; - if (gettimeofday (&now, NULL) != 0) { - SYSERROR (device->context, errno); - return -1; - } - - if (init) { - // Calculate the initial timeout. - tvt.tv_sec = (timeout / 1000); - tvt.tv_usec = (timeout % 1000) * 1000; - // Calculate the target time. - timeradd (&now, &tvt, &tve); - } else { - // Calculate the remaining timeout. - if (timercmp (&now, &tve, <)) - timersub (&tve, &now, &tvt); - else - timerclear (&tvt); - } - init = 0; - } else if (timeout == 0) { - timerclear (&tvt); - } - - int n = ftdi_read_data (device->ftdi_ctx, (char *) data + nbytes, size - nbytes); - if (n < 0) { - if (n == LIBUSB_ERROR_INTERRUPTED) - continue; //Retry. - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; //Error during read call. - } else if (n == 0) { - // Exponential backoff. - if (backoff > MAX_BACKOFF) { - ERROR(device->context, "%s", "FTDI read timed out."); - return -1; - } - serial_ftdi_sleep (device, backoff); - backoff *= 2; - } else { - // Reset backoff to 1 on success. - backoff = 1; - } - - nbytes += n; - } - - INFO (device->context, "Read %d bytes", nbytes); - - return nbytes; -} - -static int serial_ftdi_write (serial_t *device, const void *data, unsigned int size) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - struct timeval tve, tvb; - if (device->halfduplex) { - // Get the current time. - if (gettimeofday (&tvb, NULL) != 0) { - SYSERROR (device->context, errno); - return -1; - } - } - - unsigned int nbytes = 0; - while (nbytes < size) { - - int n = ftdi_write_data (device->ftdi_ctx, (char *) data + nbytes, size - nbytes); - if (n < 0) { - if (n == LIBUSB_ERROR_INTERRUPTED) - continue; // Retry. - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; // Error during write call. - } else if (n == 0) { - break; // EOF. - } - - nbytes += n; - } - - if (device->halfduplex) { - // Get the current time. - if (gettimeofday (&tve, NULL) != 0) { - SYSERROR (device->context, errno); - return -1; - } - - // Calculate the elapsed time (microseconds). - struct timeval tvt; - timersub (&tve, &tvb, &tvt); - unsigned long elapsed = tvt.tv_sec * 1000000 + tvt.tv_usec; - - // Calculate the expected duration (microseconds). A 2 millisecond fudge - // factor is added because it improves the success rate significantly. - unsigned long expected = 1000000.0 * device->nbits / device->baudrate * size + 0.5 + 2000; - - // Wait for the remaining time. - if (elapsed < expected) { - unsigned long remaining = expected - elapsed; - - // The remaining time is rounded up to the nearest millisecond to - // match the Windows implementation. The higher resolution is - // pointless anyway, since we already added a fudge factor above. - serial_ftdi_sleep (device, (remaining + 999) / 1000); - } - } - - INFO (device->context, "Wrote %d bytes", nbytes); - - return nbytes; -} - -static int serial_ftdi_flush (serial_t *device, int queue) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (device->context, "Flush: queue=%u, input=%i, output=%i", queue, - serial_ftdi_get_received (device), - serial_ftdi_get_transmitted (device)); - - switch (queue) { - case SERIAL_QUEUE_INPUT: - if (ftdi_usb_purge_tx_buffer(device->ftdi_ctx)) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; - } - break; - case SERIAL_QUEUE_OUTPUT: - if (ftdi_usb_purge_rx_buffer(device->ftdi_ctx)) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; - } - break; - default: - if (ftdi_usb_purge_buffers(device->ftdi_ctx)) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; - } - break; - } - - return 0; -} - -static int serial_ftdi_send_break (serial_t *device) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument)a - - INFO (device->context, "Break : One time period."); - - // no direct functions for sending break signals in libftdi. - // there is a suggestion to lower the baudrate and sending NUL - // and resetting the baudrate up again. But it has flaws. - // Not implementing it before researching more. - - return -1; -} - -static int serial_ftdi_set_break (serial_t *device, int level) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (device->context, "Break: value=%i", level); - - // Not implemented in libftdi yet. Research it further. - - return -1; -} - -static int serial_ftdi_set_dtr (serial_t *device, int level) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (device->context, "DTR: value=%i", level); - - if (ftdi_setdtr(device->ftdi_ctx, level)) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; - } - - return 0; -} - -static int serial_ftdi_set_rts (serial_t *device, int level) -{ - if (device == NULL) - return -1; // EINVAL (Invalid argument) - - INFO (device->context, "RTS: value=%i", level); - - if (ftdi_setrts(device->ftdi_ctx, level)) { - ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); - return -1; - } - - return 0; -} - -const dc_serial_operations_t serial_ftdi_ops = { - .open = serial_ftdi_open, - .close = serial_ftdi_close, - .read = serial_ftdi_read, - .write = serial_ftdi_write, - .flush = serial_ftdi_flush, - .get_received = serial_ftdi_get_received, - .get_transmitted = NULL, /*NOT USED ANYWHERE! serial_ftdi_get_transmitted */ - .set_timeout = serial_ftdi_set_timeout -#ifdef FIXED_SSRF_CUSTOM_SERIAL - , - .configure = serial_ftdi_configure, -//static int serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol) - .set_halfduplex = serial_ftdi_set_halfduplex, -//static int serial_ftdi_set_halfduplex (serial_t *device, int value) - .send_break = serial_ftdi_send_break, -//static int serial_ftdi_send_break (serial_t *device) - .set_break = serial_ftdi_set_break, -//static int serial_ftdi_set_break (serial_t *device, int level) - .set_dtr = serial_ftdi_set_dtr, -//static int serial_ftdi_set_dtr (serial_t *device, int level) - .set_rts = serial_ftdi_set_rts -//static int serial_ftdi_set_rts (serial_t *device, int level) -#endif -}; - -dc_status_t dc_serial_ftdi_open(dc_serial_t **out, dc_context_t *context) -{ - if (out == NULL) - return DC_STATUS_INVALIDARGS; - - // Allocate memory. - dc_serial_t *serial_device = (dc_serial_t *) malloc (sizeof (dc_serial_t)); - - if (serial_device == NULL) { - return DC_STATUS_NOMEMORY; - } - - // Initialize data and function pointers - dc_serial_init(serial_device, NULL, &serial_ftdi_ops); - - // Open the serial device. - dc_status_t rc = (dc_status_t) serial_ftdi_open (&serial_device->port, context, NULL); - if (rc != DC_STATUS_SUCCESS) { - free (serial_device); - return rc; - } - - // Set the type of the device - serial_device->type = DC_TRANSPORT_USB;; - - *out = serial_device; - - return DC_STATUS_SUCCESS; -} diff --git a/sha1.c b/sha1.c deleted file mode 100644 index acf8c5d9f..000000000 --- a/sha1.c +++ /dev/null @@ -1,300 +0,0 @@ -/* - * SHA1 routine optimized to do word accesses rather than byte accesses, - * and to avoid unnecessary copies into the context array. - * - * This was initially based on the Mozilla SHA1 implementation, although - * none of the original Mozilla code remains. - */ - -/* this is only to get definitions for memcpy(), ntohl() and htonl() */ -#include -#include -#ifdef WIN32 -#include -#else -#include -#endif -#include "sha1.h" - -#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) - -/* - * Force usage of rol or ror by selecting the one with the smaller constant. - * It _can_ generate slightly smaller code (a constant of 1 is special), but - * perhaps more importantly it's possibly faster on any uarch that does a - * rotate with a loop. - */ - -#define SHA_ASM(op, x, n) ({ unsigned int __res; __asm__(op " %1,%0":"=r" (__res):"i" (n), "0" (x)); __res; }) -#define SHA_ROL(x, n) SHA_ASM("rol", x, n) -#define SHA_ROR(x, n) SHA_ASM("ror", x, n) - -#else - -#define SHA_ROT(X, l, r) (((X) << (l)) | ((X) >> (r))) -#define SHA_ROL(X, n) SHA_ROT(X, n, 32 - (n)) -#define SHA_ROR(X, n) SHA_ROT(X, 32 - (n), n) - -#endif - -/* - * If you have 32 registers or more, the compiler can (and should) - * try to change the array[] accesses into registers. However, on - * machines with less than ~25 registers, that won't really work, - * and at least gcc will make an unholy mess of it. - * - * So to avoid that mess which just slows things down, we force - * the stores to memory to actually happen (we might be better off - * with a 'W(t)=(val);asm("":"+m" (W(t))' there instead, as - * suggested by Artur Skawina - that will also make gcc unable to - * try to do the silly "optimize away loads" part because it won't - * see what the value will be). - * - * Ben Herrenschmidt reports that on PPC, the C version comes close - * to the optimized asm with this (ie on PPC you don't want that - * 'volatile', since there are lots of registers). - * - * On ARM we get the best code generation by forcing a full memory barrier - * between each SHA_ROUND, otherwise gcc happily get wild with spilling and - * the stack frame size simply explode and performance goes down the drain. - */ - -#if defined(__i386__) || defined(__x86_64__) -#define setW(x, val) (*(volatile unsigned int *)&W(x) = (val)) -#elif defined(__GNUC__) && defined(__arm__) -#define setW(x, val) \ - do { \ - W(x) = (val); \ - __asm__("" :: : "memory"); \ - } while (0) -#else -#define setW(x, val) (W(x) = (val)) -#endif - -/* - * Performance might be improved if the CPU architecture is OK with - * unaligned 32-bit loads and a fast ntohl() is available. - * Otherwise fall back to byte loads and shifts which is portable, - * and is faster on architectures with memory alignment issues. - */ - -#if defined(__i386__) || defined(__x86_64__) || \ - defined(_M_IX86) || defined(_M_X64) || \ - defined(__ppc__) || defined(__ppc64__) || \ - defined(__powerpc__) || defined(__powerpc64__) || \ - defined(__s390__) || defined(__s390x__) - -#define get_be32(p) ntohl(*(unsigned int *)(p)) -#define put_be32(p, v) \ - do { \ - *(unsigned int *)(p) = htonl(v); \ - } while (0) - -#else - -#define get_be32(p) ( \ - (*((unsigned char *)(p) + 0) << 24) | \ - (*((unsigned char *)(p) + 1) << 16) | \ - (*((unsigned char *)(p) + 2) << 8) | \ - (*((unsigned char *)(p) + 3) << 0)) -#define put_be32(p, v) \ - do { \ - unsigned int __v = (v); \ - *((unsigned char *)(p) + 0) = __v >> 24; \ - *((unsigned char *)(p) + 1) = __v >> 16; \ - *((unsigned char *)(p) + 2) = __v >> 8; \ - *((unsigned char *)(p) + 3) = __v >> 0; \ - } while (0) - -#endif - -/* This "rolls" over the 512-bit array */ -#define W(x) (array[(x) & 15]) - -/* - * Where do we get the source from? The first 16 iterations get it from - * the input data, the next mix it from the 512-bit array. - */ -#define SHA_SRC(t) get_be32((unsigned char *)block + (t) * 4) -#define SHA_MIX(t) SHA_ROL(W((t) + 13) ^ W((t) + 8) ^ W((t) + 2) ^ W(t), 1); - -#define SHA_ROUND(t, input, fn, constant, A, B, C, D, E) \ - do { \ - unsigned int TEMP = input(t); \ - setW(t, TEMP); \ - E += TEMP + SHA_ROL(A, 5) + (fn) + (constant); \ - B = SHA_ROR(B, 2); \ - } while (0) - -#define T_0_15(t, A, B, C, D, E) SHA_ROUND(t, SHA_SRC, (((C ^ D) & B) ^ D), 0x5a827999, A, B, C, D, E) -#define T_16_19(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (((C ^ D) & B) ^ D), 0x5a827999, A, B, C, D, E) -#define T_20_39(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B ^ C ^ D), 0x6ed9eba1, A, B, C, D, E) -#define T_40_59(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, ((B &C) + (D &(B ^ C))), 0x8f1bbcdc, A, B, C, D, E) -#define T_60_79(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B ^ C ^ D), 0xca62c1d6, A, B, C, D, E) - -static void blk_SHA1_Block(blk_SHA_CTX *ctx, const void *block) -{ - unsigned int A, B, C, D, E; - unsigned int array[16]; - - A = ctx->H[0]; - B = ctx->H[1]; - C = ctx->H[2]; - D = ctx->H[3]; - E = ctx->H[4]; - - /* Round 1 - iterations 0-16 take their input from 'block' */ - T_0_15(0, A, B, C, D, E); - T_0_15(1, E, A, B, C, D); - T_0_15(2, D, E, A, B, C); - T_0_15(3, C, D, E, A, B); - T_0_15(4, B, C, D, E, A); - T_0_15(5, A, B, C, D, E); - T_0_15(6, E, A, B, C, D); - T_0_15(7, D, E, A, B, C); - T_0_15(8, C, D, E, A, B); - T_0_15(9, B, C, D, E, A); - T_0_15(10, A, B, C, D, E); - T_0_15(11, E, A, B, C, D); - T_0_15(12, D, E, A, B, C); - T_0_15(13, C, D, E, A, B); - T_0_15(14, B, C, D, E, A); - T_0_15(15, A, B, C, D, E); - - /* Round 1 - tail. Input from 512-bit mixing array */ - T_16_19(16, E, A, B, C, D); - T_16_19(17, D, E, A, B, C); - T_16_19(18, C, D, E, A, B); - T_16_19(19, B, C, D, E, A); - - /* Round 2 */ - T_20_39(20, A, B, C, D, E); - T_20_39(21, E, A, B, C, D); - T_20_39(22, D, E, A, B, C); - T_20_39(23, C, D, E, A, B); - T_20_39(24, B, C, D, E, A); - T_20_39(25, A, B, C, D, E); - T_20_39(26, E, A, B, C, D); - T_20_39(27, D, E, A, B, C); - T_20_39(28, C, D, E, A, B); - T_20_39(29, B, C, D, E, A); - T_20_39(30, A, B, C, D, E); - T_20_39(31, E, A, B, C, D); - T_20_39(32, D, E, A, B, C); - T_20_39(33, C, D, E, A, B); - T_20_39(34, B, C, D, E, A); - T_20_39(35, A, B, C, D, E); - T_20_39(36, E, A, B, C, D); - T_20_39(37, D, E, A, B, C); - T_20_39(38, C, D, E, A, B); - T_20_39(39, B, C, D, E, A); - - /* Round 3 */ - T_40_59(40, A, B, C, D, E); - T_40_59(41, E, A, B, C, D); - T_40_59(42, D, E, A, B, C); - T_40_59(43, C, D, E, A, B); - T_40_59(44, B, C, D, E, A); - T_40_59(45, A, B, C, D, E); - T_40_59(46, E, A, B, C, D); - T_40_59(47, D, E, A, B, C); - T_40_59(48, C, D, E, A, B); - T_40_59(49, B, C, D, E, A); - T_40_59(50, A, B, C, D, E); - T_40_59(51, E, A, B, C, D); - T_40_59(52, D, E, A, B, C); - T_40_59(53, C, D, E, A, B); - T_40_59(54, B, C, D, E, A); - T_40_59(55, A, B, C, D, E); - T_40_59(56, E, A, B, C, D); - T_40_59(57, D, E, A, B, C); - T_40_59(58, C, D, E, A, B); - T_40_59(59, B, C, D, E, A); - - /* Round 4 */ - T_60_79(60, A, B, C, D, E); - T_60_79(61, E, A, B, C, D); - T_60_79(62, D, E, A, B, C); - T_60_79(63, C, D, E, A, B); - T_60_79(64, B, C, D, E, A); - T_60_79(65, A, B, C, D, E); - T_60_79(66, E, A, B, C, D); - T_60_79(67, D, E, A, B, C); - T_60_79(68, C, D, E, A, B); - T_60_79(69, B, C, D, E, A); - T_60_79(70, A, B, C, D, E); - T_60_79(71, E, A, B, C, D); - T_60_79(72, D, E, A, B, C); - T_60_79(73, C, D, E, A, B); - T_60_79(74, B, C, D, E, A); - T_60_79(75, A, B, C, D, E); - T_60_79(76, E, A, B, C, D); - T_60_79(77, D, E, A, B, C); - T_60_79(78, C, D, E, A, B); - T_60_79(79, B, C, D, E, A); - - ctx->H[0] += A; - ctx->H[1] += B; - ctx->H[2] += C; - ctx->H[3] += D; - ctx->H[4] += E; -} - -void blk_SHA1_Init(blk_SHA_CTX *ctx) -{ - ctx->size = 0; - - /* Initialize H with the magic constants (see FIPS180 for constants) */ - ctx->H[0] = 0x67452301; - ctx->H[1] = 0xefcdab89; - ctx->H[2] = 0x98badcfe; - ctx->H[3] = 0x10325476; - ctx->H[4] = 0xc3d2e1f0; -} - -void blk_SHA1_Update(blk_SHA_CTX *ctx, const void *data, unsigned long len) -{ - unsigned int lenW = ctx->size & 63; - - ctx->size += len; - - /* Read the data into W and process blocks as they get full */ - if (lenW) { - unsigned int left = 64 - lenW; - if (len < left) - left = len; - memcpy(lenW + (char *)ctx->W, data, left); - lenW = (lenW + left) & 63; - len -= left; - data = ((const char *)data + left); - if (lenW) - return; - blk_SHA1_Block(ctx, ctx->W); - } - while (len >= 64) { - blk_SHA1_Block(ctx, data); - data = ((const char *)data + 64); - len -= 64; - } - if (len) - memcpy(ctx->W, data, len); -} - -void blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx) -{ - static const unsigned char pad[64] = { 0x80 }; - unsigned int padlen[2]; - int i; - - /* Pad with a binary 1 (ie 0x80), then zeroes, then length */ - padlen[0] = htonl((uint32_t)(ctx->size >> 29)); - padlen[1] = htonl((uint32_t)(ctx->size << 3)); - - i = ctx->size & 63; - blk_SHA1_Update(ctx, pad, 1 + (63 & (55 - i))); - blk_SHA1_Update(ctx, padlen, 8); - - /* Output hash */ - for (i = 0; i < 5; i++) - put_be32(hashout + i * 4, ctx->H[i]); -} diff --git a/sha1.h b/sha1.h deleted file mode 100644 index cab6ff77d..000000000 --- a/sha1.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SHA1 routine optimized to do word accesses rather than byte accesses, - * and to avoid unnecessary copies into the context array. - * - * This was initially based on the Mozilla SHA1 implementation, although - * none of the original Mozilla code remains. - */ -#ifndef SHA1_H -#define SHA1_H - -typedef struct -{ - unsigned long long size; - unsigned int H[5]; - unsigned int W[16]; -} blk_SHA_CTX; - -void blk_SHA1_Init(blk_SHA_CTX *ctx); -void blk_SHA1_Update(blk_SHA_CTX *ctx, const void *dataIn, unsigned long len); -void blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx); - -/* Make us use the standard names */ -#define SHA_CTX blk_SHA_CTX -#define SHA1_Init blk_SHA1_Init -#define SHA1_Update blk_SHA1_Update -#define SHA1_Final blk_SHA1_Final - -/* Trivial helper function */ -static inline void SHA1(const void *dataIn, unsigned long len, unsigned char hashout[20]) -{ - SHA_CTX ctx; - - SHA1_Init(&ctx); - SHA1_Update(&ctx, dataIn, len); - SHA1_Final(hashout, &ctx); -} - -#endif // SHA1_H diff --git a/statistics.c b/statistics.c deleted file mode 100644 index 19fd350eb..000000000 --- a/statistics.c +++ /dev/null @@ -1,379 +0,0 @@ -/* statistics.c - * - * core logic for the Info & Stats page - - * char *get_time_string(int seconds, int maxdays); - * char *get_minutes(int seconds); - * void process_all_dives(struct dive *dive, struct dive **prev_dive); - * void get_selected_dives_text(char *buffer, int size); - */ -#include "gettext.h" -#include -#include - -#include "dive.h" -#include "display.h" -#include "divelist.h" -#include "statistics.h" - -static stats_t stats; -stats_t stats_selection; -stats_t *stats_monthly = NULL; -stats_t *stats_yearly = NULL; -stats_t *stats_by_trip = NULL; - -static void process_temperatures(struct dive *dp, stats_t *stats) -{ - int min_temp, mean_temp, max_temp = 0; - - max_temp = dp->maxtemp.mkelvin; - if (max_temp && (!stats->max_temp || max_temp > stats->max_temp)) - stats->max_temp = max_temp; - - min_temp = dp->mintemp.mkelvin; - if (min_temp && (!stats->min_temp || min_temp < stats->min_temp)) - stats->min_temp = min_temp; - - if (min_temp || max_temp) { - mean_temp = min_temp; - if (mean_temp) - mean_temp = (mean_temp + max_temp) / 2; - else - mean_temp = max_temp; - stats->combined_temp += get_temp_units(mean_temp, NULL); - stats->combined_count++; - } -} - -static void process_dive(struct dive *dp, stats_t *stats) -{ - int old_tt, sac_time = 0; - int duration = dp->duration.seconds; - - old_tt = stats->total_time.seconds; - stats->total_time.seconds += duration; - if (duration > stats->longest_time.seconds) - stats->longest_time.seconds = duration; - if (stats->shortest_time.seconds == 0 || duration < stats->shortest_time.seconds) - stats->shortest_time.seconds = duration; - if (dp->maxdepth.mm > stats->max_depth.mm) - stats->max_depth.mm = dp->maxdepth.mm; - if (stats->min_depth.mm == 0 || dp->maxdepth.mm < stats->min_depth.mm) - stats->min_depth.mm = dp->maxdepth.mm; - - process_temperatures(dp, stats); - - /* Maybe we should drop zero-duration dives */ - if (!duration) - return; - stats->avg_depth.mm = (1.0 * old_tt * stats->avg_depth.mm + - duration * dp->meandepth.mm) / - stats->total_time.seconds; - if (dp->sac > 100) { /* less than .1 l/min is bogus, even with a pSCR */ - sac_time = stats->total_sac_time + duration; - stats->avg_sac.mliter = (1.0 * stats->total_sac_time * stats->avg_sac.mliter + - duration * dp->sac) / - sac_time; - if (dp->sac > stats->max_sac.mliter) - stats->max_sac.mliter = dp->sac; - if (stats->min_sac.mliter == 0 || dp->sac < stats->min_sac.mliter) - stats->min_sac.mliter = dp->sac; - stats->total_sac_time = sac_time; - } -} - -char *get_minutes(int seconds) -{ - static char buf[80]; - snprintf(buf, sizeof(buf), "%d:%.2d", FRACTION(seconds, 60)); - return buf; -} - -void process_all_dives(struct dive *dive, struct dive **prev_dive) -{ - int idx; - struct dive *dp; - struct tm tm; - int current_year = 0; - int current_month = 0; - int year_iter = 0; - int month_iter = 0; - int prev_month = 0, prev_year = 0; - int trip_iter = 0; - dive_trip_t *trip_ptr = 0; - unsigned int size; - - *prev_dive = NULL; - memset(&stats, 0, sizeof(stats)); - if (dive_table.nr > 0) { - stats.shortest_time.seconds = dive_table.dives[0]->duration.seconds; - stats.min_depth.mm = dive_table.dives[0]->maxdepth.mm; - stats.selection_size = dive_table.nr; - } - - /* allocate sufficient space to hold the worst - * case (one dive per year or all dives during - * one month) for yearly and monthly statistics*/ - - free(stats_yearly); - free(stats_monthly); - free(stats_by_trip); - - size = sizeof(stats_t) * (dive_table.nr + 1); - stats_yearly = malloc(size); - stats_monthly = malloc(size); - stats_by_trip = malloc(size); - if (!stats_yearly || !stats_monthly || !stats_by_trip) - return; - memset(stats_yearly, 0, size); - memset(stats_monthly, 0, size); - memset(stats_by_trip, 0, size); - stats_yearly[0].is_year = true; - - /* this relies on the fact that the dives in the dive_table - * are in chronological order */ - for_each_dive (idx, dp) { - if (dive && dp->when == dive->when) { - /* that's the one we are showing */ - if (idx > 0) - *prev_dive = dive_table.dives[idx - 1]; - } - process_dive(dp, &stats); - - /* yearly statistics */ - utc_mkdate(dp->when, &tm); - if (current_year == 0) - current_year = tm.tm_year + 1900; - - if (current_year != tm.tm_year + 1900) { - current_year = tm.tm_year + 1900; - process_dive(dp, &(stats_yearly[++year_iter])); - stats_yearly[year_iter].is_year = true; - } else { - process_dive(dp, &(stats_yearly[year_iter])); - } - stats_yearly[year_iter].selection_size++; - stats_yearly[year_iter].period = current_year; - - if (dp->divetrip != NULL) { - if (trip_ptr != dp->divetrip) { - trip_ptr = dp->divetrip; - trip_iter++; - } - - /* stats_by_trip[0] is all the dives combined */ - stats_by_trip[0].selection_size++; - process_dive(dp, &(stats_by_trip[0])); - stats_by_trip[0].is_trip = true; - stats_by_trip[0].location = strdup("All (by trip stats)"); - - process_dive(dp, &(stats_by_trip[trip_iter])); - stats_by_trip[trip_iter].selection_size++; - stats_by_trip[trip_iter].is_trip = true; - stats_by_trip[trip_iter].location = dp->divetrip->location; - } - - /* monthly statistics */ - if (current_month == 0) { - current_month = tm.tm_mon + 1; - } else { - if (current_month != tm.tm_mon + 1) - current_month = tm.tm_mon + 1; - if (prev_month != current_month || prev_year != current_year) - month_iter++; - } - process_dive(dp, &(stats_monthly[month_iter])); - stats_monthly[month_iter].selection_size++; - stats_monthly[month_iter].period = current_month; - prev_month = current_month; - prev_year = current_year; - } -} - -/* make sure we skip the selected summary entries */ -void process_selected_dives(void) -{ - struct dive *dive; - unsigned int i, nr; - - memset(&stats_selection, 0, sizeof(stats_selection)); - - nr = 0; - for_each_dive(i, dive) { - if (dive->selected) { - process_dive(dive, &stats_selection); - nr++; - } - } - stats_selection.selection_size = nr; -} - -char *get_time_string_s(int seconds, int maxdays, bool freediving) -{ - static char buf[80]; - if (maxdays && seconds > 3600 * 24 * maxdays) { - snprintf(buf, sizeof(buf), translate("gettextFromC", "more than %d days"), maxdays); - } else { - int days = seconds / 3600 / 24; - int hours = (seconds - days * 3600 * 24) / 3600; - int minutes = (seconds - days * 3600 * 24 - hours * 3600) / 60; - int secs = (seconds - days * 3600 * 24 - hours * 3600 - minutes*60); - if (days > 0) - snprintf(buf, sizeof(buf), translate("gettextFromC", "%dd %dh %dmin"), days, hours, minutes); - else - if (freediving && seconds < 3600) - snprintf(buf, sizeof(buf), translate("gettextFromC", "%dmin %dsecs"), minutes, secs); - else - snprintf(buf, sizeof(buf), translate("gettextFromC", "%dh %dmin"), hours, minutes); - } - return buf; -} - -/* this gets called when at least two but not all dives are selected */ -static void get_ranges(char *buffer, int size) -{ - int i, len; - int first = -1, last = -1; - struct dive *dive; - - snprintf(buffer, size, "%s", translate("gettextFromC", "for dives #")); - for_each_dive (i, dive) { - if (!dive->selected) - continue; - if (dive->number < 1) { - /* uhh - weird numbers - bail */ - snprintf(buffer, size, "%s", translate("gettextFromC", "for selected dives")); - return; - } - len = strlen(buffer); - if (last == -1) { - snprintf(buffer + len, size - len, "%d", dive->number); - first = last = dive->number; - } else { - if (dive->number == last + 1) { - last++; - continue; - } else { - if (first == last) - snprintf(buffer + len, size - len, ", %d", dive->number); - else if (first + 1 == last) - snprintf(buffer + len, size - len, ", %d, %d", last, dive->number); - else - snprintf(buffer + len, size - len, "-%d, %d", last, dive->number); - first = last = dive->number; - } - } - } - len = strlen(buffer); - if (first != last) { - if (first + 1 == last) - snprintf(buffer + len, size - len, ", %d", last); - else - snprintf(buffer + len, size - len, "-%d", last); - } -} - -void get_selected_dives_text(char *buffer, int size) -{ - if (amount_selected == 1) { - if (current_dive) - snprintf(buffer, size, translate("gettextFromC", "for dive #%d"), current_dive->number); - else - snprintf(buffer, size, "%s", translate("gettextFromC", "for selected dive")); - } else if (amount_selected == dive_table.nr) { - snprintf(buffer, size, "%s", translate("gettextFromC", "for all dives")); - } else if (amount_selected == 0) { - snprintf(buffer, size, "%s", translate("gettextFromC", "(no dives)")); - } else { - get_ranges(buffer, size); - if (strlen(buffer) == size - 1) { - /* add our own ellipse... the way Pango does this is ugly - * as it will leave partial numbers there which I don't like */ - int offset = 4; - while (offset < size && isdigit(buffer[size - offset])) - offset++; - strcpy(buffer + size - offset, "..."); - } - } -} - -#define SOME_GAS 5000 // 5bar drop in cylinder pressure makes cylinder used - -bool is_cylinder_used(struct dive *dive, int idx) -{ - struct divecomputer *dc; - bool firstGasExplicit = false; - if (cylinder_none(&dive->cylinder[idx])) - return false; - - if ((dive->cylinder[idx].start.mbar - dive->cylinder[idx].end.mbar) > SOME_GAS) - return true; - for_each_dc(dive, dc) { - struct event *event = get_next_event(dc->events, "gaschange"); - while (event) { - if (dc->sample && (event->time.seconds == 0 || - (dc->samples && dc->sample[0].time.seconds == event->time.seconds))) - firstGasExplicit = true; - if (get_cylinder_index(dive, event) == idx) - return true; - event = get_next_event(event->next, "gaschange"); - } - if (dc->divemode == CCR && (idx == dive->diluent_cylinder_index || idx == dive->oxygen_cylinder_index)) - return true; - } - if (idx == 0 && !firstGasExplicit) - return true; - return false; -} - -void get_gas_used(struct dive *dive, volume_t gases[MAX_CYLINDERS]) -{ - int idx; - for (idx = 0; idx < MAX_CYLINDERS; idx++) { - cylinder_t *cyl = &dive->cylinder[idx]; - pressure_t start, end; - - if (!is_cylinder_used(dive, idx)) - continue; - - start = cyl->start.mbar ? cyl->start : cyl->sample_start; - end = cyl->end.mbar ? cyl->end : cyl->sample_end; - if (end.mbar && start.mbar > end.mbar) - gases[idx].mliter = gas_volume(cyl, start) - gas_volume(cyl, end); - } -} - -/* Quite crude reverse-blender-function, but it produces a approx result */ -static void get_gas_parts(struct gasmix mix, volume_t vol, int o2_in_topup, volume_t *o2, volume_t *he) -{ - volume_t air = {}; - - if (gasmix_is_air(&mix)) { - o2->mliter = 0; - he->mliter = 0; - return; - } - - air.mliter = rint(((double)vol.mliter * (1000 - get_he(&mix) - get_o2(&mix))) / (1000 - o2_in_topup)); - he->mliter = rint(((double)vol.mliter * get_he(&mix)) / 1000.0); - o2->mliter += vol.mliter - he->mliter - air.mliter; -} - -void selected_dives_gas_parts(volume_t *o2_tot, volume_t *he_tot) -{ - int i, j; - struct dive *d; - for_each_dive (i, d) { - if (!d->selected) - continue; - volume_t diveGases[MAX_CYLINDERS] = {}; - get_gas_used(d, diveGases); - for (j = 0; j < MAX_CYLINDERS; j++) { - if (diveGases[j].mliter) { - volume_t o2 = {}, he = {}; - get_gas_parts(d->cylinder[j].gasmix, diveGases[j], O2_IN_AIR, &o2, &he); - o2_tot->mliter += o2.mliter; - he_tot->mliter += he.mliter; - } - } - } -} diff --git a/statistics.h b/statistics.h deleted file mode 100644 index dbab25761..000000000 --- a/statistics.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * statistics.h - * - * core logic functions called from statistics UI - * common types and variables - */ - -#ifndef STATISTICS_H -#define STATISTICS_H - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct -{ - int period; - duration_t total_time; - /* avg_time is simply total_time / nr -- let's not keep this */ - duration_t shortest_time; - duration_t longest_time; - depth_t max_depth; - depth_t min_depth; - depth_t avg_depth; - volume_t max_sac; - volume_t min_sac; - volume_t avg_sac; - int max_temp; - int min_temp; - double combined_temp; - unsigned int combined_count; - unsigned int selection_size; - unsigned int total_sac_time; - bool is_year; - bool is_trip; - char *location; -} stats_t; -extern stats_t stats_selection; -extern stats_t *stats_yearly; -extern stats_t *stats_monthly; -extern stats_t *stats_by_trip; - -extern char *get_time_string_s(int seconds, int maxdays, bool freediving); -extern char *get_minutes(int seconds); -extern void process_all_dives(struct dive *dive, struct dive **prev_dive); -extern void get_selected_dives_text(char *buffer, int size); -extern void get_gas_used(struct dive *dive, volume_t gases[MAX_CYLINDERS]); -extern void process_selected_dives(void); -void selected_dives_gas_parts(volume_t *o2_tot, volume_t *he_tot); - -inline char *get_time_string(int seconds, int maxdays) { - return get_time_string_s( seconds, maxdays, false); -} -#ifdef __cplusplus -} -#endif - -#endif // STATISTICS_H diff --git a/strndup.h b/strndup.h deleted file mode 100644 index 84e18b60f..000000000 --- a/strndup.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef STRNDUP_H -#define STRNDUP_H -#if __WIN32__ -static char *strndup (const char *s, size_t n) -{ - char *cpy; - size_t len = strlen(s); - if (n < len) - len = n; - if ((cpy = malloc(len + 1)) != - NULL) { - cpy[len] = - '\0'; - memcpy(cpy, - s, - len); - } - return cpy; -} -#endif -#endif /* STRNDUP_H */ diff --git a/strtod.c b/strtod.c deleted file mode 100644 index 81e5d42d1..000000000 --- a/strtod.c +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Sane helper for 'strtod()'. - * - * Sad that we even need this, but the C library version has - * insane locale behavior, and while the Qt "doDouble()" routines - * are better in that regard, they don't have an end pointer - * (having replaced it with the completely idiotic "ok" boolean - * pointer instead). - * - * I wonder what drugs people are on sometimes. - * - * Right now we support the following flags to limit the - * parsing some ways: - * - * STRTOD_NO_SIGN - don't accept signs - * STRTOD_NO_DOT - no decimal dots, I'm European - * STRTOD_NO_COMMA - no comma, please, I'm C locale - * STRTOD_NO_EXPONENT - no exponent parsing, I'm human - * - * The "negative" flags are so that the common case can just - * use a flag value of 0, and only if you have some special - * requirements do you need to state those with explicit flags. - * - * So if you want the C locale kind of parsing, you'd use the - * STRTOD_NO_COMMA flag to disallow a decimal comma. But if you - * want a more relaxed "Hey, Europeans are people too, even if - * they have locales with commas", just pass in a zero flag. - */ -#include -#include "dive.h" - -double strtod_flags(const char *str, const char **ptr, unsigned int flags) -{ - char c; - const char *p = str, *ep; - double val = 0.0; - double decimal = 1.0; - int sign = 0, esign = 0; - int numbers = 0, dot = 0; - - /* skip spaces */ - while (isspace(c = *p++)) - /* */; - - /* optional sign */ - if (!(flags & STRTOD_NO_SIGN)) { - switch (c) { - case '-': - sign = 1; - /* fallthrough */ - case '+': - c = *p++; - } - } - - /* Mantissa */ - for (;; c = *p++) { - if ((c == '.' && !(flags & STRTOD_NO_DOT)) || - (c == ',' && !(flags & STRTOD_NO_COMMA))) { - if (dot) - goto done; - dot = 1; - continue; - } - if (c >= '0' && c <= '9') { - numbers++; - val = (val * 10) + (c - '0'); - if (dot) - decimal *= 10; - continue; - } - if (c != 'e' && c != 'E') - goto done; - if (flags & STRTOD_NO_EXPONENT) - goto done; - break; - } - - if (!numbers) - goto done; - - /* Exponent */ - ep = p; - c = *ep++; - switch (c) { - case '-': - esign = 1; - /* fallthrough */ - case '+': - c = *ep++; - } - - if (c >= '0' && c <= '9') { - p = ep; - int exponent = c - '0'; - - for (;;) { - c = *p++; - if (c < '0' || c > '9') - break; - exponent *= 10; - exponent += c - '0'; - } - - /* We're not going to bother playing games */ - if (exponent > 308) - exponent = 308; - - while (exponent-- > 0) { - if (esign) - decimal *= 10; - else - decimal /= 10; - } - } - -done: - if (!numbers) - goto no_conversion; - if (ptr) - *ptr = p - 1; - return (sign ? -val : val) / decimal; - -no_conversion: - if (ptr) - *ptr = str; - return 0.0; -} diff --git a/subsurface-core/CMakeLists.txt b/subsurface-core/CMakeLists.txt new file mode 100644 index 000000000..e7f531527 --- /dev/null +++ b/subsurface-core/CMakeLists.txt @@ -0,0 +1,81 @@ +set(PLATFORM_SRC unknown_platform.c) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(PLATFORM_SRC linux.c) +elseif(ANDROID) + set(PLATFORM_SRC android.cpp) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(PLATFORM_SRC macos.c) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(PLATFORM_SRC windows.c) +endif() + +if(FTDISUPPORT) + set(SERIAL_FTDI serial_ftdi.c) +endif() + +if(BTSUPPORT) + add_definitions(-DBT_SUPPORT) + set(BT_SRC_FILES qt-ui/btdeviceselectiondialog.cpp) + set(BT_CORE_SRC_FILES qtserialbluetooth.cpp) +endif() + +# compile the core library, in C. +set(SUBSURFACE_CORE_LIB_SRCS + cochran.c + datatrak.c + deco.c + device.c + dive.c + divesite.c + divesite.cpp + divelist.c + equipment.c + file.c + git-access.c + libdivecomputer.c + liquivision.c + load-git.c + membuffer.c + ostctools.c + parse-xml.c + planner.c + profile.c + gaspressures.c + worldmap-save.c + save-git.c + save-xml.c + save-html.c + sha1.c + statistics.c + strtod.c + subsurfacestartup.c + time.c + uemis.c + uemis-downloader.c + version.c + # gettextfrommoc should be added because we are using it on the c-code. + gettextfromc.cpp + # dirk ported some core functionality to c++. + qthelper.cpp + divecomputer.cpp + exif.cpp + subsurfacesysinfo.cpp + devicedetails.cpp + configuredivecomputer.cpp + configuredivecomputerthreads.cpp + divesitehelpers.cpp + taxonomy.c + checkcloudconnection.cpp + windowtitleupdate.cpp + divelogexportlogic.cpp + qt-init.cpp + qtserialbluetooth.cpp + ${SERIAL_FTDI} + ${PLATFORM_SRC} + ${BT_CORE_SRC_FILES} +) +source_group("Subsurface Core" FILES ${SUBSURFACE_CORE_LIB_SRCS}) + +add_library(subsurface_corelib STATIC ${SUBSURFACE_CORE_LIB_SRCS} ) +target_link_libraries(subsurface_corelib ${QT_LIBRARIES}) + diff --git a/subsurface-core/android.cpp b/subsurface-core/android.cpp new file mode 100644 index 000000000..3e14bec02 --- /dev/null +++ b/subsurface-core/android.cpp @@ -0,0 +1,205 @@ +/* implements Android specific functions */ +#include "dive.h" +#include "display.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define FTDI_VID 0x0403 +#define USB_SERVICE "usb" + +extern "C" { + +const char android_system_divelist_default_font[] = "Roboto"; +const char *system_divelist_default_font = android_system_divelist_default_font; +double system_divelist_default_font_size = 8.0; + +int get_usb_fd(uint16_t idVendor, uint16_t idProduct); +void subsurface_OS_pref_setup(void) +{ + // Abusing this function to get a decent place where we can wire in + // our open callback into libusb +#ifdef libusb_android_open_callback_func + libusb_set_android_open_callback(get_usb_fd); +#elif __ANDROID__ +#error we need libusb_android_open_callback +#endif +} + +bool subsurface_ignore_font(const char *font) +{ + // there are no old default fonts that we would want to ignore + return false; +} + +void subsurface_user_info(struct user_info *user) +{ /* Encourage use of at least libgit2-0.20 */ } + +static const char *system_default_path_append(const char *append) +{ + /* Replace this when QtCore/QStandardPaths getExternalStorageDirectory landed */ + QAndroidJniObject externalStorage = QAndroidJniObject::callStaticObjectMethod("android/os/Environment", "getExternalStorageDirectory", "()Ljava/io/File;"); + QAndroidJniObject externalStorageAbsolute = externalStorage.callObjectMethod("getAbsolutePath", "()Ljava/lang/String;"); + QString path = externalStorageAbsolute.toString(); + QAndroidJniEnvironment env; + if (env->ExceptionCheck()) { + // FIXME: Handle exception here. + env->ExceptionClear(); + path = QString("/sdcard"); + } + if (append) + path += QString("/%1").arg(append); + return strdup(path.toUtf8().data()); +} + +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +const char *system_default_filename(void) +{ + static const char *filename = "subsurface.xml"; + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + /* FIXME: we need to enumerate in some other way on android */ + /* qtserialport maybee? */ + return -1; +} + +/** + * Get the file descriptor of first available matching device attached to usb in android. + * + * returns a fd to the device, or -1 and errno is set. + */ +int get_usb_fd(uint16_t idVendor, uint16_t idProduct) +{ + int i; + jint fd, vendorid, productid; + QAndroidJniObject usbName, usbDevice; + + // Get the current main activity of the application. + QAndroidJniObject activity = QtAndroid::androidActivity(); + + QAndroidJniObject usb_service = QAndroidJniObject::fromString(USB_SERVICE); + + // Get UsbManager from activity + QAndroidJniObject usbManager = activity.callObjectMethod("getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", usb_service.object()); + + // Get a HashMap of all USB devices attached to Android + QAndroidJniObject deviceMap = usbManager.callObjectMethod("getDeviceList", "()Ljava/util/HashMap;"); + jint num_devices = deviceMap.callMethod("size", "()I"); + if (num_devices == 0) { + // No USB device is attached. + return -1; + } + + // Iterate over all the devices and find the first available FTDI device. + QAndroidJniObject keySet = deviceMap.callObjectMethod("keySet", "()Ljava/util/Set;"); + QAndroidJniObject iterator = keySet.callObjectMethod("iterator", "()Ljava/util/Iterator;"); + + for (i = 0; i < num_devices; i++) { + usbName = iterator.callObjectMethod("next", "()Ljava/lang/Object;"); + usbDevice = deviceMap.callObjectMethod ("get", "(Ljava/lang/Object;)Ljava/lang/Object;", usbName.object()); + vendorid = usbDevice.callMethod("getVendorId", "()I"); + productid = usbDevice.callMethod("getProductId", "()I"); + if(vendorid == idVendor && productid == idProduct) // Found the requested device + break; + } + if (i == num_devices) { + // No device found. + errno = ENOENT; + return -1; + } + + jboolean hasPermission = usbManager.callMethod("hasPermission", "(Landroid/hardware/usb/UsbDevice;)Z", usbDevice.object()); + if (!hasPermission) { + // You do not have permission to use the usbDevice. + // Please remove and reinsert the USB device. + // Could also give an dialogbox asking for permission. + errno = EPERM; + return -1; + } + + // An device is present and we also have permission to use the device. + // Open the device and get its file descriptor. + QAndroidJniObject usbDeviceConnection = usbManager.callObjectMethod("openDevice", "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/UsbDeviceConnection;", usbDevice.object()); + if (usbDeviceConnection.object() == NULL) { + // Some error occurred while opening the device. Exit. + errno = EINVAL; + return -1; + } + + // Finally get the required file descriptor. + fd = usbDeviceConnection.callMethod("getFileDescriptor", "()I"); + if (fd == -1) { + // The device is not opened. Some error. + errno = ENODEV; + return -1; + } + return fd; +} + +/* NOP wrappers to comform with windows.c */ +int subsurface_rename(const char *path, const char *newpath) +{ + return rename(path, newpath); +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + return open(path, oflags, mode); +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + return fopen(path, mode); +} + +void *subsurface_opendir(const char *path) +{ + return (void *)opendir(path); +} + +int subsurface_access(const char *path, int mode) +{ + return access(path, mode); +} + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ + return zip_open(path, flags, errorp); +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +void subsurface_console_init(bool dedicated) +{ + /* NOP */ +} + +void subsurface_console_exit(void) +{ + /* NOP */ +} +} diff --git a/subsurface-core/checkcloudconnection.cpp b/subsurface-core/checkcloudconnection.cpp new file mode 100644 index 000000000..be2a2fa18 --- /dev/null +++ b/subsurface-core/checkcloudconnection.cpp @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include + +#include "pref.h" +#include "helpers.h" + +#include "checkcloudconnection.h" + + +CheckCloudConnection::CheckCloudConnection(QObject *parent) : + QObject(parent), + reply(0) +{ + +} + +#define TEAPOT "/make-latte?number-of-shots=3" +#define HTTP_I_AM_A_TEAPOT 418 +#define MILK "Linus does not like non-fat milk" +bool CheckCloudConnection::checkServer() +{ + QTimer timer; + timer.setSingleShot(true); + QEventLoop loop; + QNetworkRequest request; + request.setRawHeader("Accept", "text/plain"); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + request.setUrl(QString(prefs.cloud_base_url) + TEAPOT); + QNetworkAccessManager *mgr = new QNetworkAccessManager(); + reply = mgr->get(request); + connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); + timer.start(5000); // wait five seconds + loop.exec(); + if (timer.isActive()) { + // didn't time out, did we get the right response? + timer.stop(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == HTTP_I_AM_A_TEAPOT && + reply->readAll() == QByteArray(MILK)) { + reply->deleteLater(); + mgr->deleteLater(); + if (verbose > 1) + qWarning() << "Cloud storage: successfully checked connection to cloud server"; + return true; + } + } else { + disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + reply->abort(); + } + if (verbose) + qDebug() << "connection test to cloud server failed" << + reply->error() << reply->errorString() << + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << + reply->readAll(); + reply->deleteLater(); + mgr->deleteLater(); + if (verbose) + qWarning() << "Cloud storage: unable to connect to cloud server"; + return false; +} + +void CheckCloudConnection::sslErrors(QList errorList) +{ + if (verbose) { + qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; + Q_FOREACH (QSslError err, errorList) { + qDebug() << err.errorString(); + } + } + QSslConfiguration conf = reply->sslConfiguration(); + QSslCertificate cert = conf.peerCertificate(); + QByteArray hexDigest = cert.digest().toHex(); + if (reply->url().toString().contains(prefs.cloud_base_url) && + hexDigest == "13ff44c62996cfa5cd69d6810675490e") { + if (verbose) + qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; + reply->ignoreSslErrors(); + } else { + if (verbose) + qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; + } +} + +// helper to be used from C code +extern "C" bool canReachCloudServer() +{ + if (verbose) + qWarning() << "Cloud storage: checking connection to cloud server"; + CheckCloudConnection *checker = new CheckCloudConnection; + return checker->checkServer(); +} diff --git a/subsurface-core/checkcloudconnection.h b/subsurface-core/checkcloudconnection.h new file mode 100644 index 000000000..58a412797 --- /dev/null +++ b/subsurface-core/checkcloudconnection.h @@ -0,0 +1,22 @@ +#ifndef CHECKCLOUDCONNECTION_H +#define CHECKCLOUDCONNECTION_H + +#include +#include +#include + +#include "checkcloudconnection.h" + +class CheckCloudConnection : public QObject { + Q_OBJECT +public: + CheckCloudConnection(QObject *parent = 0); + bool checkServer(); +private: + QNetworkReply *reply; +private +slots: + void sslErrors(QList errorList); +}; + +#endif // CHECKCLOUDCONNECTION_H diff --git a/subsurface-core/cochran.c b/subsurface-core/cochran.c new file mode 100644 index 000000000..267fe2b4a --- /dev/null +++ b/subsurface-core/cochran.c @@ -0,0 +1,805 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "dive.h" +#include "file.h" +#include "units.h" +#include "gettext.h" +#include "cochran.h" +#include "divelist.h" + +#include + +#define POUND 0.45359237 +#define FEET 0.3048 +#define INCH 0.0254 +#define GRAVITY 9.80665 +#define ATM 101325.0 +#define BAR 100000.0 +#define FSW (ATM / 33.0) +#define MSW (BAR / 10.0) +#define PSI ((POUND * GRAVITY) / (INCH * INCH)) + +// Some say 0x4a14 and 0x4b14 are the right number for this offset +// This works with CAN files from Analyst 4.01v and computers +// such as Commander, Gemini, EMC-16, and EMC-20H +#define LOG_ENTRY_OFFSET 0x4914 + +enum cochran_type { + TYPE_GEMINI, + TYPE_COMMANDER, + TYPE_EMC +}; + +struct config { + enum cochran_type type; + unsigned int logbook_size; + unsigned int sample_size; +} config; + + +// Convert 4 bytes into an INT +#define array_uint16_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) ) +#define array_uint32_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) + ((p)[2]<<16) \ + + ((p)[3]<<24)) + +/* + * The Cochran file format is designed to be annoying to read. It's roughly: + * + * 0x00000: room for 65534 4-byte words, giving the starting offsets + * of the dives themselves. + * + * 0x3fff8: the size of the file + 1 + * 0x3ffff: 0 (high 32 bits of filesize? Bogus: the offsets into the file + * are 32-bit, so it can't be a large file anyway) + * + * 0x40000: byte 0x46 + * 0x40001: "block 0": 256 byte encryption key + * 0x40101: the random modulus, or length of the key to use + * 0x40102: block 1: Version and date of Analyst and a feature string identifying + * the computer features and the features of the file + * 0x40138: Computer configuration page 1, 512 bytes + * 0x40338: Computer configuration page 2, 512 bytes + * 0x40538: Misc data (tissues) 1500 bytes + * 0x40b14: Ownership data 512 bytes ??? + * + * 0x4171c: Ownership data 512 bytes ??? + * + * 0x45415: Time stamp 17 bytes + * 0x45426: Computer configuration page 1, 512 bytes + * 0x45626: Computer configuration page 2, 512 bytes + * + */ +static unsigned int partial_decode(unsigned int start, unsigned int end, + const unsigned char *decode, unsigned offset, unsigned mod, + const unsigned char *buf, unsigned int size, unsigned char *dst) +{ + unsigned i, sum = 0; + + for (i = start; i < end; i++) { + unsigned char d = decode[offset++]; + if (i >= size) + break; + if (offset == mod) + offset = 0; + d += buf[i]; + if (dst) + dst[i] = d; + sum += d; + } + return sum; +} + +#ifdef COCHRAN_DEBUG + +#define hexchar(n) ("0123456789abcdef"[(n) & 15]) + +static int show_line(unsigned offset, const unsigned char *data, + unsigned size, int show_empty) +{ + unsigned char bits; + int i, off; + char buffer[120]; + + if (size > 16) + size = 16; + + bits = 0; + memset(buffer, ' ', sizeof(buffer)); + off = sprintf(buffer, "%06x ", offset); + for (i = 0; i < size; i++) { + char *hex = buffer + off + 3 * i; + char *asc = buffer + off + 50 + i; + unsigned char byte = data[i]; + + hex[0] = hexchar(byte >> 4); + hex[1] = hexchar(byte); + bits |= byte; + if (byte < 32 || byte > 126) + byte = '.'; + asc[0] = byte; + asc[1] = 0; + } + + if (bits) { + puts(buffer); + return 1; + } + if (show_empty) + puts("..."); + return 0; +} + +static void cochran_debug_write(const unsigned char *data, unsigned size) +{ + return; + + int show = 1, i; + for (i = 0; i < size; i += 16) + show = show_line(i, data + i, size - i, show); +} + +static void cochran_debug_sample(const char *s, unsigned int seconds) +{ + switch (config.type) { + case TYPE_GEMINI: + switch (seconds % 4) { + case 0: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 1: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 2: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 3: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + } + break; + case TYPE_COMMANDER: + switch (seconds % 2) { + case 0: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + case 1: + printf("Hex: %02x %02x ", s[0], s[1]); + break; + } + break; + case TYPE_EMC: + switch (seconds % 2) { + case 0: + printf("Hex: %02x %02x %02x ", s[0], s[1], s[2]); + break; + case 1: + printf("Hex: %02x %02x %02x ", s[0], s[1], s[2]); + break; + } + break; + } + + printf ("%02dh %02dm %02ds: Depth: %-5.2f, ", seconds / 3660, + (seconds % 3660) / 60, seconds % 60, depth); +} + +#endif // COCHRAN_DEBUG + +static void cochran_parse_header(const unsigned char *decode, unsigned mod, + const unsigned char *in, unsigned size) +{ + unsigned char *buf = malloc(size); + + /* Do the "null decode" using a one-byte decode array of '\0' */ + /* Copies in plaintext, will be overwritten later */ + partial_decode(0, 0x0102, (const unsigned char *)"", 0, 1, in, size, buf); + + /* + * The header scrambling is different form the dive + * scrambling. Oh yay! + */ + partial_decode(0x0102, 0x010e, decode, 0, mod, in, size, buf); + partial_decode(0x010e, 0x0b14, decode, 0, mod, in, size, buf); + partial_decode(0x0b14, 0x1b14, decode, 0, mod, in, size, buf); + partial_decode(0x1b14, 0x2b14, decode, 0, mod, in, size, buf); + partial_decode(0x2b14, 0x3b14, decode, 0, mod, in, size, buf); + partial_decode(0x3b14, 0x5414, decode, 0, mod, in, size, buf); + partial_decode(0x5414, size, decode, 0, mod, in, size, buf); + + // Detect log type + switch (buf[0x133]) { + case '2': // Cochran Commander, version II log format + config.logbook_size = 256; + if (buf[0x132] == 0x10) { + config.type = TYPE_GEMINI; + config.sample_size = 2; // Gemini with tank PSI samples + } else { + config.type = TYPE_COMMANDER; + config.sample_size = 2; // Commander + } + break; + case '3': // Cochran EMC, version III log format + config.type = TYPE_EMC; + config.logbook_size = 512; + config.sample_size = 3; + break; + default: + printf ("Unknown log format v%c\n", buf[0x137]); + free(buf); + exit(1); + break; + } + +#ifdef COCHRAN_DEBUG + puts("Header\n======\n\n"); + cochran_debug_write(buf, size); +#endif + + free(buf); +} + +/* +* Bytes expected after a pre-dive event code +*/ +static int cochran_predive_event_bytes(unsigned char code) +{ + int x = 0; + int gem_event_bytes[15][2] = {{0x00, 10}, {0x02, 17}, {0x08, 18}, + {0x09, 18}, {0x0c, 18}, {0x0d, 18}, + {0x0e, 18}, + {-1, 0}}; + int cmdr_event_bytes[15][2] = {{0x00, 16}, {0x01, 20}, {0x02, 17}, + {0x03, 16}, {0x06, 18}, {0x07, 18}, + {0x08, 18}, {0x09, 18}, {0x0a, 18}, + {0x0b, 20}, {0x0c, 18}, {0x0d, 18}, + {0x0e, 18}, {0x10, 20}, + {-1, 0}}; + int emc_event_bytes[15][2] = {{0x00, 18}, {0x01, 22}, {0x02, 19}, + {0x03, 18}, {0x06, 20}, {0x07, 20}, + {0x0a, 20}, {0x0b, 20}, {0x0f, 18}, + {0x10, 20}, + {-1, 0}}; + + switch (config.type) { + case TYPE_GEMINI: + while (gem_event_bytes[x][0] != code && gem_event_bytes[x][0] != -1) + x++; + return gem_event_bytes[x][1]; + break; + case TYPE_COMMANDER: + while (cmdr_event_bytes[x][0] != code && cmdr_event_bytes[x][0] != -1) + x++; + return cmdr_event_bytes[x][1]; + break; + case TYPE_EMC: + while (emc_event_bytes[x][0] != code && emc_event_bytes[x][0] != -1) + x++; + return emc_event_bytes[x][1]; + break; + } + + return 0; +} + +int cochran_dive_event_bytes(unsigned char event) +{ + return (event == 0xAD || event == 0xAB) ? 4 : 0; +} + +static void cochran_dive_event(struct divecomputer *dc, const unsigned char *s, + unsigned int seconds, unsigned int *in_deco, + unsigned int *deco_ceiling, unsigned int *deco_time) +{ + switch (s[0]) { + case 0xC5: // Deco obligation begins + *in_deco = 1; + add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); + break; + case 0xDB: // Deco obligation ends + *in_deco = 0; + add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); + break; + case 0xAD: // Raise deco ceiling 10 ft + *deco_ceiling -= 10; // ft + *deco_time = (array_uint16_le(s + 3) + 1) * 60; + break; + case 0xAB: // Lower deco ceiling 10 ft + *deco_ceiling += 10; // ft + *deco_time = (array_uint16_le(s + 3) + 1) * 60; + break; + case 0xA8: // Entered Post Dive interval mode (surfaced) + break; + case 0xA9: // Exited PDI mode (re-submierged) + break; + case 0xBD: // Switched to normal PO2 setting + break; + case 0xC0: // Switched to FO2 21% mode (generally upon surface) + break; + case 0xC1: // "Ascent rate alarm + add_event(dc, seconds, SAMPLE_EVENT_ASCENT, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ascent")); + break; + case 0xC2: // Low battery warning +#ifdef SAMPLE_EVENT_BATTERY + add_event(dc, seconds, SAMPLE_EVENT_BATTERY, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "battery")); +#endif + break; + case 0xC3: // CNS warning + add_event(dc, seconds, SAMPLE_EVENT_OLF, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "OLF")); + break; + case 0xC4: // Depth alarm begin + add_event(dc, seconds, SAMPLE_EVENT_MAXDEPTH, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "maxdepth")); + break; + case 0xC8: // PPO2 alarm begin + add_event(dc, seconds, SAMPLE_EVENT_PO2, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚")); + break; + case 0xCC: // Low cylinder 1 pressure"; + break; + case 0xCD: // Switch to deco blend setting + add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + break; + case 0xCE: // NDL alarm begin + add_event(dc, seconds, SAMPLE_EVENT_RBT, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "rbt")); + break; + case 0xD0: // Breathing rate alarm begin + break; + case 0xD3: // Low gas 1 flow rate alarm begin"; + break; + case 0xD6: // Ceiling alarm begin + add_event(dc, seconds, SAMPLE_EVENT_CEILING, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ceiling")); + break; + case 0xD8: // End decompression mode + *in_deco = 0; + add_event(dc, seconds, SAMPLE_EVENT_DECOSTOP, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "deco stop")); + break; + case 0xE1: // Ascent alarm end + add_event(dc, seconds, SAMPLE_EVENT_ASCENT, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ascent")); + break; + case 0xE2: // Low transmitter battery alarm + add_event(dc, seconds, SAMPLE_EVENT_TRANSMITTER, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "transmitter")); + break; + case 0xE3: // Switch to FO2 mode + break; + case 0xE5: // Switched to PO2 mode + break; + case 0xE8: // PO2 too low alarm + add_event(dc, seconds, SAMPLE_EVENT_PO2, + SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚")); + break; + case 0xEE: // NDL alarm end + add_event(dc, seconds, SAMPLE_EVENT_RBT, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "rbt")); + break; + case 0xEF: // Switch to blend 2 + add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + break; + case 0xF0: // Breathing rate alarm end + break; + case 0xF3: // Switch to blend 1 (often at dive start) + add_event(dc, seconds, SAMPLE_EVENT_GASCHANGE, + SAMPLE_FLAGS_NONE, 0, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + break; + case 0xF6: // Ceiling alarm end + add_event(dc, seconds, SAMPLE_EVENT_CEILING, + SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ceiling")); + break; + default: + break; + } +} + +/* +* Parse sample data, extract events and build a dive +*/ +static void cochran_parse_samples(struct dive *dive, const unsigned char *log, + const unsigned char *samples, int size, + unsigned int *duration, double *max_depth, + double *avg_depth, double *min_temp) +{ + const unsigned char *s; + unsigned int offset = 0, seconds = 0; + double depth = 0, temp = 0, depth_sample = 0, psi = 0, sgc_rate = 0; + int ascent_rate = 0; + unsigned int ndl = 0; + unsigned int in_deco = 0, deco_ceiling = 0, deco_time = 0; + + struct divecomputer *dc = &dive->dc; + struct sample *sample; + + // Initialize stat variables + *max_depth = 0, *avg_depth = 0, *min_temp = 0xFF; + + // Get starting depth and temp (tank PSI???) + switch (config.type) { + case TYPE_GEMINI: + depth = (float) (log[CMD_START_DEPTH] + + log[CMD_START_DEPTH + 1] * 256) / 4; + temp = log[CMD_START_TEMP]; + psi = log[CMD_START_PSI] + log[CMD_START_PSI + 1] * 256; + sgc_rate = (float)(log[CMD_START_SGC] + + log[CMD_START_SGC + 1] * 256) / 2; + break; + case TYPE_COMMANDER: + depth = (float) (log[CMD_START_DEPTH] + + log[CMD_START_DEPTH + 1] * 256) / 4; + temp = log[CMD_START_TEMP]; + break; + + case TYPE_EMC: + depth = (float) log [EMC_START_DEPTH] / 256 + + log[EMC_START_DEPTH + 1]; + temp = log[EMC_START_TEMP]; + break; + } + + // Skip past pre-dive events + unsigned int x = 0; + if (samples[x] != 0x40) { + unsigned int c; + while ((samples[x] & 0x80) == 0 && samples[x] != 0x40 && x < size) { + c = cochran_predive_event_bytes(samples[x]) + 1; +#ifdef COCHRAN_DEBUG + printf("Predive event: ", samples[x]); + for (int y = 0; y < c; y++) printf("%02x ", samples[x + y]); + putchar('\n'); +#endif + x += c; + } + } + + // Now process samples + offset = x; + while (offset < size) { + s = samples + offset; + + // Start with an empty sample + sample = prepare_sample(dc); + sample->time.seconds = seconds; + + // Check for event + if (s[0] & 0x80) { + cochran_dive_event(dc, s, seconds, &in_deco, &deco_ceiling, &deco_time); + offset += cochran_dive_event_bytes(s[0]) + 1; + continue; + } + + // Depth is in every sample + depth_sample = (float)(s[0] & 0x3F) / 4 * (s[0] & 0x40 ? -1 : 1); + depth += depth_sample; + +#ifdef COCHRAN_DEBUG + cochran_debug_sample(s, seconds); +#endif + + switch (config.type) { + case TYPE_COMMANDER: + switch (seconds % 2) { + case 0: // Ascent rate + ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1: -1); + break; + case 1: // Temperature + temp = s[1] / 2 + 20; + break; + } + break; + case TYPE_GEMINI: + // Gemini with tank pressure and SAC rate. + switch (seconds % 4) { + case 0: // Ascent rate + ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1); + break; + case 2: // PSI change + psi -= (float)(s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1) / 4; + break; + case 1: // SGC rate + sgc_rate -= (float)(s[1] & 0x7f) * (s[1] & 0x80 ? 1 : -1) / 2; + break; + case 3: // Temperature + temp = (float)s[1] / 2 + 20; + break; + } + break; + case TYPE_EMC: + switch (seconds % 2) { + case 0: // Ascent rate + ascent_rate = (s[1] & 0x7f) * (s[1] & 0x80 ? 1: -1); + break; + case 1: // Temperature + temp = (float)s[1] / 2 + 20; + break; + } + // Get NDL and deco information + switch (seconds % 24) { + case 20: + if (in_deco) { + // Fist stop time + //first_deco_time = (s[2] + s[5] * 256 + 1) * 60; // seconds + ndl = 0; + } else { + // NDL + ndl = (s[2] + s[5] * 256 + 1) * 60; // seconds + deco_time = 0; + } + break; + case 22: + if (in_deco) { + // Total stop time + deco_time = (s[2] + s[5] * 256 + 1) * 60; // seconds + ndl = 0; + } + break; + } + } + + // Track dive stats + if (depth > *max_depth) *max_depth = depth; + if (temp < *min_temp) *min_temp = temp; + *avg_depth = (*avg_depth * seconds + depth) / (seconds + 1); + + sample->depth.mm = depth * FEET * 1000; + sample->ndl.seconds = ndl; + sample->in_deco = in_deco; + sample->stoptime.seconds = deco_time; + sample->stopdepth.mm = deco_ceiling * FEET * 1000; + sample->temperature.mkelvin = C_to_mkelvin((temp - 32) / 1.8); + sample->sensor = 0; + sample->cylinderpressure.mbar = psi * PSI / 100; + + finish_sample(dc); + + offset += config.sample_size; + seconds++; + } + (void)ascent_rate; // mark the variable as unused + + if (seconds > 0) + *duration = seconds - 1; +} + +static void cochran_parse_dive(const unsigned char *decode, unsigned mod, + const unsigned char *in, unsigned size) +{ + unsigned char *buf = malloc(size); + struct dive *dive; + struct divecomputer *dc; + struct tm tm = {0}; + uint32_t csum[5]; + + double max_depth, avg_depth, min_temp; + unsigned int duration = 0, corrupt_dive = 0; + + /* + * The scrambling has odd boundaries. I think the boundaries + * match some data structure size, but I don't know. They were + * discovered the same way we dynamically discover the decode + * size: automatically looking for least random output. + * + * The boundaries are also this confused "off-by-one" thing, + * the same way the file size is off by one. It's as if the + * cochran software forgot to write one byte at the beginning. + */ + partial_decode(0, 0x0fff, decode, 1, mod, in, size, buf); + partial_decode(0x0fff, 0x1fff, decode, 0, mod, in, size, buf); + partial_decode(0x1fff, 0x2fff, decode, 0, mod, in, size, buf); + partial_decode(0x2fff, 0x48ff, decode, 0, mod, in, size, buf); + + /* + * This is not all the descrambling you need - the above are just + * what appears to be the fixed-size blocks. The rest is also + * scrambled, but there seems to be size differences in the data, + * so this just descrambles part of it: + */ + // Decode log entry (512 bytes + random prefix) + partial_decode(0x48ff, 0x4914 + config.logbook_size, decode, + 0, mod, in, size, buf); + + unsigned int sample_size = size - 0x4914 - config.logbook_size; + int g; + + // Decode sample data + partial_decode(0x4914 + config.logbook_size, size, decode, + 0, mod, in, size, buf); + +#ifdef COCHRAN_DEBUG + // Display pre-logbook data + puts("\nPre Logbook Data\n"); + cochran_debug_write(buf, 0x4914); + + // Display log book + puts("\nLogbook Data\n"); + cochran_debug_write(buf + 0x4914, config.logbook_size + 0x400); + + // Display sample data + puts("\nSample Data\n"); +#endif + + dive = alloc_dive(); + dc = &dive->dc; + + unsigned char *log = (buf + 0x4914); + + switch (config.type) { + case TYPE_GEMINI: + case TYPE_COMMANDER: + if (config.type == TYPE_GEMINI) { + dc->model = "Gemini"; + dc->deviceid = buf[0x18c] * 256 + buf[0x18d]; // serial no + fill_default_cylinder(&dive->cylinder[0]); + dive->cylinder[0].gasmix.o2.permille = (log[CMD_O2_PERCENT] / 256 + + log[CMD_O2_PERCENT + 1]) * 10; + dive->cylinder[0].gasmix.he.permille = 0; + } else { + dc->model = "Commander"; + dc->deviceid = array_uint32_le(buf + 0x31e); // serial no + for (g = 0; g < 2; g++) { + fill_default_cylinder(&dive->cylinder[g]); + dive->cylinder[g].gasmix.o2.permille = (log[CMD_O2_PERCENT + g * 2] / 256 + + log[CMD_O2_PERCENT + g * 2 + 1]) * 10; + dive->cylinder[g].gasmix.he.permille = 0; + } + } + + tm.tm_year = log[CMD_YEAR]; + tm.tm_mon = log[CMD_MON] - 1; + tm.tm_mday = log[CMD_DAY]; + tm.tm_hour = log[CMD_HOUR]; + tm.tm_min = log[CMD_MIN]; + tm.tm_sec = log[CMD_SEC]; + tm.tm_isdst = -1; + + dive->when = dc->when = utc_mktime(&tm); + dive->number = log[CMD_NUMBER] + log[CMD_NUMBER + 1] * 256 + 1; + dc->duration.seconds = (log[CMD_BT] + log[CMD_BT + 1] * 256) * 60; + dc->surfacetime.seconds = (log[CMD_SIT] + log[CMD_SIT + 1] * 256) * 60; + dc->maxdepth.mm = (log[CMD_MAX_DEPTH] + + log[CMD_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->meandepth.mm = (log[CMD_AVG_DEPTH] + + log[CMD_AVG_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->watertemp.mkelvin = C_to_mkelvin((log[CMD_MIN_TEMP] / 32) - 1.8); + dc->surface_pressure.mbar = ATM / BAR * pow(1 - 0.0000225577 + * (double) log[CMD_ALTITUDE] * 250 * FEET, 5.25588) * 1000; + dc->salinity = 10000 + 150 * log[CMD_WATER_CONDUCTIVITY]; + + SHA1(log + CMD_NUMBER, 2, (unsigned char *)csum); + dc->diveid = csum[0]; + + if (log[CMD_MAX_DEPTH] == 0xff && log[CMD_MAX_DEPTH + 1] == 0xff) + corrupt_dive = 1; + + break; + case TYPE_EMC: + dc->model = "EMC"; + dc->deviceid = array_uint32_le(buf + 0x31e); // serial no + for (g = 0; g < 4; g++) { + fill_default_cylinder(&dive->cylinder[g]); + dive->cylinder[g].gasmix.o2.permille = + (log[EMC_O2_PERCENT + g * 2] / 256 + + log[EMC_O2_PERCENT + g * 2 + 1]) * 10; + dive->cylinder[g].gasmix.he.permille = + (log[EMC_HE_PERCENT + g * 2] / 256 + + log[EMC_HE_PERCENT + g * 2 + 1]) * 10; + } + + tm.tm_year = log[EMC_YEAR]; + tm.tm_mon = log[EMC_MON] - 1; + tm.tm_mday = log[EMC_DAY]; + tm.tm_hour = log[EMC_HOUR]; + tm.tm_min = log[EMC_MIN]; + tm.tm_sec = log[EMC_SEC]; + tm.tm_isdst = -1; + + dive->when = dc->when = utc_mktime(&tm); + dive->number = log[EMC_NUMBER] + log[EMC_NUMBER + 1] * 256 + 1; + dc->duration.seconds = (log[EMC_BT] + log[EMC_BT + 1] * 256) * 60; + dc->surfacetime.seconds = (log[EMC_SIT] + log[EMC_SIT + 1] * 256) * 60; + dc->maxdepth.mm = (log[EMC_MAX_DEPTH] + + log[EMC_MAX_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->meandepth.mm = (log[EMC_AVG_DEPTH] + + log[EMC_AVG_DEPTH + 1] * 256) / 4 * FEET * 1000; + dc->watertemp.mkelvin = C_to_mkelvin((log[EMC_MIN_TEMP] - 32) / 1.8); + dc->surface_pressure.mbar = ATM / BAR * pow(1 - 0.0000225577 + * (double) log[EMC_ALTITUDE] * 250 * FEET, 5.25588) * 1000; + dc->salinity = 10000 + 150 * (log[EMC_WATER_CONDUCTIVITY] & 0x3); + + SHA1(log + EMC_NUMBER, 2, (unsigned char *)csum); + dc->diveid = csum[0]; + + if (log[EMC_MAX_DEPTH] == 0xff && log[EMC_MAX_DEPTH + 1] == 0xff) + corrupt_dive = 1; + + break; + } + + cochran_parse_samples(dive, buf + 0x4914, buf + 0x4914 + + config.logbook_size, sample_size, + &duration, &max_depth, &avg_depth, &min_temp); + + // Check for corrupt dive + if (corrupt_dive) { + dc->maxdepth.mm = max_depth * FEET * 1000; + dc->meandepth.mm = avg_depth * FEET * 1000; + dc->watertemp.mkelvin = C_to_mkelvin((min_temp - 32) / 1.8); + dc->duration.seconds = duration; + } + + dive->downloaded = true; + record_dive(dive); + mark_divelist_changed(true); + + free(buf); +} + +int try_to_open_cochran(const char *filename, struct memblock *mem) +{ + unsigned int i; + unsigned int mod; + unsigned int *offsets, dive1, dive2; + unsigned char *decode = mem->buffer + 0x40001; + + if (mem->size < 0x40000) + return 0; + + offsets = (unsigned int *) mem->buffer; + dive1 = offsets[0]; + dive2 = offsets[1]; + + if (dive1 < 0x40000 || dive2 < dive1 || dive2 > mem->size) + return 0; + + mod = decode[0x100] + 1; + cochran_parse_header(decode, mod, mem->buffer + 0x40000, dive1 - 0x40000); + + // Decode each dive + for (i = 0; i < 65534; i++) { + dive1 = offsets[i]; + dive2 = offsets[i + 1]; + if (dive2 < dive1) + break; + if (dive2 > mem->size) + break; + + cochran_parse_dive(decode, mod, mem->buffer + dive1, + dive2 - dive1); + } + + return 1; // no further processing needed +} diff --git a/subsurface-core/cochran.h b/subsurface-core/cochran.h new file mode 100644 index 000000000..97d4361c8 --- /dev/null +++ b/subsurface-core/cochran.h @@ -0,0 +1,44 @@ +// Commander log fields +#define CMD_SEC 1 +#define CMD_MIN 0 +#define CMD_HOUR 3 +#define CMD_DAY 2 +#define CMD_MON 5 +#define CMD_YEAR 4 +#define CME_START_OFFSET 6 // 4 bytes +#define CMD_WATER_CONDUCTIVITY 25 // 1 byte, 0=low, 2=high +#define CMD_START_SGC 42 // 2 bytes +#define CMD_START_TEMP 45 // 1 byte, F +#define CMD_START_DEPTH 56 // 2 bytes, /4=ft +#define CMD_START_PSI 62 +#define CMD_SIT 68 // 2 bytes, minutes +#define CMD_NUMBER 70 // 2 bytes +#define CMD_ALTITUDE 73 // 1 byte, /4=Kilofeet +#define CMD_END_OFFSET 128 // 4 bytes +#define CMD_MIN_TEMP 153 // 1 byte, F +#define CMD_BT 166 // 2 bytes, minutes +#define CMD_MAX_DEPTH 168 // 2 bytes, /4=ft +#define CMD_AVG_DEPTH 170 // 2 bytes, /4=ft +#define CMD_O2_PERCENT 210 // 8 bytes, 4 x 2 byte, /256=% + +// EMC log fields +#define EMC_SEC 0 +#define EMC_MIN 1 +#define EMC_HOUR 2 +#define EMC_DAY 3 +#define EMC_MON 4 +#define EMC_YEAR 5 +#define EMC_START_OFFSET 6 // 4 bytes +#define EMC_WATER_CONDUCTIVITY 24 // 1 byte bits 0:1, 0=low, 2=high +#define EMC_START_DEPTH 42 // 2 byte, /256=ft +#define EMC_START_TEMP 55 // 1 byte, F +#define EMC_SIT 84 // 2 bytes, minutes, LE +#define EMC_NUMBER 86 // 2 bytes +#define EMC_ALTITUDE 89 // 1 byte, /4=Kilofeet +#define EMC_O2_PERCENT 144 // 20 bytes, 10 x 2 bytes, /256=% +#define EMC_HE_PERCENT 164 // 20 bytes, 10 x 2 bytes, /256=% +#define EMC_END_OFFSET 256 // 4 bytes +#define EMC_MIN_TEMP 293 // 1 byte, F +#define EMC_BT 304 // 2 bytes, minutes +#define EMC_MAX_DEPTH 306 // 2 bytes, /4=ft +#define EMC_AVG_DEPTH 310 // 2 bytes, /4=ft diff --git a/subsurface-core/color.h b/subsurface-core/color.h new file mode 100644 index 000000000..7938e59a6 --- /dev/null +++ b/subsurface-core/color.h @@ -0,0 +1,67 @@ +#ifndef COLOR_H +#define COLOR_H + +/* The colors are named by picking the closest match + from http://chir.ag/projects/name-that-color */ + +#include + +// Greens +#define CAMARONE1 QColor::fromRgbF(0.0, 0.4, 0.0, 1) +#define FUNGREEN1 QColor::fromRgbF(0.0, 0.4, 0.2, 1) +#define FUNGREEN1_HIGH_TRANS QColor::fromRgbF(0.0, 0.4, 0.2, 0.25) +#define KILLARNEY1 QColor::fromRgbF(0.2, 0.4, 0.2, 1) +#define APPLE1 QColor::fromRgbF(0.2, 0.6, 0.2, 1) +#define APPLE1_MED_TRANS QColor::fromRgbF(0.2, 0.6, 0.2, 0.5) +#define APPLE1_HIGH_TRANS QColor::fromRgbF(0.2, 0.6, 0.2, 0.25) +#define LIMENADE1 QColor::fromRgbF(0.4, 0.8, 0.0, 1) +#define ATLANTIS1 QColor::fromRgbF(0.4, 0.8, 0.2, 1) +#define ATLANTIS2 QColor::fromRgbF(0.6, 0.8, 0.2, 1) +#define RIOGRANDE1 QColor::fromRgbF(0.8, 0.8, 0.0, 1) +#define EARLSGREEN1 QColor::fromRgbF(0.8, 0.8, 0.2, 1) +#define FORESTGREEN1 QColor::fromRgbF(0.1, 0.5, 0.1, 1) +#define NITROX_GREEN QColor::fromRgbF(0, 0.54, 0.375, 1) + +// Reds +#define PERSIANRED1 QColor::fromRgbF(0.8, 0.2, 0.2, 1) +#define TUSCANY1 QColor::fromRgbF(0.8, 0.4, 0.2, 1) +#define PIRATEGOLD1 QColor::fromRgbF(0.8, 0.5, 0.0, 1) +#define HOKEYPOKEY1 QColor::fromRgbF(0.8, 0.6, 0.2, 1) +#define CINNABAR1 QColor::fromRgbF(0.9, 0.3, 0.2, 1) +#define REDORANGE1 QColor::fromRgbF(1.0, 0.2, 0.2, 1) +#define REDORANGE1_HIGH_TRANS QColor::fromRgbF(1.0, 0.2, 0.2, 0.25) +#define REDORANGE1_MED_TRANS QColor::fromRgbF(1.0, 0.2, 0.2, 0.5) +#define RED1_MED_TRANS QColor::fromRgbF(1.0, 0.0, 0.0, 0.5) +#define RED1 QColor::fromRgbF(1.0, 0.0, 0.0, 1) + +// Monochromes +#define BLACK1 QColor::fromRgbF(0.0, 0.0, 0.0, 1) +#define BLACK1_LOW_TRANS QColor::fromRgbF(0.0, 0.0, 0.0, 0.75) +#define BLACK1_HIGH_TRANS QColor::fromRgbF(0.0, 0.0, 0.0, 0.25) +#define TUNDORA1_MED_TRANS QColor::fromRgbF(0.3, 0.3, 0.3, 0.5) +#define MED_GRAY_HIGH_TRANS QColor::fromRgbF(0.5, 0.5, 0.5, 0.25) +#define MERCURY1_MED_TRANS QColor::fromRgbF(0.9, 0.9, 0.9, 0.5) +#define CONCRETE1_LOWER_TRANS QColor::fromRgbF(0.95, 0.95, 0.95, 0.9) +#define WHITE1_MED_TRANS QColor::fromRgbF(1.0, 1.0, 1.0, 0.5) +#define WHITE1 QColor::fromRgbF(1.0, 1.0, 1.0, 1) + +// Blues +#define GOVERNORBAY2 QColor::fromRgbF(0.2, 0.2, 0.7, 1) +#define GOVERNORBAY1_MED_TRANS QColor::fromRgbF(0.2, 0.2, 0.8, 0.5) +#define ROYALBLUE2 QColor::fromRgbF(0.2, 0.2, 0.9, 1) +#define ROYALBLUE2_LOW_TRANS QColor::fromRgbF(0.2, 0.2, 0.9, 0.75) +#define AIR_BLUE QColor::fromRgbF(0.25, 0.75, 1.0, 1) +#define AIR_BLUE_TRANS QColor::fromRgbF(0.25, 0.75, 1.0, 0.5) + +// Yellows / BROWNS +#define SPRINGWOOD1 QColor::fromRgbF(0.95, 0.95, 0.9, 1) +#define SPRINGWOOD1_MED_TRANS QColor::fromRgbF(0.95, 0.95, 0.9, 0.5) +#define BROOM1_LOWER_TRANS QColor::fromRgbF(1.0, 1.0, 0.1, 0.9) +#define PEANUT QColor::fromRgbF(0.5, 0.2, 0.1, 1.0) +#define PEANUT_MED_TRANS QColor::fromRgbF(0.5, 0.2, 0.1, 0.5) +#define NITROX_YELLOW QColor::fromRgbF(0.98, 0.89, 0.07, 1.0) + +// Magentas +#define MEDIUMREDVIOLET1_HIGHER_TRANS QColor::fromRgbF(0.7, 0.2, 0.7, 0.1) + +#endif // COLOR_H diff --git a/subsurface-core/configuredivecomputer.cpp b/subsurface-core/configuredivecomputer.cpp new file mode 100644 index 000000000..2457ffe82 --- /dev/null +++ b/subsurface-core/configuredivecomputer.cpp @@ -0,0 +1,681 @@ +#include "configuredivecomputer.h" +#include "libdivecomputer/hw.h" +#include +#include +#include +#include +#include +#include +#include +#include + +ConfigureDiveComputer::ConfigureDiveComputer() : readThread(0), + writeThread(0), + resetThread(0), + firmwareThread(0) +{ + setState(INITIAL); +} + +void ConfigureDiveComputer::readSettings(device_data_t *data) +{ + setState(READING); + + if (readThread) + readThread->deleteLater(); + + readThread = new ReadSettingsThread(this, data); + connect(readThread, SIGNAL(finished()), + this, SLOT(readThreadFinished()), Qt::QueuedConnection); + connect(readThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(readThread, SIGNAL(devicedetails(DeviceDetails *)), this, + SIGNAL(deviceDetailsChanged(DeviceDetails *))); + connect(readThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + readThread->start(); +} + +void ConfigureDiveComputer::saveDeviceDetails(DeviceDetails *details, device_data_t *data) +{ + setState(WRITING); + + if (writeThread) + writeThread->deleteLater(); + + writeThread = new WriteSettingsThread(this, data); + connect(writeThread, SIGNAL(finished()), + this, SLOT(writeThreadFinished()), Qt::QueuedConnection); + connect(writeThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(writeThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + writeThread->setDeviceDetails(details); + writeThread->start(); +} + +bool ConfigureDiveComputer::saveXMLBackup(QString fileName, DeviceDetails *details, device_data_t *data) +{ + QString xml = ""; + QString vendor = data->vendor; + QString product = data->product; + QXmlStreamWriter writer(&xml); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + writer.writeStartElement("DiveComputerSettingsBackup"); + writer.writeStartElement("DiveComputer"); + writer.writeTextElement("Vendor", vendor); + writer.writeTextElement("Product", product); + writer.writeEndElement(); + writer.writeStartElement("Settings"); + writer.writeTextElement("CustomText", details->customText); + //Add gasses + QString gas1 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas1.oxygen), + QString::number(details->gas1.helium), + QString::number(details->gas1.type), + QString::number(details->gas1.depth)); + QString gas2 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas2.oxygen), + QString::number(details->gas2.helium), + QString::number(details->gas2.type), + QString::number(details->gas2.depth)); + QString gas3 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas3.oxygen), + QString::number(details->gas3.helium), + QString::number(details->gas3.type), + QString::number(details->gas3.depth)); + QString gas4 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas4.oxygen), + QString::number(details->gas4.helium), + QString::number(details->gas4.type), + QString::number(details->gas4.depth)); + QString gas5 = QString("%1,%2,%3,%4") + .arg(QString::number(details->gas5.oxygen), + QString::number(details->gas5.helium), + QString::number(details->gas5.type), + QString::number(details->gas5.depth)); + writer.writeTextElement("Gas1", gas1); + writer.writeTextElement("Gas2", gas2); + writer.writeTextElement("Gas3", gas3); + writer.writeTextElement("Gas4", gas4); + writer.writeTextElement("Gas5", gas5); + // + //Add dil values + QString dil1 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil1.oxygen), + QString::number(details->dil1.helium), + QString::number(details->dil1.type), + QString::number(details->dil1.depth)); + QString dil2 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil2.oxygen), + QString::number(details->dil2.helium), + QString::number(details->dil2.type), + QString::number(details->dil2.depth)); + QString dil3 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil3.oxygen), + QString::number(details->dil3.helium), + QString::number(details->dil3.type), + QString::number(details->dil3.depth)); + QString dil4 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil4.oxygen), + QString::number(details->dil4.helium), + QString::number(details->dil4.type), + QString::number(details->dil4.depth)); + QString dil5 = QString("%1,%2,%3,%4") + .arg(QString::number(details->dil5.oxygen), + QString::number(details->dil5.helium), + QString::number(details->dil5.type), + QString::number(details->dil5.depth)); + writer.writeTextElement("Dil1", dil1); + writer.writeTextElement("Dil2", dil2); + writer.writeTextElement("Dil3", dil3); + writer.writeTextElement("Dil4", dil4); + writer.writeTextElement("Dil5", dil5); + // + //Add set point values + QString sp1 = QString("%1,%2") + .arg(QString::number(details->sp1.sp), + QString::number(details->sp1.depth)); + QString sp2 = QString("%1,%2") + .arg(QString::number(details->sp2.sp), + QString::number(details->sp2.depth)); + QString sp3 = QString("%1,%2") + .arg(QString::number(details->sp3.sp), + QString::number(details->sp3.depth)); + QString sp4 = QString("%1,%2") + .arg(QString::number(details->sp4.sp), + QString::number(details->sp4.depth)); + QString sp5 = QString("%1,%2") + .arg(QString::number(details->sp5.sp), + QString::number(details->sp5.depth)); + writer.writeTextElement("SetPoint1", sp1); + writer.writeTextElement("SetPoint2", sp2); + writer.writeTextElement("SetPoint3", sp3); + writer.writeTextElement("SetPoint4", sp4); + writer.writeTextElement("SetPoint5", sp5); + + //Other Settings + writer.writeTextElement("DiveMode", QString::number(details->diveMode)); + writer.writeTextElement("Saturation", QString::number(details->saturation)); + writer.writeTextElement("Desaturation", QString::number(details->desaturation)); + writer.writeTextElement("LastDeco", QString::number(details->lastDeco)); + writer.writeTextElement("Brightness", QString::number(details->brightness)); + writer.writeTextElement("Units", QString::number(details->units)); + writer.writeTextElement("SamplingRate", QString::number(details->samplingRate)); + writer.writeTextElement("Salinity", QString::number(details->salinity)); + writer.writeTextElement("DiveModeColor", QString::number(details->diveModeColor)); + writer.writeTextElement("Language", QString::number(details->language)); + writer.writeTextElement("DateFormat", QString::number(details->dateFormat)); + writer.writeTextElement("CompassGain", QString::number(details->compassGain)); + writer.writeTextElement("SafetyStop", QString::number(details->safetyStop)); + writer.writeTextElement("GfHigh", QString::number(details->gfHigh)); + writer.writeTextElement("GfLow", QString::number(details->gfLow)); + writer.writeTextElement("PressureSensorOffset", QString::number(details->pressureSensorOffset)); + writer.writeTextElement("PpO2Min", QString::number(details->ppO2Min)); + writer.writeTextElement("PpO2Max", QString::number(details->ppO2Max)); + writer.writeTextElement("FutureTTS", QString::number(details->futureTTS)); + writer.writeTextElement("CcrMode", QString::number(details->ccrMode)); + writer.writeTextElement("DecoType", QString::number(details->decoType)); + writer.writeTextElement("AGFSelectable", QString::number(details->aGFSelectable)); + writer.writeTextElement("AGFHigh", QString::number(details->aGFHigh)); + writer.writeTextElement("AGFLow", QString::number(details->aGFLow)); + writer.writeTextElement("CalibrationGas", QString::number(details->calibrationGas)); + writer.writeTextElement("FlipScreen", QString::number(details->flipScreen)); + writer.writeTextElement("SetPointFallback", QString::number(details->setPointFallback)); + writer.writeTextElement("LeftButtonSensitivity", QString::number(details->leftButtonSensitivity)); + writer.writeTextElement("RightButtonSensitivity", QString::number(details->rightButtonSensitivity)); + writer.writeTextElement("BottomGasConsumption", QString::number(details->bottomGasConsumption)); + writer.writeTextElement("DecoGasConsumption", QString::number(details->decoGasConsumption)); + writer.writeTextElement("ModWarning", QString::number(details->modWarning)); + writer.writeTextElement("DynamicAscendRate", QString::number(details->dynamicAscendRate)); + writer.writeTextElement("GraphicalSpeedIndicator", QString::number(details->graphicalSpeedIndicator)); + writer.writeTextElement("AlwaysShowppO2", QString::number(details->alwaysShowppO2)); + + // Suunto vyper settings. + writer.writeTextElement("Altitude", QString::number(details->altitude)); + writer.writeTextElement("PersonalSafety", QString::number(details->personalSafety)); + writer.writeTextElement("TimeFormat", QString::number(details->timeFormat)); + + writer.writeStartElement("Light"); + writer.writeAttribute("enabled", QString::number(details->lightEnabled)); + writer.writeCharacters(QString::number(details->light)); + writer.writeEndElement(); + + writer.writeStartElement("AlarmTime"); + writer.writeAttribute("enabled", QString::number(details->alarmTimeEnabled)); + writer.writeCharacters(QString::number(details->alarmTime)); + writer.writeEndElement(); + + writer.writeStartElement("AlarmDepth"); + writer.writeAttribute("enabled", QString::number(details->alarmDepthEnabled)); + writer.writeCharacters(QString::number(details->alarmDepth)); + writer.writeEndElement(); + + writer.writeEndElement(); + writer.writeEndElement(); + + writer.writeEndDocument(); + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + lastError = tr("Could not save the backup file %1. Error Message: %2") + .arg(fileName, file.errorString()); + return false; + } + //file open successful. write data and save. + QTextStream out(&file); + out << xml; + + file.close(); + return true; +} + +bool ConfigureDiveComputer::restoreXMLBackup(QString fileName, DeviceDetails *details) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + lastError = tr("Could not open backup file: %1").arg(file.errorString()); + return false; + } + + QString xml = file.readAll(); + + QXmlStreamReader reader(xml); + while (!reader.atEnd()) { + if (reader.isStartElement()) { + QString settingName = reader.name().toString(); + QXmlStreamAttributes attributes = reader.attributes(); + reader.readNext(); + QString keyString = reader.text().toString(); + + if (settingName == "CustomText") + details->customText = keyString; + + if (settingName == "Gas1") { + QStringList gasData = keyString.split(","); + gas gas1; + gas1.oxygen = gasData.at(0).toInt(); + gas1.helium = gasData.at(1).toInt(); + gas1.type = gasData.at(2).toInt(); + gas1.depth = gasData.at(3).toInt(); + details->gas1 = gas1; + } + + if (settingName == "Gas2") { + QStringList gasData = keyString.split(","); + gas gas2; + gas2.oxygen = gasData.at(0).toInt(); + gas2.helium = gasData.at(1).toInt(); + gas2.type = gasData.at(2).toInt(); + gas2.depth = gasData.at(3).toInt(); + details->gas2 = gas2; + } + + if (settingName == "Gas3") { + QStringList gasData = keyString.split(","); + gas gas3; + gas3.oxygen = gasData.at(0).toInt(); + gas3.helium = gasData.at(1).toInt(); + gas3.type = gasData.at(2).toInt(); + gas3.depth = gasData.at(3).toInt(); + details->gas3 = gas3; + } + + if (settingName == "Gas4") { + QStringList gasData = keyString.split(","); + gas gas4; + gas4.oxygen = gasData.at(0).toInt(); + gas4.helium = gasData.at(1).toInt(); + gas4.type = gasData.at(2).toInt(); + gas4.depth = gasData.at(3).toInt(); + details->gas4 = gas4; + } + + if (settingName == "Gas5") { + QStringList gasData = keyString.split(","); + gas gas5; + gas5.oxygen = gasData.at(0).toInt(); + gas5.helium = gasData.at(1).toInt(); + gas5.type = gasData.at(2).toInt(); + gas5.depth = gasData.at(3).toInt(); + details->gas5 = gas5; + } + + if (settingName == "Dil1") { + QStringList dilData = keyString.split(","); + gas dil1; + dil1.oxygen = dilData.at(0).toInt(); + dil1.helium = dilData.at(1).toInt(); + dil1.type = dilData.at(2).toInt(); + dil1.depth = dilData.at(3).toInt(); + details->dil1 = dil1; + } + + if (settingName == "Dil2") { + QStringList dilData = keyString.split(","); + gas dil2; + dil2.oxygen = dilData.at(0).toInt(); + dil2.helium = dilData.at(1).toInt(); + dil2.type = dilData.at(2).toInt(); + dil2.depth = dilData.at(3).toInt(); + details->dil1 = dil2; + } + + if (settingName == "Dil3") { + QStringList dilData = keyString.split(","); + gas dil3; + dil3.oxygen = dilData.at(0).toInt(); + dil3.helium = dilData.at(1).toInt(); + dil3.type = dilData.at(2).toInt(); + dil3.depth = dilData.at(3).toInt(); + details->dil3 = dil3; + } + + if (settingName == "Dil4") { + QStringList dilData = keyString.split(","); + gas dil4; + dil4.oxygen = dilData.at(0).toInt(); + dil4.helium = dilData.at(1).toInt(); + dil4.type = dilData.at(2).toInt(); + dil4.depth = dilData.at(3).toInt(); + details->dil4 = dil4; + } + + if (settingName == "Dil5") { + QStringList dilData = keyString.split(","); + gas dil5; + dil5.oxygen = dilData.at(0).toInt(); + dil5.helium = dilData.at(1).toInt(); + dil5.type = dilData.at(2).toInt(); + dil5.depth = dilData.at(3).toInt(); + details->dil5 = dil5; + } + + if (settingName == "SetPoint1") { + QStringList spData = keyString.split(","); + setpoint sp1; + sp1.sp = spData.at(0).toInt(); + sp1.depth = spData.at(1).toInt(); + details->sp1 = sp1; + } + + if (settingName == "SetPoint2") { + QStringList spData = keyString.split(","); + setpoint sp2; + sp2.sp = spData.at(0).toInt(); + sp2.depth = spData.at(1).toInt(); + details->sp2 = sp2; + } + + if (settingName == "SetPoint3") { + QStringList spData = keyString.split(","); + setpoint sp3; + sp3.sp = spData.at(0).toInt(); + sp3.depth = spData.at(1).toInt(); + details->sp3 = sp3; + } + + if (settingName == "SetPoint4") { + QStringList spData = keyString.split(","); + setpoint sp4; + sp4.sp = spData.at(0).toInt(); + sp4.depth = spData.at(1).toInt(); + details->sp4 = sp4; + } + + if (settingName == "SetPoint5") { + QStringList spData = keyString.split(","); + setpoint sp5; + sp5.sp = spData.at(0).toInt(); + sp5.depth = spData.at(1).toInt(); + details->sp5 = sp5; + } + + if (settingName == "Saturation") + details->saturation = keyString.toInt(); + + if (settingName == "Desaturation") + details->desaturation = keyString.toInt(); + + if (settingName == "DiveMode") + details->diveMode = keyString.toInt(); + + if (settingName == "LastDeco") + details->lastDeco = keyString.toInt(); + + if (settingName == "Brightness") + details->brightness = keyString.toInt(); + + if (settingName == "Units") + details->units = keyString.toInt(); + + if (settingName == "SamplingRate") + details->samplingRate = keyString.toInt(); + + if (settingName == "Salinity") + details->salinity = keyString.toInt(); + + if (settingName == "DiveModeColour") + details->diveModeColor = keyString.toInt(); + + if (settingName == "Language") + details->language = keyString.toInt(); + + if (settingName == "DateFormat") + details->dateFormat = keyString.toInt(); + + if (settingName == "CompassGain") + details->compassGain = keyString.toInt(); + + if (settingName == "SafetyStop") + details->safetyStop = keyString.toInt(); + + if (settingName == "GfHigh") + details->gfHigh = keyString.toInt(); + + if (settingName == "GfLow") + details->gfLow = keyString.toInt(); + + if (settingName == "PressureSensorOffset") + details->pressureSensorOffset = keyString.toInt(); + + if (settingName == "PpO2Min") + details->ppO2Min = keyString.toInt(); + + if (settingName == "PpO2Max") + details->ppO2Max = keyString.toInt(); + + if (settingName == "FutureTTS") + details->futureTTS = keyString.toInt(); + + if (settingName == "CcrMode") + details->ccrMode = keyString.toInt(); + + if (settingName == "DecoType") + details->decoType = keyString.toInt(); + + if (settingName == "AGFSelectable") + details->aGFSelectable = keyString.toInt(); + + if (settingName == "AGFHigh") + details->aGFHigh = keyString.toInt(); + + if (settingName == "AGFLow") + details->aGFLow = keyString.toInt(); + + if (settingName == "CalibrationGas") + details->calibrationGas = keyString.toInt(); + + if (settingName == "FlipScreen") + details->flipScreen = keyString.toInt(); + + if (settingName == "SetPointFallback") + details->setPointFallback = keyString.toInt(); + + if (settingName == "LeftButtonSensitivity") + details->leftButtonSensitivity = keyString.toInt(); + + if (settingName == "RightButtonSensitivity") + details->rightButtonSensitivity = keyString.toInt(); + + if (settingName == "BottomGasConsumption") + details->bottomGasConsumption = keyString.toInt(); + + if (settingName == "DecoGasConsumption") + details->decoGasConsumption = keyString.toInt(); + + if (settingName == "ModWarning") + details->modWarning = keyString.toInt(); + + if (settingName == "DynamicAscendRate") + details->dynamicAscendRate = keyString.toInt(); + + if (settingName == "GraphicalSpeedIndicator") + details->graphicalSpeedIndicator = keyString.toInt(); + + if (settingName == "AlwaysShowppO2") + details->alwaysShowppO2 = keyString.toInt(); + + if (settingName == "Altitude") + details->altitude = keyString.toInt(); + + if (settingName == "PersonalSafety") + details->personalSafety = keyString.toInt(); + + if (settingName == "TimeFormat") + details->timeFormat = keyString.toInt(); + + if (settingName == "Light") { + if (attributes.hasAttribute("enabled")) + details->lightEnabled = attributes.value("enabled").toString().toInt(); + details->light = keyString.toInt(); + } + + if (settingName == "AlarmDepth") { + if (attributes.hasAttribute("enabled")) + details->alarmDepthEnabled = attributes.value("enabled").toString().toInt(); + details->alarmDepth = keyString.toInt(); + } + + if (settingName == "AlarmTime") { + if (attributes.hasAttribute("enabled")) + details->alarmTimeEnabled = attributes.value("enabled").toString().toInt(); + details->alarmTime = keyString.toInt(); + } + } + reader.readNext(); + } + + return true; +} + +void ConfigureDiveComputer::startFirmwareUpdate(QString fileName, device_data_t *data) +{ + setState(FWUPDATE); + if (firmwareThread) + firmwareThread->deleteLater(); + + firmwareThread = new FirmwareUpdateThread(this, data, fileName); + connect(firmwareThread, SIGNAL(finished()), + this, SLOT(firmwareThreadFinished()), Qt::QueuedConnection); + connect(firmwareThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(firmwareThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + firmwareThread->start(); +} + +void ConfigureDiveComputer::resetSettings(device_data_t *data) +{ + setState(RESETTING); + + if (resetThread) + resetThread->deleteLater(); + + resetThread = new ResetSettingsThread(this, data); + connect(resetThread, SIGNAL(finished()), + this, SLOT(resetThreadFinished()), Qt::QueuedConnection); + connect(resetThread, SIGNAL(error(QString)), this, SLOT(setError(QString))); + connect(resetThread, SIGNAL(progress(int)), this, SLOT(progressEvent(int)), Qt::QueuedConnection); + + resetThread->start(); +} + +void ConfigureDiveComputer::progressEvent(int percent) +{ + emit progress(percent); +} + +void ConfigureDiveComputer::setState(ConfigureDiveComputer::states newState) +{ + currentState = newState; + emit stateChanged(currentState); +} + +void ConfigureDiveComputer::setError(QString err) +{ + lastError = err; + emit error(err); +} + +void ConfigureDiveComputer::readThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Dive computer details read successfully")); + } +} + +void ConfigureDiveComputer::writeThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Setting successfully written to device")); + } +} + +void ConfigureDiveComputer::firmwareThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Device firmware successfully updated")); + } +} + +void ConfigureDiveComputer::resetThreadFinished() +{ + setState(DONE); + if (lastError.isEmpty()) { + //No error + emit message(tr("Device settings successfully reset")); + } +} + +QString ConfigureDiveComputer::dc_open(device_data_t *data) +{ + FILE *fp = NULL; + dc_status_t rc; + + if (data->libdc_log) + fp = subsurface_fopen(logfile_name, "w"); + + data->libdc_logfile = fp; + + rc = dc_context_new(&data->context); + if (rc != DC_STATUS_SUCCESS) { + return tr("Unable to create libdivecomputer context"); + } + + if (fp) { + dc_context_set_loglevel(data->context, DC_LOGLEVEL_ALL); + dc_context_set_logfunc(data->context, logfunc, fp); + } + +#if defined(SSRF_CUSTOM_SERIAL) + dc_serial_t *serial_device = NULL; + + if (data->bluetooth_mode) { +#ifdef BT_SUPPORT + rc = dc_serial_qt_open(&serial_device, data->context, data->devname); +#endif +#ifdef SERIAL_FTDI + } else if (!strcmp(data->devname, "ftdi")) { + rc = dc_serial_ftdi_open(&serial_device, data->context); +#endif + } + + if (rc != DC_STATUS_SUCCESS) { + return errmsg(rc); + } else if (serial_device) { + rc = dc_device_custom_open(&data->device, data->context, data->descriptor, serial_device); + } else { +#else + { +#endif + rc = dc_device_open(&data->device, data->context, data->descriptor, data->devname); + } + + if (rc != DC_STATUS_SUCCESS) { + return tr("Could not a establish connection to the dive computer."); + } + + setState(OPEN); + + return NULL; +} + +void ConfigureDiveComputer::dc_close(device_data_t *data) +{ + if (data->device) + dc_device_close(data->device); + data->device = NULL; + if (data->context) + dc_context_free(data->context); + data->context = NULL; + + if (data->libdc_logfile) + fclose(data->libdc_logfile); + + setState(INITIAL); +} diff --git a/subsurface-core/configuredivecomputer.h b/subsurface-core/configuredivecomputer.h new file mode 100644 index 000000000..f14eeeca3 --- /dev/null +++ b/subsurface-core/configuredivecomputer.h @@ -0,0 +1,68 @@ +#ifndef CONFIGUREDIVECOMPUTER_H +#define CONFIGUREDIVECOMPUTER_H + +#include +#include +#include +#include "libdivecomputer.h" +#include "configuredivecomputerthreads.h" +#include + +#include "libxml/xmlreader.h" + +class ConfigureDiveComputer : public QObject { + Q_OBJECT +public: + explicit ConfigureDiveComputer(); + void readSettings(device_data_t *data); + + enum states { + OPEN, + INITIAL, + READING, + WRITING, + RESETTING, + FWUPDATE, + CANCELLING, + CANCELLED, + ERROR, + DONE, + }; + + QString lastError; + states currentState; + void saveDeviceDetails(DeviceDetails *details, device_data_t *data); + void fetchDeviceDetails(); + bool saveXMLBackup(QString fileName, DeviceDetails *details, device_data_t *data); + bool restoreXMLBackup(QString fileName, DeviceDetails *details); + void startFirmwareUpdate(QString fileName, device_data_t *data); + void resetSettings(device_data_t *data); + + QString dc_open(device_data_t *data); +public +slots: + void dc_close(device_data_t *data); +signals: + void progress(int percent); + void message(QString msg); + void error(QString err); + void stateChanged(states newState); + void deviceDetailsChanged(DeviceDetails *newDetails); + +private: + ReadSettingsThread *readThread; + WriteSettingsThread *writeThread; + ResetSettingsThread *resetThread; + FirmwareUpdateThread *firmwareThread; + void setState(states newState); +private +slots: + void progressEvent(int percent); + void readThreadFinished(); + void writeThreadFinished(); + void resetThreadFinished(); + void firmwareThreadFinished(); + void setError(QString err); +}; + +#endif // CONFIGUREDIVECOMPUTER_H diff --git a/subsurface-core/configuredivecomputerthreads.cpp b/subsurface-core/configuredivecomputerthreads.cpp new file mode 100644 index 000000000..78edcb37c --- /dev/null +++ b/subsurface-core/configuredivecomputerthreads.cpp @@ -0,0 +1,1760 @@ +#include "configuredivecomputerthreads.h" +#include "libdivecomputer/hw.h" +#include "libdivecomputer.h" +#include +#include + +#define OSTC3_GAS1 0x10 +#define OSTC3_GAS2 0x11 +#define OSTC3_GAS3 0x12 +#define OSTC3_GAS4 0x13 +#define OSTC3_GAS5 0x14 +#define OSTC3_DIL1 0x15 +#define OSTC3_DIL2 0x16 +#define OSTC3_DIL3 0x17 +#define OSTC3_DIL4 0x18 +#define OSTC3_DIL5 0x19 +#define OSTC3_SP1 0x1A +#define OSTC3_SP2 0x1B +#define OSTC3_SP3 0x1C +#define OSTC3_SP4 0x1D +#define OSTC3_SP5 0x1E +#define OSTC3_CCR_MODE 0x1F +#define OSTC3_DIVE_MODE 0x20 +#define OSTC3_DECO_TYPE 0x21 +#define OSTC3_PPO2_MAX 0x22 +#define OSTC3_PPO2_MIN 0x23 +#define OSTC3_FUTURE_TTS 0x24 +#define OSTC3_GF_LOW 0x25 +#define OSTC3_GF_HIGH 0x26 +#define OSTC3_AGF_LOW 0x27 +#define OSTC3_AGF_HIGH 0x28 +#define OSTC3_AGF_SELECTABLE 0x29 +#define OSTC3_SATURATION 0x2A +#define OSTC3_DESATURATION 0x2B +#define OSTC3_LAST_DECO 0x2C +#define OSTC3_BRIGHTNESS 0x2D +#define OSTC3_UNITS 0x2E +#define OSTC3_SAMPLING_RATE 0x2F +#define OSTC3_SALINITY 0x30 +#define OSTC3_DIVEMODE_COLOR 0x31 +#define OSTC3_LANGUAGE 0x32 +#define OSTC3_DATE_FORMAT 0x33 +#define OSTC3_COMPASS_GAIN 0x34 +#define OSTC3_PRESSURE_SENSOR_OFFSET 0x35 +#define OSTC3_SAFETY_STOP 0x36 +#define OSTC3_CALIBRATION_GAS_O2 0x37 +#define OSTC3_SETPOINT_FALLBACK 0x38 +#define OSTC3_FLIP_SCREEN 0x39 +#define OSTC3_LEFT_BUTTON_SENSIVITY 0x3A +#define OSTC3_RIGHT_BUTTON_SENSIVITY 0x3A +#define OSTC3_BOTTOM_GAS_CONSUMPTION 0x3C +#define OSTC3_DECO_GAS_CONSUMPTION 0x3D +#define OSTC3_MOD_WARNING 0x3E +#define OSTC3_DYNAMIC_ASCEND_RATE 0x3F +#define OSTC3_GRAPHICAL_SPEED_INDICATOR 0x40 +#define OSTC3_ALWAYS_SHOW_PPO2 0x41 + +#define OSTC3_HW_OSTC_3 0x0A +#define OSTC3_HW_OSTC_3P 0x1A +#define OSTC3_HW_OSTC_CR 0x05 +#define OSTC3_HW_OSTC_SPORT 0x12 +#define OSTC3_HW_OSTC_2 0x11 + + +#define SUUNTO_VYPER_MAXDEPTH 0x1e +#define SUUNTO_VYPER_TOTAL_TIME 0x20 +#define SUUNTO_VYPER_NUMBEROFDIVES 0x22 +#define SUUNTO_VYPER_COMPUTER_TYPE 0x24 +#define SUUNTO_VYPER_FIRMWARE 0x25 +#define SUUNTO_VYPER_SERIALNUMBER 0x26 +#define SUUNTO_VYPER_CUSTOM_TEXT 0x2c +#define SUUNTO_VYPER_SAMPLING_RATE 0x53 +#define SUUNTO_VYPER_ALTITUDE_SAFETY 0x54 +#define SUUNTO_VYPER_TIMEFORMAT 0x60 +#define SUUNTO_VYPER_UNITS 0x62 +#define SUUNTO_VYPER_MODEL 0x63 +#define SUUNTO_VYPER_LIGHT 0x64 +#define SUUNTO_VYPER_ALARM_DEPTH_TIME 0x65 +#define SUUNTO_VYPER_ALARM_TIME 0x66 +#define SUUNTO_VYPER_ALARM_DEPTH 0x68 +#define SUUNTO_VYPER_CUSTOM_TEXT_LENGHT 30 + +#ifdef DEBUG_OSTC +// Fake io to ostc memory banks +#define hw_ostc_device_eeprom_read local_hw_ostc_device_eeprom_read +#define hw_ostc_device_eeprom_write local_hw_ostc_device_eeprom_write +#define hw_ostc_device_clock local_hw_ostc_device_clock +#define OSTC_FILE "../OSTC-data-dump.bin" + +// Fake the open function. +static dc_status_t local_dc_device_open(dc_device_t **out, dc_context_t *context, dc_descriptor_t *descriptor, const char *name) +{ + if (strcmp(dc_descriptor_get_vendor(descriptor), "Heinrichs Weikamp") == 0 &&strcmp(dc_descriptor_get_product(descriptor), "OSTC 2N") == 0) + return DC_STATUS_SUCCESS; + else + return dc_device_open(out, context, descriptor, name); +} +#define dc_device_open local_dc_device_open + +// Fake the custom open function +static dc_status_t local_dc_device_custom_open(dc_device_t **out, dc_context_t *context, dc_descriptor_t *descriptor, dc_serial_t *serial) +{ + if (strcmp(dc_descriptor_get_vendor(descriptor), "Heinrichs Weikamp") == 0 &&strcmp(dc_descriptor_get_product(descriptor), "OSTC 2N") == 0) + return DC_STATUS_SUCCESS; + else + return dc_device_custom_open(out, context, descriptor, serial); +} +#define dc_device_custom_open local_dc_device_custom_open + +static dc_status_t local_hw_ostc_device_eeprom_read(void *ignored, unsigned char bank, unsigned char data[], unsigned int data_size) +{ + FILE *f; + if ((f = fopen(OSTC_FILE, "r")) == NULL) + return DC_STATUS_NODEVICE; + fseek(f, bank * 256, SEEK_SET); + if (fread(data, sizeof(unsigned char), data_size, f) != data_size) { + fclose(f); + return DC_STATUS_IO; + } + fclose(f); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t local_hw_ostc_device_eeprom_write(void *ignored, unsigned char bank, unsigned char data[], unsigned int data_size) +{ + FILE *f; + if ((f = fopen(OSTC_FILE, "r+")) == NULL) + f = fopen(OSTC_FILE, "w"); + fseek(f, bank * 256, SEEK_SET); + fwrite(data, sizeof(unsigned char), data_size, f); + fclose(f); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t local_hw_ostc_device_clock(void *ignored, dc_datetime_t *time) +{ + return DC_STATUS_SUCCESS; +} +#endif + +static int read_ostc_cf(unsigned char data[], unsigned char cf) +{ + return data[128 + (cf % 32) * 4 + 3] << 8 ^ data[128 + (cf % 32) * 4 + 2]; +} + +static void write_ostc_cf(unsigned char data[], unsigned char cf, unsigned char max_CF, unsigned int value) +{ + // Only write settings supported by this firmware. + if (cf > max_CF) + return; + + data[128 + (cf % 32) * 4 + 3] = (value & 0xff00) >> 8; + data[128 + (cf % 32) * 4 + 2] = (value & 0x00ff); +} + +#define EMIT_PROGRESS() do { \ + progress.current++; \ + progress_cb(device, DC_EVENT_PROGRESS, &progress, userdata); \ + } while (0) + +static dc_status_t read_suunto_vyper_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + unsigned char data[SUUNTO_VYPER_CUSTOM_TEXT_LENGHT + 1]; + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 16; + + rc = dc_device_read(device, SUUNTO_VYPER_COMPUTER_TYPE, data, 1); + if (rc == DC_STATUS_SUCCESS) { + const char *model; + // FIXME: grab this info from libdivecomputer descriptor + // instead of hard coded here + switch (data[0]) { + case 0x03: + model = "Stinger"; + break; + case 0x04: + model = "Mosquito"; + break; + case 0x05: + model = "D3"; + break; + case 0x0A: + model = "Vyper"; + break; + case 0x0B: + model = "Vytec"; + break; + case 0x0C: + model = "Cobra"; + break; + case 0x0D: + model = "Gekko"; + break; + case 0x16: + model = "Zoop"; + break; + case 20: + case 30: + case 60: + // Suunto Spyder have there sample interval at this position + // Fallthrough + default: + return DC_STATUS_UNSUPPORTED; + } + // We found a supported device + // we can safely proceed with reading/writing to this device. + m_deviceDetails->model = model; + } + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_MAXDEPTH, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + // in ft * 128.0 + int depth = feet_to_mm(data[0] << 8 ^ data[1]) / 128; + m_deviceDetails->maxDepth = depth; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_TOTAL_TIME, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + int total_time = data[0] << 8 ^ data[1]; + m_deviceDetails->totalTime = total_time; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_NUMBEROFDIVES, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + int number_of_dives = data[0] << 8 ^ data[1]; + m_deviceDetails->numberOfDives = number_of_dives; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_FIRMWARE, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->firmwareVersion = QString::number(data[0]) + ".0.0"; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_SERIALNUMBER, data, 4); + if (rc != DC_STATUS_SUCCESS) + return rc; + int serial_number = data[0] * 1000000 + data[1] * 10000 + data[2] * 100 + data[3]; + m_deviceDetails->serialNo = QString::number(serial_number); + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_CUSTOM_TEXT, data, SUUNTO_VYPER_CUSTOM_TEXT_LENGHT); + if (rc != DC_STATUS_SUCCESS) + return rc; + data[SUUNTO_VYPER_CUSTOM_TEXT_LENGHT] = 0; + m_deviceDetails->customText = (const char *)data; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_SAMPLING_RATE, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->samplingRate = (int)data[0]; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALTITUDE_SAFETY, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->altitude = data[0] & 0x03; + m_deviceDetails->personalSafety = data[0] >> 2 & 0x03; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_TIMEFORMAT, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->timeFormat = data[0] & 0x01; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_UNITS, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->units = data[0] & 0x01; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_MODEL, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->diveMode = data[0] & 0x03; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_LIGHT, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->lightEnabled = data[0] >> 7; + m_deviceDetails->light = data[0] & 0x7F; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALARM_DEPTH_TIME, data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + m_deviceDetails->alarmTimeEnabled = data[0] & 0x01; + m_deviceDetails->alarmDepthEnabled = data[0] >> 1 & 0x01; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALARM_TIME, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + int time = data[0] << 8 ^ data[1]; + // The stinger stores alarm time in seconds instead of minutes. + if (m_deviceDetails->model == "Stinger") + time /= 60; + m_deviceDetails->alarmTime = time; + EMIT_PROGRESS(); + + rc = dc_device_read(device, SUUNTO_VYPER_ALARM_DEPTH, data, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + depth = feet_to_mm(data[0] << 8 ^ data[1]) / 128; + m_deviceDetails->alarmDepth = depth; + EMIT_PROGRESS(); + + return DC_STATUS_SUCCESS; +} + +static dc_status_t write_suunto_vyper_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 10; + unsigned char data; + unsigned char data2[2]; + int time; + + // Maybee we should read the model from the device to sanity check it here too.. + // For now we just check that we actually read a device before writing to one. + if (m_deviceDetails->model == "") + return DC_STATUS_UNSUPPORTED; + + rc = dc_device_write(device, SUUNTO_VYPER_CUSTOM_TEXT, + // Convert the customText to a 30 char wide padded with " " + (const unsigned char *)QString("%1").arg(m_deviceDetails->customText, -30, QChar(' ')).toUtf8().data(), + SUUNTO_VYPER_CUSTOM_TEXT_LENGHT); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->samplingRate; + rc = dc_device_write(device, SUUNTO_VYPER_SAMPLING_RATE, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->personalSafety << 2 ^ m_deviceDetails->altitude; + rc = dc_device_write(device, SUUNTO_VYPER_ALTITUDE_SAFETY, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->timeFormat; + rc = dc_device_write(device, SUUNTO_VYPER_TIMEFORMAT, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->units; + rc = dc_device_write(device, SUUNTO_VYPER_UNITS, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->diveMode; + rc = dc_device_write(device, SUUNTO_VYPER_MODEL, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->lightEnabled << 7 ^ (m_deviceDetails->light & 0x7F); + rc = dc_device_write(device, SUUNTO_VYPER_LIGHT, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data = m_deviceDetails->alarmDepthEnabled << 1 ^ m_deviceDetails->alarmTimeEnabled; + rc = dc_device_write(device, SUUNTO_VYPER_ALARM_DEPTH_TIME, &data, 1); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + // The stinger stores alarm time in seconds instead of minutes. + time = m_deviceDetails->alarmTime; + if (m_deviceDetails->model == "Stinger") + time *= 60; + data2[0] = time >> 8; + data2[1] = time & 0xFF; + rc = dc_device_write(device, SUUNTO_VYPER_ALARM_TIME, data2, 2); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + data2[0] = (int)(mm_to_feet(m_deviceDetails->alarmDepth) * 128) >> 8; + data2[1] = (int)(mm_to_feet(m_deviceDetails->alarmDepth) * 128) & 0x0FF; + rc = dc_device_write(device, SUUNTO_VYPER_ALARM_DEPTH, data2, 2); + EMIT_PROGRESS(); + return rc; +} + +#if DC_VERSION_CHECK(0, 5, 0) +static dc_status_t read_ostc3_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 52; + unsigned char hardware[1]; + + //Read hardware type + rc = hw_ostc3_device_hardware (device, hardware, sizeof (hardware)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + // FIXME: can we grab this info from libdivecomputer descriptor + // instead of hard coded here? + switch(hardware[0]) { + case OSTC3_HW_OSTC_3: + m_deviceDetails->model = "3"; + break; + case OSTC3_HW_OSTC_3P: + m_deviceDetails->model = "3+"; + break; + case OSTC3_HW_OSTC_CR: + m_deviceDetails->model = "CR"; + break; + case OSTC3_HW_OSTC_SPORT: + m_deviceDetails->model = "Sport"; + break; + case OSTC3_HW_OSTC_2: + m_deviceDetails->model = "2"; + break; + } + + //Read gas mixes + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + unsigned char gasData[4] = { 0, 0, 0, 0 }; + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS1, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas1.oxygen = gasData[0]; + gas1.helium = gasData[1]; + gas1.type = gasData[2]; + gas1.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS2, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas2.oxygen = gasData[0]; + gas2.helium = gasData[1]; + gas2.type = gasData[2]; + gas2.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS3, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas3.oxygen = gasData[0]; + gas3.helium = gasData[1]; + gas3.type = gasData[2]; + gas3.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS4, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas4.oxygen = gasData[0]; + gas4.helium = gasData[1]; + gas4.type = gasData[2]; + gas4.depth = gasData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_GAS5, gasData, sizeof(gasData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + gas5.oxygen = gasData[0]; + gas5.helium = gasData[1]; + gas5.type = gasData[2]; + gas5.depth = gasData[3]; + EMIT_PROGRESS(); + + m_deviceDetails->gas1 = gas1; + m_deviceDetails->gas2 = gas2; + m_deviceDetails->gas3 = gas3; + m_deviceDetails->gas4 = gas4; + m_deviceDetails->gas5 = gas5; + EMIT_PROGRESS(); + + //Read Dil Values + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + unsigned char dilData[4] = { 0, 0, 0, 0 }; + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL1, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil1.oxygen = dilData[0]; + dil1.helium = dilData[1]; + dil1.type = dilData[2]; + dil1.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL2, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil2.oxygen = dilData[0]; + dil2.helium = dilData[1]; + dil2.type = dilData[2]; + dil2.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL3, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil3.oxygen = dilData[0]; + dil3.helium = dilData[1]; + dil3.type = dilData[2]; + dil3.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL4, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil4.oxygen = dilData[0]; + dil4.helium = dilData[1]; + dil4.type = dilData[2]; + dil4.depth = dilData[3]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_DIL5, dilData, sizeof(dilData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + dil5.oxygen = dilData[0]; + dil5.helium = dilData[1]; + dil5.type = dilData[2]; + dil5.depth = dilData[3]; + EMIT_PROGRESS(); + + m_deviceDetails->dil1 = dil1; + m_deviceDetails->dil2 = dil2; + m_deviceDetails->dil3 = dil3; + m_deviceDetails->dil4 = dil4; + m_deviceDetails->dil5 = dil5; + + //Read set point Values + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + unsigned char spData[2] = { 0, 0 }; + + rc = hw_ostc3_device_config_read(device, OSTC3_SP1, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp1.sp = spData[0]; + sp1.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP2, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp2.sp = spData[0]; + sp2.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP3, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp3.sp = spData[0]; + sp3.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP4, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp4.sp = spData[0]; + sp4.depth = spData[1]; + EMIT_PROGRESS(); + + rc = hw_ostc3_device_config_read(device, OSTC3_SP5, spData, sizeof(spData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + sp5.sp = spData[0]; + sp5.depth = spData[1]; + EMIT_PROGRESS(); + + m_deviceDetails->sp1 = sp1; + m_deviceDetails->sp2 = sp2; + m_deviceDetails->sp3 = sp3; + m_deviceDetails->sp4 = sp4; + m_deviceDetails->sp5 = sp5; + + //Read other settings + unsigned char uData[1] = { 0 }; + +#define READ_SETTING(_OSTC3_SETTING, _DEVICE_DETAIL) \ + do { \ + rc = hw_ostc3_device_config_read(device, _OSTC3_SETTING, uData, sizeof(uData)); \ + if (rc != DC_STATUS_SUCCESS) \ + return rc; \ + m_deviceDetails->_DEVICE_DETAIL = uData[0]; \ + EMIT_PROGRESS(); \ + } while (0) + + READ_SETTING(OSTC3_DIVE_MODE, diveMode); + READ_SETTING(OSTC3_SATURATION, saturation); + READ_SETTING(OSTC3_DESATURATION, desaturation); + READ_SETTING(OSTC3_LAST_DECO, lastDeco); + READ_SETTING(OSTC3_BRIGHTNESS, brightness); + READ_SETTING(OSTC3_UNITS, units); + READ_SETTING(OSTC3_SAMPLING_RATE, samplingRate); + READ_SETTING(OSTC3_SALINITY, salinity); + READ_SETTING(OSTC3_DIVEMODE_COLOR, diveModeColor); + READ_SETTING(OSTC3_LANGUAGE, language); + READ_SETTING(OSTC3_DATE_FORMAT, dateFormat); + READ_SETTING(OSTC3_COMPASS_GAIN, compassGain); + READ_SETTING(OSTC3_SAFETY_STOP, safetyStop); + READ_SETTING(OSTC3_GF_HIGH, gfHigh); + READ_SETTING(OSTC3_GF_LOW, gfLow); + READ_SETTING(OSTC3_PPO2_MIN, ppO2Min); + READ_SETTING(OSTC3_PPO2_MAX, ppO2Max); + READ_SETTING(OSTC3_FUTURE_TTS, futureTTS); + READ_SETTING(OSTC3_CCR_MODE, ccrMode); + READ_SETTING(OSTC3_DECO_TYPE, decoType); + READ_SETTING(OSTC3_AGF_SELECTABLE, aGFSelectable); + READ_SETTING(OSTC3_AGF_HIGH, aGFHigh); + READ_SETTING(OSTC3_AGF_LOW, aGFLow); + READ_SETTING(OSTC3_CALIBRATION_GAS_O2, calibrationGas); + READ_SETTING(OSTC3_FLIP_SCREEN, flipScreen); + READ_SETTING(OSTC3_SETPOINT_FALLBACK, setPointFallback); + READ_SETTING(OSTC3_LEFT_BUTTON_SENSIVITY, leftButtonSensitivity); + READ_SETTING(OSTC3_RIGHT_BUTTON_SENSIVITY, rightButtonSensitivity); + READ_SETTING(OSTC3_BOTTOM_GAS_CONSUMPTION, bottomGasConsumption); + READ_SETTING(OSTC3_DECO_GAS_CONSUMPTION, decoGasConsumption); + READ_SETTING(OSTC3_MOD_WARNING, modWarning); + + //Skip things not supported on the sport, if its a sport. + if (m_deviceDetails->model == "Sport") { + EMIT_PROGRESS(); + EMIT_PROGRESS(); + EMIT_PROGRESS(); + } else { + READ_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate); + READ_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator); + READ_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2); + } + +#undef READ_SETTING + + rc = hw_ostc3_device_config_read(device, OSTC3_PRESSURE_SENSOR_OFFSET, uData, sizeof(uData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + // OSTC3 stores the pressureSensorOffset in two-complement + m_deviceDetails->pressureSensorOffset = (signed char)uData[0]; + EMIT_PROGRESS(); + + //read firmware settings + unsigned char fData[64] = { 0 }; + rc = hw_ostc3_device_version(device, fData, sizeof(fData)); + if (rc != DC_STATUS_SUCCESS) + return rc; + int serial = fData[0] + (fData[1] << 8); + m_deviceDetails->serialNo = QString::number(serial); + m_deviceDetails->firmwareVersion = QString::number(fData[2]) + "." + QString::number(fData[3]); + QByteArray ar((char *)fData + 4, 60); + m_deviceDetails->customText = ar.trimmed(); + EMIT_PROGRESS(); + + return rc; +} + +static dc_status_t write_ostc3_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 51; + + //write gas values + unsigned char gas1Data[4] = { + m_deviceDetails->gas1.oxygen, + m_deviceDetails->gas1.helium, + m_deviceDetails->gas1.type, + m_deviceDetails->gas1.depth + }; + + unsigned char gas2Data[4] = { + m_deviceDetails->gas2.oxygen, + m_deviceDetails->gas2.helium, + m_deviceDetails->gas2.type, + m_deviceDetails->gas2.depth + }; + + unsigned char gas3Data[4] = { + m_deviceDetails->gas3.oxygen, + m_deviceDetails->gas3.helium, + m_deviceDetails->gas3.type, + m_deviceDetails->gas3.depth + }; + + unsigned char gas4Data[4] = { + m_deviceDetails->gas4.oxygen, + m_deviceDetails->gas4.helium, + m_deviceDetails->gas4.type, + m_deviceDetails->gas4.depth + }; + + unsigned char gas5Data[4] = { + m_deviceDetails->gas5.oxygen, + m_deviceDetails->gas5.helium, + m_deviceDetails->gas5.type, + m_deviceDetails->gas5.depth + }; + //gas 1 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS1, gas1Data, sizeof(gas1Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 2 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS2, gas2Data, sizeof(gas2Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 3 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS3, gas3Data, sizeof(gas3Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 4 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS4, gas4Data, sizeof(gas4Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //gas 5 + rc = hw_ostc3_device_config_write(device, OSTC3_GAS5, gas5Data, sizeof(gas5Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //write set point values + unsigned char sp1Data[2] = { + m_deviceDetails->sp1.sp, + m_deviceDetails->sp1.depth + }; + + unsigned char sp2Data[2] = { + m_deviceDetails->sp2.sp, + m_deviceDetails->sp2.depth + }; + + unsigned char sp3Data[2] = { + m_deviceDetails->sp3.sp, + m_deviceDetails->sp3.depth + }; + + unsigned char sp4Data[2] = { + m_deviceDetails->sp4.sp, + m_deviceDetails->sp4.depth + }; + + unsigned char sp5Data[2] = { + m_deviceDetails->sp5.sp, + m_deviceDetails->sp5.depth + }; + + //sp 1 + rc = hw_ostc3_device_config_write(device, OSTC3_SP1, sp1Data, sizeof(sp1Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 2 + rc = hw_ostc3_device_config_write(device, OSTC3_SP2, sp2Data, sizeof(sp2Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 3 + rc = hw_ostc3_device_config_write(device, OSTC3_SP3, sp3Data, sizeof(sp3Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 4 + rc = hw_ostc3_device_config_write(device, OSTC3_SP4, sp4Data, sizeof(sp4Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //sp 5 + rc = hw_ostc3_device_config_write(device, OSTC3_SP5, sp5Data, sizeof(sp5Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //write dil values + unsigned char dil1Data[4] = { + m_deviceDetails->dil1.oxygen, + m_deviceDetails->dil1.helium, + m_deviceDetails->dil1.type, + m_deviceDetails->dil1.depth + }; + + unsigned char dil2Data[4] = { + m_deviceDetails->dil2.oxygen, + m_deviceDetails->dil2.helium, + m_deviceDetails->dil2.type, + m_deviceDetails->dil2.depth + }; + + unsigned char dil3Data[4] = { + m_deviceDetails->dil3.oxygen, + m_deviceDetails->dil3.helium, + m_deviceDetails->dil3.type, + m_deviceDetails->dil3.depth + }; + + unsigned char dil4Data[4] = { + m_deviceDetails->dil4.oxygen, + m_deviceDetails->dil4.helium, + m_deviceDetails->dil4.type, + m_deviceDetails->dil4.depth + }; + + unsigned char dil5Data[4] = { + m_deviceDetails->dil5.oxygen, + m_deviceDetails->dil5.helium, + m_deviceDetails->dil5.type, + m_deviceDetails->dil5.depth + }; + //dil 1 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL1, dil1Data, sizeof(gas1Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 2 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL2, dil2Data, sizeof(dil2Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 3 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL3, dil3Data, sizeof(dil3Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 4 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL4, dil4Data, sizeof(dil4Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //dil 5 + rc = hw_ostc3_device_config_write(device, OSTC3_DIL5, dil5Data, sizeof(dil5Data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //write general settings + //custom text + rc = hw_ostc3_device_customtext(device, m_deviceDetails->customText.toUtf8().data()); + if (rc != DC_STATUS_SUCCESS) + return rc; + + unsigned char data[1] = { 0 }; +#define WRITE_SETTING(_OSTC3_SETTING, _DEVICE_DETAIL) \ + do { \ + data[0] = m_deviceDetails->_DEVICE_DETAIL; \ + rc = hw_ostc3_device_config_write(device, _OSTC3_SETTING, data, sizeof(data)); \ + if (rc != DC_STATUS_SUCCESS) \ + return rc; \ + EMIT_PROGRESS(); \ + } while (0) + + WRITE_SETTING(OSTC3_DIVE_MODE, diveMode); + WRITE_SETTING(OSTC3_SATURATION, saturation); + WRITE_SETTING(OSTC3_DESATURATION, desaturation); + WRITE_SETTING(OSTC3_LAST_DECO, lastDeco); + WRITE_SETTING(OSTC3_BRIGHTNESS, brightness); + WRITE_SETTING(OSTC3_UNITS, units); + WRITE_SETTING(OSTC3_SAMPLING_RATE, samplingRate); + WRITE_SETTING(OSTC3_SALINITY, salinity); + WRITE_SETTING(OSTC3_DIVEMODE_COLOR, diveModeColor); + WRITE_SETTING(OSTC3_LANGUAGE, language); + WRITE_SETTING(OSTC3_DATE_FORMAT, dateFormat); + WRITE_SETTING(OSTC3_COMPASS_GAIN, compassGain); + WRITE_SETTING(OSTC3_SAFETY_STOP, safetyStop); + WRITE_SETTING(OSTC3_GF_HIGH, gfHigh); + WRITE_SETTING(OSTC3_GF_LOW, gfLow); + WRITE_SETTING(OSTC3_PPO2_MIN, ppO2Min); + WRITE_SETTING(OSTC3_PPO2_MAX, ppO2Max); + WRITE_SETTING(OSTC3_FUTURE_TTS, futureTTS); + WRITE_SETTING(OSTC3_CCR_MODE, ccrMode); + WRITE_SETTING(OSTC3_DECO_TYPE, decoType); + WRITE_SETTING(OSTC3_AGF_SELECTABLE, aGFSelectable); + WRITE_SETTING(OSTC3_AGF_HIGH, aGFHigh); + WRITE_SETTING(OSTC3_AGF_LOW, aGFLow); + WRITE_SETTING(OSTC3_CALIBRATION_GAS_O2, calibrationGas); + WRITE_SETTING(OSTC3_FLIP_SCREEN, flipScreen); + WRITE_SETTING(OSTC3_SETPOINT_FALLBACK, setPointFallback); + WRITE_SETTING(OSTC3_LEFT_BUTTON_SENSIVITY, leftButtonSensitivity); + WRITE_SETTING(OSTC3_RIGHT_BUTTON_SENSIVITY, rightButtonSensitivity); + WRITE_SETTING(OSTC3_BOTTOM_GAS_CONSUMPTION, bottomGasConsumption); + WRITE_SETTING(OSTC3_DECO_GAS_CONSUMPTION, decoGasConsumption); + WRITE_SETTING(OSTC3_MOD_WARNING, modWarning); + + //Skip things not supported on the sport, if its a sport. + if (m_deviceDetails->model == "Sport") { + EMIT_PROGRESS(); + EMIT_PROGRESS(); + EMIT_PROGRESS(); + } else { + WRITE_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate); + WRITE_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator); + WRITE_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2); + } + +#undef WRITE_SETTING + + // OSTC3 stores the pressureSensorOffset in two-complement + data[0] = (unsigned char)m_deviceDetails->pressureSensorOffset; + rc = hw_ostc3_device_config_write(device, OSTC3_PRESSURE_SENSOR_OFFSET, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //sync date and time + if (m_deviceDetails->syncTime) { + QDateTime timeToSet = QDateTime::currentDateTime(); + dc_datetime_t time; + time.year = timeToSet.date().year(); + time.month = timeToSet.date().month(); + time.day = timeToSet.date().day(); + time.hour = timeToSet.time().hour(); + time.minute = timeToSet.time().minute(); + time.second = timeToSet.time().second(); + rc = hw_ostc3_device_clock(device, &time); + } + EMIT_PROGRESS(); + + return rc; +} +#endif /* DC_VERSION_CHECK(0, 5, 0) */ + +static dc_status_t read_ostc_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 3; + + unsigned char data[256] = {}; +#ifdef DEBUG_OSTC_CF + // FIXME: how should we report settings not supported back? + unsigned char max_CF = 0; +#endif + rc = hw_ostc_device_eeprom_read(device, 0, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + m_deviceDetails->serialNo = QString::number(data[1] << 8 ^ data[0]); + m_deviceDetails->numberOfDives = data[3] << 8 ^ data[2]; + //Byte5-6: + //Gas 1 default (%O2=21, %He=0) + gas gas1; + gas1.oxygen = data[6]; + gas1.helium = data[7]; + //Byte9-10: + //Gas 2 default (%O2=21, %He=0) + gas gas2; + gas2.oxygen = data[10]; + gas2.helium = data[11]; + //Byte13-14: + //Gas 3 default (%O2=21, %He=0) + gas gas3; + gas3.oxygen = data[14]; + gas3.helium = data[15]; + //Byte17-18: + //Gas 4 default (%O2=21, %He=0) + gas gas4; + gas4.oxygen = data[18]; + gas4.helium = data[19]; + //Byte21-22: + //Gas 5 default (%O2=21, %He=0) + gas gas5; + gas5.oxygen = data[22]; + gas5.helium = data[23]; + //Byte25-26: + //Gas 6 current (%O2, %He) + m_deviceDetails->salinity = data[26]; + // Active Gas Flag Register + gas1.type = data[27] & 0x01; + gas2.type = (data[27] & 0x02) >> 1; + gas3.type = (data[27] & 0x04) >> 2; + gas4.type = (data[27] & 0x08) >> 3; + gas5.type = (data[27] & 0x10) >> 4; + + // Gas switch depths + gas1.depth = data[28]; + gas2.depth = data[29]; + gas3.depth = data[30]; + gas4.depth = data[31]; + gas5.depth = data[32]; + // 33 which gas is Fist gas + switch (data[33]) { + case 1: + gas1.type = 2; + break; + case 2: + gas2.type = 2; + break; + case 3: + gas3.type = 2; + break; + case 4: + gas4.type = 2; + break; + case 5: + gas5.type = 2; + break; + default: + //Error? + break; + } + // Data filled up, set the gases. + m_deviceDetails->gas1 = gas1; + m_deviceDetails->gas2 = gas2; + m_deviceDetails->gas3 = gas3; + m_deviceDetails->gas4 = gas4; + m_deviceDetails->gas5 = gas5; + m_deviceDetails->decoType = data[34]; + //Byte36: + //Use O2 Sensor Module in CC Modes (0= OFF, 1= ON) (Only available in old OSTC1 - unused for OSTC Mk.2/2N) + //m_deviceDetails->ccrMode = data[35]; + setpoint sp1; + sp1.sp = data[36]; + sp1.depth = 0; + setpoint sp2; + sp2.sp = data[37]; + sp2.depth = 0; + setpoint sp3; + sp3.sp = data[38]; + sp3.depth = 0; + m_deviceDetails->sp1 = sp1; + m_deviceDetails->sp2 = sp2; + m_deviceDetails->sp3 = sp3; + // Byte41-42: + // Lowest Battery voltage seen (in mV) + // Byte43: + // Lowest Battery voltage seen at (Month) + // Byte44: + // Lowest Battery voltage seen at (Day) + // Byte45: + // Lowest Battery voltage seen at (Year) + // Byte46-47: + // Lowest Battery voltage seen at (Temperature in 0.1 °C) + // Byte48: + // Last complete charge at (Month) + // Byte49: + // Last complete charge at (Day) + // Byte50: + // Last complete charge at (Year) + // Byte51-52: + // Total charge cycles + // Byte53-54: + // Total complete charge cycles + // Byte55-56: + // Temperature Extrema minimum (Temperature in 0.1 °C) + // Byte57: + // Temperature Extrema minimum at (Month) + // Byte58: + // Temperature Extrema minimum at (Day) + // Byte59: + // Temperature Extrema minimum at (Year) + // Byte60-61: + // Temperature Extrema maximum (Temperature in 0.1 °C) + // Byte62: + // Temperature Extrema maximum at (Month) + // Byte63: + // Temperature Extrema maximum at (Day) + // Byte64: + // Temperature Extrema maximum at (Year) + // Byte65: + // Custom Text active (=1), Custom Text Disabled (<>1) + // Byte66-90: + // TO FIX EDITOR SYNTAX/INDENT { + // (25Bytes): Custom Text for Surfacemode (Real text must end with "}") + // Example: OSTC Dive Computer} (19 Characters incl. "}") Bytes 85-90 will be ignored. + if (data[64] == 1) { + // Make shure the data is null-terminated + data[89] = 0; + // Find the internal termination and replace it with 0 + char *term = strchr((char *)data + 65, (int)'}'); + if (term) + *term = 0; + m_deviceDetails->customText = (const char *)data + 65; + } + // Byte91: + // Dim OLED in Divemode (>0), Normal mode (=0) + // Byte92: + // Date format for all outputs: + // =0: MM/DD/YY + // =1: DD/MM/YY + // =2: YY/MM/DD + m_deviceDetails->dateFormat = data[91]; +// Byte93: +// Total number of CF used in installed firmware +#ifdef DEBUG_OSTC_CF + max_CF = data[92]; +#endif + // Byte94: + // Last selected view for customview area in surface mode + // Byte95: + // Last selected view for customview area in dive mode + // Byte96-97: + // Diluent 1 Default (%O2,%He) + // Byte98-99: + // Diluent 1 Current (%O2,%He) + gas dil1 = {}; + dil1.oxygen = data[97]; + dil1.helium = data[98]; + // Byte100-101: + // Gasuent 2 Default (%O2,%He) + // Byte102-103: + // Gasuent 2 Current (%O2,%He) + gas dil2 = {}; + dil2.oxygen = data[101]; + dil2.helium = data[102]; + // Byte104-105: + // Gasuent 3 Default (%O2,%He) + // Byte106-107: + // Gasuent 3 Current (%O2,%He) + gas dil3 = {}; + dil3.oxygen = data[105]; + dil3.helium = data[106]; + // Byte108-109: + // Gasuent 4 Default (%O2,%He) + // Byte110-111: + // Gasuent 4 Current (%O2,%He) + gas dil4 = {}; + dil4.oxygen = data[109]; + dil4.helium = data[110]; + // Byte112-113: + // Gasuent 5 Default (%O2,%He) + // Byte114-115: + // Gasuent 5 Current (%O2,%He) + gas dil5 = {}; + dil5.oxygen = data[113]; + dil5.helium = data[114]; + // Byte116: + // First Diluent (1-5) + switch (data[115]) { + case 1: + dil1.type = 2; + break; + case 2: + dil2.type = 2; + break; + case 3: + dil3.type = 2; + break; + case 4: + dil4.type = 2; + break; + case 5: + dil5.type = 2; + break; + default: + //Error? + break; + } + m_deviceDetails->dil1 = dil1; + m_deviceDetails->dil2 = dil2; + m_deviceDetails->dil3 = dil3; + m_deviceDetails->dil4 = dil4; + m_deviceDetails->dil5 = dil5; + // Byte117-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF0-CF31) + + // Decode the relevant ones + // CF11: Factor for saturation processes + m_deviceDetails->saturation = read_ostc_cf(data, 11); + // CF12: Factor for desaturation processes + m_deviceDetails->desaturation = read_ostc_cf(data, 12); + // CF17: Lower threshold for ppO2 warning + m_deviceDetails->ppO2Min = read_ostc_cf(data, 17); + // CF18: Upper threshold for ppO2 warning + m_deviceDetails->ppO2Max = read_ostc_cf(data, 18); + // CF20: Depth sampling rate for Profile storage + m_deviceDetails->samplingRate = read_ostc_cf(data, 20); + // CF29: Depth of last decompression stop + m_deviceDetails->lastDeco = read_ostc_cf(data, 29); + +#ifdef DEBUG_OSTC_CF + for (int cf = 0; cf <= 31 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + + rc = hw_ostc_device_eeprom_read(device, 1, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1: + // Logbook version indicator (Not writable!) + // Byte2-3: + // Last Firmware installed, 1st Byte.2nd Byte (e.g. „1.90“) (Not writable!) + m_deviceDetails->firmwareVersion = QString::number(data[1]) + "." + QString::number(data[2]); + // Byte4: + // OLED brightness (=0: Eco, =1 High) (Not writable!) + // Byte5-11: + // Time/Date vault during firmware updates + // Byte12-128 + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 32-63) + + // Decode the relevant ones + // CF32: Gradient Factor low + m_deviceDetails->gfLow = read_ostc_cf(data, 32); + // CF33: Gradient Factor high + m_deviceDetails->gfHigh = read_ostc_cf(data, 33); + // CF56: Bottom gas consumption + m_deviceDetails->bottomGasConsumption = read_ostc_cf(data, 56); + // CF57: Ascent gas consumption + m_deviceDetails->decoGasConsumption = read_ostc_cf(data, 57); + // CF58: Future time to surface setFutureTTS + m_deviceDetails->futureTTS = read_ostc_cf(data, 58); + +#ifdef DEBUG_OSTC_CF + for (int cf = 32; cf <= 63 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + + rc = hw_ostc_device_eeprom_read(device, 2, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1-4: + // not used/reserved (Not writable!) + // Byte5-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 64-95) + + // Decode the relevant ones + // CF60: Graphic velocity + m_deviceDetails->graphicalSpeedIndicator = read_ostc_cf(data, 60); + // CF65: Show safety stop + m_deviceDetails->safetyStop = read_ostc_cf(data, 65); + // CF67: Alternaitve Gradient Factor low + m_deviceDetails->aGFLow = read_ostc_cf(data, 67); + // CF68: Alternative Gradient Factor high + m_deviceDetails->aGFHigh = read_ostc_cf(data, 68); + // CF69: Allow Gradient Factor change + m_deviceDetails->aGFSelectable = read_ostc_cf(data, 69); +#ifdef DEBUG_OSTC_CF + for (int cf = 64; cf <= 95 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + + return rc; +} + +static dc_status_t write_ostc_settings(dc_device_t *device, DeviceDetails *m_deviceDetails, dc_event_callback_t progress_cb, void *userdata) +{ + dc_status_t rc; + dc_event_progress_t progress; + progress.current = 0; + progress.maximum = 7; + unsigned char data[256] = {}; + unsigned char max_CF = 0; + + // Because we write whole memory blocks, we read all the current + // values out and then change then ones we should change. + rc = hw_ostc_device_eeprom_read(device, 0, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + //Byte5-6: + //Gas 1 default (%O2=21, %He=0) + gas gas1 = m_deviceDetails->gas1; + data[6] = gas1.oxygen; + data[7] = gas1.helium; + //Byte9-10: + //Gas 2 default (%O2=21, %He=0) + gas gas2 = m_deviceDetails->gas2; + data[10] = gas2.oxygen; + data[11] = gas2.helium; + //Byte13-14: + //Gas 3 default (%O2=21, %He=0) + gas gas3 = m_deviceDetails->gas3; + data[14] = gas3.oxygen; + data[15] = gas3.helium; + //Byte17-18: + //Gas 4 default (%O2=21, %He=0) + gas gas4 = m_deviceDetails->gas4; + data[18] = gas4.oxygen; + data[19] = gas4.helium; + //Byte21-22: + //Gas 5 default (%O2=21, %He=0) + gas gas5 = m_deviceDetails->gas5; + data[22] = gas5.oxygen; + data[23] = gas5.helium; + //Byte25-26: + //Gas 6 current (%O2, %He) + data[26] = m_deviceDetails->salinity; + // Gas types, 0=Disabled, 1=Active, 2=Fist + // Active Gas Flag Register + data[27] = 0; + if (gas1.type) + data[27] ^= 0x01; + if (gas2.type) + data[27] ^= 0x02; + if (gas3.type) + data[27] ^= 0x04; + if (gas4.type) + data[27] ^= 0x08; + if (gas5.type) + data[27] ^= 0x10; + + // Gas switch depths + data[28] = gas1.depth; + data[29] = gas2.depth; + data[30] = gas3.depth; + data[31] = gas4.depth; + data[32] = gas5.depth; + // 33 which gas is Fist gas + if (gas1.type == 2) + data[33] = 1; + else if (gas2.type == 2) + data[33] = 2; + else if (gas3.type == 2) + data[33] = 3; + else if (gas4.type == 2) + data[33] = 4; + else if (gas5.type == 2) + data[33] = 5; + else + // FIXME: No gas was First? + // Set gas 1 to first + data[33] = 1; + + data[34] = m_deviceDetails->decoType; + //Byte36: + //Use O2 Sensor Module in CC Modes (0= OFF, 1= ON) (Only available in old OSTC1 - unused for OSTC Mk.2/2N) + //m_deviceDetails->ccrMode = data[35]; + data[36] = m_deviceDetails->sp1.sp; + data[37] = m_deviceDetails->sp2.sp; + data[38] = m_deviceDetails->sp3.sp; + // Byte41-42: + // Lowest Battery voltage seen (in mV) + // Byte43: + // Lowest Battery voltage seen at (Month) + // Byte44: + // Lowest Battery voltage seen at (Day) + // Byte45: + // Lowest Battery voltage seen at (Year) + // Byte46-47: + // Lowest Battery voltage seen at (Temperature in 0.1 °C) + // Byte48: + // Last complete charge at (Month) + // Byte49: + // Last complete charge at (Day) + // Byte50: + // Last complete charge at (Year) + // Byte51-52: + // Total charge cycles + // Byte53-54: + // Total complete charge cycles + // Byte55-56: + // Temperature Extrema minimum (Temperature in 0.1 °C) + // Byte57: + // Temperature Extrema minimum at (Month) + // Byte58: + // Temperature Extrema minimum at (Day) + // Byte59: + // Temperature Extrema minimum at (Year) + // Byte60-61: + // Temperature Extrema maximum (Temperature in 0.1 °C) + // Byte62: + // Temperature Extrema maximum at (Month) + // Byte63: + // Temperature Extrema maximum at (Day) + // Byte64: + // Temperature Extrema maximum at (Year) + // Byte65: + // Custom Text active (=1), Custom Text Disabled (<>1) + // Byte66-90: + // (25Bytes): Custom Text for Surfacemode (Real text must end with "}") + // Example: "OSTC Dive Computer}" (19 Characters incl. "}") Bytes 85-90 will be ignored. + if (m_deviceDetails->customText == "") { + data[64] = 0; + } else { + data[64] = 1; + // Copy the string to the right place in the memory, padded with 0x20 (" ") + strncpy((char *)data + 65, QString("%1").arg(m_deviceDetails->customText, -23, QChar(' ')).toUtf8().data(), 23); + // And terminate the string. + if (m_deviceDetails->customText.length() <= 23) + data[65 + m_deviceDetails->customText.length()] = '}'; + else + data[90] = '}'; + } + // Byte91: + // Dim OLED in Divemode (>0), Normal mode (=0) + // Byte92: + // Date format for all outputs: + // =0: MM/DD/YY + // =1: DD/MM/YY + // =2: YY/MM/DD + data[91] = m_deviceDetails->dateFormat; + // Byte93: + // Total number of CF used in installed firmware + max_CF = data[92]; + // Byte94: + // Last selected view for customview area in surface mode + // Byte95: + // Last selected view for customview area in dive mode + // Byte96-97: + // Diluent 1 Default (%O2,%He) + // Byte98-99: + // Diluent 1 Current (%O2,%He) + gas dil1 = m_deviceDetails->dil1; + data[97] = dil1.oxygen; + data[98] = dil1.helium; + // Byte100-101: + // Gasuent 2 Default (%O2,%He) + // Byte102-103: + // Gasuent 2 Current (%O2,%He) + gas dil2 = m_deviceDetails->dil2; + data[101] = dil2.oxygen; + data[102] = dil2.helium; + // Byte104-105: + // Gasuent 3 Default (%O2,%He) + // Byte106-107: + // Gasuent 3 Current (%O2,%He) + gas dil3 = m_deviceDetails->dil3; + data[105] = dil3.oxygen; + data[106] = dil3.helium; + // Byte108-109: + // Gasuent 4 Default (%O2,%He) + // Byte110-111: + // Gasuent 4 Current (%O2,%He) + gas dil4 = m_deviceDetails->dil4; + data[109] = dil4.oxygen; + data[110] = dil4.helium; + // Byte112-113: + // Gasuent 5 Default (%O2,%He) + // Byte114-115: + // Gasuent 5 Current (%O2,%He) + gas dil5 = m_deviceDetails->dil5; + data[113] = dil5.oxygen; + data[114] = dil5.helium; + // Byte116: + // First Diluent (1-5) + if (dil1.type == 2) + data[115] = 1; + else if (dil2.type == 2) + data[115] = 2; + else if (dil3.type == 2) + data[115] = 3; + else if (dil4.type == 2) + data[115] = 4; + else if (dil5.type == 2) + data[115] = 5; + else + // FIXME: No first diluent? + // Set gas 1 to fist + data[115] = 1; + + // Byte117-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF0-CF31) + + // Write the relevant ones + // CF11: Factor for saturation processes + write_ostc_cf(data, 11, max_CF, m_deviceDetails->saturation); + // CF12: Factor for desaturation processes + write_ostc_cf(data, 12, max_CF, m_deviceDetails->desaturation); + // CF17: Lower threshold for ppO2 warning + write_ostc_cf(data, 17, max_CF, m_deviceDetails->ppO2Min); + // CF18: Upper threshold for ppO2 warning + write_ostc_cf(data, 18, max_CF, m_deviceDetails->ppO2Max); + // CF20: Depth sampling rate for Profile storage + write_ostc_cf(data, 20, max_CF, m_deviceDetails->samplingRate); + // CF29: Depth of last decompression stop + write_ostc_cf(data, 29, max_CF, m_deviceDetails->lastDeco); + +#ifdef DEBUG_OSTC_CF + for (int cf = 0; cf <= 31 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + rc = hw_ostc_device_eeprom_write(device, 0, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + rc = hw_ostc_device_eeprom_read(device, 1, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1: + // Logbook version indicator (Not writable!) + // Byte2-3: + // Last Firmware installed, 1st Byte.2nd Byte (e.g. „1.90“) (Not writable!) + // Byte4: + // OLED brightness (=0: Eco, =1 High) (Not writable!) + // Byte5-11: + // Time/Date vault during firmware updates + // Byte12-128 + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 32-63) + + // Decode the relevant ones + // CF32: Gradient Factor low + write_ostc_cf(data, 32, max_CF, m_deviceDetails->gfLow); + // CF33: Gradient Factor high + write_ostc_cf(data, 33, max_CF, m_deviceDetails->gfHigh); + // CF56: Bottom gas consumption + write_ostc_cf(data, 56, max_CF, m_deviceDetails->bottomGasConsumption); + // CF57: Ascent gas consumption + write_ostc_cf(data, 57, max_CF, m_deviceDetails->decoGasConsumption); + // CF58: Future time to surface setFutureTTS + write_ostc_cf(data, 58, max_CF, m_deviceDetails->futureTTS); +#ifdef DEBUG_OSTC_CF + for (int cf = 32; cf <= 63 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + rc = hw_ostc_device_eeprom_write(device, 1, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + rc = hw_ostc_device_eeprom_read(device, 2, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + // Byte1-4: + // not used/reserved (Not writable!) + // Byte5-128: + // not used/reserved + // Byte129-256: + // 32 custom Functions (CF 64-95) + + // Decode the relevant ones + // CF60: Graphic velocity + write_ostc_cf(data, 60, max_CF, m_deviceDetails->graphicalSpeedIndicator); + // CF65: Show safety stop + write_ostc_cf(data, 65, max_CF, m_deviceDetails->safetyStop); + // CF67: Alternaitve Gradient Factor low + write_ostc_cf(data, 67, max_CF, m_deviceDetails->aGFLow); + // CF68: Alternative Gradient Factor high + write_ostc_cf(data, 68, max_CF, m_deviceDetails->aGFHigh); + // CF69: Allow Gradient Factor change + write_ostc_cf(data, 69, max_CF, m_deviceDetails->aGFSelectable); +#ifdef DEBUG_OSTC_CF + for (int cf = 64; cf <= 95 && cf <= max_CF; cf++) + printf("CF %d: %d\n", cf, read_ostc_cf(data, cf)); +#endif + rc = hw_ostc_device_eeprom_write(device, 2, data, sizeof(data)); + if (rc != DC_STATUS_SUCCESS) + return rc; + EMIT_PROGRESS(); + + //sync date and time + if (m_deviceDetails->syncTime) { + QDateTime timeToSet = QDateTime::currentDateTime(); + dc_datetime_t time; + time.year = timeToSet.date().year(); + time.month = timeToSet.date().month(); + time.day = timeToSet.date().day(); + time.hour = timeToSet.time().hour(); + time.minute = timeToSet.time().minute(); + time.second = timeToSet.time().second(); + rc = hw_ostc_device_clock(device, &time); + } + EMIT_PROGRESS(); + return rc; +} + +#undef EMIT_PROGRESS + +DeviceThread::DeviceThread(QObject *parent, device_data_t *data) : QThread(parent), m_data(data) +{ +} + +void DeviceThread::progressCB(int percent) +{ + emit progress(percent); +} + +void DeviceThread::event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata) +{ + const dc_event_progress_t *progress = (dc_event_progress_t *) data; + DeviceThread *dt = static_cast(userdata); + + switch (event) { + case DC_EVENT_PROGRESS: + dt->progressCB(100.0 * (double)progress->current / (double)progress->maximum); + break; + default: + emit dt->error("Unexpected event recived"); + break; + } +} + +ReadSettingsThread::ReadSettingsThread(QObject *parent, device_data_t *data) : DeviceThread(parent, data) +{ +} + +void ReadSettingsThread::run() +{ + dc_status_t rc; + + DeviceDetails *m_deviceDetails = new DeviceDetails(0); + switch (dc_device_get_type(m_data->device)) { + case DC_FAMILY_SUUNTO_VYPER: + rc = read_suunto_vyper_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_SUCCESS) { + emit devicedetails(m_deviceDetails); + } else if (rc == DC_STATUS_UNSUPPORTED) { + emit error(tr("This feature is not yet available for the selected dive computer.")); + } else { + emit error("Failed!"); + } + break; +#if DC_VERSION_CHECK(0, 5, 0) + case DC_FAMILY_HW_OSTC3: + rc = read_ostc3_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_SUCCESS) + emit devicedetails(m_deviceDetails); + else + emit error("Failed!"); + break; +#endif // divecomputer 0.5.0 +#ifdef DEBUG_OSTC + case DC_FAMILY_NULL: +#endif + case DC_FAMILY_HW_OSTC: + rc = read_ostc_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_SUCCESS) + emit devicedetails(m_deviceDetails); + else + emit error("Failed!"); + break; + default: + emit error(tr("This feature is not yet available for the selected dive computer.")); + break; + } +} + +WriteSettingsThread::WriteSettingsThread(QObject *parent, device_data_t *data) : + DeviceThread(parent, data), + m_deviceDetails(NULL) +{ +} + +void WriteSettingsThread::setDeviceDetails(DeviceDetails *details) +{ + m_deviceDetails = details; +} + +void WriteSettingsThread::run() +{ + dc_status_t rc; + + switch (dc_device_get_type(m_data->device)) { + case DC_FAMILY_SUUNTO_VYPER: + rc = write_suunto_vyper_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc == DC_STATUS_UNSUPPORTED) { + emit error(tr("This feature is not yet available for the selected dive computer.")); + } else if (rc != DC_STATUS_SUCCESS) { + emit error(tr("Failed!")); + } + break; +#if DC_VERSION_CHECK(0, 5, 0) + case DC_FAMILY_HW_OSTC3: + rc = write_ostc3_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc != DC_STATUS_SUCCESS) + emit error(tr("Failed!")); + break; +#endif // divecomputer 0.5.0 +#ifdef DEBUG_OSTC + case DC_FAMILY_NULL: +#endif + case DC_FAMILY_HW_OSTC: + rc = write_ostc_settings(m_data->device, m_deviceDetails, DeviceThread::event_cb, this); + if (rc != DC_STATUS_SUCCESS) + emit error(tr("Failed!")); + break; + default: + emit error(tr("This feature is not yet available for the selected dive computer.")); + break; + } +} + + +FirmwareUpdateThread::FirmwareUpdateThread(QObject *parent, device_data_t *data, QString fileName) : DeviceThread(parent, data), m_fileName(fileName) +{ +} + +void FirmwareUpdateThread::run() +{ + dc_status_t rc; + + rc = dc_device_set_events(m_data->device, DC_EVENT_PROGRESS, DeviceThread::event_cb, this); + if (rc != DC_STATUS_SUCCESS) { + emit error("Error registering the event handler."); + return; + } + switch (dc_device_get_type(m_data->device)) { +#if DC_VERSION_CHECK(0, 5, 0) + case DC_FAMILY_HW_OSTC3: + rc = hw_ostc3_device_fwupdate(m_data->device, m_fileName.toUtf8().data()); + break; + case DC_FAMILY_HW_OSTC: + rc = hw_ostc_device_fwupdate(m_data->device, m_fileName.toUtf8().data()); + break; +#endif // divecomputer 0.5.0 + default: + emit error(tr("This feature is not yet available for the selected dive computer.")); + return; + } + + if (rc != DC_STATUS_SUCCESS) { + emit error(tr("Firmware update failed!")); + } +} + + +ResetSettingsThread::ResetSettingsThread(QObject *parent, device_data_t *data) : DeviceThread(parent, data) +{ +} + +void ResetSettingsThread::run() +{ + dc_status_t rc = DC_STATUS_SUCCESS; + +#if DC_VERSION_CHECK(0, 5, 0) + if (dc_device_get_type(m_data->device) == DC_FAMILY_HW_OSTC3) { + rc = hw_ostc3_device_config_reset(m_data->device); + emit progress(100); + } +#endif // divecomputer 0.5.0 + if (rc != DC_STATUS_SUCCESS) { + emit error(tr("Reset settings failed!")); + } +} diff --git a/subsurface-core/configuredivecomputerthreads.h b/subsurface-core/configuredivecomputerthreads.h new file mode 100644 index 000000000..1d7a36f9b --- /dev/null +++ b/subsurface-core/configuredivecomputerthreads.h @@ -0,0 +1,62 @@ +#ifndef CONFIGUREDIVECOMPUTERTHREADS_H +#define CONFIGUREDIVECOMPUTERTHREADS_H + +#include +#include +#include +#include "libdivecomputer.h" +#include +#include "devicedetails.h" + +class DeviceThread : public QThread { + Q_OBJECT +public: + DeviceThread(QObject *parent, device_data_t *data); + virtual void run() = 0; +signals: + void error(QString err); + void progress(int value); +protected: + device_data_t *m_data; + void progressCB(int value); + static void event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata); +}; + +class ReadSettingsThread : public DeviceThread { + Q_OBJECT +public: + ReadSettingsThread(QObject *parent, device_data_t *data); + void run(); +signals: + void devicedetails(DeviceDetails *newDeviceDetails); +}; + +class WriteSettingsThread : public DeviceThread { + Q_OBJECT +public: + WriteSettingsThread(QObject *parent, device_data_t *data); + void setDeviceDetails(DeviceDetails *details); + void run(); + +private: + DeviceDetails *m_deviceDetails; +}; + +class FirmwareUpdateThread : public DeviceThread { + Q_OBJECT +public: + FirmwareUpdateThread(QObject *parent, device_data_t *data, QString fileName); + void run(); + +private: + QString m_fileName; +}; + +class ResetSettingsThread : public DeviceThread { + Q_OBJECT +public: + ResetSettingsThread(QObject *parent, device_data_t *data); + void run(); +}; + +#endif // CONFIGUREDIVECOMPUTERTHREADS_H diff --git a/subsurface-core/datatrak.c b/subsurface-core/datatrak.c new file mode 100644 index 000000000..03fa71a50 --- /dev/null +++ b/subsurface-core/datatrak.c @@ -0,0 +1,695 @@ +#include +#include +#include +#include + +#include "datatrak.h" +#include "dive.h" +#include "units.h" +#include "device.h" +#include "gettext.h" + +extern struct sample *add_sample(struct sample *sample, int time, struct divecomputer *dc); + +unsigned char lector_bytes[2], lector_word[4], tmp_1byte, *byte; +unsigned int tmp_2bytes; +char is_nitrox, is_O2, is_SCR; +unsigned long tmp_4bytes; + +static unsigned int two_bytes_to_int(unsigned char x, unsigned char y) +{ + return (x << 8) + y; +} + +static unsigned long four_bytes_to_long(unsigned char x, unsigned char y, unsigned char z, unsigned char t) +{ + return ((long)x << 24) + ((long)y << 16) + ((long)z << 8) + (long)t; +} + +static unsigned char *byte_to_bits(unsigned char byte) +{ + unsigned char i, *bits = (unsigned char *)malloc(8); + + for (i = 0; i < 8; i++) + bits[i] = byte & (1 << i); + return bits; +} + +/* + * Datatrak stores the date in days since 01-01-1600, while Subsurface uses + * time_t (seconds since 00:00 01-01-1970). Function substracts + * (1970 - 1600) * 365,2425 = 135139,725 to our date variable, getting the + * days since Epoch. + */ +static time_t date_time_to_ssrfc(unsigned long date, int time) +{ + time_t tmp; + tmp = (date - 135140) * 86400 + time * 60; + return tmp; +} + +static unsigned char to_8859(unsigned char char_cp850) +{ + static const unsigned char char_8859[46] = { 0xc7, 0xfc, 0xe9, 0xe2, 0xe4, 0xe0, 0xe5, 0xe7, + 0xea, 0xeb, 0xe8, 0xef, 0xee, 0xec, 0xc4, 0xc5, + 0xc9, 0xe6, 0xc6, 0xf4, 0xf6, 0xf2, 0xfb, 0xf9, + 0xff, 0xd6, 0xdc, 0xf8, 0xa3, 0xd8, 0xd7, 0x66, + 0xe1, 0xed, 0xf3, 0xfa, 0xf1, 0xd1, 0xaa, 0xba, + 0xbf, 0xae, 0xac, 0xbd, 0xbc, 0xa1 }; + return char_8859[char_cp850 - 0x80]; +} + +static char *to_utf8(unsigned char *in_string) +{ + int outlen, inlen, i = 0, j = 0; + inlen = strlen((char *)in_string); + outlen = inlen * 2 + 1; + + char *out_string = calloc(outlen, 1); + for (i = 0; i < inlen; i++) { + if (in_string[i] < 127) + out_string[j] = in_string[i]; + else { + if (in_string[i] > 127 && in_string[i] <= 173) + in_string[i] = to_8859(in_string[i]); + out_string[j] = (in_string[i] >> 6) | 0xC0; + j++; + out_string[j] = (in_string[i] & 0x3F) | 0x80; + } + j++; + } + out_string[j + 1] = '\0'; + return out_string; +} + +/* + * Subsurface sample structure doesn't support the flags and alarms in the dt .log + * so will treat them as dc events. + */ +static struct sample *dtrak_profile(struct dive *dt_dive, FILE *archivo) +{ + int i, j = 1, interval, o2percent = dt_dive->cylinder[0].gasmix.o2.permille / 10; + struct sample *sample = dt_dive->dc.sample; + struct divecomputer *dc = &dt_dive->dc; + + for (i = 1; i <= dt_dive->dc.alloc_samples; i++) { + if (fread(&lector_bytes, 1, 2, archivo) != 2) + return sample; + interval= 20 * (i + 1); + sample = add_sample(sample, interval, dc); + sample->depth.mm = (two_bytes_to_int(lector_bytes[0], lector_bytes[1]) & 0xFFC0) * 1000 / 410; + byte = byte_to_bits(two_bytes_to_int(lector_bytes[0], lector_bytes[1]) & 0x003F); + if (byte[0] != 0) + sample->in_deco = true; + else + sample->in_deco = false; + if (byte[1] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "rbt"); + if (byte[2] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "ascent"); + if (byte[3] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "ceiling"); + if (byte[4] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "workload"); + if (byte[5] != 0) + add_event(dc, sample->time.seconds, 0, 0, 0, "transmitter"); + if (j == 3) { + read_bytes(1); + if (is_O2) { + read_bytes(1); + o2percent = tmp_1byte; + } + j = 0; + } + free(byte); + + // In commit 5f44fdd setpoint replaced po2, so although this is not necesarily CCR dive ... + if (is_O2) + sample->setpoint.mbar = calculate_depth_to_mbar(sample->depth.mm, dt_dive->surface_pressure, 0) * o2percent / 100; + j++; + } +bail: + return sample; +} + +/* + * Reads the header of a file and returns the header struct + * If it's not a DATATRAK file returns header zero initalized + */ +static dtrakheader read_file_header(FILE *archivo) +{ + dtrakheader fileheader = { 0 }; + const short headerbytes = 12; + unsigned char *lector = (unsigned char *)malloc(headerbytes); + + if (fread(lector, 1, headerbytes, archivo) != headerbytes) { + free(lector); + return fileheader; + } + if (two_bytes_to_int(lector[0], lector[1]) != 0xA100) { + report_error(translate("gettextFromC", "Error: the file does not appear to be a DATATRAK divelog")); + free(lector); + return fileheader; + } + fileheader.header = (lector[0] << 8) + lector[1]; + fileheader.dc_serial_1 = two_bytes_to_int(lector[2], lector[3]); + fileheader.dc_serial_2 = two_bytes_to_int(lector[4], lector[5]); + fileheader.divesNum = two_bytes_to_int(lector[7], lector[6]); + free(lector); + return fileheader; +} + +#define CHECK(_func, _val) if ((_func) != (_val)) goto bail + +/* + * Parses the dive extracting its data and filling a subsurface's dive structure + */ +bool dt_dive_parser(FILE *archivo, struct dive *dt_dive) +{ + unsigned char n; + int profile_length; + char *tmp_notes_str = NULL; + unsigned char *tmp_string1 = NULL, + *locality = NULL, + *dive_point = NULL; + char buffer[1024]; + struct divecomputer *dc = &dt_dive->dc; + + is_nitrox = is_O2 = is_SCR = 0; + + /* + * Parse byte to byte till next dive entry + */ + n = 0; + CHECK(fread(&lector_bytes[n], 1, 1, archivo), 1); + while (lector_bytes[n] != 0xA0) + CHECK(fread(&lector_bytes[n], 1, 1, archivo), 1); + + /* + * Found dive header 0xA000, verify second byte + */ + CHECK(fread(&lector_bytes[n+1], 1, 1, archivo), 1); + if (two_bytes_to_int(lector_bytes[0], lector_bytes[1]) != 0xA000) { + printf("Error: byte = %4x\n", two_bytes_to_int(lector_bytes[0], lector_bytes[1])); + return false; + } + + /* + * Begin parsing + * First, Date of dive, 4 bytes + */ + read_bytes(4); + + + /* + * Next, Time in minutes since 00:00 + */ + read_bytes(2); + + dt_dive->dc.when = dt_dive->when = (timestamp_t)date_time_to_ssrfc(tmp_4bytes, tmp_2bytes); + + /* + * Now, Locality, 1st byte is long of string, rest is string + */ + read_bytes(1); + read_string(locality); + + /* + * Next, Dive point, defined as Locality + */ + read_bytes(1); + read_string(dive_point); + + /* + * Subsurface only have a location variable, so we have to merge DTrak's + * Locality and Dive points. + */ + snprintf(buffer, sizeof(buffer), "%s, %s", locality, dive_point); + dt_dive->dive_site_uuid = get_dive_site_uuid_by_name(buffer, NULL); + if (dt_dive->dive_site_uuid == 0) + dt_dive->dive_site_uuid = create_dive_site(buffer, dt_dive->when); + free(locality); + free(dive_point); + + /* + * Altitude. Don't exist in Subsurface, the equivalent would be + * surface air pressure which can, be calculated from altitude. + * As dtrak registers altitude intervals, we, arbitrarily, choose + * the lower altitude/pressure equivalence for each segment. So + * + * Datatrak table * Conversion formula: + * * + * byte = 1 0 - 700 m * P = P0 * exp(-(g * M * h ) / (R * T0)) + * byte = 2 700 - 1700m * P0 = sealevel pressure = 101325 Pa + * byte = 3 1700 - 2700 m * g = grav. acceleration = 9,80665 m/s² + * byte = 4 2700 - * m * M = molar mass (dry air) = 0,0289644 Kg/mol + * * h = altitude over sea level (m) + * * R = Universal gas constant = 8,31447 J/(mol*K) + * * T0 = sea level standard temperature = 288,15 K + */ + read_bytes(1); + switch (tmp_1byte) { + case 1: + dt_dive->dc.surface_pressure.mbar = 1013; + break; + case 2: + dt_dive->dc.surface_pressure.mbar = 932; + break; + case 3: + dt_dive->dc.surface_pressure.mbar = 828; + break; + case 4: + dt_dive->dc.surface_pressure.mbar = 735; + break; + default: + dt_dive->dc.surface_pressure.mbar = 1013; + } + + /* + * Interval (minutes) + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->dc.surfacetime.seconds = (uint32_t) tmp_2bytes * 60; + + /* + * Weather, values table, 0 to 6 + * Subsurface don't have this record but we can use tags + */ + dt_dive->tag_list = NULL; + read_bytes(1); + switch (tmp_1byte) { + case 1: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "clear"))); + break; + case 2: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "misty"))); + break; + case 3: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "fog"))); + break; + case 4: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "rain"))); + break; + case 5: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "storm"))); + break; + case 6: + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "snow"))); + break; + default: + // unknown, do nothing + break; + } + + /* + * Air Temperature + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->dc.airtemp.mkelvin = C_to_mkelvin((double)(tmp_2bytes / 100)); + + /* + * Dive suit, values table, 0 to 6 + */ + read_bytes(1); + switch (tmp_1byte) { + case 1: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "No suit")); + break; + case 2: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Shorty")); + break; + case 3: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Combi")); + break; + case 4: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Wet suit")); + break; + case 5: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Semidry suit")); + break; + case 6: + dt_dive->suit = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Dry suit")); + break; + default: + // unknown, do nothing + break; + } + + /* + * Tank, volume size in liter*100. And initialize gasmix to air (default). + * Dtrak don't record init and end pressures, but consumed bar, so let's + * init a default pressure of 200 bar. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) { + dt_dive->cylinder[0].type.size.mliter = tmp_2bytes * 10; + dt_dive->cylinder[0].type.description = strdup(""); + dt_dive->cylinder[0].start.mbar = 200000; + dt_dive->cylinder[0].gasmix.he.permille = 0; + dt_dive->cylinder[0].gasmix.o2.permille = 210; + dt_dive->cylinder[0].manually_added = true; + } + + /* + * Maximum depth, in cm. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->maxdepth.mm = dt_dive->dc.maxdepth.mm = (int32_t)tmp_2bytes * 10; + + /* + * Dive time in minutes. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF) + dt_dive->duration.seconds = dt_dive->dc.duration.seconds = (uint32_t)tmp_2bytes * 60; + + /* + * Minimum water temperature in C*100. If unknown, set it to 0K which + * is subsurface's value for "unknown" + */ + read_bytes(2); + if (tmp_2bytes != 0x7fff) + dt_dive->watertemp.mkelvin = dt_dive->dc.watertemp.mkelvin = C_to_mkelvin((double)(tmp_2bytes / 100)); + else + dt_dive->watertemp.mkelvin = 0; + + /* + * Air used in bar*100. + */ + read_bytes(2); + if (tmp_2bytes != 0x7FFF && dt_dive->cylinder[0].type.size.mliter) + dt_dive->cylinder[0].gas_used.mliter = dt_dive->cylinder[0].type.size.mliter * (tmp_2bytes / 100.0); + + /* + * Dive Type 1 - Bit table. Subsurface don't have this record, but + * will use tags. Bits 0 and 1 are not used. Reuse coincident tags. + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[2] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "no stop"))); + if (byte[3] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "deco"))); + if (byte[4] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "single ascent"))); + if (byte[5] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "multiple ascent"))); + if (byte[6] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "fresh"))); + if (byte[7] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "salt water"))); + free(byte); + + /* + * Dive Type 2 - Bit table, use tags again + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[0] != 0) { + taglist_add_tag(&dt_dive->tag_list, strdup("nitrox")); + is_nitrox = 1; + } + if (byte[1] != 0) { + taglist_add_tag(&dt_dive->tag_list, strdup("rebreather")); + is_SCR = 1; + dt_dive->dc.divemode = PSCR; + } + free(byte); + + /* + * Dive Activity 1 - Bit table, use tags again + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[0] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "sight seeing"))); + if (byte[1] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "club dive"))); + if (byte[2] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "instructor"))); + if (byte[3] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "instruction"))); + if (byte[4] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "night"))); + if (byte[5] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "cave"))); + if (byte[6] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "ice"))); + if (byte[7] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "search"))); + free(byte); + + + /* + * Dive Activity 2 - Bit table, use tags again + */ + read_bytes(1); + byte = byte_to_bits(tmp_1byte); + if (byte[0] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "wreck"))); + if (byte[1] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "river"))); + if (byte[2] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "drift"))); + if (byte[3] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "photo"))); + if (byte[4] != 0) + taglist_add_tag(&dt_dive->tag_list, strdup(QT_TRANSLATE_NOOP("gettextFromC", "other"))); + free(byte); + + /* + * Other activities - String 1st byte = long + * Will put this in dive notes before the true notes + */ + read_bytes(1); + if (tmp_1byte != 0) { + read_string(tmp_string1); + snprintf(buffer, sizeof(buffer), "%s: %s\n", + QT_TRANSLATE_NOOP("gettextFromC", "Other activities"), + tmp_string1); + tmp_notes_str = strdup(buffer); + free(tmp_string1); + } + + /* + * Dive buddies + */ + read_bytes(1); + if (tmp_1byte != 0) { + read_string(tmp_string1); + dt_dive->buddy = strdup((char *)tmp_string1); + free(tmp_string1); + } + + /* + * Dive notes + */ + read_bytes(1); + if (tmp_1byte != 0) { + read_string(tmp_string1); + int len = snprintf(buffer, sizeof(buffer), "%s%s:\n%s", + tmp_notes_str ? tmp_notes_str : "", + QT_TRANSLATE_NOOP("gettextFromC", "Datatrak/Wlog notes"), + tmp_string1); + dt_dive->notes = calloc((len +1), 1); + dt_dive->notes = memcpy(dt_dive->notes, buffer, len); + free(tmp_string1); + if (tmp_notes_str != NULL) + free(tmp_notes_str); + } + + /* + * Alarms 1 - Bit table - Not in Subsurface, we use the profile + */ + read_bytes(1); + + /* + * Alarms 2 - Bit table - Not in Subsurface, we use the profile + */ + read_bytes(1); + + /* + * Dive number (in datatrak, after import user has to renumber) + */ + read_bytes(2); + dt_dive->number = tmp_2bytes; + + /* + * Computer timestamp - Useless for Subsurface + */ + read_bytes(4); + + /* + * Model - table - Not included 0x14, 0x24, 0x41, and 0x73 + * known to exist, but not its model name - To add in the future. + * Strangely 0x00 serves for manually added dives and a dc too, at + * least in EXAMPLE.LOG file, shipped with the software. + */ + read_bytes(1); + switch (tmp_1byte) { + case 0x00: + dt_dive->dc.model = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Manually entered dive")); + break; + case 0x1C: + dt_dive->dc.model = strdup("Aladin Air"); + break; + case 0x1D: + dt_dive->dc.model = strdup("Spiro Monitor 2 plus"); + break; + case 0x1E: + dt_dive->dc.model = strdup("Aladin Sport"); + break; + case 0x1F: + dt_dive->dc.model = strdup("Aladin Pro"); + break; + case 0x34: + dt_dive->dc.model = strdup("Aladin Air X"); + break; + case 0x3D: + dt_dive->dc.model = strdup("Spiro Monitor 2 plus"); + break; + case 0x3F: + dt_dive->dc.model = strdup("Mares Genius"); + break; + case 0x44: + dt_dive->dc.model = strdup("Aladin Air X"); + break; + case 0x48: + dt_dive->dc.model = strdup("Spiro Monitor 3 Air"); + break; + case 0xA4: + dt_dive->dc.model = strdup("Aladin Air X O2"); + break; + case 0xB1: + dt_dive->dc.model = strdup("Citizen Hyper Aqualand"); + break; + case 0xB2: + dt_dive->dc.model = strdup("Citizen ProMaster"); + break; + case 0xB3: + dt_dive->dc.model = strdup("Mares Guardian"); + break; + case 0xBC: + dt_dive->dc.model = strdup("Aladin Air X Nitrox"); + break; + case 0xF4: + dt_dive->dc.model = strdup("Aladin Air X Nitrox"); + break; + case 0xFF: + dt_dive->dc.model = strdup("Aladin Pro Nitrox"); + break; + default: + dt_dive->dc.model = strdup(QT_TRANSLATE_NOOP("gettextFromC", "Unknown")); + break; + } + if ((tmp_1byte & 0xF0) == 0xF0) + is_nitrox = 1; + if ((tmp_1byte & 0xF0) == 0xA0) + is_O2 = 1; + + /* + * Air usage, unknown use. Probably allows or deny manually entering gas + * comsumption based on dc model - Useless for Subsurface + */ + read_bytes(1); + if (fseek(archivo, 6, 1) != 0) // jump over 6 bytes whitout known use + goto bail; + /* + * Profile data length + */ + read_bytes(2); + profile_length = tmp_2bytes; + if (profile_length != 0) { + /* + * 8 x 2 bytes for the tissues saturation useless for subsurface + * and other 6 bytes without known use + */ + if (fseek(archivo, 22, 1) != 0) + goto bail; + if (is_nitrox || is_O2) { + + /* + * CNS % (unsure) values table (only nitrox computers) + */ + read_bytes(1); + + /* + * % O2 in nitrox mix - (only nitrox and O2 computers but differents) + */ + read_bytes(1); + if (is_nitrox) { + dt_dive->cylinder[0].gasmix.o2.permille = + (tmp_1byte & 0x0F ? 20.0 + 2 * (tmp_1byte & 0x0F) : 21.0) * 10; + } else { + dt_dive->cylinder[0].gasmix.o2.permille = tmp_1byte * 10; + read_bytes(1) // Jump over one byte, unknown use + } + } + /* + * profileLength = Nº bytes, need to know how many samples are there. + * 2bytes per sample plus another one each three samples. Also includes the + * bytes jumped over (22) and the nitrox (2) or O2 (3). + */ + int samplenum = is_O2 ? (profile_length - 25) * 3 / 8 : (profile_length - 24) * 3 / 7; + + dc->events = calloc(samplenum, sizeof(struct event)); + dc->alloc_samples = samplenum; + dc->samples = 0; + dc->sample = calloc(samplenum, sizeof(struct sample)); + + dtrak_profile(dt_dive, archivo); + } + /* + * Initialize some dive data not supported by Datatrak/WLog + */ + if (!strcmp(dt_dive->dc.model, "Manually entered dive")) + dt_dive->dc.deviceid = 0; + else + dt_dive->dc.deviceid = 0xffffffff; + create_device_node(dt_dive->dc.model, dt_dive->dc.deviceid, "", "", dt_dive->dc.model); + dt_dive->dc.next = NULL; + if (!is_SCR && dt_dive->cylinder[0].type.size.mliter) { + dt_dive->cylinder[0].end.mbar = dt_dive->cylinder[0].start.mbar - + ((dt_dive->cylinder[0].gas_used.mliter / dt_dive->cylinder[0].type.size.mliter) * 1000); + } + return true; + +bail: + return false; +} + +void datatrak_import(const char *file, struct dive_table *table) +{ + FILE *archivo; + dtrakheader *fileheader = (dtrakheader *)malloc(sizeof(dtrakheader)); + int i = 0; + + if ((archivo = subsurface_fopen(file, "rb")) == NULL) { + report_error(translate("gettextFromC", "Error: couldn't open the file %s"), file); + free(fileheader); + return; + } + + /* + * Verify fileheader, get number of dives in datatrak divelog + */ + *fileheader = read_file_header(archivo); + while (i < fileheader->divesNum) { + struct dive *ptdive = alloc_dive(); + + if (!dt_dive_parser(archivo, ptdive)) { + report_error(translate("gettextFromC", "Error: no dive")); + free(ptdive); + } else { + record_dive(ptdive); + } + i++; + } + taglist_cleanup(&g_tag_list); + fclose(archivo); + sort_table(table); + free(fileheader); +} diff --git a/subsurface-core/datatrak.h b/subsurface-core/datatrak.h new file mode 100644 index 000000000..3a37e0465 --- /dev/null +++ b/subsurface-core/datatrak.h @@ -0,0 +1,41 @@ +#ifndef DATATRAK_HEADER_H +#define DATATRAK_HEADER_H + +#include + +typedef struct dtrakheader_ { + int header; //Must be 0xA100; + int divesNum; + int dc_serial_1; + int dc_serial_2; +} dtrakheader; + +#define read_bytes(_n) \ + switch (_n) { \ + case 1: \ + if (fread (&lector_bytes, sizeof(char), _n, archivo) != _n) \ + goto bail; \ + tmp_1byte = lector_bytes[0]; \ + break; \ + case 2: \ + if (fread (&lector_bytes, sizeof(char), _n, archivo) != _n) \ + goto bail; \ + tmp_2bytes = two_bytes_to_int (lector_bytes[1], lector_bytes[0]); \ + break; \ + default: \ + if (fread (&lector_word, sizeof(char), _n, archivo) != _n) \ + goto bail; \ + tmp_4bytes = four_bytes_to_long(lector_word[3], lector_word[2], lector_word[1], lector_word[0]); \ + break; \ + } + +#define read_string(_property) \ + unsigned char *_property##tmp = (unsigned char *)calloc(tmp_1byte + 1, 1); \ + if (fread((char *)_property##tmp, 1, tmp_1byte, archivo) != tmp_1byte) { \ + free(_property##tmp); \ + goto bail; \ + } \ + _property = (unsigned char *)strcat(to_utf8(_property##tmp), ""); \ + free(_property##tmp); + +#endif // DATATRAK_HEADER_H diff --git a/subsurface-core/deco.c b/subsurface-core/deco.c new file mode 100644 index 000000000..86acc0351 --- /dev/null +++ b/subsurface-core/deco.c @@ -0,0 +1,600 @@ +/* calculate deco values + * based on Bühlmann ZHL-16b + * based on an implemention by heinrichs weikamp for the DR5 + * the original file was given to Subsurface under the GPLv2 + * by Matthias Heinrichs + * + * The implementation below is a fairly complete rewrite since then + * (C) Robert C. Helling 2013 and released under the GPLv2 + * + * add_segment() - add at the given pressure, breathing gasmix + * deco_allowed_depth() - ceiling based on lead tissue, surface pressure, 3m increments or smooth + * set_gf() - set Buehlmann gradient factors + * clear_deco() + * cache_deco_state() + * restore_deco_state() + * dump_tissues() + */ +#include +#include +#include "dive.h" +#include +#include + +#define cube(x) (x * x * x) + +// Subsurface appears to produce marginally less conservative plans than our benchmarks +// Introduce 1.2% additional conservatism +#define subsurface_conservatism_factor 1.012 + + +extern bool in_planner(); + +extern pressure_t first_ceiling_pressure; + +//! Option structure for Buehlmann decompression. +struct buehlmann_config { + double satmult; //! safety at inert gas accumulation as percentage of effect (more than 100). + double desatmult; //! safety at inert gas depletion as percentage of effect (less than 100). + unsigned int last_deco_stop_in_mtr; //! depth of last_deco_stop. + double gf_high; //! gradient factor high (at surface). + double gf_low; //! gradient factor low (at bottom/start of deco calculation). + double gf_low_position_min; //! gf_low_position below surface_min_shallow. + bool gf_low_at_maxdepth; //! if true, gf_low applies at max depth instead of at deepest ceiling. +}; + +struct buehlmann_config buehlmann_config = { + .satmult = 1.0, + .desatmult = 1.01, + .last_deco_stop_in_mtr = 0, + .gf_high = 0.75, + .gf_low = 0.35, + .gf_low_position_min = 1.0, + .gf_low_at_maxdepth = false +}; + +//! Option structure for VPM-B decompression. +struct vpmb_config { + double crit_radius_N2; //! Critical radius of N2 nucleon (microns). + double crit_radius_He; //! Critical radius of He nucleon (microns). + double crit_volume_lambda; //! Constant corresponding to critical gas volume (bar * min). + double gradient_of_imperm; //! Gradient after which bubbles become impermeable (bar). + double surface_tension_gamma; //! Nucleons surface tension constant (N / bar = m2). + double skin_compression_gammaC; //! Skin compression gammaC (N / bar = m2). + double regeneration_time; //! Time needed for the bubble to regenerate to the start radius (min). + double other_gases_pressure; //! Always present pressure of other gasses in tissues (bar). +}; + +struct vpmb_config vpmb_config = { + .crit_radius_N2 = 0.55, + .crit_radius_He = 0.45, + .crit_volume_lambda = 199.58, + .gradient_of_imperm = 8.30865, // = 8.2 atm + .surface_tension_gamma = 0.18137175, // = 0.0179 N/msw + .skin_compression_gammaC = 2.6040525, // = 0.257 N/msw + .regeneration_time = 20160.0, + .other_gases_pressure = 0.1359888 +}; + +const double buehlmann_N2_a[] = { 1.1696, 1.0, 0.8618, 0.7562, + 0.62, 0.5043, 0.441, 0.4, + 0.375, 0.35, 0.3295, 0.3065, + 0.2835, 0.261, 0.248, 0.2327 }; + +const double buehlmann_N2_b[] = { 0.5578, 0.6514, 0.7222, 0.7825, + 0.8126, 0.8434, 0.8693, 0.8910, + 0.9092, 0.9222, 0.9319, 0.9403, + 0.9477, 0.9544, 0.9602, 0.9653 }; + +const double buehlmann_N2_t_halflife[] = { 5.0, 8.0, 12.5, 18.5, + 27.0, 38.3, 54.3, 77.0, + 109.0, 146.0, 187.0, 239.0, + 305.0, 390.0, 498.0, 635.0 }; + +const double buehlmann_N2_factor_expositon_one_second[] = { + 2.30782347297664E-003, 1.44301447809736E-003, 9.23769302935806E-004, 6.24261986779007E-004, + 4.27777107246730E-004, 3.01585140931371E-004, 2.12729727268379E-004, 1.50020603047807E-004, + 1.05980191127841E-004, 7.91232600646508E-005, 6.17759153688224E-005, 4.83354552742732E-005, + 3.78761777920511E-005, 2.96212356654113E-005, 2.31974277413727E-005, 1.81926738960225E-005 +}; + +const double buehlmann_He_a[] = { 1.6189, 1.383, 1.1919, 1.0458, + 0.922, 0.8205, 0.7305, 0.6502, + 0.595, 0.5545, 0.5333, 0.5189, + 0.5181, 0.5176, 0.5172, 0.5119 }; + +const double buehlmann_He_b[] = { 0.4770, 0.5747, 0.6527, 0.7223, + 0.7582, 0.7957, 0.8279, 0.8553, + 0.8757, 0.8903, 0.8997, 0.9073, + 0.9122, 0.9171, 0.9217, 0.9267 }; + +const double buehlmann_He_t_halflife[] = { 1.88, 3.02, 4.72, 6.99, + 10.21, 14.48, 20.53, 29.11, + 41.20, 55.19, 70.69, 90.34, + 115.29, 147.42, 188.24, 240.03 }; + +const double buehlmann_He_factor_expositon_one_second[] = { + 6.12608039419837E-003, 3.81800836683133E-003, 2.44456078654209E-003, 1.65134647076792E-003, + 1.13084424730725E-003, 7.97503165599123E-004, 5.62552521860549E-004, 3.96776399429366E-004, + 2.80360036664540E-004, 2.09299583354805E-004, 1.63410794820518E-004, 1.27869320250551E-004, + 1.00198406028040E-004, 7.83611475491108E-005, 6.13689891868496E-005, 4.81280465299827E-005 +}; + +const double conservatism_lvls[] = { 1.0, 1.05, 1.12, 1.22, 1.35 }; + +/* Inspired gas loading equations depend on the partial pressure of inert gas in the alveolar. + * P_alv = (P_amb - P_H2O + (1 - Rq) / Rq * P_CO2) * f + * where: + * P_alv alveolar partial pressure of inert gas + * P_amb ambient pressure + * P_H2O water vapour partial pressure = ~0.0627 bar + * P_CO2 carbon dioxide partial pressure = ~0.0534 bar + * Rq respiratory quotient (O2 consumption / CO2 production) + * f fraction of inert gas + * + * In our calculations, we simplify this to use an effective water vapour pressure + * WV = P_H20 - (1 - Rq) / Rq * P_CO2 + * + * Buhlmann ignored the contribution of CO2 (i.e. Rq = 1.0), whereas Schreiner adopted Rq = 0.8. + * WV_Buhlmann = PP_H2O = 0.0627 bar + * WV_Schreiner = 0.0627 - (1 - 0.8) / Rq * 0.0534 = 0.0493 bar + + * Buhlmann calculations use the Buhlmann value, VPM-B calculations use the Schreiner value. +*/ +#define WV_PRESSURE 0.0627 // water vapor pressure in bar, based on respiratory quotient Rq = 1.0 (Buhlmann value) +#define WV_PRESSURE_SCHREINER 0.0493 // water vapor pressure in bar, based on respiratory quotient Rq = 0.8 (Schreiner value) + +#define DECO_STOPS_MULTIPLIER_MM 3000.0 +#define NITROGEN_FRACTION 0.79 + +double tissue_n2_sat[16]; +double tissue_he_sat[16]; +int ci_pointing_to_guiding_tissue; +double gf_low_pressure_this_dive; +#define TISSUE_ARRAY_SZ sizeof(tissue_n2_sat) + +double tolerated_by_tissue[16]; +double tissue_inertgas_saturation[16]; +double buehlmann_inertgas_a[16], buehlmann_inertgas_b[16]; + +double max_n2_crushing_pressure[16]; +double max_he_crushing_pressure[16]; + +double crushing_onset_tension[16]; // total inert gas tension in the t* moment +double n2_regen_radius[16]; // rs +double he_regen_radius[16]; +double max_ambient_pressure; // last moment we were descending + +double bottom_n2_gradient[16]; +double bottom_he_gradient[16]; + +double initial_n2_gradient[16]; +double initial_he_gradient[16]; + +double get_crit_radius_He() +{ + if (prefs.conservatism_level <= 4) + return vpmb_config.crit_radius_He * conservatism_lvls[prefs.conservatism_level] * subsurface_conservatism_factor; + return vpmb_config.crit_radius_He; +} + +double get_crit_radius_N2() +{ + if (prefs.conservatism_level <= 4) + return vpmb_config.crit_radius_N2 * conservatism_lvls[prefs.conservatism_level] * subsurface_conservatism_factor; + return vpmb_config.crit_radius_N2; +} + +// Solve another cubic equation, this time +// x^3 - B x - C == 0 +// Use trigonometric formula for negative discriminants (see Wikipedia for details) + +double solve_cubic2(double B, double C) +{ + double discriminant = 27 * C * C - 4 * cube(B); + if (discriminant < 0.0) { + return 2.0 * sqrt(B / 3.0) * cos(acos(3.0 * C * sqrt(3.0 / B) / (2.0 * B)) / 3.0); + } + + double denominator = pow(9 * C + sqrt(3 * discriminant), 1 / 3.0); + + return pow(2.0 / 3.0, 1.0 / 3.0) * B / denominator + denominator / pow(18.0, 1.0 / 3.0); +} + +// This is a simplified formula avoiding radii. It uses the fact that Boyle's law says +// pV = (G + P_amb) / G^3 is constant to solve for the new gradient G. + +double update_gradient(double next_stop_pressure, double first_gradient) +{ + double B = cube(first_gradient) / (first_ceiling_pressure.mbar / 1000.0 + first_gradient); + double C = next_stop_pressure * B; + + double new_gradient = solve_cubic2(B, C); + + if (new_gradient < 0.0) + report_error("Negative gradient encountered!"); + return new_gradient; +} + +double vpmb_tolerated_ambient_pressure(double reference_pressure, int ci) +{ + double n2_gradient, he_gradient, total_gradient; + + if (reference_pressure >= first_ceiling_pressure.mbar / 1000.0 || !first_ceiling_pressure.mbar) { + n2_gradient = bottom_n2_gradient[ci]; + he_gradient = bottom_he_gradient[ci]; + } else { + n2_gradient = update_gradient(reference_pressure, bottom_n2_gradient[ci]); + he_gradient = update_gradient(reference_pressure, bottom_he_gradient[ci]); + } + + total_gradient = ((n2_gradient * tissue_n2_sat[ci]) + (he_gradient * tissue_he_sat[ci])) / (tissue_n2_sat[ci] + tissue_he_sat[ci]); + + return tissue_n2_sat[ci] + tissue_he_sat[ci] + vpmb_config.other_gases_pressure - total_gradient; +} + + +double tissue_tolerance_calc(const struct dive *dive, double pressure) +{ + int ci = -1; + double ret_tolerance_limit_ambient_pressure = 0.0; + double gf_high = buehlmann_config.gf_high; + double gf_low = buehlmann_config.gf_low; + double surface = get_surface_pressure_in_mbar(dive, true) / 1000.0; + double lowest_ceiling = 0.0; + double tissue_lowest_ceiling[16]; + + if (prefs.deco_mode != VPMB || !in_planner()) { + for (ci = 0; ci < 16; ci++) { + tissue_inertgas_saturation[ci] = tissue_n2_sat[ci] + tissue_he_sat[ci]; + buehlmann_inertgas_a[ci] = ((buehlmann_N2_a[ci] * tissue_n2_sat[ci]) + (buehlmann_He_a[ci] * tissue_he_sat[ci])) / tissue_inertgas_saturation[ci]; + buehlmann_inertgas_b[ci] = ((buehlmann_N2_b[ci] * tissue_n2_sat[ci]) + (buehlmann_He_b[ci] * tissue_he_sat[ci])) / tissue_inertgas_saturation[ci]; + + + /* tolerated = (tissue_inertgas_saturation - buehlmann_inertgas_a) * buehlmann_inertgas_b; */ + + tissue_lowest_ceiling[ci] = (buehlmann_inertgas_b[ci] * tissue_inertgas_saturation[ci] - gf_low * buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci]) / + ((1.0 - buehlmann_inertgas_b[ci]) * gf_low + buehlmann_inertgas_b[ci]); + if (tissue_lowest_ceiling[ci] > lowest_ceiling) + lowest_ceiling = tissue_lowest_ceiling[ci]; + if (!buehlmann_config.gf_low_at_maxdepth) { + if (lowest_ceiling > gf_low_pressure_this_dive) + gf_low_pressure_this_dive = lowest_ceiling; + } + } + for (ci = 0; ci < 16; ci++) { + double tolerated; + + if ((surface / buehlmann_inertgas_b[ci] + buehlmann_inertgas_a[ci] - surface) * gf_high + surface < + (gf_low_pressure_this_dive / buehlmann_inertgas_b[ci] + buehlmann_inertgas_a[ci] - gf_low_pressure_this_dive) * gf_low + gf_low_pressure_this_dive) + tolerated = (-buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci] * (gf_high * gf_low_pressure_this_dive - gf_low * surface) - + (1.0 - buehlmann_inertgas_b[ci]) * (gf_high - gf_low) * gf_low_pressure_this_dive * surface + + buehlmann_inertgas_b[ci] * (gf_low_pressure_this_dive - surface) * tissue_inertgas_saturation[ci]) / + (-buehlmann_inertgas_a[ci] * buehlmann_inertgas_b[ci] * (gf_high - gf_low) + + (1.0 - buehlmann_inertgas_b[ci]) * (gf_low * gf_low_pressure_this_dive - gf_high * surface) + + buehlmann_inertgas_b[ci] * (gf_low_pressure_this_dive - surface)); + else + tolerated = ret_tolerance_limit_ambient_pressure; + + + tolerated_by_tissue[ci] = tolerated; + + if (tolerated >= ret_tolerance_limit_ambient_pressure) { + ci_pointing_to_guiding_tissue = ci; + ret_tolerance_limit_ambient_pressure = tolerated; + } + } + } else { + // VPM-B ceiling + double reference_pressure; + + ret_tolerance_limit_ambient_pressure = pressure; + // The Boyle compensated gradient depends on ambient pressure. For the ceiling, this should set the ambient pressure. + do { + reference_pressure = ret_tolerance_limit_ambient_pressure; + ret_tolerance_limit_ambient_pressure = 0.0; + for (ci = 0; ci < 16; ci++) { + double tolerated = vpmb_tolerated_ambient_pressure(reference_pressure, ci); + if (tolerated >= ret_tolerance_limit_ambient_pressure) { + ci_pointing_to_guiding_tissue = ci; + ret_tolerance_limit_ambient_pressure = tolerated; + } + tolerated_by_tissue[ci] = tolerated; + } + // We are doing ok if the gradient was computed within ten centimeters of the ceiling. + } while (fabs(ret_tolerance_limit_ambient_pressure - reference_pressure) > 0.01); + } + return ret_tolerance_limit_ambient_pressure; +} + +/* + * Return buelman factor for a particular period and tissue index. + * + * We cache the last factor, since we commonly call this with the + * same values... We have a special "fixed cache" for the one second + * case, although I wonder if that's even worth it considering the + * more general-purpose cache. + */ +struct factor_cache { + int last_period; + double last_factor; +}; + +double n2_factor(int period_in_seconds, int ci) +{ + static struct factor_cache cache[16]; + + if (period_in_seconds == 1) + return buehlmann_N2_factor_expositon_one_second[ci]; + + if (period_in_seconds != cache[ci].last_period) { + cache[ci].last_period = period_in_seconds; + cache[ci].last_factor = 1 - pow(2.0, -period_in_seconds / (buehlmann_N2_t_halflife[ci] * 60)); + } + + return cache[ci].last_factor; +} + +double he_factor(int period_in_seconds, int ci) +{ + static struct factor_cache cache[16]; + + if (period_in_seconds == 1) + return buehlmann_He_factor_expositon_one_second[ci]; + + if (period_in_seconds != cache[ci].last_period) { + cache[ci].last_period = period_in_seconds; + cache[ci].last_factor = 1 - pow(2.0, -period_in_seconds / (buehlmann_He_t_halflife[ci] * 60)); + } + + return cache[ci].last_factor; +} + +double calc_surface_phase(double surface_pressure, double he_pressure, double n2_pressure, double he_time_constant, double n2_time_constant) +{ + double inspired_n2 = (surface_pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE)) * NITROGEN_FRACTION; + + if (n2_pressure > inspired_n2) + return (he_pressure / he_time_constant + (n2_pressure - inspired_n2) / n2_time_constant) / (he_pressure + n2_pressure - inspired_n2); + + if (he_pressure + n2_pressure >= inspired_n2){ + double gradient_decay_time = 1.0 / (n2_time_constant - he_time_constant) * log ((inspired_n2 - n2_pressure) / he_pressure); + double gradients_integral = he_pressure / he_time_constant * (1.0 - exp(-he_time_constant * gradient_decay_time)) + (n2_pressure - inspired_n2) / n2_time_constant * (1.0 - exp(-n2_time_constant * gradient_decay_time)); + return gradients_integral / (he_pressure + n2_pressure - inspired_n2); + } + + return 0; +} + +void vpmb_start_gradient() +{ + int ci; + + for (ci = 0; ci < 16; ++ci) { + initial_n2_gradient[ci] = bottom_n2_gradient[ci] = 2.0 * (vpmb_config.surface_tension_gamma / vpmb_config.skin_compression_gammaC) * ((vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma) / n2_regen_radius[ci]); + initial_he_gradient[ci] = bottom_he_gradient[ci] = 2.0 * (vpmb_config.surface_tension_gamma / vpmb_config.skin_compression_gammaC) * ((vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma) / he_regen_radius[ci]); + } +} + +void vpmb_next_gradient(double deco_time, double surface_pressure) +{ + int ci; + double n2_b, n2_c; + double he_b, he_c; + double desat_time; + deco_time /= 60.0; + + for (ci = 0; ci < 16; ++ci) { + desat_time = deco_time + calc_surface_phase(surface_pressure, tissue_he_sat[ci], tissue_n2_sat[ci], log(2.0) / buehlmann_He_t_halflife[ci], log(2.0) / buehlmann_N2_t_halflife[ci]); + + n2_b = initial_n2_gradient[ci] + (vpmb_config.crit_volume_lambda * vpmb_config.surface_tension_gamma) / (vpmb_config.skin_compression_gammaC * desat_time); + he_b = initial_he_gradient[ci] + (vpmb_config.crit_volume_lambda * vpmb_config.surface_tension_gamma) / (vpmb_config.skin_compression_gammaC * desat_time); + + n2_c = vpmb_config.surface_tension_gamma * vpmb_config.surface_tension_gamma * vpmb_config.crit_volume_lambda * max_n2_crushing_pressure[ci]; + n2_c = n2_c / (vpmb_config.skin_compression_gammaC * vpmb_config.skin_compression_gammaC * desat_time); + he_c = vpmb_config.surface_tension_gamma * vpmb_config.surface_tension_gamma * vpmb_config.crit_volume_lambda * max_he_crushing_pressure[ci]; + he_c = he_c / (vpmb_config.skin_compression_gammaC * vpmb_config.skin_compression_gammaC * desat_time); + + bottom_n2_gradient[ci] = 0.5 * ( n2_b + sqrt(n2_b * n2_b - 4.0 * n2_c)); + bottom_he_gradient[ci] = 0.5 * ( he_b + sqrt(he_b * he_b - 4.0 * he_c)); + } +} + +// A*r^3 - B*r^2 - C == 0 +// Solved with the help of mathematica + +double solve_cubic(double A, double B, double C) +{ + double BA = B/A; + double CA = C/A; + + double discriminant = CA * (4 * cube(BA) + 27 * CA); + + // Let's make sure we have a real solution: + if (discriminant < 0.0) { + // This should better not happen + report_error("Complex solution for inner pressure encountered!\n A=%f\tB=%f\tC=%f\n", A, B, C); + return 0.0; + } + double denominator = pow(cube(BA) + 1.5 * (9 * CA + sqrt(3.0) * sqrt(discriminant)), 1/3.0); + return (BA + BA * BA / denominator + denominator) / 3.0; + +} + + +void nuclear_regeneration(double time) +{ + time /= 60.0; + int ci; + double crushing_radius_N2, crushing_radius_He; + for (ci = 0; ci < 16; ++ci) { + //rm + crushing_radius_N2 = 1.0 / (max_n2_crushing_pressure[ci] / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / get_crit_radius_N2()); + crushing_radius_He = 1.0 / (max_he_crushing_pressure[ci] / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / get_crit_radius_He()); + //rs + n2_regen_radius[ci] = crushing_radius_N2 + (get_crit_radius_N2() - crushing_radius_N2) * (1.0 - exp (-time / vpmb_config.regeneration_time)); + he_regen_radius[ci] = crushing_radius_He + (get_crit_radius_He() - crushing_radius_He) * (1.0 - exp (-time / vpmb_config.regeneration_time)); + } +} + + +// Calculates the nucleons inner pressure during the impermeable period +double calc_inner_pressure(double crit_radius, double onset_tension, double current_ambient_pressure) +{ + double onset_radius = 1.0 / (vpmb_config.gradient_of_imperm / (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) + 1.0 / crit_radius); + + + double A = current_ambient_pressure - vpmb_config.gradient_of_imperm + (2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma)) / onset_radius; + double B = 2.0 * (vpmb_config.skin_compression_gammaC - vpmb_config.surface_tension_gamma); + double C = onset_tension * pow(onset_radius, 3); + + double current_radius = solve_cubic(A, B, C); + + return onset_tension * onset_radius * onset_radius * onset_radius / (current_radius * current_radius * current_radius); +} + +// Calculates the crushing pressure in the given moment. Updates crushing_onset_tension and critical radius if needed +void calc_crushing_pressure(double pressure) +{ + int ci; + double gradient; + double gas_tension; + double n2_crushing_pressure, he_crushing_pressure; + double n2_inner_pressure, he_inner_pressure; + + for (ci = 0; ci < 16; ++ci) { + gas_tension = tissue_n2_sat[ci] + tissue_he_sat[ci] + vpmb_config.other_gases_pressure; + gradient = pressure - gas_tension; + + if (gradient <= vpmb_config.gradient_of_imperm) { // permeable situation + n2_crushing_pressure = he_crushing_pressure = gradient; + crushing_onset_tension[ci] = gas_tension; + } + else { // impermeable + if (max_ambient_pressure >= pressure) + return; + + n2_inner_pressure = calc_inner_pressure(get_crit_radius_N2(), crushing_onset_tension[ci], pressure); + he_inner_pressure = calc_inner_pressure(get_crit_radius_He(), crushing_onset_tension[ci], pressure); + + n2_crushing_pressure = pressure - n2_inner_pressure; + he_crushing_pressure = pressure - he_inner_pressure; + } + max_n2_crushing_pressure[ci] = MAX(max_n2_crushing_pressure[ci], n2_crushing_pressure); + max_he_crushing_pressure[ci] = MAX(max_he_crushing_pressure[ci], he_crushing_pressure); + } + max_ambient_pressure = MAX(pressure, max_ambient_pressure); +} + +/* add period_in_seconds at the given pressure and gas to the deco calculation */ +void add_segment(double pressure, const struct gasmix *gasmix, int period_in_seconds, int ccpo2, const struct dive *dive, int sac) +{ + int ci; + struct gas_pressures pressures; + + fill_pressures(&pressures, pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE), + gasmix, (double) ccpo2 / 1000.0, dive->dc.divemode); + + if (buehlmann_config.gf_low_at_maxdepth && pressure > gf_low_pressure_this_dive) + gf_low_pressure_this_dive = pressure; + + for (ci = 0; ci < 16; ci++) { + double pn2_oversat = pressures.n2 - tissue_n2_sat[ci]; + double phe_oversat = pressures.he - tissue_he_sat[ci]; + double n2_f = n2_factor(period_in_seconds, ci); + double he_f = he_factor(period_in_seconds, ci); + double n2_satmult = pn2_oversat > 0 ? buehlmann_config.satmult : buehlmann_config.desatmult; + double he_satmult = phe_oversat > 0 ? buehlmann_config.satmult : buehlmann_config.desatmult; + + tissue_n2_sat[ci] += n2_satmult * pn2_oversat * n2_f; + tissue_he_sat[ci] += he_satmult * phe_oversat * he_f; + } + if(prefs.deco_mode == VPMB && in_planner()) + calc_crushing_pressure(pressure); + return; +} + +void dump_tissues() +{ + int ci; + printf("N2 tissues:"); + for (ci = 0; ci < 16; ci++) + printf(" %6.3e", tissue_n2_sat[ci]); + printf("\nHe tissues:"); + for (ci = 0; ci < 16; ci++) + printf(" %6.3e", tissue_he_sat[ci]); + printf("\n"); +} + +void clear_deco(double surface_pressure) +{ + int ci; + for (ci = 0; ci < 16; ci++) { + tissue_n2_sat[ci] = (surface_pressure - ((in_planner() && (prefs.deco_mode == VPMB)) ? WV_PRESSURE_SCHREINER : WV_PRESSURE)) * N2_IN_AIR / 1000; + tissue_he_sat[ci] = 0.0; + max_n2_crushing_pressure[ci] = 0.0; + max_he_crushing_pressure[ci] = 0.0; + n2_regen_radius[ci] = get_crit_radius_N2(); + he_regen_radius[ci] = get_crit_radius_He(); + } + gf_low_pressure_this_dive = surface_pressure; + if (!buehlmann_config.gf_low_at_maxdepth) + gf_low_pressure_this_dive += buehlmann_config.gf_low_position_min; + max_ambient_pressure = 0.0; +} + +void cache_deco_state(char **cached_datap) +{ + char *data = *cached_datap; + + if (!data) { + data = malloc(2 * TISSUE_ARRAY_SZ + sizeof(double) + sizeof(int)); + *cached_datap = data; + } + memcpy(data, tissue_n2_sat, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(data, tissue_he_sat, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(data, &gf_low_pressure_this_dive, sizeof(double)); + data += sizeof(double); + memcpy(data, &ci_pointing_to_guiding_tissue, sizeof(int)); +} + +void restore_deco_state(char *data) +{ + memcpy(tissue_n2_sat, data, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(tissue_he_sat, data, TISSUE_ARRAY_SZ); + data += TISSUE_ARRAY_SZ; + memcpy(&gf_low_pressure_this_dive, data, sizeof(double)); + data += sizeof(double); + memcpy(&ci_pointing_to_guiding_tissue, data, sizeof(int)); +} + +unsigned int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth) +{ + unsigned int depth; + double pressure_delta; + + /* Avoid negative depths */ + pressure_delta = tissues_tolerance > surface_pressure ? tissues_tolerance - surface_pressure : 0.0; + + depth = rel_mbar_to_depth(pressure_delta * 1000, dive); + + if (!smooth) + depth = ceil(depth / DECO_STOPS_MULTIPLIER_MM) * DECO_STOPS_MULTIPLIER_MM; + + if (depth > 0 && depth < buehlmann_config.last_deco_stop_in_mtr * 1000) + depth = buehlmann_config.last_deco_stop_in_mtr * 1000; + + return depth; +} + +void set_gf(short gflow, short gfhigh, bool gf_low_at_maxdepth) +{ + if (gflow != -1) + buehlmann_config.gf_low = (double)gflow / 100.0; + if (gfhigh != -1) + buehlmann_config.gf_high = (double)gfhigh / 100.0; + buehlmann_config.gf_low_at_maxdepth = gf_low_at_maxdepth; +} diff --git a/subsurface-core/deco.h b/subsurface-core/deco.h new file mode 100644 index 000000000..08ff93422 --- /dev/null +++ b/subsurface-core/deco.h @@ -0,0 +1,19 @@ +#ifndef DECO_H +#define DECO_H + +#ifdef __cplusplus +extern "C" { +#endif + +extern double tolerated_by_tissue[]; +extern double buehlmann_N2_t_halflife[]; +extern double tissue_inertgas_saturation[16]; +extern double buehlmann_inertgas_a[16], buehlmann_inertgas_b[16]; +extern double gf_low_pressure_this_dive; + + +#ifdef __cplusplus +} +#endif + +#endif // DECO_H diff --git a/subsurface-core/device.c b/subsurface-core/device.c new file mode 100644 index 000000000..c952c84be --- /dev/null +++ b/subsurface-core/device.c @@ -0,0 +1,180 @@ +#include +#include "dive.h" +#include "device.h" + +/* + * Good fake dive profiles are hard. + * + * "depthtime" is the integral of the dive depth over + * time ("area" of the dive profile). We want that + * area to match the average depth (avg_d*max_t). + * + * To do that, we generate a 6-point profile: + * + * (0, 0) + * (t1, max_d) + * (t2, max_d) + * (t3, d) + * (t4, d) + * (max_t, 0) + * + * with the same ascent/descent rates between the + * different depths. + * + * NOTE: avg_d, max_d and max_t are given constants. + * The rest we can/should play around with to get a + * good-looking profile. + * + * That six-point profile gives a total area of: + * + * (max_d*max_t) - (max_d*t1) - (max_d-d)*(t4-t3) + * + * And the "same ascent/descent rates" requirement + * gives us (time per depth must be same): + * + * t1 / max_d = (t3-t2) / (max_d-d) + * t1 / max_d = (max_t-t4) / d + * + * We also obviously require: + * + * 0 <= t1 <= t2 <= t3 <= t4 <= max_t + * + * Let us call 'd_frac = d / max_d', and we get: + * + * Total area must match average depth-time: + * + * (max_d*max_t) - (max_d*t1) - (max_d-d)*(t4-t3) = avg_d*max_t + * max_d*(max_t-t1-(1-d_frac)*(t4-t3)) = avg_d*max_t + * max_t-t1-(1-d_frac)*(t4-t3) = avg_d*max_t/max_d + * t1+(1-d_frac)*(t4-t3) = max_t*(1-avg_d/max_d) + * + * and descent slope must match ascent slopes: + * + * t1 / max_d = (t3-t2) / (max_d*(1-d_frac)) + * t1 = (t3-t2)/(1-d_frac) + * + * and + * + * t1 / max_d = (max_t-t4) / (max_d*d_frac) + * t1 = (max_t-t4)/d_frac + * + * In general, we have more free variables than we have constraints, + * but we can aim for certain basics, like a good ascent slope. + */ +static int fill_samples(struct sample *s, int max_d, int avg_d, int max_t, double slope, double d_frac) +{ + double t_frac = max_t * (1 - avg_d / (double)max_d); + int t1 = max_d / slope; + int t4 = max_t - t1 * d_frac; + int t3 = t4 - (t_frac - t1) / (1 - d_frac); + int t2 = t3 - t1 * (1 - d_frac); + + if (t1 < 0 || t1 > t2 || t2 > t3 || t3 > t4 || t4 > max_t) + return 0; + + s[1].time.seconds = t1; + s[1].depth.mm = max_d; + s[2].time.seconds = t2; + s[2].depth.mm = max_d; + s[3].time.seconds = t3; + s[3].depth.mm = max_d * d_frac; + s[4].time.seconds = t4; + s[4].depth.mm = max_d * d_frac; + + return 1; +} + +/* we have no average depth; instead of making up a random average depth + * we should assume either a PADI recrangular profile (for short and/or + * shallow dives) or more reasonably a six point profile with a 3 minute + * safety stop at 5m */ +static void fill_samples_no_avg(struct sample *s, int max_d, int max_t, double slope) +{ + // shallow or short dives are just trapecoids based on the given slope + if (max_d < 10000 || max_t < 600) { + s[1].time.seconds = max_d / slope; + s[1].depth.mm = max_d; + s[2].time.seconds = max_t - max_d / slope; + s[2].depth.mm = max_d; + } else { + s[1].time.seconds = max_d / slope; + s[1].depth.mm = max_d; + s[2].time.seconds = max_t - max_d / slope - 180; + s[2].depth.mm = max_d; + s[3].time.seconds = max_t - 5000 / slope - 180; + s[3].depth.mm = 5000; + s[4].time.seconds = max_t - 5000 / slope; + s[4].depth.mm = 5000; + } +} + +struct divecomputer *fake_dc(struct divecomputer *dc) +{ + static struct sample fake[6]; + static struct divecomputer fakedc; + + fakedc = (*dc); + fakedc.sample = fake; + fakedc.samples = 6; + + /* The dive has no samples, so create a few fake ones */ + int max_t = dc->duration.seconds; + int max_d = dc->maxdepth.mm; + int avg_d = dc->meandepth.mm; + + memset(fake, 0, sizeof(fake)); + fake[5].time.seconds = max_t; + if (!max_t || !max_d) + return &fakedc; + + /* + * We want to fake the profile so that the average + * depth ends up correct. However, in the absense of + * a reasonable average, let's just make something + * up. Note that 'avg_d == max_d' is _not_ a reasonable + * average. + * We explicitly treat avg_d == 0 differently */ + if (avg_d == 0) { + /* we try for a sane slope, but bow to the insanity of + * the user supplied data */ + fill_samples_no_avg(fake, max_d, max_t, MAX(2.0 * max_d / max_t, 5000.0 / 60)); + if (fake[3].time.seconds == 0) { // just a 4 point profile + fakedc.samples = 4; + fake[3].time.seconds = max_t; + } + return &fakedc; + } + if (avg_d < max_d / 10 || avg_d >= max_d) { + avg_d = (max_d + 10000) / 3; + if (avg_d > max_d) + avg_d = max_d * 2 / 3; + } + if (!avg_d) + avg_d = 1; + + /* + * Ok, first we try a basic profile with a specific ascent + * rate (5 meters per minute) and d_frac (1/3). + */ + if (fill_samples(fake, max_d, avg_d, max_t, 5000.0 / 60, 0.33)) + return &fakedc; + + /* + * Ok, assume that didn't work because we cannot make the + * average come out right because it was a quick deep dive + * followed by a much shallower region + */ + if (fill_samples(fake, max_d, avg_d, max_t, 10000.0 / 60, 0.10)) + return &fakedc; + + /* + * Uhhuh. That didn't work. We'd need to find a good combination that + * satisfies our constraints. Currently, we don't, we just give insane + * slopes. + */ + if (fill_samples(fake, max_d, avg_d, max_t, 10000.0, 0.01)) + return &fakedc; + + /* Even that didn't work? Give up, there's something wrong */ + return &fakedc; +} diff --git a/subsurface-core/device.h b/subsurface-core/device.h new file mode 100644 index 000000000..9ff2ce23a --- /dev/null +++ b/subsurface-core/device.h @@ -0,0 +1,18 @@ +#ifndef DEVICE_H +#define DEVICE_H + +#ifdef __cplusplus +#include "dive.h" +extern "C" { +#endif + +extern struct divecomputer *fake_dc(struct divecomputer *dc); +extern void create_device_node(const char *model, uint32_t deviceid, const char *serial, const char *firmware, const char *nickname); +extern void call_for_each_dc(void *f, void (*callback)(void *, const char *, uint32_t, + const char *, const char *, const char *), bool select_only); + +#ifdef __cplusplus +} +#endif + +#endif // DEVICE_H diff --git a/subsurface-core/devicedetails.cpp b/subsurface-core/devicedetails.cpp new file mode 100644 index 000000000..1ac56375d --- /dev/null +++ b/subsurface-core/devicedetails.cpp @@ -0,0 +1,78 @@ +#include "devicedetails.h" + +// This can probably be done better by someone with better c++-FU +const struct gas zero_gas = {0}; +const struct setpoint zero_setpoint = {0}; + +DeviceDetails::DeviceDetails(QObject *parent) : + QObject(parent), + data(0), + serialNo(""), + firmwareVersion(""), + customText(""), + model(""), + syncTime(false), + gas1(zero_gas), + gas2(zero_gas), + gas3(zero_gas), + gas4(zero_gas), + gas5(zero_gas), + dil1(zero_gas), + dil2(zero_gas), + dil3(zero_gas), + dil4(zero_gas), + dil5(zero_gas), + sp1(zero_setpoint), + sp2(zero_setpoint), + sp3(zero_setpoint), + sp4(zero_setpoint), + sp5(zero_setpoint), + setPointFallback(0), + ccrMode(0), + calibrationGas(0), + diveMode(0), + decoType(0), + ppO2Max(0), + ppO2Min(0), + futureTTS(0), + gfLow(0), + gfHigh(0), + aGFLow(0), + aGFHigh(0), + aGFSelectable(0), + saturation(0), + desaturation(0), + lastDeco(0), + brightness(0), + units(0), + samplingRate(0), + salinity(0), + diveModeColor(0), + language(0), + dateFormat(0), + compassGain(0), + pressureSensorOffset(0), + flipScreen(0), + safetyStop(0), + maxDepth(0), + totalTime(0), + numberOfDives(0), + altitude(0), + personalSafety(0), + timeFormat(0), + lightEnabled(false), + light(0), + alarmTimeEnabled(false), + alarmTime(0), + alarmDepthEnabled(false), + alarmDepth(0), + leftButtonSensitivity(0), + rightButtonSensitivity(0), + bottomGasConsumption(0), + decoGasConsumption(0), + modWarning(false), + dynamicAscendRate(false), + graphicalSpeedIndicator(false), + alwaysShowppO2(false) +{ +} diff --git a/subsurface-core/devicedetails.h b/subsurface-core/devicedetails.h new file mode 100644 index 000000000..1ed9914ef --- /dev/null +++ b/subsurface-core/devicedetails.h @@ -0,0 +1,97 @@ +#ifndef DEVICEDETAILS_H +#define DEVICEDETAILS_H + +#include +#include +#include "libdivecomputer.h" + +struct gas { + unsigned char oxygen; + unsigned char helium; + unsigned char type; + unsigned char depth; +}; + +struct setpoint { + unsigned char sp; + unsigned char depth; +}; + +class DeviceDetails : public QObject +{ + Q_OBJECT +public: + explicit DeviceDetails(QObject *parent = 0); + + device_data_t *data; + QString serialNo; + QString firmwareVersion; + QString customText; + QString model; + bool syncTime; + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + bool setPointFallback; + int ccrMode; + int calibrationGas; + int diveMode; + int decoType; + int ppO2Max; + int ppO2Min; + int futureTTS; + int gfLow; + int gfHigh; + int aGFLow; + int aGFHigh; + int aGFSelectable; + int saturation; + int desaturation; + int lastDeco; + int brightness; + int units; + int samplingRate; + int salinity; + int diveModeColor; + int language; + int dateFormat; + int compassGain; + int pressureSensorOffset; + bool flipScreen; + bool safetyStop; + int maxDepth; + int totalTime; + int numberOfDives; + int altitude; + int personalSafety; + int timeFormat; + bool lightEnabled; + int light; + bool alarmTimeEnabled; + int alarmTime; + bool alarmDepthEnabled; + int alarmDepth; + int leftButtonSensitivity; + int rightButtonSensitivity; + int bottomGasConsumption; + int decoGasConsumption; + bool modWarning; + bool dynamicAscendRate; + bool graphicalSpeedIndicator; + bool alwaysShowppO2; +}; + + +#endif // DEVICEDETAILS_H diff --git a/subsurface-core/display.h b/subsurface-core/display.h new file mode 100644 index 000000000..9e3e1d159 --- /dev/null +++ b/subsurface-core/display.h @@ -0,0 +1,63 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct membuffer; + +#define SCALE_SCREEN 1.0 +#define SCALE_PRINT (1.0 / get_screen_dpi()) + +extern double get_screen_dpi(void); + +/* Plot info with smoothing, velocity indication + * and one-, two- and three-minute minimums and maximums */ +struct plot_info { + int nr; + int maxtime; + int meandepth, maxdepth; + int minpressure, maxpressure; + int minhr, maxhr; + int mintemp, maxtemp; + enum {AIR, NITROX, TRIMIX, FREEDIVING} dive_type; + double endtempcoord; + double maxpp; + bool has_ndl; + struct plot_data *entry; +}; + +typedef enum { + SC_SCREEN, + SC_PRINT +} scale_mode_t; + +extern struct divecomputer *select_dc(struct dive *); + +extern unsigned int dc_number; + +extern unsigned int amount_selected; + +extern int is_default_dive_computer_device(const char *); +extern int is_default_dive_computer(const char *, const char *); + +typedef void (*device_callback_t)(const char *name, void *userdata); + +#define DC_TYPE_SERIAL 1 +#define DC_TYPE_UEMIS 2 +#define DC_TYPE_OTHER 3 + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type); + +extern const char *default_dive_computer_vendor; +extern const char *default_dive_computer_product; +extern const char *default_dive_computer_device; +extern int default_dive_computer_download_mode; +#define AMB_PERCENTAGE 50.0 + +#ifdef __cplusplus +} +#endif + +#endif // DISPLAY_H diff --git a/subsurface-core/dive.c b/subsurface-core/dive.c new file mode 100644 index 000000000..2ae84ca1e --- /dev/null +++ b/subsurface-core/dive.c @@ -0,0 +1,3465 @@ +/* dive.c */ +/* maintains the internal dive list structure */ +#include +#include +#include +#include +#include "gettext.h" +#include "dive.h" +#include "libdivecomputer.h" +#include "device.h" +#include "divelist.h" +#include "qthelperfromc.h" + +/* one could argue about the best place to have this variable - + * it's used in the UI, but it seems to make the most sense to have it + * here */ +struct dive displayed_dive; +struct dive_site displayed_dive_site; + +struct tag_entry *g_tag_list = NULL; + +static const char *default_tags[] = { + QT_TRANSLATE_NOOP("gettextFromC", "boat"), QT_TRANSLATE_NOOP("gettextFromC", "shore"), QT_TRANSLATE_NOOP("gettextFromC", "drift"), + QT_TRANSLATE_NOOP("gettextFromC", "deep"), QT_TRANSLATE_NOOP("gettextFromC", "cavern"), QT_TRANSLATE_NOOP("gettextFromC", "ice"), + QT_TRANSLATE_NOOP("gettextFromC", "wreck"), QT_TRANSLATE_NOOP("gettextFromC", "cave"), QT_TRANSLATE_NOOP("gettextFromC", "altitude"), + QT_TRANSLATE_NOOP("gettextFromC", "pool"), QT_TRANSLATE_NOOP("gettextFromC", "lake"), QT_TRANSLATE_NOOP("gettextFromC", "river"), + QT_TRANSLATE_NOOP("gettextFromC", "night"), QT_TRANSLATE_NOOP("gettextFromC", "fresh"), QT_TRANSLATE_NOOP("gettextFromC", "student"), + QT_TRANSLATE_NOOP("gettextFromC", "instructor"), QT_TRANSLATE_NOOP("gettextFromC", "photo"), QT_TRANSLATE_NOOP("gettextFromC", "video"), + QT_TRANSLATE_NOOP("gettextFromC", "deco") +}; + +const char *cylinderuse_text[] = { + QT_TRANSLATE_NOOP("gettextFromC", "OC-gas"), QT_TRANSLATE_NOOP("gettextFromC", "diluent"), QT_TRANSLATE_NOOP("gettextFromC", "oxygen") +}; +const char *divemode_text[] = { "OC", "CCR", "PSCR", "Freedive" }; + +int event_is_gaschange(struct event *ev) +{ + return ev->type == SAMPLE_EVENT_GASCHANGE || + ev->type == SAMPLE_EVENT_GASCHANGE2; +} + +/* + * Does the gas mix data match the legacy + * libdivecomputer event format? If so, + * we can skip saving it, in order to maintain + * the old save formats. We'll re-generate the + * gas mix when loading. + */ +int event_gasmix_redundant(struct event *ev) +{ + int value = ev->value; + int o2, he; + + o2 = (value & 0xffff) * 10; + he = (value >> 16) * 10; + return o2 == ev->gas.mix.o2.permille && + he == ev->gas.mix.he.permille; +} + +struct event *add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name) +{ + int gas_index = -1; + struct event *ev, **p; + unsigned int size, len = strlen(name); + + size = sizeof(*ev) + len + 1; + ev = malloc(size); + if (!ev) + return NULL; + memset(ev, 0, size); + memcpy(ev->name, name, len); + ev->time.seconds = time; + ev->type = type; + ev->flags = flags; + ev->value = value; + + /* + * Expand the events into a sane format. Currently + * just gas switches + */ + switch (type) { + case SAMPLE_EVENT_GASCHANGE2: + /* High 16 bits are He percentage */ + ev->gas.mix.he.permille = (value >> 16) * 10; + + /* Extension to the GASCHANGE2 format: cylinder index in 'flags' */ + if (flags > 0 && flags <= MAX_CYLINDERS) + gas_index = flags-1; + /* Fallthrough */ + case SAMPLE_EVENT_GASCHANGE: + /* Low 16 bits are O2 percentage */ + ev->gas.mix.o2.permille = (value & 0xffff) * 10; + ev->gas.index = gas_index; + break; + } + + p = &dc->events; + + /* insert in the sorted list of events */ + while (*p && (*p)->time.seconds <= time) + p = &(*p)->next; + ev->next = *p; + *p = ev; + remember_event(name); + return ev; +} + +static int same_event(struct event *a, struct event *b) +{ + if (a->time.seconds != b->time.seconds) + return 0; + if (a->type != b->type) + return 0; + if (a->flags != b->flags) + return 0; + if (a->value != b->value) + return 0; + return !strcmp(a->name, b->name); +} + +void remove_event(struct event *event) +{ + struct event **ep = ¤t_dc->events; + while (ep && !same_event(*ep, event)) + ep = &(*ep)->next; + if (ep) { + /* we can't link directly with event->next + * because 'event' can be a copy from another + * dive (for instance the displayed_dive + * that we use on the interface to show things). */ + struct event *temp = (*ep)->next; + free(*ep); + *ep = temp; + } +} + +/* since the name is an array as part of the structure (how silly is that?) we + * have to actually remove the existing event and replace it with a new one. + * WARNING, WARNING... this may end up freeing event in case that event is indeed + * WARNING, WARNING... part of this divecomputer on this dive! */ +void update_event_name(struct dive *d, struct event *event, char *name) +{ + if (!d || !event) + return; + struct divecomputer *dc = get_dive_dc(d, dc_number); + if (!dc) + return; + struct event **removep = &dc->events; + struct event *remove; + while ((*removep)->next && !same_event(*removep, event)) + removep = &(*removep)->next; + if (!same_event(*removep, event)) + return; + remove = *removep; + *removep = (*removep)->next; + add_event(dc, event->time.seconds, event->type, event->flags, event->value, name); + free(remove); +} + +void add_extra_data(struct divecomputer *dc, const char *key, const char *value) +{ + struct extra_data **ed = &dc->extra_data; + + while (*ed) + ed = &(*ed)->next; + *ed = malloc(sizeof(struct extra_data)); + if (*ed) { + (*ed)->key = strdup(key); + (*ed)->value = strdup(value); + (*ed)->next = NULL; + } +} + +/* this returns a pointer to static variable - so use it right away after calling */ +struct gasmix *get_gasmix_from_event(struct event *ev) +{ + static struct gasmix dummy; + if (ev && event_is_gaschange(ev)) + return &ev->gas.mix; + + return &dummy; +} + +int get_pressure_units(int mb, const char **units) +{ + int pressure; + const char *unit; + struct units *units_p = get_units(); + + switch (units_p->pressure) { + case PASCAL: + pressure = mb * 100; + unit = translate("gettextFromC", "pascal"); + break; + case BAR: + default: + pressure = (mb + 500) / 1000; + unit = translate("gettextFromC", "bar"); + break; + case PSI: + pressure = mbar_to_PSI(mb); + unit = translate("gettextFromC", "psi"); + break; + } + if (units) + *units = unit; + return pressure; +} + +double get_temp_units(unsigned int mk, const char **units) +{ + double deg; + const char *unit; + struct units *units_p = get_units(); + + if (units_p->temperature == FAHRENHEIT) { + deg = mkelvin_to_F(mk); + unit = UTF8_DEGREE "F"; + } else { + deg = mkelvin_to_C(mk); + unit = UTF8_DEGREE "C"; + } + if (units) + *units = unit; + return deg; +} + +double get_volume_units(unsigned int ml, int *frac, const char **units) +{ + int decimals; + double vol; + const char *unit; + struct units *units_p = get_units(); + + switch (units_p->volume) { + case LITER: + default: + vol = ml / 1000.0; + unit = translate("gettextFromC", "â„“"); + decimals = 1; + break; + case CUFT: + vol = ml_to_cuft(ml); + unit = translate("gettextFromC", "cuft"); + decimals = 2; + break; + } + if (frac) + *frac = decimals; + if (units) + *units = unit; + return vol; +} + +int units_to_sac(double volume) +{ + if (get_units()->volume == CUFT) + return rint(cuft_to_l(volume) * 1000.0); + else + return rint(volume * 1000); +} + +unsigned int units_to_depth(double depth) +{ + if (get_units()->length == METERS) + return rint(depth * 1000); + return feet_to_mm(depth); +} + +double get_depth_units(int mm, int *frac, const char **units) +{ + int decimals; + double d; + const char *unit; + struct units *units_p = get_units(); + + switch (units_p->length) { + case METERS: + default: + d = mm / 1000.0; + unit = translate("gettextFromC", "m"); + decimals = d < 20; + break; + case FEET: + d = mm_to_feet(mm); + unit = translate("gettextFromC", "ft"); + decimals = 0; + break; + } + if (frac) + *frac = decimals; + if (units) + *units = unit; + return d; +} + +double get_vertical_speed_units(unsigned int mms, int *frac, const char **units) +{ + double d; + const char *unit; + const struct units *units_p = get_units(); + const double time_factor = units_p->vertical_speed_time == MINUTES ? 60.0 : 1.0; + + switch (units_p->length) { + case METERS: + default: + d = mms / 1000.0 * time_factor; + if (units_p->vertical_speed_time == MINUTES) + unit = translate("gettextFromC", "m/min"); + else + unit = translate("gettextFromC", "m/s"); + break; + case FEET: + d = mm_to_feet(mms) * time_factor; + if (units_p->vertical_speed_time == MINUTES) + unit = translate("gettextFromC", "ft/min"); + else + unit = translate("gettextFromC", "ft/s"); + break; + } + if (frac) + *frac = d < 10; + if (units) + *units = unit; + return d; +} + +double get_weight_units(unsigned int grams, int *frac, const char **units) +{ + int decimals; + double value; + const char *unit; + struct units *units_p = get_units(); + + if (units_p->weight == LBS) { + value = grams_to_lbs(grams); + unit = translate("gettextFromC", "lbs"); + decimals = 0; + } else { + value = grams / 1000.0; + unit = translate("gettextFromC", "kg"); + decimals = 1; + } + if (frac) + *frac = decimals; + if (units) + *units = unit; + return value; +} + +bool has_hr_data(struct divecomputer *dc) +{ + int i; + struct sample *sample; + + if (!dc) + return false; + + sample = dc->sample; + for (i = 0; i < dc->samples; i++) + if (sample[i].heartbeat) + return true; + return false; +} + +struct dive *alloc_dive(void) +{ + struct dive *dive; + + dive = malloc(sizeof(*dive)); + if (!dive) + exit(1); + memset(dive, 0, sizeof(*dive)); + dive->id = dive_getUniqID(dive); + return dive; +} + +static void free_dc(struct divecomputer *dc); +static void free_pic(struct picture *picture); + +/* this is very different from the copy_divecomputer later in this file; + * this function actually makes full copies of the content */ +static void copy_dc(struct divecomputer *sdc, struct divecomputer *ddc) +{ + *ddc = *sdc; + ddc->model = copy_string(sdc->model); + copy_samples(sdc, ddc); + copy_events(sdc, ddc); +} + +/* copy an element in a list of pictures */ +static void copy_pl(struct picture *sp, struct picture *dp) +{ + *dp = *sp; + dp->filename = copy_string(sp->filename); + dp->hash = copy_string(sp->hash); +} + +/* copy an element in a list of tags */ +static void copy_tl(struct tag_entry *st, struct tag_entry *dt) +{ + dt->tag = malloc(sizeof(struct divetag)); + dt->tag->name = copy_string(st->tag->name); + dt->tag->source = copy_string(st->tag->source); +} + +/* Clear everything but the first element; + * this works for taglist, picturelist, even dive computers */ +#define STRUCTURED_LIST_FREE(_type, _start, _free) \ + { \ + _type *_ptr = _start; \ + while (_ptr) { \ + _type *_next = _ptr->next; \ + _free(_ptr); \ + _ptr = _next; \ + } \ + } + +#define STRUCTURED_LIST_COPY(_type, _first, _dest, _cpy) \ + { \ + _type *_sptr = _first; \ + _type **_dptr = &_dest; \ + while (_sptr) { \ + *_dptr = malloc(sizeof(_type)); \ + _cpy(_sptr, *_dptr); \ + _sptr = _sptr->next; \ + _dptr = &(*_dptr)->next; \ + } \ + *_dptr = 0; \ + } + +/* copy_dive makes duplicates of many components of a dive; + * in order not to leak memory, we need to free those . + * copy_dive doesn't play with the divetrip and forward/backward pointers + * so we can ignore those */ +void clear_dive(struct dive *d) +{ + if (!d) + return; + /* free the strings */ + free(d->buddy); + free(d->divemaster); + free(d->notes); + free(d->suit); + /* free tags, additional dive computers, and pictures */ + taglist_free(d->tag_list); + STRUCTURED_LIST_FREE(struct divecomputer, d->dc.next, free_dc); + STRUCTURED_LIST_FREE(struct picture, d->picture_list, free_pic); + for (int i = 0; i < MAX_CYLINDERS; i++) + free((void *)d->cylinder[i].type.description); + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) + free((void *)d->weightsystem[i].description); + memset(d, 0, sizeof(struct dive)); +} + +/* make a true copy that is independent of the source dive; + * all data structures are duplicated, so the copy can be modified without + * any impact on the source */ +void copy_dive(struct dive *s, struct dive *d) +{ + clear_dive(d); + /* simply copy things over, but then make actual copies of the + * relevant components that are referenced through pointers, + * so all the strings and the structured lists */ + *d = *s; + d->buddy = copy_string(s->buddy); + d->divemaster = copy_string(s->divemaster); + d->notes = copy_string(s->notes); + d->suit = copy_string(s->suit); + for (int i = 0; i < MAX_CYLINDERS; i++) + d->cylinder[i].type.description = copy_string(s->cylinder[i].type.description); + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) + d->weightsystem[i].description = copy_string(s->weightsystem[i].description); + STRUCTURED_LIST_COPY(struct picture, s->picture_list, d->picture_list, copy_pl); + STRUCTURED_LIST_COPY(struct tag_entry, s->tag_list, d->tag_list, copy_tl); + STRUCTURED_LIST_COPY(struct divecomputer, s->dc.next, d->dc.next, copy_dc); + /* this only copied dive computers 2 and up. The first dive computer is part + * of the struct dive, so let's make copies of its samples and events */ + copy_samples(&s->dc, &d->dc); + copy_events(&s->dc, &d->dc); +} + +/* make a clone of the source dive and clean out the source dive; + * this is specifically so we can create a dive in the displayed_dive and then + * add it to the divelist. + * Note the difference to copy_dive() / clean_dive() */ +struct dive *clone_dive(struct dive *s) +{ + struct dive *dive = alloc_dive(); + *dive = *s; // so all the pointers in dive point to the things s pointed to + memset(s, 0, sizeof(struct dive)); // and now the pointers in s are gone + return dive; +} + +#define CONDITIONAL_COPY_STRING(_component) \ + if (what._component) \ + d->_component = copy_string(s->_component) + +// copy elements, depending on bits in what that are set +void selective_copy_dive(struct dive *s, struct dive *d, struct dive_components what, bool clear) +{ + if (clear) + clear_dive(d); + CONDITIONAL_COPY_STRING(notes); + CONDITIONAL_COPY_STRING(divemaster); + CONDITIONAL_COPY_STRING(buddy); + CONDITIONAL_COPY_STRING(suit); + if (what.rating) + d->rating = s->rating; + if (what.visibility) + d->visibility = s->visibility; + if (what.divesite) + d->dive_site_uuid = s->dive_site_uuid; + if (what.tags) + STRUCTURED_LIST_COPY(struct tag_entry, s->tag_list, d->tag_list, copy_tl); + if (what.cylinders) + copy_cylinders(s, d, false); + if (what.weights) + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + free((void *)d->weightsystem[i].description); + d->weightsystem[i] = s->weightsystem[i]; + d->weightsystem[i].description = copy_string(s->weightsystem[i].description); + } +} +#undef CONDITIONAL_COPY_STRING + +/* copies all events in this dive computer */ +void copy_events(struct divecomputer *s, struct divecomputer *d) +{ + struct event *ev, **pev; + if (!s || !d) + return; + ev = s->events; + pev = &d->events; + while (ev != NULL) { + int size = sizeof(*ev) + strlen(ev->name) + 1; + struct event *new_ev = malloc(size); + memcpy(new_ev, ev, size); + *pev = new_ev; + pev = &new_ev->next; + ev = ev->next; + } + *pev = NULL; +} + +int nr_cylinders(struct dive *dive) +{ + int nr; + + for (nr = MAX_CYLINDERS; nr; --nr) { + cylinder_t *cylinder = dive->cylinder + nr - 1; + if (!cylinder_nodata(cylinder)) + break; + } + return nr; +} + +int nr_weightsystems(struct dive *dive) +{ + int nr; + + for (nr = MAX_WEIGHTSYSTEMS; nr; --nr) { + weightsystem_t *ws = dive->weightsystem + nr - 1; + if (!weightsystem_none(ws)) + break; + } + return nr; +} + +/* copy the equipment data part of the cylinders */ +void copy_cylinders(struct dive *s, struct dive *d, bool used_only) +{ + int i,j; + if (!s || !d) + return; + for (i = 0; i < MAX_CYLINDERS; i++) { + free((void *)d->cylinder[i].type.description); + memset(&d->cylinder[i], 0, sizeof(cylinder_t)); + } + for (i = j = 0; i < MAX_CYLINDERS; i++) { + if (!used_only || is_cylinder_used(s, i)) { + d->cylinder[j].type = s->cylinder[i].type; + d->cylinder[j].type.description = copy_string(s->cylinder[i].type.description); + d->cylinder[j].gasmix = s->cylinder[i].gasmix; + d->cylinder[j].depth = s->cylinder[i].depth; + d->cylinder[j].cylinder_use = s->cylinder[i].cylinder_use; + d->cylinder[j].manually_added = true; + j++; + } + } +} + +int cylinderuse_from_text(const char *text) +{ + for (enum cylinderuse i = 0; i < NUM_GAS_USE; i++) { + if (same_string(text, cylinderuse_text[i]) || same_string(text, translate("gettextFromC", cylinderuse_text[i]))) + return i; + } + return -1; +} + +void copy_samples(struct divecomputer *s, struct divecomputer *d) +{ + /* instead of carefully copying them one by one and calling add_sample + * over and over again, let's just copy the whole blob */ + if (!s || !d) + return; + int nr = s->samples; + d->samples = nr; + d->alloc_samples = nr; + // We expect to be able to read the memory in the other end of the pointer + // if its a valid pointer, so don't expect malloc() to return NULL for + // zero-sized malloc, do it ourselves. + d->sample = NULL; + + if(!nr) + return; + + d->sample = malloc(nr * sizeof(struct sample)); + if (d->sample) + memcpy(d->sample, s->sample, nr * sizeof(struct sample)); +} + +struct sample *prepare_sample(struct divecomputer *dc) +{ + if (dc) { + int nr = dc->samples; + int alloc_samples = dc->alloc_samples; + struct sample *sample; + if (nr >= alloc_samples) { + struct sample *newsamples; + + alloc_samples = (alloc_samples * 3) / 2 + 10; + newsamples = realloc(dc->sample, alloc_samples * sizeof(struct sample)); + if (!newsamples) + return NULL; + dc->alloc_samples = alloc_samples; + dc->sample = newsamples; + } + sample = dc->sample + nr; + memset(sample, 0, sizeof(*sample)); + return sample; + } + return NULL; +} + +void finish_sample(struct divecomputer *dc) +{ + dc->samples++; +} + +/* + * So when we re-calculate maxdepth and meandepth, we will + * not override the old numbers if they are close to the + * new ones. + * + * Why? Because a dive computer may well actually track the + * max depth and mean depth at finer granularity than the + * samples it stores. So it's possible that the max and mean + * have been reported more correctly originally. + * + * Only if the values calculated from the samples are clearly + * different do we override the normal depth values. + * + * This considers 1m to be "clearly different". That's + * a totally random number. + */ +static void update_depth(depth_t *depth, int new) +{ + if (new) { + int old = depth->mm; + + if (abs(old - new) > 1000) + depth->mm = new; + } +} + +static void update_temperature(temperature_t *temperature, int new) +{ + if (new) { + int old = temperature->mkelvin; + + if (abs(old - new) > 1000) + temperature->mkelvin = new; + } +} + +/* + * Calculate how long we were actually under water, and the average + * depth while under water. + * + * This ignores any surface time in the middle of the dive. + */ +void fixup_dc_duration(struct divecomputer *dc) +{ + int duration, i; + int lasttime, lastdepth, depthtime; + + duration = 0; + lasttime = 0; + lastdepth = 0; + depthtime = 0; + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int depth = sample->depth.mm; + + /* We ignore segments at the surface */ + if (depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) { + duration += time - lasttime; + depthtime += (time - lasttime) * (depth + lastdepth) / 2; + } + lastdepth = depth; + lasttime = time; + } + if (duration) { + dc->duration.seconds = duration; + dc->meandepth.mm = (depthtime + duration / 2) / duration; + } +} + +void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration) +{ + int i; + int depthtime[MAX_CYLINDERS] = { 0, }; + int lasttime = 0, lastdepth = 0; + int idx = 0; + + for (i = 0; i < MAX_CYLINDERS; i++) + mean[i] = duration[i] = 0; + if (!dc) + return; + struct event *ev = get_next_event(dc->events, "gaschange"); + if (!ev || (dc && dc->sample && ev->time.seconds == dc->sample[0].time.seconds && get_next_event(ev->next, "gaschange") == NULL)) { + // we have either no gas change or only one gas change and that's setting an explicit first cylinder + mean[explicit_first_cylinder(dive, dc)] = dc->meandepth.mm; + duration[explicit_first_cylinder(dive, dc)] = dc->duration.seconds; + + if (dc->divemode == CCR) { + // Do the same for the O2 cylinder + int o2_cyl = get_cylinder_idx_by_use(dive, OXYGEN); + if (o2_cyl < 0) + return; + mean[o2_cyl] = dc->meandepth.mm; + duration[o2_cyl] = dc->duration.seconds; + } + return; + } + if (!dc->samples) + dc = fake_dc(dc); + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int depth = sample->depth.mm; + + /* Make sure to move the event past 'lasttime' */ + while (ev && lasttime >= ev->time.seconds) { + idx = get_cylinder_index(dive, ev); + ev = get_next_event(ev->next, "gaschange"); + } + + /* Do we need to fake a midway sample at an event? */ + if (ev && time > ev->time.seconds) { + int newtime = ev->time.seconds; + int newdepth = interpolate(lastdepth, depth, newtime - lasttime, time - lasttime); + + time = newtime; + depth = newdepth; + i--; + } + /* We ignore segments at the surface */ + if (depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) { + duration[idx] += time - lasttime; + depthtime[idx] += (time - lasttime) * (depth + lastdepth) / 2; + } + lastdepth = depth; + lasttime = time; + } + for (i = 0; i < MAX_CYLINDERS; i++) { + if (duration[i]) + mean[i] = (depthtime[i] + duration[i] / 2) / duration[i]; + } +} + +static void fixup_pressure(struct dive *dive, struct sample *sample, enum cylinderuse cyl_use) +{ + int pressure, index; + cylinder_t *cyl; + + if (cyl_use != OXYGEN) { + pressure = sample->cylinderpressure.mbar; + index = sample->sensor; + } else { // for the CCR oxygen cylinder: + pressure = sample->o2cylinderpressure.mbar; + index = get_cylinder_idx_by_use(dive, OXYGEN); + } + if (index < 0) + return; + if (!pressure) + return; + + /* + * Ignore surface samples for tank pressure information. + * + * At the beginning of the dive, let the cylinder cool down + * if the diver starts off at the surface. And at the end + * of the dive, there may be surface pressures where the + * diver has already turned off the air supply (especially + * for computers like the Uemis Zurich that end up saving + * quite a bit of samples after the dive has ended). + */ + if (sample->depth.mm < SURFACE_THRESHOLD) + return; + + /* FIXME! sensor -> cylinder mapping? */ + if (index >= MAX_CYLINDERS) + return; + cyl = dive->cylinder + index; + if (!cyl->sample_start.mbar) + cyl->sample_start.mbar = pressure; + cyl->sample_end.mbar = pressure; +} + +static void update_min_max_temperatures(struct dive *dive, temperature_t temperature) +{ + if (temperature.mkelvin) { + if (!dive->maxtemp.mkelvin || temperature.mkelvin > dive->maxtemp.mkelvin) + dive->maxtemp = temperature; + if (!dive->mintemp.mkelvin || temperature.mkelvin < dive->mintemp.mkelvin) + dive->mintemp = temperature; + } +} + +/* + * At high pressures air becomes less compressible, and + * does not follow the ideal gas law any more. + * + * This tries to correct for that, becoming the same + * as to_ATM() at lower pressures. + * + * THIS IS A ROUGH APPROXIMATION! The real numbers will + * depend on the exact gas mix and temperature. + */ +double surface_volume_multiplier(pressure_t pressure) +{ + double bar = pressure.mbar / 1000.0; + + if (bar > 200) + bar = 0.00038 * bar * bar + 0.51629 * bar + 81.542; + return bar_to_atm(bar); +} + +int gas_volume(cylinder_t *cyl, pressure_t p) +{ + return cyl->type.size.mliter * surface_volume_multiplier(p); +} + +int wet_volume(double cuft, pressure_t p) +{ + return cuft_to_l(cuft) * 1000 / surface_volume_multiplier(p); +} + +/* + * If the cylinder tank pressures are within half a bar + * (about 8 PSI) of the sample pressures, we consider it + * to be a rounding error, and throw them away as redundant. + */ +static int same_rounded_pressure(pressure_t a, pressure_t b) +{ + return abs(a.mbar - b.mbar) <= 500; +} + +/* Some dive computers (Cobalt) don't start the dive with cylinder 0 but explicitly + * tell us what the first gas is with a gas change event in the first sample. + * Sneakily we'll use a return value of 0 (or FALSE) when there is no explicit + * first cylinder - in which case cylinder 0 is indeed the first cylinder */ +int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc) +{ + if (dc) { + struct event *ev = get_next_event(dc->events, "gaschange"); + if (ev && dc->sample && ev->time.seconds == dc->sample[0].time.seconds) + return get_cylinder_index(dive, ev); + else if (dc->divemode == CCR) + return MAX(get_cylinder_idx_by_use(dive, DILUENT), 0); + } + return 0; +} + +/* this gets called when the dive mode has changed (so OC vs. CC) + * there are two places we might have setpoints... events or in the samples + */ +void update_setpoint_events(struct divecomputer *dc) +{ + struct event *ev; + int new_setpoint = 0; + + if (dc->divemode == CCR) + new_setpoint = prefs.defaultsetpoint; + + if (dc->divemode == OC && + (same_string(dc->model, "Shearwater Predator") || + same_string(dc->model, "Shearwater Petrel") || + same_string(dc->model, "Shearwater Nerd"))) { + // make sure there's no setpoint in the samples + // this is an irreversible change - so switching a dive to OC + // by mistake when it's actually CCR is _bad_ + // So we make sure, this comes from a Predator or Petrel and we only remove + // pO2 values we would have computed anyway. + struct event *ev = get_next_event(dc->events, "gaschange"); + struct gasmix *gasmix = get_gasmix_from_event(ev); + struct event *next = get_next_event(ev, "gaschange"); + + for (int i = 0; i < dc->samples; i++) { + struct gas_pressures pressures; + if (next && dc->sample[i].time.seconds >= next->time.seconds) { + ev = next; + gasmix = get_gasmix_from_event(ev); + next = get_next_event(ev, "gaschange"); + } + fill_pressures(&pressures, calculate_depth_to_mbar(dc->sample[i].depth.mm, dc->surface_pressure, 0), gasmix ,0, OC); + if (abs(dc->sample[i].setpoint.mbar - (int)(1000 * pressures.o2) <= 50)) + dc->sample[i].setpoint.mbar = 0; + } + } + + // an "SP change" event at t=0 is currently our marker for OC vs CCR + // this will need to change to a saner setup, but for now we can just + // check if such an event is there and adjust it, or add that event + ev = get_next_event(dc->events, "SP change"); + if (ev && ev->time.seconds == 0) { + ev->value = new_setpoint; + } else { + if (!add_event(dc, 0, SAMPLE_EVENT_PO2, 0, new_setpoint, "SP change")) + fprintf(stderr, "Could not add setpoint change event\n"); + } +} + +void sanitize_gasmix(struct gasmix *mix) +{ + unsigned int o2, he; + + o2 = mix->o2.permille; + he = mix->he.permille; + + /* Regular air: leave empty */ + if (!he) { + if (!o2) + return; + /* 20.8% to 21% O2 is just air */ + if (gasmix_is_air(mix)) { + mix->o2.permille = 0; + return; + } + } + + /* Sane mix? */ + if (o2 <= 1000 && he <= 1000 && o2 + he <= 1000) + return; + fprintf(stderr, "Odd gasmix: %u O2 %u He\n", o2, he); + memset(mix, 0, sizeof(*mix)); +} + +/* + * See if the size/workingpressure looks like some standard cylinder + * size, eg "AL80". + */ +static void match_standard_cylinder(cylinder_type_t *type) +{ + double cuft; + int psi, len; + const char *fmt; + char buffer[40], *p; + + /* Do we already have a cylinder description? */ + if (type->description) + return; + + cuft = ml_to_cuft(type->size.mliter); + cuft *= surface_volume_multiplier(type->workingpressure); + psi = to_PSI(type->workingpressure); + + switch (psi) { + case 2300 ... 2500: /* 2400 psi: LP tank */ + fmt = "LP%d"; + break; + case 2600 ... 2700: /* 2640 psi: LP+10% */ + fmt = "LP%d"; + break; + case 2900 ... 3100: /* 3000 psi: ALx tank */ + fmt = "AL%d"; + break; + case 3400 ... 3500: /* 3442 psi: HP tank */ + fmt = "HP%d"; + break; + case 3700 ... 3850: /* HP+10% */ + fmt = "HP%d+"; + break; + default: + return; + } + len = snprintf(buffer, sizeof(buffer), fmt, (int)rint(cuft)); + p = malloc(len + 1); + if (!p) + return; + memcpy(p, buffer, len + 1); + type->description = p; +} + + +/* + * There are two ways to give cylinder size information: + * - total amount of gas in cuft (depends on working pressure and physical size) + * - physical size + * + * where "physical size" is the one that actually matters and is sane. + * + * We internally use physical size only. But we save the workingpressure + * so that we can do the conversion if required. + */ +static void sanitize_cylinder_type(cylinder_type_t *type) +{ + double volume_of_air, volume; + + /* If we have no working pressure, it had *better* be just a physical size! */ + if (!type->workingpressure.mbar) + return; + + /* No size either? Nothing to go on */ + if (!type->size.mliter) + return; + + if (xml_parsing_units.volume == CUFT) { + /* confusing - we don't really start from ml but millicuft !*/ + volume_of_air = cuft_to_l(type->size.mliter); + /* milliliters at 1 atm: "true size" */ + volume = volume_of_air / surface_volume_multiplier(type->workingpressure); + type->size.mliter = rint(volume); + } + + /* Ok, we have both size and pressure: try to match a description */ + match_standard_cylinder(type); +} + +static void sanitize_cylinder_info(struct dive *dive) +{ + int i; + + for (i = 0; i < MAX_CYLINDERS; i++) { + sanitize_gasmix(&dive->cylinder[i].gasmix); + sanitize_cylinder_type(&dive->cylinder[i].type); + } +} + +/* some events should never be thrown away */ +static bool is_potentially_redundant(struct event *event) +{ + if (!strcmp(event->name, "gaschange")) + return false; + if (!strcmp(event->name, "bookmark")) + return false; + if (!strcmp(event->name, "heading")) + return false; + return true; +} + +/* match just by name - we compare the details in the code that uses this helper */ +static struct event *find_previous_event(struct divecomputer *dc, struct event *event) +{ + struct event *ev = dc->events; + struct event *previous = NULL; + + if (same_string(event->name, "")) + return NULL; + while (ev && ev != event) { + if (same_string(ev->name, event->name)) + previous = ev; + ev = ev->next; + } + return previous; +} + +static void fixup_surface_pressure(struct dive *dive) +{ + struct divecomputer *dc; + int sum = 0, nr = 0; + + for_each_dc (dive, dc) { + if (dc->surface_pressure.mbar) { + sum += dc->surface_pressure.mbar; + nr++; + } + } + if (nr) + dive->surface_pressure.mbar = (sum + nr / 2) / nr; +} + +static void fixup_water_salinity(struct dive *dive) +{ + struct divecomputer *dc; + int sum = 0, nr = 0; + + for_each_dc (dive, dc) { + if (dc->salinity) { + sum += dc->salinity; + nr++; + } + } + if (nr) + dive->salinity = (sum + nr / 2) / nr; +} + +static void fixup_meandepth(struct dive *dive) +{ + struct divecomputer *dc; + int sum = 0, nr = 0; + + for_each_dc (dive, dc) { + if (dc->meandepth.mm) { + sum += dc->meandepth.mm; + nr++; + } + } + if (nr) + dive->meandepth.mm = (sum + nr / 2) / nr; +} + +static void fixup_duration(struct dive *dive) +{ + struct divecomputer *dc; + unsigned int duration = 0; + + for_each_dc (dive, dc) + duration = MAX(duration, dc->duration.seconds); + + dive->duration.seconds = duration; +} + +/* + * What do the dive computers say the water temperature is? + * (not in the samples, but as dc property for dcs that support that) + */ +unsigned int dc_watertemp(struct divecomputer *dc) +{ + int sum = 0, nr = 0; + + do { + if (dc->watertemp.mkelvin) { + sum += dc->watertemp.mkelvin; + nr++; + } + } while ((dc = dc->next) != NULL); + if (!nr) + return 0; + return (sum + nr / 2) / nr; +} + +static void fixup_watertemp(struct dive *dive) +{ + if (!dive->watertemp.mkelvin) + dive->watertemp.mkelvin = dc_watertemp(&dive->dc); +} + +/* + * What do the dive computers say the air temperature is? + */ +unsigned int dc_airtemp(struct divecomputer *dc) +{ + int sum = 0, nr = 0; + + do { + if (dc->airtemp.mkelvin) { + sum += dc->airtemp.mkelvin; + nr++; + } + } while ((dc = dc->next) != NULL); + if (!nr) + return 0; + return (sum + nr / 2) / nr; +} + +static void fixup_cylinder_use(struct dive *dive) // for CCR dives, store the indices +{ // of the oxygen and diluent cylinders + dive->oxygen_cylinder_index = get_cylinder_idx_by_use(dive, OXYGEN); + dive->diluent_cylinder_index = get_cylinder_idx_by_use(dive, DILUENT); +} + +static void fixup_airtemp(struct dive *dive) +{ + if (!dive->airtemp.mkelvin) + dive->airtemp.mkelvin = dc_airtemp(&dive->dc); +} + +/* zero out the airtemp in the dive structure if it was just created by + * running fixup on the dive. keep it if it had been edited by hand */ +static void un_fixup_airtemp(struct dive *a) +{ + if (a->airtemp.mkelvin && a->airtemp.mkelvin == dc_airtemp(&a->dc)) + a->airtemp.mkelvin = 0; +} + +/* + * events are stored as a linked list, so the concept of + * "consecutive, identical events" is somewhat hard to + * implement correctly (especially given that on some dive + * computers events are asynchronous, so they can come in + * between what would be the non-constant sample rate). + * + * So what we do is that we throw away clearly redundant + * events that are fewer than 61 seconds apart (assuming there + * is no dive computer with a sample rate of more than 60 + * seconds... that would be pretty pointless to plot the + * profile with) + * + * We first only mark the events for deletion so that we + * still know when the previous event happened. + */ +static void fixup_dc_events(struct divecomputer *dc) +{ + struct event *event; + + event = dc->events; + while (event) { + struct event *prev; + if (is_potentially_redundant(event)) { + prev = find_previous_event(dc, event); + if (prev && prev->value == event->value && + prev->flags == event->flags && + event->time.seconds - prev->time.seconds < 61) + event->deleted = true; + } + event = event->next; + } + event = dc->events; + while (event) { + if (event->next && event->next->deleted) { + struct event *nextnext = event->next->next; + free(event->next); + event->next = nextnext; + } else { + event = event->next; + } + } +} + +static int interpolate_depth(struct divecomputer *dc, int idx, int lastdepth, int lasttime, int now) +{ + int i; + int nextdepth = lastdepth; + int nexttime = now; + + for (i = idx+1; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + if (sample->depth.mm < 0) + continue; + nextdepth = sample->depth.mm; + nexttime = sample->time.seconds; + break; + } + return interpolate(lastdepth, nextdepth, now-lasttime, nexttime-lasttime); +} + +static void fixup_dive_dc(struct dive *dive, struct divecomputer *dc) +{ + int i, j; + double depthtime = 0; + int lasttime = 0; + int lastindex = -1; + int maxdepth = dc->maxdepth.mm; + int mintemp = 0; + int lastdepth = 0; + int lasttemp = 0; + int lastpressure = 0, lasto2pressure = 0; + int pressure_delta[MAX_CYLINDERS] = { INT_MAX, }; + int first_cylinder; + + /* Add device information to table */ + if (dc->deviceid && (dc->serial || dc->fw_version)) + create_device_node(dc->model, dc->deviceid, dc->serial, dc->fw_version, ""); + + /* Fixup duration and mean depth */ + fixup_dc_duration(dc); + update_min_max_temperatures(dive, dc->watertemp); + + /* make sure we know for which tank the pressure values are intended */ + first_cylinder = explicit_first_cylinder(dive, dc); + for (i = 0; i < dc->samples; i++) { + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int depth = sample->depth.mm; + int temp = sample->temperature.mkelvin; + int pressure = sample->cylinderpressure.mbar; + int o2_pressure = sample->o2cylinderpressure.mbar; + int index; + + if (depth < 0) { + depth = interpolate_depth(dc, i, lastdepth, lasttime, time); + sample->depth.mm = depth; + } + + /* if we have an explicit first cylinder */ + if (sample->sensor == 0 && first_cylinder != 0) + sample->sensor = first_cylinder; + + index = sample->sensor; + + if (index == lastindex) { + /* Remove duplicate redundant pressure information */ + if (pressure == lastpressure) + sample->cylinderpressure.mbar = 0; + if (o2_pressure == lasto2pressure) + sample->o2cylinderpressure.mbar = 0; + /* check for simply linear data in the samples + +INT_MAX means uninitialized, -INT_MAX means not linear */ + if (pressure_delta[index] != -INT_MAX && lastpressure) { + if (pressure_delta[index] == INT_MAX) { + pressure_delta[index] = abs(pressure - lastpressure); + } else { + int cur_delta = abs(pressure - lastpressure); + if (cur_delta && abs(cur_delta - pressure_delta[index]) > 150) { + /* ok the samples aren't just a linearisation + * between start and end */ + pressure_delta[index] = -INT_MAX; + } + } + } + } + lastindex = index; + lastpressure = pressure; + lasto2pressure = o2_pressure; + + if (depth > SURFACE_THRESHOLD) { + if (depth > maxdepth) + maxdepth = depth; + } + + fixup_pressure(dive, sample, OC_GAS); + if (dive->dc.divemode == CCR) + fixup_pressure(dive, sample, OXYGEN); + + if (temp) { + /* + * If we have consecutive identical + * temperature readings, throw away + * the redundant ones. + */ + if (lasttemp == temp) + sample->temperature.mkelvin = 0; + else + lasttemp = temp; + + if (!mintemp || temp < mintemp) + mintemp = temp; + } + + update_min_max_temperatures(dive, sample->temperature); + + depthtime += (time - lasttime) * (lastdepth + depth) / 2; + lastdepth = depth; + lasttime = time; + if (sample->cns > dive->maxcns) + dive->maxcns = sample->cns; + } + + /* if all the samples for a cylinder have pressure data that + * is basically equidistant throw out the sample cylinder pressure + * information but make sure we still have a valid start and end + * pressure + * this happens when DivingLog decides to linearalize the + * pressure between beginning and end and for strange reasons + * decides to put that in the sample data as if it came from + * the dive computer; we don't want that (we'll visualize with + * constant SAC rate instead) + * WARNING WARNING - I have only seen this in single tank dives + * --- maybe I should try to create a multi tank dive and see what + * --- divinglog does there - but the code right now is only tested + * --- for the single tank case */ + for (j = 0; j < MAX_CYLINDERS; j++) { + if (abs(pressure_delta[j]) != INT_MAX) { + cylinder_t *cyl = dive->cylinder + j; + for (i = 0; i < dc->samples; i++) + if (dc->sample[i].sensor == j) + dc->sample[i].cylinderpressure.mbar = 0; + if (!cyl->start.mbar) + cyl->start.mbar = cyl->sample_start.mbar; + if (!cyl->end.mbar) + cyl->end.mbar = cyl->sample_end.mbar; + cyl->sample_start.mbar = 0; + cyl->sample_end.mbar = 0; + } + } + + update_temperature(&dc->watertemp, mintemp); + update_depth(&dc->maxdepth, maxdepth); + if (maxdepth > dive->maxdepth.mm) + dive->maxdepth.mm = maxdepth; + fixup_dc_events(dc); +} + +struct dive *fixup_dive(struct dive *dive) +{ + int i; + struct divecomputer *dc; + + sanitize_cylinder_info(dive); + dive->maxcns = dive->cns; + + /* + * Use the dive's temperatures for minimum and maximum in case + * we do not have temperatures recorded by DC. + */ + + update_min_max_temperatures(dive, dive->watertemp); + + for_each_dc (dive, dc) + fixup_dive_dc(dive, dc); + + fixup_water_salinity(dive); + fixup_surface_pressure(dive); + fixup_meandepth(dive); + fixup_duration(dive); + fixup_watertemp(dive); + fixup_airtemp(dive); + fixup_cylinder_use(dive); // store indices for CCR oxygen and diluent cylinders + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + add_cylinder_description(&cyl->type); + if (same_rounded_pressure(cyl->sample_start, cyl->start)) + cyl->start.mbar = 0; + if (same_rounded_pressure(cyl->sample_end, cyl->end)) + cyl->end.mbar = 0; + } + update_cylinder_related_info(dive); + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + weightsystem_t *ws = dive->weightsystem + i; + add_weightsystem_description(ws); + } + /* we should always have a uniq ID as that gets assigned during alloc_dive(), + * but we want to make sure... */ + if (!dive->id) + dive->id = dive_getUniqID(dive); + + return dive; +} + +/* Don't pick a zero for MERGE_MIN() */ +#define MERGE_MAX(res, a, b, n) res->n = MAX(a->n, b->n) +#define MERGE_MIN(res, a, b, n) res->n = (a->n) ? (b->n) ? MIN(a->n, b->n) : (a->n) : (b->n) +#define MERGE_TXT(res, a, b, n) res->n = merge_text(a->n, b->n) +#define MERGE_NONZERO(res, a, b, n) res->n = a->n ? a->n : b->n + +struct sample *add_sample(struct sample *sample, int time, struct divecomputer *dc) +{ + struct sample *p = prepare_sample(dc); + + if (p) { + *p = *sample; + p->time.seconds = time; + finish_sample(dc); + } + return p; +} + +/* + * This is like add_sample(), but if the distance from the last sample + * is excessive, we add two surface samples in between. + * + * This is so that if you merge two non-overlapping dives, we make sure + * that the time in between the dives is at the surface, not some "last + * sample that happened to be at a depth of 1.2m". + */ +static void merge_one_sample(struct sample *sample, int time, struct divecomputer *dc) +{ + int last = dc->samples - 1; + if (last >= 0) { + static struct sample surface; + struct sample *prev = dc->sample + last; + int last_time = prev->time.seconds; + int last_depth = prev->depth.mm; + + /* + * Only do surface events if the samples are more than + * a minute apart, and shallower than 5m + */ + if (time > last_time + 60 && last_depth < 5000) { + add_sample(&surface, last_time + 20, dc); + add_sample(&surface, time - 20, dc); + } + } + add_sample(sample, time, dc); +} + + +/* + * Merge samples. Dive 'a' is "offset" seconds before Dive 'b' + */ +static void merge_samples(struct divecomputer *res, struct divecomputer *a, struct divecomputer *b, int offset) +{ + int asamples = a->samples; + int bsamples = b->samples; + struct sample *as = a->sample; + struct sample *bs = b->sample; + + /* + * We want a positive sample offset, so that sample + * times are always positive. So if the samples for + * 'b' are before the samples for 'a' (so the offset + * is negative), we switch a and b around, and use + * the reverse offset. + */ + if (offset < 0) { + offset = -offset; + asamples = bsamples; + bsamples = a->samples; + as = bs; + bs = a->sample; + } + + for (;;) { + int at, bt; + struct sample sample; + + if (!res) + return; + + at = asamples ? as->time.seconds : -1; + bt = bsamples ? bs->time.seconds + offset : -1; + + /* No samples? All done! */ + if (at < 0 && bt < 0) + return; + + /* Only samples from a? */ + if (bt < 0) { + add_sample_a: + merge_one_sample(as, at, res); + as++; + asamples--; + continue; + } + + /* Only samples from b? */ + if (at < 0) { + add_sample_b: + merge_one_sample(bs, bt, res); + bs++; + bsamples--; + continue; + } + + if (at < bt) + goto add_sample_a; + if (at > bt) + goto add_sample_b; + + /* same-time sample: add a merged sample. Take the non-zero ones */ + sample = *bs; + if (as->depth.mm) + sample.depth = as->depth; + if (as->temperature.mkelvin) + sample.temperature = as->temperature; + if (as->cylinderpressure.mbar) + sample.cylinderpressure = as->cylinderpressure; + if (as->sensor) + sample.sensor = as->sensor; + if (as->cns) + sample.cns = as->cns; + if (as->setpoint.mbar) + sample.setpoint = as->setpoint; + if (as->ndl.seconds) + sample.ndl = as->ndl; + if (as->stoptime.seconds) + sample.stoptime = as->stoptime; + if (as->stopdepth.mm) + sample.stopdepth = as->stopdepth; + if (as->in_deco) + sample.in_deco = true; + + merge_one_sample(&sample, at, res); + + as++; + bs++; + asamples--; + bsamples--; + } +} + +static char *merge_text(const char *a, const char *b) +{ + char *res; + if (!a && !b) + return NULL; + if (!a || !*a) + return copy_string(b); + if (!b || !*b) + return strdup(a); + if (!strcmp(a, b)) + return copy_string(a); + res = malloc(strlen(a) + strlen(b) + 32); + if (!res) + return (char *)a; + sprintf(res, translate("gettextFromC", "(%s) or (%s)"), a, b); + return res; +} + +#define SORT(a, b, field) \ + if (a->field != b->field) \ + return a->field < b->field ? -1 : 1 + +static int sort_event(struct event *a, struct event *b) +{ + SORT(a, b, time.seconds); + SORT(a, b, type); + SORT(a, b, flags); + SORT(a, b, value); + return strcmp(a->name, b->name); +} + +static void merge_events(struct divecomputer *res, struct divecomputer *src1, struct divecomputer *src2, int offset) +{ + struct event *a, *b; + struct event **p = &res->events; + + /* Always use positive offsets */ + if (offset < 0) { + struct divecomputer *tmp; + + offset = -offset; + tmp = src1; + src1 = src2; + src2 = tmp; + } + + a = src1->events; + b = src2->events; + while (b) { + b->time.seconds += offset; + b = b->next; + } + b = src2->events; + + while (a || b) { + int s; + if (!b) { + *p = a; + break; + } + if (!a) { + *p = b; + break; + } + s = sort_event(a, b); + /* Pick b */ + if (s > 0) { + *p = b; + p = &b->next; + b = b->next; + continue; + } + /* Pick 'a' or neither */ + if (s < 0) { + *p = a; + p = &a->next; + } + a = a->next; + continue; + } +} + +/* Pick whichever has any info (if either). Prefer 'a' */ +static void merge_cylinder_type(cylinder_type_t *src, cylinder_type_t *dst) +{ + if (!dst->size.mliter) + dst->size.mliter = src->size.mliter; + if (!dst->workingpressure.mbar) + dst->workingpressure.mbar = src->workingpressure.mbar; + if (!dst->description) { + dst->description = src->description; + src->description = NULL; + } +} + +static void merge_cylinder_mix(struct gasmix *src, struct gasmix *dst) +{ + if (!dst->o2.permille) + *dst = *src; +} + +static void merge_cylinder_info(cylinder_t *src, cylinder_t *dst) +{ + merge_cylinder_type(&src->type, &dst->type); + merge_cylinder_mix(&src->gasmix, &dst->gasmix); + MERGE_MAX(dst, dst, src, start.mbar); + MERGE_MIN(dst, dst, src, end.mbar); +} + +static void merge_weightsystem_info(weightsystem_t *res, weightsystem_t *a, weightsystem_t *b) +{ + if (!a->weight.grams) + a = b; + *res = *a; +} + +/* get_cylinder_idx_by_use(): Find the index of the first cylinder with a particular CCR use type. + * The index returned corresponds to that of the first cylinder with a cylinder_use that + * equals the appropriate enum value [oxygen, diluent, bailout] given by cylinder_use_type. + * A negative number returned indicates that a match could not be found. + * Call parameters: dive = the dive being processed + * cylinder_use_type = an enum, one of {oxygen, diluent, bailout} */ +extern int get_cylinder_idx_by_use(struct dive *dive, enum cylinderuse cylinder_use_type) +{ + int cylinder_index; + for (cylinder_index = 0; cylinder_index < MAX_CYLINDERS; cylinder_index++) { + if (dive->cylinder[cylinder_index].cylinder_use == cylinder_use_type) + return cylinder_index; // return the index of the cylinder with that cylinder use type + } + return -1; // negative number means cylinder_use_type not found in list of cylinders +} + +int gasmix_distance(const struct gasmix *a, const struct gasmix *b) +{ + int a_o2 = get_o2(a), b_o2 = get_o2(b); + int a_he = get_he(a), b_he = get_he(b); + int delta_o2 = a_o2 - b_o2, delta_he = a_he - b_he; + + delta_he = delta_he * delta_he; + delta_o2 = delta_o2 * delta_o2; + return delta_he + delta_o2; +} + +/* fill_pressures(): Compute partial gas pressures in bar from gasmix and ambient pressures, possibly for OC or CCR, to be + * extended to PSCT. This function does the calculations of gass pressures applicable to a single point on the dive profile. + * The structure "pressures" is used to return calculated gas pressures to the calling software. + * Call parameters: po2 = po2 value applicable to the record in calling function + * amb_pressure = ambient pressure applicable to the record in calling function + * *pressures = structure for communicating o2 sensor values from and gas pressures to the calling function. + * *mix = structure containing cylinder gas mixture information. + * This function called by: calculate_gas_information_new() in profile.c; add_segment() in deco.c. + */ +extern void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, const struct gasmix *mix, double po2, enum dive_comp_type divemode) +{ + if (po2) { // This is probably a CCR dive where pressures->o2 is defined + if (po2 >= amb_pressure) { + pressures->o2 = amb_pressure; + pressures->n2 = pressures->he = 0.0; + } else { + pressures->o2 = po2; + if (get_o2(mix) == 1000) { + pressures->he = pressures->n2 = 0; + } else { + pressures->he = (amb_pressure - pressures->o2) * (double)get_he(mix) / (1000 - get_o2(mix)); + pressures->n2 = amb_pressure - pressures->o2 - pressures->he; + } + } + } else { + if (divemode == PSCR) { /* The steady state approximation should be good enough */ + pressures->o2 = get_o2(mix) / 1000.0 * amb_pressure - (1.0 - get_o2(mix) / 1000.0) * prefs.o2consumption / (prefs.bottomsac * prefs.pscr_ratio / 1000.0); + if (pressures->o2 < 0) // He's dead, Jim. + pressures->o2 = 0; + if (get_o2(mix) != 1000) { + pressures->he = (amb_pressure - pressures->o2) * get_he(mix) / (1000.0 - get_o2(mix)); + pressures->n2 = (amb_pressure - pressures->o2) * (1000 - get_o2(mix) - get_he(mix)) / (1000.0 - get_o2(mix)); + } else { + pressures->he = pressures->n2 = 0; + } + } else { + // Open circuit dives: no gas pressure values available, they need to be calculated + pressures->o2 = get_o2(mix) / 1000.0 * amb_pressure; // These calculations are also used if the CCR calculation above.. + pressures->he = get_he(mix) / 1000.0 * amb_pressure; // ..returned a po2 of zero (i.e. o2 sensor data not resolvable) + pressures->n2 = (1000 - get_o2(mix) - get_he(mix)) / 1000.0 * amb_pressure; + } + } +} + +static int find_cylinder_match(cylinder_t *cyl, cylinder_t array[], unsigned int used) +{ + int i; + int best = -1, score = INT_MAX; + + if (cylinder_nodata(cyl)) + return -1; + for (i = 0; i < MAX_CYLINDERS; i++) { + const cylinder_t *match; + int distance; + + if (used & (1 << i)) + continue; + match = array + i; + distance = gasmix_distance(&cyl->gasmix, &match->gasmix); + if (distance >= score) + continue; + best = i; + score = distance; + } + return best; +} + +/* Force an initial gaschange event to the (old) gas #0 */ +static void add_initial_gaschange(struct dive *dive, struct divecomputer *dc) +{ + struct event *ev = get_next_event(dc->events, "gaschange"); + + if (ev && ev->time.seconds < 30) + return; + + /* Old starting gas mix */ + add_gas_switch_event(dive, dc, 0, 0); +} + +void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]) +{ + int i; + struct event *ev; + + /* Did the first gas get remapped? Add gas switch event */ + if (mapping[0] > 0) + add_initial_gaschange(dive, dc); + + /* Remap the sensor indexes */ + for (i = 0; i < dc->samples; i++) { + struct sample *s = dc->sample + i; + int sensor; + + if (!s->cylinderpressure.mbar) + continue; + sensor = mapping[s->sensor]; + if (sensor >= 0) + s->sensor = sensor; + } + + /* Remap the gas change indexes */ + for (ev = dc->events; ev; ev = ev->next) { + if (!event_is_gaschange(ev)) + continue; + if (ev->gas.index < 0) + continue; + ev->gas.index = mapping[ev->gas.index]; + } +} + +/* + * If the cylinder indexes change (due to merging dives or deleting + * cylinders in the middle), we need to change the indexes in the + * dive computer data for this dive. + * + * Also note that we assume that the initial cylinder is cylinder 0, + * so if that got renamed, we need to create a fake gas change event + */ +static void cylinder_renumber(struct dive *dive, int mapping[]) +{ + struct divecomputer *dc; + for_each_dc (dive, dc) + dc_cylinder_renumber(dive, dc, mapping); +} + +/* + * Merging cylinder information is non-trivial, because the two dive computers + * may have different ideas of what the different cylinder indexing is. + * + * Logic: take all the cylinder information from the preferred dive ('a'), and + * then try to match each of the cylinders in the other dive by the gasmix that + * is the best match and hasn't been used yet. + */ +static void merge_cylinders(struct dive *res, struct dive *a, struct dive *b) +{ + int i, renumber = 0; + int mapping[MAX_CYLINDERS]; + unsigned int used = 0; + + /* Copy the cylinder info raw from 'a' */ + memcpy(res->cylinder, a->cylinder, sizeof(res->cylinder)); + memset(a->cylinder, 0, sizeof(a->cylinder)); + + for (i = 0; i < MAX_CYLINDERS; i++) { + int j; + cylinder_t *cyl = b->cylinder + i; + + j = find_cylinder_match(cyl, res->cylinder, used); + mapping[i] = j; + if (j < 0) + continue; + used |= 1 << j; + merge_cylinder_info(cyl, res->cylinder + j); + + /* If that renumbered the cylinders, fix it up! */ + if (i != j) + renumber = 1; + } + if (renumber) + cylinder_renumber(b, mapping); +} + +static void merge_equipment(struct dive *res, struct dive *a, struct dive *b) +{ + int i; + + merge_cylinders(res, a, b); + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + merge_weightsystem_info(res->weightsystem + i, a->weightsystem + i, b->weightsystem + i); +} + +static void merge_airtemps(struct dive *res, struct dive *a, struct dive *b) +{ + un_fixup_airtemp(a); + un_fixup_airtemp(b); + MERGE_NONZERO(res, a, b, airtemp.mkelvin); +} + +/* + * When merging two dives, this picks the trip from one, and removes it + * from the other. + * + * The 'next' dive is not involved in the dive merging, but is the dive + * that will be the next dive after the merged dive. + */ +static void pick_trip(struct dive *res, struct dive *pick) +{ + tripflag_t tripflag = pick->tripflag; + dive_trip_t *trip = pick->divetrip; + + res->tripflag = tripflag; + add_dive_to_trip(res, trip); +} + +/* + * Pick a trip for a dive + */ +static void merge_trip(struct dive *res, struct dive *a, struct dive *b) +{ + dive_trip_t *atrip, *btrip; + + /* + * The larger tripflag is more relevant: we prefer + * take manually assigned trips over auto-generated + * ones. + */ + if (a->tripflag > b->tripflag) + goto pick_a; + + if (a->tripflag < b->tripflag) + goto pick_b; + + /* Otherwise, look at the trip data and pick the "better" one */ + atrip = a->divetrip; + btrip = b->divetrip; + if (!atrip) + goto pick_b; + if (!btrip) + goto pick_a; + if (!atrip->location) + goto pick_b; + if (!btrip->location) + goto pick_a; + if (!atrip->notes) + goto pick_b; + if (!btrip->notes) + goto pick_a; + + /* + * Ok, so both have location and notes. + * Pick the earlier one. + */ + if (a->when < b->when) + goto pick_a; + goto pick_b; + +pick_a: + b = a; +pick_b: + pick_trip(res, b); +} + +#if CURRENTLY_NOT_USED +/* + * Sample 's' is between samples 'a' and 'b'. It is 'offset' seconds before 'b'. + * + * If 's' and 'a' are at the same time, offset is 0, and b is NULL. + */ +static int compare_sample(struct sample *s, struct sample *a, struct sample *b, int offset) +{ + unsigned int depth = a->depth.mm; + int diff; + + if (offset) { + unsigned int interval = b->time.seconds - a->time.seconds; + unsigned int depth_a = a->depth.mm; + unsigned int depth_b = b->depth.mm; + + if (offset > interval) + return -1; + + /* pick the average depth, scaled by the offset from 'b' */ + depth = (depth_a * offset) + (depth_b * (interval - offset)); + depth /= interval; + } + diff = s->depth.mm - depth; + if (diff < 0) + diff = -diff; + /* cut off at one meter difference */ + if (diff > 1000) + diff = 1000; + return diff * diff; +} + +/* + * Calculate a "difference" in samples between the two dives, given + * the offset in seconds between them. Use this to find the best + * match of samples between two different dive computers. + */ +static unsigned long sample_difference(struct divecomputer *a, struct divecomputer *b, int offset) +{ + int asamples = a->samples; + int bsamples = b->samples; + struct sample *as = a->sample; + struct sample *bs = b->sample; + unsigned long error = 0; + int start = -1; + + if (!asamples || !bsamples) + return 0; + + /* + * skip the first sample - this way we know can always look at + * as/bs[-1] to look at the samples around it in the loop. + */ + as++; + bs++; + asamples--; + bsamples--; + + for (;;) { + int at, bt, diff; + + + /* If we run out of samples, punt */ + if (!asamples) + return INT_MAX; + if (!bsamples) + return INT_MAX; + + at = as->time.seconds; + bt = bs->time.seconds + offset; + + /* b hasn't started yet? Ignore it */ + if (bt < 0) { + bs++; + bsamples--; + continue; + } + + if (at < bt) { + diff = compare_sample(as, bs - 1, bs, bt - at); + as++; + asamples--; + } else if (at > bt) { + diff = compare_sample(bs, as - 1, as, at - bt); + bs++; + bsamples--; + } else { + diff = compare_sample(as, bs, NULL, 0); + as++; + bs++; + asamples--; + bsamples--; + } + + /* Invalid comparison point? */ + if (diff < 0) + continue; + + if (start < 0) + start = at; + + error += diff; + + if (at - start > 120) + break; + } + return error; +} + +/* + * Dive 'a' is 'offset' seconds before dive 'b' + * + * This is *not* because the dive computers clocks aren't in sync, + * it is because the dive computers may "start" the dive at different + * points in the dive, so the sample at time X in dive 'a' is the + * same as the sample at time X+offset in dive 'b'. + * + * For example, some dive computers take longer to "wake up" when + * they sense that you are under water (ie Uemis Zurich if it was off + * when the dive started). And other dive computers have different + * depths that they activate at, etc etc. + * + * If we cannot find a shared offset, don't try to merge. + */ +static int find_sample_offset(struct divecomputer *a, struct divecomputer *b) +{ + int offset, best; + unsigned long max; + + /* No samples? Merge at any time (0 offset) */ + if (!a->samples) + return 0; + if (!b->samples) + return 0; + + /* + * Common special-case: merging a dive that came from + * the same dive computer, so the samples are identical. + * Check this first, without wasting time trying to find + * some minimal offset case. + */ + best = 0; + max = sample_difference(a, b, 0); + if (!max) + return 0; + + /* + * Otherwise, look if we can find anything better within + * a thirty second window.. + */ + for (offset = -30; offset <= 30; offset++) { + unsigned long diff; + + diff = sample_difference(a, b, offset); + if (diff > max) + continue; + best = offset; + max = diff; + } + + return best; +} +#endif + +/* + * Are a and b "similar" values, when given a reasonable lower end expected + * difference? + * + * So for example, we'd expect different dive computers to give different + * max depth readings. You might have them on different arms, and they + * have different pressure sensors and possibly different ideas about + * water salinity etc. + * + * So have an expected minimum difference, but also allow a larger relative + * error value. + */ +static int similar(unsigned long a, unsigned long b, unsigned long expected) +{ + if (a && b) { + unsigned long min, max, diff; + + min = a; + max = b; + if (a > b) { + min = b; + max = a; + } + diff = max - min; + + /* Smaller than expected difference? */ + if (diff < expected) + return 1; + /* Error less than 10% or the maximum */ + if (diff * 10 < max) + return 1; + } + return 0; +} + +/* + * Match two dive computer entries against each other, and + * tell if it's the same dive. Return 0 if "don't know", + * positive for "same dive" and negative for "definitely + * not the same dive" + */ +int match_one_dc(struct divecomputer *a, struct divecomputer *b) +{ + /* Not same model? Don't know if matching.. */ + if (!a->model || !b->model) + return 0; + if (strcasecmp(a->model, b->model)) + return 0; + + /* Different device ID's? Don't know */ + if (a->deviceid != b->deviceid) + return 0; + + /* Do we have dive IDs? */ + if (!a->diveid || !b->diveid) + return 0; + + /* + * If they have different dive ID's on the same + * dive computer, that's a definite "same or not" + */ + return a->diveid == b->diveid ? 1 : -1; +} + +/* + * Match every dive computer against each other to see if + * we have a matching dive. + * + * Return values: + * -1 for "is definitely *NOT* the same dive" + * 0 for "don't know" + * 1 for "is definitely the same dive" + */ +static int match_dc_dive(struct divecomputer *a, struct divecomputer *b) +{ + do { + struct divecomputer *tmp = b; + do { + int match = match_one_dc(a, tmp); + if (match) + return match; + tmp = tmp->next; + } while (tmp); + a = a->next; + } while (a); + return 0; +} + +static bool new_without_trip(struct dive *a) +{ + return a->downloaded && !a->divetrip; +} + +/* + * Do we want to automatically try to merge two dives that + * look like they are the same dive? + * + * This happens quite commonly because you download a dive + * that you already had, or perhaps because you maintained + * multiple dive logs and want to load them all together + * (possibly one of them was imported from another dive log + * application entirely). + * + * NOTE! We mainly look at the dive time, but it can differ + * between two dives due to a few issues: + * + * - rounding the dive date to the nearest minute in other dive + * applications + * + * - dive computers with "relative datestamps" (ie the dive + * computer doesn't actually record an absolute date at all, + * but instead at download-time syncronizes its internal + * time with real-time on the downloading computer) + * + * - using multiple dive computers with different real time on + * the same dive + * + * We do not merge dives that look radically different, and if + * the dates are *too* far off the user will have to join two + * dives together manually. But this tries to handle the sane + * cases. + */ +static int likely_same_dive(struct dive *a, struct dive *b) +{ + int match, fuzz = 20 * 60; + + /* don't merge manually added dives with anything */ + if (same_string(a->dc.model, "manually added dive") || + same_string(b->dc.model, "manually added dive")) + return 0; + + /* Don't try to merge dives with different trip information */ + if (a->divetrip != b->divetrip) { + /* + * Exception: if the dive is downloaded without any + * explicit trip information, we do want to merge it + * with existing old dives even if they have trips. + */ + if (!new_without_trip(a) && !new_without_trip(b)) + return 0; + } + + /* + * Do some basic sanity testing of the values we + * have filled in during 'fixup_dive()' + */ + if (!similar(a->maxdepth.mm, b->maxdepth.mm, 1000) || + (a->meandepth.mm && b->meandepth.mm && !similar(a->meandepth.mm, b->meandepth.mm, 1000)) || + !similar(a->duration.seconds, b->duration.seconds, 5 * 60)) + return 0; + + /* See if we can get an exact match on the dive computer */ + match = match_dc_dive(&a->dc, &b->dc); + if (match) + return match > 0; + + /* + * Allow a time difference due to dive computer time + * setting etc. Check if they overlap. + */ + fuzz = MAX(a->duration.seconds, b->duration.seconds) / 2; + if (fuzz < 60) + fuzz = 60; + + return ((a->when <= b->when + fuzz) && (a->when >= b->when - fuzz)); +} + +/* + * This could do a lot more merging. Right now it really only + * merges almost exact duplicates - something that happens easily + * with overlapping dive downloads. + */ +struct dive *try_to_merge(struct dive *a, struct dive *b, bool prefer_downloaded) +{ + if (likely_same_dive(a, b)) + return merge_dives(a, b, 0, prefer_downloaded); + return NULL; +} + +void free_events(struct event *ev) +{ + while (ev) { + struct event *next = ev->next; + free(ev); + ev = next; + } +} + +static void free_dc(struct divecomputer *dc) +{ + free(dc->sample); + free((void *)dc->model); + free_events(dc->events); + free(dc); +} + +static void free_pic(struct picture *picture) +{ + if (picture) { + free(picture->filename); + free(picture); + } +} + +static int same_sample(struct sample *a, struct sample *b) +{ + if (a->time.seconds != b->time.seconds) + return 0; + if (a->depth.mm != b->depth.mm) + return 0; + if (a->temperature.mkelvin != b->temperature.mkelvin) + return 0; + if (a->cylinderpressure.mbar != b->cylinderpressure.mbar) + return 0; + return a->sensor == b->sensor; +} + +static int same_dc(struct divecomputer *a, struct divecomputer *b) +{ + int i; + struct event *eva, *evb; + + i = match_one_dc(a, b); + if (i) + return i > 0; + + if (a->when && b->when && a->when != b->when) + return 0; + if (a->samples != b->samples) + return 0; + for (i = 0; i < a->samples; i++) + if (!same_sample(a->sample + i, b->sample + i)) + return 0; + eva = a->events; + evb = b->events; + while (eva && evb) { + if (!same_event(eva, evb)) + return 0; + eva = eva->next; + evb = evb->next; + } + return eva == evb; +} + +static int might_be_same_device(struct divecomputer *a, struct divecomputer *b) +{ + /* No dive computer model? That matches anything */ + if (!a->model || !b->model) + return 1; + + /* Otherwise at least the model names have to match */ + if (strcasecmp(a->model, b->model)) + return 0; + + /* No device ID? Match */ + if (!a->deviceid || !b->deviceid) + return 1; + + return a->deviceid == b->deviceid; +} + +static void remove_redundant_dc(struct divecomputer *dc, int prefer_downloaded) +{ + do { + struct divecomputer **p = &dc->next; + + /* Check this dc against all the following ones.. */ + while (*p) { + struct divecomputer *check = *p; + if (same_dc(dc, check) || (prefer_downloaded && might_be_same_device(dc, check))) { + *p = check->next; + check->next = NULL; + free_dc(check); + continue; + } + p = &check->next; + } + + /* .. and then continue down the chain, but we */ + prefer_downloaded = 0; + dc = dc->next; + } while (dc); +} + +static void clear_dc(struct divecomputer *dc) +{ + memset(dc, 0, sizeof(*dc)); +} + +static struct divecomputer *find_matching_computer(struct divecomputer *match, struct divecomputer *list) +{ + struct divecomputer *p; + + while ((p = list) != NULL) { + list = list->next; + + if (might_be_same_device(match, p)) + break; + } + return p; +} + + +static void copy_dive_computer(struct divecomputer *res, struct divecomputer *a) +{ + *res = *a; + res->model = copy_string(a->model); + res->samples = res->alloc_samples = 0; + res->sample = NULL; + res->events = NULL; + res->next = NULL; +} + +/* + * Join dive computers with a specific time offset between + * them. + * + * Use the dive computer ID's (or names, if ID's are missing) + * to match them up. If we find a matching dive computer, we + * merge them. If not, we just take the data from 'a'. + */ +static void interleave_dive_computers(struct divecomputer *res, + struct divecomputer *a, struct divecomputer *b, int offset) +{ + do { + struct divecomputer *match; + + copy_dive_computer(res, a); + + match = find_matching_computer(a, b); + if (match) { + merge_events(res, a, match, offset); + merge_samples(res, a, match, offset); + /* Use the diveid of the later dive! */ + if (offset > 0) + res->diveid = match->diveid; + } else { + res->sample = a->sample; + res->samples = a->samples; + res->events = a->events; + a->sample = NULL; + a->samples = 0; + a->events = NULL; + } + a = a->next; + if (!a) + break; + res->next = calloc(1, sizeof(struct divecomputer)); + res = res->next; + } while (res); +} + + +/* + * Join dive computer information. + * + * If we have old-style dive computer information (no model + * name etc), we will prefer a new-style one and just throw + * away the old. We're assuming it's a re-download. + * + * Otherwise, we'll just try to keep all the information, + * unless the user has specified that they prefer the + * downloaded computer, in which case we'll aggressively + * try to throw out old information that *might* be from + * that one. + */ +static void join_dive_computers(struct divecomputer *res, struct divecomputer *a, struct divecomputer *b, int prefer_downloaded) +{ + struct divecomputer *tmp; + + if (a->model && !b->model) { + *res = *a; + clear_dc(a); + return; + } + if (b->model && !a->model) { + *res = *b; + clear_dc(b); + return; + } + + *res = *a; + clear_dc(a); + tmp = res; + while (tmp->next) + tmp = tmp->next; + + tmp->next = calloc(1, sizeof(*tmp)); + *tmp->next = *b; + clear_dc(b); + + remove_redundant_dc(res, prefer_downloaded); +} + +static bool tag_seen_before(struct tag_entry *start, struct tag_entry *before) +{ + while (start && start != before) { + if (same_string(start->tag->name, before->tag->name)) + return true; + start = start->next; + } + return false; +} + +/* remove duplicates and empty nodes */ +void taglist_cleanup(struct tag_entry **tag_list) +{ + struct tag_entry **tl = tag_list; + while (*tl) { + /* skip tags that are empty or that we have seen before */ + if (same_string((*tl)->tag->name, "") || tag_seen_before(*tag_list, *tl)) { + *tl = (*tl)->next; + continue; + } + tl = &(*tl)->next; + } +} + +int taglist_get_tagstring(struct tag_entry *tag_list, char *buffer, int len) +{ + int i = 0; + struct tag_entry *tmp; + tmp = tag_list; + memset(buffer, 0, len); + while (tmp != NULL) { + int newlength = strlen(tmp->tag->name); + if (i > 0) + newlength += 2; + if ((i + newlength) < len) { + if (i > 0) { + strcpy(buffer + i, ", "); + strcpy(buffer + i + 2, tmp->tag->name); + } else { + strcpy(buffer, tmp->tag->name); + } + } else { + return i; + } + i += newlength; + tmp = tmp->next; + } + return i; +} + +static inline void taglist_free_divetag(struct divetag *tag) +{ + if (tag->name != NULL) + free(tag->name); + if (tag->source != NULL) + free(tag->source); + free(tag); +} + +/* Add a tag to the tag_list, keep the list sorted */ +static struct divetag *taglist_add_divetag(struct tag_entry **tag_list, struct divetag *tag) +{ + struct tag_entry *next, *entry; + + while ((next = *tag_list) != NULL) { + int cmp = strcmp(next->tag->name, tag->name); + + /* Already have it? */ + if (!cmp) + return next->tag; + /* Is the entry larger? If so, insert here */ + if (cmp > 0) + break; + /* Continue traversing the list */ + tag_list = &next->next; + } + + /* Insert in front of it */ + entry = malloc(sizeof(struct tag_entry)); + entry->next = next; + entry->tag = tag; + *tag_list = entry; + return tag; +} + +struct divetag *taglist_add_tag(struct tag_entry **tag_list, const char *tag) +{ + int i = 0, is_default_tag = 0; + struct divetag *ret_tag, *new_tag; + const char *translation; + new_tag = malloc(sizeof(struct divetag)); + + for (i = 0; i < sizeof(default_tags) / sizeof(char *); i++) { + if (strcmp(default_tags[i], tag) == 0) { + is_default_tag = 1; + break; + } + } + /* Only translate default tags */ + if (is_default_tag) { + translation = translate("gettextFromC", tag); + new_tag->name = malloc(strlen(translation) + 1); + memcpy(new_tag->name, translation, strlen(translation) + 1); + new_tag->source = malloc(strlen(tag) + 1); + memcpy(new_tag->source, tag, strlen(tag) + 1); + } else { + new_tag->source = NULL; + new_tag->name = malloc(strlen(tag) + 1); + memcpy(new_tag->name, tag, strlen(tag) + 1); + } + /* Try to insert new_tag into g_tag_list if we are not operating on it */ + if (tag_list != &g_tag_list) { + ret_tag = taglist_add_divetag(&g_tag_list, new_tag); + /* g_tag_list already contains new_tag, free the duplicate */ + if (ret_tag != new_tag) + taglist_free_divetag(new_tag); + ret_tag = taglist_add_divetag(tag_list, ret_tag); + } else { + ret_tag = taglist_add_divetag(tag_list, new_tag); + if (ret_tag != new_tag) + taglist_free_divetag(new_tag); + } + return ret_tag; +} + +void taglist_free(struct tag_entry *entry) +{ + STRUCTURED_LIST_FREE(struct tag_entry, entry, free) +} + +/* Merge src1 and src2, write to *dst */ +static void taglist_merge(struct tag_entry **dst, struct tag_entry *src1, struct tag_entry *src2) +{ + struct tag_entry *entry; + + for (entry = src1; entry; entry = entry->next) + taglist_add_divetag(dst, entry->tag); + for (entry = src2; entry; entry = entry->next) + taglist_add_divetag(dst, entry->tag); +} + +void taglist_init_global() +{ + int i; + + for (i = 0; i < sizeof(default_tags) / sizeof(char *); i++) + taglist_add_tag(&g_tag_list, default_tags[i]); +} + +bool taglist_contains(struct tag_entry *tag_list, const char *tag) +{ + while (tag_list) { + if (same_string(tag_list->tag->name, tag)) + return true; + tag_list = tag_list->next; + } + return false; +} + +// check if all tags in subtl are included in supertl (so subtl is a subset of supertl) +static bool taglist_contains_all(struct tag_entry *subtl, struct tag_entry *supertl) +{ + while (subtl) { + if (!taglist_contains(supertl, subtl->tag->name)) + return false; + subtl = subtl->next; + } + return true; +} + +struct tag_entry *taglist_added(struct tag_entry *original_list, struct tag_entry *new_list) +{ + struct tag_entry *added_list = NULL; + while (new_list) { + if (!taglist_contains(original_list, new_list->tag->name)) + taglist_add_tag(&added_list, new_list->tag->name); + new_list = new_list->next; + } + return added_list; +} + +void dump_taglist(const char *intro, struct tag_entry *tl) +{ + char *comma = ""; + fprintf(stderr, "%s", intro); + while(tl) { + fprintf(stderr, "%s %s", comma, tl->tag->name); + comma = ","; + tl = tl->next; + } + fprintf(stderr, "\n"); +} + +// if tl1 is both a subset and superset of tl2 they must be the same +bool taglist_equal(struct tag_entry *tl1, struct tag_entry *tl2) +{ + return taglist_contains_all(tl1, tl2) && taglist_contains_all(tl2, tl1); +} + +// count the dives where the tag list contains the given tag +int count_dives_with_tag(const char *tag) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(tag, "")) { + // count dives with no tags + if (d->tag_list == NULL) + counter++; + } else if (taglist_contains(d->tag_list, tag)) { + counter++; + } + } + return counter; +} + +extern bool string_sequence_contains(const char *string_sequence, const char *text); + +// count the dives where the person is included in the comma separated string sequences of buddies or divemasters +int count_dives_with_person(const char *person) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(person, "")) { + // solo dive + if (same_string(d->buddy, "") && same_string(d->divemaster, "")) + counter++; + } else if (string_sequence_contains(d->buddy, person) || string_sequence_contains(d->divemaster, person)) { + counter++; + } + } + return counter; +} + +// count the dives with exactly the location +int count_dives_with_location(const char *location) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(get_dive_location(d), location)) + counter++; + } + return counter; +} + +// count the dives with exactly the suit +int count_dives_with_suit(const char *suit) +{ + int i, counter = 0; + struct dive *d; + + for_each_dive (i, d) { + if (same_string(d->suit, suit)) + counter++; + } + return counter; +} + +/* + * Merging two dives can be subtle, because there's two different ways + * of merging: + * + * (a) two distinctly _different_ dives that have the same dive computer + * are merged into one longer dive, because the user asked for it + * in the divelist. + * + * Because this case is with teh same dive computer, we *know* the + * two must have a different start time, and "offset" is the relative + * time difference between the two. + * + * (a) two different dive computers that we migth want to merge into + * one single dive with multiple dive computers. + * + * This is the "try_to_merge()" case, which will have offset == 0, + * even if the dive times migth be different. + */ +struct dive *merge_dives(struct dive *a, struct dive *b, int offset, bool prefer_downloaded) +{ + struct dive *res = alloc_dive(); + struct dive *dl = NULL; + + if (offset) { + /* + * If "likely_same_dive()" returns true, that means that + * it is *not* the same dive computer, and we do not want + * to try to turn it into a single longer dive. So we'd + * join them as two separate dive computers at zero offset. + */ + if (likely_same_dive(a, b)) + offset = 0; + } else { + /* Aim for newly downloaded dives to be 'b' (keep old dive data first) */ + if (a->downloaded && !b->downloaded) { + struct dive *tmp = a; + a = b; + b = tmp; + } + if (prefer_downloaded && b->downloaded) + dl = b; + } + + res->when = dl ? dl->when : a->when; + res->selected = a->selected || b->selected; + merge_trip(res, a, b); + MERGE_TXT(res, a, b, notes); + MERGE_TXT(res, a, b, buddy); + MERGE_TXT(res, a, b, divemaster); + MERGE_MAX(res, a, b, rating); + MERGE_TXT(res, a, b, suit); + MERGE_MAX(res, a, b, number); + MERGE_NONZERO(res, a, b, cns); + MERGE_NONZERO(res, a, b, visibility); + MERGE_NONZERO(res, a, b, picture_list); + taglist_merge(&res->tag_list, a->tag_list, b->tag_list); + merge_equipment(res, a, b); + merge_airtemps(res, a, b); + if (dl) { + /* If we prefer downloaded, do those first, and get rid of "might be same" computers */ + join_dive_computers(&res->dc, &dl->dc, &a->dc, 1); + } else if (offset && might_be_same_device(&a->dc, &b->dc)) + interleave_dive_computers(&res->dc, &a->dc, &b->dc, offset); + else + join_dive_computers(&res->dc, &a->dc, &b->dc, 0); + res->dive_site_uuid = a->dive_site_uuid ?: b->dive_site_uuid; + fixup_dive(res); + return res; +} + +// copy_dive(), but retaining the new ID for the copied dive +static struct dive *create_new_copy(struct dive *from) +{ + struct dive *to = alloc_dive(); + int id; + + // alloc_dive() gave us a new ID, we just need to + // make sure it's not overwritten. + id = to->id; + copy_dive(from, to); + to->id = id; + return to; +} + +static void force_fixup_dive(struct dive *d) +{ + struct divecomputer *dc = &d->dc; + int old_maxdepth = dc->maxdepth.mm; + int old_temp = dc->watertemp.mkelvin; + int old_mintemp = d->mintemp.mkelvin; + int old_maxtemp = d->maxtemp.mkelvin; + duration_t old_duration = d->duration; + + d->maxdepth.mm = 0; + dc->maxdepth.mm = 0; + d->watertemp.mkelvin = 0; + dc->watertemp.mkelvin = 0; + d->duration.seconds = 0; + d->maxtemp.mkelvin = 0; + d->mintemp.mkelvin = 0; + + fixup_dive(d); + + if (!d->watertemp.mkelvin) + d->watertemp.mkelvin = old_temp; + + if (!dc->watertemp.mkelvin) + dc->watertemp.mkelvin = old_temp; + + if (!d->maxtemp.mkelvin) + d->maxtemp.mkelvin = old_maxtemp; + + if (!d->mintemp.mkelvin) + d->mintemp.mkelvin = old_mintemp; + + if (!d->duration.seconds) + d->duration = old_duration; + +} + +/* + * Split a dive that has a surface interval from samples 'a' to 'b' + * into two dives. + */ +static int split_dive_at(struct dive *dive, int a, int b) +{ + int i, t, nr; + struct dive *d1, *d2; + struct divecomputer *dc1, *dc2; + struct event *event, **evp; + + /* if we can't find the dive in the dive list, don't bother */ + if ((nr = get_divenr(dive)) < 0) + return 0; + + /* We're not trying to be efficient here.. */ + d1 = create_new_copy(dive); + d2 = create_new_copy(dive); + + /* now unselect the first first segment so we don't keep all + * dives selected by mistake. But do keep the second one selected + * so the algorithm keeps splitting the dive further */ + d1->selected = false; + + dc1 = &d1->dc; + dc2 = &d2->dc; + /* + * Cut off the samples of d1 at the beginning + * of the interval. + */ + dc1->samples = a; + + /* And get rid of the 'b' first samples of d2 */ + dc2->samples -= b; + memmove(dc2->sample, dc2->sample+b, dc2->samples * sizeof(struct sample)); + + /* + * This is where we cut off events from d1, + * and shift everything in d2 + */ + t = dc2->sample[0].time.seconds; + d2->when += t; + for (i = 0; i < dc2->samples; i++) + dc2->sample[i].time.seconds -= t; + + /* Remove the events past 't' from d1 */ + evp = &dc1->events; + while ((event = *evp) != NULL && event->time.seconds < t) + evp = &event->next; + *evp = NULL; + while (event) { + struct event *next = event->next; + free(event); + event = next; + } + + /* Remove the events before 't' from d2, and shift the rest */ + evp = &dc2->events; + while ((event = *evp) != NULL) { + if (event->time.seconds < t) { + *evp = event->next; + free(event); + } else { + event->time.seconds -= t; + } + } + + force_fixup_dive(d1); + force_fixup_dive(d2); + + if (dive->divetrip) { + d1->divetrip = d2->divetrip = 0; + add_dive_to_trip(d1, dive->divetrip); + add_dive_to_trip(d2, dive->divetrip); + } + + delete_single_dive(nr); + add_single_dive(nr, d1); + + /* + * Was the dive numbered? If it was the last dive, then we'll + * increment the dive number for the tail part that we split off. + * Otherwise the tail is unnumbered. + */ + if (d2->number) { + if (dive_table.nr == nr + 1) + d2->number++; + else + d2->number = 0; + } + add_single_dive(nr + 1, d2); + + mark_divelist_changed(true); + + return 1; +} + +/* in freedive mode we split for as little as 10 seconds on the surface, + * otherwise we use a minute */ +static bool should_split(struct divecomputer *dc, int t1, int t2) +{ + int threshold = dc->divemode == FREEDIVE ? 10 : 60; + + return t2 - t1 >= threshold; +} + +/* + * Try to split a dive into multiple dives at a surface interval point. + * + * NOTE! We will not split dives with multiple dive computers, and + * only split when there is at least one surface event that has + * non-surface events on both sides. + * + * In other words, this is a (simplified) reversal of the dive merging. + */ +int split_dive(struct dive *dive) +{ + int i; + int at_surface, surface_start; + struct divecomputer *dc; + + if (!dive || (dc = &dive->dc)->next) + return 0; + + surface_start = 0; + at_surface = 1; + for (i = 1; i < dc->samples; i++) { + struct sample *sample = dc->sample+i; + int surface_sample = sample->depth.mm < SURFACE_THRESHOLD; + + /* + * We care about the transition from and to depth 0, + * not about the depth staying similar. + */ + if (at_surface == surface_sample) + continue; + at_surface = surface_sample; + + // Did it become surface after having been non-surface? We found the start + if (at_surface) { + surface_start = i; + continue; + } + + // Goind down again? We want at least a minute from + // the surface start. + if (!surface_start) + continue; + if (!should_split(dc, dc->sample[surface_start].time.seconds, sample[i - 1].time.seconds)) + continue; + + return split_dive_at(dive, surface_start, i-1); + } + return 0; +} + +/* + * "dc_maxtime()" is how much total time this dive computer + * has for this dive. Note that it can differ from "duration" + * if there are surface events in the middle. + * + * Still, we do ignore all but the last surface samples from the + * end, because some divecomputers just generate lots of them. + */ +static inline int dc_totaltime(const struct divecomputer *dc) +{ + int time = dc->duration.seconds; + int nr = dc->samples; + + while (nr--) { + struct sample *s = dc->sample + nr; + time = s->time.seconds; + if (s->depth.mm >= SURFACE_THRESHOLD) + break; + } + return time; +} + +/* + * The end of a dive is actually not trivial, because "duration" + * is not the duration until the end, but the time we spend under + * water, which can be very different if there are surface events + * during the dive. + * + * So walk the dive computers, looking for the longest actual + * time in the samples (and just default to the dive duration if + * there are no samples). + */ +static inline int dive_totaltime(const struct dive *dive) +{ + int time = dive->duration.seconds; + const struct divecomputer *dc; + + for_each_dc(dive, dc) { + int dc_time = dc_totaltime(dc); + if (dc_time > time) + time = dc_time; + } + return time; +} + +timestamp_t dive_endtime(const struct dive *dive) +{ + return dive->when + dive_totaltime(dive); +} + +struct dive *find_dive_including(timestamp_t when) +{ + int i; + struct dive *dive; + + /* binary search, anyone? Too lazy for now; + * also we always use the duration from the first divecomputer + * could this ever be a problem? */ + for_each_dive (i, dive) { + if (dive->when <= when && when <= dive_endtime(dive)) + return dive; + } + return NULL; +} + +bool time_during_dive_with_offset(struct dive *dive, timestamp_t when, timestamp_t offset) +{ + timestamp_t start = dive->when; + timestamp_t end = dive_endtime(dive); + return start - offset <= when && when <= end + offset; +} + +bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset) +{ + timestamp_t start = dive->when; + timestamp_t end = dive_endtime(dive); + return when - offset <= start && end <= when + offset; +} + +/* find the n-th dive that is part of a group of dives within the offset around 'when'. + * How is that for a vague definition of what this function should do... */ +struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset) +{ + int i, j = 0; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive_within_time_range(dive, when, offset)) + if (++j == n) + return dive; + } + return NULL; +} + +void shift_times(const timestamp_t amount) +{ + int i; + struct dive *dive; + + for_each_dive (i, dive) { + if (!dive->selected) + continue; + dive->when += amount; + } +} + +timestamp_t get_times() +{ + int i; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive->selected) + break; + } + return dive->when; +} + +void set_save_userid_local(short value) +{ + prefs.save_userid_local = value; +} + +void set_userid(char *rUserId) +{ + if (prefs.userid) + free(prefs.userid); + prefs.userid = strdup(rUserId); + if (strlen(prefs.userid) > 30) + prefs.userid[30]='\0'; +} + +/* this sets a usually unused copy of the preferences with the units + * that were active the last time the dive list was saved to git storage + * (this isn't used in XML files); storing the unit preferences in the + * data file is usually pointless (that's a setting of the software, + * not a property of the data), but it's a great hint of what the user + * might expect to see when creating a backend service that visualizes + * the dive list without Subsurface running - so this is basically a + * functionality for the core library that Subsurface itself doesn't + * use but that another consumer of the library (like an HTML exporter) + * will need */ +void set_informational_units(char *units) +{ + if (strstr(units, "METRIC")) { + informational_prefs.unit_system = METRIC; + } else if (strstr(units, "IMPERIAL")) { + informational_prefs.unit_system = IMPERIAL; + } else if (strstr(units, "PERSONALIZE")) { + informational_prefs.unit_system = PERSONALIZE; + if (strstr(units, "METERS")) + informational_prefs.units.length = METERS; + if (strstr(units, "FEET")) + informational_prefs.units.length = FEET; + if (strstr(units, "LITER")) + informational_prefs.units.volume = LITER; + if (strstr(units, "CUFT")) + informational_prefs.units.volume = CUFT; + if (strstr(units, "BAR")) + informational_prefs.units.pressure = BAR; + if (strstr(units, "PSI")) + informational_prefs.units.pressure = PSI; + if (strstr(units, "PASCAL")) + informational_prefs.units.pressure = PASCAL; + if (strstr(units, "CELSIUS")) + informational_prefs.units.temperature = CELSIUS; + if (strstr(units, "FAHRENHEIT")) + informational_prefs.units.temperature = FAHRENHEIT; + if (strstr(units, "KG")) + informational_prefs.units.weight = KG; + if (strstr(units, "LBS")) + informational_prefs.units.weight = LBS; + if (strstr(units, "SECONDS")) + informational_prefs.units.vertical_speed_time = SECONDS; + if (strstr(units, "MINUTES")) + informational_prefs.units.vertical_speed_time = MINUTES; + } +} + +void average_max_depth(struct diveplan *dive, int *avg_depth, int *max_depth) +{ + int integral = 0; + int last_time = 0; + int last_depth = 0; + struct divedatapoint *dp = dive->dp; + + *max_depth = 0; + + while (dp) { + if (dp->time) { + /* Ignore gas indication samples */ + integral += (dp->depth + last_depth) * (dp->time - last_time) / 2; + last_time = dp->time; + last_depth = dp->depth; + if (dp->depth > *max_depth) + *max_depth = dp->depth; + } + dp = dp->next; + } + if (last_time) + *avg_depth = integral / last_time; + else + *avg_depth = *max_depth = 0; +} + +struct picture *alloc_picture() +{ + struct picture *pic = malloc(sizeof(struct picture)); + if (!pic) + exit(1); + memset(pic, 0, sizeof(struct picture)); + return pic; +} + +static bool new_picture_for_dive(struct dive *d, char *filename) +{ + FOR_EACH_PICTURE (d) { + if (same_string(picture->filename, filename)) + return false; + } + return true; +} + +// only add pictures that have timestamps between 30 minutes before the dive and +// 30 minutes after the dive ends +#define D30MIN (30 * 60) +bool dive_check_picture_time(struct dive *d, int shift_time, timestamp_t timestamp) +{ + offset_t offset; + if (timestamp) { + offset.seconds = timestamp - d->when + shift_time; + if (offset.seconds > -D30MIN && offset.seconds < dive_totaltime(d) + D30MIN) { + // this picture belongs to this dive + return true; + } + } + return false; +} + +bool picture_check_valid(char *filename, int shift_time) +{ + int i; + struct dive *dive; + + timestamp_t timestamp = picture_get_timestamp(filename); + for_each_dive (i, dive) + if (dive->selected && dive_check_picture_time(dive, shift_time, timestamp)) + return true; + return false; +} + +void dive_create_picture(struct dive *dive, char *filename, int shift_time, bool match_all) +{ + timestamp_t timestamp = picture_get_timestamp(filename); + if (!new_picture_for_dive(dive, filename)) + return; + if (!match_all && !dive_check_picture_time(dive, shift_time, timestamp)) + return; + + struct picture *picture = alloc_picture(); + picture->filename = strdup(filename); + picture->offset.seconds = timestamp - dive->when + shift_time; + picture_load_exif_data(picture); + + dive_add_picture(dive, picture); + dive_set_geodata_from_picture(dive, picture); +} + +void dive_add_picture(struct dive *dive, struct picture *newpic) +{ + struct picture **pic_ptr = &dive->picture_list; + /* let's keep the list sorted by time */ + while (*pic_ptr && (*pic_ptr)->offset.seconds <= newpic->offset.seconds) + pic_ptr = &(*pic_ptr)->next; + newpic->next = *pic_ptr; + *pic_ptr = newpic; + cache_picture(newpic); + return; +} + +unsigned int dive_get_picture_count(struct dive *dive) +{ + unsigned int i = 0; + FOR_EACH_PICTURE (dive) + i++; + return i; +} + +void dive_set_geodata_from_picture(struct dive *dive, struct picture *picture) +{ + struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (!dive_site_has_gps_location(ds) && (picture->latitude.udeg || picture->longitude.udeg)) { + if (ds) { + ds->latitude = picture->latitude; + ds->longitude = picture->longitude; + } else { + dive->dive_site_uuid = create_dive_site_with_gps("", picture->latitude, picture->longitude, dive->when); + } + } +} + +static void picture_free(struct picture *picture) +{ + if (!picture) + return; + free(picture->filename); + free(picture->hash); + free(picture); +} + +void dive_remove_picture(char *filename) +{ + struct picture **picture = ¤t_dive->picture_list; + while (picture && !same_string((*picture)->filename, filename)) + picture = &(*picture)->next; + if (picture) { + struct picture *temp = (*picture)->next; + picture_free(*picture); + *picture = temp; + } +} + +/* this always acts on the current divecomputer of the current dive */ +void make_first_dc() +{ + struct divecomputer *dc = ¤t_dive->dc; + struct divecomputer *newdc = malloc(sizeof(*newdc)); + struct divecomputer *cur_dc = current_dc; /* needs to be in a local variable so the macro isn't re-executed */ + + /* skip the current DC in the linked list */ + while (dc && dc->next != cur_dc) + dc = dc->next; + if (!dc) { + free(newdc); + fprintf(stderr, "data inconsistent: can't find the current DC"); + return; + } + dc->next = cur_dc->next; + *newdc = current_dive->dc; + current_dive->dc = *cur_dc; + current_dive->dc.next = newdc; + free(cur_dc); +} + +/* always acts on the current dive */ +int count_divecomputers(void) +{ + int ret = 1; + struct divecomputer *dc = current_dive->dc.next; + while (dc) { + ret++; + dc = dc->next; + } + return ret; +} + +/* always acts on the current dive */ +void delete_current_divecomputer(void) +{ + struct divecomputer *dc = current_dc; + + if (dc == ¤t_dive->dc) { + /* remove the first one, so copy the second one in place of the first and free the second one + * be careful about freeing the no longer needed structures - since we copy things around we can't use free_dc()*/ + struct divecomputer *fdc = dc->next; + free(dc->sample); + free((void *)dc->model); + free_events(dc->events); + memcpy(dc, fdc, sizeof(struct divecomputer)); + free(fdc); + } else { + struct divecomputer *pdc = ¤t_dive->dc; + while (pdc->next != dc && pdc->next) + pdc = pdc->next; + if (pdc->next == dc) { + pdc->next = dc->next; + free_dc(dc); + } + } + if (dc_number == count_divecomputers()) + dc_number--; +} + +/* helper function to make it easier to work with our structures + * we don't interpolate here, just use the value from the last sample up to that time */ +int get_depth_at_time(struct divecomputer *dc, int time) +{ + int depth = 0; + if (dc && dc->sample) + for (int i = 0; i < dc->samples; i++) { + if (dc->sample[i].time.seconds > time) + break; + depth = dc->sample[i].depth.mm; + } + return depth; +} diff --git a/subsurface-core/dive.h b/subsurface-core/dive.h new file mode 100644 index 000000000..cef1106fd --- /dev/null +++ b/subsurface-core/dive.h @@ -0,0 +1,894 @@ +#ifndef DIVE_H +#define DIVE_H + +#include +#include +#include +#include +#include +#include +#include +#include "divesite.h" + +/* Windows has no MIN/MAX macros - so let's just roll our own */ +#define MIN(x, y) ({ \ + __typeof__(x) _min1 = (x); \ + __typeof__(y) _min2 = (y); \ + (void) (&_min1 == &_min2); \ + _min1 < _min2 ? _min1 : _min2; }) + +#define MAX(x, y) ({ \ + __typeof__(x) _max1 = (x); \ + __typeof__(y) _max2 = (y); \ + (void) (&_max1 == &_max2); \ + _max1 > _max2 ? _max1 : _max2; }) + +#define IS_FP_SAME(_a, _b) (fabs((_a) - (_b)) <= 0.000001 * MAX(fabs(_a), fabs(_b))) + +static inline int same_string(const char *a, const char *b) +{ + return !strcmp(a ?: "", b ?: ""); +} + +static inline char *copy_string(const char *s) +{ + return (s && *s) ? strdup(s) : NULL; +} + +#include +#include +#include + +#include "sha1.h" +#include "units.h" + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +extern int last_xml_version; + +enum dive_comp_type {OC, CCR, PSCR, FREEDIVE, NUM_DC_TYPE}; // Flags (Open-circuit and Closed-circuit-rebreather) for setting dive computer type +enum cylinderuse {OC_GAS, DILUENT, OXYGEN, NUM_GAS_USE}; // The different uses for cylinders + +extern const char *cylinderuse_text[]; +extern const char *divemode_text[]; + +struct gasmix { + fraction_t o2; + fraction_t he; +}; + +typedef struct +{ + volume_t size; + pressure_t workingpressure; + const char *description; /* "LP85", "AL72", "AL80", "HP100+" or whatever */ +} cylinder_type_t; + +typedef struct +{ + cylinder_type_t type; + struct gasmix gasmix; + pressure_t start, end, sample_start, sample_end; + depth_t depth; + bool manually_added; + volume_t gas_used; + volume_t deco_gas_used; + enum cylinderuse cylinder_use; +} cylinder_t; + +typedef struct +{ + weight_t weight; + const char *description; /* "integrated", "belt", "ankle" */ +} weightsystem_t; + +/* + * Events are currently based straight on what libdivecomputer gives us. + * We need to wrap these into our own events at some point to remove some of the limitations. + */ +struct event { + struct event *next; + duration_t time; + int type; + /* This is the annoying libdivecomputer format. */ + int flags, value; + /* .. and this is our "extended" data for some event types */ + union { + /* + * Currently only for gas switch events. + * + * NOTE! The index may be -1, which means "unknown". In that + * case, the get_cylinder_index() function will give the best + * match with the cylinders in the dive based on gasmix. + */ + struct { + int index; + struct gasmix mix; + } gas; + }; + bool deleted; + char name[]; +}; + +extern int event_is_gaschange(struct event *ev); +extern int event_gasmix_redundant(struct event *ev); + +extern int get_pressure_units(int mb, const char **units); +extern double get_depth_units(int mm, int *frac, const char **units); +extern double get_volume_units(unsigned int ml, int *frac, const char **units); +extern double get_temp_units(unsigned int mk, const char **units); +extern double get_weight_units(unsigned int grams, int *frac, const char **units); +extern double get_vertical_speed_units(unsigned int mms, int *frac, const char **units); + +extern unsigned int units_to_depth(double depth); +extern int units_to_sac(double volume); + +/* Volume in mliter of a cylinder at pressure 'p' */ +extern int gas_volume(cylinder_t *cyl, pressure_t p); +extern int wet_volume(double cuft, pressure_t p); + + +static inline int get_o2(const struct gasmix *mix) +{ + return mix->o2.permille ?: O2_IN_AIR; +} + +static inline int get_he(const struct gasmix *mix) +{ + return mix->he.permille; +} + +struct gas_pressures { + double o2, n2, he; +}; + +extern void fill_pressures(struct gas_pressures *pressures, const double amb_pressure, const struct gasmix *mix, double po2, enum dive_comp_type dctype); + +extern void sanitize_gasmix(struct gasmix *mix); +extern int gasmix_distance(const struct gasmix *a, const struct gasmix *b); +extern struct gasmix *get_gasmix_from_event(struct event *ev); + +static inline bool gasmix_is_air(const struct gasmix *gasmix) +{ + int o2 = gasmix->o2.permille; + int he = gasmix->he.permille; + return (he == 0) && (o2 == 0 || ((o2 >= O2_IN_AIR - 1) && (o2 <= O2_IN_AIR + 1))); +} + +/* Linear interpolation between 'a' and 'b', when we are 'part'way into the 'whole' distance from a to b */ +static inline int interpolate(int a, int b, int part, int whole) +{ + /* It is doubtful that we actually need floating point for this, but whatever */ + double x = (double)a * (whole - part) + (double)b * part; + return rint(x / whole); +} + +void get_gas_string(const struct gasmix *gasmix, char *text, int len); +const char *gasname(const struct gasmix *gasmix); + +struct sample // BASE TYPE BYTES UNITS RANGE DESCRIPTION +{ // --------- ----- ----- ----- ----------- + duration_t time; // uint32_t 4 seconds (0-68 yrs) elapsed dive time up to this sample + duration_t stoptime; // uint32_t 4 seconds (0-18 h) time duration of next deco stop + duration_t ndl; // uint32_t 4 seconds (0-18 h) time duration before no-deco limit + duration_t tts; // uint32_t 4 seconds (0-18 h) time duration to reach the surface + duration_t rbt; // uint32_t 4 seconds (0-18 h) remaining bottom time + depth_t depth; // int32_t 4 mm (0-2000 km) dive depth of this sample + depth_t stopdepth; // int32_t 4 mm (0-2000 km) depth of next deco stop + temperature_t temperature; // int32_t 4 mdegrK (0-2 MdegK) ambient temperature + pressure_t cylinderpressure; // int32_t 4 mbar (0-2 Mbar) main cylinder pressure + pressure_t o2cylinderpressure; // int32_t 4 mbar (0-2 Mbar) CCR o2 cylinder pressure (rebreather) + o2pressure_t setpoint; // uint16_t 2 mbar (0-65 bar) O2 partial pressure (will be setpoint) + o2pressure_t o2sensor[3]; // uint16_t 6 mbar (0-65 bar) Up to 3 PO2 sensor values (rebreather) + bearing_t bearing; // int16_t 2 degrees (-32k to 32k deg) compass bearing + uint8_t sensor; // uint8_t 1 sensorID (0-255) ID of cylinder pressure sensor + uint8_t cns; // uint8_t 1 % (0-255 %) cns% accumulated + uint8_t heartbeat; // uint8_t 1 beats/m (0-255) heart rate measurement + volume_t sac; // 4 ml/min predefined SAC + bool in_deco; // bool 1 y/n y/n this sample is part of deco + bool manually_entered; // bool 1 y/n y/n this sample was entered by the user, + // not calculated when planning a dive +}; // Total size of structure: 57 bytes, excluding padding at end + +struct divetag { + /* + * The name of the divetag. If a translation is available, name contains + * the translated tag + */ + char *name; + /* + * If a translation is available, we write the original tag to source. + * This enables us to write a non-localized tag to the xml file. + */ + char *source; +}; + +struct tag_entry { + struct divetag *tag; + struct tag_entry *next; +}; + +/* + * divetags are only stored once, each dive only contains + * a list of tag_entries which then point to the divetags + * in the global g_tag_list + */ + +extern struct tag_entry *g_tag_list; + +struct divetag *taglist_add_tag(struct tag_entry **tag_list, const char *tag); +struct tag_entry *taglist_added(struct tag_entry *original_list, struct tag_entry *new_list); +void dump_taglist(const char *intro, struct tag_entry *tl); + +/* + * Writes all divetags in tag_list to buffer, limited by the buffer's (len)gth. + * Returns the characters written + */ +int taglist_get_tagstring(struct tag_entry *tag_list, char *buffer, int len); + +/* cleans up a list: removes empty tags and duplicates */ +void taglist_cleanup(struct tag_entry **tag_list); + +void taglist_init_global(); +void taglist_free(struct tag_entry *tag_list); + +bool taglist_contains(struct tag_entry *tag_list, const char *tag); +bool taglist_equal(struct tag_entry *tl1, struct tag_entry *tl2); +int count_dives_with_tag(const char *tag); +int count_dives_with_person(const char *person); +int count_dives_with_location(const char *location); +int count_dives_with_suit(const char *suit); + +struct extra_data { + const char *key; + const char *value; + struct extra_data *next; +}; + +/* + * NOTE! The deviceid and diveid are model-specific *hashes* of + * whatever device identification that model may have. Different + * dive computers will have different identifying data, it could + * be a firmware number or a serial ID (in either string or in + * numeric format), and we do not care. + * + * The only thing we care about is that subsurface will hash + * that information the same way. So then you can check the ID + * of a dive computer by comparing the hashes for equality. + * + * A deviceid or diveid of zero is assumed to be "no ID". + */ +struct divecomputer { + timestamp_t when; + duration_t duration, surfacetime; + depth_t maxdepth, meandepth; + temperature_t airtemp, watertemp; + pressure_t surface_pressure; + enum dive_comp_type divemode; // dive computer type: OC(default) or CCR + uint8_t no_o2sensors; // rebreathers: number of O2 sensors used + int salinity; // kg per 10000 l + const char *model, *serial, *fw_version; + uint32_t deviceid, diveid; + int samples, alloc_samples; + struct sample *sample; + struct event *events; + struct extra_data *extra_data; + struct divecomputer *next; +}; + +#define MAX_CYLINDERS (8) +#define MAX_WEIGHTSYSTEMS (6) +#define W_IDX_PRIMARY 0 +#define W_IDX_SECONDARY 1 + +typedef enum { + TF_NONE, + NO_TRIP, + IN_TRIP, + ASSIGNED_TRIP, + NUM_TRIPFLAGS +} tripflag_t; + +typedef struct dive_trip +{ + timestamp_t when; + char *location; + char *notes; + struct dive *dives; + int nrdives; + int index; + unsigned expanded : 1, selected : 1, autogen : 1, fixup : 1; + struct dive_trip *next; +} dive_trip_t; + +/* List of dive trips (sorted by date) */ +extern dive_trip_t *dive_trip_list; +struct picture; +struct dive { + int number; + tripflag_t tripflag; + dive_trip_t *divetrip; + struct dive *next, **pprev; + bool selected; + bool hidden_by_filter; + bool downloaded; + timestamp_t when; + uint32_t dive_site_uuid; + char *notes; + char *divemaster, *buddy; + int rating; + int visibility; /* 0 - 5 star rating */ + cylinder_t cylinder[MAX_CYLINDERS]; + weightsystem_t weightsystem[MAX_WEIGHTSYSTEMS]; + char *suit; + int sac, otu, cns, maxcns; + + /* Calculated based on dive computer data */ + temperature_t mintemp, maxtemp, watertemp, airtemp; + depth_t maxdepth, meandepth; + pressure_t surface_pressure; + duration_t duration; + int salinity; // kg per 10000 l + + struct tag_entry *tag_list; + struct divecomputer dc; + int id; // unique ID for this dive + struct picture *picture_list; + int oxygen_cylinder_index, diluent_cylinder_index; // CCR dive cylinder indices +}; + +extern int get_cylinder_idx_by_use(struct dive *dive, enum cylinderuse cylinder_use_type); +extern void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]); + +/* when selectively copying dive information, which parts should be copied? */ +struct dive_components { + unsigned int divesite : 1; + unsigned int notes : 1; + unsigned int divemaster : 1; + unsigned int buddy : 1; + unsigned int suit : 1; + unsigned int rating : 1; + unsigned int visibility : 1; + unsigned int tags : 1; + unsigned int cylinders : 1; + unsigned int weights : 1; +}; + +/* picture list and methods related to dive picture handling */ +struct picture { + char *filename; + char *hash; + offset_t offset; + degrees_t latitude; + degrees_t longitude; + struct picture *next; +}; + +#define FOR_EACH_PICTURE(_dive) \ + if (_dive) \ + for (struct picture *picture = (_dive)->picture_list; picture; picture = picture->next) + +#define FOR_EACH_PICTURE_NON_PTR(_divestruct) \ + for (struct picture *picture = (_divestruct).picture_list; picture; picture = picture->next) + +extern struct picture *alloc_picture(); +extern bool dive_check_picture_time(struct dive *d, int shift_time, timestamp_t timestamp); +extern void dive_create_picture(struct dive *d, char *filename, int shift_time, bool match_all); +extern void dive_add_picture(struct dive *d, struct picture *newpic); +extern void dive_remove_picture(char *filename); +extern unsigned int dive_get_picture_count(struct dive *d); +extern bool picture_check_valid(char *filename, int shift_time); +extern void picture_load_exif_data(struct picture *p); +extern timestamp_t picture_get_timestamp(char *filename); +extern void dive_set_geodata_from_picture(struct dive *d, struct picture *pic); + +extern int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc); +extern int get_depth_at_time(struct divecomputer *dc, int time); + +static inline int get_surface_pressure_in_mbar(const struct dive *dive, bool non_null) +{ + int mbar = dive->surface_pressure.mbar; + if (!mbar && non_null) + mbar = SURFACE_PRESSURE; + return mbar; +} + +/* Pa = N/m^2 - so we determine the weight (in N) of the mass of 10m + * of water (and use standard salt water at 1.03kg per liter if we don't know salinity) + * and add that to the surface pressure (or to 1013 if that's unknown) */ +static inline int calculate_depth_to_mbar(int depth, pressure_t surface_pressure, int salinity) +{ + double specific_weight; + int mbar = surface_pressure.mbar; + + if (!mbar) + mbar = SURFACE_PRESSURE; + if (!salinity) + salinity = SEAWATER_SALINITY; + specific_weight = salinity / 10000.0 * 0.981; + mbar += rint(depth / 10.0 * specific_weight); + return mbar; +} + +static inline int depth_to_mbar(int depth, struct dive *dive) +{ + return calculate_depth_to_mbar(depth, dive->surface_pressure, dive->salinity); +} + +static inline double depth_to_bar(int depth, struct dive *dive) +{ + return depth_to_mbar(depth, dive) / 1000.0; +} + +static inline double depth_to_atm(int depth, struct dive *dive) +{ + return mbar_to_atm(depth_to_mbar(depth, dive)); +} + +/* for the inverse calculation we use just the relative pressure + * (that's the one that some dive computers like the Uemis Zurich + * provide - for the other models that do this libdivecomputer has to + * take care of this, but the Uemis we support natively */ +static inline int rel_mbar_to_depth(int mbar, struct dive *dive) +{ + int cm; + double specific_weight = 1.03 * 0.981; + if (dive->dc.salinity) + specific_weight = dive->dc.salinity / 10000.0 * 0.981; + /* whole mbar gives us cm precision */ + cm = rint(mbar / specific_weight); + return cm * 10; +} + +static inline int mbar_to_depth(int mbar, struct dive *dive) +{ + pressure_t surface_pressure; + if (dive->surface_pressure.mbar) + surface_pressure = dive->surface_pressure; + else + surface_pressure.mbar = SURFACE_PRESSURE; + return rel_mbar_to_depth(mbar - surface_pressure.mbar, dive); +} + +/* MOD rounded to multiples of roundto mm */ +static inline depth_t gas_mod(struct gasmix *mix, pressure_t po2_limit, struct dive *dive, int roundto) { + depth_t rounded_depth; + + double depth = (double) mbar_to_depth(po2_limit.mbar * 1000 / get_o2(mix), dive); + rounded_depth.mm = rint(depth / roundto) * roundto; + return rounded_depth; +} + +#define SURFACE_THRESHOLD 750 /* somewhat arbitrary: only below 75cm is it really diving */ + +/* this is a global spot for a temporary dive structure that we use to + * be able to edit a dive without unintended side effects */ +extern struct dive edit_dive; + +extern short autogroup; +/* random threashold: three days without diving -> new trip + * this works very well for people who usually dive as part of a trip and don't + * regularly dive at a local facility; this is why trips are an optional feature */ +#define TRIP_THRESHOLD 3600 * 24 * 3 + +#define UNGROUPED_DIVE(_dive) ((_dive)->tripflag == NO_TRIP) +#define DIVE_IN_TRIP(_dive) ((_dive)->tripflag == IN_TRIP || (_dive)->tripflag == ASSIGNED_TRIP) +#define DIVE_NEEDS_TRIP(_dive) ((_dive)->tripflag == TF_NONE) + +extern void add_dive_to_trip(struct dive *, dive_trip_t *); + +extern void delete_single_dive(int idx); +extern void add_single_dive(int idx, struct dive *dive); + +extern void insert_trip(dive_trip_t **trip); + + +extern const struct units SI_units, IMPERIAL_units; +extern struct units xml_parsing_units; + +extern struct units *get_units(void); +extern int run_survey, verbose, quit; + +struct dive_table { + int nr, allocated, preexisting; + struct dive **dives; +}; + +extern struct dive_table dive_table, downloadTable; +extern struct dive displayed_dive; +extern struct dive_site displayed_dive_site; +extern int selected_dive; +extern unsigned int dc_number; +#define current_dive (get_dive(selected_dive)) +#define current_dc (get_dive_dc(current_dive, dc_number)) + +static inline struct dive *get_dive(int nr) +{ + if (nr >= dive_table.nr || nr < 0) + return NULL; + return dive_table.dives[nr]; +} + +static inline struct dive *get_dive_from_table(int nr, struct dive_table *dt) +{ + if (nr >= dt->nr || nr < 0) + return NULL; + return dt->dives[nr]; +} + +static inline struct dive_site *get_dive_site_for_dive(struct dive *dive) +{ + if (dive) + return get_dive_site_by_uuid(dive->dive_site_uuid); + return NULL; +} + +static inline char *get_dive_location(struct dive *dive) +{ + struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (ds && ds->name) + return ds->name; + return NULL; +} + +static inline unsigned int number_of_computers(struct dive *dive) +{ + unsigned int total_number = 0; + struct divecomputer *dc = &dive->dc; + + if (!dive) + return 1; + + do { + total_number++; + dc = dc->next; + } while (dc); + return total_number; +} + +static inline struct divecomputer *get_dive_dc(struct dive *dive, int nr) +{ + struct divecomputer *dc = &dive->dc; + + while (nr-- > 0) { + dc = dc->next; + if (!dc) + return &dive->dc; + } + return dc; +} + +extern timestamp_t dive_endtime(const struct dive *dive); + +extern void make_first_dc(void); +extern int count_divecomputers(void); +extern void delete_current_divecomputer(void); + +/* + * Iterate over each dive, with the first parameter being the index + * iterator variable, and the second one being the dive one. + * + * I don't think anybody really wants the index, and we could make + * it local to the for-loop, but that would make us requires C99. + */ +#define for_each_dive(_i, _x) \ + for ((_i) = 0; ((_x) = get_dive(_i)) != NULL; (_i)++) + +#define for_each_dc(_dive, _dc) \ + for (_dc = &_dive->dc; _dc; _dc = _dc->next) + +#define for_each_gps_location(_i, _x) \ + for ((_i) = 0; ((_x) = get_gps_location(_i, &gps_location_table)) != NULL; (_i)++) + +static inline struct dive *get_dive_by_uniq_id(int id) +{ + int i; + struct dive *dive = NULL; + + for_each_dive (i, dive) { + if (dive->id == id) + break; + } +#ifdef DEBUG + if (dive == NULL) { + fprintf(stderr, "Invalid id %x passed to get_dive_by_diveid, try to fix the code\n", id); + exit(1); + } +#endif + return dive; +} + +static inline int get_idx_by_uniq_id(int id) +{ + int i; + struct dive *dive = NULL; + + for_each_dive (i, dive) { + if (dive->id == id) + break; + } +#ifdef DEBUG + if (dive == NULL) { + fprintf(stderr, "Invalid id %x passed to get_dive_by_diveid, try to fix the code\n", id); + exit(1); + } +#endif + return i; +} + +static inline bool dive_site_has_gps_location(struct dive_site *ds) +{ + return ds && (ds->latitude.udeg || ds->longitude.udeg); +} + +static inline int dive_has_gps_location(struct dive *dive) +{ + if (!dive) + return false; + return dive_site_has_gps_location(get_dive_site_by_uuid(dive->dive_site_uuid)); +} + +#ifdef __cplusplus +extern "C" { +#endif + +extern int report_error(const char *fmt, ...); +extern const char *get_error_string(void); + +extern struct dive *find_dive_including(timestamp_t when); +extern bool dive_within_time_range(struct dive *dive, timestamp_t when, timestamp_t offset); +extern bool time_during_dive_with_offset(struct dive *dive, timestamp_t when, timestamp_t offset); +struct dive *find_dive_n_near(timestamp_t when, int n, timestamp_t offset); + +/* Check if two dive computer entries are the exact same dive (-1=no/0=maybe/1=yes) */ +extern int match_one_dc(struct divecomputer *a, struct divecomputer *b); + +extern void parse_xml_init(void); +extern int parse_xml_buffer(const char *url, const char *buf, int size, struct dive_table *table, const char **params); +extern void parse_xml_exit(void); +extern void set_filename(const char *filename, bool force); + +extern int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buf, int size, struct dive_table *table); +extern int parse_dlf_buffer(unsigned char *buffer, size_t size); + +extern int parse_file(const char *filename); +extern int parse_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate); +extern int parse_seabear_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate); +extern int parse_txt_file(const char *filename, const char *csv); +extern int parse_manual_file(const char *filename, char **params, int pnr); +extern int save_dives(const char *filename); +extern int save_dives_logic(const char *filename, bool select_only); +extern int save_dive(FILE *f, struct dive *dive); +extern int export_dives_xslt(const char *filename, const bool selected, const int units, const char *export_xslt); + +struct membuffer; +extern void save_one_dive_to_mb(struct membuffer *b, struct dive *dive); + +int cylinderuse_from_text(const char *text); + + +struct user_info { + const char *name; + const char *email; +}; + +extern void subsurface_user_info(struct user_info *); +extern int subsurface_rename(const char *path, const char *newpath); +extern int subsurface_dir_rename(const char *path, const char *newpath); +extern int subsurface_open(const char *path, int oflags, mode_t mode); +extern FILE *subsurface_fopen(const char *path, const char *mode); +extern void *subsurface_opendir(const char *path); +extern int subsurface_access(const char *path, int mode); +extern struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp); +extern int subsurface_zip_close(struct zip *zip); +extern void subsurface_console_init(bool dedicated); +extern void subsurface_console_exit(void); + +extern void shift_times(const timestamp_t amount); +extern timestamp_t get_times(); + +extern xsltStylesheetPtr get_stylesheet(const char *name); + +extern timestamp_t utc_mktime(struct tm *tm); +extern void utc_mkdate(timestamp_t, struct tm *tm); + +extern struct dive *alloc_dive(void); +extern void record_dive_to_table(struct dive *dive, struct dive_table *table); +extern void record_dive(struct dive *dive); +extern void clear_dive(struct dive *dive); +extern void copy_dive(struct dive *s, struct dive *d); +extern void selective_copy_dive(struct dive *s, struct dive *d, struct dive_components what, bool clear); +extern struct dive *clone_dive(struct dive *s); + +extern void clear_table(struct dive_table *table); + +extern struct sample *prepare_sample(struct divecomputer *dc); +extern void finish_sample(struct divecomputer *dc); + +extern bool has_hr_data(struct divecomputer *dc); + +extern void sort_table(struct dive_table *table); +extern struct dive *fixup_dive(struct dive *dive); +extern void fixup_dc_duration(struct divecomputer *dc); +extern int dive_getUniqID(struct dive *d); +extern unsigned int dc_airtemp(struct divecomputer *dc); +extern unsigned int dc_watertemp(struct divecomputer *dc); +extern int split_dive(struct dive *); +extern struct dive *merge_dives(struct dive *a, struct dive *b, int offset, bool prefer_downloaded); +extern struct dive *try_to_merge(struct dive *a, struct dive *b, bool prefer_downloaded); +extern void renumber_dives(int start_nr, bool selected_only); +extern void copy_events(struct divecomputer *s, struct divecomputer *d); +extern void free_events(struct event *ev); +extern void copy_cylinders(struct dive *s, struct dive *d, bool used_only); +extern void copy_samples(struct divecomputer *s, struct divecomputer *d); +extern bool is_cylinder_used(struct dive *dive, int idx); +extern void fill_default_cylinder(cylinder_t *cyl); +extern void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int time, int idx); +extern struct event *add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name); +extern void remove_event(struct event *event); +extern void update_event_name(struct dive *d, struct event* event, char *name); +extern void add_extra_data(struct divecomputer *dc, const char *key, const char *value); +extern void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration); +extern int get_cylinder_index(struct dive *dive, struct event *ev); +extern int nr_cylinders(struct dive *dive); +extern int nr_weightsystems(struct dive *dive); + +/* UI related protopypes */ + +// extern void report_error(GError* error); + +extern void add_cylinder_description(cylinder_type_t *); +extern void add_weightsystem_description(weightsystem_t *); +extern void remember_event(const char *eventname); + +#if WE_DONT_USE_THIS /* this is a missing feature in Qt - selecting which events to display */ +extern int evn_foreach(void (*callback)(const char *, bool *, void *), void *data); +#endif /* WE_DONT_USE_THIS */ + +extern void clear_events(void); + +extern void set_dc_nickname(struct dive *dive); +extern void set_autogroup(bool value); +extern int total_weight(struct dive *); + +#ifdef __cplusplus +} +#endif + +#define DIVE_ERROR_PARSE 1 +#define DIVE_ERROR_PLAN 2 + +const char *weekday(int wday); +const char *monthname(int mon); + +#define UTF8_DEGREE "\xc2\xb0" +#define UTF8_DELTA "\xce\x94" +#define UTF8_UPWARDS_ARROW "\xE2\x86\x91" +#define UTF8_DOWNWARDS_ARROW "\xE2\x86\x93" +#define UTF8_AVERAGE "\xc3\xb8" +#define UTF8_SUBSCRIPT_2 "\xe2\x82\x82" +#define UTF8_WHITESTAR "\xe2\x98\x86" +#define UTF8_BLACKSTAR "\xe2\x98\x85" + +extern const char *existing_filename; +extern void subsurface_command_line_init(int *, char ***); +extern void subsurface_command_line_exit(int *, char ***); + +#define FRACTION(n, x) ((unsigned)(n) / (x)), ((unsigned)(n) % (x)) + +extern void add_segment(double pressure, const struct gasmix *gasmix, int period_in_seconds, int setpoint, const struct dive *dive, int sac); +extern void clear_deco(double surface_pressure); +extern void dump_tissues(void); +extern unsigned int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth); +extern void set_gf(short gflow, short gfhigh, bool gf_low_at_maxdepth); +extern void cache_deco_state(char **datap); +extern void restore_deco_state(char *data); +extern void nuclear_regeneration(double time); +extern void vpmb_start_gradient(); +extern void vpmb_next_gradient(double deco_time, double surface_pressure); +extern double tissue_tolerance_calc(const struct dive *dive, double pressure); + +/* this should be converted to use our types */ +struct divedatapoint { + int time; + unsigned int depth; + struct gasmix gasmix; + int setpoint; + bool entered; + struct divedatapoint *next; +}; + +struct diveplan { + timestamp_t when; + int surface_pressure; /* mbar */ + int bottomsac; /* ml/min */ + int decosac; /* ml/min */ + int salinity; + short gflow; + short gfhigh; + struct divedatapoint *dp; +}; + +struct divedatapoint *plan_add_segment(struct diveplan *diveplan, int duration, int depth, struct gasmix gasmix, int po2, bool entered); +struct divedatapoint *create_dp(int time_incr, int depth, struct gasmix gasmix, int po2); +#if DEBUG_PLAN +void dump_plan(struct diveplan *diveplan); +#endif +bool plan(struct diveplan *diveplan, char **cached_datap, bool is_planner, bool show_disclaimer); +void calc_crushing_pressure(double pressure); +void vpmb_start_gradient(); + +void delete_single_dive(int idx); + +struct event *get_next_event(struct event *event, const char *name); + + +/* these structs holds the information that + * describes the cylinders / weight systems. + * they are global variables initialized in equipment.c + * used to fill the combobox in the add/edit cylinder + * dialog + */ + +struct tank_info_t { + const char *name; + int cuft, ml, psi, bar; +}; +extern struct tank_info_t tank_info[100]; + +struct ws_info_t { + const char *name; + int grams; +}; +extern struct ws_info_t ws_info[100]; + +extern bool cylinder_nodata(cylinder_t *cyl); +extern bool cylinder_none(void *_data); +extern bool weightsystem_none(void *_data); +extern bool no_weightsystems(weightsystem_t *ws); +extern bool weightsystems_equal(weightsystem_t *ws1, weightsystem_t *ws2); +extern void remove_cylinder(struct dive *dive, int idx); +extern void remove_weightsystem(struct dive *dive, int idx); +extern void reset_cylinders(struct dive *dive, bool track_gas); + +/* + * String handling. + */ +#define STRTOD_NO_SIGN 0x01 +#define STRTOD_NO_DOT 0x02 +#define STRTOD_NO_COMMA 0x04 +#define STRTOD_NO_EXPONENT 0x08 +extern double strtod_flags(const char *str, const char **ptr, unsigned int flags); + +#define STRTOD_ASCII (STRTOD_NO_COMMA) + +#define ascii_strtod(str, ptr) strtod_flags(str, ptr, STRTOD_ASCII) + +extern void set_save_userid_local(short value); +extern void set_userid(char *user_id); +extern void set_informational_units(char *units); + +extern const char *get_dive_date_c_string(timestamp_t when); +extern void update_setpoint_events(struct divecomputer *dc); +#ifdef __cplusplus +} +#endif + +extern weight_t string_to_weight(const char *str); +extern depth_t string_to_depth(const char *str); +extern pressure_t string_to_pressure(const char *str); +extern volume_t string_to_volume(const char *str, pressure_t workp); +extern fraction_t string_to_fraction(const char *str); +extern void average_max_depth(struct diveplan *dive, int *avg_depth, int *max_depth); + +#include "pref.h" + +#endif // DIVE_H diff --git a/subsurface-core/divecomputer.cpp b/subsurface-core/divecomputer.cpp new file mode 100644 index 000000000..e4081e1cd --- /dev/null +++ b/subsurface-core/divecomputer.cpp @@ -0,0 +1,228 @@ +#include "divecomputer.h" +#include "dive.h" + +#include + +const char *default_dive_computer_vendor; +const char *default_dive_computer_product; +const char *default_dive_computer_device; +int default_dive_computer_download_mode; +DiveComputerList dcList; + +DiveComputerList::DiveComputerList() +{ +} + +DiveComputerList::~DiveComputerList() +{ +} + +bool DiveComputerNode::operator==(const DiveComputerNode &a) const +{ + return model == a.model && + deviceId == a.deviceId && + firmware == a.firmware && + serialNumber == a.serialNumber && + nickName == a.nickName; +} + +bool DiveComputerNode::operator!=(const DiveComputerNode &a) const +{ + return !(*this == a); +} + +bool DiveComputerNode::changesValues(const DiveComputerNode &b) const +{ + if (model != b.model || deviceId != b.deviceId) { + qDebug("DiveComputerNodes were not for the same DC"); + return false; + } + return (firmware != b.firmware) || + (serialNumber != b.serialNumber) || + (nickName != b.nickName); +} + +const DiveComputerNode *DiveComputerList::getExact(const QString &m, uint32_t d) +{ + for (QMap::iterator it = dcMap.find(m); it != dcMap.end() && it.key() == m; ++it) + if (it->deviceId == d) + return &*it; + return NULL; +} + +const DiveComputerNode *DiveComputerList::get(const QString &m) +{ + QMap::iterator it = dcMap.find(m); + if (it != dcMap.end()) + return &*it; + return NULL; +} + +void DiveComputerNode::showchanges(const QString &n, const QString &s, const QString &f) const +{ + if (nickName != n) + qDebug("new nickname %s for DC model %s deviceId 0x%x", n.toUtf8().data(), model.toUtf8().data(), deviceId); + if (serialNumber != s) + qDebug("new serial number %s for DC model %s deviceId 0x%x", s.toUtf8().data(), model.toUtf8().data(), deviceId); + if (firmware != f) + qDebug("new firmware version %s for DC model %s deviceId 0x%x", f.toUtf8().data(), model.toUtf8().data(), deviceId); +} + +void DiveComputerList::addDC(QString m, uint32_t d, QString n, QString s, QString f) +{ + if (m.isEmpty() || d == 0) + return; + const DiveComputerNode *existNode = this->getExact(m, d); + + if (existNode) { + // Update any non-existent fields from the old entry + if (n.isEmpty()) + n = existNode->nickName; + if (s.isEmpty()) + s = existNode->serialNumber; + if (f.isEmpty()) + f = existNode->firmware; + + // Do all the old values match? + if (n == existNode->nickName && s == existNode->serialNumber && f == existNode->firmware) + return; + + // debugging: show changes + if (verbose) + existNode->showchanges(n, s, f); + dcMap.remove(m, *existNode); + } + + DiveComputerNode newNode(m, d, s, f, n); + dcMap.insert(m, newNode); +} + +extern "C" void create_device_node(const char *model, uint32_t deviceid, const char *serial, const char *firmware, const char *nickname) +{ + dcList.addDC(model, deviceid, nickname, serial, firmware); +} + +extern "C" bool compareDC(const DiveComputerNode &a, const DiveComputerNode &b) +{ + return a.deviceId < b.deviceId; +} + +extern "C" void call_for_each_dc (void *f, void (*callback)(void *, const char *, uint32_t, + const char *, const char *, const char *), + bool select_only) +{ + QList values = dcList.dcMap.values(); + qSort(values.begin(), values.end(), compareDC); + for (int i = 0; i < values.size(); i++) { + const DiveComputerNode *node = &values.at(i); + bool found = false; + if (select_only) { + int j; + struct dive *d; + for_each_dive (j, d) { + struct divecomputer *dc; + if (!d->selected) + continue; + for_each_dc(d, dc) { + if (dc->deviceid == node->deviceId) { + found = true; + break; + } + } + if (found) + break; + } + } else { + found = true; + } + if (found) + callback(f, node->model.toUtf8().data(), node->deviceId, node->nickName.toUtf8().data(), + node->serialNumber.toUtf8().data(), node->firmware.toUtf8().data()); + } +} + + +extern "C" int is_default_dive_computer(const char *vendor, const char *product) +{ + return default_dive_computer_vendor && !strcmp(vendor, default_dive_computer_vendor) && + default_dive_computer_product && !strcmp(product, default_dive_computer_product); +} + +extern "C" int is_default_dive_computer_device(const char *name) +{ + return default_dive_computer_device && !strcmp(name, default_dive_computer_device); +} + +void set_default_dive_computer(const char *vendor, const char *product) +{ + QSettings s; + + if (!vendor || !*vendor) + return; + if (!product || !*product) + return; + if (is_default_dive_computer(vendor, product)) + return; + + free((void *)default_dive_computer_vendor); + free((void *)default_dive_computer_product); + default_dive_computer_vendor = strdup(vendor); + default_dive_computer_product = strdup(product); + s.beginGroup("DiveComputer"); + s.setValue("dive_computer_vendor", vendor); + s.setValue("dive_computer_product", product); + s.endGroup(); +} + +void set_default_dive_computer_device(const char *name) +{ + QSettings s; + + if (!name || !*name) + return; + if (is_default_dive_computer_device(name)) + return; + + free((void *)default_dive_computer_device); + default_dive_computer_device = strdup(name); + s.beginGroup("DiveComputer"); + s.setValue("dive_computer_device", name); + s.endGroup(); +} + +void set_default_dive_computer_download_mode(int download_mode) +{ + QSettings s; + + default_dive_computer_download_mode = download_mode; + s.beginGroup("DiveComputer"); + s.setValue("dive_computer_download_mode", download_mode); + s.endGroup(); +} + +extern "C" void set_dc_nickname(struct dive *dive) +{ + if (!dive) + return; + + struct divecomputer *dc; + + for_each_dc (dive, dc) { + if (dc->model && *dc->model && dc->deviceid && + !dcList.getExact(dc->model, dc->deviceid)) { + // we don't have this one, yet + const DiveComputerNode *existNode = dcList.get(dc->model); + if (existNode) { + // we already have this model but a different deviceid + QString simpleNick(dc->model); + if (dc->deviceid == 0) + simpleNick.append(" (unknown deviceid)"); + else + simpleNick.append(" (").append(QString::number(dc->deviceid, 16)).append(")"); + dcList.addDC(dc->model, dc->deviceid, simpleNick); + } else { + dcList.addDC(dc->model, dc->deviceid); + } + } + } +} diff --git a/subsurface-core/divecomputer.h b/subsurface-core/divecomputer.h new file mode 100644 index 000000000..98d12ab8b --- /dev/null +++ b/subsurface-core/divecomputer.h @@ -0,0 +1,38 @@ +#ifndef DIVECOMPUTER_H +#define DIVECOMPUTER_H + +#include +#include +#include + +class DiveComputerNode { +public: + DiveComputerNode(QString m, uint32_t d, QString s, QString f, QString n) + : model(m), deviceId(d), serialNumber(s), firmware(f), nickName(n) {}; + bool operator==(const DiveComputerNode &a) const; + bool operator!=(const DiveComputerNode &a) const; + bool changesValues(const DiveComputerNode &b) const; + void showchanges(const QString &n, const QString &s, const QString &f) const; + QString model; + uint32_t deviceId; + QString serialNumber; + QString firmware; + QString nickName; +}; + +class DiveComputerList { +public: + DiveComputerList(); + ~DiveComputerList(); + const DiveComputerNode *getExact(const QString &m, uint32_t d); + const DiveComputerNode *get(const QString &m); + void addDC(QString m, uint32_t d, QString n = QString(), QString s = QString(), QString f = QString()); + DiveComputerNode matchDC(const QString &m, uint32_t d); + DiveComputerNode matchModel(const QString &m); + QMultiMap dcMap; + QMultiMap dcWorkingMap; +}; + +extern DiveComputerList dcList; + +#endif diff --git a/subsurface-core/divelist.c b/subsurface-core/divelist.c new file mode 100644 index 000000000..a14fabf88 --- /dev/null +++ b/subsurface-core/divelist.c @@ -0,0 +1,1141 @@ +/* divelist.c */ +/* core logic for the dive list - + * accessed through the following interfaces: + * + * dive_trip_t *dive_trip_list; + * unsigned int amount_selected; + * void dump_selection(void) + * dive_trip_t *find_trip_by_idx(int idx) + * int trip_has_selected_dives(dive_trip_t *trip) + * void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p) + * int total_weight(struct dive *dive) + * int get_divenr(struct dive *dive) + * double init_decompression(struct dive *dive) + * void update_cylinder_related_info(struct dive *dive) + * void dump_trip_list(void) + * dive_trip_t *find_matching_trip(timestamp_t when) + * void insert_trip(dive_trip_t **dive_trip_p) + * void remove_dive_from_trip(struct dive *dive) + * void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) + * dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) + * void autogroup_dives(void) + * void delete_single_dive(int idx) + * void add_single_dive(int idx, struct dive *dive) + * void merge_two_dives(struct dive *a, struct dive *b) + * void select_dive(int idx) + * void deselect_dive(int idx) + * void mark_divelist_changed(int changed) + * int unsaved_changes() + * void remove_autogen_trips() + */ +#include +#include +#include +#include +#include +#include +#include "gettext.h" +#include +#include +#include + +#include "dive.h" +#include "divelist.h" +#include "display.h" +#include "planner.h" +#include "qthelperfromc.h" + +static short dive_list_changed = false; + +short autogroup = false; + +dive_trip_t *dive_trip_list; + +unsigned int amount_selected; + +#if DEBUG_SELECTION_TRACKING +void dump_selection(void) +{ + int i; + struct dive *dive; + + printf("currently selected are %u dives:", amount_selected); + for_each_dive(i, dive) { + if (dive->selected) + printf(" %d", i); + } + printf("\n"); +} +#endif + +void set_autogroup(bool value) +{ + /* if we keep the UI paradigm, this needs to toggle + * the checkbox on the autogroup menu item */ + autogroup = value; +} + +dive_trip_t *find_trip_by_idx(int idx) +{ + dive_trip_t *trip = dive_trip_list; + + if (idx >= 0) + return NULL; + idx = -idx; + while (trip) { + if (trip->index == idx) + return trip; + trip = trip->next; + } + return NULL; +} + +int trip_has_selected_dives(dive_trip_t *trip) +{ + struct dive *dive; + for (dive = trip->dives; dive; dive = dive->next) { + if (dive->selected) + return 1; + } + return 0; +} + +/* + * Get "maximal" dive gas for a dive. + * Rules: + * - Trimix trumps nitrox (highest He wins, O2 breaks ties) + * - Nitrox trumps air (even if hypoxic) + * These are the same rules as the inter-dive sorting rules. + */ +void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2max_p) +{ + int i; + int maxo2 = -1, maxhe = -1, mino2 = 1000; + + + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + int o2 = get_o2(&cyl->gasmix); + int he = get_he(&cyl->gasmix); + + if (!is_cylinder_used(dive, i)) + continue; + if (cylinder_none(cyl)) + continue; + if (o2 > maxo2) + maxo2 = o2; + if (he > maxhe) + goto newmax; + if (he < maxhe) + continue; + if (o2 <= maxo2) + continue; + newmax: + maxhe = he; + mino2 = o2; + } + /* All air? Show/sort as "air"/zero */ + if ((!maxhe && maxo2 == O2_IN_AIR && mino2 == maxo2) || + (maxo2 == -1 && maxhe == -1 && mino2 == 1000)) + maxo2 = mino2 = 0; + *o2_p = mino2; + *he_p = maxhe; + *o2max_p = maxo2; +} + +int total_weight(struct dive *dive) +{ + int i, total_grams = 0; + + if (dive) + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + total_grams += dive->weightsystem[i].weight.grams; + return total_grams; +} + +static int active_o2(struct dive *dive, struct divecomputer *dc, duration_t time) +{ + struct gasmix gas; + get_gas_at_time(dive, dc, time, &gas); + return get_o2(&gas); +} + +/* calculate OTU for a dive - this only takes the first divecomputer into account */ +static int calculate_otu(struct dive *dive) +{ + int i; + double otu = 0.0; + struct divecomputer *dc = &dive->dc; + + for (i = 1; i < dc->samples; i++) { + int t; + int po2; + struct sample *sample = dc->sample + i; + struct sample *psample = sample - 1; + t = sample->time.seconds - psample->time.seconds; + if (sample->setpoint.mbar) { + po2 = sample->setpoint.mbar; + } else { + int o2 = active_o2(dive, dc, sample->time); + po2 = o2 * depth_to_atm(sample->depth.mm, dive); + } + if (po2 >= 500) + otu += pow((po2 - 500) / 1000.0, 0.83) * t / 30.0; + } + return rint(otu); +} +/* calculate CNS for a dive - this only takes the first divecomputer into account */ +int const cns_table[][3] = { + /* po2, Maximum Single Exposure, Maximum 24 hour Exposure */ + { 1600, 45 * 60, 150 * 60 }, + { 1500, 120 * 60, 180 * 60 }, + { 1400, 150 * 60, 180 * 60 }, + { 1300, 180 * 60, 210 * 60 }, + { 1200, 210 * 60, 240 * 60 }, + { 1100, 240 * 60, 270 * 60 }, + { 1000, 300 * 60, 300 * 60 }, + { 900, 360 * 60, 360 * 60 }, + { 800, 450 * 60, 450 * 60 }, + { 700, 570 * 60, 570 * 60 }, + { 600, 720 * 60, 720 * 60 } +}; + +/* this only gets called if dive->maxcns == 0 which means we know that + * none of the divecomputers has tracked any CNS for us + * so we calculated it "by hand" */ +static int calculate_cns(struct dive *dive) +{ + int i, j, divenr; + double cns = 0.0; + struct divecomputer *dc = &dive->dc; + struct dive *prev_dive; + timestamp_t endtime; + + /* shortcut */ + if (dive->cns) + return dive->cns; + /* + * Do we start with a cns loading from a previous dive? + * Check if we did a dive 12 hours prior, and what cns we had from that. + * Then apply ha 90min halftime to see whats left. + */ + divenr = get_divenr(dive); + if (divenr) { + prev_dive = get_dive(divenr - 1); + if (prev_dive) { + endtime = prev_dive->when + prev_dive->duration.seconds; + if (dive->when < (endtime + 3600 * 12)) { + cns = calculate_cns(prev_dive); + cns = cns * 1 / pow(2, (dive->when - endtime) / (90.0 * 60.0)); + } + } + } + /* Caclulate the cns for each sample in this dive and sum them */ + for (i = 1; i < dc->samples; i++) { + int t; + int po2; + struct sample *sample = dc->sample + i; + struct sample *psample = sample - 1; + t = sample->time.seconds - psample->time.seconds; + if (sample->setpoint.mbar) { + po2 = sample->setpoint.mbar; + } else { + int o2 = active_o2(dive, dc, sample->time); + po2 = o2 * depth_to_atm(sample->depth.mm, dive); + } + /* CNS don't increse when below 500 matm */ + if (po2 < 500) + continue; + /* Find what table-row we should calculate % for */ + for (j = 1; j < sizeof(cns_table) / (sizeof(int) * 3); j++) + if (po2 > cns_table[j][0]) + break; + j--; + cns += ((double)t) / ((double)cns_table[j][1]) * 100; + } + /* save calculated cns in dive struct */ + dive->cns = cns; + return dive->cns; +} +/* + * Return air usage (in liters). + */ +static double calculate_airuse(struct dive *dive) +{ + int airuse = 0; + int i; + + for (i = 0; i < MAX_CYLINDERS; i++) { + pressure_t start, end; + cylinder_t *cyl = dive->cylinder + i; + + start = cyl->start.mbar ? cyl->start : cyl->sample_start; + end = cyl->end.mbar ? cyl->end : cyl->sample_end; + if (!end.mbar || start.mbar <= end.mbar) + continue; + + airuse += gas_volume(cyl, start) - gas_volume(cyl, end); + } + return airuse / 1000.0; +} + +/* this only uses the first divecomputer to calculate the SAC rate */ +static int calculate_sac(struct dive *dive) +{ + struct divecomputer *dc = &dive->dc; + double airuse, pressure, sac; + int duration, meandepth; + + airuse = calculate_airuse(dive); + if (!airuse) + return 0; + + duration = dc->duration.seconds; + if (!duration) + return 0; + + meandepth = dc->meandepth.mm; + if (!meandepth) + return 0; + + /* Mean pressure in ATM (SAC calculations are in atm*l/min) */ + pressure = depth_to_atm(meandepth, dive); + sac = airuse / pressure * 60 / duration; + + /* milliliters per minute.. */ + return sac * 1000; +} + +/* for now we do this based on the first divecomputer */ +static void add_dive_to_deco(struct dive *dive) +{ + struct divecomputer *dc = &dive->dc; + int i; + + if (!dc) + return; + for (i = 1; i < dc->samples; i++) { + struct sample *psample = dc->sample + i - 1; + struct sample *sample = dc->sample + i; + int t0 = psample->time.seconds; + int t1 = sample->time.seconds; + int j; + + for (j = t0; j < t1; j++) { + int depth = interpolate(psample->depth.mm, sample->depth.mm, j - t0, t1 - t0); + add_segment(depth_to_bar(depth, dive), + &dive->cylinder[sample->sensor].gasmix, 1, sample->setpoint.mbar, dive, dive->sac); + } + } +} + +int get_divenr(struct dive *dive) +{ + int i; + struct dive *d; + // tempting as it may be, don't die when called with dive=NULL + if (dive) + for_each_dive(i, d) { + if (d->id == dive->id) // don't compare pointers, we could be passing in a copy of the dive + return i; + } + return -1; +} + +int get_divesite_idx(struct dive_site *ds) +{ + int i; + struct dive_site *d; + // tempting as it may be, don't die when called with dive=NULL + if (ds) + for_each_dive_site(i, d) { + if (d->uuid == ds->uuid) // don't compare pointers, we could be passing in a copy of the dive + return i; + } + return -1; +} + +static struct gasmix air = { .o2.permille = O2_IN_AIR, .he.permille = 0 }; + +/* take into account previous dives until there is a 48h gap between dives */ +double init_decompression(struct dive *dive) +{ + int i, divenr = -1; + unsigned int surface_time; + timestamp_t when, lasttime = 0, laststart = 0; + bool deco_init = false; + double surface_pressure; + + if (!dive) + return 0.0; + + surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; + divenr = get_divenr(dive); + when = dive->when; + i = divenr; + if (i < 0) { + i = dive_table.nr - 1; + while (i >= 0 && get_dive(i)->when > when) + --i; + i++; + } + while (i--) { + struct dive *pdive = get_dive(i); + /* we don't want to mix dives from different trips as we keep looking + * for how far back we need to go */ + if (dive->divetrip && pdive->divetrip != dive->divetrip) + continue; + if (!pdive || pdive->when >= when || pdive->when + pdive->duration.seconds + 48 * 60 * 60 < when) + break; + /* For simultaneous dives, only consider the first */ + if (pdive->when == laststart) + continue; + when = pdive->when; + lasttime = when + pdive->duration.seconds; + } + while (++i < (divenr >= 0 ? divenr : dive_table.nr)) { + struct dive *pdive = get_dive(i); + /* again skip dives from different trips */ + if (dive->divetrip && dive->divetrip != pdive->divetrip) + continue; + surface_pressure = get_surface_pressure_in_mbar(pdive, true) / 1000.0; + if (!deco_init) { + clear_deco(surface_pressure); + deco_init = true; +#if DECO_CALC_DEBUG & 2 + dump_tissues(); +#endif + } + add_dive_to_deco(pdive); + laststart = pdive->when; +#if DECO_CALC_DEBUG & 2 + printf("added dive #%d\n", pdive->number); + dump_tissues(); +#endif + if (pdive->when > lasttime) { + surface_time = pdive->when - lasttime; + lasttime = pdive->when + pdive->duration.seconds; + add_segment(surface_pressure, &air, surface_time, 0, dive, prefs.decosac); +#if DECO_CALC_DEBUG & 2 + printf("after surface intervall of %d:%02u\n", FRACTION(surface_time, 60)); + dump_tissues(); +#endif + } + } + /* add the final surface time */ + if (lasttime && dive->when > lasttime) { + surface_time = dive->when - lasttime; + surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; + add_segment(surface_pressure, &air, surface_time, 0, dive, prefs.decosac); +#if DECO_CALC_DEBUG & 2 + printf("after surface intervall of %d:%02u\n", FRACTION(surface_time, 60)); + dump_tissues(); +#endif + } + if (!deco_init) { + surface_pressure = get_surface_pressure_in_mbar(dive, true) / 1000.0; + clear_deco(surface_pressure); +#if DECO_CALC_DEBUG & 2 + printf("no previous dive\n"); + dump_tissues(); +#endif + } + return tissue_tolerance_calc(dive, surface_pressure); +} + +void update_cylinder_related_info(struct dive *dive) +{ + if (dive != NULL) { + dive->sac = calculate_sac(dive); + dive->otu = calculate_otu(dive); + if (dive->maxcns == 0) + dive->maxcns = calculate_cns(dive); + } +} + +#define MAX_GAS_STRING 80 +#define UTF8_ELLIPSIS "\xE2\x80\xA6" + +/* callers needs to free the string */ +char *get_dive_gas_string(struct dive *dive) +{ + int o2, he, o2max; + char *buffer = malloc(MAX_GAS_STRING); + + if (buffer) { + get_dive_gas(dive, &o2, &he, &o2max); + o2 = (o2 + 5) / 10; + he = (he + 5) / 10; + o2max = (o2max + 5) / 10; + + if (he) + if (o2 == o2max) + snprintf(buffer, MAX_GAS_STRING, "%d/%d", o2, he); + else + snprintf(buffer, MAX_GAS_STRING, "%d/%d" UTF8_ELLIPSIS "%d%%", o2, he, o2max); + else if (o2) + if (o2 == o2max) + snprintf(buffer, MAX_GAS_STRING, "%d%%", o2); + else + snprintf(buffer, MAX_GAS_STRING, "%d" UTF8_ELLIPSIS "%d%%", o2, o2max); + else + strcpy(buffer, translate("gettextFromC", "air")); + } + return buffer; +} + +/* + * helper functions for dive_trip handling + */ +#ifdef DEBUG_TRIP +void dump_trip_list(void) +{ + dive_trip_t *trip; + int i = 0; + timestamp_t last_time = 0; + + for (trip = dive_trip_list; trip; trip = trip->next) { + struct tm tm; + utc_mkdate(trip->when, &tm); + if (trip->when < last_time) + printf("\n\ndive_trip_list OUT OF ORDER!!!\n\n\n"); + printf("%s trip %d to \"%s\" on %04u-%02u-%02u %02u:%02u:%02u (%d dives - %p)\n", + trip->autogen ? "autogen " : "", + ++i, trip->location, + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, + trip->nrdives, trip); + last_time = trip->when; + } + printf("-----\n"); +} +#endif + +/* this finds the last trip that at or before the time given */ +dive_trip_t *find_matching_trip(timestamp_t when) +{ + dive_trip_t *trip = dive_trip_list; + + if (!trip || trip->when > when) { +#ifdef DEBUG_TRIP + printf("no matching trip\n"); +#endif + return NULL; + } + while (trip->next && trip->next->when <= when) + trip = trip->next; +#ifdef DEBUG_TRIP + { + struct tm tm; + utc_mkdate(trip->when, &tm); + printf("found trip %p @ %04d-%02d-%02d %02d:%02d:%02d\n", + trip, + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); + } +#endif + return trip; +} + +/* insert the trip into the dive_trip_list - but ensure you don't have + * two trips for the same date; but if you have, make sure you don't + * keep the one with less information */ +void insert_trip(dive_trip_t **dive_trip_p) +{ + dive_trip_t *dive_trip = *dive_trip_p; + dive_trip_t **p = &dive_trip_list; + dive_trip_t *trip; + struct dive *divep; + + /* Walk the dive trip list looking for the right location.. */ + while ((trip = *p) != NULL && trip->when < dive_trip->when) + p = &trip->next; + + if (trip && trip->when == dive_trip->when) { + if (!trip->location) + trip->location = dive_trip->location; + if (!trip->notes) + trip->notes = dive_trip->notes; + divep = dive_trip->dives; + while (divep) { + add_dive_to_trip(divep, trip); + divep = divep->next; + } + *dive_trip_p = trip; + } else { + dive_trip->next = trip; + *p = dive_trip; + } +#ifdef DEBUG_TRIP + dump_trip_list(); +#endif +} + +static void delete_trip(dive_trip_t *trip) +{ + dive_trip_t **p, *tmp; + + assert(!trip->dives); + + /* Remove the trip from the list of trips */ + p = &dive_trip_list; + while ((tmp = *p) != NULL) { + if (tmp == trip) { + *p = trip->next; + break; + } + p = &tmp->next; + } + + /* .. and free it */ + free(trip->location); + free(trip->notes); + free(trip); +} + +void find_new_trip_start_time(dive_trip_t *trip) +{ + struct dive *dive = trip->dives; + timestamp_t when = dive->when; + + while ((dive = dive->next) != NULL) { + if (dive->when < when) + when = dive->when; + } + trip->when = when; +} + +/* check if we have a trip right before / after this dive */ +bool is_trip_before_after(struct dive *dive, bool before) +{ + int idx = get_idx_by_uniq_id(dive->id); + if (before) { + if (idx > 0 && get_dive(idx - 1)->divetrip) + return true; + } else { + if (idx < dive_table.nr - 1 && get_dive(idx + 1)->divetrip) + return true; + } + return false; +} + +struct dive *first_selected_dive() +{ + int idx; + struct dive *d; + + for_each_dive (idx, d) { + if (d->selected) + return d; + } + return NULL; +} + +struct dive *last_selected_dive() +{ + int idx; + struct dive *d, *ret = NULL; + + for_each_dive (idx, d) { + if (d->selected) + ret = d; + } + return ret; +} + +void remove_dive_from_trip(struct dive *dive, short was_autogen) +{ + struct dive *next, **pprev; + dive_trip_t *trip = dive->divetrip; + + if (!trip) + return; + + /* Remove the dive from the trip's list of dives */ + next = dive->next; + pprev = dive->pprev; + *pprev = next; + if (next) + next->pprev = pprev; + + dive->divetrip = NULL; + if (was_autogen) + dive->tripflag = TF_NONE; + else + dive->tripflag = NO_TRIP; + assert(trip->nrdives > 0); + if (!--trip->nrdives) + delete_trip(trip); + else if (trip->when == dive->when) + find_new_trip_start_time(trip); +} + +void add_dive_to_trip(struct dive *dive, dive_trip_t *trip) +{ + if (dive->divetrip == trip) + return; + remove_dive_from_trip(dive, false); + trip->nrdives++; + dive->divetrip = trip; + dive->tripflag = ASSIGNED_TRIP; + + /* Add it to the trip's list of dives*/ + dive->next = trip->dives; + if (dive->next) + dive->next->pprev = &dive->next; + trip->dives = dive; + dive->pprev = &trip->dives; + + if (dive->when && trip->when > dive->when) + trip->when = dive->when; +} + +dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive) +{ + dive_trip_t *dive_trip = calloc(1, sizeof(dive_trip_t)); + + dive_trip->when = dive->when; + dive_trip->location = copy_string(get_dive_location(dive)); + insert_trip(&dive_trip); + + dive->tripflag = IN_TRIP; + add_dive_to_trip(dive, dive_trip); + return dive_trip; +} + +/* + * Walk the dives from the oldest dive, and see if we can autogroup them + */ +void autogroup_dives(void) +{ + int i; + struct dive *dive, *lastdive = NULL; + + for_each_dive(i, dive) { + dive_trip_t *trip; + + if (dive->divetrip) { + lastdive = dive; + continue; + } + + if (!DIVE_NEEDS_TRIP(dive)) { + lastdive = NULL; + continue; + } + + /* Do we have a trip we can combine this into? */ + if (lastdive && dive->when < lastdive->when + TRIP_THRESHOLD) { + dive_trip_t *trip = lastdive->divetrip; + add_dive_to_trip(dive, trip); + if (get_dive_location(dive) && !trip->location) + trip->location = copy_string(get_dive_location(dive)); + lastdive = dive; + continue; + } + + lastdive = dive; + trip = create_and_hookup_trip_from_dive(dive); + trip->autogen = 1; + } + +#ifdef DEBUG_TRIP + dump_trip_list(); +#endif +} + +/* this implements the mechanics of removing the dive from the table, + * but doesn't deal with updating dive trips, etc */ +void delete_single_dive(int idx) +{ + int i; + struct dive *dive = get_dive(idx); + if (!dive) + return; /* this should never happen */ + remove_dive_from_trip(dive, false); + if (dive->selected) + deselect_dive(idx); + for (i = idx; i < dive_table.nr - 1; i++) + dive_table.dives[i] = dive_table.dives[i + 1]; + dive_table.dives[--dive_table.nr] = NULL; + /* free all allocations */ + free(dive->dc.sample); + free((void *)dive->notes); + free((void *)dive->divemaster); + free((void *)dive->buddy); + free((void *)dive->suit); + taglist_free(dive->tag_list); + free(dive); +} + +struct dive **grow_dive_table(struct dive_table *table) +{ + int nr = table->nr, allocated = table->allocated; + struct dive **dives = table->dives; + + if (nr >= allocated) { + allocated = (nr + 32) * 3 / 2; + dives = realloc(dives, allocated * sizeof(struct dive *)); + if (!dives) + exit(1); + table->dives = dives; + table->allocated = allocated; + } + return dives; +} + +void add_single_dive(int idx, struct dive *dive) +{ + int i; + grow_dive_table(&dive_table); + dive_table.nr++; + if (dive->selected) + amount_selected++; + for (i = idx; i < dive_table.nr; i++) { + struct dive *tmp = dive_table.dives[i]; + dive_table.dives[i] = dive; + dive = tmp; + } +} + +bool consecutive_selected() +{ + struct dive *d; + int i; + bool consecutive = true; + bool firstfound = false; + bool lastfound = false; + + if (amount_selected == 0 || amount_selected == 1) + return true; + + for_each_dive(i, d) { + if (d->selected) { + if (!firstfound) + firstfound = true; + else if (lastfound) + consecutive = false; + } else if (firstfound) { + lastfound = true; + } + } + return consecutive; +} + +struct dive *merge_two_dives(struct dive *a, struct dive *b) +{ + struct dive *res; + int i, j; + int id; + + if (!a || !b) + return NULL; + + id = a->id; + i = get_divenr(a); + j = get_divenr(b); + if (i < 0 || j < 0) + // something is wrong with those dives. Bail + return NULL; + res = merge_dives(a, b, b->when - a->when, false); + if (!res) + return NULL; + + add_single_dive(i, res); + delete_single_dive(i + 1); + delete_single_dive(j); + // now make sure that we keep the id of the first dive. + // why? + // because this way one of the previously selected ids is still around + res->id = id; + mark_divelist_changed(true); + return res; +} + +void select_dive(int idx) +{ + struct dive *dive = get_dive(idx); + if (dive) { + /* never select an invalid dive that isn't displayed */ + if (!dive->selected) { + dive->selected = 1; + amount_selected++; + } + selected_dive = idx; + } +} + +void deselect_dive(int idx) +{ + struct dive *dive = get_dive(idx); + if (dive && dive->selected) { + dive->selected = 0; + if (amount_selected) + amount_selected--; + if (selected_dive == idx && amount_selected > 0) { + /* pick a different dive as selected */ + while (--selected_dive >= 0) { + dive = get_dive(selected_dive); + if (dive && dive->selected) + return; + } + selected_dive = idx; + while (++selected_dive < dive_table.nr) { + dive = get_dive(selected_dive); + if (dive && dive->selected) + return; + } + } + if (amount_selected == 0) + selected_dive = -1; + } +} + +void deselect_dives_in_trip(struct dive_trip *trip) +{ + struct dive *dive; + if (!trip) + return; + for (dive = trip->dives; dive; dive = dive->next) + deselect_dive(get_divenr(dive)); +} + +void select_dives_in_trip(struct dive_trip *trip) +{ + struct dive *dive; + if (!trip) + return; + for (dive = trip->dives; dive; dive = dive->next) + if (!dive->hidden_by_filter) + select_dive(get_divenr(dive)); +} + +void filter_dive(struct dive *d, bool shown) +{ + if (!d) + return; + d->hidden_by_filter = !shown; + if (!shown && d->selected) + deselect_dive(get_divenr(d)); +} + + +/* This only gets called with non-NULL trips. + * It does not combine notes or location, just picks the first one + * (or the second one if the first one is empty */ +void combine_trips(struct dive_trip *trip_a, struct dive_trip *trip_b) +{ + if (same_string(trip_a->location, "") && trip_b->location) { + free(trip_a->location); + trip_a->location = strdup(trip_b->location); + } + if (same_string(trip_a->notes, "") && trip_b->notes) { + free(trip_a->notes); + trip_a->notes = strdup(trip_b->notes); + } + /* this also removes the dives from trip_b and eventually + * calls delete_trip(trip_b) when the last dive has been moved */ + while (trip_b->dives) + add_dive_to_trip(trip_b->dives, trip_a); +} + +void mark_divelist_changed(int changed) +{ + dive_list_changed = changed; + updateWindowTitle(); +} + +int unsaved_changes() +{ + return dive_list_changed; +} + +void remove_autogen_trips() +{ + int i; + struct dive *dive; + + for_each_dive(i, dive) { + dive_trip_t *trip = dive->divetrip; + + if (trip && trip->autogen) + remove_dive_from_trip(dive, true); + } +} + +/* + * When adding dives to the dive table, we try to renumber + * the new dives based on any old dives in the dive table. + * + * But we only do it if: + * + * - there are no dives in the dive table + * + * OR + * + * - the last dive in the old dive table was numbered + * + * - all the new dives are strictly at the end (so the + * "last dive" is at the same location in the dive table + * after re-sorting the dives. + * + * - none of the new dives have any numbers + * + * This catches the common case of importing new dives from + * a dive computer, and gives them proper numbers based on + * your old dive list. But it tries to be very conservative + * and not give numbers if there is *any* question about + * what the numbers should be - in which case you need to do + * a manual re-numbering. + */ +static void try_to_renumber(struct dive *last, int preexisting) +{ + int i, nr; + + /* + * If the new dives aren't all strictly at the end, + * we're going to expect the user to do a manual + * renumbering. + */ + if (preexisting && get_dive(preexisting - 1) != last) + return; + + /* + * If any of the new dives already had a number, + * we'll have to do a manual renumbering. + */ + for (i = preexisting; i < dive_table.nr; i++) { + struct dive *dive = get_dive(i); + if (dive->number) + return; + } + + /* + * Ok, renumber.. + */ + if (last) + nr = last->number; + else + nr = 0; + for (i = preexisting; i < dive_table.nr; i++) { + struct dive *dive = get_dive(i); + dive->number = ++nr; + } +} + +void process_dives(bool is_imported, bool prefer_imported) +{ + int i; + int preexisting = dive_table.preexisting; + bool did_merge = false; + struct dive *last; + + /* check if we need a nickname for the divecomputer for newly downloaded dives; + * since we know they all came from the same divecomputer we just check for the + * first one */ + if (preexisting < dive_table.nr && dive_table.dives[preexisting]->downloaded) + set_dc_nickname(dive_table.dives[preexisting]); + else + /* they aren't downloaded, so record / check all new ones */ + for (i = preexisting; i < dive_table.nr; i++) + set_dc_nickname(dive_table.dives[i]); + + for (i = preexisting; i < dive_table.nr; i++) + dive_table.dives[i]->downloaded = true; + + /* This does the right thing for -1: NULL */ + last = get_dive(preexisting - 1); + + sort_table(&dive_table); + + for (i = 1; i < dive_table.nr; i++) { + struct dive **pp = &dive_table.dives[i - 1]; + struct dive *prev = pp[0]; + struct dive *dive = pp[1]; + struct dive *merged; + int id; + + /* only try to merge overlapping dives - or if one of the dives has + * zero duration (that might be a gps marker from the webservice) */ + if (prev->duration.seconds && dive->duration.seconds && + prev->when + prev->duration.seconds < dive->when) + continue; + + merged = try_to_merge(prev, dive, prefer_imported); + if (!merged) + continue; + + // remember the earlier dive's id + id = prev->id; + + /* careful - we might free the dive that last points to. Oops... */ + if (last == prev || last == dive) + last = merged; + + /* Redo the new 'i'th dive */ + i--; + add_single_dive(i, merged); + delete_single_dive(i + 1); + delete_single_dive(i + 1); + // keep the id or the first dive for the merged dive + merged->id = id; + + /* this means the table was changed */ + did_merge = true; + } + /* make sure no dives are still marked as downloaded */ + for (i = 1; i < dive_table.nr; i++) + dive_table.dives[i]->downloaded = false; + + if (is_imported) { + /* If there are dives in the table, are they numbered */ + if (!last || last->number) + try_to_renumber(last, preexisting); + + /* did we add dives or divecomputers to the dive table? */ + if (did_merge || preexisting < dive_table.nr) { + mark_divelist_changed(true); + } + } +} + +void set_dive_nr_for_current_dive() +{ + if (dive_table.nr == 1) + current_dive->number = 1; + else if (selected_dive == dive_table.nr - 1 && get_dive(dive_table.nr - 2)->number) + current_dive->number = get_dive(dive_table.nr - 2)->number + 1; +} + +static int min_datafile_version; + +int get_min_datafile_version() +{ + return min_datafile_version; +} + +void reset_min_datafile_version() +{ + min_datafile_version = 0; +} + +void report_datafile_version(int version) +{ + if (min_datafile_version == 0 || min_datafile_version > version) + min_datafile_version = version; +} + +void clear_dive_file_data() +{ + while (dive_table.nr) + delete_single_dive(0); + while (dive_site_table.nr) + delete_dive_site(get_dive_site(0)->uuid); + + clear_dive(&displayed_dive); + clear_dive_site(&displayed_dive_site); + + free((void *)existing_filename); + existing_filename = NULL; + + reset_min_datafile_version(); +} diff --git a/subsurface-core/divelist.h b/subsurface-core/divelist.h new file mode 100644 index 000000000..5bae09cff --- /dev/null +++ b/subsurface-core/divelist.h @@ -0,0 +1,62 @@ +#ifndef DIVELIST_H +#define DIVELIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* this is used for both git and xml format */ +#define DATAFORMAT_VERSION 3 + +struct dive; + +extern void update_cylinder_related_info(struct dive *); +extern void mark_divelist_changed(int); +extern int unsaved_changes(void); +extern void remove_autogen_trips(void); +extern double init_decompression(struct dive *dive); + +/* divelist core logic functions */ +extern void process_dives(bool imported, bool prefer_imported); +extern char *get_dive_gas_string(struct dive *dive); + +extern dive_trip_t *find_trip_by_idx(int idx); + +struct dive **grow_dive_table(struct dive_table *table); +extern int trip_has_selected_dives(dive_trip_t *trip); +extern void get_dive_gas(struct dive *dive, int *o2_p, int *he_p, int *o2low_p); +extern int get_divenr(struct dive *dive); +extern int get_divesite_idx(struct dive_site *ds); +extern dive_trip_t *find_matching_trip(timestamp_t when); +extern void remove_dive_from_trip(struct dive *dive, short was_autogen); +extern dive_trip_t *create_and_hookup_trip_from_dive(struct dive *dive); +extern void autogroup_dives(void); +extern struct dive *merge_two_dives(struct dive *a, struct dive *b); +extern bool consecutive_selected(); +extern void select_dive(int idx); +extern void deselect_dive(int idx); +extern void select_dives_in_trip(struct dive_trip *trip); +extern void deselect_dives_in_trip(struct dive_trip *trip); +extern void filter_dive(struct dive *d, bool shown); +extern void combine_trips(struct dive_trip *trip_a, struct dive_trip *trip_b); +extern void find_new_trip_start_time(dive_trip_t *trip); +extern struct dive *first_selected_dive(); +extern struct dive *last_selected_dive(); +extern bool is_trip_before_after(struct dive *dive, bool before); +extern void set_dive_nr_for_current_dive(); + +int get_min_datafile_version(); +void reset_min_datafile_version(); +void report_datafile_version(int version); +void clear_dive_file_data(); + +#ifdef DEBUG_TRIP +extern void dump_selection(void); +extern void dump_trip_list(void); +#endif + +#ifdef __cplusplus +} +#endif + +#endif // DIVELIST_H diff --git a/subsurface-core/divelogexportlogic.cpp b/subsurface-core/divelogexportlogic.cpp new file mode 100644 index 000000000..af5157f4a --- /dev/null +++ b/subsurface-core/divelogexportlogic.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include +#include +#include +#include "divelogexportlogic.h" +#include "helpers.h" +#include "units.h" +#include "statistics.h" +#include "save-html.h" + +void file_copy_and_overwrite(const QString &fileName, const QString &newName) +{ + QFile file(newName); + if (file.exists()) + file.remove(); + QFile::copy(fileName, newName); +} + +void exportHTMLsettings(const QString &filename, struct htmlExportSetting &hes) +{ + QString fontSize = hes.fontSize; + QString fontFamily = hes.fontFamily; + QFile file(filename); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + out << "settings = {\"fontSize\":\"" << fontSize << "\",\"fontFamily\":\"" << fontFamily << "\",\"listOnly\":\"" + << hes.listOnly << "\",\"subsurfaceNumbers\":\"" << hes.subsurfaceNumbers << "\","; + //save units preferences + if (prefs.unit_system == METRIC) { + out << "\"unit_system\":\"Meteric\""; + } else if (prefs.unit_system == IMPERIAL) { + out << "\"unit_system\":\"Imperial\""; + } else { + QVariant v; + QString length, pressure, volume, temperature, weight; + length = prefs.units.length == units::METERS ? "METER" : "FEET"; + pressure = prefs.units.pressure == units::BAR ? "BAR" : "PSI"; + volume = prefs.units.volume == units::LITER ? "LITER" : "CUFT"; + temperature = prefs.units.temperature == units::CELSIUS ? "CELSIUS" : "FAHRENHEIT"; + weight = prefs.units.weight == units::KG ? "KG" : "LBS"; + out << "\"unit_system\":\"Personalize\","; + out << "\"units\":{\"depth\":\"" << length << "\",\"pressure\":\"" << pressure << "\",\"volume\":\"" << volume << "\",\"temperature\":\"" << temperature << "\",\"weight\":\"" << weight << "\"}"; + } + out << "}"; + file.close(); +} + +static void exportHTMLstatisticsTotal(QTextStream &out, stats_t *total_stats) +{ + out << "{"; + out << "\"YEAR\":\"Total\","; + out << "\"DIVES\":\"" << total_stats->selection_size << "\","; + out << "\"TOTAL_TIME\":\"" << get_time_string(total_stats->total_time.seconds, 0) << "\","; + out << "\"AVERAGE_TIME\":\"--\","; + out << "\"SHORTEST_TIME\":\"--\","; + out << "\"LONGEST_TIME\":\"--\","; + out << "\"AVG_DEPTH\":\"--\","; + out << "\"MIN_DEPTH\":\"--\","; + out << "\"MAX_DEPTH\":\"--\","; + out << "\"AVG_SAC\":\"--\","; + out << "\"MIN_SAC\":\"--\","; + out << "\"MAX_SAC\":\"--\","; + out << "\"AVG_TEMP\":\"--\","; + out << "\"MIN_TEMP\":\"--\","; + out << "\"MAX_TEMP\":\"--\","; + out << "},"; +} + + +static void exportHTMLstatistics(const QString filename, struct htmlExportSetting &hes) +{ + QFile file(filename); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + + stats_t total_stats; + + total_stats.selection_size = 0; + total_stats.total_time.seconds = 0; + + int i = 0; + out << "divestat=["; + if (hes.yearlyStatistics) { + while (stats_yearly != NULL && stats_yearly[i].period) { + out << "{"; + out << "\"YEAR\":\"" << stats_yearly[i].period << "\","; + out << "\"DIVES\":\"" << stats_yearly[i].selection_size << "\","; + out << "\"TOTAL_TIME\":\"" << get_time_string(stats_yearly[i].total_time.seconds, 0) << "\","; + out << "\"AVERAGE_TIME\":\"" << get_minutes(stats_yearly[i].total_time.seconds / stats_yearly[i].selection_size) << "\","; + out << "\"SHORTEST_TIME\":\"" << get_minutes(stats_yearly[i].shortest_time.seconds) << "\","; + out << "\"LONGEST_TIME\":\"" << get_minutes(stats_yearly[i].longest_time.seconds) << "\","; + out << "\"AVG_DEPTH\":\"" << get_depth_string(stats_yearly[i].avg_depth) << "\","; + out << "\"MIN_DEPTH\":\"" << get_depth_string(stats_yearly[i].min_depth) << "\","; + out << "\"MAX_DEPTH\":\"" << get_depth_string(stats_yearly[i].max_depth) << "\","; + out << "\"AVG_SAC\":\"" << get_volume_string(stats_yearly[i].avg_sac) << "\","; + out << "\"MIN_SAC\":\"" << get_volume_string(stats_yearly[i].min_sac) << "\","; + out << "\"MAX_SAC\":\"" << get_volume_string(stats_yearly[i].max_sac) << "\","; + if ( stats_yearly[i].combined_count ) + out << "\"AVG_TEMP\":\"" << QString::number(stats_yearly[i].combined_temp / stats_yearly[i].combined_count, 'f', 1) << "\","; + else + out << "\"AVG_TEMP\":\"0.0\","; + out << "\"MIN_TEMP\":\"" << ( stats_yearly[i].min_temp == 0 ? 0 : get_temp_units(stats_yearly[i].min_temp, NULL)) << "\","; + out << "\"MAX_TEMP\":\"" << ( stats_yearly[i].max_temp == 0 ? 0 : get_temp_units(stats_yearly[i].max_temp, NULL)) << "\","; + out << "},"; + total_stats.selection_size += stats_yearly[i].selection_size; + total_stats.total_time.seconds += stats_yearly[i].total_time.seconds; + i++; + } + exportHTMLstatisticsTotal(out, &total_stats); + } + out << "]"; + file.close(); + +} + +void exportHtmlInitLogic(const QString &filename, struct htmlExportSetting &hes) +{ + QString photosDirectory; + QFile file(filename); + QFileInfo info(file); + QDir mainDir = info.absoluteDir(); + mainDir.mkdir(file.fileName() + "_files"); + QString exportFiles = file.fileName() + "_files"; + + QString json_dive_data = exportFiles + QDir::separator() + "file.js"; + QString json_settings = exportFiles + QDir::separator() + "settings.js"; + QString translation = exportFiles + QDir::separator() + "translation.js"; + QString stat_file = exportFiles + QDir::separator() + "stat.js"; + exportFiles += "/"; + + if (hes.exportPhotos) { + photosDirectory = exportFiles + QDir::separator() + "photos" + QDir::separator(); + mainDir.mkdir(photosDirectory); + } + + + exportHTMLsettings(json_settings, hes); + exportHTMLstatistics(stat_file, hes); + export_translation(translation.toUtf8().data()); + + export_HTML(qPrintable(json_dive_data), qPrintable(photosDirectory), hes.selectedOnly, hes.listOnly); + + QString searchPath = getSubsurfaceDataPath("theme"); + if (searchPath.isEmpty()) + return; + + searchPath += QDir::separator(); + + file_copy_and_overwrite(searchPath + "dive_export.html", filename); + file_copy_and_overwrite(searchPath + "list_lib.js", exportFiles + "list_lib.js"); + file_copy_and_overwrite(searchPath + "poster.png", exportFiles + "poster.png"); + file_copy_and_overwrite(searchPath + "jqplot.highlighter.min.js", exportFiles + "jqplot.highlighter.min.js"); + file_copy_and_overwrite(searchPath + "jquery.jqplot.min.js", exportFiles + "jquery.jqplot.min.js"); + file_copy_and_overwrite(searchPath + "jqplot.canvasAxisTickRenderer.min.js", exportFiles + "jqplot.canvasAxisTickRenderer.min.js"); + file_copy_and_overwrite(searchPath + "jqplot.canvasTextRenderer.min.js", exportFiles + "jqplot.canvasTextRenderer.min.js"); + file_copy_and_overwrite(searchPath + "jquery.min.js", exportFiles + "jquery.min.js"); + file_copy_and_overwrite(searchPath + "jquery.jqplot.css", exportFiles + "jquery.jqplot.css"); + file_copy_and_overwrite(searchPath + hes.themeFile, exportFiles + "theme.css"); +} diff --git a/subsurface-core/divelogexportlogic.h b/subsurface-core/divelogexportlogic.h new file mode 100644 index 000000000..84f09c362 --- /dev/null +++ b/subsurface-core/divelogexportlogic.h @@ -0,0 +1,20 @@ +#ifndef DIVELOGEXPORTLOGIC_H +#define DIVELOGEXPORTLOGIC_H + +struct htmlExportSetting { + bool exportPhotos; + bool selectedOnly; + bool listOnly; + QString fontFamily; + QString fontSize; + int themeSelection; + bool subsurfaceNumbers; + bool yearlyStatistics; + QString themeFile; +}; + +void file_copy_and_overwrite(const QString &fileName, const QString &newName); +void exportHtmlInitLogic(const QString &filename, struct htmlExportSetting &hes); + +#endif // DIVELOGEXPORTLOGIC_H + diff --git a/subsurface-core/divesite.c b/subsurface-core/divesite.c new file mode 100644 index 000000000..e9eed2a07 --- /dev/null +++ b/subsurface-core/divesite.c @@ -0,0 +1,337 @@ +/* divesite.c */ +#include "divesite.h" +#include "dive.h" +#include "divelist.h" + +#include + +struct dive_site_table dive_site_table; + +/* there could be multiple sites of the same name - return the first one */ +uint32_t get_dive_site_uuid_by_name(const char *name, struct dive_site **dsp) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) { + if (same_string(ds->name, name)) { + if (dsp) + *dsp = ds; + return ds->uuid; + } + } + return 0; +} + +/* there could be multiple sites at the same GPS fix - return the first one */ +uint32_t get_dive_site_uuid_by_gps(degrees_t latitude, degrees_t longitude, struct dive_site **dsp) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) { + if (ds->latitude.udeg == latitude.udeg && ds->longitude.udeg == longitude.udeg) { + if (dsp) + *dsp = ds; + return ds->uuid; + } + } + return 0; +} + + +/* to avoid a bug where we have two dive sites with different name and the same GPS coordinates + * and first get the gps coordinates (reading a V2 file) and happen to get back "the other" name, + * this function allows us to verify if a very specific name/GPS combination already exists */ +uint32_t get_dive_site_uuid_by_gps_and_name(char *name, degrees_t latitude, degrees_t longitude) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) { + if (ds->latitude.udeg == latitude.udeg && ds->longitude.udeg == longitude.udeg && same_string(ds->name, name)) + return ds->uuid; + } + return 0; +} + +// Calculate the distance in meters between two coordinates. +unsigned int get_distance(degrees_t lat1, degrees_t lon1, degrees_t lat2, degrees_t lon2) +{ + double lat2_r = udeg_to_radians(lat2.udeg); + double lat_d_r = udeg_to_radians(lat2.udeg-lat1.udeg); + double lon_d_r = udeg_to_radians(lon2.udeg-lon1.udeg); + + double a = sin(lat_d_r/2) * sin(lat_d_r/2) + + cos(lat2_r) * cos(lat2_r) * sin(lon_d_r/2) * sin(lon_d_r/2); + double c = 2 * atan2(sqrt(a), sqrt(1.0 - a)); + + // Earth radious in metres + return 6371000 * c; +} + +/* find the closest one, no more than distance meters away - if more than one at same distance, pick the first */ +uint32_t get_dive_site_uuid_by_gps_proximity(degrees_t latitude, degrees_t longitude, int distance, struct dive_site **dsp) +{ + int i; + int uuid = 0; + struct dive_site *ds; + unsigned int cur_distance, min_distance = distance; + for_each_dive_site (i, ds) { + if (dive_site_has_gps_location(ds) && + (cur_distance = get_distance(ds->latitude, ds->longitude, latitude, longitude)) < min_distance) { + min_distance = cur_distance; + uuid = ds->uuid; + if (dsp) + *dsp = ds; + } + } + return uuid; +} + +/* try to create a uniqe ID - fingers crossed */ +static uint32_t dive_site_getUniqId() +{ + uint32_t id = 0; + + while (id == 0 || get_dive_site_by_uuid(id)) { + id = rand() & 0xff; + id |= (rand() & 0xff) << 8; + id |= (rand() & 0xff) << 16; + id |= (rand() & 0xff) << 24; + } + + return id; +} + +/* we never allow a second dive site with the same uuid */ +struct dive_site *alloc_or_get_dive_site(uint32_t uuid) +{ + struct dive_site *ds; + if (uuid) { + if ((ds = get_dive_site_by_uuid(uuid)) != NULL) { + fprintf(stderr, "PROBLEM: refusing to create dive site with the same uuid %08x\n", uuid); + return ds; + } + } + int nr = dive_site_table.nr; + int allocated = dive_site_table.allocated; + struct dive_site **sites = dive_site_table.dive_sites; + + if (nr >= allocated) { + allocated = (nr + 32) * 3 / 2; + sites = realloc(sites, allocated * sizeof(struct dive_site *)); + if (!sites) + exit(1); + dive_site_table.dive_sites = sites; + dive_site_table.allocated = allocated; + } + ds = calloc(1, sizeof(*ds)); + if (!ds) + exit(1); + sites[nr] = ds; + dive_site_table.nr = nr + 1; + // we should always be called with a valid uuid except in the special + // case where we want to copy a dive site into the memory we allocated + // here - then we need to pass in 0 and create a temporary uuid here + // (just so things are always consistent) + if (uuid) + ds->uuid = uuid; + else + ds->uuid = dive_site_getUniqId(); + return ds; +} + +int nr_of_dives_at_dive_site(uint32_t uuid, bool select_only) +{ + int j; + int nr = 0; + struct dive *d; + for_each_dive(j, d) { + if (d->dive_site_uuid == uuid && (!select_only || d->selected)) { + nr++; + } + } + return nr; +} + +bool is_dive_site_used(uint32_t uuid, bool select_only) +{ + int j; + bool found = false; + struct dive *d; + for_each_dive(j, d) { + if (d->dive_site_uuid == uuid && (!select_only || d->selected)) { + found = true; + break; + } + } + return found; +} + +void delete_dive_site(uint32_t id) +{ + int nr = dive_site_table.nr; + for (int i = 0; i < nr; i++) { + struct dive_site *ds = get_dive_site(i); + if (ds->uuid == id) { + free(ds->name); + free(ds->notes); + free(ds); + if (nr - 1 > i) + memmove(&dive_site_table.dive_sites[i], + &dive_site_table.dive_sites[i+1], + (nr - 1 - i) * sizeof(dive_site_table.dive_sites[0])); + dive_site_table.nr = nr - 1; + break; + } + } +} + +uint32_t create_divesite_uuid(const char *name, timestamp_t divetime) +{ + if (name == NULL) + name =""; + unsigned char hash[20]; + SHA_CTX ctx; + SHA1_Init(&ctx); + SHA1_Update(&ctx, &divetime, sizeof(timestamp_t)); + SHA1_Update(&ctx, name, strlen(name)); + SHA1_Final(hash, &ctx); + // now return the first 32 of the 160 bit hash + return *(uint32_t *)hash; +} + +/* allocate a new site and add it to the table */ +uint32_t create_dive_site(const char *name, timestamp_t divetime) +{ + uint32_t uuid = create_divesite_uuid(name, divetime); + struct dive_site *ds = alloc_or_get_dive_site(uuid); + ds->name = copy_string(name); + + return uuid; +} + +/* same as before, but with current time if no current_dive is present */ +uint32_t create_dive_site_from_current_dive(const char *name) +{ + if (current_dive != NULL) { + return create_dive_site(name, current_dive->when); + } else { + timestamp_t when; + time_t now = time(0); + when = utc_mktime(localtime(&now)); + return create_dive_site(name, when); + } +} + +/* same as before, but with GPS data */ +uint32_t create_dive_site_with_gps(const char *name, degrees_t latitude, degrees_t longitude, timestamp_t divetime) +{ + uint32_t uuid = create_divesite_uuid(name, divetime); + struct dive_site *ds = alloc_or_get_dive_site(uuid); + ds->name = copy_string(name); + ds->latitude = latitude; + ds->longitude = longitude; + + return ds->uuid; +} + +/* a uuid is always present - but if all the other fields are empty, the dive site is pointless */ +bool dive_site_is_empty(struct dive_site *ds) +{ + return same_string(ds->name, "") && + same_string(ds->description, "") && + same_string(ds->notes, "") && + ds->latitude.udeg == 0 && + ds->longitude.udeg == 0; +} + +void copy_dive_site(struct dive_site *orig, struct dive_site *copy) +{ + free(copy->name); + free(copy->notes); + free(copy->description); + + copy->latitude = orig->latitude; + copy->longitude = orig->longitude; + copy->name = copy_string(orig->name); + copy->notes = copy_string(orig->notes); + copy->description = copy_string(orig->description); + copy->uuid = orig->uuid; + if (orig->taxonomy.category == NULL) { + free_taxonomy(©->taxonomy); + } else { + if (copy->taxonomy.category == NULL) + copy->taxonomy.category = alloc_taxonomy(); + for (int i = 0; i < TC_NR_CATEGORIES; i++) { + if (i < copy->taxonomy.nr) + free((void *)copy->taxonomy.category[i].value); + if (i < orig->taxonomy.nr) { + copy->taxonomy.category[i] = orig->taxonomy.category[i]; + copy->taxonomy.category[i].value = copy_string(orig->taxonomy.category[i].value); + } + } + copy->taxonomy.nr = orig->taxonomy.nr; + } +} + +void clear_dive_site(struct dive_site *ds) +{ + free(ds->name); + free(ds->notes); + free(ds->description); + ds->name = 0; + ds->notes = 0; + ds->description = 0; + ds->latitude.udeg = 0; + ds->longitude.udeg = 0; + ds->uuid = 0; + ds->taxonomy.nr = 0; + free_taxonomy(&ds->taxonomy); +} + +void merge_dive_sites(uint32_t ref, uint32_t* uuids, int count) +{ + int curr_dive, i; + struct dive *d; + for(i = 0; i < count; i++){ + if (uuids[i] == ref) + continue; + + for_each_dive(curr_dive, d) { + if (d->dive_site_uuid != uuids[i] ) + continue; + d->dive_site_uuid = ref; + } + } + + for(i = 0; i < count; i++) { + if (uuids[i] == ref) + continue; + delete_dive_site(uuids[i]); + } + mark_divelist_changed(true); +} + +uint32_t find_or_create_dive_site_with_name(const char *name, timestamp_t divetime) +{ + int i; + struct dive_site *ds; + for_each_dive_site(i,ds) { + if (same_string(name, ds->name)) + break; + } + if (ds) + return ds->uuid; + return create_dive_site(name, divetime); +} + +static int compare_sites(const void *_a, const void *_b) +{ + const struct dive_site *a = (const struct dive_site *)*(void **)_a; + const struct dive_site *b = (const struct dive_site *)*(void **)_b; + return a->uuid > b->uuid ? 1 : a->uuid == b->uuid ? 0 : -1; +} + +void dive_site_table_sort() +{ + qsort(dive_site_table.dive_sites, dive_site_table.nr, sizeof(struct dive_site *), compare_sites); +} diff --git a/subsurface-core/divesite.cpp b/subsurface-core/divesite.cpp new file mode 100644 index 000000000..ae102a14b --- /dev/null +++ b/subsurface-core/divesite.cpp @@ -0,0 +1,31 @@ +#include "divesite.h" +#include "pref.h" + +QString constructLocationTags(uint32_t ds_uuid) +{ + QString locationTag; + struct dive_site *ds = get_dive_site_by_uuid(ds_uuid); + + if (!ds || !ds->taxonomy.nr) + return locationTag; + + locationTag = "(tags: "; + QString connector; + for (int i = 0; i < 3; i++) { + if (prefs.geocoding.category[i] == TC_NONE) + continue; + for (int j = 0; j < TC_NR_CATEGORIES; j++) { + if (ds->taxonomy.category[j].category == prefs.geocoding.category[i]) { + QString tag = ds->taxonomy.category[j].value; + if (!tag.isEmpty()) { + locationTag += connector + tag; + connector = " / "; + } + break; + } + } + } + + locationTag += ")"; + return locationTag; +} diff --git a/subsurface-core/divesite.h b/subsurface-core/divesite.h new file mode 100644 index 000000000..f18b2e8e8 --- /dev/null +++ b/subsurface-core/divesite.h @@ -0,0 +1,80 @@ +#ifndef DIVESITE_H +#define DIVESITE_H + +#include "units.h" +#include "taxonomy.h" +#include + +#ifdef __cplusplus +#include +extern "C" { +#else +#include +#endif + +struct dive_site +{ + uint32_t uuid; + char *name; + degrees_t latitude, longitude; + char *description; + char *notes; + struct taxonomy_data taxonomy; +}; + +struct dive_site_table { + int nr, allocated; + struct dive_site **dive_sites; +}; + +extern struct dive_site_table dive_site_table; + +static inline struct dive_site *get_dive_site(int nr) +{ + if (nr >= dive_site_table.nr || nr < 0) + return NULL; + return dive_site_table.dive_sites[nr]; +} + +/* iterate over each dive site */ +#define for_each_dive_site(_i, _x) \ + for ((_i) = 0; ((_x) = get_dive_site(_i)) != NULL; (_i)++) + +static inline struct dive_site *get_dive_site_by_uuid(uint32_t uuid) +{ + int i; + struct dive_site *ds; + for_each_dive_site (i, ds) + if (ds->uuid == uuid) + return get_dive_site(i); + return NULL; +} + +void dive_site_table_sort(); +struct dive_site *alloc_or_get_dive_site(uint32_t uuid); +int nr_of_dives_at_dive_site(uint32_t uuid, bool select_only); +bool is_dive_site_used(uint32_t uuid, bool select_only); +void delete_dive_site(uint32_t id); +uint32_t create_dive_site(const char *name, timestamp_t divetime); +uint32_t create_dive_site_from_current_dive(const char *name); +uint32_t create_dive_site_with_gps(const char *name, degrees_t latitude, degrees_t longitude, timestamp_t divetime); +uint32_t get_dive_site_uuid_by_name(const char *name, struct dive_site **dsp); +uint32_t get_dive_site_uuid_by_gps(degrees_t latitude, degrees_t longitude, struct dive_site **dsp); +uint32_t get_dive_site_uuid_by_gps_and_name(char *name, degrees_t latitude, degrees_t longitude); +uint32_t get_dive_site_uuid_by_gps_proximity(degrees_t latitude, degrees_t longitude, int distance, struct dive_site **dsp); +bool dive_site_is_empty(struct dive_site *ds); +void copy_dive_site(struct dive_site *orig, struct dive_site *copy); +void clear_dive_site(struct dive_site *ds); +unsigned int get_distance(degrees_t lat1, degrees_t lon1, degrees_t lat2, degrees_t lon2); +uint32_t find_or_create_dive_site_with_name(const char *name, timestamp_t divetime); +void merge_dive_sites(uint32_t ref, uint32_t *uuids, int count); + +#define INVALID_DIVE_SITE_NAME "development use only - not a valid dive site name" + +#ifdef __cplusplus +} +QString constructLocationTags(uint32_t ds_uuid); + +#endif + +#endif // DIVESITE_H diff --git a/subsurface-core/divesitehelpers.cpp b/subsurface-core/divesitehelpers.cpp new file mode 100644 index 000000000..3542f96fa --- /dev/null +++ b/subsurface-core/divesitehelpers.cpp @@ -0,0 +1,208 @@ +// +// infrastructure to deal with dive sites +// + +#include "divesitehelpers.h" + +#include "divesite.h" +#include "helpers.h" +#include "membuffer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GeoLookupInfo { + degrees_t lat; + degrees_t lon; + uint32_t uuid; +}; + +QVector geo_lookup_data; + +ReverseGeoLookupThread* ReverseGeoLookupThread::instance() { + static ReverseGeoLookupThread* self = new ReverseGeoLookupThread(); + return self; +} + +ReverseGeoLookupThread::ReverseGeoLookupThread(QObject *obj) : QThread(obj) +{ +} + +void ReverseGeoLookupThread::run() { + if (geo_lookup_data.isEmpty()) + return; + + QNetworkRequest request; + QNetworkAccessManager *rgl = new QNetworkAccessManager(); + QEventLoop loop; + QString mapquestURL("http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&accept-language=%1&lat=%2&lon=%3"); + QString geonamesURL("http://api.geonames.org/findNearbyPlaceNameJSON?language=%1&lat=%2&lng=%3&radius=50&username=dirkhh"); + QString geonamesOceanURL("http://api.geonames.org/oceanJSON?language=%1&lat=%2&lng=%3&radius=50&username=dirkhh"); + QString divelogsURL("https://www.divelogs.de/mapsearch_divespotnames.php?lat=%1&lng=%2&radius=50"); + QTimer timer; + + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + + Q_FOREACH (const GeoLookupInfo& info, geo_lookup_data ) { + struct dive_site *ds = info.uuid ? get_dive_site_by_uuid(info.uuid) : &displayed_dive_site; + + // first check the findNearbyPlaces API from geonames - that should give us country, state, city + request.setUrl(geonamesURL.arg(uiLanguage(NULL)).arg(info.lat.udeg / 1000000.0).arg(info.lon.udeg / 1000000.0)); + + QNetworkReply *reply = rgl->get(request); + timer.setSingleShot(true); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + timer.start(5000); // 5 secs. timeout + loop.exec(); + + if(timer.isActive()) { + timer.stop(); + if(reply->error() > 0) { + report_error("got error accessing geonames.org: %s", qPrintable(reply->errorString())); + goto clear_reply; + } + int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (v < 200 || v >= 300) + goto clear_reply; + QByteArray fullReply = reply->readAll(); + QJsonParseError errorObject; + QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject); + if (errorObject.error != QJsonParseError::NoError) { + report_error("error parsing geonames.org response: %s", qPrintable(errorObject.errorString())); + goto clear_reply; + } + QJsonObject obj = jsonDoc.object(); + QVariant geoNamesObject = obj.value("geonames").toVariant(); + QVariantList geoNames = geoNamesObject.toList(); + if (geoNames.count() > 0) { + QVariantMap firstData = geoNames.at(0).toMap(); + int ri = 0, l3 = -1, lt = -1; + if (ds->taxonomy.category == NULL) { + ds->taxonomy.category = alloc_taxonomy(); + } else { + // clear out the data (except for the ocean data) + int ocean; + if ((ocean = taxonomy_index_for_category(&ds->taxonomy, TC_OCEAN)) > 0) { + ds->taxonomy.category[0] = ds->taxonomy.category[ocean]; + ds->taxonomy.nr = 1; + } else { + // ocean is -1 if there is no such entry, and we didn't copy above + // if ocean is 0, so the following gets us the correct count + ds->taxonomy.nr = ocean + 1; + } + } + // get all the data - OCEAN is special, so start at COUNTRY + for (int j = TC_COUNTRY; j < TC_NR_CATEGORIES; j++) { + if (firstData[taxonomy_api_names[j]].isValid()) { + ds->taxonomy.category[ri].category = j; + ds->taxonomy.category[ri].origin = taxonomy::GEOCODED; + free((void*)ds->taxonomy.category[ri].value); + ds->taxonomy.category[ri].value = copy_string(qPrintable(firstData[taxonomy_api_names[j]].toString())); + ri++; + } + } + ds->taxonomy.nr = ri; + l3 = taxonomy_index_for_category(&ds->taxonomy, TC_ADMIN_L3); + lt = taxonomy_index_for_category(&ds->taxonomy, TC_LOCALNAME); + if (l3 == -1 && lt != -1) { + // basically this means we did get a local name (what we call town), but just like most places + // we didn't get an adminName_3 - which in some regions is the actual city that town belongs to, + // then we copy the town into the city + ds->taxonomy.category[ri].value = copy_string(ds->taxonomy.category[lt].value); + ds->taxonomy.category[ri].origin = taxonomy::COPIED; + ds->taxonomy.category[ri].category = TC_ADMIN_L3; + ds->taxonomy.nr++; + } + mark_divelist_changed(true); + } else { + report_error("geonames.org did not provide reverse lookup information"); + qDebug() << "no reverse geo lookup; geonames returned\n" << fullReply; + } + } else { + report_error("timeout accessing geonames.org"); + disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + reply->abort(); + } + // next check the oceans API to figure out the body of water + request.setUrl(geonamesOceanURL.arg(uiLanguage(NULL)).arg(info.lat.udeg / 1000000.0).arg(info.lon.udeg / 1000000.0)); + reply = rgl->get(request); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + timer.start(5000); // 5 secs. timeout + loop.exec(); + if(timer.isActive()) { + timer.stop(); + if(reply->error() > 0) { + report_error("got error accessing oceans API of geonames.org: %s", qPrintable(reply->errorString())); + goto clear_reply; + } + int v = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (v < 200 || v >= 300) + goto clear_reply; + QByteArray fullReply = reply->readAll(); + QJsonParseError errorObject; + QJsonDocument jsonDoc = QJsonDocument::fromJson(fullReply, &errorObject); + if (errorObject.error != QJsonParseError::NoError) { + report_error("error parsing geonames.org response: %s", qPrintable(errorObject.errorString())); + goto clear_reply; + } + QJsonObject obj = jsonDoc.object(); + QVariant oceanObject = obj.value("ocean").toVariant(); + QVariantMap oceanName = oceanObject.toMap(); + if (oceanName["name"].isValid()) { + int idx; + if (ds->taxonomy.category == NULL) + ds->taxonomy.category = alloc_taxonomy(); + idx = taxonomy_index_for_category(&ds->taxonomy, TC_OCEAN); + if (idx == -1) + idx = ds->taxonomy.nr; + if (idx < TC_NR_CATEGORIES) { + ds->taxonomy.category[idx].category = TC_OCEAN; + ds->taxonomy.category[idx].origin = taxonomy::GEOCODED; + ds->taxonomy.category[idx].value = copy_string(qPrintable(oceanName["name"].toString())); + if (idx == ds->taxonomy.nr) + ds->taxonomy.nr++; + } + mark_divelist_changed(true); + } + } else { + report_error("timeout accessing geonames.org"); + disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + reply->abort(); + } + +clear_reply: + reply->deleteLater(); + } + rgl->deleteLater(); +} + +void ReverseGeoLookupThread::lookup(dive_site *ds) +{ + if (!ds) + return; + GeoLookupInfo info; + info.lat = ds->latitude; + info.lon = ds->longitude; + info.uuid = ds->uuid; + + geo_lookup_data.clear(); + geo_lookup_data.append(info); + run(); +} + +extern "C" void add_geo_information_for_lookup(degrees_t latitude, degrees_t longitude, uint32_t uuid) { + GeoLookupInfo info; + info.lat = latitude; + info.lon = longitude; + info.uuid = uuid; + + geo_lookup_data.append(info); +} diff --git a/subsurface-core/divesitehelpers.h b/subsurface-core/divesitehelpers.h new file mode 100644 index 000000000..a08069bc0 --- /dev/null +++ b/subsurface-core/divesitehelpers.h @@ -0,0 +1,18 @@ +#ifndef DIVESITEHELPERS_H +#define DIVESITEHELPERS_H + +#include "units.h" +#include + +class ReverseGeoLookupThread : public QThread { +Q_OBJECT +public: + static ReverseGeoLookupThread *instance(); + void lookup(struct dive_site *ds); + void run() Q_DECL_OVERRIDE; + +private: + ReverseGeoLookupThread(QObject *parent = 0); +}; + +#endif // DIVESITEHELPERS_H diff --git a/subsurface-core/equipment.c b/subsurface-core/equipment.c new file mode 100644 index 000000000..47c439735 --- /dev/null +++ b/subsurface-core/equipment.c @@ -0,0 +1,235 @@ +/* equipment.c */ +#include +#include +#include +#include +#include +#include "gettext.h" +#include "dive.h" +#include "display.h" +#include "divelist.h" + +/* placeholders for a few functions that we need to redesign for the Qt UI */ +void add_cylinder_description(cylinder_type_t *type) +{ + const char *desc; + int i; + + desc = type->description; + if (!desc) + return; + for (i = 0; i < 100 && tank_info[i].name != NULL; i++) { + if (strcmp(tank_info[i].name, desc) == 0) + return; + } + if (i < 100) { + // FIXME: leaked on exit + tank_info[i].name = strdup(desc); + tank_info[i].ml = type->size.mliter; + tank_info[i].bar = type->workingpressure.mbar / 1000; + } +} +void add_weightsystem_description(weightsystem_t *weightsystem) +{ + const char *desc; + int i; + + desc = weightsystem->description; + if (!desc) + return; + for (i = 0; i < 100 && ws_info[i].name != NULL; i++) { + if (strcmp(ws_info[i].name, desc) == 0) { + ws_info[i].grams = weightsystem->weight.grams; + return; + } + } + if (i < 100) { + // FIXME: leaked on exit + ws_info[i].name = strdup(desc); + ws_info[i].grams = weightsystem->weight.grams; + } +} + +bool cylinder_nodata(cylinder_t *cyl) +{ + return !cyl->type.size.mliter && + !cyl->type.workingpressure.mbar && + !cyl->type.description && + !cyl->gasmix.o2.permille && + !cyl->gasmix.he.permille && + !cyl->start.mbar && + !cyl->end.mbar && + !cyl->gas_used.mliter && + !cyl->deco_gas_used.mliter; +} + +static bool cylinder_nosamples(cylinder_t *cyl) +{ + return !cyl->sample_start.mbar && + !cyl->sample_end.mbar; +} + +bool cylinder_none(void *_data) +{ + cylinder_t *cyl = _data; + return cylinder_nodata(cyl) && cylinder_nosamples(cyl); +} + +void get_gas_string(const struct gasmix *gasmix, char *text, int len) +{ + if (gasmix_is_air(gasmix)) + snprintf(text, len, "%s", translate("gettextFromC", "air")); + else if (get_he(gasmix) == 0 && get_o2(gasmix) < 1000) + snprintf(text, len, translate("gettextFromC", "EAN%d"), (get_o2(gasmix) + 5) / 10); + else if (get_he(gasmix) == 0 && get_o2(gasmix) == 1000) + snprintf(text, len, "%s", translate("gettextFromC", "oxygen")); + else + snprintf(text, len, "(%d/%d)", (get_o2(gasmix) + 5) / 10, (get_he(gasmix) + 5) / 10); +} + +/* Returns a static char buffer - only good for immediate use by printf etc */ +const char *gasname(const struct gasmix *gasmix) +{ + static char gas[64]; + get_gas_string(gasmix, gas, sizeof(gas)); + return gas; +} + +bool weightsystem_none(void *_data) +{ + weightsystem_t *ws = _data; + return !ws->weight.grams && !ws->description; +} + +bool no_weightsystems(weightsystem_t *ws) +{ + int i; + + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + if (!weightsystem_none(ws + i)) + return false; + return true; +} + +static bool one_weightsystem_equal(weightsystem_t *ws1, weightsystem_t *ws2) +{ + return ws1->weight.grams == ws2->weight.grams && + same_string(ws1->description, ws2->description); +} + +bool weightsystems_equal(weightsystem_t *ws1, weightsystem_t *ws2) +{ + int i; + + for (i = 0; i < MAX_WEIGHTSYSTEMS; i++) + if (!one_weightsystem_equal(ws1 + i, ws2 + i)) + return false; + return true; +} + +/* + * We hardcode the most common standard cylinders, + * we should pick up any other names from the dive + * logs directly. + */ +struct tank_info_t tank_info[100] = { + /* Need an empty entry for the no-cylinder case */ + { "", }, + + /* Size-only metric cylinders */ + { "10.0â„“", .ml = 10000 }, + { "11.1â„“", .ml = 11100 }, + + /* Most common AL cylinders */ + { "AL40", .cuft = 40, .psi = 3000 }, + { "AL50", .cuft = 50, .psi = 3000 }, + { "AL63", .cuft = 63, .psi = 3000 }, + { "AL72", .cuft = 72, .psi = 3000 }, + { "AL80", .cuft = 80, .psi = 3000 }, + { "AL100", .cuft = 100, .psi = 3300 }, + + /* Metric AL cylinders */ + { "ALU7", .ml = 7000, .bar = 200 }, + + /* Somewhat common LP steel cylinders */ + { "LP85", .cuft = 85, .psi = 2640 }, + { "LP95", .cuft = 95, .psi = 2640 }, + { "LP108", .cuft = 108, .psi = 2640 }, + { "LP121", .cuft = 121, .psi = 2640 }, + + /* Somewhat common HP steel cylinders */ + { "HP65", .cuft = 65, .psi = 3442 }, + { "HP80", .cuft = 80, .psi = 3442 }, + { "HP100", .cuft = 100, .psi = 3442 }, + { "HP119", .cuft = 119, .psi = 3442 }, + { "HP130", .cuft = 130, .psi = 3442 }, + + /* Common European steel cylinders */ + { "3â„“ 232 bar", .ml = 3000, .bar = 232 }, + { "3â„“ 300 bar", .ml = 3000, .bar = 300 }, + { "10â„“ 300 bar", .ml = 10000, .bar = 300 }, + { "12â„“ 200 bar", .ml = 12000, .bar = 200 }, + { "12â„“ 232 bar", .ml = 12000, .bar = 232 }, + { "12â„“ 300 bar", .ml = 12000, .bar = 300 }, + { "15â„“ 200 bar", .ml = 15000, .bar = 200 }, + { "15â„“ 232 bar", .ml = 15000, .bar = 232 }, + { "D7 300 bar", .ml = 14000, .bar = 300 }, + { "D8.5 232 bar", .ml = 17000, .bar = 232 }, + { "D12 232 bar", .ml = 24000, .bar = 232 }, + { "D13 232 bar", .ml = 26000, .bar = 232 }, + { "D15 232 bar", .ml = 30000, .bar = 232 }, + { "D16 232 bar", .ml = 32000, .bar = 232 }, + { "D18 232 bar", .ml = 36000, .bar = 232 }, + { "D20 232 bar", .ml = 40000, .bar = 232 }, + + /* We'll fill in more from the dive log dynamically */ + { NULL, } +}; + +/* + * We hardcode the most common weight system types + * This is a bit odd as the weight system types don't usually encode weight + */ +struct ws_info_t ws_info[100] = { + { QT_TRANSLATE_NOOP("gettextFromC", "integrated"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "belt"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "ankle"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "backplate weight"), 0 }, + { QT_TRANSLATE_NOOP("gettextFromC", "clip-on"), 0 }, +}; + +void remove_cylinder(struct dive *dive, int idx) +{ + cylinder_t *cyl = dive->cylinder + idx; + int nr = MAX_CYLINDERS - idx - 1; + memmove(cyl, cyl + 1, nr * sizeof(*cyl)); + memset(cyl + nr, 0, sizeof(*cyl)); +} + +void remove_weightsystem(struct dive *dive, int idx) +{ + weightsystem_t *ws = dive->weightsystem + idx; + int nr = MAX_WEIGHTSYSTEMS - idx - 1; + memmove(ws, ws + 1, nr * sizeof(*ws)); + memset(ws + nr, 0, sizeof(*ws)); +} + +/* when planning a dive we need to make sure that all cylinders have a sane depth assigned + * and if we are tracking gas consumption the pressures need to be reset to start = end = workingpressure */ +void reset_cylinders(struct dive *dive, bool track_gas) +{ + int i; + pressure_t decopo2 = {.mbar = prefs.decopo2}; + + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = &dive->cylinder[i]; + if (cylinder_none(cyl)) + continue; + if (cyl->depth.mm == 0) /* if the gas doesn't give a mod, calculate based on prefs */ + cyl->depth = gas_mod(&cyl->gasmix, decopo2, dive, M_OR_FT(3,10)); + if (track_gas) + cyl->start.mbar = cyl->end.mbar = cyl->type.workingpressure.mbar; + cyl->gas_used.mliter = 0; + cyl->deco_gas_used.mliter = 0; + } +} diff --git a/subsurface-core/exif.cpp b/subsurface-core/exif.cpp new file mode 100644 index 000000000..0b1cda2bc --- /dev/null +++ b/subsurface-core/exif.cpp @@ -0,0 +1,587 @@ +#include +/************************************************************************** + exif.cpp -- A simple ISO C++ library to parse basic EXIF + information from a JPEG file. + + Copyright (c) 2010-2013 Mayank Lahiri + mlahiri@gmail.com + All rights reserved (BSD License). + + See exif.h for version history. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + -- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + -- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include +#include "dive.h" +#include "exif.h" + +using std::string; + +namespace { + // IF Entry + struct IFEntry { + // Raw fields + unsigned short tag; + unsigned short format; + unsigned data; + unsigned length; + + // Parsed fields + string val_string; + unsigned short val_16; + unsigned val_32; + double val_rational; + unsigned char val_byte; + }; + + // Helper functions + unsigned int parse32(const unsigned char *buf, bool intel) + { + if (intel) + return ((unsigned)buf[3] << 24) | + ((unsigned)buf[2] << 16) | + ((unsigned)buf[1] << 8) | + buf[0]; + + return ((unsigned)buf[0] << 24) | + ((unsigned)buf[1] << 16) | + ((unsigned)buf[2] << 8) | + buf[3]; + } + + unsigned short parse16(const unsigned char *buf, bool intel) + { + if (intel) + return ((unsigned)buf[1] << 8) | buf[0]; + return ((unsigned)buf[0] << 8) | buf[1]; + } + + string parseEXIFString(const unsigned char *buf, + const unsigned num_components, + const unsigned data, + const unsigned base, + const unsigned len) + { + string value; + if (num_components <= 4) + value.assign((const char *)&data, num_components); + else { + if (base + data + num_components <= len) + value.assign((const char *)(buf + base + data), num_components); + } + return value; + } + + double parseEXIFRational(const unsigned char *buf, bool intel) + { + double numerator = 0; + double denominator = 1; + + numerator = (double)parse32(buf, intel); + denominator = (double)parse32(buf + 4, intel); + if (denominator < 1e-20) + return 0; + return numerator / denominator; + } + + IFEntry parseIFEntry(const unsigned char *buf, + const unsigned offs, + const bool alignIntel, + const unsigned base, + const unsigned len) + { + IFEntry result; + + // Each directory entry is composed of: + // 2 bytes: tag number (data field) + // 2 bytes: data format + // 4 bytes: number of components + // 4 bytes: data value or offset to data value + result.tag = parse16(buf + offs, alignIntel); + result.format = parse16(buf + offs + 2, alignIntel); + result.length = parse32(buf + offs + 4, alignIntel); + result.data = parse32(buf + offs + 8, alignIntel); + + // Parse value in specified format + switch (result.format) { + case 1: + result.val_byte = (unsigned char)*(buf + offs + 8); + break; + case 2: + result.val_string = parseEXIFString(buf, result.length, result.data, base, len); + break; + case 3: + result.val_16 = parse16((const unsigned char *)buf + offs + 8, alignIntel); + break; + case 4: + result.val_32 = result.data; + break; + case 5: + if (base + result.data + 8 <= len) + result.val_rational = parseEXIFRational(buf + base + result.data, alignIntel); + break; + case 7: + case 9: + case 10: + break; + default: + result.tag = 0xFF; + } + return result; + } +} + +// +// Locates the EXIF segment and parses it using parseFromEXIFSegment +// +int EXIFInfo::parseFrom(const unsigned char *buf, unsigned len) +{ + // Sanity check: all JPEG files start with 0xFFD8 and end with 0xFFD9 + // This check also ensures that the user has supplied a correct value for len. + if (!buf || len < 4) + return PARSE_EXIF_ERROR_NO_EXIF; + if (buf[0] != 0xFF || buf[1] != 0xD8) + return PARSE_EXIF_ERROR_NO_JPEG; + if (buf[len - 2] != 0xFF || buf[len - 1] != 0xD9) + return PARSE_EXIF_ERROR_NO_JPEG; + clear(); + + // Scan for EXIF header (bytes 0xFF 0xE1) and do a sanity check by + // looking for bytes "Exif\0\0". The marker length data is in Motorola + // byte order, which results in the 'false' parameter to parse16(). + // The marker has to contain at least the TIFF header, otherwise the + // EXIF data is corrupt. So the minimum length specified here has to be: + // 2 bytes: section size + // 6 bytes: "Exif\0\0" string + // 2 bytes: TIFF header (either "II" or "MM" string) + // 2 bytes: TIFF magic (short 0x2a00 in Motorola byte order) + // 4 bytes: Offset to first IFD + // ========= + // 16 bytes + unsigned offs = 0; // current offset into buffer + for (offs = 0; offs < len - 1; offs++) + if (buf[offs] == 0xFF && buf[offs + 1] == 0xE1) + break; + if (offs + 4 > len) + return PARSE_EXIF_ERROR_NO_EXIF; + offs += 2; + unsigned short section_length = parse16(buf + offs, false); + if (offs + section_length > len || section_length < 16) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + + return parseFromEXIFSegment(buf + offs, len - offs); +} + +int EXIFInfo::parseFrom(const string &data) +{ + return parseFrom((const unsigned char *)data.data(), data.length()); +} + +// +// Main parsing function for an EXIF segment. +// +// PARAM: 'buf' start of the EXIF TIFF, which must be the bytes "Exif\0\0". +// PARAM: 'len' length of buffer +// +int EXIFInfo::parseFromEXIFSegment(const unsigned char *buf, unsigned len) +{ + bool alignIntel = true; // byte alignment (defined in EXIF header) + unsigned offs = 0; // current offset into buffer + if (!buf || len < 6) + return PARSE_EXIF_ERROR_NO_EXIF; + + if (!std::equal(buf, buf + 6, "Exif\0\0")) + return PARSE_EXIF_ERROR_NO_EXIF; + offs += 6; + + // Now parsing the TIFF header. The first two bytes are either "II" or + // "MM" for Intel or Motorola byte alignment. Sanity check by parsing + // the unsigned short that follows, making sure it equals 0x2a. The + // last 4 bytes are an offset into the first IFD, which are added to + // the global offset counter. For this block, we expect the following + // minimum size: + // 2 bytes: 'II' or 'MM' + // 2 bytes: 0x002a + // 4 bytes: offset to first IDF + // ----------------------------- + // 8 bytes + if (offs + 8 > len) + return PARSE_EXIF_ERROR_CORRUPT; + unsigned tiff_header_start = offs; + if (buf[offs] == 'I' && buf[offs + 1] == 'I') + alignIntel = true; + else { + if (buf[offs] == 'M' && buf[offs + 1] == 'M') + alignIntel = false; + else + return PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN; + } + this->ByteAlign = alignIntel; + offs += 2; + if (0x2a != parse16(buf + offs, alignIntel)) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + unsigned first_ifd_offset = parse32(buf + offs, alignIntel); + offs += first_ifd_offset - 4; + if (offs >= len) + return PARSE_EXIF_ERROR_CORRUPT; + + // Now parsing the first Image File Directory (IFD0, for the main image). + // An IFD consists of a variable number of 12-byte directory entries. The + // first two bytes of the IFD section contain the number of directory + // entries in the section. The last 4 bytes of the IFD contain an offset + // to the next IFD, which means this IFD must contain exactly 6 + 12 * num + // bytes of data. + if (offs + 2 > len) + return PARSE_EXIF_ERROR_CORRUPT; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + unsigned exif_sub_ifd_offset = len; + unsigned gps_sub_ifd_offset = len; + while (--num_entries >= 0) { + IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); + offs += 12; + switch (result.tag) { + case 0x102: + // Bits per sample + if (result.format == 3) + this->BitsPerSample = result.val_16; + break; + + case 0x10E: + // Image description + if (result.format == 2) + this->ImageDescription = result.val_string; + break; + + case 0x10F: + // Digicam make + if (result.format == 2) + this->Make = result.val_string; + break; + + case 0x110: + // Digicam model + if (result.format == 2) + this->Model = result.val_string; + break; + + case 0x112: + // Orientation of image + if (result.format == 3) + this->Orientation = result.val_16; + break; + + case 0x131: + // Software used for image + if (result.format == 2) + this->Software = result.val_string; + break; + + case 0x132: + // EXIF/TIFF date/time of image modification + if (result.format == 2) + this->DateTime = result.val_string; + break; + + case 0x8298: + // Copyright information + if (result.format == 2) + this->Copyright = result.val_string; + break; + + case 0x8825: + // GPS IFS offset + gps_sub_ifd_offset = tiff_header_start + result.data; + break; + + case 0x8769: + // EXIF SubIFD offset + exif_sub_ifd_offset = tiff_header_start + result.data; + break; + } + } + + // Jump to the EXIF SubIFD if it exists and parse all the information + // there. Note that it's possible that the EXIF SubIFD doesn't exist. + // The EXIF SubIFD contains most of the interesting information that a + // typical user might want. + if (exif_sub_ifd_offset + 4 <= len) { + offs = exif_sub_ifd_offset; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + while (--num_entries >= 0) { + IFEntry result = parseIFEntry(buf, offs, alignIntel, tiff_header_start, len); + switch (result.tag) { + case 0x829a: + // Exposure time in seconds + if (result.format == 5) + this->ExposureTime = result.val_rational; + break; + + case 0x829d: + // FNumber + if (result.format == 5) + this->FNumber = result.val_rational; + break; + + case 0x8827: + // ISO Speed Rating + if (result.format == 3) + this->ISOSpeedRatings = result.val_16; + break; + + case 0x9003: + // Original date and time + if (result.format == 2) + this->DateTimeOriginal = result.val_string; + break; + + case 0x9004: + // Digitization date and time + if (result.format == 2) + this->DateTimeDigitized = result.val_string; + break; + + case 0x9201: + // Shutter speed value + if (result.format == 5) + this->ShutterSpeedValue = result.val_rational; + break; + + case 0x9204: + // Exposure bias value + if (result.format == 5) + this->ExposureBiasValue = result.val_rational; + break; + + case 0x9206: + // Subject distance + if (result.format == 5) + this->SubjectDistance = result.val_rational; + break; + + case 0x9209: + // Flash used + if (result.format == 3) + this->Flash = result.data ? 1 : 0; + break; + + case 0x920a: + // Focal length + if (result.format == 5) + this->FocalLength = result.val_rational; + break; + + case 0x9207: + // Metering mode + if (result.format == 3) + this->MeteringMode = result.val_16; + break; + + case 0x9291: + // Subsecond original time + if (result.format == 2) + this->SubSecTimeOriginal = result.val_string; + break; + + case 0xa002: + // EXIF Image width + if (result.format == 4) + this->ImageWidth = result.val_32; + if (result.format == 3) + this->ImageWidth = result.val_16; + break; + + case 0xa003: + // EXIF Image height + if (result.format == 4) + this->ImageHeight = result.val_32; + if (result.format == 3) + this->ImageHeight = result.val_16; + break; + + case 0xa405: + // Focal length in 35mm film + if (result.format == 3) + this->FocalLengthIn35mm = result.val_16; + break; + } + offs += 12; + } + } + + // Jump to the GPS SubIFD if it exists and parse all the information + // there. Note that it's possible that the GPS SubIFD doesn't exist. + if (gps_sub_ifd_offset + 4 <= len) { + offs = gps_sub_ifd_offset; + int num_entries = parse16(buf + offs, alignIntel); + if (offs + 6 + 12 * num_entries > len) + return PARSE_EXIF_ERROR_CORRUPT; + offs += 2; + while (--num_entries >= 0) { + unsigned short tag = parse16(buf + offs, alignIntel); + unsigned short format = parse16(buf + offs + 2, alignIntel); + unsigned length = parse32(buf + offs + 4, alignIntel); + unsigned data = parse32(buf + offs + 8, alignIntel); + switch (tag) { + case 1: + // GPS north or south + this->GeoLocation.LatComponents.direction = *(buf + offs + 8); + if ('S' == this->GeoLocation.LatComponents.direction) + this->GeoLocation.Latitude = -this->GeoLocation.Latitude; + break; + + case 2: + // GPS latitude + if (format == 5 && length == 3) { + this->GeoLocation.LatComponents.degrees = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + this->GeoLocation.LatComponents.minutes = + parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); + this->GeoLocation.LatComponents.seconds = + parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); + this->GeoLocation.Latitude = + this->GeoLocation.LatComponents.degrees + + this->GeoLocation.LatComponents.minutes / 60 + + this->GeoLocation.LatComponents.seconds / 3600; + if ('S' == this->GeoLocation.LatComponents.direction) + this->GeoLocation.Latitude = -this->GeoLocation.Latitude; + } + break; + + case 3: + // GPS east or west + this->GeoLocation.LonComponents.direction = *(buf + offs + 8); + if ('W' == this->GeoLocation.LonComponents.direction) + this->GeoLocation.Longitude = -this->GeoLocation.Longitude; + break; + + case 4: + // GPS longitude + if (format == 5 && length == 3) { + this->GeoLocation.LonComponents.degrees = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + this->GeoLocation.LonComponents.minutes = + parseEXIFRational(buf + data + tiff_header_start + 8, alignIntel); + this->GeoLocation.LonComponents.seconds = + parseEXIFRational(buf + data + tiff_header_start + 16, alignIntel); + this->GeoLocation.Longitude = + this->GeoLocation.LonComponents.degrees + + this->GeoLocation.LonComponents.minutes / 60 + + this->GeoLocation.LonComponents.seconds / 3600; + if ('W' == this->GeoLocation.LonComponents.direction) + this->GeoLocation.Longitude = -this->GeoLocation.Longitude; + } + break; + + case 5: + // GPS altitude reference (below or above sea level) + this->GeoLocation.AltitudeRef = *(buf + offs + 8); + if (1 == this->GeoLocation.AltitudeRef) + this->GeoLocation.Altitude = -this->GeoLocation.Altitude; + break; + + case 6: + // GPS altitude reference + if (format == 5) { + this->GeoLocation.Altitude = + parseEXIFRational(buf + data + tiff_header_start, alignIntel); + if (1 == this->GeoLocation.AltitudeRef) + this->GeoLocation.Altitude = -this->GeoLocation.Altitude; + } + break; + } + offs += 12; + } + } + + return PARSE_EXIF_SUCCESS; +} + +void EXIFInfo::clear() +{ + // Strings + ImageDescription.clear(); + Make.clear(); + Model.clear(); + Software.clear(); + DateTime.clear(); + DateTimeOriginal.clear(); + DateTimeDigitized.clear(); + SubSecTimeOriginal.clear(); + Copyright.clear(); + + // Shorts / unsigned / double + ByteAlign = 0; + Orientation = 0; + + BitsPerSample = 0; + ExposureTime = 0; + FNumber = 0; + ISOSpeedRatings = 0; + ShutterSpeedValue = 0; + ExposureBiasValue = 0; + SubjectDistance = 0; + FocalLength = 0; + FocalLengthIn35mm = 0; + Flash = 0; + MeteringMode = 0; + ImageWidth = 0; + ImageHeight = 0; + + // Geolocation + GeoLocation.Latitude = 0; + GeoLocation.Longitude = 0; + GeoLocation.Altitude = 0; + GeoLocation.AltitudeRef = 0; + GeoLocation.LatComponents.degrees = 0; + GeoLocation.LatComponents.minutes = 0; + GeoLocation.LatComponents.seconds = 0; + GeoLocation.LatComponents.direction = 0; + GeoLocation.LonComponents.degrees = 0; + GeoLocation.LonComponents.minutes = 0; + GeoLocation.LonComponents.seconds = 0; + GeoLocation.LonComponents.direction = 0; +} + +time_t EXIFInfo::epoch() +{ + struct tm tm; + int year, month, day, hour, min, sec; + + if (DateTimeOriginal.size()) + sscanf(DateTimeOriginal.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); + else + sscanf(DateTime.c_str(), "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour, &min, &sec); + tm.tm_year = year; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + return (utc_mktime(&tm)); +} diff --git a/subsurface-core/exif.h b/subsurface-core/exif.h new file mode 100644 index 000000000..0fb3a7d4a --- /dev/null +++ b/subsurface-core/exif.h @@ -0,0 +1,147 @@ +/************************************************************************** + exif.h -- A simple ISO C++ library to parse basic EXIF + information from a JPEG file. + + Based on the description of the EXIF file format at: + -- http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html + -- http://www.media.mit.edu/pia/Research/deepview/exif.html + -- http://www.exif.org/Exif2-2.PDF + + Copyright (c) 2010-2013 Mayank Lahiri + mlahiri@gmail.com + All rights reserved. + + VERSION HISTORY: + ================ + + 2.1: Released July 2013 + -- fixed a bug where JPEGs without an EXIF SubIFD would not be parsed + -- fixed a bug in parsing GPS coordinate seconds + -- fixed makefile bug + -- added two pathological test images from Matt Galloway + http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/ + -- split main parsing routine for easier integration into Firefox + + 2.0: Released February 2013 + -- complete rewrite + -- no new/delete + -- added GPS support + + 1.0: Released 2010 + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + -- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + -- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef EXIF_H +#define EXIF_H + +#include + +// +// Class responsible for storing and parsing EXIF information from a JPEG blob +// +class EXIFInfo { +public: + // Parsing function for an entire JPEG image buffer. + // + // PARAM 'data': A pointer to a JPEG image. + // PARAM 'length': The length of the JPEG image. + // RETURN: PARSE_EXIF_SUCCESS (0) on succes with 'result' filled out + // error code otherwise, as defined by the PARSE_EXIF_ERROR_* macros + int parseFrom(const unsigned char *data, unsigned length); + int parseFrom(const std::string &data); + + // Parsing function for an EXIF segment. This is used internally by parseFrom() + // but can be called for special cases where only the EXIF section is + // available (i.e., a blob starting with the bytes "Exif\0\0"). + int parseFromEXIFSegment(const unsigned char *buf, unsigned len); + + // Set all data members to default values. + void clear(); + + // Data fields filled out by parseFrom() + char ByteAlign; // 0 = Motorola byte alignment, 1 = Intel + std::string ImageDescription; // Image description + std::string Make; // Camera manufacturer's name + std::string Model; // Camera model + unsigned short Orientation; // Image orientation, start of data corresponds to + // 0: unspecified in EXIF data + // 1: upper left of image + // 3: lower right of image + // 6: upper right of image + // 8: lower left of image + // 9: undefined + unsigned short BitsPerSample; // Number of bits per component + std::string Software; // Software used + std::string DateTime; // File change date and time + std::string DateTimeOriginal; // Original file date and time (may not exist) + std::string DateTimeDigitized; // Digitization date and time (may not exist) + std::string SubSecTimeOriginal; // Sub-second time that original picture was taken + std::string Copyright; // File copyright information + double ExposureTime; // Exposure time in seconds + double FNumber; // F/stop + unsigned short ISOSpeedRatings; // ISO speed + double ShutterSpeedValue; // Shutter speed (reciprocal of exposure time) + double ExposureBiasValue; // Exposure bias value in EV + double SubjectDistance; // Distance to focus point in meters + double FocalLength; // Focal length of lens in millimeters + unsigned short FocalLengthIn35mm; // Focal length in 35mm film + char Flash; // 0 = no flash, 1 = flash used + unsigned short MeteringMode; // Metering mode + // 1: average + // 2: center weighted average + // 3: spot + // 4: multi-spot + // 5: multi-segment + unsigned ImageWidth; // Image width reported in EXIF data + unsigned ImageHeight; // Image height reported in EXIF data + struct Geolocation_t + { // GPS information embedded in file + double Latitude; // Image latitude expressed as decimal + double Longitude; // Image longitude expressed as decimal + double Altitude; // Altitude in meters, relative to sea level + char AltitudeRef; // 0 = above sea level, -1 = below sea level + struct Coord_t { + double degrees; + double minutes; + double seconds; + char direction; + } LatComponents, LonComponents; // Latitude, Longitude expressed in deg/min/sec + } GeoLocation; + EXIFInfo() + { + clear(); + } + + time_t epoch(); +}; + +// Parse was successful +#define PARSE_EXIF_SUCCESS 0 +// No JPEG markers found in buffer, possibly invalid JPEG file +#define PARSE_EXIF_ERROR_NO_JPEG 1982 +// No EXIF header found in JPEG file. +#define PARSE_EXIF_ERROR_NO_EXIF 1983 +// Byte alignment specified in EXIF file was unknown (not Motorola or Intel). +#define PARSE_EXIF_ERROR_UNKNOWN_BYTEALIGN 1984 +// EXIF header was found, but data was corrupted. +#define PARSE_EXIF_ERROR_CORRUPT 1985 + +#endif // EXIF_H diff --git a/subsurface-core/file.c b/subsurface-core/file.c new file mode 100644 index 000000000..c4032c1f2 --- /dev/null +++ b/subsurface-core/file.c @@ -0,0 +1,1066 @@ +#include +#include +#include +#include +#include +#include +#include "gettext.h" +#include +#include + +#include "dive.h" +#include "file.h" +#include "git-access.h" +#include "qthelperfromc.h" + +/* For SAMPLE_* */ +#include + +/* to check XSLT version number */ +#include + +/* Crazy windows sh*t */ +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +int readfile(const char *filename, struct memblock *mem) +{ + int ret, fd; + struct stat st; + char *buf; + + mem->buffer = NULL; + mem->size = 0; + + fd = subsurface_open(filename, O_RDONLY | O_BINARY, 0); + if (fd < 0) + return fd; + ret = fstat(fd, &st); + if (ret < 0) + goto out; + ret = -EINVAL; + if (!S_ISREG(st.st_mode)) + goto out; + ret = 0; + if (!st.st_size) + goto out; + buf = malloc(st.st_size + 1); + ret = -1; + errno = ENOMEM; + if (!buf) + goto out; + mem->buffer = buf; + mem->size = st.st_size; + ret = read(fd, buf, mem->size); + if (ret < 0) + goto free; + buf[ret] = 0; + if (ret == mem->size) + goto out; + errno = EIO; + ret = -1; +free: + free(mem->buffer); + mem->buffer = NULL; + mem->size = 0; +out: + close(fd); + return ret; +} + + +static void zip_read(struct zip_file *file, const char *filename) +{ + int size = 1024, n, read = 0; + char *mem = malloc(size); + + while ((n = zip_fread(file, mem + read, size - read)) > 0) { + read += n; + size = read * 3 / 2; + mem = realloc(mem, size); + } + mem[read] = 0; + (void) parse_xml_buffer(filename, mem, read, &dive_table, NULL); + free(mem); +} + +int try_to_open_zip(const char *filename, struct memblock *mem) +{ + int success = 0; + /* Grr. libzip needs to re-open the file, it can't take a buffer */ + struct zip *zip = subsurface_zip_open_readonly(filename, ZIP_CHECKCONS, NULL); + + if (zip) { + int index; + for (index = 0;; index++) { + struct zip_file *file = zip_fopen_index(zip, index, 0); + if (!file) + break; + /* skip parsing the divelogs.de pictures */ + if (strstr(zip_get_name(zip, index, 0), "pictures/")) + continue; + zip_read(file, filename); + zip_fclose(file); + success++; + } + subsurface_zip_close(zip); + } + return success; +} + +static int try_to_xslt_open_csv(const char *filename, struct memblock *mem, const char *tag) +{ + char *buf; + + if (mem->size == 0 && readfile(filename, mem) < 0) + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + + /* Surround the CSV file content with XML tags to enable XSLT + * parsing + * + * Tag markers take: strlen("<>") = 5 + */ + buf = realloc(mem->buffer, mem->size + 7 + strlen(tag) * 2); + if (buf != NULL) { + char *starttag = NULL; + char *endtag = NULL; + + starttag = malloc(3 + strlen(tag)); + endtag = malloc(5 + strlen(tag)); + + if (starttag == NULL || endtag == NULL) { + /* this is fairly silly - so the malloc fails, but we strdup the error? + * let's complete the silliness by freeing the two pointers in case one malloc succeeded + * and the other one failed - this will make static analysis tools happy */ + free(starttag); + free(endtag); + free(buf); + return report_error("Memory allocation failed in %s", __func__); + } + + sprintf(starttag, "<%s>", tag); + sprintf(endtag, "\n", tag); + + memmove(buf + 2 + strlen(tag), buf, mem->size); + memcpy(buf, starttag, 2 + strlen(tag)); + memcpy(buf + mem->size + 2 + strlen(tag), endtag, 5 + strlen(tag)); + mem->size += (6 + 2 * strlen(tag)); + mem->buffer = buf; + + free(starttag); + free(endtag); + } else { + free(mem->buffer); + return report_error("realloc failed in %s", __func__); + } + + return 0; +} + +int db_test_func(void *param, int columns, char **data, char **column) +{ + return *data[0] == '0'; +} + + +static int try_to_open_db(const char *filename, struct memblock *mem) +{ + sqlite3 *handle; + char dm4_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%ProfileBlob%'"; + char dm5_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%SampleBlob%'"; + char shearwater_test[] = "select count(*) from sqlite_master where type='table' and name='system' and sql like '%dbVersion%'"; + char cobalt_test[] = "select count(*) from sqlite_master where type='table' and name='TrackPoints' and sql like '%DepthPressure%'"; + char divinglog_test[] = "select count(*) from sqlite_master where type='table' and name='DBInfo' and sql like '%PrgName%'"; + int retval; + + retval = sqlite3_open(filename, &handle); + + if (retval) { + fprintf(stderr, "Database connection failed '%s'.\n", filename); + return 1; + } + + /* Testing if DB schema resembles Suunto DM5 database format */ + retval = sqlite3_exec(handle, dm5_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_dm5_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Suunto DM4 database format */ + retval = sqlite3_exec(handle, dm4_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_dm4_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Shearwater database format */ + retval = sqlite3_exec(handle, shearwater_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_shearwater_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Atomic Cobalt database format */ + retval = sqlite3_exec(handle, cobalt_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_cobalt_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + /* Testing if DB schema resembles Divinglog database format */ + retval = sqlite3_exec(handle, divinglog_test, &db_test_func, 0, NULL); + if (!retval) { + retval = parse_divinglog_buffer(handle, filename, mem->buffer, mem->size, &dive_table); + sqlite3_close(handle); + return retval; + } + + sqlite3_close(handle); + + return retval; +} + +timestamp_t parse_date(const char *date) +{ + int hour, min, sec; + struct tm tm; + char *p; + + memset(&tm, 0, sizeof(tm)); + tm.tm_mday = strtol(date, &p, 10); + if (tm.tm_mday < 1 || tm.tm_mday > 31) + return 0; + for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) { + if (!memcmp(p, monthname(tm.tm_mon), 3)) + break; + } + if (tm.tm_mon > 11) + return 0; + date = p + 3; + tm.tm_year = strtol(date, &p, 10); + if (date == p) + return 0; + if (tm.tm_year < 70) + tm.tm_year += 2000; + if (tm.tm_year < 100) + tm.tm_year += 1900; + if (sscanf(p, "%d:%d:%d", &hour, &min, &sec) != 3) + return 0; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + return utc_mktime(&tm); +} + +enum csv_format { + CSV_DEPTH, + CSV_TEMP, + CSV_PRESSURE, + POSEIDON_DEPTH, + POSEIDON_TEMP, + POSEIDON_SETPOINT, + POSEIDON_SENSOR1, + POSEIDON_SENSOR2, + POSEIDON_PRESSURE, + POSEIDON_O2CYLINDER, + POSEIDON_NDL, + POSEIDON_CEILING +}; + +static void add_sample_data(struct sample *sample, enum csv_format type, double val) +{ + switch (type) { + case CSV_DEPTH: + sample->depth.mm = feet_to_mm(val); + break; + case CSV_TEMP: + sample->temperature.mkelvin = F_to_mkelvin(val); + break; + case CSV_PRESSURE: + sample->cylinderpressure.mbar = psi_to_mbar(val * 4); + break; + case POSEIDON_DEPTH: + sample->depth.mm = val * 0.5 *1000; + break; + case POSEIDON_TEMP: + sample->temperature.mkelvin = C_to_mkelvin(val * 0.2); + break; + case POSEIDON_SETPOINT: + sample->setpoint.mbar = val * 10; + break; + case POSEIDON_SENSOR1: + sample->o2sensor[0].mbar = val * 10; + break; + case POSEIDON_SENSOR2: + sample->o2sensor[1].mbar = val * 10; + break; + case POSEIDON_PRESSURE: + sample->cylinderpressure.mbar = val * 1000; + break; + case POSEIDON_O2CYLINDER: + sample->o2cylinderpressure.mbar = val * 1000; + break; + case POSEIDON_NDL: + sample->ndl.seconds = val * 60; + break; + case POSEIDON_CEILING: + sample->stopdepth.mm = val * 1000; + break; + } +} + +/* + * Cochran comma-separated values: depth in feet, temperature in F, pressure in psi. + * + * They start with eight comma-separated fields like: + * + * filename: {C:\Analyst4\can\T036785.can},{C:\Analyst4\can\K031892.can} + * divenr: %d + * datetime: {03Sep11 16:37:22},{15Dec11 18:27:02} + * ??: 1 + * serialnr??: {CCI134},{CCI207} + * computer??: {GeminiII},{CommanderIII} + * computer??: {GeminiII},{CommanderIII} + * ??: 1 + * + * Followed by the data values (all comma-separated, all one long line). + */ +static int try_to_open_csv(const char *filename, struct memblock *mem, enum csv_format type) +{ + char *p = mem->buffer; + char *header[8]; + int i, time; + timestamp_t date; + struct dive *dive; + struct divecomputer *dc; + + for (i = 0; i < 8; i++) { + header[i] = p; + p = strchr(p, ','); + if (!p) + return 0; + p++; + } + + date = parse_date(header[2]); + if (!date) + return 0; + + dive = alloc_dive(); + dive->when = date; + dive->number = atoi(header[1]); + dc = &dive->dc; + + time = 0; + for (;;) { + char *end; + double val; + struct sample *sample; + + errno = 0; + val = strtod(p, &end); // FIXME == localization issue + if (end == p) + break; + if (errno) + break; + + sample = prepare_sample(dc); + sample->time.seconds = time; + add_sample_data(sample, type, val); + finish_sample(dc); + + time++; + dc->duration.seconds = time; + if (*end != ',') + break; + p = end + 1; + } + record_dive(dive); + return 1; +} + +static int open_by_filename(const char *filename, const char *fmt, struct memblock *mem) +{ + // hack to be able to provide a comment for the translated string + static char *csv_warning = QT_TRANSLATE_NOOP3("gettextFromC", + "Cannot open CSV file %s; please use Import log file dialog", + "'Import log file' should be the same text as corresponding label in Import menu"); + + /* Suunto Dive Manager files: SDE, ZIP; divelogs.de files: DLD */ + if (!strcasecmp(fmt, "SDE") || !strcasecmp(fmt, "ZIP") || !strcasecmp(fmt, "DLD")) + return try_to_open_zip(filename, mem); + + /* CSV files */ + if (!strcasecmp(fmt, "CSV")) + return report_error(translate("gettextFromC", csv_warning), filename); + /* Truly nasty intentionally obfuscated Cochran Anal software */ + if (!strcasecmp(fmt, "CAN")) + return try_to_open_cochran(filename, mem); + /* Cochran export comma-separated-value files */ + if (!strcasecmp(fmt, "DPT")) + return try_to_open_csv(filename, mem, CSV_DEPTH); + if (!strcasecmp(fmt, "LVD")) + return try_to_open_liquivision(filename, mem); + if (!strcasecmp(fmt, "TMP")) + return try_to_open_csv(filename, mem, CSV_TEMP); + if (!strcasecmp(fmt, "HP1")) + return try_to_open_csv(filename, mem, CSV_PRESSURE); + + return 0; +} + +static int parse_file_buffer(const char *filename, struct memblock *mem) +{ + int ret; + char *fmt = strrchr(filename, '.'); + if (fmt && (ret = open_by_filename(filename, fmt + 1, mem)) != 0) + return ret; + + if (!mem->size || !mem->buffer) + return report_error("Out of memory parsing file %s\n", filename); + + return parse_xml_buffer(filename, mem->buffer, mem->size, &dive_table, NULL); +} + +int parse_file(const char *filename) +{ + struct git_repository *git; + const char *branch; + struct memblock mem; + char *fmt; + int ret; + + git = is_git_repository(filename, &branch, NULL, false); + if (prefs.cloud_git_url && + strstr(filename, prefs.cloud_git_url) + && git == dummy_git_repository) + /* opening the cloud storage repository failed for some reason + * give up here and don't send errors about git repositories */ + return 0; + + if (git && !git_load_dives(git, branch)) + return 0; + + if ((ret = readfile(filename, &mem)) < 0) { + /* we don't want to display an error if this was the default file or the cloud storage */ + if ((prefs.default_filename && !strcmp(filename, prefs.default_filename)) || + isCloudUrl(filename)) + return 0; + + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + } else if (ret == 0) { + return report_error(translate("gettextFromC", "Empty file '%s'"), filename); + } + + fmt = strrchr(filename, '.'); + if (fmt && (!strcasecmp(fmt + 1, "DB") || !strcasecmp(fmt + 1, "BAK") || !strcasecmp(fmt + 1, "SQL"))) { + if (!try_to_open_db(filename, &mem)) { + free(mem.buffer); + return 0; + } + } + + /* Divesoft Freedom */ + if (fmt && (!strcasecmp(fmt + 1, "DLF"))) { + if (!parse_dlf_buffer(mem.buffer, mem.size)) { + free(mem.buffer); + return 0; + } + return -1; + } + + /* DataTrak/Wlog */ + if (fmt && !strcasecmp(fmt + 1, "LOG")) { + datatrak_import(filename, &dive_table); + return 0; + } + + /* OSTCtools */ + if (fmt && (!strcasecmp(fmt + 1, "DIVE"))) { + ostctools_import(filename, &dive_table); + return 0; + } + + ret = parse_file_buffer(filename, &mem); + free(mem.buffer); + return ret; +} + +#define MATCH(buffer, pattern) \ + memcmp(buffer, pattern, strlen(pattern)) + +char *parse_mkvi_value(const char *haystack, const char *needle) +{ + char *lineptr, *valueptr, *endptr, *ret = NULL; + + if ((lineptr = strstr(haystack, needle)) != NULL) { + if ((valueptr = strstr(lineptr, ": ")) != NULL) { + valueptr += 2; + } + if ((endptr = strstr(lineptr, "\n")) != NULL) { + char terminator = '\n'; + if (*(endptr - 1) == '\r') { + --endptr; + terminator = '\r'; + } + *endptr = 0; + ret = copy_string(valueptr); + *endptr = terminator; + + } + } + return ret; +} + +char *next_mkvi_key(const char *haystack) +{ + char *valueptr, *endptr, *ret = NULL; + + if ((valueptr = strstr(haystack, "\n")) != NULL) { + valueptr += 1; + if ((endptr = strstr(valueptr, ": ")) != NULL) { + *endptr = 0; + ret = strdup(valueptr); + *endptr = ':'; + } + } + return ret; +} + +int parse_txt_file(const char *filename, const char *csv) +{ + struct memblock memtxt, memcsv; + + if (readfile(filename, &memtxt) < 0) { + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + } + + /* + * MkVI stores some information in .txt file but the whole profile and events are stored in .csv file. First + * make sure the input .txt looks like proper MkVI file, then start parsing the .csv. + */ + if (MATCH(memtxt.buffer, "MkVI_Config") == 0) { + int d, m, y, he; + int hh = 0, mm = 0, ss = 0; + int prev_depth = 0, cur_sampletime = 0, prev_setpoint = -1, prev_ndl = -1; + bool has_depth = false, has_setpoint = false, has_ndl = false; + char *lineptr, *key, *value; + int o2cylinder_pressure = 0, cylinder_pressure = 0, cur_cylinder_index = 0; + unsigned int prev_time = 0; + + struct dive *dive; + struct divecomputer *dc; + struct tm cur_tm; + + value = parse_mkvi_value(memtxt.buffer, "Dive started at"); + if (sscanf(value, "%d-%d-%d %d:%d:%d", &y, &m, &d, &hh, &mm, &ss) != 6) { + free(value); + return -1; + } + free(value); + cur_tm.tm_year = y; + cur_tm.tm_mon = m - 1; + cur_tm.tm_mday = d; + cur_tm.tm_hour = hh; + cur_tm.tm_min = mm; + cur_tm.tm_sec = ss; + + dive = alloc_dive(); + dive->when = utc_mktime(&cur_tm);; + dive->dc.model = strdup("Poseidon MkVI Discovery"); + value = parse_mkvi_value(memtxt.buffer, "Rig Serial number"); + dive->dc.deviceid = atoi(value); + free(value); + dive->dc.divemode = CCR; + dive->dc.no_o2sensors = 2; + + dive->cylinder[cur_cylinder_index].cylinder_use = OXYGEN; + dive->cylinder[cur_cylinder_index].type.size.mliter = 3000; + dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000; + dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6"); + dive->cylinder[cur_cylinder_index].gasmix.o2.permille = 1000; + cur_cylinder_index++; + + dive->cylinder[cur_cylinder_index].cylinder_use = DILUENT; + dive->cylinder[cur_cylinder_index].type.size.mliter = 3000; + dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000; + dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6"); + value = parse_mkvi_value(memtxt.buffer, "Helium percentage"); + he = atoi(value); + free(value); + value = parse_mkvi_value(memtxt.buffer, "Nitrogen percentage"); + dive->cylinder[cur_cylinder_index].gasmix.o2.permille = (100 - atoi(value) - he) * 10; + free(value); + dive->cylinder[cur_cylinder_index].gasmix.he.permille = he * 10; + cur_cylinder_index++; + + lineptr = strstr(memtxt.buffer, "Dive started at"); + while (lineptr && *lineptr && (lineptr = strchr(lineptr, '\n')) && ++lineptr) { + key = next_mkvi_key(lineptr); + if (!key) + break; + value = parse_mkvi_value(lineptr, key); + if (!value) { + free(key); + break; + } + add_extra_data(&dive->dc, key, value); + free(key); + free(value); + } + dc = &dive->dc; + + /* + * Read samples from the CSV file. A sample contains all the lines with same timestamp. The CSV file has + * the following format: + * + * timestamp, type, value + * + * And following fields are of interest to us: + * + * 6 sensor1 + * 7 sensor2 + * 8 depth + * 13 o2 tank pressure + * 14 diluent tank pressure + * 20 o2 setpoint + * 39 water temp + */ + + if (readfile(csv, &memcsv) < 0) { + free(dive); + return report_error(translate("gettextFromC", "Poseidon import failed: unable to read '%s'"), csv); + } + lineptr = memcsv.buffer; + for (;;) { + struct sample *sample; + int type; + int value; + int sampletime; + int gaschange = 0; + + /* Collect all the information for one sample */ + sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value); + + has_depth = false; + has_setpoint = false; + has_ndl = false; + sample = prepare_sample(dc); + + /* + * There was a bug in MKVI download tool that resulted in erroneous sample + * times. This fix should work similarly as the vendor's own. + */ + + sample->time.seconds = cur_sampletime < 0xFFFF * 3 / 4 ? cur_sampletime : prev_time; + prev_time = sample->time.seconds; + + do { + int i = sscanf(lineptr, "%d,%d,%d", &sampletime, &type, &value); + switch (i) { + case 3: + switch (type) { + case 0: + //Mouth piece position event: 0=OC, 1=CC, 2=UN, 3=NC + switch (value) { + case 0: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position OC")); + break; + case 1: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position CC")); + break; + case 2: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position unknown")); + break; + case 3: + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Mouth piece position not connected")); + break; + } + break; + case 3: + //Power Off event + add_event(dc, cur_sampletime, 0, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Power off")); + break; + case 4: + //Battery State of Charge in % +#ifdef SAMPLE_EVENT_BATTERY + add_event(dc, cur_sampletime, SAMPLE_EVENT_BATTERY, 0, + value, QT_TRANSLATE_NOOP("gettextFromC", "battery")); +#endif + break; + case 6: + //PO2 Cell 1 Average + add_sample_data(sample, POSEIDON_SENSOR1, value); + break; + case 7: + //PO2 Cell 2 Average + add_sample_data(sample, POSEIDON_SENSOR2, value); + break; + case 8: + //Depth * 2 + has_depth = true; + prev_depth = value; + add_sample_data(sample, POSEIDON_DEPTH, value); + break; + //9 Max Depth * 2 + //10 Ascent/Descent Rate * 2 + case 11: + //Ascent Rate Alert >10 m/s + add_event(dc, cur_sampletime, SAMPLE_EVENT_ASCENT, 0, 0, + QT_TRANSLATE_NOOP("gettextFromC", "ascent")); + break; + case 13: + //O2 Tank Pressure + add_sample_data(sample, POSEIDON_O2CYLINDER, value); + if (!o2cylinder_pressure) { + dive->cylinder[0].sample_start.mbar = value * 1000; + o2cylinder_pressure = value; + } else + o2cylinder_pressure = value; + break; + case 14: + //Diluent Tank Pressure + add_sample_data(sample, POSEIDON_PRESSURE, value); + if (!cylinder_pressure) { + dive->cylinder[1].sample_start.mbar = value * 1000; + cylinder_pressure = value; + } else + cylinder_pressure = value; + break; + //16 Remaining dive time #1? + //17 related to O2 injection + case 20: + //PO2 Setpoint + has_setpoint = true; + prev_setpoint = value; + add_sample_data(sample, POSEIDON_SETPOINT, value); + break; + case 22: + //End of O2 calibration Event: 0 = OK, 2 = Failed, rest of dive setpoint 1.0 + if (value == 2) + add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration failed")); + add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_END, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration")); + break; + case 25: + //25 Max Ascent depth + add_sample_data(sample, POSEIDON_CEILING, value); + break; + case 31: + //Start of O2 calibration Event + add_event(dc, cur_sampletime, 0, SAMPLE_FLAGS_BEGIN, 0, + QT_TRANSLATE_NOOP("gettextFromC", "Oâ‚‚ calibration")); + break; + case 37: + //Remaining dive time #2? + has_ndl = true; + prev_ndl = value; + add_sample_data(sample, POSEIDON_NDL, value); + break; + case 39: + // Water Temperature in Celcius + add_sample_data(sample, POSEIDON_TEMP, value); + break; + case 85: + //He diluent part in % + gaschange += value << 16; + break; + case 86: + //O2 diluent part in % + gaschange += value; + break; + //239 Unknown, maybe PO2 at sensor validation? + //240 Unknown, maybe PO2 at sensor validation? + //247 Unknown, maybe PO2 Cell 1 during pressure test + //248 Unknown, maybe PO2 Cell 2 during pressure test + //250 PO2 Cell 1 + //251 PO2 Cell 2 + default: + break; + } /* sample types */ + break; + case EOF: + break; + default: + printf("Unable to parse input: %s\n", lineptr); + break; + } + + lineptr = strchr(lineptr, '\n'); + if (!lineptr || !*lineptr) + break; + lineptr++; + + /* Grabbing next sample time */ + sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value); + } while (sampletime == cur_sampletime); + + if (gaschange) + add_event(dc, cur_sampletime, SAMPLE_EVENT_GASCHANGE2, 0, gaschange, + QT_TRANSLATE_NOOP("gettextFromC", "gaschange")); + if (!has_depth) + add_sample_data(sample, POSEIDON_DEPTH, prev_depth); + if (!has_setpoint && prev_setpoint >= 0) + add_sample_data(sample, POSEIDON_SETPOINT, prev_setpoint); + if (!has_ndl && prev_ndl >= 0) + add_sample_data(sample, POSEIDON_NDL, prev_ndl); + if (cylinder_pressure) + dive->cylinder[1].sample_end.mbar = cylinder_pressure * 1000; + if (o2cylinder_pressure) + dive->cylinder[0].sample_end.mbar = o2cylinder_pressure * 1000; + finish_sample(dc); + + if (!lineptr || !*lineptr) + break; + } + record_dive(dive); + return 1; + } else { + return report_error(translate("gettextFromC", "No matching DC found for file '%s'"), csv); + } + + return 0; +} + +#define MAXCOLDIGITS 10 +#define DATESTR 9 +#define TIMESTR 6 + +int parse_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate) +{ + int ret, i; + struct memblock mem; + time_t now; + struct tm *timep = NULL; + char tmpbuf[MAXCOLDIGITS]; + + /* Increase the limits for recursion and variables on XSLT + * parsing */ + xsltMaxDepth = 30000; +#if LIBXSLT_VERSION > 10126 + xsltMaxVars = 150000; +#endif + + if (filename == NULL) + return report_error("No CSV filename"); + + time(&now); + timep = localtime(&now); + + strftime(tmpbuf, MAXCOLDIGITS, "%Y%m%d", timep); + params[pnr++] = "date"; + params[pnr++] = strdup(tmpbuf); + + /* As the parameter is numeric, we need to ensure that the leading zero + * is not discarded during the transform, thus prepend time with 1 */ + + strftime(tmpbuf, MAXCOLDIGITS, "1%H%M", timep); + params[pnr++] = "time"; + params[pnr++] = strdup(tmpbuf); + params[pnr++] = NULL; + + mem.size = 0; + if (try_to_xslt_open_csv(filename, &mem, csvtemplate)) + return -1; + + /* + * Lets print command line for manual testing with xsltproc if + * verbosity level is high enough. The printed line needs the + * input file added as last parameter. + */ + + if (verbose >= 2) { + fprintf(stderr, "(echo ''; cat %s;echo '') | xsltproc ", filename); + for (i=0; params[i]; i+=2) + fprintf(stderr, "--stringparam %s %s ", params[i], params[i+1]); + fprintf(stderr, "%s/xslt/csv2xml.xslt -\n", SUBSURFACE_SOURCE); + } + + ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); + + free(mem.buffer); + for (i = 0; params[i]; i += 2) + free(params[i + 1]); + + return ret; +} + +#define SBPARAMS 40 +int parse_seabear_csv_file(const char *filename, char **params, int pnr, const char *csvtemplate) +{ + int ret, i; + struct memblock mem; + time_t now; + struct tm *timep = NULL; + char *ptr, *ptr_old = NULL; + char *NL = NULL; + char tmpbuf[MAXCOLDIGITS]; + + /* Increase the limits for recursion and variables on XSLT + * parsing */ + xsltMaxDepth = 30000; +#if LIBXSLT_VERSION > 10126 + xsltMaxVars = 150000; +#endif + + time(&now); + timep = localtime(&now); + + strftime(tmpbuf, MAXCOLDIGITS, "%Y%m%d", timep); + params[pnr++] = "date"; + params[pnr++] = strdup(tmpbuf); + + /* As the parameter is numeric, we need to ensure that the leading zero + * is not discarded during the transform, thus prepend time with 1 */ + strftime(tmpbuf, MAXCOLDIGITS, "1%H%M", timep); + params[pnr++] = "time"; + params[pnr++] = strdup(tmpbuf); + + + if (filename == NULL) + return report_error("No CSV filename"); + + if (readfile(filename, &mem) < 0) + return report_error(translate("gettextFromC", "Failed to read '%s'"), filename); + + /* Determine NL (new line) character and the start of CSV data */ + ptr = mem.buffer; + while ((ptr = strstr(ptr, "\r\n\r\n")) != NULL) { + ptr_old = ptr; + ptr += 1; + NL = "\r\n"; + } + + if (!ptr_old) { + ptr = mem.buffer; + while ((ptr = strstr(ptr, "\n\n")) != NULL) { + ptr_old = ptr; + ptr += 1; + NL = "\n"; + } + ptr_old += 2; + } else + ptr_old += 4; + + /* + * If file does not contain empty lines, it is not a valid + * Seabear CSV file. + */ + if (NL == NULL) + return -1; + + /* + * On my current sample of Seabear DC log file, the date is + * without any identifier. Thus we must search for the previous + * line and step through from there. That is the line after + * Serial number. + */ + ptr = strstr(mem.buffer, "Serial number:"); + if (ptr) + ptr = strstr(ptr, NL); + + /* + * Write date and time values to params array, if available in + * the CSV header + */ + + if (ptr) { + ptr += strlen(NL) + 2; + /* + * pnr is the index of NULL on the params as filled by + * the init function. The two last entries should be + * date and time. Here we overwrite them with the data + * from the CSV header. + */ + + memcpy(params[pnr - 3], ptr, 4); + memcpy(params[pnr - 3] + 4, ptr + 5, 2); + memcpy(params[pnr - 3] + 6, ptr + 8, 2); + params[pnr - 3][8] = 0; + + memcpy(params[pnr - 1] + 1, ptr + 11, 2); + memcpy(params[pnr - 1] + 3, ptr + 14, 2); + params[pnr - 1][5] = 0; + } + + params[pnr++] = NULL; + + /* Move the CSV data to the start of mem buffer */ + memmove(mem.buffer, ptr_old, mem.size - (ptr_old - (char*)mem.buffer)); + mem.size = (int)mem.size - (ptr_old - (char*)mem.buffer); + + if (try_to_xslt_open_csv(filename, &mem, csvtemplate)) + return -1; + + /* + * Lets print command line for manual testing with xsltproc if + * verbosity level is high enough. The printed line needs the + * input file added as last parameter. + */ + + if (verbose >= 2) { + fprintf(stderr, "xsltproc "); + for (i=0; params[i]; i+=2) + fprintf(stderr, "--stringparam %s %s ", params[i], params[i+1]); + fprintf(stderr, "xslt/csv2xml.xslt\n"); + } + + ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); + free(mem.buffer); + for (i = 0; params[i]; i += 2) + free(params[i + 1]); + + return ret; +} + +int parse_manual_file(const char *filename, char **params, int pnr) +{ + struct memblock mem; + time_t now; + struct tm *timep; + char curdate[9]; + char curtime[6]; + int ret, i; + + + time(&now); + timep = localtime(&now); + strftime(curdate, DATESTR, "%Y%m%d", timep); + + /* As the parameter is numeric, we need to ensure that the leading zero + * is not discarded during the transform, thus prepend time with 1 */ + strftime(curtime, TIMESTR, "1%H%M", timep); + + + params[pnr++] = strdup("date"); + params[pnr++] = strdup(curdate); + params[pnr++] = strdup("time"); + params[pnr++] = strdup(curtime); + params[pnr++] = NULL; + + if (filename == NULL) + return report_error("No manual CSV filename"); + + mem.size = 0; + if (try_to_xslt_open_csv(filename, &mem, "manualCSV")) + return -1; + + ret = parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params); + + free(mem.buffer); + for (i = 0; i < pnr - 2; ++i) + free(params[i]); + return ret; +} diff --git a/subsurface-core/file.h b/subsurface-core/file.h new file mode 100644 index 000000000..855109960 --- /dev/null +++ b/subsurface-core/file.h @@ -0,0 +1,24 @@ +#ifndef FILE_H +#define FILE_H + +struct memblock { + void *buffer; + size_t size; +}; + +extern int try_to_open_cochran(const char *filename, struct memblock *mem); +extern int try_to_open_liquivision(const char *filename, struct memblock *mem); +extern void datatrak_import(const char *file, struct dive_table *table); +extern void ostctools_import(const char *file, struct dive_table *table); + +#ifdef __cplusplus +extern "C" { +#endif +extern int readfile(const char *filename, struct memblock *mem); +extern timestamp_t parse_date(const char *date); +extern int try_to_open_zip(const char *filename, struct memblock *mem); +#ifdef __cplusplus +} +#endif + +#endif // FILE_H diff --git a/subsurface-core/gaspressures.c b/subsurface-core/gaspressures.c new file mode 100644 index 000000000..5f46d6080 --- /dev/null +++ b/subsurface-core/gaspressures.c @@ -0,0 +1,435 @@ +/* gaspressures.c + * --------------- + * This file contains the routines to calculate the gas pressures in the cylinders. + * The functions below support the code in profile.c. + * The high-level function is populate_pressure_information(), called by function + * create_plot_info_new() in profile.c. The other functions below are, in turn, + * called by populate_pressure_information(). The calling sequence is as follows: + * + * populate_pressure_information() -> calc_pressure_time() + * -> fill_missing_tank_pressures() -> fill_missing_segment_pressures() + * -> get_pr_interpolate_data() + * + * The pr_track_t related functions below implement a linked list that is used by + * the majority of the functions below. The linked list covers a part of the dive profile + * for which there are no cylinder pressure data. Each element in the linked list + * represents a segment between two consecutive points on the dive profile. + * pr_track_t is defined in gaspressures.h + */ + +#include "dive.h" +#include "display.h" +#include "profile.h" +#include "gaspressures.h" + +static pr_track_t *pr_track_alloc(int start, int t_start) +{ + pr_track_t *pt = malloc(sizeof(pr_track_t)); + pt->start = start; + pt->end = 0; + pt->t_start = pt->t_end = t_start; + pt->pressure_time = 0; + pt->next = NULL; + return pt; +} + +/* poor man's linked list */ +static pr_track_t *list_last(pr_track_t *list) +{ + pr_track_t *tail = list; + if (!tail) + return NULL; + while (tail->next) { + tail = tail->next; + } + return tail; +} + +static pr_track_t *list_add(pr_track_t *list, pr_track_t *element) +{ + pr_track_t *tail = list_last(list); + if (!tail) + return element; + tail->next = element; + return list; +} + +static void list_free(pr_track_t *list) +{ + if (!list) + return; + list_free(list->next); + free(list); +} + +#ifdef DEBUG_PR_TRACK +static void dump_pr_track(pr_track_t **track_pr) +{ + int cyl; + pr_track_t *list; + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { + list = track_pr[cyl]; + while (list) { + printf("cyl%d: start %d end %d t_start %d t_end %d pt %d\n", cyl, + list->start, list->end, list->t_start, list->t_end, list->pressure_time); + list = list->next; + } + } +} +#endif + +/* + * This looks at the pressures for one cylinder, and + * calculates any missing beginning/end pressures for + * each segment by taking the over-all SAC-rate into + * account for that cylinder. + * + * NOTE! Many segments have full pressure information + * (both beginning and ending pressure). But if we have + * switched away from a cylinder, we will have the + * beginning pressure for the first segment with a + * missing end pressure. We may then have one or more + * segments without beginning or end pressures, until + * we finally have a segment with an end pressure. + * + * We want to spread out the pressure over these missing + * segments according to how big of a time_pressure area + * they have. + */ +static void fill_missing_segment_pressures(pr_track_t *list, enum interpolation_strategy strategy) +{ + double magic; + + while (list) { + int start = list->start, end; + pr_track_t *tmp = list; + int pt_sum = 0, pt = 0; + + for (;;) { + pt_sum += tmp->pressure_time; + end = tmp->end; + if (end) + break; + end = start; + if (!tmp->next) + break; + tmp = tmp->next; + } + + if (!start) + start = end; + + /* + * Now 'start' and 'end' contain the pressure values + * for the set of segments described by 'list'..'tmp'. + * pt_sum is the sum of all the pressure-times of the + * segments. + * + * Now dole out the pressures relative to pressure-time. + */ + list->start = start; + tmp->end = end; + switch (strategy) { + case SAC: + for (;;) { + int pressure; + pt += list->pressure_time; + pressure = start; + if (pt_sum) + pressure -= (start - end) * (double)pt / pt_sum; + list->end = pressure; + if (list == tmp) + break; + list = list->next; + list->start = pressure; + } + break; + case TIME: + if (list->t_end && (tmp->t_start - tmp->t_end)) { + magic = (list->t_start - tmp->t_end) / (tmp->t_start - tmp->t_end); + list->end = rint(start - (start - end) * magic); + } else { + list->end = start; + } + break; + case CONSTANT: + list->end = start; + } + + /* Ok, we've done that set of segments */ + list = list->next; + } +} + +#ifdef DEBUG_PR_INTERPOLATE +void dump_pr_interpolate(int i, pr_interpolate_t interpolate_pr) +{ + printf("Interpolate for entry %d: start %d - end %d - pt %d - acc_pt %d\n", i, + interpolate_pr.start, interpolate_pr.end, interpolate_pr.pressure_time, interpolate_pr.acc_pressure_time); +} +#endif + + +static struct pr_interpolate_struct get_pr_interpolate_data(pr_track_t *segment, struct plot_info *pi, int cur, int pressure) +{ // cur = index to pi->entry corresponding to t_end of segment; + struct pr_interpolate_struct interpolate; + int i; + struct plot_data *entry; + + interpolate.start = segment->start; + interpolate.end = segment->end; + interpolate.acc_pressure_time = 0; + interpolate.pressure_time = 0; + + for (i = 0; i < pi->nr; i++) { + entry = pi->entry + i; + + if (entry->sec < segment->t_start) + continue; + if (entry->sec >= segment->t_end) { + interpolate.pressure_time += entry->pressure_time; + break; + } + if (entry->sec == segment->t_start) { + interpolate.acc_pressure_time = 0; + interpolate.pressure_time = 0; + if (pressure) + interpolate.start = pressure; + continue; + } + if (i < cur) { + if (pressure) { + interpolate.start = pressure; + interpolate.acc_pressure_time = 0; + interpolate.pressure_time = 0; + } else { + interpolate.acc_pressure_time += entry->pressure_time; + interpolate.pressure_time += entry->pressure_time; + } + continue; + } + if (i == cur) { + interpolate.acc_pressure_time += entry->pressure_time; + interpolate.pressure_time += entry->pressure_time; + continue; + } + interpolate.pressure_time += entry->pressure_time; + if (pressure) { + interpolate.end = pressure; + break; + } + } + return interpolate; +} + +static void fill_missing_tank_pressures(struct dive *dive, struct plot_info *pi, pr_track_t **track_pr, bool o2_flag) +{ + int cyl, i; + struct plot_data *entry; + int cur_pr[MAX_CYLINDERS]; // cur_pr[MAX_CYLINDERS] is the CCR diluent cylinder + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { + enum interpolation_strategy strategy; + if (!track_pr[cyl]) { + /* no segment where this cylinder is used */ + cur_pr[cyl] = -1; + continue; + } + if (dive->cylinder[cyl].cylinder_use == OC_GAS) + strategy = SAC; + else + strategy = TIME; + fill_missing_segment_pressures(track_pr[cyl], strategy); // Interpolate the missing tank pressure values .. + cur_pr[cyl] = track_pr[cyl]->start; // in the pr_track_t lists of structures + } // and keep the starting pressure for each cylinder. + +#ifdef DEBUG_PR_TRACK + /* another great debugging tool */ + dump_pr_track(track_pr); +#endif + + /* Transfer interpolated cylinder pressures from pr_track strucktures to plotdata + * Go down the list of tank pressures in plot_info. Align them with the start & + * end times of each profile segment represented by a pr_track_t structure. Get + * the accumulated pressure_depths from the pr_track_t structures and then + * interpolate the pressure where these do not exist in the plot_info pressure + * variables. Pressure values are transferred from the pr_track_t structures + * to the plot_info structure, allowing us to plot the tank pressure. + * + * The first two pi structures are "fillers", but in case we don't have a sample + * at time 0 we need to process the second of them here, therefore i=1 */ + for (i = 1; i < pi->nr; i++) { // For each point on the profile: + double magic; + pr_track_t *segment; + pr_interpolate_t interpolate; + int pressure; + int *save_pressure, *save_interpolated; + + entry = pi->entry + i; + + if (o2_flag) { + // Find the cylinder index (cyl) and pressure + cyl = dive->oxygen_cylinder_index; + if (cyl < 0) + return; // Can we do this?!? + pressure = O2CYLINDER_PRESSURE(entry); + save_pressure = &(entry->o2cylinderpressure[SENSOR_PR]); + save_interpolated = &(entry->o2cylinderpressure[INTERPOLATED_PR]); + } else { + pressure = SENSOR_PRESSURE(entry); + save_pressure = &(entry->pressure[SENSOR_PR]); + save_interpolated = &(entry->pressure[INTERPOLATED_PR]); + cyl = entry->cylinderindex; + } + + if (pressure) { // If there is a valid pressure value, + cur_pr[cyl] = pressure; // set current pressure + continue; // and skip to next point. + } + // If there is NO valid pressure value.. + // Find the pressure segment corresponding to this entry.. + segment = track_pr[cyl]; + while (segment && segment->t_end < entry->sec) // Find the track_pr with end time.. + segment = segment->next; // ..that matches the plot_info time (entry->sec) + + if (!segment || !segment->pressure_time) { // No (or empty) segment? + *save_pressure = cur_pr[cyl]; // Just use our current pressure + continue; // and skip to next point. + } + + // If there is a valid segment but no tank pressure .. + interpolate = get_pr_interpolate_data(segment, pi, i, pressure); // Set up an interpolation structure + if(dive->cylinder[cyl].cylinder_use == OC_GAS) { + + /* if this segment has pressure_time, then calculate a new interpolated pressure */ + if (interpolate.pressure_time) { + /* Overall pressure change over total pressure-time for this segment*/ + magic = (interpolate.end - interpolate.start) / (double)interpolate.pressure_time; + + /* Use that overall pressure change to update the current pressure */ + cur_pr[cyl] = rint(interpolate.start + magic * interpolate.acc_pressure_time); + } + } else { + magic = (interpolate.end - interpolate.start) / (segment->t_end - segment->t_start); + cur_pr[cyl] = rint(segment->start + magic * (entry->sec - segment->t_start)); + } + *save_interpolated = cur_pr[cyl]; // and store the interpolated data in plot_info + } +} + + +/* + * What's the pressure-time between two plot data entries? + * We're calculating the integral of pressure over time by + * adding these up. + * + * The units won't matter as long as everybody agrees about + * them, since they'll cancel out - we use this to calculate + * a constant SAC-rate-equivalent, but we only use it to + * scale pressures, so it ends up being a unitless scaling + * factor. + */ +static inline int calc_pressure_time(struct dive *dive, struct divecomputer *dc, struct plot_data *a, struct plot_data *b) +{ + int time = b->sec - a->sec; + int depth = (a->depth + b->depth) / 2; + + if (depth <= SURFACE_THRESHOLD) + return 0; + + return depth_to_mbar(depth, dive) * time; +} + +#ifdef PRINT_PRESSURES_DEBUG +// A CCR debugging tool that prints the gas pressures in cylinder 0 and in the diluent cylinder, used in populate_pressure_information(): +static void debug_print_pressures(struct plot_info *pi) +{ + int i; + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + printf("%5d |%9d | %9d || %9d | %9d |\n", i, SENSOR_PRESSURE(entry), INTERPOLATED_PRESSURE(entry), DILUENT_PRESSURE(entry), INTERPOLATED_DILUENT_PRESSURE(entry)); + } +} +#endif + +/* This function goes through the list of tank pressures, either SENSOR_PRESSURE(entry) or O2CYLINDER_PRESSURE(entry), + * of structure plot_info for the dive profile where each item in the list corresponds to one point (node) of the + * profile. It finds values for which there are no tank pressures (pressure==0). For each missing item (node) of + * tank pressure it creates a pr_track_alloc structure that represents a segment on the dive profile and that + * contains tank pressures. There is a linked list of pr_track_alloc structures for each cylinder. These pr_track_alloc + * structures ultimately allow for filling the missing tank pressure values on the dive profile using the depth_pressure + * of the dive. To do this, it calculates the summed pressure-time value for the duration of the dive and stores these + * in the pr_track_alloc structures. If diluent_flag = 1, then DILUENT_PRESSURE(entry) is used instead of SENSOR_PRESSURE. + * This function is called by create_plot_info_new() in profile.c + */ +void populate_pressure_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, int o2_flag) +{ + int i, cylinderid, cylinderindex = -1; + pr_track_t *track_pr[MAX_CYLINDERS] = { NULL, }; + pr_track_t *current = NULL; + bool missing_pr = false; + + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + unsigned pressure; + if (o2_flag) { // if this is a diluent cylinder: + pressure = O2CYLINDER_PRESSURE(entry); + cylinderid = dive->oxygen_cylinder_index; + if (cylinderid < 0) + goto GIVE_UP; + } else { + pressure = SENSOR_PRESSURE(entry); + cylinderid = entry->cylinderindex; + } + /* If track_pr structure already exists, then update it: */ + /* discrete integration of pressure over time to get the SAC rate equivalent */ + if (current) { + entry->pressure_time = calc_pressure_time(dive, dc, entry - 1, entry); + current->pressure_time += entry->pressure_time; + current->t_end = entry->sec; + } + + /* If 1st record or different cylinder: Create a new track_pr structure: */ + /* track the segments per cylinder and their pressure/time integral */ + if (cylinderid != cylinderindex) { + if (o2_flag) // For CCR dives: + cylinderindex = dive->oxygen_cylinder_index; // indicate o2 cylinder + else + cylinderindex = entry->cylinderindex; + current = pr_track_alloc(pressure, entry->sec); + track_pr[cylinderindex] = list_add(track_pr[cylinderindex], current); + continue; + } + + if (!pressure) { + missing_pr = 1; + continue; + } + if (current) + current->end = pressure; + + /* Was it continuous? */ + if ((o2_flag) && (O2CYLINDER_PRESSURE(entry - 1))) // in the case of CCR o2 pressure + continue; + else if (SENSOR_PRESSURE(entry - 1)) // for all other cylinders + continue; + + /* transmitter stopped transmitting cylinder pressure data */ + current = pr_track_alloc(pressure, entry->sec); + if (cylinderindex >= 0) + track_pr[cylinderindex] = list_add(track_pr[cylinderindex], current); + } + + if (missing_pr) { + fill_missing_tank_pressures(dive, pi, track_pr, o2_flag); + } + +#ifdef PRINT_PRESSURES_DEBUG + debug_print_pressures(pi); +#endif + +GIVE_UP: + for (i = 0; i < MAX_CYLINDERS; i++) + list_free(track_pr[i]); +} diff --git a/subsurface-core/gaspressures.h b/subsurface-core/gaspressures.h new file mode 100644 index 000000000..420c117a2 --- /dev/null +++ b/subsurface-core/gaspressures.h @@ -0,0 +1,35 @@ +#ifndef GASPRESSURES_H +#define GASPRESSURES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * simple structure to track the beginning and end tank pressure as + * well as the integral of depth over time spent while we have no + * pressure reading from the tank */ +typedef struct pr_track_struct pr_track_t; +struct pr_track_struct { + int start; + int end; + int t_start; + int t_end; + int pressure_time; + pr_track_t *next; +}; + +typedef struct pr_interpolate_struct pr_interpolate_t; +struct pr_interpolate_struct { + int start; + int end; + int pressure_time; + int acc_pressure_time; +}; + +enum interpolation_strategy {SAC, TIME, CONSTANT}; + +#ifdef __cplusplus +} +#endif +#endif // GASPRESSURES_H diff --git a/subsurface-core/gettext.h b/subsurface-core/gettext.h new file mode 100644 index 000000000..43ff023c7 --- /dev/null +++ b/subsurface-core/gettext.h @@ -0,0 +1,10 @@ +#ifndef MYGETTEXT_H +#define MYGETTEXT_H + +/* this is for the Qt based translations */ +extern const char *trGettext(const char *); +#define translate(_context, arg) trGettext(arg) +#define QT_TRANSLATE_NOOP(_context, arg) arg +#define QT_TRANSLATE_NOOP3(_context, arg, _comment) arg + +#endif // MYGETTEXT_H diff --git a/subsurface-core/gettextfromc.cpp b/subsurface-core/gettextfromc.cpp new file mode 100644 index 000000000..c579e3c3c --- /dev/null +++ b/subsurface-core/gettextfromc.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +const char *gettextFromC::trGettext(const char *text) +{ + QByteArray &result = translationCache[QByteArray(text)]; + if (result.isEmpty()) + result = translationCache[QByteArray(text)] = trUtf8(text).toUtf8(); + return result.constData(); +} + +void gettextFromC::reset(void) +{ + translationCache.clear(); +} + +gettextFromC *gettextFromC::instance() +{ + static QScopedPointer self(new gettextFromC()); + return self.data(); +} + +extern "C" const char *trGettext(const char *text) +{ + return gettextFromC::instance()->trGettext(text); +} diff --git a/subsurface-core/gettextfromc.h b/subsurface-core/gettextfromc.h new file mode 100644 index 000000000..53df18365 --- /dev/null +++ b/subsurface-core/gettextfromc.h @@ -0,0 +1,18 @@ +#ifndef GETTEXTFROMC_H +#define GETTEXTFROMC_H + +#include +#include + +extern "C" const char *trGettext(const char *text); + +class gettextFromC { + Q_DECLARE_TR_FUNCTIONS(gettextFromC) +public: + static gettextFromC *instance(); + const char *trGettext(const char *text); + void reset(void); + QHash translationCache; +}; + +#endif // GETTEXTFROMC_H diff --git a/subsurface-core/git-access.c b/subsurface-core/git-access.c new file mode 100644 index 000000000..607789f98 --- /dev/null +++ b/subsurface-core/git-access.c @@ -0,0 +1,891 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dive.h" +#include "membuffer.h" +#include "strndup.h" +#include "qthelperfromc.h" +#include "git-access.h" +#include "gettext.h" + +/* + * The libgit2 people are incompetent at making libraries. They randomly change + * the interfaces, often just renaming things without any sane way to know which + * version you should check for etc etc. It's a disgrace. + */ +#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR < 22 + #define git_remote_lookup(res, repo, name) git_remote_load(res, repo, name) + #if LIBGIT2_VER_MINOR <= 20 + #define git_remote_fetch(remote, refspecs, signature, reflog) git_remote_fetch(remote) + #else + #define git_remote_fetch(remote, refspecs, signature, reflog) git_remote_fetch(remote, signature, reflog) + #endif +#endif + +#if !USE_LIBGIT23_API && !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR == 22 + #define git_remote_push(remote,refspecs,opts) git_remote_push(remote,refspecs,opts,NULL,NULL) + #define git_reference_set_target(out,ref,id,log_message) git_reference_set_target(out,ref,id,NULL,log_message) + #define git_reset(repo,target,reset_type,checkout_opts) git_reset(repo,target,reset_type,checkout_opts,NULL,NULL) +#endif + +/* + * api break introduced in libgit2 master after 0.22 - let's guess this is the v0.23 API + */ +#if USE_LIBGIT23_API || (!LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR >= 23) + #define git_branch_create(out, repo, branch_name, target, force, signature, log_message) \ + git_branch_create(out, repo, branch_name, target, force) +#endif + +bool is_subsurface_cloud = false; + +int (*update_progress_cb)(int) = NULL; + +void set_git_update_cb(int(*cb)(int)) +{ + update_progress_cb = cb; +} + +static int update_progress(int percent) +{ + static int last_percent = -10; + int ret = 0; + if (update_progress_cb) + ret = (*update_progress_cb)(percent); + if (verbose && percent - 10 >= last_percent) { + last_percent = percent; + fprintf(stderr, "git progress %d%%\n", percent); + } + return ret; +} + +// the checkout_progress_cb doesn't allow canceling of the operation +static void progress_cb(const char *path, size_t completed_steps, size_t total_steps, void *payload) +{ + int percent = 0; + if (total_steps) + percent = 100 * completed_steps / total_steps; + (void)update_progress(percent); +} + +// this randomly assumes that 80% of the time is spent on the objects and 20% on the deltas +// if the user cancels the dialog this is passed back to libgit2 +static int transfer_progress_cb(const git_transfer_progress *stats, void *payload) +{ + int percent = 0; + if (stats->total_objects) + percent = 80 * stats->received_objects / stats->total_objects; + if (stats->total_deltas) + percent += 20 * stats->indexed_deltas / stats->total_deltas; + return update_progress(percent); +} + +static int push_transfer_progress_cb(unsigned int current, unsigned int total, size_t bytes, void *payload) +{ + int percent = 0; + if (total != 0) + percent = 100 * current / total; + return update_progress(percent); +} + +char *get_local_dir(const char *remote, const char *branch) +{ + SHA_CTX ctx; + unsigned char hash[20]; + + // That zero-byte update is so that we don't get hash + // collisions for "repo1 branch" vs "repo 1branch". + SHA1_Init(&ctx); + SHA1_Update(&ctx, remote, strlen(remote)); + SHA1_Update(&ctx, "", 1); + SHA1_Update(&ctx, branch, strlen(branch)); + SHA1_Final(hash, &ctx); + + return format_string("%s/cloudstorage/%02x%02x%02x%02x%02x%02x%02x%02x", + system_default_directory(), + hash[0], hash[1], hash[2], hash[3], + hash[4], hash[5], hash[6], hash[7]); +} + +static char *move_local_cache(const char *remote, const char *branch) +{ + char *old_path = get_local_dir(remote, branch); + return move_away(old_path); +} + +static int check_clean(const char *path, unsigned int status, void *payload) +{ + status &= ~GIT_STATUS_CURRENT | GIT_STATUS_IGNORED; + if (!status) + return 0; + if (is_subsurface_cloud) + report_error(translate("gettextFromC", "Local cache directory %s corrupted - can't sync with Subsurface cloud storage"), path); + else + report_error("WARNING: Git cache directory modified (path %s) status %0x", path, status); + return 1; +} + +/* + * The remote is strictly newer than the local branch. + */ +static int reset_to_remote(git_repository *repo, git_reference *local, const git_oid *new_id) +{ + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + opts.progress_cb = &progress_cb; + git_object *target; + + if (verbose) + fprintf(stderr, "git storage: reset to remote\n"); + + // If it's not checked out (bare or not HEAD), just update the reference */ + if (git_repository_is_bare(repo) || git_branch_is_head(local) != 1) { + git_reference *out; + + if (git_reference_set_target(&out, local, new_id, "Update to remote")) + return report_error(translate("gettextFromC", "Could not update local cache to newer remote data")); + + git_reference_free(out); + +#ifdef DEBUG + // Not really an error, just informational + report_error("Updated local branch from remote"); +#endif + return 0; + } + + if (git_object_lookup(&target, repo, new_id, GIT_OBJ_COMMIT)) { + if (is_subsurface_cloud) + return report_error(translate("gettextFromC", "Subsurface cloud storage corrupted")); + else + return report_error("Could not look up remote commit"); + } + opts.checkout_strategy = GIT_CHECKOUT_SAFE; + if (git_reset(repo, target, GIT_RESET_HARD, &opts)) { + if (is_subsurface_cloud) + return report_error(translate("gettextFromC", "Could not update local cache to newer remote data")); + else + return report_error("Local head checkout failed after update"); + } + // Not really an error, just informational +#ifdef DEBUG + report_error("Updated local information from remote"); +#endif + return 0; +} + +#if USE_LIBGIT23_API +int credential_ssh_cb(git_cred **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + const char *priv_key = format_string("%s/%s", system_default_directory(), "ssrf_remote.key"); + const char *passphrase = prefs.cloud_storage_password ? strdup(prefs.cloud_storage_password) : strdup(""); + return git_cred_ssh_key_new(out, username_from_url, NULL, priv_key, passphrase); +} + +int credential_https_cb(git_cred **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) +{ + const char *username = prefs.cloud_storage_email_encoded; + const char *password = prefs.cloud_storage_password ? strdup(prefs.cloud_storage_password) : strdup(""); + return git_cred_userpass_plaintext_new(out, username, password); +} + +#define KNOWN_CERT "\xfd\xb8\xf7\x73\x76\xe2\x75\x53\x93\x37\xdc\xfe\x1e\x55\x43\x3d\xf2\x2c\x18\x2c" +int certificate_check_cb(git_cert *cert, int valid, const char *host, void *payload) +{ + if (same_string(host, "cloud.subsurface-divelog.org") && cert->cert_type == GIT_CERT_X509) { + SHA_CTX ctx; + unsigned char hash[21]; + git_cert_x509 *cert509 = (git_cert_x509 *)cert; + SHA1_Init(&ctx); + SHA1_Update(&ctx, cert509->data, cert509->len); + SHA1_Final(hash, &ctx); + hash[20] = 0; + if (verbose > 1) + if (same_string((char *)hash, KNOWN_CERT)) { + fprintf(stderr, "cloud certificate considered %s, forcing it valid\n", + valid ? "valid" : "not valid"); + return 1; + } + } + return valid; +} + +#endif + +static int update_remote(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, enum remote_transport rt) +{ + git_push_options opts = GIT_PUSH_OPTIONS_INIT; + git_strarray refspec; + const char *name = git_reference_name(local); + + if (verbose) + fprintf(stderr, "git storage: update remote\n"); + + refspec.count = 1; + refspec.strings = (char **)&name; + +#if USE_LIBGIT23_API + opts.callbacks.push_transfer_progress = &push_transfer_progress_cb; + if (rt == RT_SSH) + opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.callbacks.credentials = credential_https_cb; + opts.callbacks.certificate_check = certificate_check_cb; +#endif + if (git_remote_push(origin, &refspec, &opts)) { + if (is_subsurface_cloud) + return report_error(translate("gettextFromC", "Could not update Subsurface cloud storage, try again later")); + else + return report_error("Unable to update remote with current local cache state (%s)", giterr_last()->message); + } + return 0; +} + +extern int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree); + +static int try_to_git_merge(git_repository *repo, git_reference *local, git_reference *remote, git_oid *base, const git_oid *local_id, const git_oid *remote_id) +{ + git_tree *local_tree, *remote_tree, *base_tree; + git_commit *local_commit, *remote_commit, *base_commit; + git_index *merged_index; + git_merge_options merge_options; + + if (verbose) { + char outlocal[41], outremote[41]; + outlocal[40] = outremote[40] = 0; + git_oid_fmt(outlocal, local_id); + git_oid_fmt(outremote, remote_id); + fprintf(stderr, "trying to merge local SHA %s remote SHA %s\n", outlocal, outremote); + } + + git_merge_init_options(&merge_options, GIT_MERGE_OPTIONS_VERSION); +#ifdef USE_LIBGIT23_API + merge_options.tree_flags = GIT_MERGE_TREE_FIND_RENAMES; +#else + merge_options.flags = GIT_MERGE_TREE_FIND_RENAMES; +#endif + merge_options.file_favor = GIT_MERGE_FILE_FAVOR_UNION; + merge_options.rename_threshold = 100; + if (git_commit_lookup(&local_commit, repo, local_id)) { + fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_tree(&local_tree, local_commit)) { + fprintf(stderr, "Remote storage and local data diverged. Error: failed local tree lookup (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_lookup(&remote_commit, repo, remote_id)) { + fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_tree(&remote_tree, remote_commit)) { + fprintf(stderr, "Remote storage and local data diverged. Error: failed local tree lookup (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_lookup(&base_commit, repo, base)) { + fprintf(stderr, "Remote storage and local data diverged. Error: can't get commit (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_commit_tree(&base_tree, base_commit)) { + fprintf(stderr, "Remote storage and local data diverged. Error: failed base tree lookup (%s)", giterr_last()->message); + goto diverged_error; + } + if (git_merge_trees(&merged_index, repo, base_tree, local_tree, remote_tree, &merge_options)) { + fprintf(stderr, "Remote storage and local data diverged. Error: merge failed (%s)", giterr_last()->message); + // this is the one where I want to report more detail to the user - can't quite explain why + return report_error(translate("gettextFromC", "Remote storage and local data diverged. Error: merge failed (%s)"), giterr_last()->message); + } + if (git_index_has_conflicts(merged_index)) { + int error; + const git_index_entry *ancestor = NULL, + *ours = NULL, + *theirs = NULL; + git_index_conflict_iterator *iter = NULL; + error = git_index_conflict_iterator_new(&iter, merged_index); + while (git_index_conflict_next(&ancestor, &ours, &theirs, iter) + != GIT_ITEROVER) { + /* Mark this conflict as resolved */ + fprintf(stderr, "conflict in %s / %s / %s -- ", + ours ? ours->path : "-", + theirs ? theirs->path : "-", + ancestor ? ancestor->path : "-"); + if ((!ours && theirs && ancestor) || + (ours && !theirs && ancestor)) { + // the file was removed on one side or the other - just remove it + fprintf(stderr, "looks like a delete on one side; removing the file from the index\n"); + error = git_index_remove(merged_index, ours ? ours->path : theirs->path, GIT_INDEX_STAGE_ANY); + } else if (ancestor) { + error = git_index_conflict_remove(merged_index, ours ? ours->path : theirs ? theirs->path : ancestor->path); + } + if (error) { + fprintf(stderr, "error at conflict resplution (%s)", giterr_last()->message); + } + } + git_index_conflict_cleanup(merged_index); + git_index_conflict_iterator_free(iter); + report_error(translate("gettextFromC", "Remote storage and local data diverged. Cannot combine local and remote changes")); + } + git_oid merge_oid, commit_oid; + git_tree *merged_tree; + git_signature *author; + git_commit *commit; + + if (git_index_write_tree_to(&merge_oid, merged_index, repo)) + goto write_error; + if (git_tree_lookup(&merged_tree, repo, &merge_oid)) + goto write_error; + if (git_signature_default(&author, repo) < 0) + if (git_signature_now(&author, "Subsurface", "noemail@given") < 0) + goto write_error; + if (git_commit_create_v(&commit_oid, repo, NULL, author, author, NULL, "automatic merge", merged_tree, 2, local_commit, remote_commit)) + goto write_error; + if (git_commit_lookup(&commit, repo, &commit_oid)) + goto write_error; + if (git_branch_is_head(local) && !git_repository_is_bare(repo)) { + git_object *parent; + git_reference_peel(&parent, local, GIT_OBJ_COMMIT); + if (update_git_checkout(repo, parent, merged_tree)) { + goto write_error; + } + } + if (git_reference_set_target(&local, local, &commit_oid, "Subsurface merge event")) + goto write_error; + set_git_id(&commit_oid); + git_signature_free(author); + + return 0; + +diverged_error: + return report_error(translate("gettextFromC", "Remote storage and local data diverged")); + +write_error: + return report_error(translate("gettextFromC", "Remote storage and local data diverged. Error: writing the data failed (%s)"), giterr_last()->message); +} + +// if accessing the local cache of Subsurface cloud storage fails, we simplify things +// for the user and simply move the cache away (in case they want to try and extract data) +// and ask them to retry the operation (which will then refresh the data from the cloud server) +static int cleanup_local_cache(const char *remote_url, const char *branch) +{ + char *backup_path = move_local_cache(remote_url, branch); + report_error(translate("gettextFromC", "Problems with local cache of Subsurface cloud data")); + report_error(translate("gettextFromC", "Moved cache data to %s. Please try the operation again."), backup_path); + free(backup_path); + return -1; +} +static int try_to_update(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, + const char *remote_url, const char *branch, enum remote_transport rt) +{ + git_oid base; + const git_oid *local_id, *remote_id; + + if (verbose) + fprintf(stderr, "git storage: try to update\n"); + + if (!git_reference_cmp(local, remote)) + return 0; + + // Dirty modified state in the working tree? We're not going + // to update either way + if (git_status_foreach(repo, check_clean, NULL)) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("local cached copy is dirty, skipping update"); + } + local_id = git_reference_target(local); + remote_id = git_reference_target(remote); + + if (!local_id || !remote_id) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Unable to get local or remote SHA1"); + } + if (git_merge_base(&base, repo, local_id, remote_id)) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Unable to find common commit of local and remote branches"); + } + /* Is the remote strictly newer? Use it */ + if (git_oid_equal(&base, local_id)) + return reset_to_remote(repo, local, remote_id); + + /* Is the local repo the more recent one? See if we can update upstream */ + if (git_oid_equal(&base, remote_id)) + return update_remote(repo, origin, local, remote, rt); + + /* Merging a bare repository always needs user action */ + if (git_repository_is_bare(repo)) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Local and remote have diverged, merge of bare branch needed"); + } + /* Merging will definitely need the head branch too */ + if (git_branch_is_head(local) != 1) { + if (is_subsurface_cloud) + goto cloud_data_error; + else + return report_error("Local and remote do not match, local branch not HEAD - cannot update"); + } + /* Ok, let's try to merge these */ + return try_to_git_merge(repo, local, remote, &base, local_id, remote_id); + +cloud_data_error: + // since we are working with Subsurface cloud storage we want to make the user interaction + // as painless as possible. So if something went wrong with the local cache, tell the user + // about it an move it away + return cleanup_local_cache(remote_url, branch); +} + +static int check_remote_status(git_repository *repo, git_remote *origin, const char *remote, const char *branch, enum remote_transport rt) +{ + int error = 0; + + git_reference *local_ref, *remote_ref; + + if (verbose) + fprintf(stderr, "git storage: check remote status\n"); + + if (git_branch_lookup(&local_ref, repo, branch, GIT_BRANCH_LOCAL)) { + if (is_subsurface_cloud) + return cleanup_local_cache(remote, branch); + else + return report_error("Git cache branch %s no longer exists", branch); + } + if (git_branch_upstream(&remote_ref, local_ref)) { + /* so there is no upstream branch for our branch; that's a problem. + * let's push our branch */ + git_strarray refspec; + git_reference_list(&refspec, repo); +#if USE_LIBGIT23_API + git_push_options opts = GIT_PUSH_OPTIONS_INIT; + opts.callbacks.transfer_progress = &transfer_progress_cb; + if (rt == RT_SSH) + opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.callbacks.credentials = credential_https_cb; + opts.callbacks.certificate_check = certificate_check_cb; + error = git_remote_push(origin, &refspec, &opts); +#else + error = git_remote_push(origin, &refspec, NULL); +#endif + } else { + error = try_to_update(repo, origin, local_ref, remote_ref, remote, branch, rt); + git_reference_free(remote_ref); + } + git_reference_free(local_ref); + return error; +} + +int sync_with_remote(git_repository *repo, const char *remote, const char *branch, enum remote_transport rt) +{ + int error; + git_remote *origin; + char *proxy_string; + git_config *conf; + + if (verbose) + fprintf(stderr, "sync with remote %s[%s]\n", remote, branch); + + git_repository_config(&conf, repo); + if (rt == RT_HTTPS && getProxyString(&proxy_string)) { + if (verbose) + fprintf(stderr, "set proxy to \"%s\"\n", proxy_string); + git_config_set_string(conf, "http.proxy", proxy_string); + free(proxy_string); + } else { + if (verbose) + fprintf(stderr, "delete proxy setting\n"); + git_config_delete_entry(conf, "http.proxy"); + } + + /* + * NOTE! Remote errors are reported, but are nonfatal: + * we still successfully return the local repository. + */ + error = git_remote_lookup(&origin, repo, "origin"); + if (error) { + if (!is_subsurface_cloud) + report_error("Repository '%s' origin lookup failed (%s)", remote, giterr_last()->message); + return 0; + } + + if (rt == RT_HTTPS && !canReachCloudServer()) { + // this is not an error, just a warning message, so return 0 + report_error("Cannot connect to cloud server, working with local copy"); + return 0; + } + if (verbose) + fprintf(stderr, "git storage: fetch remote\n"); +#if USE_LIBGIT23_API + git_fetch_options opts = GIT_FETCH_OPTIONS_INIT; + opts.callbacks.transfer_progress = &transfer_progress_cb; + if (rt == RT_SSH) + opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.callbacks.credentials = credential_https_cb; + opts.callbacks.certificate_check = certificate_check_cb; + error = git_remote_fetch(origin, NULL, &opts, NULL); +#else + error = git_remote_fetch(origin, NULL, NULL, NULL); +#endif + // NOTE! A fetch error is not fatal, we just report it + if (error) { + if (is_subsurface_cloud) + report_error("Cannot sync with cloud server, working with offline copy"); + else + report_error("Unable to fetch remote '%s'", remote); + if (verbose) + fprintf(stderr, "remote fetch failed (%s)\n", giterr_last()->message); + error = 0; + } else { + error = check_remote_status(repo, origin, remote, branch, rt); + } + git_remote_free(origin); + return error; +} + +static git_repository *update_local_repo(const char *localdir, const char *remote, const char *branch, enum remote_transport rt) +{ + int error; + git_repository *repo = NULL; + + if (verbose) + fprintf(stderr, "git storage: update local repo\n"); + + error = git_repository_open(&repo, localdir); + if (error) { + if (is_subsurface_cloud) + (void)cleanup_local_cache(remote, branch); + else + report_error("Unable to open git cache repository at %s: %s", localdir, giterr_last()->message); + return NULL; + } + sync_with_remote(repo, remote, branch, rt); + return repo; +} + +static int repository_create_cb(git_repository **out, const char *path, int bare, void *payload) +{ + char *proxy_string; + git_config *conf; + + int ret = git_repository_init(out, path, bare); + + git_repository_config(&conf, *out); + if (getProxyString(&proxy_string)) { + if (verbose) + fprintf(stderr, "set proxy to \"%s\"\n", proxy_string); + git_config_set_string(conf, "http.proxy", proxy_string); + free(proxy_string); + } else { + if (verbose) + fprintf(stderr, "delete proxy setting\n"); + git_config_delete_entry(conf, "http.proxy"); + } + return ret; +} + +/* this should correctly initialize both the local and remote + * repository for the Subsurface cloud storage */ +static git_repository *create_and_push_remote(const char *localdir, const char *remote, const char *branch) +{ + git_repository *repo; + git_config *conf; + int len; + char *variable_name, *merge_head; + + if (verbose) + fprintf(stderr, "git storage: create and push remote\n"); + + /* first make sure the directory for the local cache exists */ + subsurface_mkdir(localdir); + + /* set up the origin to point to our remote */ + git_repository_init_options init_opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; + init_opts.origin_url = remote; + + /* now initialize the repository with */ + git_repository_init_ext(&repo, localdir, &init_opts); + + /* create a config so we can set the remote tracking branch */ + git_repository_config(&conf, repo); + len = sizeof("branch..remote") + strlen(branch); + variable_name = malloc(len); + snprintf(variable_name, len, "branch.%s.remote", branch); + git_config_set_string(conf, variable_name, "origin"); + /* we know this is shorter than the previous one, so we reuse the variable*/ + snprintf(variable_name, len, "branch.%s.merge", branch); + len = sizeof("refs/heads/") + strlen(branch); + merge_head = malloc(len); + snprintf(merge_head, len, "refs/heads/%s", branch); + git_config_set_string(conf, variable_name, merge_head); + + /* finally create an empty commit and push it to the remote */ + if (do_git_save(repo, branch, remote, false, true)) + return NULL; + return(repo); +} + +static git_repository *create_local_repo(const char *localdir, const char *remote, const char *branch, enum remote_transport rt) +{ + int error; + git_repository *cloned_repo = NULL; + git_clone_options opts = GIT_CLONE_OPTIONS_INIT; + + if (verbose) + fprintf(stderr, "git storage: create_local_repo\n"); + +#if USE_LIBGIT23_API + opts.fetch_opts.callbacks.transfer_progress = &transfer_progress_cb; + if (rt == RT_SSH) + opts.fetch_opts.callbacks.credentials = credential_ssh_cb; + else if (rt == RT_HTTPS) + opts.fetch_opts.callbacks.credentials = credential_https_cb; + opts.repository_cb = repository_create_cb; + opts.fetch_opts.callbacks.certificate_check = certificate_check_cb; +#endif + opts.checkout_branch = branch; + if (rt == RT_HTTPS && !canReachCloudServer()) + return 0; + if (verbose > 1) + fprintf(stderr, "git storage: calling git_clone()\n"); + error = git_clone(&cloned_repo, remote, localdir, &opts); + if (verbose > 1) + fprintf(stderr, "git storage: returned from git_clone() with error %d\n", error); + if (error) { + char *msg = giterr_last()->message; + int len = sizeof("Reference 'refs/remotes/origin/' not found") + strlen(branch); + char *pattern = malloc(len); + snprintf(pattern, len, "Reference 'refs/remotes/origin/%s' not found", branch); + if (strstr(remote, prefs.cloud_git_url) && strstr(msg, pattern)) { + /* we're trying to open the remote branch that corresponds + * to our cloud storage and the branch doesn't exist. + * So we need to create the branch and push it to the remote */ + cloned_repo = create_and_push_remote(localdir, remote, branch); +#if !defined(DEBUG) + } else if (is_subsurface_cloud) { + report_error(translate("gettextFromC", "Error connecting to Subsurface cloud storage")); +#endif + } else { + report_error(translate("gettextFromC", "git clone of %s failed (%s)"), remote, msg); + } + free(pattern); + } + return cloned_repo; +} + +static struct git_repository *get_remote_repo(const char *localdir, const char *remote, const char *branch) +{ + struct stat st; + enum remote_transport rt; + + /* figure out the remote transport */ + if (strncmp(remote, "ssh://", 6) == 0) + rt = RT_SSH; + else if (strncmp(remote, "https://", 8) == 0) + rt = RT_HTTPS; + else + rt = RT_OTHER; + + if (verbose > 1) { + fprintf(stderr, "git storage: accessing %s\n", remote); + } + /* Do we already have a local cache? */ + if (!stat(localdir, &st)) { + if (!S_ISDIR(st.st_mode)) { + if (is_subsurface_cloud) + (void)cleanup_local_cache(remote, branch); + else + report_error("local git cache at '%s' is corrupt"); + return NULL; + } + return update_local_repo(localdir, remote, branch, rt); + } + return create_local_repo(localdir, remote, branch, rt); +} + +/* + * This turns a remote repository into a local one if possible. + * + * The recognized formats are + * git://host/repo[branch] + * ssh://host/repo[branch] + * http://host/repo[branch] + * https://host/repo[branch] + * file://repo[branch] + */ +static struct git_repository *is_remote_git_repository(char *remote, const char *branch) +{ + char c, *localdir; + const char *p = remote; + + while ((c = *p++) >= 'a' && c <= 'z') + /* nothing */; + if (c != ':') + return NULL; + if (*p++ != '/' || *p++ != '/') + return NULL; + + /* Special-case "file://", since it's already local */ + if (!strncmp(remote, "file://", 7)) + remote += 7; + + /* + * Ok, we found "[a-z]*://", we've simplified the + * local repo case (because libgit2 is insanely slow + * for that), and we think we have a real "remote + * git" format. + * + * We now create the SHA1 hash of the whole thing, + * including the branch name. That will be our unique + * unique local repository name. + * + * NOTE! We will create a local repository per branch, + * because + * + * (a) libgit2 remote tracking branch support seems to + * be a bit lacking + * (b) we'll actually check the branch out so that we + * can do merges etc too. + * + * so even if you have a single remote git repo with + * multiple branches for different people, the local + * caches will sadly force that to split into multiple + * individual repositories. + */ + + /* + * next we need to make sure that any encoded username + * has been extracted from an https:// based URL + */ + if (!strncmp(remote, "https://", 8)) { + char *at = strchr(remote, '@'); + if (at) { + /* was this the @ that denotes an account? that means it was before the + * first '/' after the https:// - so let's find a '/' after that and compare */ + char *slash = strchr(remote + 8, '/'); + if (slash && slash > at) { + /* grab the part between "https://" and "@" as encoded email address + * (that's our username) and move the rest of the URL forward, remembering + * to copy the closing NUL as well */ + prefs.cloud_storage_email_encoded = strndup(remote + 8, at - remote - 8); + memmove(remote + 8, at + 1, strlen(at + 1) + 1); + } + } + } + localdir = get_local_dir(remote, branch); + if (!localdir) + return NULL; + + /* remember if the current git storage we are working on is our cloud storage + * this is used to create more user friendly error message and warnings */ + is_subsurface_cloud = strstr(remote, prefs.cloud_git_url) != NULL; + + return get_remote_repo(localdir, remote, branch); +} + +/* + * If it's not a git repo, return NULL. Be very conservative. + */ +struct git_repository *is_git_repository(const char *filename, const char **branchp, const char **remote, bool dry_run) +{ + int flen, blen, ret; + int offset = 1; + struct stat st; + git_repository *repo; + char *loc, *branch; + + flen = strlen(filename); + if (!flen || filename[--flen] != ']') + return NULL; + + /* Find the matching '[' */ + blen = 0; + while (flen && filename[--flen] != '[') + blen++; + + /* Ignore slashes at the end of the repo name */ + while (flen && filename[flen-1] == '/') { + flen--; + offset++; + } + + if (!flen) + return NULL; + + /* + * This is the "point of no return": the name matches + * the git repository name rules, and we will no longer + * return NULL. + * + * We will either return "dummy_git_repository" and the + * branch pointer will have the _whole_ filename in it, + * or we will return a real git repository with the + * branch pointer being filled in with just the branch + * name. + * + * The actual git reading/writing routines can use this + * to generate proper error messages. + */ + *branchp = filename; + loc = format_string("%.*s", flen, filename); + if (!loc) + return dummy_git_repository; + + branch = format_string("%.*s", blen, filename + flen + offset); + if (!branch) { + free(loc); + return dummy_git_repository; + } + + if (dry_run) { + *branchp = branch; + *remote = loc; + return dummy_git_repository; + } + repo = is_remote_git_repository(loc, branch); + if (repo) { + if (remote) + *remote = loc; + else + free(loc); + *branchp = branch; + return repo; + } + + if (stat(loc, &st) < 0 || !S_ISDIR(st.st_mode)) { + free(loc); + free(branch); + return dummy_git_repository; + } + + ret = git_repository_open(&repo, loc); + free(loc); + if (ret < 0) { + free(branch); + return dummy_git_repository; + } + if (remote) + *remote = NULL; + *branchp = branch; + return repo; +} diff --git a/subsurface-core/git-access.h b/subsurface-core/git-access.h new file mode 100644 index 000000000..a2a9ba3ae --- /dev/null +++ b/subsurface-core/git-access.h @@ -0,0 +1,31 @@ +#ifndef GITACCESS_H +#define GITACCESS_H + +#include "git2.h" + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +enum remote_transport { RT_OTHER, RT_HTTPS, RT_SSH }; + +struct git_oid; +struct git_repository; +#define dummy_git_repository ((git_repository *)3ul) /* Random bogus pointer, not NULL */ +extern struct git_repository *is_git_repository(const char *filename, const char **branchp, const char **remote, bool dry_run); +extern int sync_with_remote(struct git_repository *repo, const char *remote, const char *branch, enum remote_transport rt); +extern int git_save_dives(struct git_repository *, const char *, const char *remote, bool select_only); +extern int git_load_dives(struct git_repository *, const char *); +extern int do_git_save(git_repository *repo, const char *branch, const char *remote, bool select_only, bool create_empty); +extern const char *saved_git_id; +extern void clear_git_id(void); +extern void set_git_id(const struct git_oid *); +void set_git_update_cb(int(*cb)(int)); +char *get_local_dir(const char *remote, const char *branch); +#ifdef __cplusplus +} +#endif +#endif // GITACCESS_H + diff --git a/subsurface-core/helpers.h b/subsurface-core/helpers.h new file mode 100644 index 000000000..5f2f2f2c5 --- /dev/null +++ b/subsurface-core/helpers.h @@ -0,0 +1,52 @@ +/* + * helpers.h + * + * header file for random helper functions of Subsurface + * + */ +#ifndef HELPERS_H +#define HELPERS_H + +#include +#include "dive.h" +#include "qthelper.h" + +QString get_depth_string(depth_t depth, bool showunit = false, bool showdecimal = true); +QString get_depth_string(int mm, bool showunit = false, bool showdecimal = true); +QString get_depth_unit(); +QString get_weight_string(weight_t weight, bool showunit = false); +QString get_weight_unit(); +QString get_cylinder_used_gas_string(cylinder_t *cyl, bool showunit = false); +QString get_temperature_string(temperature_t temp, bool showunit = false); +QString get_temp_unit(); +QString get_volume_string(volume_t volume, bool showunit = false, int mbar = 0); +QString get_volume_unit(); +QString get_pressure_string(pressure_t pressure, bool showunit = false); +QString get_pressure_unit(); +void set_default_dive_computer(const char *vendor, const char *product); +void set_default_dive_computer_device(const char *name); +void set_default_dive_computer_download_mode(int downloadMode); +QString getSubsurfaceDataPath(QString folderToFind); +QString getPrintingTemplatePathUser(); +QString getPrintingTemplatePathBundle(); +void copyPath(QString src, QString dst); +extern const QString get_dc_nickname(const char *model, uint32_t deviceid); +int gettimezoneoffset(timestamp_t when = 0); +int parseTemperatureToMkelvin(const QString &text); +QString get_dive_duration_string(timestamp_t when, QString hourText, QString minutesText); +QString get_dive_date_string(timestamp_t when); +QString get_short_dive_date_string(timestamp_t when); +bool is_same_day (timestamp_t trip_when, timestamp_t dive_when); +QString get_trip_date_string(timestamp_t when, int nr, bool getday); +QString uiLanguage(QLocale *callerLoc); +QLocale getLocale(); +QString getDateFormat(); +void selectedDivesGasUsed(QVector > &gasUsed); +QString getUserAgent(); + +#if defined __APPLE__ +#define TITLE_OR_TEXT(_t, _m) "", _t + "\n" + _m +#else +#define TITLE_OR_TEXT(_t, _m) _t, _m +#endif +#endif // HELPERS_H diff --git a/subsurface-core/libdivecomputer.c b/subsurface-core/libdivecomputer.c new file mode 100644 index 000000000..ca8378379 --- /dev/null +++ b/subsurface-core/libdivecomputer.c @@ -0,0 +1,1083 @@ +#include +#include +#include +#include +#include "gettext.h" +#include "dive.h" +#include "device.h" +#include "divelist.h" +#include "display.h" + +#include "libdivecomputer.h" +#include +#include + + +/* Christ. Libdivecomputer has the worst configuration system ever. */ +#ifdef HW_FROG_H +#define NOT_FROG , 0 +#define LIBDIVECOMPUTER_SUPPORTS_FROG +#else +#define NOT_FROG +#endif + +char *dumpfile_name; +char *logfile_name; +const char *progress_bar_text = ""; +double progress_bar_fraction = 0.0; + +static int stoptime, stopdepth, ndl, po2, cns; +static bool in_deco, first_temp_is_air; + +/* + * Directly taken from libdivecomputer's examples/common.c to improve + * the error messages resulting from libdc's return codes + */ +const char *errmsg (dc_status_t rc) +{ + switch (rc) { + case DC_STATUS_SUCCESS: + return "Success"; + case DC_STATUS_UNSUPPORTED: + return "Unsupported operation"; + case DC_STATUS_INVALIDARGS: + return "Invalid arguments"; + case DC_STATUS_NOMEMORY: + return "Out of memory"; + case DC_STATUS_NODEVICE: + return "No device found"; + case DC_STATUS_NOACCESS: + return "Access denied"; + case DC_STATUS_IO: + return "Input/output error"; + case DC_STATUS_TIMEOUT: + return "Timeout"; + case DC_STATUS_PROTOCOL: + return "Protocol error"; + case DC_STATUS_DATAFORMAT: + return "Data format error"; + case DC_STATUS_CANCELLED: + return "Cancelled"; + default: + return "Unknown error"; + } +} + +static dc_status_t create_parser(device_data_t *devdata, dc_parser_t **parser) +{ + return dc_parser_new(parser, devdata->device); +} + +static int parse_gasmixes(device_data_t *devdata, struct dive *dive, dc_parser_t *parser, int ngases) +{ + static bool shown_warning = false; + int i, rc; + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + int ntanks = 0; + rc = dc_parser_get_field(parser, DC_FIELD_TANK_COUNT, 0, &ntanks); + if (rc == DC_STATUS_SUCCESS) { + if (ntanks != ngases) { + shown_warning = true; + report_error("different number of gases (%d) and tanks (%d)", ngases, ntanks); + } + } + dc_tank_t tank = { 0 }; +#endif + + for (i = 0; i < ngases; i++) { + dc_gasmix_t gasmix = { 0 }; + int o2, he; + bool no_volume = true; + + rc = dc_parser_get_field(parser, DC_FIELD_GASMIX, i, &gasmix); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) + return rc; + + if (i >= MAX_CYLINDERS) + continue; + + o2 = rint(gasmix.oxygen * 1000); + he = rint(gasmix.helium * 1000); + + /* Ignore bogus data - libdivecomputer does some crazy stuff */ + if (o2 + he <= O2_IN_AIR || o2 > 1000) { + if (!shown_warning) { + shown_warning = true; + report_error("unlikely dive gas data from libdivecomputer: o2 = %d he = %d", o2, he); + } + o2 = 0; + } + if (he < 0 || o2 + he > 1000) { + if (!shown_warning) { + shown_warning = true; + report_error("unlikely dive gas data from libdivecomputer: o2 = %d he = %d", o2, he); + } + he = 0; + } + dive->cylinder[i].gasmix.o2.permille = o2; + dive->cylinder[i].gasmix.he.permille = he; + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + tank.volume = 0.0; + if (i < ntanks) { + rc = dc_parser_get_field(parser, DC_FIELD_TANK, i, &tank); + if (rc == DC_STATUS_SUCCESS) { + if (tank.type == DC_TANKVOLUME_IMPERIAL) { + dive->cylinder[i].type.size.mliter = rint(tank.volume * 1000); + dive->cylinder[i].type.workingpressure.mbar = rint(tank.workpressure * 1000); + if (same_string(devdata->model, "Suunto EON Steel")) { + /* Suunto EON Steele gets this wrong. Badly. + * but on the plus side it only supports a few imperial sizes, + * so let's try and guess at least the most common ones. + * First, the pressures are off by a constant factor. WTF? + * Then we can round the wet sizes so we get to multiples of 10 + * for cuft sizes (as that's all that you can enter) */ + dive->cylinder[i].type.workingpressure.mbar *= 206.843 / 206.7; + char name_buffer[9]; + int rounded_size = ml_to_cuft(gas_volume(&dive->cylinder[i], + dive->cylinder[i].type.workingpressure)); + rounded_size = (int)((rounded_size + 5) / 10) * 10; + switch (dive->cylinder[i].type.workingpressure.mbar) { + case 206843: + snprintf(name_buffer, 9, "AL%d", rounded_size); + break; + case 234422: /* this is wrong - HP tanks tend to be 3440, but Suunto only allows 3400 */ + snprintf(name_buffer, 9, "HP%d", rounded_size); + break; + case 179263: + snprintf(name_buffer, 9, "LP+%d", rounded_size); + break; + case 165474: + snprintf(name_buffer, 9, "LP%d", rounded_size); + break; + default: + snprintf(name_buffer, 9, "%d cuft", rounded_size); + break; + } + dive->cylinder[i].type.description = copy_string(name_buffer); + dive->cylinder[i].type.size.mliter = cuft_to_l(rounded_size) * 1000 / + mbar_to_atm(dive->cylinder[i].type.workingpressure.mbar); + } + } else if (tank.type == DC_TANKVOLUME_METRIC) { + dive->cylinder[i].type.size.mliter = rint(tank.volume * 1000); + } + if (tank.gasmix != i) { // we don't handle this, yet + shown_warning = true; + report_error("gasmix %d for tank %d doesn't match", tank.gasmix, i); + } + } + } + if (!IS_FP_SAME(tank.volume, 0.0)) + no_volume = false; + + // this new API also gives us the beginning and end pressure for the tank + if (!IS_FP_SAME(tank.beginpressure, 0.0) && !IS_FP_SAME(tank.endpressure, 0.0)) { + dive->cylinder[i].start.mbar = tank.beginpressure * 1000; + dive->cylinder[i].end.mbar = tank.endpressure * 1000; + } +#endif + if (no_volume) { + /* for the first tank, if there is no tanksize available from the + * dive computer, fill in the default tank information (if set) */ + fill_default_cylinder(&dive->cylinder[i]); + } + /* whatever happens, make sure there is a name for the cylinder */ + if (same_string(dive->cylinder[i].type.description, "")) + dive->cylinder[i].type.description = strdup(translate("gettextFromC", "unknown")); + } + return DC_STATUS_SUCCESS; +} + +static void handle_event(struct divecomputer *dc, struct sample *sample, dc_sample_value_t value) +{ + int type, time; + /* we mark these for translation here, but we store the untranslated strings + * and only translate them when they are displayed on screen */ + static const char *events[] = { + QT_TRANSLATE_NOOP("gettextFromC", "none"), QT_TRANSLATE_NOOP("gettextFromC", "deco stop"), QT_TRANSLATE_NOOP("gettextFromC", "rbt"), QT_TRANSLATE_NOOP("gettextFromC", "ascent"), QT_TRANSLATE_NOOP("gettextFromC", "ceiling"), QT_TRANSLATE_NOOP("gettextFromC", "workload"), + QT_TRANSLATE_NOOP("gettextFromC", "transmitter"), QT_TRANSLATE_NOOP("gettextFromC", "violation"), QT_TRANSLATE_NOOP("gettextFromC", "bookmark"), QT_TRANSLATE_NOOP("gettextFromC", "surface"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop"), + QT_TRANSLATE_NOOP("gettextFromC", "gaschange"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop (voluntary)"), QT_TRANSLATE_NOOP("gettextFromC", "safety stop (mandatory)"), + QT_TRANSLATE_NOOP("gettextFromC", "deepstop"), QT_TRANSLATE_NOOP("gettextFromC", "ceiling (safety stop)"), QT_TRANSLATE_NOOP3("gettextFromC", "below floor", "event showing dive is below deco floor and adding deco time"), QT_TRANSLATE_NOOP("gettextFromC", "divetime"), + QT_TRANSLATE_NOOP("gettextFromC", "maxdepth"), QT_TRANSLATE_NOOP("gettextFromC", "OLF"), QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚"), QT_TRANSLATE_NOOP("gettextFromC", "airtime"), QT_TRANSLATE_NOOP("gettextFromC", "rgbm"), QT_TRANSLATE_NOOP("gettextFromC", "heading"), + QT_TRANSLATE_NOOP("gettextFromC", "tissue level warning"), QT_TRANSLATE_NOOP("gettextFromC", "gaschange"), QT_TRANSLATE_NOOP("gettextFromC", "non stop time") + }; + const int nr_events = sizeof(events) / sizeof(const char *); + const char *name; + /* + * Just ignore surface events. They are pointless. What "surface" + * means depends on the dive computer (and possibly even settings + * in the dive computer). It does *not* necessarily mean "depth 0", + * so don't even turn it into that. + */ + if (value.event.type == SAMPLE_EVENT_SURFACE) + return; + + /* + * Other evens might be more interesting, but for now we just print them out. + */ + type = value.event.type; + name = QT_TRANSLATE_NOOP("gettextFromC", "invalid event number"); + if (type < nr_events) + name = events[type]; + + time = value.event.time; + if (sample) + time += sample->time.seconds; + + add_event(dc, time, type, value.event.flags, value.event.value, name); +} + +void +sample_cb(dc_sample_type_t type, dc_sample_value_t value, void *userdata) +{ + static unsigned int nsensor = 0; + struct divecomputer *dc = userdata; + struct sample *sample; + + /* + * We fill in the "previous" sample - except for DC_SAMPLE_TIME, + * which creates a new one. + */ + sample = dc->samples ? dc->sample + dc->samples - 1 : NULL; + + /* + * Ok, sanity check. + * If first sample is not a DC_SAMPLE_TIME, Allocate a sample for us + */ + if (sample == NULL && type != DC_SAMPLE_TIME) + sample = prepare_sample(dc); + + switch (type) { + case DC_SAMPLE_TIME: + nsensor = 0; + + // The previous sample gets some sticky values + // that may have been around from before, even + // if there was no new data + if (sample) { + sample->in_deco = in_deco; + sample->ndl.seconds = ndl; + sample->stoptime.seconds = stoptime; + sample->stopdepth.mm = stopdepth; + sample->setpoint.mbar = po2; + sample->cns = cns; + } + // Create a new sample. + // Mark depth as negative + sample = prepare_sample(dc); + sample->time.seconds = value.time; + sample->depth.mm = -1; + finish_sample(dc); + break; + case DC_SAMPLE_DEPTH: + sample->depth.mm = rint(value.depth * 1000); + break; + case DC_SAMPLE_PRESSURE: + sample->sensor = value.pressure.tank; + sample->cylinderpressure.mbar = rint(value.pressure.value * 1000); + break; + case DC_SAMPLE_TEMPERATURE: + sample->temperature.mkelvin = C_to_mkelvin(value.temperature); + break; + case DC_SAMPLE_EVENT: + handle_event(dc, sample, value); + break; + case DC_SAMPLE_RBT: + sample->rbt.seconds = (!strncasecmp(dc->model, "suunto", 6)) ? value.rbt : value.rbt * 60; + break; + case DC_SAMPLE_HEARTBEAT: + sample->heartbeat = value.heartbeat; + break; + case DC_SAMPLE_BEARING: + sample->bearing.degrees = value.bearing; + break; +#ifdef DEBUG_DC_VENDOR + case DC_SAMPLE_VENDOR: + printf(" ", FRACTION(sample->time.seconds, 60), + value.vendor.type, value.vendor.size); + for (int i = 0; i < value.vendor.size; ++i) + printf("%02X", ((unsigned char *)value.vendor.data)[i]); + printf("\n"); + break; +#endif +#if DC_VERSION_CHECK(0, 3, 0) + case DC_SAMPLE_SETPOINT: + /* for us a setpoint means constant pO2 from here */ + sample->setpoint.mbar = po2 = rint(value.setpoint * 1000); + break; + case DC_SAMPLE_PPO2: + if (nsensor < 3) + sample->o2sensor[nsensor].mbar = rint(value.ppo2 * 1000); + else + report_error("%d is more o2 sensors than we can handle", nsensor); + nsensor++; + // Set the amount of detected o2 sensors + if (nsensor > dc->no_o2sensors) + dc->no_o2sensors = nsensor; + break; + case DC_SAMPLE_CNS: + sample->cns = cns = rint(value.cns * 100); + break; + case DC_SAMPLE_DECO: + if (value.deco.type == DC_DECO_NDL) { + sample->ndl.seconds = ndl = value.deco.time; + sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); + sample->in_deco = in_deco = false; + } else if (value.deco.type == DC_DECO_DECOSTOP || + value.deco.type == DC_DECO_DEEPSTOP) { + sample->in_deco = in_deco = true; + sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); + sample->stoptime.seconds = stoptime = value.deco.time; + ndl = 0; + } else if (value.deco.type == DC_DECO_SAFETYSTOP) { + sample->in_deco = in_deco = false; + sample->stopdepth.mm = stopdepth = rint(value.deco.depth * 1000.0); + sample->stoptime.seconds = stoptime = value.deco.time; + } +#endif + default: + break; + } +} + +static void dev_info(device_data_t *devdata, const char *fmt, ...) +{ + static char buffer[1024]; + va_list ap; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + progress_bar_text = buffer; +} + +static int import_dive_number = 0; + +static int parse_samples(device_data_t *devdata, struct divecomputer *dc, dc_parser_t *parser) +{ + // Parse the sample data. + return dc_parser_samples_foreach(parser, sample_cb, dc); +} + +static int might_be_same_dc(struct divecomputer *a, struct divecomputer *b) +{ + if (!a->model || !b->model) + return 1; + if (strcasecmp(a->model, b->model)) + return 0; + if (!a->deviceid || !b->deviceid) + return 1; + return a->deviceid == b->deviceid; +} + +static int match_one_dive(struct divecomputer *a, struct dive *dive) +{ + struct divecomputer *b = &dive->dc; + + /* + * Walk the existing dive computer data, + * see if we have a match (or an anti-match: + * the same dive computer but a different + * dive ID). + */ + do { + int match = match_one_dc(a, b); + if (match) + return match > 0; + b = b->next; + } while (b); + + /* Ok, no exact dive computer match. Does the date match? */ + b = &dive->dc; + do { + if (a->when == b->when && might_be_same_dc(a, b)) + return 1; + b = b->next; + } while (b); + + return 0; +} + +/* + * Check if this dive already existed before the import + */ +static int find_dive(struct divecomputer *match) +{ + int i; + + for (i = 0; i < dive_table.preexisting; i++) { + struct dive *old = dive_table.dives[i]; + + if (match_one_dive(match, old)) + return 1; + } + return 0; +} + +static inline int year(int year) +{ + if (year < 70) + return year + 2000; + if (year < 100) + return year + 1900; + return year; +} + +/* + * Like g_strdup_printf(), but without the stupid g_malloc/g_free confusion. + * And we limit the string to some arbitrary size. + */ +static char *str_printf(const char *fmt, ...) +{ + va_list args; + char buf[1024]; + + va_start(args, fmt); + vsnprintf(buf, sizeof(buf) - 1, fmt, args); + va_end(args); + buf[sizeof(buf) - 1] = 0; + return strdup(buf); +} + +/* + * The dive ID for libdivecomputer dives is the first word of the + * SHA1 of the fingerprint, if it exists. + * + * NOTE! This is byte-order dependent, and I don't care. + */ +static uint32_t calculate_diveid(const unsigned char *fingerprint, unsigned int fsize) +{ + uint32_t csum[5]; + + if (!fingerprint || !fsize) + return 0; + + SHA1(fingerprint, fsize, (unsigned char *)csum); + return csum[0]; +} + +#ifdef DC_FIELD_STRING +static uint32_t calculate_string_hash(const char *str) +{ + return calculate_diveid((const unsigned char *)str, strlen(str)); +} + +static void parse_string_field(struct dive *dive, dc_field_string_t *str) +{ + // Our dive ID is the string hash of the "Dive ID" string + if (!strcmp(str->desc, "Dive ID")) { + if (!dive->dc.diveid) + dive->dc.diveid = calculate_string_hash(str->value); + return; + } + add_extra_data(&dive->dc, str->desc, str->value); + if (!strcmp(str->desc, "Serial")) { + dive->dc.serial = strdup(str->value); + /* should we just overwrite this whenever we have the "Serial" field? + * It's a much better deviceid then what we have so far... for now I'm leaving it as is */ + if (!dive->dc.deviceid) + dive->dc.deviceid = calculate_string_hash(str->value); + return; + } + if (!strcmp(str->desc, "FW Version")) { + dive->dc.fw_version = strdup(str->value); + return; + } +} +#endif + +static dc_status_t libdc_header_parser(dc_parser_t *parser, struct device_data_t *devdata, struct dive *dive) +{ + dc_status_t rc = 0; + dc_datetime_t dt = { 0 }; + struct tm tm; + + rc = dc_parser_get_datetime(parser, &dt); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the datetime")); + return rc; + } + + dive->dc.deviceid = devdata->deviceid; + + if (rc == DC_STATUS_SUCCESS) { + tm.tm_year = dt.year; + tm.tm_mon = dt.month - 1; + tm.tm_mday = dt.day; + tm.tm_hour = dt.hour; + tm.tm_min = dt.minute; + tm.tm_sec = dt.second; + dive->when = dive->dc.when = utc_mktime(&tm); + } + + // Parse the divetime. + const char *date_string = get_dive_date_c_string(dive->when); + dev_info(devdata, translate("gettextFromC", "Dive %d: %s"), import_dive_number, date_string); + free((void *)date_string); + + unsigned int divetime = 0; + rc = dc_parser_get_field(parser, DC_FIELD_DIVETIME, 0, &divetime); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the divetime")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.duration.seconds = divetime; + + // Parse the maxdepth. + double maxdepth = 0.0; + rc = dc_parser_get_field(parser, DC_FIELD_MAXDEPTH, 0, &maxdepth); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the maxdepth")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.maxdepth.mm = rint(maxdepth * 1000); + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + // if this is defined then we have a fairly late version of libdivecomputer + // from the 0.5 development cylcle - most likely temperatures and tank sizes + // are supported + + // Parse temperatures + double temperature; + dc_field_type_t temp_fields[] = {DC_FIELD_TEMPERATURE_SURFACE, + DC_FIELD_TEMPERATURE_MAXIMUM, + DC_FIELD_TEMPERATURE_MINIMUM}; + for (int i = 0; i < 3; i++) { + rc = dc_parser_get_field(parser, temp_fields[i], 0, &temperature); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing temperature")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + switch(i) { + case 0: + dive->dc.airtemp.mkelvin = C_to_mkelvin(temperature); + break; + case 1: // we don't distinguish min and max water temp here, so take min if given, max otherwise + case 2: + dive->dc.watertemp.mkelvin = C_to_mkelvin(temperature); + break; + } + } +#endif + + // Parse the gas mixes. + unsigned int ngases = 0; + rc = dc_parser_get_field(parser, DC_FIELD_GASMIX_COUNT, 0, &ngases); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the gas mix count")); + return rc; + } + +#if DC_VERSION_CHECK(0, 3, 0) + // Check if the libdivecomputer version already supports salinity & atmospheric + dc_salinity_t salinity = { + .type = DC_WATER_SALT, + .density = SEAWATER_SALINITY / 10.0 + }; + rc = dc_parser_get_field(parser, DC_FIELD_SALINITY, 0, &salinity); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error obtaining water salinity")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.salinity = rint(salinity.density * 10.0); + + double surface_pressure = 0; + rc = dc_parser_get_field(parser, DC_FIELD_ATMOSPHERIC, 0, &surface_pressure); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error obtaining surface pressure")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + dive->dc.surface_pressure.mbar = rint(surface_pressure * 1000.0); +#endif + +#ifdef DC_FIELD_STRING + // The dive parsing may give us more device information + int idx; + for (idx = 0; idx < 100; idx++) { + dc_field_string_t str = { NULL }; + rc = dc_parser_get_field(parser, DC_FIELD_STRING, idx, &str); + if (rc != DC_STATUS_SUCCESS) + break; + if (!str.desc || !str.value) + break; + parse_string_field(dive, &str); + } +#endif + +#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN) + dc_divemode_t divemode; + rc = dc_parser_get_field(parser, DC_FIELD_DIVEMODE, 0, &divemode); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error obtaining divemode")); + return rc; + } + if (rc == DC_STATUS_SUCCESS) + switch(divemode) { + case DC_DIVEMODE_FREEDIVE: + dive->dc.divemode = FREEDIVE; + break; + case DC_DIVEMODE_GAUGE: + case DC_DIVEMODE_OC: /* Open circuit */ + dive->dc.divemode = OC; + break; + case DC_DIVEMODE_CC: /* Closed circuit */ + dive->dc.divemode = CCR; + break; + } +#endif + + rc = parse_gasmixes(devdata, dive, parser, ngases); + if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_UNSUPPORTED) { + dev_info(devdata, translate("gettextFromC", "Error parsing the gas mix")); + return rc; + } + + return DC_STATUS_SUCCESS; +} + +/* returns true if we want libdivecomputer's dc_device_foreach() to continue, + * false otherwise */ +static int dive_cb(const unsigned char *data, unsigned int size, + const unsigned char *fingerprint, unsigned int fsize, + void *userdata) +{ + int rc; + dc_parser_t *parser = NULL; + device_data_t *devdata = userdata; + struct dive *dive = NULL; + + /* reset the deco / ndl data */ + ndl = stoptime = stopdepth = 0; + in_deco = false; + + rc = create_parser(devdata, &parser); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("gettextFromC", "Unable to create parser for %s %s"), devdata->vendor, devdata->product); + return false; + } + + rc = dc_parser_set_data(parser, data, size); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("gettextFromC", "Error registering the data")); + goto error_exit; + } + + import_dive_number++; + dive = alloc_dive(); + + // Parse the dive's header data + rc = libdc_header_parser (parser, devdata, dive); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("getextFromC", "Error parsing the header")); + goto error_exit; + } + + dive->dc.model = strdup(devdata->model); + dive->dc.diveid = calculate_diveid(fingerprint, fsize); + + // Initialize the sample data. + rc = parse_samples(devdata, &dive->dc, parser); + if (rc != DC_STATUS_SUCCESS) { + dev_info(devdata, translate("gettextFromC", "Error parsing the samples")); + goto error_exit; + } + + /* If we already saw this dive, abort. */ + if (!devdata->force_download && find_dive(&dive->dc)) + goto error_exit; + + dc_parser_destroy(parser); + + /* Various libdivecomputer interface fixups */ + if (first_temp_is_air && dive->dc.samples) { + dive->dc.airtemp = dive->dc.sample[0].temperature; + dive->dc.sample[0].temperature.mkelvin = 0; + } + + if (devdata->create_new_trip) { + if (!devdata->trip) + devdata->trip = create_and_hookup_trip_from_dive(dive); + else + add_dive_to_trip(dive, devdata->trip); + } + + dive->downloaded = true; + record_dive_to_table(dive, devdata->download_table); + mark_divelist_changed(true); + return true; + +error_exit: + dc_parser_destroy(parser); + free(dive); + return false; + +} + +/* + * The device ID for libdivecomputer devices is the first 32-bit word + * of the SHA1 hash of the model/firmware/serial numbers. + * + * NOTE! This is byte-order-dependent. And I can't find it in myself to + * care. + */ +static uint32_t calculate_sha1(unsigned int model, unsigned int firmware, unsigned int serial) +{ + SHA_CTX ctx; + uint32_t csum[5]; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, &model, sizeof(model)); + SHA1_Update(&ctx, &firmware, sizeof(firmware)); + SHA1_Update(&ctx, &serial, sizeof(serial)); + SHA1_Final((unsigned char *)csum, &ctx); + return csum[0]; +} + +/* + * libdivecomputer has returned two different serial numbers for the + * same device in different versions. First it used to just do the four + * bytes as one 32-bit number, then it turned it into a decimal number + * with each byte giving two digits (0-99). + * + * The only way we can tell is by looking at the format of the number, + * so we'll just fix it to the first format. + */ +static unsigned int undo_libdivecomputer_suunto_nr_changes(unsigned int serial) +{ + unsigned char b0, b1, b2, b3; + + /* + * The second format will never have more than 8 decimal + * digits, so do a cheap check first + */ + if (serial >= 100000000) + return serial; + + /* The original format seems to be four bytes of values 00-99 */ + b0 = (serial >> 0) & 0xff; + b1 = (serial >> 8) & 0xff; + b2 = (serial >> 16) & 0xff; + b3 = (serial >> 24) & 0xff; + + /* Looks like an old-style libdivecomputer serial number */ + if ((b0 < 100) && (b1 < 100) && (b2 < 100) && (b3 < 100)) + return serial; + + /* Nope, it was converted. */ + b0 = serial % 100; + serial /= 100; + b1 = serial % 100; + serial /= 100; + b2 = serial % 100; + serial /= 100; + b3 = serial % 100; + + serial = b0 + (b1 << 8) + (b2 << 16) + (b3 << 24); + return serial; +} + +static unsigned int fixup_suunto_versions(device_data_t *devdata, const dc_event_devinfo_t *devinfo) +{ + unsigned int serial = devinfo->serial; + char serial_nr[13] = ""; + char firmware[13] = ""; + + first_temp_is_air = 1; + + serial = undo_libdivecomputer_suunto_nr_changes(serial); + + if (serial) { + snprintf(serial_nr, sizeof(serial_nr), "%02d%02d%02d%02d", + (devinfo->serial >> 24) & 0xff, + (devinfo->serial >> 16) & 0xff, + (devinfo->serial >> 8) & 0xff, + (devinfo->serial >> 0) & 0xff); + } + if (devinfo->firmware) { + snprintf(firmware, sizeof(firmware), "%d.%d.%d", + (devinfo->firmware >> 16) & 0xff, + (devinfo->firmware >> 8) & 0xff, + (devinfo->firmware >> 0) & 0xff); + } + create_device_node(devdata->model, devdata->deviceid, serial_nr, firmware, ""); + + return serial; +} + +static void event_cb(dc_device_t *device, dc_event_type_t event, const void *data, void *userdata) +{ + const dc_event_progress_t *progress = data; + const dc_event_devinfo_t *devinfo = data; + const dc_event_clock_t *clock = data; + const dc_event_vendor_t *vendor = data; + device_data_t *devdata = userdata; + unsigned int serial; + + switch (event) { + case DC_EVENT_WAITING: + dev_info(devdata, translate("gettextFromC", "Event: waiting for user action")); + break; + case DC_EVENT_PROGRESS: + if (!progress->maximum) + break; + progress_bar_fraction = (double)progress->current / (double)progress->maximum; + break; + case DC_EVENT_DEVINFO: + dev_info(devdata, translate("gettextFromC", "model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)"), + devinfo->model, devinfo->model, + devinfo->firmware, devinfo->firmware, + devinfo->serial, devinfo->serial); + if (devdata->libdc_logfile) { + fprintf(devdata->libdc_logfile, "Event: model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)\n", + devinfo->model, devinfo->model, + devinfo->firmware, devinfo->firmware, + devinfo->serial, devinfo->serial); + } + /* + * libdivecomputer doesn't give serial numbers in the proper string form, + * so we have to see if we can do some vendor-specific munging. + */ + serial = devinfo->serial; + if (!strcmp(devdata->vendor, "Suunto")) + serial = fixup_suunto_versions(devdata, devinfo); + devdata->deviceid = calculate_sha1(devinfo->model, devinfo->firmware, serial); + /* really, serial and firmware version are NOT numbers. We'll try to save them here + * in something that might work, but this really needs to be handled with the + * DC_FIELD_STRING interface instead */ + devdata->libdc_serial = devinfo->serial; + devdata->libdc_firmware = devinfo->firmware; + break; + case DC_EVENT_CLOCK: + dev_info(devdata, translate("gettextFromC", "Event: systime=%" PRId64 ", devtime=%u\n"), + (uint64_t)clock->systime, clock->devtime); + if (devdata->libdc_logfile) { + fprintf(devdata->libdc_logfile, "Event: systime=%" PRId64 ", devtime=%u\n", + (uint64_t)clock->systime, clock->devtime); + } + break; + case DC_EVENT_VENDOR: + if (devdata->libdc_logfile) { + fprintf(devdata->libdc_logfile, "Event: vendor="); + for (unsigned int i = 0; i < vendor->size; ++i) + fprintf(devdata->libdc_logfile, "%02X", vendor->data[i]); + fprintf(devdata->libdc_logfile, "\n"); + } + break; + default: + break; + } +} + +int import_thread_cancelled; + +static int +cancel_cb(void *userdata) +{ + return import_thread_cancelled; +} + +static const char *do_device_import(device_data_t *data) +{ + dc_status_t rc; + dc_device_t *device = data->device; + + data->model = str_printf("%s %s", data->vendor, data->product); + + // Register the event handler. + int events = DC_EVENT_WAITING | DC_EVENT_PROGRESS | DC_EVENT_DEVINFO | DC_EVENT_CLOCK | DC_EVENT_VENDOR; + rc = dc_device_set_events(device, events, event_cb, data); + if (rc != DC_STATUS_SUCCESS) + return translate("gettextFromC", "Error registering the event handler."); + + // Register the cancellation handler. + rc = dc_device_set_cancel(device, cancel_cb, data); + if (rc != DC_STATUS_SUCCESS) + return translate("gettextFromC", "Error registering the cancellation handler."); + + if (data->libdc_dump) { + dc_buffer_t *buffer = dc_buffer_new(0); + + rc = dc_device_dump(device, buffer); + if (rc == DC_STATUS_SUCCESS && dumpfile_name) { + FILE *fp = subsurface_fopen(dumpfile_name, "wb"); + if (fp != NULL) { + fwrite(dc_buffer_get_data(buffer), 1, dc_buffer_get_size(buffer), fp); + fclose(fp); + } + } + + dc_buffer_free(buffer); + } else { + rc = dc_device_foreach(device, dive_cb, data); + } + + if (rc != DC_STATUS_SUCCESS) { + progress_bar_fraction = 0.0; + return translate("gettextFromC", "Dive data import error"); + } + + /* All good */ + return NULL; +} + +void +logfunc(dc_context_t *context, dc_loglevel_t loglevel, const char *file, unsigned int line, const char *function, const char *msg, void *userdata) +{ + const char *loglevels[] = { "NONE", "ERROR", "WARNING", "INFO", "DEBUG", "ALL" }; + + FILE *fp = (FILE *)userdata; + + if (loglevel == DC_LOGLEVEL_ERROR || loglevel == DC_LOGLEVEL_WARNING) { + fprintf(fp, "%s: %s [in %s:%d (%s)]\n", loglevels[loglevel], msg, file, line, function); + } else { + fprintf(fp, "%s: %s\n", loglevels[loglevel], msg); + } +} + +const char *do_libdivecomputer_import(device_data_t *data) +{ + dc_status_t rc; + const char *err; + FILE *fp = NULL; + + import_dive_number = 0; + first_temp_is_air = 0; + data->device = NULL; + data->context = NULL; + + if (data->libdc_log && logfile_name) + fp = subsurface_fopen(logfile_name, "w"); + + data->libdc_logfile = fp; + + rc = dc_context_new(&data->context); + if (rc != DC_STATUS_SUCCESS) + return translate("gettextFromC", "Unable to create libdivecomputer context"); + + if (fp) { + dc_context_set_loglevel(data->context, DC_LOGLEVEL_ALL); + dc_context_set_logfunc(data->context, logfunc, fp); + } + + err = translate("gettextFromC", "Unable to open %s %s (%s)"); + +#if defined(SSRF_CUSTOM_SERIAL) + dc_serial_t *serial_device = NULL; + + if (data->bluetooth_mode) { +#ifdef BT_SUPPORT + rc = dc_serial_qt_open(&serial_device, data->context, data->devname); +#endif +#ifdef SERIAL_FTDI + } else if (!strcmp(data->devname, "ftdi")) { + rc = dc_serial_ftdi_open(&serial_device, data->context); +#endif + } + + if (rc != DC_STATUS_SUCCESS) { + report_error(errmsg(rc)); + } else if (serial_device) { + rc = dc_device_custom_open(&data->device, data->context, data->descriptor, serial_device); + } else { +#else + { +#endif + rc = dc_device_open(&data->device, data->context, data->descriptor, data->devname); + + if (rc != DC_STATUS_SUCCESS && subsurface_access(data->devname, R_OK | W_OK) != 0) + err = translate("gettextFromC", "Insufficient privileges to open the device %s %s (%s)"); + } + + if (rc == DC_STATUS_SUCCESS) { + err = do_device_import(data); + /* TODO: Show the logfile to the user on error. */ + dc_device_close(data->device); + data->device = NULL; + } + + dc_context_free(data->context); + data->context = NULL; + + if (fp) { + fclose(fp); + } + + return err; +} + +/* + * Parse data buffers instead of dc devices downloaded data. + * Intended to be used to parse profile data from binary files during import tasks. + * Actually included Uwatec families because of works on datatrak and smartrak logs + * and OSTC families for OSTCTools logs import. + * For others, simply include them in the switch (check parameters). + * Note that dc_descriptor_t in data *must* have been filled using dc_descriptor_iterator() + * calls. + */ +dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size) +{ + dc_status_t rc; + dc_parser_t *parser = NULL; + + switch (data->descriptor->type) { + case DC_FAMILY_UWATEC_ALADIN: + case DC_FAMILY_UWATEC_MEMOMOUSE: + rc = uwatec_memomouse_parser_create(&parser, data->context, 0, 0); + break; + case DC_FAMILY_UWATEC_SMART: + case DC_FAMILY_UWATEC_MERIDIAN: + rc = uwatec_smart_parser_create (&parser, data->context, data->descriptor->model, 0, 0); + break; + case DC_FAMILY_HW_OSTC: +#if defined(SSRF_CUSTOM_SERIAL) + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid, 0); +#else + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid); +#endif + break; + case DC_FAMILY_HW_FROG: + case DC_FAMILY_HW_OSTC3: +#if defined(SSRF_CUSTOM_SERIAL) + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid, 1); +#else + rc = hw_ostc_parser_create (&parser, data->context, data->deviceid); +#endif + break; + default: + report_error("Device type not handled!"); + return DC_STATUS_UNSUPPORTED; + } + if (rc != DC_STATUS_SUCCESS) { + report_error("Error creating parser."); + dc_parser_destroy (parser); + return rc; + } + rc = dc_parser_set_data(parser, buffer, size); + if (rc != DC_STATUS_SUCCESS) { + report_error("Error registering the data."); + dc_parser_destroy (parser); + return rc; + } + // Do not parse Aladin/Memomouse headers as they are fakes + // Do not return on error, we can still parse the samples + if (data->descriptor->type != DC_FAMILY_UWATEC_ALADIN && data->descriptor->type != DC_FAMILY_UWATEC_MEMOMOUSE) { + rc = libdc_header_parser (parser, data, dive); + if (rc != DC_STATUS_SUCCESS) { + report_error("Error parsing the dive header data. Dive # %d\nStatus = %s", dive->number, errmsg(rc)); + } + } + rc = dc_parser_samples_foreach (parser, sample_cb, &dive->dc); + if (rc != DC_STATUS_SUCCESS) { + report_error("Error parsing the sample data. Dive # %d\nStatus = %s", dive->number, errmsg(rc)); + dc_parser_destroy (parser); + return rc; + } + dc_parser_destroy(parser); + return(DC_STATUS_SUCCESS); +} diff --git a/subsurface-core/libdivecomputer.h b/subsurface-core/libdivecomputer.h new file mode 100644 index 000000000..79817e509 --- /dev/null +++ b/subsurface-core/libdivecomputer.h @@ -0,0 +1,66 @@ +#ifndef LIBDIVECOMPUTER_H +#define LIBDIVECOMPUTER_H + + +/* libdivecomputer */ +#include +#include +#include + +#include "dive.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct dc_descriptor_t { + const char *vendor; + const char *product; + dc_family_t type; + unsigned int model; +}; + +/* don't forget to include the UI toolkit specific display-XXX.h first + to get the definition of progressbar_t */ +typedef struct device_data_t +{ + dc_descriptor_t *descriptor; + const char *vendor, *product, *devname; + const char *model; + uint32_t libdc_firmware, libdc_serial; + uint32_t deviceid, diveid; + dc_device_t *device; + dc_context_t *context; + struct dive_trip *trip; + int preexisting; + bool force_download; + bool create_new_trip; + bool libdc_log; + bool libdc_dump; + bool bluetooth_mode; + FILE *libdc_logfile; + struct dive_table *download_table; +} device_data_t; + +const char *errmsg (dc_status_t rc); +const char *do_libdivecomputer_import(device_data_t *data); +const char *do_uemis_import(device_data_t *data); +dc_status_t libdc_buffer_parser(struct dive *dive, device_data_t *data, unsigned char *buffer, int size); +void logfunc(dc_context_t *context, dc_loglevel_t loglevel, const char *file, unsigned int line, const char *function, const char *msg, void *userdata); + +extern int import_thread_cancelled; +extern const char *progress_bar_text; +extern double progress_bar_fraction; +extern char *logfile_name; +extern char *dumpfile_name; + +#if SSRF_CUSTOM_SERIAL +extern dc_status_t dc_serial_qt_open(dc_serial_t **out, dc_context_t *context, const char *devaddr); +extern dc_status_t dc_serial_ftdi_open(dc_serial_t **out, dc_context_t *context); +#endif + +#ifdef __cplusplus +} +#endif + +#endif // LIBDIVECOMPUTER_H diff --git a/subsurface-core/linux.c b/subsurface-core/linux.c new file mode 100644 index 000000000..d4131c7ea --- /dev/null +++ b/subsurface-core/linux.c @@ -0,0 +1,225 @@ +/* linux.c */ +/* implements Linux specific functions */ +#include "dive.h" +#include "display.h" +#include "membuffer.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// the DE should provide us with a default font and font size... +const char linux_system_divelist_default_font[] = "Sans"; +const char *system_divelist_default_font = linux_system_divelist_default_font; +double system_divelist_default_font_size = -1.0; + +void subsurface_OS_pref_setup(void) +{ + // nothing +} + +bool subsurface_ignore_font(const char *font) +{ + // there are no old default fonts to ignore + return false; +} + +void subsurface_user_info(struct user_info *user) +{ + struct passwd *pwd = getpwuid(getuid()); + const char *username = getenv("USER"); + + if (pwd) { + if (pwd->pw_gecos && *pwd->pw_gecos) + user->name = pwd->pw_gecos; + if (!username) + username = pwd->pw_name; + } + if (username && *username) { + char hostname[64]; + struct membuffer mb = { 0 }; + gethostname(hostname, sizeof(hostname)); + put_format(&mb, "%s@%s", username, hostname); + user->email = mb_cstring(&mb); + } +} + +static const char *system_default_path_append(const char *append) +{ + const char *home = getenv("HOME"); + if (!home) + home = "~"; + const char *path = "/.subsurface"; + + int len = strlen(home) + strlen(path) + 1; + if (append) + len += strlen(append) + 1; + + char *buffer = (char *)malloc(len); + memset(buffer, 0, len); + strcat(buffer, home); + strcat(buffer, path); + if (append) { + strcat(buffer, "/"); + strcat(buffer, append); + } + + return buffer; +} + +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +const char *system_default_filename(void) +{ + static char *filename = NULL; + if (!filename) { + const char *user = getenv("LOGNAME"); + if (same_string(user, "")) + user = "username"; + filename = calloc(strlen(user) + 5, 1); + strcat(filename, user); + strcat(filename, ".xml"); + } + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + int index = -1, entries = 0; + DIR *dp = NULL; + struct dirent *ep = NULL; + size_t i; + FILE *file; + char *line = NULL; + char *fname; + size_t len; + if (dc_type != DC_TYPE_UEMIS) { + const char *dirname = "/dev"; + const char *patterns[] = { + "ttyUSB*", + "ttyS*", + "ttyACM*", + "rfcomm*", + NULL + }; + + dp = opendir(dirname); + if (dp == NULL) { + return -1; + } + + while ((ep = readdir(dp)) != NULL) { + for (i = 0; patterns[i] != NULL; ++i) { + if (fnmatch(patterns[i], ep->d_name, 0) == 0) { + char filename[1024]; + int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); + if (n >= sizeof(filename)) { + closedir(dp); + return -1; + } + callback(filename, userdata); + if (is_default_dive_computer_device(filename)) + index = entries; + entries++; + break; + } + } + } + closedir(dp); + } + if (dc_type != DC_TYPE_SERIAL) { + int num_uemis = 0; + file = fopen("/proc/mounts", "r"); + if (file == NULL) + return index; + + while ((getline(&line, &len, file)) != -1) { + char *ptr = strstr(line, "UEMISSDA"); + if (ptr) { + char *end = ptr, *start = ptr; + while (start > line && *start != ' ') + start--; + if (*start == ' ') + start++; + while (*end != ' ' && *end != '\0') + end++; + + *end = '\0'; + fname = strdup(start); + + callback(fname, userdata); + + if (is_default_dive_computer_device(fname)) + index = entries; + entries++; + num_uemis++; + free((void *)fname); + } + } + free(line); + fclose(file); + if (num_uemis == 1 && entries == 1) /* if we found only one and it's a mounted Uemis, pick it */ + index = 0; + } + return index; +} + +/* NOP wrappers to comform with windows.c */ +int subsurface_rename(const char *path, const char *newpath) +{ + return rename(path, newpath); +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + return open(path, oflags, mode); +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + return fopen(path, mode); +} + +void *subsurface_opendir(const char *path) +{ + return (void *)opendir(path); +} + +int subsurface_access(const char *path, int mode) +{ + return access(path, mode); +} + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ + return zip_open(path, flags, errorp); +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +void subsurface_console_init(bool dedicated) +{ + /* NOP */ +} + +void subsurface_console_exit(void) +{ + /* NOP */ +} diff --git a/subsurface-core/liquivision.c b/subsurface-core/liquivision.c new file mode 100644 index 000000000..295287c15 --- /dev/null +++ b/subsurface-core/liquivision.c @@ -0,0 +1,414 @@ +#include + +#include "dive.h" +#include "divelist.h" +#include "file.h" +#include "strndup.h" + +// Convert bytes into an INT +#define array_uint16_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) ) +#define array_uint32_le(p) ((unsigned int) (p)[0] \ + + ((p)[1]<<8) + ((p)[2]<<16) \ + + ((p)[3]<<24)) + +struct lv_event { + time_t time; + struct pressure { + int sensor; + int mbar; + } pressure; +}; + +uint16_t primary_sensor; + +static int handle_event_ver2(int code, const unsigned char *ps, unsigned int ps_ptr, struct lv_event *event) +{ + // Skip 4 bytes + return 4; +} + + +static int handle_event_ver3(int code, const unsigned char *ps, unsigned int ps_ptr, struct lv_event *event) +{ + int skip = 4; + uint16_t current_sensor; + + switch (code) { + case 0x0002: // Unknown + case 0x0004: // Unknown + skip = 4; + break; + case 0x0005: // Unknown + skip = 6; + break; + case 0x0007: // Gas + // 4 byte time + // 1 byte O2, 1 bye He + skip = 6; + break; + case 0x0008: + // 4 byte time + // 2 byte gas set point 2 + skip = 6; + break; + case 0x000f: + // Tank pressure + event->time = array_uint32_le(ps + ps_ptr); + + /* As far as I know, Liquivision supports 2 sensors, own and buddie's. This is my + * best guess how it is represented. */ + + current_sensor = array_uint16_le(ps + ps_ptr + 4); + if (primary_sensor == 0) { + primary_sensor = current_sensor; + } + if (current_sensor == primary_sensor) { + event->pressure.sensor = 0; + event->pressure.mbar = array_uint16_le(ps + ps_ptr + 6) * 10; // cb->mb + } else { + /* Ignoring the buddy sensor for no as we cannot draw it on the profile. + event->pressure.sensor = 1; + event->pressure.mbar = array_uint16_le(ps + ps_ptr + 6) * 10; // cb->mb + */ + } + // 1 byte PSR + // 1 byte ST + skip = 10; + break; + case 0x0010: + skip = 26; + break; + case 0x0015: // Unknown + skip = 2; + break; + default: + skip = 4; + break; + } + + return skip; +} + +static void parse_dives (int log_version, const unsigned char *buf, unsigned int buf_size) +{ + unsigned int ptr = 0; + unsigned char model; + + struct dive *dive; + struct divecomputer *dc; + struct sample *sample; + + while (ptr < buf_size) { + int i; + bool found_divesite = false; + dive = alloc_dive(); + primary_sensor = 0; + dc = &dive->dc; + + /* Just the main cylinder until we can handle the buddy cylinder porperly */ + for (i = 0; i < 1; i++) + fill_default_cylinder(&dive->cylinder[i]); + + // Model 0=Xen, 1,2=Xeo, 4=Lynx, other=Liquivision + model = *(buf + ptr); + switch (model) { + case 0: + dc->model = "Xen"; + break; + case 1: + case 2: + dc->model = "Xeo"; + break; + case 4: + dc->model = "Lynx"; + break; + default: + dc->model = "Liquivision"; + break; + } + ptr++; + + // Dive location, assemble Location and Place + unsigned int len, place_len; + char *location; + len = array_uint32_le(buf + ptr); + ptr += 4; + place_len = array_uint32_le(buf + ptr + len); + + if (len && place_len) { + location = malloc(len + place_len + 4); + memset(location, 0, len + place_len + 4); + memcpy(location, buf + ptr, len); + memcpy(location + len, ", ", 2); + memcpy(location + len + 2, buf + ptr + len + 4, place_len); + } else if (len) { + location = strndup((char *)buf + ptr, len); + } else if (place_len) { + location = strndup((char *)buf + ptr + len + 4, place_len); + } + + /* Store the location only if we have one */ + if (len || place_len) + found_divesite = true; + + ptr += len + 4 + place_len; + + // Dive comment + len = array_uint32_le(buf + ptr); + ptr += 4; + + // Blank notes are better than the default text + if (len && strncmp((char *)buf + ptr, "Comment ...", 11)) { + dive->notes = strndup((char *)buf + ptr, len); + } + ptr += len; + + dive->id = array_uint32_le(buf + ptr); + ptr += 4; + + dive->number = array_uint16_le(buf + ptr) + 1; + ptr += 2; + + dive->duration.seconds = array_uint32_le(buf + ptr); // seconds + ptr += 4; + + dive->maxdepth.mm = array_uint16_le(buf + ptr) * 10; // cm->mm + ptr += 2; + + dive->meandepth.mm = array_uint16_le(buf + ptr) * 10; // cm->mm + ptr += 2; + + dive->when = array_uint32_le(buf + ptr); + ptr += 4; + + // now that we have the dive time we can store the divesite + // (we need the dive time to create deterministic uuids) + if (found_divesite) { + dive->dive_site_uuid = find_or_create_dive_site_with_name(location, dive->when); + free(location); + } + //unsigned int end_time = array_uint32_le(buf + ptr); + ptr += 4; + + //unsigned int sit = array_uint32_le(buf + ptr); + ptr += 4; + //if (sit == 0xffffffff) { + //} + + dive->surface_pressure.mbar = array_uint16_le(buf + ptr); // ??? + ptr += 2; + + //unsigned int rep_dive = array_uint16_le(buf + ptr); + ptr += 2; + + dive->mintemp.mkelvin = C_to_mkelvin((float)array_uint16_le(buf + ptr)/10);// C->mK + ptr += 2; + + dive->maxtemp.mkelvin = C_to_mkelvin((float)array_uint16_le(buf + ptr)/10);// C->mK + ptr += 2; + + dive->salinity = *(buf + ptr); // ??? + ptr += 1; + + unsigned int sample_count = array_uint32_le(buf + ptr); + ptr += 4; + + // Sample interval + unsigned char sample_interval; + sample_interval = 1; + + unsigned char intervals[6] = {1,2,5,10,30,60}; + if (*(buf + ptr) < 6) + sample_interval = intervals[*(buf + ptr)]; + ptr += 1; + + float start_cns = 0; + unsigned char dive_mode = 0, algorithm = 0; + if (array_uint32_le(buf + ptr) != sample_count) { + // Xeo, with CNS and OTU + start_cns = *(float *) (buf + ptr); + ptr += 4; + dive->cns = *(float *) (buf + ptr); // end cns + ptr += 4; + dive->otu = *(float *) (buf + ptr); + ptr += 4; + dive_mode = *(buf + ptr++); // 0=Deco, 1=Gauge, 2=None + algorithm = *(buf + ptr++); // 0=ZH-L16C+GF + sample_count = array_uint32_le(buf + ptr); + } + // we aren't using the start_cns, dive_mode, and algorithm, yet + (void)start_cns; + (void)dive_mode; + (void)algorithm; + + ptr += 4; + + // Parse dive samples + const unsigned char *ds = buf + ptr; + const unsigned char *ts = buf + ptr + sample_count * 2 + 4; + const unsigned char *ps = buf + ptr + sample_count * 4 + 4; + unsigned int ps_count = array_uint32_le(ps); + ps += 4; + + // Bump ptr + ptr += sample_count * 4 + 4; + + // Handle events + unsigned int ps_ptr; + ps_ptr = 0; + + unsigned int event_code, d = 0, e; + struct lv_event event; + + // Loop through events + for (e = 0; e < ps_count; e++) { + // Get event + event_code = array_uint16_le(ps + ps_ptr); + ps_ptr += 2; + + if (log_version == 3) { + ps_ptr += handle_event_ver3(event_code, ps, ps_ptr, &event); + if (event_code != 0xf) + continue; // ignore all by pressure sensor event + } else { // version 2 + ps_ptr += handle_event_ver2(event_code, ps, ps_ptr, &event); + continue; // ignore all events + } + int sample_time, last_time; + int depth_mm, last_depth, temp_mk, last_temp; + + while (true) { + sample = prepare_sample(dc); + + // Get sample times + sample_time = d * sample_interval; + depth_mm = array_uint16_le(ds + d * 2) * 10; // cm->mm + temp_mk = C_to_mkelvin((float)array_uint16_le(ts + d * 2) / 10); // dC->mK + last_time = (d ? (d - 1) * sample_interval : 0); + + if (d == sample_count) { + // We still have events to record + sample->time.seconds = event.time; + sample->depth.mm = array_uint16_le(ds + (d - 1) * 2) * 10; // cm->mm + sample->temperature.mkelvin = C_to_mkelvin((float) array_uint16_le(ts + (d - 1) * 2) / 10); // dC->mK + sample->sensor = event.pressure.sensor; + sample->cylinderpressure.mbar = event.pressure.mbar; + finish_sample(dc); + + break; + } else if (event.time > sample_time) { + // Record sample and loop + sample->time.seconds = sample_time; + sample->depth.mm = depth_mm; + sample->temperature.mkelvin = temp_mk; + finish_sample(dc); + d++; + + continue; + } else if (event.time == sample_time) { + sample->time.seconds = sample_time; + sample->depth.mm = depth_mm; + sample->temperature.mkelvin = temp_mk; + sample->sensor = event.pressure.sensor; + sample->cylinderpressure.mbar = event.pressure.mbar; + finish_sample(dc); + + break; + } else { // Event is prior to sample + sample->time.seconds = event.time; + sample->sensor = event.pressure.sensor; + sample->cylinderpressure.mbar = event.pressure.mbar; + if (last_time == sample_time) { + sample->depth.mm = depth_mm; + sample->temperature.mkelvin = temp_mk; + } else { + // Extrapolate + last_depth = array_uint16_le(ds + (d - 1) * 2) * 10; // cm->mm + last_temp = C_to_mkelvin((float) array_uint16_le(ts + (d - 1) * 2) / 10); // dC->mK + sample->depth.mm = last_depth + (depth_mm - last_depth) + * (event.time - last_time) / sample_interval; + sample->temperature.mkelvin = last_temp + (temp_mk - last_temp) + * (event.time - last_time) / sample_interval; + } + finish_sample(dc); + + break; + } + } // while (true); + } // for each event sample + + // record trailing depth samples + for ( ;d < sample_count; d++) { + sample = prepare_sample(dc); + sample->time.seconds = d * sample_interval; + + sample->depth.mm = array_uint16_le(ds + d * 2) * 10; // cm->mm + sample->temperature.mkelvin = + C_to_mkelvin((float)array_uint16_le(ts + d * 2) / 10); + finish_sample(dc); + } + + if (log_version == 3 && model == 4) { + // Advance to begin of next dive + switch (array_uint16_le(ps + ps_ptr)) { + case 0x0000: + ps_ptr += 5; + break; + case 0x0100: + ps_ptr += 7; + break; + case 0x0200: + ps_ptr += 9; + break; + case 0x0300: + ps_ptr += 11; + break; + case 0x0b0b: + ps_ptr += 27; + break; + } + + while (*(ps + ps_ptr) != 0x04) + ps_ptr++; + } + + // End dive + dive->downloaded = true; + record_dive(dive); + mark_divelist_changed(true); + + // Advance ptr for next dive + ptr += ps_ptr + 4; + } // while + + //DEBUG save_dives("/tmp/test.xml"); +} + +int try_to_open_liquivision(const char *filename, struct memblock *mem) +{ + const unsigned char *buf = mem->buffer; + unsigned int buf_size = mem->size; + unsigned int ptr; + int log_version; + + // Get name length + unsigned int len = array_uint32_le(buf); + // Ignore length field and the name + ptr = 4 + len; + + unsigned int dive_count = array_uint32_le(buf + ptr); + if (dive_count == 0xffffffff) { + // File version 3.0 + log_version = 3; + ptr += 6; + dive_count = array_uint32_le(buf + ptr); + } else { + log_version = 2; + } + ptr += 4; + + parse_dives(log_version, buf + ptr, buf_size - ptr); + + return 1; +} diff --git a/subsurface-core/load-git.c b/subsurface-core/load-git.c new file mode 100644 index 000000000..39dab4367 --- /dev/null +++ b/subsurface-core/load-git.c @@ -0,0 +1,1638 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gettext.h" + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "git-access.h" +#include "qthelperfromc.h" + +const char *saved_git_id = NULL; + +struct picture_entry_list { + void *data; + int len; + const char *hash; + struct picture_entry_list *next; +}; +struct picture_entry_list *pel = NULL; + +struct keyword_action { + const char *keyword; + void (*fn)(char *, struct membuffer *, void *); +}; +#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0])) + +extern degrees_t parse_degrees(char *buf, char **end); +git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry); + +static void save_picture_from_git(struct picture *picture) +{ + struct picture_entry_list *pic_entry = pel; + + while (pic_entry) { + if (same_string(pic_entry->hash, picture->hash)) { + savePictureLocal(picture, pic_entry->data, pic_entry->len); + return; + } + pic_entry = pic_entry->next; + } + fprintf(stderr, "didn't find picture entry for %s\n", picture->filename); +} + +static char *get_utf8(struct membuffer *b) +{ + int len = b->len; + char *res; + + if (!len) + return NULL; + res = malloc(len+1); + if (res) { + memcpy(res, b->buffer, len); + res[len] = 0; + } + return res; +} + +static temperature_t get_temperature(const char *line) +{ + temperature_t t; + t.mkelvin = C_to_mkelvin(ascii_strtod(line, NULL)); + return t; +} + +static depth_t get_depth(const char *line) +{ + depth_t d; + d.mm = rint(1000*ascii_strtod(line, NULL)); + return d; +} + +static volume_t get_volume(const char *line) +{ + volume_t v; + v.mliter = rint(1000*ascii_strtod(line, NULL)); + return v; +} + +static weight_t get_weight(const char *line) +{ + weight_t w; + w.grams = rint(1000*ascii_strtod(line, NULL)); + return w; +} + +static pressure_t get_pressure(const char *line) +{ + pressure_t p; + p.mbar = rint(1000*ascii_strtod(line, NULL)); + return p; +} + +static int get_salinity(const char *line) +{ + return rint(10*ascii_strtod(line, NULL)); +} + +static fraction_t get_fraction(const char *line) +{ + fraction_t f; + f.permille = rint(10*ascii_strtod(line, NULL)); + return f; +} + +static void update_date(timestamp_t *when, const char *line) +{ + unsigned yyyy, mm, dd; + struct tm tm; + + if (sscanf(line, "%04u-%02u-%02u", &yyyy, &mm, &dd) != 3) + return; + utc_mkdate(*when, &tm); + tm.tm_year = yyyy - 1900; + tm.tm_mon = mm - 1; + tm.tm_mday = dd; + *when = utc_mktime(&tm); +} + +static void update_time(timestamp_t *when, const char *line) +{ + unsigned h, m, s = 0; + struct tm tm; + + if (sscanf(line, "%02u:%02u:%02u", &h, &m, &s) < 2) + return; + utc_mkdate(*when, &tm); + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = s; + *when = utc_mktime(&tm); +} + +static duration_t get_duration(const char *line) +{ + int m = 0, s = 0; + duration_t d; + sscanf(line, "%d:%d", &m, &s); + d.seconds = m*60+s; + return d; +} + +static enum dive_comp_type get_dctype(const char *line) +{ + for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) { + if (strcmp(line, divemode_text[i]) == 0) + return i; + } + return 0; +} + +static int get_index(const char *line) +{ return atoi(line); } + +static int get_hex(const char *line) +{ return strtoul(line, NULL, 16); } + +/* this is in qthelper.cpp, so including the .h file is a pain */ +extern const char *printGPSCoords(int lat, int lon); + +static void parse_dive_gps(char *line, struct membuffer *str, void *_dive) +{ + uint32_t uuid; + degrees_t latitude = parse_degrees(line, &line); + degrees_t longitude = parse_degrees(line, &line); + struct dive *dive = _dive; + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + uuid = get_dive_site_uuid_by_gps(latitude, longitude, NULL); + if (!uuid) + uuid = create_dive_site_with_gps("", latitude, longitude, dive->when); + dive->dive_site_uuid = uuid; + } else { + if (dive_site_has_gps_location(ds) && + (ds->latitude.udeg != latitude.udeg || ds->longitude.udeg != longitude.udeg)) { + const char *coords = printGPSCoords(latitude.udeg, longitude.udeg); + // we have a dive site that already has GPS coordinates + ds->notes = add_to_string(ds->notes, translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords); + free((void *)coords); + } + ds->latitude = latitude; + ds->longitude = longitude; + } + +} + +static void parse_dive_location(char *line, struct membuffer *str, void *_dive) +{ + uint32_t uuid; + char *name = get_utf8(str); + struct dive *dive = _dive; + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + uuid = get_dive_site_uuid_by_name(name, NULL); + if (!uuid) + uuid = create_dive_site(name, dive->when); + dive->dive_site_uuid = uuid; + } else { + // we already had a dive site linked to the dive + if (same_string(ds->name, "")) { + ds->name = strdup(name); + } else { + // and that dive site had a name. that's weird - if our name is different, add it to the notes + if (!same_string(ds->name, name)) + ds->notes = add_to_string(ds->notes, translate("gettextFromC", "additional name for site: %s\n"), name); + } + } + free(name); +} + +static void parse_dive_divemaster(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->divemaster = get_utf8(str); } + +static void parse_dive_buddy(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->buddy = get_utf8(str); } + +static void parse_dive_suit(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->suit = get_utf8(str); } + +static void parse_dive_notes(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->notes = get_utf8(str); } + +static void parse_dive_divesiteid(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->dive_site_uuid = get_hex(line); } + +/* + * We can have multiple tags in the membuffer. They are separated by + * NUL bytes. + */ +static void parse_dive_tags(char *line, struct membuffer *str, void *_dive) +{ + struct dive *dive = _dive; + const char *tag; + int len = str->len; + + if (!len) + return; + + /* Make sure there is a NUL at the end too */ + tag = mb_cstring(str); + for (;;) { + int taglen = strlen(tag); + if (taglen) + taglist_add_tag(&dive->tag_list, tag); + len -= taglen; + if (!len) + return; + tag += taglen+1; + len--; + } +} + +static void parse_dive_airtemp(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->airtemp = get_temperature(line); } + +static void parse_dive_watertemp(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->watertemp = get_temperature(line); } + +static void parse_dive_duration(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->duration = get_duration(line); } + +static void parse_dive_rating(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->rating = get_index(line); } + +static void parse_dive_visibility(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->visibility = get_index(line); } + +static void parse_dive_notrip(char *line, struct membuffer *str, void *_dive) +{ struct dive *dive = _dive; dive->tripflag = NO_TRIP; } + +static void parse_site_description(char *line, struct membuffer *str, void *_ds) +{ struct dive_site *ds = _ds; ds->description = strdup(mb_cstring(str)); } + +static void parse_site_name(char *line, struct membuffer *str, void *_ds) +{ struct dive_site *ds = _ds; ds->name = strdup(mb_cstring(str)); } + +static void parse_site_notes(char *line, struct membuffer *str, void *_ds) +{ struct dive_site *ds = _ds; ds->notes = strdup(mb_cstring(str)); } + +extern degrees_t parse_degrees(char *buf, char **end); +static void parse_site_gps(char *line, struct membuffer *str, void *_ds) +{ + struct dive_site *ds = _ds; + + ds->latitude = parse_degrees(line, &line); + ds->longitude = parse_degrees(line, &line); +} + +static void parse_site_geo(char *line, struct membuffer *str, void *_ds) +{ + struct dive_site *ds = _ds; + if (ds->taxonomy.category == NULL) + ds->taxonomy.category = alloc_taxonomy(); + int nr = ds->taxonomy.nr; + if (nr < TC_NR_CATEGORIES) { + struct taxonomy *t = &ds->taxonomy.category[nr]; + t->value = strdup(mb_cstring(str)); + sscanf(line, "cat %d origin %d \"", &t->category, (int *)&t->origin); + ds->taxonomy.nr++; + } +} + +/* Parse key=val parts of samples and cylinders etc */ +static char *parse_keyvalue_entry(void (*fn)(void *, const char *, const char *), void *fndata, char *line) +{ + char *key = line, *val, c; + + while ((c = *line) != 0) { + if (isspace(c) || c == '=') + break; + line++; + } + + if (c == '=') + *line++ = 0; + val = line; + + while ((c = *line) != 0) { + if (isspace(c)) + break; + line++; + } + if (c) + *line++ = 0; + + fn(fndata, key, val); + return line; +} + +static int cylinder_index, weightsystem_index; + +static void parse_cylinder_keyvalue(void *_cylinder, const char *key, const char *value) +{ + cylinder_t *cylinder = _cylinder; + if (!strcmp(key, "vol")) { + cylinder->type.size = get_volume(value); + return; + } + if (!strcmp(key, "workpressure")) { + cylinder->type.workingpressure = get_pressure(value); + return; + } + /* This is handled by the "get_utf8()" */ + if (!strcmp(key, "description")) + return; + if (!strcmp(key, "o2")) { + cylinder->gasmix.o2 = get_fraction(value); + return; + } + if (!strcmp(key, "he")) { + cylinder->gasmix.he = get_fraction(value); + return; + } + if (!strcmp(key, "start")) { + cylinder->start = get_pressure(value); + return; + } + if (!strcmp(key, "end")) { + cylinder->end = get_pressure(value); + return; + } + if (!strcmp(key, "use")) { + cylinder->cylinder_use = cylinderuse_from_text(value); + return; + } + report_error("Unknown cylinder key/value pair (%s/%s)", key, value); +} + +static void parse_dive_cylinder(char *line, struct membuffer *str, void *_dive) +{ + struct dive *dive = _dive; + cylinder_t *cylinder = dive->cylinder + cylinder_index; + + cylinder_index++; + cylinder->type.description = get_utf8(str); + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_cylinder_keyvalue, cylinder, line); + } +} + +static void parse_weightsystem_keyvalue(void *_ws, const char *key, const char *value) +{ + weightsystem_t *ws = _ws; + if (!strcmp(key, "weight")) { + ws->weight = get_weight(value); + return; + } + /* This is handled by the "get_utf8()" */ + if (!strcmp(key, "description")) + return; + report_error("Unknown weightsystem key/value pair (%s/%s)", key, value); +} + +static void parse_dive_weightsystem(char *line, struct membuffer *str, void *_dive) +{ + struct dive *dive = _dive; + weightsystem_t *ws = dive->weightsystem + weightsystem_index; + + weightsystem_index++; + ws->description = get_utf8(str); + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_weightsystem_keyvalue, ws, line); + } +} + +static int match_action(char *line, struct membuffer *str, void *data, + struct keyword_action *action, unsigned nr_action) +{ + char *p = line, c; + unsigned low, high; + + while ((c = *p) >= 'a' && c <= 'z') // skip over 1st word + p++; // Extract the second word from the line: + if (p == line) + return -1; + switch (c) { + case 0: // if 2nd word is C-terminated + break; + case ' ': // =end of 2nd word? + *p++ = 0; // then C-terminate that word + break; + default: + return -1; + } + + /* Standard binary search in a table */ + low = 0; + high = nr_action; + while (low < high) { + unsigned mid = (low+high)/2; + struct keyword_action *a = action + mid; + int cmp = strcmp(line, a->keyword); + if (!cmp) { // attribute found: + a->fn(p, str, data); // Execute appropriate function, + return 0; // .. passing 2n word from above + } // (p) as a function argument. + if (cmp < 0) + high = mid; + else + low = mid+1; + } +report_error("Unmatched action '%s'", line); + return -1; +} + +/* FIXME! We should do the array thing here too. */ +static void parse_sample_keyvalue(void *_sample, const char *key, const char *value) +{ + struct sample *sample = _sample; + + if (!strcmp(key, "sensor")) { + sample->sensor = atoi(value); + return; + } + if (!strcmp(key, "ndl")) { + sample->ndl = get_duration(value); + return; + } + if (!strcmp(key, "tts")) { + sample->tts = get_duration(value); + return; + } + if (!strcmp(key, "in_deco")) { + sample->in_deco = atoi(value); + return; + } + if (!strcmp(key, "stoptime")) { + sample->stoptime = get_duration(value); + return; + } + if (!strcmp(key, "stopdepth")) { + sample->stopdepth = get_depth(value); + return; + } + if (!strcmp(key, "cns")) { + sample->cns = atoi(value); + return; + } + + if (!strcmp(key, "rbt")) { + sample->rbt = get_duration(value); + return; + } + + if (!strcmp(key, "po2")) { + pressure_t p = get_pressure(value); + sample->setpoint.mbar = p.mbar; + return; + } + if (!strcmp(key, "sensor1")) { + pressure_t p = get_pressure(value); + sample->o2sensor[0].mbar = p.mbar; + return; + } + if (!strcmp(key, "sensor2")) { + pressure_t p = get_pressure(value); + sample->o2sensor[1].mbar = p.mbar; + return; + } + if (!strcmp(key, "sensor3")) { + pressure_t p = get_pressure(value); + sample->o2sensor[2].mbar = p.mbar; + return; + } + if (!strcmp(key, "o2pressure")) { + pressure_t p = get_pressure(value); + sample->o2cylinderpressure.mbar = p.mbar; + return; + } + if (!strcmp(key, "heartbeat")) { + sample->heartbeat = atoi(value); + return; + } + if (!strcmp(key, "bearing")) { + sample->bearing.degrees = atoi(value); + return; + } + + report_error("Unexpected sample key/value pair (%s/%s)", key, value); +} + +static char *parse_sample_unit(struct sample *sample, double val, char *unit) +{ + char *end = unit, c; + + /* Skip over the unit */ + while ((c = *end) != 0) { + if (isspace(c)) { + *end++ = 0; + break; + } + end++; + } + + /* The units are "°C", "m" or "bar", so let's just look at the first character */ + switch (*unit) { + case 'm': + sample->depth.mm = rint(1000*val); + break; + case 'b': + sample->cylinderpressure.mbar = rint(1000*val); + break; + default: + sample->temperature.mkelvin = C_to_mkelvin(val); + break; + } + + return end; +} + +/* + * By default the sample data does not change unless the + * save-file gives an explicit new value. So we copy the + * data from the previous sample if one exists, and then + * the parsing will update it as necessary. + * + * There are a few exceptions, like the sample pressure: + * missing sample pressure doesn't mean "same as last + * time", but "interpolate". We clear those ones + * explicitly. + */ +static struct sample *new_sample(struct divecomputer *dc) +{ + struct sample *sample = prepare_sample(dc); + if (sample != dc->sample) { + memcpy(sample, sample-1, sizeof(struct sample)); + sample->cylinderpressure.mbar = 0; + } + return sample; +} + +static void sample_parser(char *line, struct divecomputer *dc) +{ + int m, s = 0; + struct sample *sample = new_sample(dc); + + m = strtol(line, &line, 10); + if (*line == ':') + s = strtol(line+1, &line, 10); + sample->time.seconds = m*60+s; + + for (;;) { + char c; + + while (isspace(c = *line)) + line++; + if (!c) + break; + /* Less common sample entries have a name */ + if (c >= 'a' && c <= 'z') { + line = parse_keyvalue_entry(parse_sample_keyvalue, sample, line); + } else { + const char *end; + double val = ascii_strtod(line, &end); + if (end == line) { + report_error("Odd sample data: %s", line); + break; + } + line = (char *)end; + line = parse_sample_unit(sample, val, line); + } + } + finish_sample(dc); +} + +static void parse_dc_airtemp(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->airtemp = get_temperature(line); } + +static void parse_dc_date(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; update_date(&dc->when, line); } + +static void parse_dc_deviceid(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->deviceid = get_hex(line); } + +static void parse_dc_diveid(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->diveid = get_hex(line); } + +static void parse_dc_duration(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->duration = get_duration(line); } + +static void parse_dc_dctype(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->divemode = get_dctype(line); } + +static void parse_dc_maxdepth(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->maxdepth = get_depth(line); } + +static void parse_dc_meandepth(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->meandepth = get_depth(line); } + +static void parse_dc_model(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->model = get_utf8(str); } + +static void parse_dc_numberofoxygensensors(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->no_o2sensors = get_index(line); } + +static void parse_dc_surfacepressure(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->surface_pressure = get_pressure(line); } + +static void parse_dc_salinity(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->salinity = get_salinity(line); } + +static void parse_dc_surfacetime(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->surfacetime = get_duration(line); } + +static void parse_dc_time(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; update_time(&dc->when, line); } + +static void parse_dc_watertemp(char *line, struct membuffer *str, void *_dc) +{ struct divecomputer *dc = _dc; dc->watertemp = get_temperature(line); } + +static void parse_event_keyvalue(void *_event, const char *key, const char *value) +{ + struct event *event = _event; + int val = atoi(value); + + if (!strcmp(key, "type")) { + event->type = val; + } else if (!strcmp(key, "flags")) { + event->flags = val; + } else if (!strcmp(key, "value")) { + event->value = val; + } else if (!strcmp(key, "name")) { + /* We get the name from the string handling */ + } else if (!strcmp(key, "cylinder")) { + /* NOTE! We add one here as a marker that "yes, we got a cylinder index" */ + event->gas.index = 1+get_index(value); + } else if (!strcmp(key, "o2")) { + event->gas.mix.o2 = get_fraction(value); + } else if (!strcmp(key, "he")) { + event->gas.mix.he = get_fraction(value); + } else + report_error("Unexpected event key/value pair (%s/%s)", key, value); +} + +/* keyvalue "key" "value" + * so we have two strings (possibly empty) in the membuffer, separated by a '\0' */ +static void parse_dc_keyvalue(char *line, struct membuffer *str, void *_dc) +{ + const char *key, *value; + struct divecomputer *dc = _dc; + + // Let's make sure we have two strings... + int string_counter = 0; + while(*line) { + if (*line == '"') + string_counter++; + line++; + } + if (string_counter != 2) + return; + + // stupidly the second string in the membuffer isn't NUL terminated; + // asking for a cstring fixes that; interestingly enough, given that there are two + // strings in the mb, the next command at the same time assigns a pointer to the + // first string to 'key' and NUL terminates the second string (which then goes to 'value') + key = mb_cstring(str); + value = key + strlen(key) + 1; + add_extra_data(dc, key, value); +} + +static void parse_dc_event(char *line, struct membuffer *str, void *_dc) +{ + int m, s = 0; + const char *name; + struct divecomputer *dc = _dc; + struct event event = { 0 }, *ev; + + m = strtol(line, &line, 10); + if (*line == ':') + s = strtol(line+1, &line, 10); + event.time.seconds = m*60+s; + + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_event_keyvalue, &event, line); + } + + name = ""; + if (str->len) + name = mb_cstring(str); + ev = add_event(dc, event.time.seconds, event.type, event.flags, event.value, name); + if (ev && event_is_gaschange(ev)) { + /* + * We subtract one here because "0" is "no index", + * and the parsing will add one for actual cylinder + * index data (see parse_event_keyvalue) + */ + ev->gas.index = event.gas.index-1; + if (event.gas.mix.o2.permille || event.gas.mix.he.permille) + ev->gas.mix = event.gas.mix; + } +} + +static void parse_trip_date(char *line, struct membuffer *str, void *_trip) +{ dive_trip_t *trip = _trip; update_date(&trip->when, line); } + +static void parse_trip_time(char *line, struct membuffer *str, void *_trip) +{ dive_trip_t *trip = _trip; update_time(&trip->when, line); } + +static void parse_trip_location(char *line, struct membuffer *str, void *_trip) +{ dive_trip_t *trip = _trip; trip->location = get_utf8(str); } + +static void parse_trip_notes(char *line, struct membuffer *str, void *_trip) +{ dive_trip_t *trip = _trip; trip->notes = get_utf8(str); } + +static void parse_settings_autogroup(char *line, struct membuffer *str, void *_unused) +{ set_autogroup(1); } + +static void parse_settings_units(char *line, struct membuffer *str, void *unused) +{ + if (line) + set_informational_units(line); +} + +static void parse_settings_userid(char *line, struct membuffer *str, void *_unused) +{ + if (line) { + set_save_userid_local(true); + set_userid(line); + } +} + +/* + * Our versioning is a joke right now, but this is more of an example of what we + * *can* do some day. And if we do change the version, this warning will show if + * you read with a version of subsurface that doesn't know about it. + * We MUST keep this in sync with the XML version (so we can report a consistent + * minimum datafile version) + */ +static void parse_settings_version(char *line, struct membuffer *str, void *_unused) +{ + int version = atoi(line); + report_datafile_version(version); + if (version > DATAFORMAT_VERSION) + report_error("Git save file version %d is newer than version %d I know about", version, DATAFORMAT_VERSION); +} + +/* The string in the membuffer is the version string of subsurface that saved things, just FYI */ +static void parse_settings_subsurface(char *line, struct membuffer *str, void *_unused) +{ } + +struct divecomputerid { + const char *model; + const char *nickname; + const char *firmware; + const char *serial; + const char *cstr; + unsigned int deviceid; +}; + +static void parse_divecomputerid_keyvalue(void *_cid, const char *key, const char *value) +{ + struct divecomputerid *cid = _cid; + + if (*value == '"') { + value = cid->cstr; + cid->cstr += strlen(cid->cstr)+1; + } + if (!strcmp(key, "deviceid")) { + cid->deviceid = get_hex(value); + return; + } + if (!strcmp(key, "serial")) { + cid->serial = value; + return; + } + if (!strcmp(key, "firmware")) { + cid->firmware = value; + return; + } + if (!strcmp(key, "nickname")) { + cid->nickname = value; + return; + } + report_error("Unknow divecomputerid key/value pair (%s/%s)", key, value); +} + +/* + * The 'divecomputerid' is a bit harder to parse than some other things, because + * it can have multiple strings (but see the tag parsing for another example of + * that) in addition to the non-string entries. + * + * We keep the "next" string in "id.cstr" and update it as we use it. + */ +static void parse_settings_divecomputerid(char *line, struct membuffer *str, void *_unused) +{ + struct divecomputerid id = { mb_cstring(str) }; + + id.cstr = id.model + strlen(id.model) + 1; + + /* Skip the '"' that stood for the model string */ + line++; + + for (;;) { + char c; + while (isspace(c = *line)) + line++; + if (!c) + break; + line = parse_keyvalue_entry(parse_divecomputerid_keyvalue, &id, line); + } + create_device_node(id.model, id.deviceid, id.serial, id.firmware, id.nickname); +} + +static void parse_picture_filename(char *line, struct membuffer *str, void *_pic) +{ + struct picture *pic = _pic; + pic->filename = get_utf8(str); +} + +static void parse_picture_gps(char *line, struct membuffer *str, void *_pic) +{ + struct picture *pic = _pic; + + pic->latitude = parse_degrees(line, &line); + pic->longitude = parse_degrees(line, &line); +} + +static void parse_picture_hash(char *line, struct membuffer *str, void *_pic) +{ + struct picture *pic = _pic; + pic->hash = get_utf8(str); +} + +/* These need to be sorted! */ +struct keyword_action dc_action[] = { +#undef D +#define D(x) { #x, parse_dc_ ## x } + D(airtemp), D(date), D(dctype), D(deviceid), D(diveid), D(duration), + D(event), D(keyvalue), D(maxdepth), D(meandepth), D(model), D(numberofoxygensensors), + D(salinity), D(surfacepressure), D(surfacetime), D(time), D(watertemp) +}; + +/* Sample lines start with a space or a number */ +static void divecomputer_parser(char *line, struct membuffer *str, void *_dc) +{ + char c = *line; + if (c < 'a' || c > 'z') + sample_parser(line, _dc); + match_action(line, str, _dc, dc_action, ARRAY_SIZE(dc_action)); +} + +/* These need to be sorted! */ +struct keyword_action dive_action[] = { +#undef D +#define D(x) { #x, parse_dive_ ## x } + D(airtemp), D(buddy), D(cylinder), D(divemaster), D(divesiteid), D(duration), + D(gps), D(location), D(notes), D(notrip), D(rating), D(suit), + D(tags), D(visibility), D(watertemp), D(weightsystem) +}; + +static void dive_parser(char *line, struct membuffer *str, void *_dive) +{ + match_action(line, str, _dive, dive_action, ARRAY_SIZE(dive_action)); +} + +/* These need to be sorted! */ +struct keyword_action site_action[] = { +#undef D +#define D(x) { #x, parse_site_ ## x } + D(description), D(geo), D(gps), D(name), D(notes) +}; + +static void site_parser(char *line, struct membuffer *str, void *_ds) +{ + match_action(line, str, _ds, site_action, ARRAY_SIZE(site_action)); +} + +/* These need to be sorted! */ +struct keyword_action trip_action[] = { +#undef D +#define D(x) { #x, parse_trip_ ## x } + D(date), D(location), D(notes), D(time), +}; + +static void trip_parser(char *line, struct membuffer *str, void *_trip) +{ + match_action(line, str, _trip, trip_action, ARRAY_SIZE(trip_action)); +} + +/* These need to be sorted! */ +static struct keyword_action settings_action[] = { +#undef D +#define D(x) { #x, parse_settings_ ## x } + D(autogroup), D(divecomputerid), D(subsurface), D(units), D(userid), D(version), +}; + +static void settings_parser(char *line, struct membuffer *str, void *_unused) +{ + match_action(line, str, NULL, settings_action, ARRAY_SIZE(settings_action)); +} + +/* These need to be sorted! */ +static struct keyword_action picture_action[] = { +#undef D +#define D(x) { #x, parse_picture_ ## x } + D(filename), D(gps), D(hash) +}; + +static void picture_parser(char *line, struct membuffer *str, void *_pic) +{ + match_action(line, str, _pic, picture_action, ARRAY_SIZE(picture_action)); +} + +/* + * We have a very simple line-based interface, with the small + * complication that lines can have strings in the middle, and + * a string can be multiple lines. + * + * The UTF-8 string escaping is *very* simple, though: + * + * - a string starts and ends with double quotes (") + * + * - inside the string we escape: + * (a) double quotes with '\"' + * (b) backslash (\) with '\\' + * + * - additionally, for human readability, we escape + * newlines with '\n\t', with the exception that + * consecutive newlines are left unescaped (so an + * empty line doesn't become a line with just a tab + * on it). + * + * Also, while the UTF-8 string can have arbitrarily + * long lines, the non-string parts of the lines are + * never long, so we can use a small temporary buffer + * on stack for that part. + * + * Also, note that if a line has one or more strings + * in it: + * + * - each string will be represented as a single '"' + * character in the output. + * + * - all string will exist in the same 'membuffer', + * separated by NUL characters (that cannot exist + * in a string, not even quoted). + */ +static const char *parse_one_string(const char *buf, const char *end, struct membuffer *b) +{ + const char *p = buf; + + /* + * We turn multiple strings one one line (think dive tags) into one + * membuffer that has NUL characters in between strings. + */ + if (b->len) + put_bytes(b, "", 1); + + while (p < end) { + char replace; + + switch (*p++) { + default: + continue; + case '\n': + if (p < end && *p == '\t') { + replace = '\n'; + break; + } + continue; + case '\\': + if (p < end) { + replace = *p; + break; + } + continue; + case '"': + replace = 0; + break; + } + put_bytes(b, buf, p - buf - 1); + if (!replace) + break; + put_bytes(b, &replace, 1); + buf = ++p; + } + return p; +} + +typedef void (line_fn_t)(char *, struct membuffer *, void *); +#define MAXLINE 500 +static unsigned parse_one_line(const char *buf, unsigned size, line_fn_t *fn, void *fndata, struct membuffer *b) +{ + const char *end = buf + size; + const char *p = buf; + char line[MAXLINE+1]; + int off = 0; + + while (p < end) { + char c = *p++; + if (c == '\n') + break; + line[off] = c; + off++; + if (off > MAXLINE) + off = MAXLINE; + if (c == '"') + p = parse_one_string(p, end, b); + } + line[off] = 0; + fn(line, b, fndata); + return p - buf; +} + +/* + * We keep on re-using the membuffer that we use for + * strings, but the callback function can "steal" it by + * saving its value and just clear the original. + */ +static void for_each_line(git_blob *blob, line_fn_t *fn, void *fndata) +{ + const char *content = git_blob_rawcontent(blob); + unsigned int size = git_blob_rawsize(blob); + struct membuffer str = { 0 }; + + while (size) { + unsigned int n = parse_one_line(content, size, fn, fndata, &str); + content += n; + size -= n; + + /* Re-use the allocation, but forget the data */ + str.len = 0; + } + free_buffer(&str); +} + +#define GIT_WALK_OK 0 +#define GIT_WALK_SKIP 1 + +static struct divecomputer *active_dc; +static struct dive *active_dive; +static dive_trip_t *active_trip; + +static void finish_active_trip(void) +{ + dive_trip_t *trip = active_trip; + + if (trip) { + active_trip = NULL; + insert_trip(&trip); + } +} + +static void finish_active_dive(void) +{ + struct dive *dive = active_dive; + + if (dive) { + /* check if we need to save pictures */ + FOR_EACH_PICTURE(dive) { + if (!picture_exists(picture)) + save_picture_from_git(picture); + } + /* free any memory we allocated to track pictures */ + while (pel) { + free(pel->data); + void *lastone = pel; + pel = pel->next; + free(lastone); + } + active_dive = NULL; + record_dive(dive); + } +} + +static struct dive *create_new_dive(timestamp_t when) +{ + struct dive *dive = alloc_dive(); + + /* We'll fill in more data from the dive file */ + dive->when = when; + + if (active_trip) + add_dive_to_trip(dive, active_trip); + return dive; +} + +static dive_trip_t *create_new_trip(int yyyy, int mm, int dd) +{ + dive_trip_t *trip = calloc(1, sizeof(dive_trip_t)); + struct tm tm = { 0 }; + + /* We'll fill in the real data from the trip descriptor file */ + tm.tm_year = yyyy; + tm.tm_mon = mm-1; + tm.tm_mday = dd; + trip->when = utc_mktime(&tm); + + return trip; +} + +static bool validate_date(int yyyy, int mm, int dd) +{ + return yyyy > 1970 && yyyy < 3000 && + mm > 0 && mm < 13 && + dd > 0 && dd < 32; +} + +static bool validate_time(int h, int m, int s) +{ + return h >= 0 && h < 24 && + m >= 0 && m < 60 && + s >=0 && s <= 60; +} + +/* + * Dive trip directory, name is 'nn-alphabetic[~hex]' + */ +static int dive_trip_directory(const char *root, const char *name) +{ + int yyyy = -1, mm = -1, dd = -1; + + if (sscanf(root, "%d/%d", &yyyy, &mm) != 2) + return GIT_WALK_SKIP; + dd = atoi(name); + if (!validate_date(yyyy, mm, dd)) + return GIT_WALK_SKIP; + finish_active_trip(); + active_trip = create_new_trip(yyyy, mm, dd); + return GIT_WALK_OK; +} + +/* + * Dive directory, name is [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] in older git repositories + * but [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] in newer repos as ':' is an illegal character for Windows files + * and 'timeoff' points to what should be the time part of + * the name (the first digit of the hour). + * + * The root path will be of the form yyyy/mm[/tripdir], + */ +static int dive_directory(const char *root, const char *name, int timeoff) +{ + int yyyy = -1, mm = -1, dd = -1; + int h, m, s; + int mday_off, month_off, year_off; + struct tm tm; + + /* Skip the '-' before the time */ + mday_off = timeoff; + if (!mday_off || name[--mday_off] != '-') + return GIT_WALK_SKIP; + /* Skip the day name */ + while (mday_off > 0 && name[--mday_off] != '-') + /* nothing */; + + mday_off = mday_off - 2; + month_off = mday_off - 3; + year_off = month_off - 5; + if (mday_off < 0) + return GIT_WALK_SKIP; + + /* Get the time of day -- parse both time formats so we can read old repos when not on Windows */ + if (sscanf(name+timeoff, "%d:%d:%d", &h, &m, &s) != 3 && sscanf(name+timeoff, "%d=%d=%d", &h, &m, &s) != 3) + return GIT_WALK_SKIP; + if (!validate_time(h, m, s)) + return GIT_WALK_SKIP; + + /* + * Using the "git_tree_walk()" interface is simple, but + * it kind of sucks as an interface because there is + * no sane way to pass the hierarchy to the callbacks. + * The "payload" is a fixed one-time thing: we'd like + * the "current trip" to be passed down to the dives + * that get parsed under that trip, but we can't. + * + * So "active_trip" is not the trip that is in the hierarchy + * _above_ us, it's just the trip that was _before_ us. But + * if a dive is not in a trip at all, we can't tell. + * + * We could just do a better walker that passes the + * return value around, but we hack around this by + * instead looking at the one hierarchical piece of + * data we have: the pathname to the current entry. + * + * This is pretty hacky. The magic '8' is the length + * of a pathname of the form 'yyyy/mm/'. + */ + if (strlen(root) == 8) + finish_active_trip(); + + /* + * Get the date. The day of the month is in the dive directory + * name, the year and month might be in the path leading up + * to it. + */ + dd = atoi(name + mday_off); + if (year_off < 0) { + if (sscanf(root, "%d/%d", &yyyy, &mm) != 2) + return GIT_WALK_SKIP; + } else + yyyy = atoi(name + year_off); + if (month_off >= 0) + mm = atoi(name + month_off); + + if (!validate_date(yyyy, mm, dd)) + return GIT_WALK_SKIP; + + /* Ok, close enough. We've gotten sufficient information */ + memset(&tm, 0, sizeof(tm)); + tm.tm_hour = h; + tm.tm_min = m; + tm.tm_sec = s; + tm.tm_year = yyyy - 1900; + tm.tm_mon = mm-1; + tm.tm_mday = dd; + + finish_active_dive(); + active_dive = create_new_dive(utc_mktime(&tm)); + return GIT_WALK_OK; +} + +static int picture_directory(const char *root, const char *name) +{ + if (!active_dive) + return GIT_WALK_SKIP; + return GIT_WALK_OK; +} + +/* + * Return the length of the string without the unique part. + */ +static int nonunique_length(const char *str) +{ + int len = 0; + + for (;;) { + char c = *str++; + if (!c || c == '~') + return len; + len++; + } +} + +/* + * When hitting a directory node, we have a couple of cases: + * + * - It's just a date entry - all numeric (either year or month): + * + * [yyyy|mm] + * + * We don't do anything with these, we just traverse into them. + * The numeric data will show up as part of the full path when + * we hit more interesting entries. + * + * - It's a trip directory. The name will be of the form + * + * nn-alphabetic[~hex] + * + * where 'nn' is the day of the month (year and month will be + * encoded in the path leading up to this). + * + * - It's a dive directory. The name will be of the form + * + * [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] + * + * (older versions had this as [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] + * but that faile on Windows) + * + * which describes the date and time of a dive (yyyy and mm + * are optional, and may be encoded in the path leading up to + * the dive). + * + * - It is a per-dive picture directory ("Pictures") + * + * - It's some random non-dive-data directory. + * + * If it doesn't match the above patterns, we'll ignore them + * for dive loading purposes, and not even recurse into them. + */ +static int walk_tree_directory(const char *root, const git_tree_entry *entry) +{ + const char *name = git_tree_entry_name(entry); + int digits = 0, len; + char c; + + if (!strcmp(name, "Pictures")) + return picture_directory(root, name); + + if (!strcmp(name, "01-Divesites")) + return GIT_WALK_OK; + + while (isdigit(c = name[digits])) + digits++; + + /* Doesn't start with two or four digits? Skip */ + if (digits != 4 && digits != 2) + return GIT_WALK_SKIP; + + /* Only digits? Do nothing, but recurse into it */ + if (!c) + return GIT_WALK_OK; + + /* All valid cases need to have a slash following */ + if (c != '-') + return GIT_WALK_SKIP; + + /* Do a quick check for a common dive case */ + len = nonunique_length(name); + + /* + * We know the len is at least 3, because we had at least + * two digits and a dash + */ + if (name[len-3] == ':' || name[len-3] == '=') + return dive_directory(root, name, len-8); + + if (digits != 2) + return GIT_WALK_SKIP; + + return dive_trip_directory(root, name); +} + +git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry) +{ + const git_oid *id = git_tree_entry_id(entry); + git_blob *blob; + + if (git_blob_lookup(&blob, repo, id)) + return NULL; + return blob; +} + +static struct divecomputer *create_new_dc(struct dive *dive) +{ + struct divecomputer *dc = &dive->dc; + + while (dc->next) + dc = dc->next; + /* Did we already fill that in? */ + if (dc->samples || dc->model || dc->when) { + struct divecomputer *newdc = calloc(1, sizeof(*newdc)); + if (!newdc) + return NULL; + dc->next = newdc; + dc = newdc; + } + dc->when = dive->when; + dc->duration = dive->duration; + return dc; +} + +/* + * We should *really* try to delay the dive computer data parsing + * until necessary, in order to reduce load-time. The parsing is + * cheap, but the loading of the git blob into memory can be pretty + * costly. + */ +static int parse_divecomputer_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) +{ + git_blob *blob = git_tree_entry_blob(repo, entry); + + if (!blob) + return report_error("Unable to read divecomputer file"); + + active_dc = create_new_dc(active_dive); + for_each_line(blob, divecomputer_parser, active_dc); + git_blob_free(blob); + active_dc = NULL; + return 0; +} + +static int parse_dive_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) +{ + struct dive *dive = active_dive; + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read dive file"); + if (*suffix) + dive->number = atoi(suffix+1); + cylinder_index = weightsystem_index = 0; + for_each_line(blob, dive_parser, active_dive); + git_blob_free(blob); + return 0; +} + +static int parse_site_entry(git_repository *repo, const git_tree_entry *entry, const char *suffix) +{ + if (*suffix == '\0') + return report_error("Dive site without uuid"); + uint32_t uuid = strtoul(suffix, NULL, 16); + struct dive_site *ds = alloc_or_get_dive_site(uuid); + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read dive site file"); + for_each_line(blob, site_parser, ds); + git_blob_free(blob); + return 0; +} + +static int parse_trip_entry(git_repository *repo, const git_tree_entry *entry) +{ + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read trip file"); + for_each_line(blob, trip_parser, active_trip); + git_blob_free(blob); + return 0; +} + +static int parse_settings_entry(git_repository *repo, const git_tree_entry *entry) +{ + git_blob *blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read settings file"); + set_save_userid_local(false); + for_each_line(blob, settings_parser, NULL); + git_blob_free(blob); + return 0; +} + +static int parse_picture_file(git_repository *repo, const git_tree_entry *entry, const char *name) +{ + /* remember the picture data so we can handle it when all dive data has been loaded + * the name of the git file is PIC- */ + git_blob *blob = git_tree_entry_blob(repo, entry); + const void *rawdata = git_blob_rawcontent(blob); + int len = git_blob_rawsize(blob); + struct picture_entry_list *new_pel = malloc(sizeof(struct picture_entry_list)); + new_pel->next = pel; + pel = new_pel; + pel->data = malloc(len); + memcpy(pel->data, rawdata, len); + pel->len = len; + pel->hash = strdup(name + 4); + git_blob_free(blob); + return 0; +} + +static int parse_picture_entry(git_repository *repo, const git_tree_entry *entry, const char *name) +{ + git_blob *blob; + struct picture *pic; + int hh, mm, ss, offset; + char sign; + + /* + * The format of the picture name files is just the offset within + * the dive in form [[+-]hh=mm=ss (previously [[+-]hh:mm:ss, but + * that didn't work on Windows), possibly followed by a hash to + * make the filename unique (which we can just ignore). + */ + if (sscanf(name, "%c%d:%d:%d", &sign, &hh, &mm, &ss) != 4 && + sscanf(name, "%c%d=%d=%d", &sign, &hh, &mm, &ss) != 4) + return report_error("Unknown file name %s", name); + offset = ss + 60*(mm + 60*hh); + if (sign == '-') + offset = -offset; + + blob = git_tree_entry_blob(repo, entry); + if (!blob) + return report_error("Unable to read picture file"); + + pic = alloc_picture(); + pic->offset.seconds = offset; + + for_each_line(blob, picture_parser, pic); + dive_add_picture(active_dive, pic); + git_blob_free(blob); + return 0; +} + +static int walk_tree_file(const char *root, const git_tree_entry *entry, git_repository *repo) +{ + struct dive *dive = active_dive; + dive_trip_t *trip = active_trip; + const char *name = git_tree_entry_name(entry); + if (verbose > 1) + fprintf(stderr, "git load handling file %s\n", name); + switch (*name) { + /* Picture file? They are saved as time offsets in the dive */ + case '-': case '+': + if (dive) + return parse_picture_entry(repo, entry, name); + break; + case 'D': + if (dive && !strncmp(name, "Divecomputer", 12)) + return parse_divecomputer_entry(repo, entry, name+12); + if (dive && !strncmp(name, "Dive", 4)) + return parse_dive_entry(repo, entry, name+4); + break; + case 'S': + if (!strncmp(name, "Site", 4)) + return parse_site_entry(repo, entry, name + 5); + break; + case '0': + if (trip && !strcmp(name, "00-Trip")) + return parse_trip_entry(repo, entry); + if (!strcmp(name, "00-Subsurface")) + return parse_settings_entry(repo, entry); + break; + case 'P': + if (dive && !strncmp(name, "PIC-", 4)) + return parse_picture_file(repo, entry, name); + break; + } + report_error("Unknown file %s%s (%p %p)", root, name, dive, trip); + return GIT_WALK_SKIP; +} + +static int walk_tree_cb(const char *root, const git_tree_entry *entry, void *payload) +{ + git_repository *repo = payload; + git_filemode_t mode = git_tree_entry_filemode(entry); + + if (mode == GIT_FILEMODE_TREE) + return walk_tree_directory(root, entry); + + walk_tree_file(root, entry, repo); + /* Ignore failed blob loads */ + return GIT_WALK_OK; +} + +static int load_dives_from_tree(git_repository *repo, git_tree *tree) +{ + git_tree_walk(tree, GIT_TREEWALK_PRE, walk_tree_cb, repo); + return 0; +} + +void clear_git_id(void) +{ + saved_git_id = NULL; +} + +void set_git_id(const struct git_oid * id) +{ + static char git_id_buffer[GIT_OID_HEXSZ+1]; + + git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), id); + saved_git_id = git_id_buffer; +} + +static int do_git_load(git_repository *repo, const char *branch) +{ + int ret; + git_object *object; + git_commit *commit; + git_tree *tree; + + if (git_revparse_single(&object, repo, branch)) + return report_error("Unable to look up revision '%s'", branch); + if (git_object_peel((git_object **)&commit, object, GIT_OBJ_COMMIT)) + return report_error("Revision '%s' is not a valid commit", branch); + if (git_commit_tree(&tree, commit)) + return report_error("Could not look up tree of commit in branch '%s'", branch); + ret = load_dives_from_tree(repo, tree); + if (!ret) + set_git_id(git_commit_id(commit)); + git_object_free((git_object *)tree); + return ret; +} + +/* + * Like git_save_dives(), this silently returns a negative + * value if it's not a git repository at all (so that you + * can try to load it some other way. + * + * If it is a git repository, we return zero for success, + * or report an error and return 1 if the load failed. + */ +int git_load_dives(struct git_repository *repo, const char *branch) +{ + int ret; + + if (repo == dummy_git_repository) + return report_error("Unable to open git repository at '%s'", branch); + ret = do_git_load(repo, branch); + git_repository_free(repo); + free((void *)branch); + finish_active_dive(); + finish_active_trip(); + return ret; +} diff --git a/subsurface-core/macos.c b/subsurface-core/macos.c new file mode 100644 index 000000000..aa2be4b3b --- /dev/null +++ b/subsurface-core/macos.c @@ -0,0 +1,206 @@ +/* macos.c */ +/* implements Mac OS X specific functions */ +#include +#include +#include +#include "dive.h" +#include "display.h" +#include +#include +#include +#include +#include +#include +#include + +void subsurface_user_info(struct user_info *info) +{ /* Nothing, let's use libgit2-20 on MacOS */ } + +/* macos defines CFSTR to create a CFString object from a constant, + * but no similar macros if a C string variable is supposed to be + * the argument. We add this here (hardcoding the default allocator + * and MacRoman encoding */ +#define CFSTR_VAR(_var) CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, \ + (_var), kCFStringEncodingMacRoman, \ + kCFAllocatorNull) + +#define SUBSURFACE_PREFERENCES CFSTR("org.hohndel.subsurface") +#define ICON_NAME "Subsurface.icns" +#define UI_FONT "Arial 12" + +const char mac_system_divelist_default_font[] = "Arial"; +const char *system_divelist_default_font = mac_system_divelist_default_font; +double system_divelist_default_font_size = -1.0; + +void subsurface_OS_pref_setup(void) +{ + // nothing +} + +bool subsurface_ignore_font(const char *font) +{ + // there are no old default fonts to ignore + return false; +} + +static const char *system_default_path_append(const char *append) +{ + const char *home = getenv("HOME"); + const char *path = "/Library/Application Support/Subsurface"; + + int len = strlen(home) + strlen(path) + 1; + if (append) + len += strlen(append) + 1; + + char *buffer = (char *)malloc(len); + memset(buffer, 0, len); + strcat(buffer, home); + strcat(buffer, path); + if (append) { + strcat(buffer, "/"); + strcat(buffer, append); + } + + return buffer; +} + +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +const char *system_default_filename(void) +{ + static char *filename = NULL; + if (!filename) { + const char *user = getenv("LOGNAME"); + if (same_string(user, "")) + user = "username"; + filename = calloc(strlen(user) + 5, 1); + strcat(filename, user); + strcat(filename, ".xml"); + } + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + int index = -1, entries = 0; + DIR *dp = NULL; + struct dirent *ep = NULL; + size_t i; + if (dc_type != DC_TYPE_UEMIS) { + const char *dirname = "/dev"; + const char *patterns[] = { + "tty.*", + "usbserial", + NULL + }; + + dp = opendir(dirname); + if (dp == NULL) { + return -1; + } + + while ((ep = readdir(dp)) != NULL) { + for (i = 0; patterns[i] != NULL; ++i) { + if (fnmatch(patterns[i], ep->d_name, 0) == 0) { + char filename[1024]; + int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); + if (n >= sizeof(filename)) { + closedir(dp); + return -1; + } + callback(filename, userdata); + if (is_default_dive_computer_device(filename)) + index = entries; + entries++; + break; + } + } + } + closedir(dp); + } + if (dc_type != DC_TYPE_SERIAL) { + const char *dirname = "/Volumes"; + int num_uemis = 0; + dp = opendir(dirname); + if (dp == NULL) { + return -1; + } + + while ((ep = readdir(dp)) != NULL) { + if (fnmatch("UEMISSDA", ep->d_name, 0) == 0) { + char filename[1024]; + int n = snprintf(filename, sizeof(filename), "%s/%s", dirname, ep->d_name); + if (n >= sizeof(filename)) { + closedir(dp); + return -1; + } + callback(filename, userdata); + if (is_default_dive_computer_device(filename)) + index = entries; + entries++; + num_uemis++; + break; + } + } + closedir(dp); + if (num_uemis == 1 && entries == 1) /* if we find exactly one entry and that's a Uemis, select it */ + index = 0; + } + return index; +} + +/* NOP wrappers to comform with windows.c */ +int subsurface_rename(const char *path, const char *newpath) +{ + return rename(path, newpath); +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + return open(path, oflags, mode); +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + return fopen(path, mode); +} + +void *subsurface_opendir(const char *path) +{ + return (void *)opendir(path); +} + +int subsurface_access(const char *path, int mode) +{ + return access(path, mode); +} + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ + return zip_open(path, flags, errorp); +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +void subsurface_console_init(bool dedicated) +{ + /* NOP */ +} + +void subsurface_console_exit(void) +{ + /* NOP */ +} diff --git a/subsurface-core/membuffer.c b/subsurface-core/membuffer.c new file mode 100644 index 000000000..2889a0cdc --- /dev/null +++ b/subsurface-core/membuffer.c @@ -0,0 +1,285 @@ +#include +#include +#include +#include + +#include "dive.h" +#include "membuffer.h" + +char *detach_buffer(struct membuffer *b) +{ + char *result = b->buffer; + b->buffer = NULL; + b->len = 0; + b->alloc = 0; + return result; +} + +void free_buffer(struct membuffer *b) +{ + free(detach_buffer(b)); +} + +void flush_buffer(struct membuffer *b, FILE *f) +{ + if (b->len) { + fwrite(b->buffer, 1, b->len, f); + free_buffer(b); + } +} + +void strip_mb(struct membuffer *b) +{ + while (b->len && isspace(b->buffer[b->len - 1])) + b->len--; +} + +/* + * Running out of memory isn't really an issue these days. + * So rather than do insane error handling and making the + * interface very complex, we'll just die. It won't happen + * unless you're running on a potato. + */ +static void oom(void) +{ + fprintf(stderr, "Out of memory\n"); + exit(1); +} + +static void make_room(struct membuffer *b, unsigned int size) +{ + unsigned int needed = b->len + size; + if (needed > b->alloc) { + char *n; + /* round it up to not reallocate all the time.. */ + needed = needed * 9 / 8 + 1024; + n = realloc(b->buffer, needed); + if (!n) + oom(); + b->buffer = n; + b->alloc = needed; + } +} + +const char *mb_cstring(struct membuffer *b) +{ + make_room(b, 1); + b->buffer[b->len] = 0; + return b->buffer; +} + +void put_bytes(struct membuffer *b, const char *str, int len) +{ + make_room(b, len); + memcpy(b->buffer + b->len, str, len); + b->len += len; +} + +void put_string(struct membuffer *b, const char *str) +{ + put_bytes(b, str, strlen(str)); +} + +void put_vformat(struct membuffer *b, const char *fmt, va_list args) +{ + int room = 128; + + for (;;) { + int len; + va_list copy; + char *target; + + make_room(b, room); + room = b->alloc - b->len; + target = b->buffer + b->len; + + va_copy(copy, args); + len = vsnprintf(target, room, fmt, copy); + va_end(copy); + + if (len < room) { + b->len += len; + return; + } + + room = len + 1; + } +} + +/* Silly helper using membuffer */ +char *vformat_string(const char *fmt, va_list args) +{ + struct membuffer mb = { 0 }; + put_vformat(&mb, fmt, args); + mb_cstring(&mb); + return detach_buffer(&mb); +} + +char *format_string(const char *fmt, ...) +{ + va_list args; + char *result; + + va_start(args, fmt); + result = vformat_string(fmt, args); + va_end(args); + return result; +} + +void put_format(struct membuffer *b, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + put_vformat(b, fmt, args); + va_end(args); +} + +void put_milli(struct membuffer *b, const char *pre, int value, const char *post) +{ + int i; + char buf[4]; + const char *sign = ""; + unsigned v; + + v = value; + if (value < 0) { + sign = "-"; + v = -value; + } + for (i = 2; i >= 0; i--) { + buf[i] = (v % 10) + '0'; + v /= 10; + } + buf[3] = 0; + if (buf[2] == '0') { + buf[2] = 0; + if (buf[1] == '0') + buf[1] = 0; + } + + put_format(b, "%s%s%u.%s%s", pre, sign, v, buf, post); +} + +void put_temperature(struct membuffer *b, temperature_t temp, const char *pre, const char *post) +{ + if (temp.mkelvin) + put_milli(b, pre, temp.mkelvin - ZERO_C_IN_MKELVIN, post); +} + +void put_depth(struct membuffer *b, depth_t depth, const char *pre, const char *post) +{ + if (depth.mm) + put_milli(b, pre, depth.mm, post); +} + +void put_duration(struct membuffer *b, duration_t duration, const char *pre, const char *post) +{ + if (duration.seconds) + put_format(b, "%s%u:%02u%s", pre, FRACTION(duration.seconds, 60), post); +} + +void put_pressure(struct membuffer *b, pressure_t pressure, const char *pre, const char *post) +{ + if (pressure.mbar) + put_milli(b, pre, pressure.mbar, post); +} + +void put_salinity(struct membuffer *b, int salinity, const char *pre, const char *post) +{ + if (salinity) + put_format(b, "%s%d%s", pre, salinity / 10, post); +} + +void put_degrees(struct membuffer *b, degrees_t value, const char *pre, const char *post) +{ + int udeg = value.udeg; + const char *sign = ""; + + if (udeg < 0) { + udeg = -udeg; + sign = "-"; + } + put_format(b, "%s%s%u.%06u%s", pre, sign, FRACTION(udeg, 1000000), post); +} + +void put_quoted(struct membuffer *b, const char *text, int is_attribute, int is_html) +{ + const char *p = text; + + for (;;) { + const char *escape; + + switch (*p++) { + default: + continue; + case 0: + escape = NULL; + break; + case 1 ... 8: + case 11: + case 12: + case 14 ... 31: + escape = "?"; + break; + case '<': + escape = "<"; + break; + case '>': + escape = ">"; + break; + case '&': + escape = "&"; + break; + case '\'': + if (!is_attribute) + continue; + escape = "'"; + break; + case '\"': + if (!is_attribute) + continue; + escape = """; + break; + case '\n': + if (!is_html) + continue; + else + escape = "
"; + } + put_bytes(b, text, (p - text - 1)); + if (!escape) + break; + put_string(b, escape); + text = p; + } +} + +char *add_to_string_va(const char *old, const char *fmt, va_list args) +{ + char *res; + struct membuffer o = { 0 }, n = { 0 }; + put_vformat(&n, fmt, args); + put_format(&o, "%s\n%s", old ?: "", mb_cstring(&n)); + res = strdup(mb_cstring(&o)); + free_buffer(&o); + free_buffer(&n); + free((void *)old); + return res; +} + +/* this is a convenience function that cleverly adds text to a string, using our membuffer + * infrastructure. + * WARNING - this will free(old), the intended pattern is + * string = add_to_string(string, fmt, ...) + */ +char *add_to_string(const char *old, const char *fmt, ...) +{ + char *res; + va_list args; + + va_start(args, fmt); + res = add_to_string_va(old, fmt, args); + va_end(args); + return res; +} diff --git a/subsurface-core/membuffer.h b/subsurface-core/membuffer.h new file mode 100644 index 000000000..434b34c71 --- /dev/null +++ b/subsurface-core/membuffer.h @@ -0,0 +1,74 @@ +#ifndef MEMBUFFER_H +#define MEMBUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct membuffer { + unsigned int len, alloc; + char *buffer; +}; + +#ifdef __GNUC__ +#define __printf(x, y) __attribute__((__format__(__printf__, x, y))) +#else +#define __printf(x, y) +#endif + +extern char *detach_buffer(struct membuffer *b); +extern void free_buffer(struct membuffer *); +extern void flush_buffer(struct membuffer *, FILE *); +extern void put_bytes(struct membuffer *, const char *, int); +extern void put_string(struct membuffer *, const char *); +extern void put_quoted(struct membuffer *, const char *, int, int); +extern void strip_mb(struct membuffer *); +extern const char *mb_cstring(struct membuffer *); +extern __printf(2, 0) void put_vformat(struct membuffer *, const char *, va_list); +extern __printf(2, 3) void put_format(struct membuffer *, const char *fmt, ...); +extern __printf(2, 0) char *add_to_string_va(const char *old, const char *fmt, va_list args); +extern __printf(2, 3) char *add_to_string(const char *old, const char *fmt, ...); + +/* Helpers that use membuffers internally */ +extern __printf(1, 0) char *vformat_string(const char *, va_list); +extern __printf(1, 2) char *format_string(const char *, ...); + + +/* Output one of our "milli" values with type and pre/post data */ +extern void put_milli(struct membuffer *, const char *, int, const char *); + +/* + * Helper functions for showing particular types. If the type + * is empty, nothing is done, and the function returns false. + * Otherwise, it returns true. + * + * The two "const char *" at the end are pre/post data. + * + * The reason for the pre/post data is so that you can easily + * prepend and append a string without having to test whether the + * type is empty. So + * + * put_temperature(b, temp, "Temp=", " C\n"); + * + * writes nothing to the buffer if there is no temperature data, + * but otherwise would a string that looks something like + * + * "Temp=28.1 C\n" + * + * to the memory buffer (typically the post/pre will be some XML + * pattern and unit string or whatever). + */ +extern void put_temperature(struct membuffer *, temperature_t, const char *, const char *); +extern void put_depth(struct membuffer *, depth_t, const char *, const char *); +extern void put_duration(struct membuffer *, duration_t, const char *, const char *); +extern void put_pressure(struct membuffer *, pressure_t, const char *, const char *); +extern void put_salinity(struct membuffer *, int, const char *, const char *); +extern void put_degrees(struct membuffer *b, degrees_t value, const char *, const char *); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/subsurface-core/ostctools.c b/subsurface-core/ostctools.c new file mode 100644 index 000000000..4b4cff241 --- /dev/null +++ b/subsurface-core/ostctools.c @@ -0,0 +1,193 @@ +#include +#include +#include + +#include "dive.h" +#include "gettext.h" +#include "divelist.h" +#include "libdivecomputer.h" + +/* + * Returns a dc_descriptor_t structure based on dc model's number and family. + */ + +static dc_descriptor_t *ostc_get_data_descriptor(int data_model, dc_family_t data_fam) +{ + dc_descriptor_t *descriptor = NULL, *current = NULL; + ; + dc_iterator_t *iterator = NULL; + dc_status_t rc; + + rc = dc_descriptor_iterator(&iterator); + if (rc != DC_STATUS_SUCCESS) { + fprintf(stderr, "Error creating the device descriptor iterator.\n"); + return current; + } + while ((dc_iterator_next(iterator, &descriptor)) == DC_STATUS_SUCCESS) { + int desc_model = dc_descriptor_get_model(descriptor); + dc_family_t desc_fam = dc_descriptor_get_type(descriptor); + if (data_model == desc_model && data_fam == desc_fam) { + current = descriptor; + break; + } + dc_descriptor_free(descriptor); + } + dc_iterator_free(iterator); + return current; +} + +/* + * Fills a device_data_t structure with known dc data and a descriptor. + */ +static int ostc_prepare_data(int data_model, dc_family_t dc_fam, device_data_t *dev_data) +{ + dc_descriptor_t *data_descriptor; + + dev_data->device = NULL; + dev_data->context = NULL; + + data_descriptor = ostc_get_data_descriptor(data_model, dc_fam); + if (data_descriptor) { + dev_data->descriptor = data_descriptor; + dev_data->vendor = copy_string(data_descriptor->vendor); + dev_data->model = copy_string(data_descriptor->product); + } else { + return 0; + } + return 1; +} + +/* + * OSTCTools stores the raw dive data in heavily padded files, one dive + * each file. So it's not necesary to iterate once and again on a parsing + * function. Actually there's only one kind of archive for every DC model. + */ +void ostctools_import(const char *file, struct dive_table *divetable) +{ + FILE *archive; + device_data_t *devdata = calloc(1, sizeof(device_data_t)); + dc_family_t dc_fam; + unsigned char *buffer = calloc(65536, 1), *uc_tmp; + char *tmp; + struct dive *ostcdive = alloc_dive(); + dc_status_t rc = 0; + int model, ret, i = 0; + unsigned int serial; + struct extra_data *ptr; + + // Open the archive + if ((archive = subsurface_fopen(file, "rb")) == NULL) { + report_error(translate("gettextFromC", "Failed to read '%s'"), file); + free(ostcdive); + goto out; + } + + // Read dive number from the log + uc_tmp = calloc(2, 1); + fseek(archive, 258, 0); + fread(uc_tmp, 1, 2, archive); + ostcdive->number = uc_tmp[0] + (uc_tmp[1] << 8); + free(uc_tmp); + + // Read device's serial number + uc_tmp = calloc(2, 1); + fseek(archive, 265, 0); + fread(uc_tmp, 1, 2, archive); + serial = uc_tmp[0] + (uc_tmp[1] << 8); + free(uc_tmp); + + // Read dive's raw data, header + profile + fseek(archive, 456, 0); + while (!feof(archive)) { + fread(buffer + i, 1, 1, archive); + if (buffer[i] == 0xFD && buffer[i - 1] == 0xFD) + break; + i++; + } + + // Try to determine the dc family based on the header type + if (buffer[2] == 0x20 || buffer[2] == 0x21) { + dc_fam = DC_FAMILY_HW_OSTC; + } else { + switch (buffer[8]) { + case 0x22: + dc_fam = DC_FAMILY_HW_FROG; + break; + case 0x23: + dc_fam = DC_FAMILY_HW_OSTC3; + break; + default: + report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number); + free(ostcdive); + fclose(archive); + goto out; + } + } + + // Try to determine the model based on serial number + switch (dc_fam) { + case DC_FAMILY_HW_OSTC: + if (serial > 7000) + model = 3; //2C + else if (serial > 2048) + model = 2; //2N + else if (serial > 300) + model = 1; //MK2 + else + model = 0; //OSTC + break; + case DC_FAMILY_HW_FROG: + model = 0; + break; + default: + if (serial > 10000) + model = 0x12; //Sport + else + model = 0x0A; //OSTC3 + } + + // Prepare data to pass to libdivecomputer. + ret = ostc_prepare_data(model, dc_fam, devdata); + if (ret == 0) { + report_error(translate("gettextFromC", "Unknown DC in dive %d"), ostcdive->number); + free(ostcdive); + fclose(archive); + goto out; + } + tmp = calloc(strlen(devdata->vendor) + strlen(devdata->model) + 28, 1); + sprintf(tmp, "%s %s (Imported from OSTCTools)", devdata->vendor, devdata->model); + ostcdive->dc.model = copy_string(tmp); + free(tmp); + + // Parse the dive data + rc = libdc_buffer_parser(ostcdive, devdata, buffer, i + 1); + if (rc != DC_STATUS_SUCCESS) + report_error(translate("gettextFromC", "Error - %s - parsing dive %d"), errmsg(rc), ostcdive->number); + + // Serial number is not part of the header nor the profile, so libdc won't + // catch it. If Serial is part of the extra_data, and set to zero, remove + // it from the list and add again. + tmp = calloc(12, 1); + sprintf(tmp, "%d", serial); + ostcdive->dc.serial = copy_string(tmp); + free(tmp); + + if (ostcdive->dc.extra_data) { + ptr = ostcdive->dc.extra_data; + while (strcmp(ptr->key, "Serial")) + ptr = ptr->next; + if (!strcmp(ptr->value, "0")) { + add_extra_data(&ostcdive->dc, "Serial", ostcdive->dc.serial); + *ptr = *(ptr)->next; + } + } else { + add_extra_data(&ostcdive->dc, "Serial", ostcdive->dc.serial); + } + record_dive_to_table(ostcdive, divetable); + mark_divelist_changed(true); + sort_table(divetable); + fclose(archive); +out: + free(devdata); + free(buffer); +} diff --git a/subsurface-core/parse-xml.c b/subsurface-core/parse-xml.c new file mode 100644 index 000000000..3d86222b9 --- /dev/null +++ b/subsurface-core/parse-xml.c @@ -0,0 +1,3641 @@ +#include +#include +#include +#include +#include +#include +#include +#define __USE_XOPEN +#include +#include +#include +#include +#include +#include + +#include "gettext.h" + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" + +int verbose, quit; +int metric = 1; +int last_xml_version = -1; +int diveid = -1; + +static xmlDoc *test_xslt_transforms(xmlDoc *doc, const char **params); + +/* the dive table holds the overall dive list; target table points at + * the table we are currently filling */ +struct dive_table dive_table; +struct dive_table *target_table = NULL; + +/* Trim a character string by removing leading and trailing white space characters. + * Parameter: a pointer to a null-terminated character string (buffer); + * Return value: length of the trimmed string, excluding the terminal 0x0 byte + * The original pointer (buffer) remains valid after this function has been called + * and points to the trimmed string */ +int trimspace(char *buffer) { + int i, size, start, end; + size = strlen(buffer); + for(start = 0; isspace(buffer[start]); start++) + if (start >= size) return 0; // Find 1st character following leading whitespace + for(end = size - 1; isspace(buffer[end]); end--) // Find last character before trailing whitespace + if (end <= 0) return 0; + for(i = start; i <= end; i++) // Move the nonspace characters to the start of the string + buffer[i-start] = buffer[i]; + size = end - start + 1; + buffer[size] = 0x0; // then terminate the string + return size; // return string length +} + +/* + * Clear a dive_table + */ +void clear_table(struct dive_table *table) +{ + for (int i = 0; i < table->nr; i++) + free(table->dives[i]); + table->nr = 0; +} + +/* + * Add a dive into the dive_table array + */ +void record_dive_to_table(struct dive *dive, struct dive_table *table) +{ + assert(table != NULL); + struct dive **dives = grow_dive_table(table); + int nr = table->nr; + + dives[nr] = fixup_dive(dive); + table->nr = nr + 1; +} + +void record_dive(struct dive *dive) +{ + record_dive_to_table(dive, &dive_table); +} + +static void start_match(const char *type, const char *name, char *buffer) +{ + if (verbose > 2) + printf("Matching %s '%s' (%s)\n", + type, name, buffer); +} + +static void nonmatch(const char *type, const char *name, char *buffer) +{ + if (verbose > 1) + printf("Unable to match %s '%s' (%s)\n", + type, name, buffer); +} + +typedef void (*matchfn_t)(char *buffer, void *); + +static int match(const char *pattern, int plen, + const char *name, + matchfn_t fn, char *buf, void *data) +{ + switch (name[plen]) { + case '\0': + case '.': + break; + default: + return 0; + } + if (memcmp(pattern, name, plen)) + return 0; + fn(buf, data); + return 1; +} + + +struct units xml_parsing_units; +const struct units SI_units = SI_UNITS; +const struct units IMPERIAL_units = IMPERIAL_UNITS; + +/* + * Dive info as it is being built up.. + */ +#define MAX_EVENT_NAME 128 +static struct divecomputer *cur_dc; +static struct dive *cur_dive; +static struct dive_site *cur_dive_site; +degrees_t cur_latitude, cur_longitude; +static dive_trip_t *cur_trip = NULL; +static struct sample *cur_sample; +static struct picture *cur_picture; +static union { + struct event event; + char allocation[sizeof(struct event)+MAX_EVENT_NAME]; +} event_allocation = { .event.deleted = 1 }; +#define cur_event event_allocation.event +static struct { + struct { + const char *model; + uint32_t deviceid; + const char *nickname, *serial_nr, *firmware; + } dc; +} cur_settings; +static bool in_settings = false; +static bool in_userid = false; +static struct tm cur_tm; +static int cur_cylinder_index, cur_ws_index; +static int lastndl, laststoptime, laststopdepth, lastcns, lastpo2, lastindeco; +static int lastcylinderindex, lastsensor, next_o2_sensor; +static struct extra_data cur_extra_data; + +/* + * If we don't have an explicit dive computer, + * we use the implicit one that every dive has.. + */ +static struct divecomputer *get_dc(void) +{ + return cur_dc ?: &cur_dive->dc; +} + +static enum import_source { + UNKNOWN, + LIBDIVECOMPUTER, + DIVINGLOG, + UDDF, + SSRF_WS, +} import_source; + +static void divedate(const char *buffer, timestamp_t *when) +{ + int d, m, y; + int hh, mm, ss; + + hh = 0; + mm = 0; + ss = 0; + if (sscanf(buffer, "%d.%d.%d %d:%d:%d", &d, &m, &y, &hh, &mm, &ss) >= 3) { + /* This is ok, and we got at least the date */ + } else if (sscanf(buffer, "%d-%d-%d %d:%d:%d", &y, &m, &d, &hh, &mm, &ss) >= 3) { + /* This is also ok */ + } else { + fprintf(stderr, "Unable to parse date '%s'\n", buffer); + return; + } + cur_tm.tm_year = y; + cur_tm.tm_mon = m - 1; + cur_tm.tm_mday = d; + cur_tm.tm_hour = hh; + cur_tm.tm_min = mm; + cur_tm.tm_sec = ss; + + *when = utc_mktime(&cur_tm); +} + +static void divetime(const char *buffer, timestamp_t *when) +{ + int h, m, s = 0; + + if (sscanf(buffer, "%d:%d:%d", &h, &m, &s) >= 2) { + cur_tm.tm_hour = h; + cur_tm.tm_min = m; + cur_tm.tm_sec = s; + *when = utc_mktime(&cur_tm); + } +} + +/* Libdivecomputer: "2011-03-20 10:22:38" */ +static void divedatetime(char *buffer, timestamp_t *when) +{ + int y, m, d; + int hr, min, sec; + + if (sscanf(buffer, "%d-%d-%d %d:%d:%d", + &y, &m, &d, &hr, &min, &sec) == 6) { + cur_tm.tm_year = y; + cur_tm.tm_mon = m - 1; + cur_tm.tm_mday = d; + cur_tm.tm_hour = hr; + cur_tm.tm_min = min; + cur_tm.tm_sec = sec; + *when = utc_mktime(&cur_tm); + } +} + +enum ParseState { + FINDSTART, + FINDEND +}; +static void divetags(char *buffer, struct tag_entry **tags) +{ + int i = 0, start = 0, end = 0; + enum ParseState state = FINDEND; + int len = buffer ? strlen(buffer) : 0; + + while (i < len) { + if (buffer[i] == ',') { + if (state == FINDSTART) { + /* Detect empty tags */ + } else if (state == FINDEND) { + /* Found end of tag */ + if (i > 0 && buffer[i - 1] != '\\') { + buffer[i] = '\0'; + state = FINDSTART; + taglist_add_tag(tags, buffer + start); + } else { + state = FINDSTART; + } + } + } else if (buffer[i] == ' ') { + /* Handled */ + } else { + /* Found start of tag */ + if (state == FINDSTART) { + state = FINDEND; + start = i; + } else if (state == FINDEND) { + end = i; + } + } + i++; + } + if (state == FINDEND) { + if (end < start) + end = len - 1; + if (len > 0) { + buffer[end + 1] = '\0'; + taglist_add_tag(tags, buffer + start); + } + } +} + +enum number_type { + NEITHER, + FLOAT +}; + +static enum number_type parse_float(const char *buffer, double *res, const char **endp) +{ + double val; + static bool first_time = true; + + errno = 0; + val = ascii_strtod(buffer, endp); + if (errno || *endp == buffer) + return NEITHER; + if (**endp == ',') { + if (IS_FP_SAME(val, rint(val))) { + /* we really want to send an error if this is a Subsurface native file + * as this is likely indication of a bug - but right now we don't have + * that information available */ + if (first_time) { + fprintf(stderr, "Floating point value with decimal comma (%s)?\n", buffer); + first_time = false; + } + /* Try again in permissive mode*/ + val = strtod_flags(buffer, endp, 0); + } + } + + *res = val; + return FLOAT; +} + +union int_or_float { + double fp; +}; + +static enum number_type integer_or_float(char *buffer, union int_or_float *res) +{ + const char *end; + return parse_float(buffer, &res->fp, &end); +} + +static void pressure(char *buffer, pressure_t *pressure) +{ + double mbar = 0.0; + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + /* Just ignore zero values */ + if (!val.fp) + break; + switch (xml_parsing_units.pressure) { + case PASCAL: + mbar = val.fp / 100; + break; + case BAR: + /* Assume mbar, but if it's really small, it's bar */ + mbar = val.fp; + if (fabs(mbar) < 5000) + mbar = mbar * 1000; + break; + case PSI: + mbar = psi_to_mbar(val.fp); + break; + } + if (fabs(mbar) > 5 && fabs(mbar) < 5000000) { + pressure->mbar = rint(mbar); + break; + } + /* fallthrough */ + default: + printf("Strange pressure reading %s\n", buffer); + } +} + +static void cylinder_use(char *buffer, enum cylinderuse *cyl_use) +{ + if (trimspace(buffer)) + *cyl_use = cylinderuse_from_text(buffer); +} + +static void salinity(char *buffer, int *salinity) +{ + union int_or_float val; + switch (integer_or_float(buffer, &val)) { + case FLOAT: + *salinity = rint(val.fp * 10.0); + break; + default: + printf("Strange salinity reading %s\n", buffer); + } +} + +static void depth(char *buffer, depth_t *depth) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + switch (xml_parsing_units.length) { + case METERS: + depth->mm = rint(val.fp * 1000); + break; + case FEET: + depth->mm = feet_to_mm(val.fp); + break; + } + break; + default: + printf("Strange depth reading %s\n", buffer); + } +} + +static void extra_data_start(void) +{ + memset(&cur_extra_data, 0, sizeof(struct extra_data)); +} + +static void extra_data_end(void) +{ + // don't save partial structures - we must have both key and value + if (cur_extra_data.key && cur_extra_data.value) + add_extra_data(cur_dc, cur_extra_data.key, cur_extra_data.value); +} + +static void weight(char *buffer, weight_t *weight) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + switch (xml_parsing_units.weight) { + case KG: + weight->grams = rint(val.fp * 1000); + break; + case LBS: + weight->grams = lbs_to_grams(val.fp); + break; + } + break; + default: + printf("Strange weight reading %s\n", buffer); + } +} + +static void temperature(char *buffer, temperature_t *temperature) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + switch (xml_parsing_units.temperature) { + case KELVIN: + temperature->mkelvin = val.fp * 1000; + break; + case CELSIUS: + temperature->mkelvin = C_to_mkelvin(val.fp); + break; + case FAHRENHEIT: + temperature->mkelvin = F_to_mkelvin(val.fp); + break; + } + break; + default: + printf("Strange temperature reading %s\n", buffer); + } + /* temperatures outside -40C .. +70C should be ignored */ + if (temperature->mkelvin < ZERO_C_IN_MKELVIN - 40000 || + temperature->mkelvin > ZERO_C_IN_MKELVIN + 70000) + temperature->mkelvin = 0; +} + +static void sampletime(char *buffer, duration_t *time) +{ + int i; + int min, sec; + + i = sscanf(buffer, "%d:%d", &min, &sec); + switch (i) { + case 1: + sec = min; + min = 0; + /* fallthrough */ + case 2: + time->seconds = sec + min * 60; + break; + default: + printf("Strange sample time reading %s\n", buffer); + } +} + +static void offsettime(char *buffer, offset_t *time) +{ + duration_t uoffset; + int sign = 1; + if (*buffer == '-') { + sign = -1; + buffer++; + } + /* yes, this could indeed fail if we have an offset > 34yrs + * - too bad */ + sampletime(buffer, &uoffset); + time->seconds = sign * uoffset.seconds; +} + +static void duration(char *buffer, duration_t *time) +{ + /* DivingLog 5.08 (and maybe other versions) appear to sometimes + * store the dive time as 44.00 instead of 44:00; + * This attempts to parse this in a fairly robust way */ + if (!strchr(buffer, ':') && strchr(buffer, '.')) { + char *mybuffer = strdup(buffer); + char *dot = strchr(mybuffer, '.'); + *dot = ':'; + sampletime(mybuffer, time); + free(mybuffer); + } else { + sampletime(buffer, time); + } +} + +static void percent(char *buffer, fraction_t *fraction) +{ + double val; + const char *end; + + switch (parse_float(buffer, &val, &end)) { + case FLOAT: + /* Turn fractions into percent unless explicit.. */ + if (val <= 1.0) { + while (isspace(*end)) + end++; + if (*end != '%') + val *= 100; + } + + /* Then turn percent into our integer permille format */ + if (val >= 0 && val <= 100.0) { + fraction->permille = rint(val * 10); + break; + } + default: + printf(translate("gettextFromC", "Strange percentage reading %s\n"), buffer); + break; + } +} + +static void gasmix(char *buffer, fraction_t *fraction) +{ + /* libdivecomputer does negative percentages. */ + if (*buffer == '-') + return; + if (cur_cylinder_index < MAX_CYLINDERS) + percent(buffer, fraction); +} + +static void gasmix_nitrogen(char *buffer, struct gasmix *gasmix) +{ + /* Ignore n2 percentages. There's no value in them. */ +} + +static void cylindersize(char *buffer, volume_t *volume) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + volume->mliter = rint(val.fp * 1000); + break; + + default: + printf("Strange volume reading %s\n", buffer); + break; + } +} + +static void utf8_string(char *buffer, void *_res) +{ + char **res = _res; + int size; + size = trimspace(buffer); + if(size) + *res = strdup(buffer); +} + +static void event_name(char *buffer, char *name) +{ + int size = trimspace(buffer); + if (size >= MAX_EVENT_NAME) + size = MAX_EVENT_NAME-1; + memcpy(name, buffer, size); + name[size] = 0; +} + +// We don't use gauge as a mode, and pscr doesn't exist as a libdc divemode +const char *libdc_divemode_text[] = { "oc", "cc", "pscr", "freedive", "gauge"}; + +/* Extract the dive computer type from the xml text buffer */ +static void get_dc_type(char *buffer, enum dive_comp_type *dct) +{ + if (trimspace(buffer)) { + for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) { + if (strcmp(buffer, divemode_text[i]) == 0) + *dct = i; + else if (strcmp(buffer, libdc_divemode_text[i]) == 0) + *dct = i; + } + } +} + +#define MATCH(pattern, fn, dest) ({ \ + /* Silly type compatibility test */ \ + if (0) (fn)("test", dest); \ + match(pattern, strlen(pattern), name, (matchfn_t) (fn), buf, dest); }) + +static void get_index(char *buffer, int *i) +{ + *i = atoi(buffer); +} + +static void get_uint8(char *buffer, uint8_t *i) +{ + *i = atoi(buffer); +} + +static void get_bearing(char *buffer, bearing_t *bearing) +{ + bearing->degrees = atoi(buffer); +} + +static void get_rating(char *buffer, int *i) +{ + int j = atoi(buffer); + if (j >= 0 && j <= 5) { + *i = j; + } +} + +static void double_to_o2pressure(char *buffer, o2pressure_t *i) +{ + i->mbar = rint(ascii_strtod(buffer, NULL) * 1000.0); +} + +static void hex_value(char *buffer, uint32_t *i) +{ + *i = strtoul(buffer, NULL, 16); +} + +static void get_tripflag(char *buffer, tripflag_t *tf) +{ + *tf = strcmp(buffer, "NOTRIP") ? TF_NONE : NO_TRIP; +} + +/* + * Divinglog is crazy. The temperatures are in celsius. EXCEPT + * for the sample temperatures, that are in Fahrenheit. + * WTF? + * + * Oh, and I think Diving Log *internally* probably kept them + * in celsius, because I'm seeing entries like + * + * 32.0 + * + * in there. Which is freezing, aka 0 degC. I bet the "0" is + * what Diving Log uses for "no temperature". + * + * So throw away crap like that. + * + * It gets worse. Sometimes the sample temperatures are in + * Celsius, which apparently happens if you are in a SI + * locale. So we now do: + * + * - temperatures < 32.0 == Celsius + * - temperature == 32.0 -> garbage, it's a missing temperature (zero converted from C to F) + * - temperatures > 32.0 == Fahrenheit + */ +static void fahrenheit(char *buffer, temperature_t *temperature) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + if (IS_FP_SAME(val.fp, 32.0)) + break; + if (val.fp < 32.0) + temperature->mkelvin = C_to_mkelvin(val.fp); + else + temperature->mkelvin = F_to_mkelvin(val.fp); + break; + default: + fprintf(stderr, "Crazy Diving Log temperature reading %s\n", buffer); + } +} + +/* + * Did I mention how bat-shit crazy divinglog is? The sample + * pressures are in PSI. But the tank working pressure is in + * bar. WTF^2? + * + * Crazy stuff like this is why subsurface has everything in + * these inconvenient typed structures, and you have to say + * "pressure->mbar" to get the actual value. Exactly so that + * you can never have unit confusion. + * + * It gets worse: sometimes apparently the pressures are in + * bar, sometimes in psi. Dirk suspects that this may be a + * DivingLog Uemis importer bug, and that they are always + * supposed to be in bar, but that the importer got the + * sample importing wrong. + * + * Sadly, there's no way to really tell. So I think we just + * have to have some arbitrary cut-off point where we assume + * that smaller values mean bar.. Not good. + */ +static void psi_or_bar(char *buffer, pressure_t *pressure) +{ + union int_or_float val; + + switch (integer_or_float(buffer, &val)) { + case FLOAT: + if (val.fp > 400) + pressure->mbar = psi_to_mbar(val.fp); + else + pressure->mbar = rint(val.fp * 1000); + break; + default: + fprintf(stderr, "Crazy Diving Log PSI reading %s\n", buffer); + } +} + +static int divinglog_fill_sample(struct sample *sample, const char *name, char *buf) +{ + return MATCH("time.p", sampletime, &sample->time) || + MATCH("depth.p", depth, &sample->depth) || + MATCH("temp.p", fahrenheit, &sample->temperature) || + MATCH("press1.p", psi_or_bar, &sample->cylinderpressure) || + 0; +} + +static void uddf_gasswitch(char *buffer, struct sample *sample) +{ + int idx = atoi(buffer); + int seconds = sample->time.seconds; + struct dive *dive = cur_dive; + struct divecomputer *dc = get_dc(); + + add_gas_switch_event(dive, dc, seconds, idx); +} + +static int uddf_fill_sample(struct sample *sample, const char *name, char *buf) +{ + return MATCH("divetime", sampletime, &sample->time) || + MATCH("depth", depth, &sample->depth) || + MATCH("temperature", temperature, &sample->temperature) || + MATCH("tankpressure", pressure, &sample->cylinderpressure) || + MATCH("ref.switchmix", uddf_gasswitch, sample) || + 0; +} + +static void eventtime(char *buffer, duration_t *duration) +{ + sampletime(buffer, duration); + if (cur_sample) + duration->seconds += cur_sample->time.seconds; +} + +static void try_to_match_autogroup(const char *name, char *buf) +{ + int autogroupvalue; + + start_match("autogroup", name, buf); + if (MATCH("state.autogroup", get_index, &autogroupvalue)) { + set_autogroup(autogroupvalue); + return; + } + nonmatch("autogroup", name, buf); +} + +void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int seconds, int idx) +{ + /* sanity check so we don't crash */ + if (idx < 0 || idx >= MAX_CYLINDERS) + return; + /* The gas switch event format is insane for historical reasons */ + struct gasmix *mix = &dive->cylinder[idx].gasmix; + int o2 = get_o2(mix); + int he = get_he(mix); + struct event *ev; + int value; + + o2 = (o2 + 5) / 10; + he = (he + 5) / 10; + value = o2 + (he << 16); + + ev = add_event(dc, seconds, he ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE, 0, value, "gaschange"); + if (ev) { + ev->gas.index = idx; + ev->gas.mix = *mix; + } +} + +static void get_cylinderindex(char *buffer, uint8_t *i) +{ + *i = atoi(buffer); + if (lastcylinderindex != *i) { + add_gas_switch_event(cur_dive, get_dc(), cur_sample->time.seconds, *i); + lastcylinderindex = *i; + } +} + +static void get_sensor(char *buffer, uint8_t *i) +{ + *i = atoi(buffer); + lastsensor = *i; +} + +static void parse_libdc_deco(char *buffer, struct sample *s) +{ + if (strcmp(buffer, "deco") == 0) { + s->in_deco = true; + } else if (strcmp(buffer, "ndl") == 0) { + s->in_deco = false; + // The time wasn't stoptime, it was ndl + s->ndl = s->stoptime; + s->stoptime.seconds = 0; + } +} + +static void try_to_fill_dc_settings(const char *name, char *buf) +{ + start_match("divecomputerid", name, buf); + if (MATCH("model.divecomputerid", utf8_string, &cur_settings.dc.model)) + return; + if (MATCH("deviceid.divecomputerid", hex_value, &cur_settings.dc.deviceid)) + return; + if (MATCH("nickname.divecomputerid", utf8_string, &cur_settings.dc.nickname)) + return; + if (MATCH("serial.divecomputerid", utf8_string, &cur_settings.dc.serial_nr)) + return; + if (MATCH("firmware.divecomputerid", utf8_string, &cur_settings.dc.firmware)) + return; + + nonmatch("divecomputerid", name, buf); +} + +static void try_to_fill_event(const char *name, char *buf) +{ + start_match("event", name, buf); + if (MATCH("event", event_name, cur_event.name)) + return; + if (MATCH("name", event_name, cur_event.name)) + return; + if (MATCH("time", eventtime, &cur_event.time)) + return; + if (MATCH("type", get_index, &cur_event.type)) + return; + if (MATCH("flags", get_index, &cur_event.flags)) + return; + if (MATCH("value", get_index, &cur_event.value)) + return; + if (MATCH("cylinder", get_index, &cur_event.gas.index)) { + /* We add one to indicate that we got an actual cylinder index value */ + cur_event.gas.index++; + return; + } + if (MATCH("o2", percent, &cur_event.gas.mix.o2)) + return; + if (MATCH("he", percent, &cur_event.gas.mix.he)) + return; + nonmatch("event", name, buf); +} + +static int match_dc_data_fields(struct divecomputer *dc, const char *name, char *buf) +{ + if (MATCH("maxdepth", depth, &dc->maxdepth)) + return 1; + if (MATCH("meandepth", depth, &dc->meandepth)) + return 1; + if (MATCH("max.depth", depth, &dc->maxdepth)) + return 1; + if (MATCH("mean.depth", depth, &dc->meandepth)) + return 1; + if (MATCH("duration", duration, &dc->duration)) + return 1; + if (MATCH("divetime", duration, &dc->duration)) + return 1; + if (MATCH("divetimesec", duration, &dc->duration)) + return 1; + if (MATCH("surfacetime", duration, &dc->surfacetime)) + return 1; + if (MATCH("airtemp", temperature, &dc->airtemp)) + return 1; + if (MATCH("watertemp", temperature, &dc->watertemp)) + return 1; + if (MATCH("air.temperature", temperature, &dc->airtemp)) + return 1; + if (MATCH("water.temperature", temperature, &dc->watertemp)) + return 1; + if (MATCH("pressure.surface", pressure, &dc->surface_pressure)) + return 1; + if (MATCH("salinity.water", salinity, &dc->salinity)) + return 1; + if (MATCH("key.extradata", utf8_string, &cur_extra_data.key)) + return 1; + if (MATCH("value.extradata", utf8_string, &cur_extra_data.value)) + return 1; + if (MATCH("divemode", get_dc_type, &dc->divemode)) + return 1; + if (MATCH("salinity", salinity, &dc->salinity)) + return 1; + if (MATCH("atmospheric", pressure, &dc->surface_pressure)) + return 1; + return 0; +} + +/* We're in the top-level dive xml. Try to convert whatever value to a dive value */ +static void try_to_fill_dc(struct divecomputer *dc, const char *name, char *buf) +{ + start_match("divecomputer", name, buf); + + if (MATCH("date", divedate, &dc->when)) + return; + if (MATCH("time", divetime, &dc->when)) + return; + if (MATCH("model", utf8_string, &dc->model)) + return; + if (MATCH("deviceid", hex_value, &dc->deviceid)) + return; + if (MATCH("diveid", hex_value, &dc->diveid)) + return; + if (MATCH("dctype", get_dc_type, &dc->divemode)) + return; + if (MATCH("no_o2sensors", get_sensor, &dc->no_o2sensors)) + return; + if (match_dc_data_fields(dc, name, buf)) + return; + + nonmatch("divecomputer", name, buf); +} + +/* We're in samples - try to convert the random xml value to something useful */ +static void try_to_fill_sample(struct sample *sample, const char *name, char *buf) +{ + int in_deco; + + start_match("sample", name, buf); + if (MATCH("pressure.sample", pressure, &sample->cylinderpressure)) + return; + if (MATCH("cylpress.sample", pressure, &sample->cylinderpressure)) + return; + if (MATCH("pdiluent.sample", pressure, &sample->cylinderpressure)) + return; + if (MATCH("o2pressure.sample", pressure, &sample->o2cylinderpressure)) + return; + if (MATCH("cylinderindex.sample", get_cylinderindex, &sample->sensor)) + return; + if (MATCH("sensor.sample", get_sensor, &sample->sensor)) + return; + if (MATCH("depth.sample", depth, &sample->depth)) + return; + if (MATCH("temp.sample", temperature, &sample->temperature)) + return; + if (MATCH("temperature.sample", temperature, &sample->temperature)) + return; + if (MATCH("sampletime.sample", sampletime, &sample->time)) + return; + if (MATCH("time.sample", sampletime, &sample->time)) + return; + if (MATCH("ndl.sample", sampletime, &sample->ndl)) + return; + if (MATCH("tts.sample", sampletime, &sample->tts)) + return; + if (MATCH("in_deco.sample", get_index, &in_deco)) { + sample->in_deco = (in_deco == 1); + return; + } + if (MATCH("stoptime.sample", sampletime, &sample->stoptime)) + return; + if (MATCH("stopdepth.sample", depth, &sample->stopdepth)) + return; + if (MATCH("cns.sample", get_uint8, &sample->cns)) + return; + if (MATCH("rbt.sample", sampletime, &sample->rbt)) + return; + if (MATCH("sensor1.sample", double_to_o2pressure, &sample->o2sensor[0])) // CCR O2 sensor data + return; + if (MATCH("sensor2.sample", double_to_o2pressure, &sample->o2sensor[1])) + return; + if (MATCH("sensor3.sample", double_to_o2pressure, &sample->o2sensor[2])) // up to 3 CCR sensors + return; + if (MATCH("po2.sample", double_to_o2pressure, &sample->setpoint)) + return; + if (MATCH("heartbeat", get_uint8, &sample->heartbeat)) + return; + if (MATCH("bearing", get_bearing, &sample->bearing)) + return; + if (MATCH("setpoint.sample", double_to_o2pressure, &sample->setpoint)) + return; + if (MATCH("ppo2.sample", double_to_o2pressure, &sample->o2sensor[next_o2_sensor])) { + next_o2_sensor++; + return; + } + if (MATCH("deco.sample", parse_libdc_deco, sample)) + return; + if (MATCH("time.deco", sampletime, &sample->stoptime)) + return; + if (MATCH("depth.deco", depth, &sample->stopdepth)) + return; + + switch (import_source) { + case DIVINGLOG: + if (divinglog_fill_sample(sample, name, buf)) + return; + break; + + case UDDF: + if (uddf_fill_sample(sample, name, buf)) + return; + break; + + default: + break; + } + + nonmatch("sample", name, buf); +} + +void try_to_fill_userid(const char *name, char *buf) +{ + if (prefs.save_userid_local) + set_userid(buf); +} + +static const char *country, *city; + +static void divinglog_place(char *place, uint32_t *uuid) +{ + char buffer[1024]; + + snprintf(buffer, sizeof(buffer), + "%s%s%s%s%s", + place, + city ? ", " : "", + city ? city : "", + country ? ", " : "", + country ? country : ""); + *uuid = get_dive_site_uuid_by_name(buffer, NULL); + if (*uuid == 0) + *uuid = create_dive_site(buffer, cur_dive->when); + + city = NULL; + country = NULL; +} + +static int divinglog_dive_match(struct dive *dive, const char *name, char *buf) +{ + return MATCH("divedate", divedate, &dive->when) || + MATCH("entrytime", divetime, &dive->when) || + MATCH("divetime", duration, &dive->dc.duration) || + MATCH("depth", depth, &dive->dc.maxdepth) || + MATCH("depthavg", depth, &dive->dc.meandepth) || + MATCH("tanktype", utf8_string, &dive->cylinder[0].type.description) || + MATCH("tanksize", cylindersize, &dive->cylinder[0].type.size) || + MATCH("presw", pressure, &dive->cylinder[0].type.workingpressure) || + MATCH("press", pressure, &dive->cylinder[0].start) || + MATCH("prese", pressure, &dive->cylinder[0].end) || + MATCH("comments", utf8_string, &dive->notes) || + MATCH("names.buddy", utf8_string, &dive->buddy) || + MATCH("name.country", utf8_string, &country) || + MATCH("name.city", utf8_string, &city) || + MATCH("name.place", divinglog_place, &dive->dive_site_uuid) || + 0; +} + +/* + * Uddf specifies ISO 8601 time format. + * + * There are many variations on that. This handles the useful cases. + */ +static void uddf_datetime(char *buffer, timestamp_t *when) +{ + char c; + int y, m, d, hh, mm, ss; + struct tm tm = { 0 }; + int i; + + i = sscanf(buffer, "%d-%d-%d%c%d:%d:%d", &y, &m, &d, &c, &hh, &mm, &ss); + if (i == 7) + goto success; + ss = 0; + if (i == 6) + goto success; + + i = sscanf(buffer, "%04d%02d%02d%c%02d%02d%02d", &y, &m, &d, &c, &hh, &mm, &ss); + if (i == 7) + goto success; + ss = 0; + if (i == 6) + goto success; +bad_date: + printf("Bad date time %s\n", buffer); + return; + +success: + if (c != 'T' && c != ' ') + goto bad_date; + tm.tm_year = y; + tm.tm_mon = m - 1; + tm.tm_mday = d; + tm.tm_hour = hh; + tm.tm_min = mm; + tm.tm_sec = ss; + *when = utc_mktime(&tm); +} + +#define uddf_datedata(name, offset) \ + static void uddf_##name(char *buffer, timestamp_t *when) \ + { \ + cur_tm.tm_##name = atoi(buffer) + offset; \ + *when = utc_mktime(&cur_tm); \ + } + +uddf_datedata(year, 0) +uddf_datedata(mon, -1) +uddf_datedata(mday, 0) +uddf_datedata(hour, 0) +uddf_datedata(min, 0) + +static int uddf_dive_match(struct dive *dive, const char *name, char *buf) +{ + return MATCH("datetime", uddf_datetime, &dive->when) || + MATCH("diveduration", duration, &dive->dc.duration) || + MATCH("greatestdepth", depth, &dive->dc.maxdepth) || + MATCH("year.date", uddf_year, &dive->when) || + MATCH("month.date", uddf_mon, &dive->when) || + MATCH("day.date", uddf_mday, &dive->when) || + MATCH("hour.time", uddf_hour, &dive->when) || + MATCH("minute.time", uddf_min, &dive->when) || + 0; +} + +/* + * This parses "floating point" into micro-degrees. + * We don't do exponentials etc, if somebody does + * GPS locations in that format, they are insane. + */ +degrees_t parse_degrees(char *buf, char **end) +{ + int sign = 1, decimals = 6, value = 0; + degrees_t ret; + + while (isspace(*buf)) + buf++; + switch (*buf) { + case '-': + sign = -1; + /* fallthrough */ + case '+': + buf++; + } + while (isdigit(*buf)) { + value = 10 * value + *buf - '0'; + buf++; + } + + /* Get the first six decimals if they exist */ + if (*buf == '.') + buf++; + do { + value *= 10; + if (isdigit(*buf)) { + value += *buf - '0'; + buf++; + } + } while (--decimals); + + /* Rounding */ + switch (*buf) { + case '5' ... '9': + value++; + } + while (isdigit(*buf)) + buf++; + + *end = buf; + ret.udeg = value * sign; + return ret; +} + +static void gps_lat(char *buffer, struct dive *dive) +{ + char *end; + degrees_t latitude = parse_degrees(buffer, &end); + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + dive->dive_site_uuid = create_dive_site_with_gps(NULL, latitude, (degrees_t){0}, dive->when); + } else { + if (ds->latitude.udeg && ds->latitude.udeg != latitude.udeg) + fprintf(stderr, "Oops, changing the latitude of existing dive site id %8x name %s; not good\n", ds->uuid, ds->name ?: "(unknown)"); + ds->latitude = latitude; + } +} + +static void gps_long(char *buffer, struct dive *dive) +{ + char *end; + degrees_t longitude = parse_degrees(buffer, &end); + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) { + dive->dive_site_uuid = create_dive_site_with_gps(NULL, (degrees_t){0}, longitude, dive->when); + } else { + if (ds->longitude.udeg && ds->longitude.udeg != longitude.udeg) + fprintf(stderr, "Oops, changing the longitude of existing dive site id %8x name %s; not good\n", ds->uuid, ds->name ?: "(unknown)"); + ds->longitude = longitude; + } + +} + +static void gps_location(char *buffer, struct dive_site *ds) +{ + char *end; + + ds->latitude = parse_degrees(buffer, &end); + ds->longitude = parse_degrees(end, &end); +} + +/* this is in qthelper.cpp, so including the .h file is a pain */ +extern const char *printGPSCoords(int lat, int lon); + +static void gps_in_dive(char *buffer, struct dive *dive) +{ + char *end; + struct dive_site *ds = NULL; + degrees_t latitude = parse_degrees(buffer, &end); + degrees_t longitude = parse_degrees(end, &end); + uint32_t uuid = dive->dive_site_uuid; + if (uuid == 0) { + // check if we have a dive site within 20 meters of that gps fix + uuid = get_dive_site_uuid_by_gps_proximity(latitude, longitude, 20, &ds); + + if (ds) { + // found a site nearby; in case it turns out this one had a different name let's + // remember the original coordinates so we can create the correct dive site later + cur_latitude = latitude; + cur_longitude = longitude; + dive->dive_site_uuid = uuid; + } else { + dive->dive_site_uuid = create_dive_site_with_gps("", latitude, longitude, dive->when); + ds = get_dive_site_by_uuid(dive->dive_site_uuid); + } + } else { + ds = get_dive_site_by_uuid(uuid); + if (dive_site_has_gps_location(ds) && + (latitude.udeg != 0 || longitude.udeg != 0) && + (ds->latitude.udeg != latitude.udeg || ds->longitude.udeg != longitude.udeg)) { + // Houston, we have a problem + fprintf(stderr, "dive site uuid in dive, but gps location (%10.6f/%10.6f) different from dive location (%10.6f/%10.6f)\n", + ds->latitude.udeg / 1000000.0, ds->longitude.udeg / 1000000.0, + latitude.udeg / 1000000.0, longitude.udeg / 1000000.0); + const char *coords = printGPSCoords(latitude.udeg, longitude.udeg); + ds->notes = add_to_string(ds->notes, translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords); + free((void *)coords); + } else { + ds->latitude = latitude; + ds->longitude = longitude; + } + } +} + +static void add_dive_site(char *ds_name, struct dive *dive) +{ + static int suffix = 1; + char *buffer = ds_name; + char *to_free = NULL; + int size = trimspace(buffer); + if(size) { + uint32_t uuid = dive->dive_site_uuid; + struct dive_site *ds = get_dive_site_by_uuid(uuid); + if (uuid && !ds) { + // that's strange - we have a uuid but it doesn't exist - let's just ignore it + fprintf(stderr, "dive contains a non-existing dive site uuid %x\n", dive->dive_site_uuid); + uuid = 0; + } + if (!uuid) { + // if the dive doesn't have a uuid, check if there's already a dive site by this name + uuid = get_dive_site_uuid_by_name(buffer, &ds); + if (uuid && import_source == SSRF_WS) { + // when downloading GPS fixes from the Subsurface webservice we will often + // get a lot of dives with identical names (the autogenerated fixes). + // So in this case modify the name to make it unique + int name_size = strlen(buffer) + 10; // 8 digits - enough for 100 million sites + to_free = buffer = malloc(name_size); + do { + suffix++; + snprintf(buffer, name_size, "%s %8d", ds_name, suffix); + } while (get_dive_site_uuid_by_name(buffer, NULL) != 0); + ds = NULL; + } + } + if (ds) { + // we have a uuid, let's hope there isn't a different name + if (same_string(ds->name, "")) { + ds->name = copy_string(buffer); + } else if (!same_string(ds->name, buffer)) { + // if it's not the same name, it's not the same dive site + // but wait, we could have gotten this one based on GPS coords and could + // have had two different names for the same site... so let's search the other + // way around + uint32_t exact_match_uuid = get_dive_site_uuid_by_gps_and_name(buffer, ds->latitude, ds->longitude); + if (exact_match_uuid) { + dive->dive_site_uuid = exact_match_uuid; + } else { + dive->dive_site_uuid = create_dive_site(buffer, dive->when); + struct dive_site *newds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (cur_latitude.udeg || cur_longitude.udeg) { + // we started this uuid with GPS data, so lets use those + newds->latitude = cur_latitude; + newds->longitude = cur_longitude; + } else { + newds->latitude = ds->latitude; + newds->longitude = ds->longitude; + } + newds->notes = add_to_string(newds->notes, translate("gettextFromC", "additional name for site: %s\n"), ds->name); + } + } else { + // add the existing dive site to the current dive + dive->dive_site_uuid = uuid; + } + } else { + dive->dive_site_uuid = create_dive_site(buffer, dive->when); + } + } + free(to_free); +} + +static void gps_picture_location(char *buffer, struct picture *pic) +{ + char *end; + + pic->latitude = parse_degrees(buffer, &end); + pic->longitude = parse_degrees(end, &end); +} + +/* We're in the top-level dive xml. Try to convert whatever value to a dive value */ +static void try_to_fill_dive(struct dive *dive, const char *name, char *buf) +{ + start_match("dive", name, buf); + + switch (import_source) { + case DIVINGLOG: + if (divinglog_dive_match(dive, name, buf)) + return; + break; + + case UDDF: + if (uddf_dive_match(dive, name, buf)) + return; + break; + + default: + break; + } + if (MATCH("divesiteid", hex_value, &dive->dive_site_uuid)) + return; + if (MATCH("number", get_index, &dive->number)) + return; + if (MATCH("tags", divetags, &dive->tag_list)) + return; + if (MATCH("tripflag", get_tripflag, &dive->tripflag)) + return; + if (MATCH("date", divedate, &dive->when)) + return; + if (MATCH("time", divetime, &dive->when)) + return; + if (MATCH("datetime", divedatetime, &dive->when)) + return; + /* + * Legacy format note: per-dive depths and duration get saved + * in the first dive computer entry + */ + if (match_dc_data_fields(&dive->dc, name, buf)) + return; + + if (MATCH("filename.picture", utf8_string, &cur_picture->filename)) + return; + if (MATCH("offset.picture", offsettime, &cur_picture->offset)) + return; + if (MATCH("gps.picture", gps_picture_location, cur_picture)) + return; + if (MATCH("hash.picture", utf8_string, &cur_picture->hash)) + return; + if (MATCH("cylinderstartpressure", pressure, &dive->cylinder[0].start)) + return; + if (MATCH("cylinderendpressure", pressure, &dive->cylinder[0].end)) + return; + if (MATCH("gps", gps_in_dive, dive)) + return; + if (MATCH("Place", gps_in_dive, dive)) + return; + if (MATCH("latitude", gps_lat, dive)) + return; + if (MATCH("sitelat", gps_lat, dive)) + return; + if (MATCH("lat", gps_lat, dive)) + return; + if (MATCH("longitude", gps_long, dive)) + return; + if (MATCH("sitelon", gps_long, dive)) + return; + if (MATCH("lon", gps_long, dive)) + return; + if (MATCH("location", add_dive_site, dive)) + return; + if (MATCH("name.dive", add_dive_site, dive)) + return; + if (MATCH("suit", utf8_string, &dive->suit)) + return; + if (MATCH("divesuit", utf8_string, &dive->suit)) + return; + if (MATCH("notes", utf8_string, &dive->notes)) + return; + if (MATCH("divemaster", utf8_string, &dive->divemaster)) + return; + if (MATCH("buddy", utf8_string, &dive->buddy)) + return; + if (MATCH("rating.dive", get_rating, &dive->rating)) + return; + if (MATCH("visibility.dive", get_rating, &dive->visibility)) + return; + if (MATCH("size.cylinder", cylindersize, &dive->cylinder[cur_cylinder_index].type.size)) + return; + if (MATCH("workpressure.cylinder", pressure, &dive->cylinder[cur_cylinder_index].type.workingpressure)) + return; + if (MATCH("description.cylinder", utf8_string, &dive->cylinder[cur_cylinder_index].type.description)) + return; + if (MATCH("start.cylinder", pressure, &dive->cylinder[cur_cylinder_index].start)) + return; + if (MATCH("end.cylinder", pressure, &dive->cylinder[cur_cylinder_index].end)) + return; + if (MATCH("use.cylinder", cylinder_use, &dive->cylinder[cur_cylinder_index].cylinder_use)) + return; + if (MATCH("description.weightsystem", utf8_string, &dive->weightsystem[cur_ws_index].description)) + return; + if (MATCH("weight.weightsystem", weight, &dive->weightsystem[cur_ws_index].weight)) + return; + if (MATCH("weight", weight, &dive->weightsystem[cur_ws_index].weight)) + return; + if (MATCH("o2", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.o2)) + return; + if (MATCH("o2percent", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.o2)) + return; + if (MATCH("n2", gasmix_nitrogen, &dive->cylinder[cur_cylinder_index].gasmix)) + return; + if (MATCH("he", gasmix, &dive->cylinder[cur_cylinder_index].gasmix.he)) + return; + if (MATCH("air.divetemperature", temperature, &dive->airtemp)) + return; + if (MATCH("water.divetemperature", temperature, &dive->watertemp)) + return; + + nonmatch("dive", name, buf); +} + +/* We're in the top-level trip xml. Try to convert whatever value to a trip value */ +static void try_to_fill_trip(dive_trip_t **dive_trip_p, const char *name, char *buf) +{ + start_match("trip", name, buf); + + dive_trip_t *dive_trip = *dive_trip_p; + + if (MATCH("date", divedate, &dive_trip->when)) + return; + if (MATCH("time", divetime, &dive_trip->when)) + return; + if (MATCH("location", utf8_string, &dive_trip->location)) + return; + if (MATCH("notes", utf8_string, &dive_trip->notes)) + return; + + nonmatch("trip", name, buf); +} + +/* We're processing a divesite entry - try to fill the components */ +static void try_to_fill_dive_site(struct dive_site **ds_p, const char *name, char *buf) +{ + start_match("divesite", name, buf); + + struct dive_site *ds = *ds_p; + if (ds->taxonomy.category == NULL) + ds->taxonomy.category = alloc_taxonomy(); + + if (MATCH("uuid", hex_value, &ds->uuid)) + return; + if (MATCH("name", utf8_string, &ds->name)) + return; + if (MATCH("description", utf8_string, &ds->description)) + return; + if (MATCH("notes", utf8_string, &ds->notes)) + return; + if (MATCH("gps", gps_location, ds)) + return; + if (MATCH("cat.geo", get_index, (int *)&ds->taxonomy.category[ds->taxonomy.nr].category)) + return; + if (MATCH("origin.geo", get_index, (int *)&ds->taxonomy.category[ds->taxonomy.nr].origin)) + return; + if (MATCH("value.geo", utf8_string, &ds->taxonomy.category[ds->taxonomy.nr].value)) { + if (ds->taxonomy.nr < TC_NR_CATEGORIES) + ds->taxonomy.nr++; + return; + } + + nonmatch("divesite", name, buf); +} + +/* + * While in some formats file boundaries are dive boundaries, in many + * others (as for example in our native format) there are + * multiple dives per file, so there can be other events too that + * trigger a "new dive" marker and you may get some nesting due + * to that. Just ignore nesting levels. + * On the flipside it is possible that we start an XML file that ends + * up having no dives in it at all - don't create a bogus empty dive + * for those. It's not entirely clear what is the minimum set of data + * to make a dive valid, but if it has no location, no date and no + * samples I'm pretty sure it's useless. + */ +static bool is_dive(void) +{ + return (cur_dive && + (cur_dive->dive_site_uuid || cur_dive->when || cur_dive->dc.samples)); +} + +static void reset_dc_info(struct divecomputer *dc) +{ + lastcns = lastpo2 = lastndl = laststoptime = laststopdepth = lastindeco = 0; + lastsensor = lastcylinderindex = 0; +} + +static void reset_dc_settings(void) +{ + free((void *)cur_settings.dc.model); + free((void *)cur_settings.dc.nickname); + free((void *)cur_settings.dc.serial_nr); + free((void *)cur_settings.dc.firmware); + cur_settings.dc.model = NULL; + cur_settings.dc.nickname = NULL; + cur_settings.dc.serial_nr = NULL; + cur_settings.dc.firmware = NULL; + cur_settings.dc.deviceid = 0; +} + +static void settings_start(void) +{ + in_settings = true; +} + +static void settings_end(void) +{ + in_settings = false; +} + +static void dc_settings_start(void) +{ + reset_dc_settings(); +} + +static void dc_settings_end(void) +{ + create_device_node(cur_settings.dc.model, cur_settings.dc.deviceid, cur_settings.dc.serial_nr, + cur_settings.dc.firmware, cur_settings.dc.nickname); + reset_dc_settings(); +} + +static void dive_site_start(void) +{ + if (cur_dive_site) + return; + cur_dive_site = calloc(1, sizeof(struct dive_site)); +} + +static void dive_site_end(void) +{ + if (!cur_dive_site) + return; + if (cur_dive_site->uuid) { + // we intentionally call this with '0' to ensure we get + // a new structure and then copy things into that new + // structure a few lines below (which sets the correct + // uuid) + struct dive_site *ds = alloc_or_get_dive_site(0); + if (cur_dive_site->taxonomy.nr == 0) { + free(cur_dive_site->taxonomy.category); + cur_dive_site->taxonomy.category = NULL; + } + copy_dive_site(cur_dive_site, ds); + + if (verbose > 3) + printf("completed dive site uuid %x8 name {%s}\n", ds->uuid, ds->name); + } + free_taxonomy(&cur_dive_site->taxonomy); + free(cur_dive_site); + cur_dive_site = NULL; +} + +// now we need to add the code to parse the parts of the divesite enry + +static void dive_start(void) +{ + if (cur_dive) + return; + cur_dive = alloc_dive(); + reset_dc_info(&cur_dive->dc); + memset(&cur_tm, 0, sizeof(cur_tm)); + if (cur_trip) { + add_dive_to_trip(cur_dive, cur_trip); + cur_dive->tripflag = IN_TRIP; + } +} + +static void dive_end(void) +{ + if (!cur_dive) + return; + if (!is_dive()) + free(cur_dive); + else + record_dive_to_table(cur_dive, target_table); + cur_dive = NULL; + cur_dc = NULL; + cur_latitude.udeg = 0; + cur_longitude.udeg = 0; + cur_cylinder_index = 0; + cur_ws_index = 0; +} + +static void trip_start(void) +{ + if (cur_trip) + return; + dive_end(); + cur_trip = calloc(1, sizeof(dive_trip_t)); + memset(&cur_tm, 0, sizeof(cur_tm)); +} + +static void trip_end(void) +{ + if (!cur_trip) + return; + insert_trip(&cur_trip); + cur_trip = NULL; +} + +static void event_start(void) +{ + memset(&cur_event, 0, sizeof(cur_event)); + cur_event.deleted = 0; /* Active */ +} + +static void event_end(void) +{ + struct divecomputer *dc = get_dc(); + if (strcmp(cur_event.name, "surface") != 0) { /* 123 is a magic event that we used for a while to encode images in dives */ + if (cur_event.type == 123) { + struct picture *pic = alloc_picture(); + pic->filename = strdup(cur_event.name); + /* theoretically this could fail - but we didn't support multi year offsets */ + pic->offset.seconds = cur_event.time.seconds; + dive_add_picture(cur_dive, pic); + } else { + struct event *ev; + /* At some point gas change events did not have any type. Thus we need to add + * one on import, if we encounter the type one missing. + */ + if (cur_event.type == 0 && strcmp(cur_event.name, "gaschange") == 0) + cur_event.type = cur_event.value >> 16 > 0 ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE; + ev = add_event(dc, cur_event.time.seconds, + cur_event.type, cur_event.flags, + cur_event.value, cur_event.name); + if (ev && event_is_gaschange(ev)) { + /* See try_to_fill_event() on why the filled-in index is one too big */ + ev->gas.index = cur_event.gas.index-1; + if (cur_event.gas.mix.o2.permille || cur_event.gas.mix.he.permille) + ev->gas.mix = cur_event.gas.mix; + } + } + } + cur_event.deleted = 1; /* No longer active */ +} + +static void picture_start(void) +{ + cur_picture = alloc_picture(); +} + +static void picture_end(void) +{ + dive_add_picture(cur_dive, cur_picture); + cur_picture = NULL; +} + +static void cylinder_start(void) +{ +} + +static void cylinder_end(void) +{ + cur_cylinder_index++; +} + +static void ws_start(void) +{ +} + +static void ws_end(void) +{ + cur_ws_index++; +} + +static void sample_start(void) +{ + cur_sample = prepare_sample(get_dc()); + cur_sample->ndl.seconds = lastndl; + cur_sample->in_deco = lastindeco; + cur_sample->stoptime.seconds = laststoptime; + cur_sample->stopdepth.mm = laststopdepth; + cur_sample->cns = lastcns; + cur_sample->setpoint.mbar = lastpo2; + cur_sample->sensor = lastsensor; + next_o2_sensor = 0; +} + +static void sample_end(void) +{ + if (!cur_dive) + return; + + finish_sample(get_dc()); + lastndl = cur_sample->ndl.seconds; + lastindeco = cur_sample->in_deco; + laststoptime = cur_sample->stoptime.seconds; + laststopdepth = cur_sample->stopdepth.mm; + lastcns = cur_sample->cns; + lastpo2 = cur_sample->setpoint.mbar; + cur_sample = NULL; +} + +static void divecomputer_start(void) +{ + struct divecomputer *dc; + + /* Start from the previous dive computer */ + dc = &cur_dive->dc; + while (dc->next) + dc = dc->next; + + /* Did we already fill that in? */ + if (dc->samples || dc->model || dc->when) { + struct divecomputer *newdc = calloc(1, sizeof(*newdc)); + if (newdc) { + dc->next = newdc; + dc = newdc; + } + } + + /* .. this is the one we'll use */ + cur_dc = dc; + reset_dc_info(dc); +} + +static void divecomputer_end(void) +{ + if (!cur_dc->when) + cur_dc->when = cur_dive->when; + cur_dc = NULL; +} + +static void userid_start(void) +{ + in_userid = true; + set_save_userid_local(true); //if the xml contains userid, keep saving it. +} + +static void userid_stop(void) +{ + in_userid = false; +} + +static bool entry(const char *name, char *buf) +{ + if (!strncmp(name, "version.program", sizeof("version.program") - 1) || + !strncmp(name, "version.divelog", sizeof("version.divelog") - 1)) { + last_xml_version = atoi(buf); + report_datafile_version(last_xml_version); + } + if (in_userid) { + try_to_fill_userid(name, buf); + return true; + } + if (in_settings) { + try_to_fill_dc_settings(name, buf); + try_to_match_autogroup(name, buf); + return true; + } + if (cur_dive_site) { + try_to_fill_dive_site(&cur_dive_site, name, buf); + return true; + } + if (!cur_event.deleted) { + try_to_fill_event(name, buf); + return true; + } + if (cur_sample) { + try_to_fill_sample(cur_sample, name, buf); + return true; + } + if (cur_dc) { + try_to_fill_dc(cur_dc, name, buf); + return true; + } + if (cur_dive) { + try_to_fill_dive(cur_dive, name, buf); + return true; + } + if (cur_trip) { + try_to_fill_trip(&cur_trip, name, buf); + return true; + } + return true; +} + +static const char *nodename(xmlNode *node, char *buf, int len) +{ + int levels = 2; + char *p = buf; + + if (!node || (node->type != XML_CDATA_SECTION_NODE && !node->name)) { + return "root"; + } + + if (node->type == XML_CDATA_SECTION_NODE || (node->parent && !strcmp((const char *)node->name, "text"))) + node = node->parent; + + /* Make sure it's always NUL-terminated */ + p[--len] = 0; + + for (;;) { + const char *name = (const char *)node->name; + char c; + while ((c = *name++) != 0) { + /* Cheaper 'tolower()' for ASCII */ + c = (c >= 'A' && c <= 'Z') ? c - 'A' + 'a' : c; + *p++ = c; + if (!--len) + return buf; + } + *p = 0; + node = node->parent; + if (!node || !node->name) + return buf; + *p++ = '.'; + if (!--len) + return buf; + if (!--levels) + return buf; + } +} + +#define MAXNAME 32 + +static bool visit_one_node(xmlNode *node) +{ + xmlChar *content; + static char buffer[MAXNAME]; + const char *name; + + content = node->content; + if (!content || xmlIsBlankNode(node)) + return true; + + name = nodename(node, buffer, sizeof(buffer)); + + return entry(name, (char *)content); +} + +static bool traverse(xmlNode *root); + +static bool traverse_properties(xmlNode *node) +{ + xmlAttr *p; + bool ret = true; + + for (p = node->properties; p; p = p->next) + if ((ret = traverse(p->children)) == false) + break; + return ret; +} + +static bool visit(xmlNode *n) +{ + return visit_one_node(n) && traverse_properties(n) && traverse(n->children); +} + +static void DivingLog_importer(void) +{ + import_source = DIVINGLOG; + + /* + * Diving Log units are really strange. + * + * Temperatures are in C, except in samples, + * when they are in Fahrenheit. Depths are in + * meters, an dpressure is in PSI in the samples, + * but in bar when it comes to working pressure. + * + * Crazy f*%^ morons. + */ + xml_parsing_units = SI_units; +} + +static void uddf_importer(void) +{ + import_source = UDDF; + xml_parsing_units = SI_units; + xml_parsing_units.pressure = PASCAL; + xml_parsing_units.temperature = KELVIN; +} + +static void subsurface_webservice(void) +{ + import_source = SSRF_WS; +} + +/* + * I'm sure this could be done as some fancy DTD rules. + * It's just not worth the headache. + */ +static struct nesting { + const char *name; + void (*start)(void), (*end)(void); +} nesting[] = { + { "divecomputerid", dc_settings_start, dc_settings_end }, + { "settings", settings_start, settings_end }, + { "site", dive_site_start, dive_site_end }, + { "dive", dive_start, dive_end }, + { "Dive", dive_start, dive_end }, + { "trip", trip_start, trip_end }, + { "sample", sample_start, sample_end }, + { "waypoint", sample_start, sample_end }, + { "SAMPLE", sample_start, sample_end }, + { "reading", sample_start, sample_end }, + { "event", event_start, event_end }, + { "mix", cylinder_start, cylinder_end }, + { "gasmix", cylinder_start, cylinder_end }, + { "cylinder", cylinder_start, cylinder_end }, + { "weightsystem", ws_start, ws_end }, + { "divecomputer", divecomputer_start, divecomputer_end }, + { "P", sample_start, sample_end }, + { "userid", userid_start, userid_stop}, + { "picture", picture_start, picture_end }, + { "extradata", extra_data_start, extra_data_end }, + + /* Import type recognition */ + { "Divinglog", DivingLog_importer }, + { "uddf", uddf_importer }, + { "output", subsurface_webservice }, + { NULL, } + }; + +static bool traverse(xmlNode *root) +{ + xmlNode *n; + bool ret = true; + + for (n = root; n; n = n->next) { + struct nesting *rule = nesting; + + if (!n->name) { + if ((ret = visit(n)) == false) + break; + continue; + } + + do { + if (!strcmp(rule->name, (const char *)n->name)) + break; + rule++; + } while (rule->name); + + if (rule->start) + rule->start(); + if ((ret = visit(n)) == false) + break; + if (rule->end) + rule->end(); + } + return ret; +} + +/* Per-file reset */ +static void reset_all(void) +{ + /* + * We reset the units for each file. You'd think it was + * a per-dive property, but I'm not going to trust people + * to do per-dive setup. If the xml does have per-dive + * data within one file, we might have to reset it per + * dive for that format. + */ + xml_parsing_units = SI_units; + import_source = UNKNOWN; +} + +/* divelog.de sends us xml files that claim to be iso-8859-1 + * but once we decode the HTML encoded characters they turn + * into UTF-8 instead. So skip the incorrect encoding + * declaration and decode the HTML encoded characters */ +const char *preprocess_divelog_de(const char *buffer) +{ + char *ret = strstr(buffer, ""); + + if (ret) { + xmlParserCtxtPtr ctx; + char buf[] = ""; + int i; + + for (i = 0; i < strlen(ret); ++i) + if (!isascii(ret[i])) + return buffer; + + ctx = xmlCreateMemoryParserCtxt(buf, sizeof(buf)); + ret = (char *)xmlStringLenDecodeEntities(ctx, (xmlChar *)ret, strlen(ret), XML_SUBSTITUTE_REF, 0, 0, 0); + + return ret; + } + return buffer; +} + +int parse_xml_buffer(const char *url, const char *buffer, int size, + struct dive_table *table, const char **params) +{ + xmlDoc *doc; + const char *res = preprocess_divelog_de(buffer); + int ret = 0; + + target_table = table; + doc = xmlReadMemory(res, strlen(res), url, NULL, 0); + if (res != buffer) + free((char *)res); + + if (!doc) + return report_error(translate("gettextFromC", "Failed to parse '%s'"), url); + + set_save_userid_local(false); + reset_all(); + dive_start(); + doc = test_xslt_transforms(doc, params); + if (!traverse(xmlDocGetRootElement(doc))) { + // we decided to give up on parsing... why? + ret = -1; + } + dive_end(); + xmlFreeDoc(doc); + return ret; +} + +void parse_mkvi_buffer(struct membuffer *txt, struct membuffer *csv, const char *starttime) +{ + dive_start(); + divedate(starttime, &cur_dive->when); + dive_end(); +} + +extern int dm4_events(void *handle, int columns, char **data, char **column) +{ + event_start(); + if (data[1]) + cur_event.time.seconds = atoi(data[1]); + + if (data[2]) { + switch (atoi(data[2])) { + case 1: + /* 1 Mandatory Safety Stop */ + strcpy(cur_event.name, "safety stop (mandatory)"); + break; + case 3: + /* 3 Deco */ + /* What is Subsurface's term for going to + * deco? */ + strcpy(cur_event.name, "deco"); + break; + case 4: + /* 4 Ascent warning */ + strcpy(cur_event.name, "ascent"); + break; + case 5: + /* 5 Ceiling broken */ + strcpy(cur_event.name, "violation"); + break; + case 6: + /* 6 Mandatory safety stop ceiling error */ + strcpy(cur_event.name, "violation"); + break; + case 7: + /* 7 Below deco floor */ + strcpy(cur_event.name, "below floor"); + break; + case 8: + /* 8 Dive time alarm */ + strcpy(cur_event.name, "divetime"); + break; + case 9: + /* 9 Depth alarm */ + strcpy(cur_event.name, "maxdepth"); + break; + case 10: + /* 10 OLF 80% */ + case 11: + /* 11 OLF 100% */ + strcpy(cur_event.name, "OLF"); + break; + case 12: + /* 12 High pO₂ */ + strcpy(cur_event.name, "PO2"); + break; + case 13: + /* 13 Air time */ + strcpy(cur_event.name, "airtime"); + break; + case 17: + /* 17 Ascent warning */ + strcpy(cur_event.name, "ascent"); + break; + case 18: + /* 18 Ceiling error */ + strcpy(cur_event.name, "ceiling"); + break; + case 19: + /* 19 Surfaced */ + strcpy(cur_event.name, "surface"); + break; + case 20: + /* 20 Deco */ + strcpy(cur_event.name, "deco"); + break; + case 22: + case 32: + /* 22 Mandatory safety stop violation */ + /* 32 Deep stop violation */ + strcpy(cur_event.name, "violation"); + break; + case 30: + /* Tissue level warning */ + strcpy(cur_event.name, "tissue warning"); + break; + case 37: + /* Tank pressure alarm */ + strcpy(cur_event.name, "tank pressure"); + break; + case 257: + /* 257 Dive active */ + /* This seems to be given after surface when + * descending again. */ + strcpy(cur_event.name, "surface"); + break; + case 258: + /* 258 Bookmark */ + if (data[3]) { + strcpy(cur_event.name, "heading"); + cur_event.value = atoi(data[3]); + } else { + strcpy(cur_event.name, "bookmark"); + } + break; + case 259: + /* Deep stop */ + strcpy(cur_event.name, "Deep stop"); + break; + case 260: + /* Deep stop */ + strcpy(cur_event.name, "Deep stop cleared"); + break; + case 266: + /* Mandatory safety stop activated */ + strcpy(cur_event.name, "safety stop (mandatory)"); + break; + case 267: + /* Mandatory safety stop deactivated */ + /* DM5 shows this only on event list, not on the + * profile so skipping as well for now */ + break; + default: + strcpy(cur_event.name, "unknown"); + cur_event.value = atoi(data[2]); + break; + } + } + event_end(); + + return 0; +} + +extern int dm5_cylinders(void *handle, int columns, char **data, char **column) +{ + cylinder_start(); + if (data[7] && atoi(data[7]) > 0 && atoi(data[7]) < 350000) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[7]); + if (data[8] && atoi(data[8]) > 0 && atoi(data[8]) < 350000) + cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[8])); + if (data[6]) { + /* DM5 shows tank size of 12 liters when the actual + * value is 0 (and using metric units). So we just use + * the same 12 liters when size is not available */ + if (atof(data[6]) == 0.0 && cur_dive->cylinder[cur_cylinder_index].start.mbar) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = 12000; + else + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = (atof(data[6])) * 1000; + } + if (data[2]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[2]) * 10; + if (data[3]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[3]) * 10; + cylinder_end(); + return 0; +} + +extern int dm5_gaschange(void *handle, int columns, char **data, char **column) +{ + event_start(); + if (data[0]) + cur_event.time.seconds = atoi(data[0]); + if (data[1]) { + strcpy(cur_event.name, "gaschange"); + cur_event.value = atof(data[1]); + } + event_end(); + + return 0; +} + +extern int dm4_tags(void *handle, int columns, char **data, char **column) +{ + if (data[0]) + taglist_add_tag(&cur_dive->tag_list, data[0]); + + return 0; +} + +extern int dm4_dive(void *param, int columns, char **data, char **column) +{ + int i, interval, retval = 0; + sqlite3 *handle = (sqlite3 *)param; + float *profileBlob; + unsigned char *tempBlob; + int *pressureBlob; + char *err = NULL; + char get_events_template[] = "select * from Mark where DiveId = %d"; + char get_tags_template[] = "select Text from DiveTag where DiveId = %d"; + char get_events[64]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + if (data[2]) + utf8_string(data[2], &cur_dive->notes); + + /* + * DM4 stores Duration and DiveTime. It looks like DiveTime is + * 10 to 60 seconds shorter than Duration. However, I have no + * idea what is the difference and which one should be used. + * Duration = data[3] + * DiveTime = data[15] + */ + if (data[3]) + cur_dive->duration.seconds = atoi(data[3]); + if (data[15]) + cur_dive->dc.duration.seconds = atoi(data[15]); + + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[4]) + utf8_string(data[4], &cur_settings.dc.serial_nr); + if (data[5]) + utf8_string(data[5], &cur_settings.dc.model); + + cur_settings.dc.deviceid = 0xffffffff; + dc_settings_end(); + settings_end(); + + if (data[6]) + cur_dive->dc.maxdepth.mm = atof(data[6]) * 1000; + if (data[8]) + cur_dive->dc.airtemp.mkelvin = C_to_mkelvin(atoi(data[8])); + if (data[9]) + cur_dive->dc.watertemp.mkelvin = C_to_mkelvin(atoi(data[9])); + + /* + * TODO: handle multiple cylinders + */ + cylinder_start(); + if (data[22] && atoi(data[22]) > 0) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[22]); + else if (data[10] && atoi(data[10]) > 0) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atoi(data[10]); + if (data[23] && atoi(data[23]) > 0) + cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[23])); + if (data[11] && atoi(data[11]) > 0) + cur_dive->cylinder[cur_cylinder_index].end.mbar = (atoi(data[11])); + if (data[12]) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = (atof(data[12])) * 1000; + if (data[13]) + cur_dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = (atoi(data[13])); + if (data[20]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[20]) * 10; + if (data[21]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[21]) * 10; + cylinder_end(); + + if (data[14]) + cur_dive->dc.surface_pressure.mbar = (atoi(data[14]) * 1000); + + interval = data[16] ? atoi(data[16]) : 0; + profileBlob = (float *)data[17]; + tempBlob = (unsigned char *)data[18]; + pressureBlob = (int *)data[19]; + for (i = 0; interval && i * interval < cur_dive->duration.seconds; i++) { + sample_start(); + cur_sample->time.seconds = i * interval; + if (profileBlob) + cur_sample->depth.mm = profileBlob[i] * 1000; + else + cur_sample->depth.mm = cur_dive->dc.maxdepth.mm; + + if (data[18] && data[18][0]) + cur_sample->temperature.mkelvin = C_to_mkelvin(tempBlob[i]); + if (data[19] && data[19][0]) + cur_sample->cylinderpressure.mbar = pressureBlob[i]; + sample_end(); + } + + snprintf(get_events, sizeof(get_events) - 1, get_events_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_events, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_events failed.\n"); + return 1; + } + + snprintf(get_events, sizeof(get_events) - 1, get_tags_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_tags, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_tags failed.\n"); + return 1; + } + + dive_end(); + + /* + for (i=0; inumber = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + if (data[2]) + utf8_string(data[2], &cur_dive->notes); + + if (data[3]) + cur_dive->duration.seconds = atoi(data[3]); + if (data[15]) + cur_dive->dc.duration.seconds = atoi(data[15]); + + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[4]) { + utf8_string(data[4], &cur_settings.dc.serial_nr); + cur_settings.dc.deviceid = atoi(data[4]); + } + if (data[5]) + utf8_string(data[5], &cur_settings.dc.model); + + dc_settings_end(); + settings_end(); + + if (data[6]) + cur_dive->dc.maxdepth.mm = atof(data[6]) * 1000; + if (data[8]) + cur_dive->dc.airtemp.mkelvin = C_to_mkelvin(atoi(data[8])); + if (data[9]) + cur_dive->dc.watertemp.mkelvin = C_to_mkelvin(atoi(data[9])); + + if (data[4]) { + cur_dive->dc.deviceid = atoi(data[4]); + } + if (data[5]) + utf8_string(data[5], &cur_dive->dc.model); + + snprintf(get_events, sizeof(get_events) - 1, get_cylinders_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm5_cylinders, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm5_cylinders failed.\n"); + return 1; + } + + if (data[14]) + cur_dive->dc.surface_pressure.mbar = (atoi(data[14]) / 100); + + interval = data[16] ? atoi(data[16]) : 0; + sampleBlob = (unsigned const char *)data[24]; + + if (sampleBlob) { + switch (sampleBlob[0]) { + case 2: + block_size = 19; + break; + case 3: + block_size = 23; + break; + default: + block_size = 16; + break; + } + } + + for (i = 0; interval && sampleBlob && i * interval < cur_dive->duration.seconds; i++) { + float *depth = (float *)&sampleBlob[i * block_size + 3]; + int32_t temp = (sampleBlob[i * block_size + 10] << 8) + sampleBlob[i * block_size + 11]; + int32_t pressure = (sampleBlob[i * block_size + 9] << 16) + (sampleBlob[i * block_size + 8] << 8) + sampleBlob[i * block_size + 7]; + + sample_start(); + cur_sample->time.seconds = i * interval; + cur_sample->depth.mm = depth[0] * 1000; + /* + * Limit temperatures and cylinder pressures to somewhat + * sensible values + */ + if (temp >= -10 && temp < 50) + cur_sample->temperature.mkelvin = C_to_mkelvin(temp); + if (pressure >= 0 && pressure < 350000) + cur_sample->cylinderpressure.mbar = pressure; + sample_end(); + } + + /* + * Log was converted from DM4, thus we need to parse the profile + * from DM4 format + */ + + if (i == 0) { + float *profileBlob; + unsigned char *tempBlob; + int *pressureBlob; + + profileBlob = (float *)data[17]; + tempBlob = (unsigned char *)data[18]; + pressureBlob = (int *)data[19]; + for (i = 0; interval && i * interval < cur_dive->duration.seconds; i++) { + sample_start(); + cur_sample->time.seconds = i * interval; + if (profileBlob) + cur_sample->depth.mm = profileBlob[i] * 1000; + else + cur_sample->depth.mm = cur_dive->dc.maxdepth.mm; + + if (data[18] && data[18][0]) + cur_sample->temperature.mkelvin = C_to_mkelvin(tempBlob[i]); + if (data[19] && data[19][0]) + cur_sample->cylinderpressure.mbar = pressureBlob[i]; + sample_end(); + } + } + + snprintf(get_events, sizeof(get_events) - 1, get_gaschange_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm5_gaschange, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm5_gaschange failed.\n"); + return 1; + } + + snprintf(get_events, sizeof(get_events) - 1, get_events_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_events, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_events failed.\n"); + return 1; + } + + snprintf(get_events, sizeof(get_events) - 1, get_tags_template, cur_dive->number); + retval = sqlite3_exec(handle, get_events, &dm4_tags, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query dm4_tags failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + + +int parse_dm4_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + int retval; + char *err = NULL; + target_table = table; + + /* StartTime is converted from Suunto's nano seconds to standard + * time. We also need epoch, not seconds since year 1. */ + char get_dives[] = "select D.DiveId,StartTime/10000000-62135596800,Note,Duration,SourceSerialNumber,Source,MaxDepth,SampleInterval,StartTemperature,BottomTemperature,D.StartPressure,D.EndPressure,Size,CylinderWorkPressure,SurfacePressure,DiveTime,SampleInterval,ProfileBlob,TemperatureBlob,PressureBlob,Oxygen,Helium,MIX.StartPressure,MIX.EndPressure FROM Dive AS D JOIN DiveMixture AS MIX ON D.DiveId=MIX.DiveId"; + + retval = sqlite3_exec(handle, get_dives, &dm4_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +int parse_dm5_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + int retval; + char *err = NULL; + target_table = table; + + /* StartTime is converted from Suunto's nano seconds to standard + * time. We also need epoch, not seconds since year 1. */ + char get_dives[] = "select DiveId,StartTime/10000000-62135596800,Note,Duration,coalesce(SourceSerialNumber,SerialNumber),Source,MaxDepth,SampleInterval,StartTemperature,BottomTemperature,StartPressure,EndPressure,'','',SurfacePressure,DiveTime,SampleInterval,ProfileBlob,TemperatureBlob,PressureBlob,'','','','',SampleBlob FROM Dive where Deleted is null"; + + retval = sqlite3_exec(handle, get_dives, &dm5_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +extern int shearwater_cylinders(void *handle, int columns, char **data, char **column) +{ + cylinder_start(); + if (data[0]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atof(data[0]) * 1000; + if (data[1]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atof(data[1]) * 1000; + cylinder_end(); + + return 0; +} + +extern int shearwater_changes(void *handle, int columns, char **data, char **column) +{ + event_start(); + if (data[0]) + cur_event.time.seconds = atoi(data[0]); + if (data[1]) { + strcpy(cur_event.name, "gaschange"); + cur_event.value = atof(data[1]) * 100; + } + event_end(); + + return 0; +} + + +extern int cobalt_profile_sample(void *handle, int columns, char **data, char **column) +{ + sample_start(); + if (data[0]) + cur_sample->time.seconds = atoi(data[0]); + if (data[1]) + cur_sample->depth.mm = atoi(data[1]); + if (data[2]) + cur_sample->temperature.mkelvin = metric ? C_to_mkelvin(atof(data[2])) : F_to_mkelvin(atof(data[2])); + sample_end(); + + return 0; +} + + +extern int shearwater_profile_sample(void *handle, int columns, char **data, char **column) +{ + sample_start(); + if (data[0]) + cur_sample->time.seconds = atoi(data[0]); + if (data[1]) + cur_sample->depth.mm = metric ? atof(data[1]) * 1000 : feet_to_mm(atof(data[1])); + if (data[2]) + cur_sample->temperature.mkelvin = metric ? C_to_mkelvin(atof(data[2])) : F_to_mkelvin(atof(data[2])); + if (data[3]) { + cur_sample->setpoint.mbar = atof(data[3]) * 1000; + cur_dive->dc.divemode = CCR; + } + if (data[4]) + cur_sample->ndl.seconds = atoi(data[4]) * 60; + if (data[5]) + cur_sample->cns = atoi(data[5]); + if (data[6]) + cur_sample->stopdepth.mm = metric ? atoi(data[6]) * 1000 : feet_to_mm(atoi(data[6])); + + /* We don't actually have data[3], but it should appear in the + * SQL query at some point. + if (data[3]) + cur_sample->cylinderpressure.mbar = metric ? atoi(data[3]) * 1000 : psi_to_mbar(atoi(data[3])); + */ + sample_end(); + + return 0; +} + +extern int shearwater_dive(void *param, int columns, char **data, char **column) +{ + int retval = 0; + sqlite3 *handle = (sqlite3 *)param; + char *err = NULL; + char get_profile_template[] = "select currentTime,currentDepth,waterTemp,averagePPO2,currentNdl,CNSPercent,decoCeiling from dive_log_records where diveLogId = %d"; + char get_cylinder_template[] = "select fractionO2,fractionHe from dive_log_records where diveLogId = %d group by fractionO2,fractionHe"; + char get_changes_template[] = "select a.currentTime,a.fractionO2,a.fractionHe from dive_log_records as a,dive_log_records as b where a.diveLogId = %d and b.diveLogId = %d and (a.id - 1) = b.id and (a.fractionO2 != b.fractionO2 or a.fractionHe != b.fractionHe) union select min(currentTime),fractionO2,fractionHe from dive_log_records"; + char get_buffer[1024]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + + if (data[2]) + add_dive_site(data[2], cur_dive); + if (data[3]) + utf8_string(data[3], &cur_dive->buddy); + if (data[4]) + utf8_string(data[4], &cur_dive->notes); + + metric = atoi(data[5]) == 1 ? 0 : 1; + + /* TODO: verify that metric calculation is correct */ + if (data[6]) + cur_dive->dc.maxdepth.mm = metric ? atof(data[6]) * 1000 : feet_to_mm(atof(data[6])); + + if (data[7]) + cur_dive->dc.duration.seconds = atoi(data[7]) * 60; + + if (data[8]) + cur_dive->dc.surface_pressure.mbar = atoi(data[8]); + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[9]) + utf8_string(data[9], &cur_settings.dc.serial_nr); + if (data[10]) + utf8_string(data[10], &cur_settings.dc.model); + + cur_settings.dc.deviceid = 0xffffffff; + dc_settings_end(); + settings_end(); + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &shearwater_cylinders, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query shearwater_cylinders failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_changes_template, cur_dive->number, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &shearwater_changes, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query shearwater_changes failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &shearwater_profile_sample, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query shearwater_profile_sample failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + +extern int cobalt_cylinders(void *handle, int columns, char **data, char **column) +{ + cylinder_start(); + if (data[0]) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atoi(data[0]) * 10; + if (data[1]) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atoi(data[1]) * 10; + if (data[2]) + cur_dive->cylinder[cur_cylinder_index].start.mbar = psi_to_mbar(atoi(data[2])); + if (data[3]) + cur_dive->cylinder[cur_cylinder_index].end.mbar = psi_to_mbar(atoi(data[3])); + if (data[4]) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = atoi(data[4]) * 100; + if (data[5]) + cur_dive->cylinder[cur_cylinder_index].gas_used.mliter = atoi(data[5]) * 1000; + cylinder_end(); + + return 0; +} + +extern int cobalt_buddies(void *handle, int columns, char **data, char **column) +{ + if (data[0]) + utf8_string(data[0], &cur_dive->buddy); + + return 0; +} + +/* + * We still need to figure out how to map free text visibility to + * Subsurface star rating. + */ + +extern int cobalt_visibility(void *handle, int columns, char **data, char **column) +{ + return 0; +} + +extern int cobalt_location(void *handle, int columns, char **data, char **column) +{ + static char *location = NULL; + if (data[0]) { + if (location) { + char *tmp = malloc(strlen(location) + strlen(data[0]) + 4); + if (!tmp) + return -1; + sprintf(tmp, "%s / %s", location, data[0]); + free(location); + location = NULL; + cur_dive->dive_site_uuid = find_or_create_dive_site_with_name(tmp, cur_dive->when); + free(tmp); + } else { + location = strdup(data[0]); + } + } + return 0; +} + + +extern int cobalt_dive(void *param, int columns, char **data, char **column) +{ + int retval = 0; + sqlite3 *handle = (sqlite3 *)param; + char *err = NULL; + char get_profile_template[] = "select runtime*60,(DepthPressure*10000/SurfacePressure)-10000,p.Temperature from Dive AS d JOIN TrackPoints AS p ON d.Id=p.DiveId where d.Id=%d"; + char get_cylinder_template[] = "select FO2,FHe,StartingPressure,EndingPressure,TankSize,TankPressure,TotalConsumption from GasMixes where DiveID=%d and StartingPressure>0 group by FO2,FHe"; + char get_buddy_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=4"; + char get_visibility_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=3"; + char get_location_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=0"; + char get_site_template[] = "select l.Data from Items AS i, List AS l ON i.Value1=l.Id where i.DiveId=%d and l.Type=1"; + char get_buffer[1024]; + + dive_start(); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + + if (data[4]) + utf8_string(data[4], &cur_dive->notes); + + /* data[5] should have information on Units used, but I cannot + * parse it at all based on the sample log I have received. The + * temperatures in the samples are all Imperial, so let's go by + * that. + */ + + metric = 0; + + /* Cobalt stores the pressures, not the depth */ + if (data[6]) + cur_dive->dc.maxdepth.mm = atoi(data[6]); + + if (data[7]) + cur_dive->dc.duration.seconds = atoi(data[7]); + + if (data[8]) + cur_dive->dc.surface_pressure.mbar = atoi(data[8]); + /* + * TODO: the deviceid hash should be calculated here. + */ + settings_start(); + dc_settings_start(); + if (data[9]) { + utf8_string(data[9], &cur_settings.dc.serial_nr); + cur_settings.dc.deviceid = atoi(data[9]); + cur_settings.dc.model = strdup("Cobalt import"); + } + + dc_settings_end(); + settings_end(); + + if (data[9]) { + cur_dive->dc.deviceid = atoi(data[9]); + cur_dive->dc.model = strdup("Cobalt import"); + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_cylinders, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_cylinders failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_buddy_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_buddies, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_buddies failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_visibility_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_visibility, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_visibility failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_location_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_location, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_location failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_site_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_location, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_location (site) failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, cur_dive->number); + retval = sqlite3_exec(handle, get_buffer, &cobalt_profile_sample, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query cobalt_profile_sample failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + + +int parse_shearwater_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + int retval; + char *err = NULL; + target_table = table; + + char get_dives[] = "select i.diveId,timestamp,location||' / '||site,buddy,notes,imperialUnits,maxDepth,maxTime,startSurfacePressure,computerSerial,computerModel FROM dive_info AS i JOIN dive_logs AS l ON i.diveId=l.diveId"; + + retval = sqlite3_exec(handle, get_dives, &shearwater_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +int parse_cobalt_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + int retval; + char *err = NULL; + target_table = table; + + char get_dives[] = "select Id,strftime('%s',DiveStartTime),LocationId,'buddy','notes',Units,(MaxDepthPressure*10000/SurfacePressure)-10000,DiveMinutes,SurfacePressure,SerialNumber,'model' from Dive where IsViewDeleted = 0"; + + retval = sqlite3_exec(handle, get_dives, &cobalt_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +extern int divinglog_cylinder(void *handle, int columns, char **data, char **column) +{ + short dbl = 1; + //char get_cylinder_template[] = "select TankID,TankSize,PresS,PresE,PresW,O2,He,DblTank from Tank where LogID = %d"; + + /* + * Divinglog might have more cylinders than what we support. So + * better to ignore those. + */ + + if (cur_cylinder_index >= MAX_CYLINDERS) + return 0; + + if (data[7] && atoi(data[7]) > 0) + dbl = 2; + + cylinder_start(); + + /* + * Assuming that we have to double the cylinder size, if double + * is set + */ + + if (data[1] && atoi(data[1]) > 0) + cur_dive->cylinder[cur_cylinder_index].type.size.mliter = atol(data[1]) * 1000 * dbl; + + if (data[2] && atoi(data[2]) > 0) + cur_dive->cylinder[cur_cylinder_index].start.mbar = atol(data[2]) * 1000; + if (data[3] && atoi(data[3]) > 0) + cur_dive->cylinder[cur_cylinder_index].end.mbar = atol(data[3]) * 1000; + if (data[4] && atoi(data[4]) > 0) + cur_dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = atol(data[4]) * 1000; + if (data[5] && atoi(data[5]) > 0) + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = atol(data[5]) * 10; + if (data[6] && atoi(data[6]) > 0) + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = atol(data[6]) * 10; + + cylinder_end(); + + return 0; +} + +extern int divinglog_profile(void *handle, int columns, char **data, char **column) +{ + int sinterval = 0; + unsigned long i, len, lenprofile2 = 0; + char *ptr, temp[4], pres[5], hbeat[4], stop[4], stime[4], ndl[4], ppo2_1[4], ppo2_2[4], ppo2_3[4], cns[5], setpoint[3]; + short oldcyl = -1; + + /* We do not have samples */ + if (!data[1]) + return 0; + + if (data[0]) + sinterval = atoi(data[0]); + + /* + * Profile + * + * DDDDDCRASWEE + * D: Depth (in meter with two decimals) + * C: Deco (1 = yes, 0 = no) + * R: RBT (Remaining Bottom Time warning) + * A: Ascent warning + * S: Decostop ignored + * W: Work warning + * E: Extra info (different for every computer) + * + * Example: 004500010000 + * 4.5 m, no deco, no RBT warning, ascanding too fast, no decostop ignored, no work, no extra info + * + * + * Profile2 + * + * TTTFFFFIRRR + * + * T: Temperature (in °C with one decimal) + * F: Tank pressure 1 (in bar with one decimal) + * I: Tank ID (0, 1, 2 ... 9) + * R: RBT (in min) + * + * Example: 25518051099 + * 25.5 °C, 180.5 bar, Tank 1, 99 min RBT + * + */ + + len = strlen(data[1]); + + if (data[2]) + lenprofile2 = strlen(data[2]); + + for (i = 0, ptr = data[1]; i * 12 < len; ++i) { + sample_start(); + + cur_sample->time.seconds = sinterval * i; + cur_sample->in_deco = ptr[5] - '0' ? true : false; + ptr[5] = 0; + cur_sample->depth.mm = atoi(ptr) * 10; + + if (i * 11 < lenprofile2) { + memcpy(temp, &data[2][i * 11], 3); + cur_sample->temperature.mkelvin = C_to_mkelvin(atoi(temp) / 10); + } + + if (data[2]) { + memcpy(pres, &data[2][i * 11 + 3], 4); + cur_sample->cylinderpressure.mbar = atoi(pres) * 100; + } + + if (data[3] && strlen(data[3])) { + memcpy(hbeat, &data[3][i * 14 + 8], 3); + cur_sample->heartbeat = atoi(hbeat); + } + + if (data[4] && strlen(data[4])) { + memcpy(stop, &data[4][i * 9 + 6], 3); + cur_sample->stopdepth.mm = atoi(stop) * 1000; + + memcpy(stime, &data[4][i * 9 + 3], 3); + cur_sample->stoptime.seconds = atoi(stime) * 60; + + /* + * Following value is NDL when not in deco, and + * either 0 or TTS when in deco. + */ + + memcpy(ndl, &data[4][i * 9 + 0], 3); + if (cur_sample->in_deco == false) + cur_sample->ndl.seconds = atoi(ndl) * 60; + else if (atoi(ndl)) + cur_sample->tts.seconds = atoi(ndl) * 60; + + if (cur_sample->in_deco == true) + cur_sample->ndl.seconds = 0; + } + + /* + * AAABBBCCCOOOONNNNSS + * + * A = ppO2 cell 1 (measured) + * B = ppO2 cell 2 (measured) + * C = ppO2 cell 3 (measured) + * O = OTU + * N = CNS + * S = Setpoint + * + * Example: 1121131141548026411 + * 1.12 bar, 1.13 bar, 1.14 bar, OTU = 154.8, CNS = 26.4, Setpoint = 1.1 + */ + + if (data[5] && strlen(data[5])) { + memcpy(ppo2_1, &data[5][i * 19 + 0], 3); + memcpy(ppo2_2, &data[5][i * 19 + 3], 3); + memcpy(ppo2_3, &data[5][i * 19 + 6], 3); + memcpy(cns, &data[5][i * 19 + 13], 4); + memcpy(setpoint, &data[5][i * 19 + 17], 2); + + if (atoi(ppo2_1) > 0) + cur_sample->o2sensor[0].mbar = atoi(ppo2_1) * 100; + if (atoi(ppo2_2) > 0) + cur_sample->o2sensor[1].mbar = atoi(ppo2_2) * 100; + if (atoi(ppo2_3) > 0) + cur_sample->o2sensor[2].mbar = atoi(ppo2_3) * 100; + if (atoi(cns) > 0) + cur_sample->cns = rint(atoi(cns) / 10); + if (atoi(setpoint) > 0) + cur_sample->setpoint.mbar = atoi(setpoint) * 100; + + } + + /* + * My best guess is that if we have o2sensors, then it + * is either CCR or PSCR dive. And the first time we + * have O2 sensor readings, we can count them to get + * the amount O2 sensors. + */ + + if (!cur_dive->dc.no_o2sensors) { + cur_dive->dc.no_o2sensors = cur_sample->o2sensor[0].mbar ? 1 : 0 + + cur_sample->o2sensor[1].mbar ? 1 : 0 + + cur_sample->o2sensor[2].mbar ? 1 : 0; + cur_dive->dc.divemode = CCR; + } + + ptr += 12; + sample_end(); + } + + for (i = 0, ptr = data[1]; i * 12 < len; ++i) { + /* Remaining bottom time warning */ + if (ptr[6] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "rbt"); + event_end(); + } + + /* Ascent warning */ + if (ptr[7] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "ascent"); + event_end(); + } + + /* Deco stop ignored */ + if (ptr[8] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "violation"); + event_end(); + } + + /* Workload warning */ + if (ptr[9] - '0') { + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "workload"); + event_end(); + } + ptr += 12; + } + + for (i = 0; i * 11 < lenprofile2; ++i) { + short tank = data[2][i * 11 + 7] - '0'; + if (oldcyl != tank) { + struct gasmix *mix = &cur_dive->cylinder[tank].gasmix; + int o2 = get_o2(mix); + int he = get_he(mix); + + event_start(); + cur_event.time.seconds = sinterval * i; + strcpy(cur_event.name, "gaschange"); + + o2 = (o2 + 5) / 10; + he = (he + 5) / 10; + cur_event.value = o2 + (he << 16); + + event_end(); + oldcyl = tank; + } + } + + return 0; +} + + +extern int divinglog_dive(void *param, int columns, char **data, char **column) +{ + int retval = 0; + sqlite3 *handle = (sqlite3 *)param; + char *err = NULL; + char get_profile_template[] = "select ProfileInt,Profile,Profile2,Profile3,Profile4,Profile5 from Logbook where ID = %d"; + char get_cylinder0_template[] = "select 0,TankSize,PresS,PresE,PresW,O2,He,DblTank from Logbook where ID = %d"; + char get_cylinder_template[] = "select TankID,TankSize,PresS,PresE,PresW,O2,He,DblTank from Tank where LogID = %d order by TankID"; + char get_buffer[1024]; + + dive_start(); + diveid = atoi(data[13]); + cur_dive->number = atoi(data[0]); + + cur_dive->when = (time_t)(atol(data[1])); + + if (data[2]) + cur_dive->dive_site_uuid = find_or_create_dive_site_with_name(data[2], cur_dive->when); + + if (data[3]) + utf8_string(data[3], &cur_dive->buddy); + + if (data[4]) + utf8_string(data[4], &cur_dive->notes); + + if (data[5]) + cur_dive->dc.maxdepth.mm = atof(data[5]) * 1000; + + if (data[6]) + cur_dive->dc.duration.seconds = atoi(data[6]) * 60; + + if (data[7]) + utf8_string(data[7], &cur_dive->divemaster); + + if (data[8]) + cur_dive->airtemp.mkelvin = C_to_mkelvin(atol(data[8])); + + if (data[9]) + cur_dive->watertemp.mkelvin = C_to_mkelvin(atol(data[9])); + + if (data[10]) { + cur_dive->weightsystem[0].weight.grams = atol(data[10]) * 1000; + cur_dive->weightsystem[0].description = strdup(translate("gettextFromC", "unknown")); + } + + if (data[11]) + cur_dive->suit = strdup(data[11]); + + settings_start(); + dc_settings_start(); + + if (data[12]) { + cur_dive->dc.model = strdup(data[12]); + } else { + cur_settings.dc.model = strdup("Divinglog import"); + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder0_template, diveid); + retval = sqlite3_exec(handle, get_buffer, &divinglog_cylinder, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query divinglog_cylinder0 failed.\n"); + return 1; + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_cylinder_template, diveid); + retval = sqlite3_exec(handle, get_buffer, &divinglog_cylinder, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query divinglog_cylinder failed.\n"); + return 1; + } + + + dc_settings_end(); + settings_end(); + + if (data[12]) { + cur_dive->dc.model = strdup(data[12]); + } else { + cur_dive->dc.model = strdup("Divinglog import"); + } + + snprintf(get_buffer, sizeof(get_buffer) - 1, get_profile_template, diveid); + retval = sqlite3_exec(handle, get_buffer, &divinglog_profile, 0, &err); + if (retval != SQLITE_OK) { + fprintf(stderr, "%s", "Database query divinglog_profile failed.\n"); + return 1; + } + + dive_end(); + + return SQLITE_OK; +} + + +int parse_divinglog_buffer(sqlite3 *handle, const char *url, const char *buffer, int size, + struct dive_table *table) +{ + int retval; + char *err = NULL; + target_table = table; + + char get_dives[] = "select Number,strftime('%s',Divedate || ' ' || ifnull(Entrytime,'00:00')),Country || ' - ' || City || ' - ' || Place,Buddy,Comments,Depth,Divetime,Divemaster,Airtemp,Watertemp,Weight,Divesuit,Computer,ID from Logbook where UUID not in (select UUID from DeletedRecords)"; + + retval = sqlite3_exec(handle, get_dives, &divinglog_dive, handle, &err); + + if (retval != SQLITE_OK) { + fprintf(stderr, "Database query failed '%s'.\n", url); + return 1; + } + + return 0; +} + +/* + * Parse a unsigned 32-bit integer in little-endian mode, + * that is seconds since Jan 1, 2000. + */ +static timestamp_t parse_dlf_timestamp(unsigned char *buffer) +{ + timestamp_t offset; + + offset = buffer[3]; + offset = (offset << 8) + buffer[2]; + offset = (offset << 8) + buffer[1]; + offset = (offset << 8) + buffer[0]; + + // Jan 1, 2000 is 946684800 seconds after Jan 1, 1970, which is + // the Unix epoch date that "timestamp_t" uses. + return offset + 946684800; +} + +int parse_dlf_buffer(unsigned char *buffer, size_t size) +{ + unsigned char *ptr = buffer; + unsigned char event; + bool found; + unsigned int time = 0; + int i; + char serial[6]; + + target_table = &dive_table; + + // Check for the correct file magic + if (ptr[0] != 'D' || ptr[1] != 'i' || ptr[2] != 'v' || ptr[3] != 'E') + return -1; + + dive_start(); + divecomputer_start(); + + cur_dc->model = strdup("DLF import"); + // (ptr[7] << 8) + ptr[6] Is "Serial" + snprintf(serial, sizeof(serial), "%d", (ptr[7] << 8) + ptr[6]); + cur_dc->serial = strdup(serial); + cur_dc->when = parse_dlf_timestamp(ptr + 8); + cur_dive->when = cur_dc->when; + + cur_dc->duration.seconds = ((ptr[14] & 0xFE) << 16) + (ptr[13] << 8) + ptr[12]; + + // ptr[14] >> 1 is scrubber used in % + + // 3 bit dive type + switch((ptr[15] & 0x30) >> 3) { + case 0: // unknown + case 1: + cur_dc->divemode = OC; + break; + case 2: + cur_dc->divemode = CCR; + break; + case 3: + cur_dc->divemode = CCR; // mCCR + break; + case 4: + cur_dc->divemode = FREEDIVE; + break; + case 5: + cur_dc->divemode = OC; // Gauge + break; + case 6: + cur_dc->divemode = PSCR; // ASCR + break; + case 7: + cur_dc->divemode = PSCR; + break; + } + + cur_dc->maxdepth.mm = ((ptr[21] << 8) + ptr[20]) * 10; + cur_dc->surface_pressure.mbar = ((ptr[25] << 8) + ptr[24]) / 10; + + /* Done with parsing what we know about the dive header */ + ptr += 32; + + // We're going to interpret ppO2 saved as a sensor value in these modes. + if (cur_dc->divemode == CCR || cur_dc->divemode == PSCR) + cur_dc->no_o2sensors = 1; + + while (ptr < buffer + size) { + time = ((ptr[0] >> 4) & 0x0f) + + ((ptr[1] << 4) & 0xff0) + + (ptr[2] & 0x0f) * 3600; /* hours */ + event = ptr[0] & 0x0f; + switch (event) { + case 0: + /* Regular sample */ + sample_start(); + cur_sample->time.seconds = time; + cur_sample->depth.mm = ((ptr[5] << 8) + ptr[4]) * 10; + // Crazy precision on these stored values... + // Only store value if we're in CCR/PSCR mode, + // because we rather calculate ppo2 our selfs. + if (cur_dc->divemode == CCR || cur_dc->divemode == PSCR) + cur_sample->o2sensor[0].mbar = ((ptr[7] << 8) + ptr[6]) / 10; + // NDL in minutes, 10 bit + cur_sample->ndl.seconds = (((ptr[9] & 0x03) << 8) + ptr[8]) * 60; + // TTS in minutes, 10 bit + cur_sample->tts.seconds = (((ptr[10] & 0x0F) << 6) + (ptr[9] >> 2)) * 60; + // Temperature in 1/10 C, 10 bit signed + cur_sample->temperature.mkelvin = ((ptr[11] & 0x20) ? -1 : 1) * (((ptr[11] & 0x1F) << 4) + (ptr[10] >> 4)) * 100 + ZERO_C_IN_MKELVIN; + // ptr[11] & 0xF0 is unknown, and always 0xC in all checked files + cur_sample->stopdepth.mm = ((ptr[13] << 8) + ptr[12]) * 10; + if (cur_sample->stopdepth.mm) + cur_sample->in_deco = true; + //ptr[14] is helium content, always zero? + //ptr[15] is setpoint, always zero? + sample_end(); + break; + case 1: /* dive event */ + case 2: /* automatic parameter change */ + case 3: /* diver error */ + case 4: /* internal error */ + case 5: /* device activity log */ + event_start(); + cur_event.time.seconds = time; + switch (ptr[4]) { + case 1: + strcpy(cur_event.name, "Setpoint Manual"); + // There is a setpoint value somewhere... + break; + case 2: + strcpy(cur_event.name, "Setpoint Auto"); + // There is a setpoint value somewhere... + switch (ptr[7]) { + case 0: + strcat(cur_event.name, " Manual"); + break; + case 1: + strcat(cur_event.name, " Auto Start"); + break; + case 2: + strcat(cur_event.name, " Auto Hypox"); + break; + case 3: + strcat(cur_event.name, " Auto Timeout"); + break; + case 4: + strcat(cur_event.name, " Auto Ascent"); + break; + case 5: + strcat(cur_event.name, " Auto Stall"); + break; + case 6: + strcat(cur_event.name, " Auto SP Low"); + break; + default: + break; + } + break; + case 3: + // obsolete + strcpy(cur_event.name, "OC"); + break; + case 4: + // obsolete + strcpy(cur_event.name, "CCR"); + break; + case 5: + strcpy(cur_event.name, "gaschange"); + cur_event.type = SAMPLE_EVENT_GASCHANGE2; + cur_event.value = ptr[7] << 8 ^ ptr[6]; + + found = false; + for (i = 0; i < cur_cylinder_index; ++i) { + if (cur_dive->cylinder[i].gasmix.o2.permille == ptr[6] * 10 && cur_dive->cylinder[i].gasmix.he.permille == ptr[7] * 10) { + found = true; + break; + } + } + if (!found) { + cylinder_start(); + cur_dive->cylinder[cur_cylinder_index].gasmix.o2.permille = ptr[6] * 10; + cur_dive->cylinder[cur_cylinder_index].gasmix.he.permille = ptr[7] * 10; + cylinder_end(); + cur_event.gas.index = cur_cylinder_index; + } else { + cur_event.gas.index = i; + } + break; + case 6: + strcpy(cur_event.name, "Start"); + break; + case 7: + strcpy(cur_event.name, "Too Fast"); + break; + case 8: + strcpy(cur_event.name, "Above Ceiling"); + break; + case 9: + strcpy(cur_event.name, "Toxic"); + break; + case 10: + strcpy(cur_event.name, "Hypox"); + break; + case 11: + strcpy(cur_event.name, "Critical"); + break; + case 12: + strcpy(cur_event.name, "Sensor Disabled"); + break; + case 13: + strcpy(cur_event.name, "Sensor Enabled"); + break; + case 14: + strcpy(cur_event.name, "O2 Backup"); + break; + case 15: + strcpy(cur_event.name, "Peer Down"); + break; + case 16: + strcpy(cur_event.name, "HS Down"); + break; + case 17: + strcpy(cur_event.name, "Inconsistent"); + break; + case 18: + // key pressed - probably not + // interesting to view on profile + break; + case 19: + // obsolete + strcpy(cur_event.name, "SCR"); + break; + case 20: + strcpy(cur_event.name, "Above Stop"); + break; + case 21: + strcpy(cur_event.name, "Safety Miss"); + break; + case 22: + strcpy(cur_event.name, "Fatal"); + break; + case 23: + strcpy(cur_event.name, "Diluent"); + break; + case 24: + strcpy(cur_event.name, "gaschange"); + cur_event.type = SAMPLE_EVENT_GASCHANGE2; + cur_event.value = ptr[7] << 8 ^ ptr[6]; + event_end(); + // This is both a mode change and a gas change event + // so we encode it as two separate events. + event_start(); + strcpy(cur_event.name, "Change Mode"); + switch (ptr[8]) { + case 1: + strcat(cur_event.name, ": OC"); + break; + case 2: + strcat(cur_event.name, ": CCR"); + break; + case 3: + strcat(cur_event.name, ": mCCR"); + break; + case 4: + strcat(cur_event.name, ": Free"); + break; + case 5: + strcat(cur_event.name, ": Gauge"); + break; + case 6: + strcat(cur_event.name, ": ASCR"); + break; + case 7: + strcat(cur_event.name, ": PSCR"); + break; + default: + break; + } + event_end(); + break; + case 25: + strcpy(cur_event.name, "CCR O2 solenoid opened/closed"); + break; + case 26: + strcpy(cur_event.name, "User mark"); + break; + case 27: + snprintf(cur_event.name, MAX_EVENT_NAME, "%sGF Switch (%d/%d)", ptr[6] ? "Bailout, ": "", ptr[7], ptr[8]); + break; + case 28: + strcpy(cur_event.name, "Peer Up"); + break; + case 29: + strcpy(cur_event.name, "HS Up"); + break; + case 30: + snprintf(cur_event.name, MAX_EVENT_NAME, "CNS %d%%", ptr[6]); + break; + default: + // No values above 30 had any description + break; + } + event_end(); + break; + case 6: + /* device configuration */ + break; + case 7: + /* measure record */ + /* Po2 sample? Solenoid inject? */ + //fprintf(stderr, "%02X %02X%02X %02X%02X\n", ptr[5], ptr[6], ptr[7], ptr[8], ptr[9]); + break; + default: + /* Unknown... */ + break; + } + ptr += 16; + } + divecomputer_end(); + dive_end(); + return 0; +} + + +void parse_xml_init(void) +{ + LIBXML_TEST_VERSION +} + +void parse_xml_exit(void) +{ + xmlCleanupParser(); +} + +static struct xslt_files { + const char *root; + const char *file; + const char *attribute; +} xslt_files[] = { + { "SUUNTO", "SuuntoSDM.xslt", NULL }, + { "Dive", "SuuntoDM4.xslt", "xmlns" }, + { "Dive", "shearwater.xslt", "version" }, + { "JDiveLog", "jdivelog2subsurface.xslt", NULL }, + { "dives", "MacDive.xslt", NULL }, + { "DIVELOGSDATA", "divelogs.xslt", NULL }, + { "uddf", "uddf.xslt", NULL }, + { "UDDF", "uddf.xslt", NULL }, + { "profile", "udcf.xslt", NULL }, + { "Divinglog", "DivingLog.xslt", NULL }, + { "csv", "csv2xml.xslt", NULL }, + { "sensuscsv", "sensuscsv.xslt", NULL }, + { "SubsurfaceCSV", "subsurfacecsv.xslt", NULL }, + { "manualcsv", "manualcsv2xml.xslt", NULL }, + { "logbook", "DiveLog.xslt", NULL }, + { NULL, } + }; + +static xmlDoc *test_xslt_transforms(xmlDoc *doc, const char **params) +{ + struct xslt_files *info = xslt_files; + xmlDoc *transformed; + xsltStylesheetPtr xslt = NULL; + xmlNode *root_element = xmlDocGetRootElement(doc); + char *attribute; + + while (info->root) { + if ((strcasecmp((const char *)root_element->name, info->root) == 0)) { + if (info->attribute == NULL) + break; + else if (xmlGetProp(root_element, (const xmlChar *)info->attribute) != NULL) + break; + } + info++; + } + + if (info->root) { + attribute = (char *)xmlGetProp(xmlFirstElementChild(root_element), (const xmlChar *)"name"); + if (attribute) { + if (strcasecmp(attribute, "subsurface") == 0) { + free((void *)attribute); + return doc; + } + free((void *)attribute); + } + xmlSubstituteEntitiesDefault(1); + xslt = get_stylesheet(info->file); + if (xslt == NULL) { + report_error(translate("gettextFromC", "Can't open stylesheet %s"), info->file); + return doc; + } + transformed = xsltApplyStylesheet(xslt, doc, params); + xmlFreeDoc(doc); + xsltFreeStylesheet(xslt); + + return transformed; + } + return doc; +} diff --git a/subsurface-core/planner.c b/subsurface-core/planner.c new file mode 100644 index 000000000..22d37165a --- /dev/null +++ b/subsurface-core/planner.c @@ -0,0 +1,1455 @@ +/* planner.c + * + * code that allows us to plan future dives + * + * (c) Dirk Hohndel 2013 + */ +#include +#include +#include +#include +#include "dive.h" +#include "divelist.h" +#include "planner.h" +#include "gettext.h" +#include "libdivecomputer/parser.h" + +#define TIMESTEP 2 /* second */ +#define DECOTIMESTEP 60 /* seconds. Unit of deco stop times */ + +int decostoplevels_metric[] = { 0, 3000, 6000, 9000, 12000, 15000, 18000, 21000, 24000, 27000, + 30000, 33000, 36000, 39000, 42000, 45000, 48000, 51000, 54000, 57000, + 60000, 63000, 66000, 69000, 72000, 75000, 78000, 81000, 84000, 87000, + 90000, 100000, 110000, 120000, 130000, 140000, 150000, 160000, 170000, + 180000, 190000, 200000, 220000, 240000, 260000, 280000, 300000, + 320000, 340000, 360000, 380000 }; +int decostoplevels_imperial[] = { 0, 3048, 6096, 9144, 12192, 15240, 18288, 21336, 24384, 27432, + 30480, 33528, 36576, 39624, 42672, 45720, 48768, 51816, 54864, 57912, + 60960, 64008, 67056, 70104, 73152, 76200, 79248, 82296, 85344, 88392, + 91440, 101600, 111760, 121920, 132080, 142240, 152400, 162560, 172720, + 182880, 193040, 203200, 223520, 243840, 264160, 284480, 304800, + 325120, 345440, 365760, 386080 }; + +double plangflow, plangfhigh; +bool plan_verbatim, plan_display_runtime, plan_display_duration, plan_display_transitions; + +pressure_t first_ceiling_pressure, max_bottom_ceiling_pressure = {}; + +const char *disclaimer; + +#if DEBUG_PLAN +void dump_plan(struct diveplan *diveplan) +{ + struct divedatapoint *dp; + struct tm tm; + + if (!diveplan) { + printf("Diveplan NULL\n"); + return; + } + utc_mkdate(diveplan->when, &tm); + + printf("\nDiveplan @ %04d-%02d-%02d %02d:%02d:%02d (surfpres %dmbar):\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + diveplan->surface_pressure); + dp = diveplan->dp; + while (dp) { + printf("\t%3u:%02u: %dmm gas: %d o2 %d h2\n", FRACTION(dp->time, 60), dp->depth, get_o2(&dp->gasmix), get_he(&dp->gasmix)); + dp = dp->next; + } +} +#endif + +bool diveplan_empty(struct diveplan *diveplan) +{ + struct divedatapoint *dp; + if (!diveplan || !diveplan->dp) + return true; + dp = diveplan->dp; + while (dp) { + if (dp->time) + return false; + dp = dp->next; + } + return true; +} + +/* get the gas at a certain time during the dive */ +void get_gas_at_time(struct dive *dive, struct divecomputer *dc, duration_t time, struct gasmix *gas) +{ + // we always start with the first gas, so that's our gas + // unless an event tells us otherwise + struct event *event = dc->events; + *gas = dive->cylinder[0].gasmix; + while (event && event->time.seconds <= time.seconds) { + if (!strcmp(event->name, "gaschange")) { + int cylinder_idx = get_cylinder_index(dive, event); + *gas = dive->cylinder[cylinder_idx].gasmix; + } + event = event->next; + } +} + +int get_gasidx(struct dive *dive, struct gasmix *mix) +{ + int gasidx = -1; + + while (++gasidx < MAX_CYLINDERS) + if (gasmix_distance(&dive->cylinder[gasidx].gasmix, mix) < 100) + return gasidx; + return -1; +} + +void interpolate_transition(struct dive *dive, duration_t t0, duration_t t1, depth_t d0, depth_t d1, const struct gasmix *gasmix, o2pressure_t po2) +{ + int j; + + for (j = t0.seconds; j < t1.seconds; j++) { + int depth = interpolate(d0.mm, d1.mm, j - t0.seconds, t1.seconds - t0.seconds); + add_segment(depth_to_bar(depth, dive), gasmix, 1, po2.mbar, dive, prefs.bottomsac); + } + if (d1.mm > d0.mm) + calc_crushing_pressure(depth_to_bar(d1.mm, &displayed_dive)); +} + +/* returns the tissue tolerance at the end of this (partial) dive */ +void tissue_at_end(struct dive *dive, char **cached_datap) +{ + struct divecomputer *dc; + struct sample *sample, *psample; + int i; + depth_t lastdepth = {}; + duration_t t0 = {}, t1 = {}; + struct gasmix gas; + + if (!dive) + return; + if (*cached_datap) { + restore_deco_state(*cached_datap); + } else { + init_decompression(dive); + cache_deco_state(cached_datap); + } + dc = &dive->dc; + if (!dc->samples) + return; + psample = sample = dc->sample; + + for (i = 0; i < dc->samples; i++, sample++) { + o2pressure_t setpoint; + + if (i) + setpoint = sample[-1].setpoint; + else + setpoint = sample[0].setpoint; + + t1 = sample->time; + get_gas_at_time(dive, dc, t0, &gas); + if (i > 0) + lastdepth = psample->depth; + + /* The ceiling in the deeper portion of a multilevel dive is sometimes critical for the VPM-B + * Boyle's law compensation. We should check the ceiling prior to ascending during the bottom + * portion of the dive. The maximum ceiling might be reached while ascending, but testing indicates + * that it is only marginally deeper than the ceiling at the start of ascent. + * Do not set the first_ceiling_pressure variable (used for the Boyle's law compensation calculation) + * at this stage, because it would interfere with calculating the ceiling at the end of the bottom + * portion of the dive. + * Remember the value for later. + */ + if ((prefs.deco_mode == VPMB) && (lastdepth.mm > sample->depth.mm)) { + pressure_t ceiling_pressure; + nuclear_regeneration(t0.seconds); + vpmb_start_gradient(); + ceiling_pressure.mbar = depth_to_mbar(deco_allowed_depth(tissue_tolerance_calc(dive, + depth_to_bar(lastdepth.mm, dive)), + dive->surface_pressure.mbar / 1000.0, + dive, + 1), + dive); + if (ceiling_pressure.mbar > max_bottom_ceiling_pressure.mbar) + max_bottom_ceiling_pressure.mbar = ceiling_pressure.mbar; + } + + interpolate_transition(dive, t0, t1, lastdepth, sample->depth, &gas, setpoint); + psample = sample; + t0 = t1; + } +} + + +/* if a default cylinder is set, use that */ +void fill_default_cylinder(cylinder_t *cyl) +{ + const char *cyl_name = prefs.default_cylinder; + struct tank_info_t *ti = tank_info; + pressure_t pO2 = {.mbar = 1600}; + + if (!cyl_name) + return; + while (ti->name != NULL) { + if (strcmp(ti->name, cyl_name) == 0) + break; + ti++; + } + if (ti->name == NULL) + /* didn't find it */ + return; + cyl->type.description = strdup(ti->name); + if (ti->ml) { + cyl->type.size.mliter = ti->ml; + cyl->type.workingpressure.mbar = ti->bar * 1000; + } else { + cyl->type.workingpressure.mbar = psi_to_mbar(ti->psi); + if (ti->psi) + cyl->type.size.mliter = cuft_to_l(ti->cuft) * 1000 / bar_to_atm(psi_to_bar(ti->psi)); + } + // MOD of air + cyl->depth = gas_mod(&cyl->gasmix, pO2, &displayed_dive, 1); +} + +/* make sure that the gas we are switching to is represented in our + * list of cylinders */ +static int verify_gas_exists(struct gasmix mix_in) +{ + int i; + cylinder_t *cyl; + + for (i = 0; i < MAX_CYLINDERS; i++) { + cyl = displayed_dive.cylinder + i; + if (cylinder_nodata(cyl)) + continue; + if (gasmix_distance(&cyl->gasmix, &mix_in) < 100) + return i; + } + fprintf(stderr, "this gas %s should have been on the cylinder list\nThings will fail now\n", gasname(&mix_in)); + return -1; +} + +/* calculate the new end pressure of the cylinder, based on its current end pressure and the + * latest segment. */ +static void update_cylinder_pressure(struct dive *d, int old_depth, int new_depth, int duration, int sac, cylinder_t *cyl, bool in_deco) +{ + volume_t gas_used; + pressure_t delta_p; + depth_t mean_depth; + int factor = 1000; + + if (d->dc.divemode == PSCR) + factor = prefs.pscr_ratio; + + if (!cyl) + return; + mean_depth.mm = (old_depth + new_depth) / 2; + gas_used.mliter = depth_to_atm(mean_depth.mm, d) * sac / 60 * duration * factor / 1000; + cyl->gas_used.mliter += gas_used.mliter; + if (in_deco) + cyl->deco_gas_used.mliter += gas_used.mliter; + if (cyl->type.size.mliter) { + delta_p.mbar = gas_used.mliter * 1000.0 / cyl->type.size.mliter; + cyl->end.mbar -= delta_p.mbar; + } +} + +/* simply overwrite the data in the displayed_dive + * return false if something goes wrong */ +static void create_dive_from_plan(struct diveplan *diveplan, bool track_gas) +{ + struct divedatapoint *dp; + struct divecomputer *dc; + struct sample *sample; + struct gasmix oldgasmix; + struct event *ev; + cylinder_t *cyl; + int oldpo2 = 0; + int lasttime = 0; + int lastdepth = 0; + enum dive_comp_type type = displayed_dive.dc.divemode; + + if (!diveplan || !diveplan->dp) + return; +#if DEBUG_PLAN & 4 + printf("in create_dive_from_plan\n"); + dump_plan(diveplan); +#endif + displayed_dive.salinity = diveplan->salinity; + // reset the cylinders and clear out the samples and events of the + // displayed dive so we can restart + reset_cylinders(&displayed_dive, track_gas); + dc = &displayed_dive.dc; + dc->when = displayed_dive.when = diveplan->when; + free(dc->sample); + dc->sample = NULL; + dc->samples = 0; + dc->alloc_samples = 0; + while ((ev = dc->events)) { + dc->events = dc->events->next; + free(ev); + } + dp = diveplan->dp; + cyl = &displayed_dive.cylinder[0]; + oldgasmix = cyl->gasmix; + sample = prepare_sample(dc); + sample->setpoint.mbar = dp->setpoint; + sample->sac.mliter = prefs.bottomsac; + oldpo2 = dp->setpoint; + if (track_gas && cyl->type.workingpressure.mbar) + sample->cylinderpressure.mbar = cyl->end.mbar; + sample->manually_entered = true; + finish_sample(dc); + while (dp) { + struct gasmix gasmix = dp->gasmix; + int po2 = dp->setpoint; + if (dp->setpoint) + type = CCR; + int time = dp->time; + int depth = dp->depth; + + if (time == 0) { + /* special entries that just inform the algorithm about + * additional gases that are available */ + if (verify_gas_exists(gasmix) < 0) + goto gas_error_exit; + dp = dp->next; + continue; + } + + /* Check for SetPoint change */ + if (oldpo2 != po2) { + /* this is a bad idea - we should get a different SAMPLE_EVENT type + * reserved for this in libdivecomputer... overloading SMAPLE_EVENT_PO2 + * with a different meaning will only cause confusion elsewhere in the code */ + add_event(dc, lasttime, SAMPLE_EVENT_PO2, 0, po2, "SP change"); + oldpo2 = po2; + } + + /* Make sure we have the new gas, and create a gas change event */ + if (gasmix_distance(&gasmix, &oldgasmix) > 0) { + int idx; + if ((idx = verify_gas_exists(gasmix)) < 0) + goto gas_error_exit; + /* need to insert a first sample for the new gas */ + add_gas_switch_event(&displayed_dive, dc, lasttime + 1, idx); + cyl = &displayed_dive.cylinder[idx]; + sample = prepare_sample(dc); + sample[-1].setpoint.mbar = po2; + sample->time.seconds = lasttime + 1; + sample->depth.mm = lastdepth; + sample->manually_entered = dp->entered; + sample->sac.mliter = dp->entered ? prefs.bottomsac : prefs.decosac; + if (track_gas && cyl->type.workingpressure.mbar) + sample->cylinderpressure.mbar = cyl->sample_end.mbar; + finish_sample(dc); + oldgasmix = gasmix; + } + /* Create sample */ + sample = prepare_sample(dc); + /* set po2 at beginning of this segment */ + /* and keep it valid for last sample - where it likely doesn't matter */ + sample[-1].setpoint.mbar = po2; + sample->setpoint.mbar = po2; + sample->time.seconds = lasttime = time; + sample->depth.mm = lastdepth = depth; + sample->manually_entered = dp->entered; + sample->sac.mliter = dp->entered ? prefs.bottomsac : prefs.decosac; + if (track_gas && !sample[-1].setpoint.mbar) { /* Don't track gas usage for CCR legs of dive */ + update_cylinder_pressure(&displayed_dive, sample[-1].depth.mm, depth, time - sample[-1].time.seconds, + dp->entered ? diveplan->bottomsac : diveplan->decosac, cyl, !dp->entered); + if (cyl->type.workingpressure.mbar) + sample->cylinderpressure.mbar = cyl->end.mbar; + } + finish_sample(dc); + dp = dp->next; + } + dc->divemode = type; +#if DEBUG_PLAN & 32 + save_dive(stdout, &displayed_dive); +#endif + return; + +gas_error_exit: + report_error(translate("gettextFromC", "Too many gas mixes")); + return; +} + +void free_dps(struct diveplan *diveplan) +{ + if (!diveplan) + return; + struct divedatapoint *dp = diveplan->dp; + while (dp) { + struct divedatapoint *ndp = dp->next; + free(dp); + dp = ndp; + } + diveplan->dp = NULL; +} + +struct divedatapoint *create_dp(int time_incr, int depth, struct gasmix gasmix, int po2) +{ + struct divedatapoint *dp; + + dp = malloc(sizeof(struct divedatapoint)); + dp->time = time_incr; + dp->depth = depth; + dp->gasmix = gasmix; + dp->setpoint = po2; + dp->entered = false; + dp->next = NULL; + return dp; +} + +void add_to_end_of_diveplan(struct diveplan *diveplan, struct divedatapoint *dp) +{ + struct divedatapoint **lastdp = &diveplan->dp; + struct divedatapoint *ldp = *lastdp; + int lasttime = 0; + while (*lastdp) { + ldp = *lastdp; + if (ldp->time > lasttime) + lasttime = ldp->time; + lastdp = &(*lastdp)->next; + } + *lastdp = dp; + if (ldp && dp->time != 0) + dp->time += lasttime; +} + +struct divedatapoint *plan_add_segment(struct diveplan *diveplan, int duration, int depth, struct gasmix gasmix, int po2, bool entered) +{ + struct divedatapoint *dp = create_dp(duration, depth, gasmix, po2); + dp->entered = entered; + add_to_end_of_diveplan(diveplan, dp); + return (dp); +} + +struct gaschanges { + int depth; + int gasidx; +}; + + +static struct gaschanges *analyze_gaslist(struct diveplan *diveplan, int *gaschangenr, int depth, int *asc_cylinder) +{ + struct gasmix gas; + int nr = 0; + struct gaschanges *gaschanges = NULL; + struct divedatapoint *dp = diveplan->dp; + int best_depth = displayed_dive.cylinder[*asc_cylinder].depth.mm; + while (dp) { + if (dp->time == 0) { + gas = dp->gasmix; + if (dp->depth <= depth) { + int i = 0; + nr++; + gaschanges = realloc(gaschanges, nr * sizeof(struct gaschanges)); + while (i < nr - 1) { + if (dp->depth < gaschanges[i].depth) { + memmove(gaschanges + i + 1, gaschanges + i, (nr - i - 1) * sizeof(struct gaschanges)); + break; + } + i++; + } + gaschanges[i].depth = dp->depth; + gaschanges[i].gasidx = get_gasidx(&displayed_dive, &gas); + assert(gaschanges[i].gasidx != -1); + } else { + /* is there a better mix to start deco? */ + if (dp->depth < best_depth) { + best_depth = dp->depth; + *asc_cylinder = get_gasidx(&displayed_dive, &gas); + } + } + } + dp = dp->next; + } + *gaschangenr = nr; +#if DEBUG_PLAN & 16 + for (nr = 0; nr < *gaschangenr; nr++) { + int idx = gaschanges[nr].gasidx; + printf("gaschange nr %d: @ %5.2lfm gasidx %d (%s)\n", nr, gaschanges[nr].depth / 1000.0, + idx, gasname(&displayed_dive.cylinder[idx].gasmix)); + } +#endif + return gaschanges; +} + +/* sort all the stops into one ordered list */ +static unsigned int *sort_stops(int *dstops, int dnr, struct gaschanges *gstops, int gnr) +{ + int i, gi, di; + int total = dnr + gnr; + unsigned int *stoplevels = malloc(total * sizeof(int)); + + /* no gaschanges */ + if (gnr == 0) { + memcpy(stoplevels, dstops, dnr * sizeof(int)); + return stoplevels; + } + i = total - 1; + gi = gnr - 1; + di = dnr - 1; + while (i >= 0) { + if (dstops[di] > gstops[gi].depth) { + stoplevels[i] = dstops[di]; + di--; + } else if (dstops[di] == gstops[gi].depth) { + stoplevels[i] = dstops[di]; + di--; + gi--; + } else { + stoplevels[i] = gstops[gi].depth; + gi--; + } + i--; + if (di < 0) { + while (gi >= 0) + stoplevels[i--] = gstops[gi--].depth; + break; + } + if (gi < 0) { + while (di >= 0) + stoplevels[i--] = dstops[di--]; + break; + } + } + while (i >= 0) + stoplevels[i--] = 0; + +#if DEBUG_PLAN & 16 + int k; + for (k = gnr + dnr - 1; k >= 0; k--) { + printf("stoplevel[%d]: %5.2lfm\n", k, stoplevels[k] / 1000.0); + if (stoplevels[k] == 0) + break; + } +#endif + return stoplevels; +} + +static void add_plan_to_notes(struct diveplan *diveplan, struct dive *dive, bool show_disclaimer, int error) +{ + const unsigned int sz_buffer = 2000000; + const unsigned int sz_temp = 100000; + char *buffer = (char *)malloc(sz_buffer); + char *temp = (char *)malloc(sz_temp); + char buf[1000], *deco; + int len, lastdepth = 0, lasttime = 0, lastsetpoint = -1, newdepth = 0, lastprintdepth = 0, lastprintsetpoint = -1; + struct gasmix lastprintgasmix = {{ -1 }, { -1 }}; + struct divedatapoint *dp = diveplan->dp; + bool gaschange_after = !plan_verbatim; + bool gaschange_before; + bool lastentered = true; + struct divedatapoint *nextdp = NULL; + + plan_verbatim = prefs.verbatim_plan; + plan_display_runtime = prefs.display_runtime; + plan_display_duration = prefs.display_duration; + plan_display_transitions = prefs.display_transitions; + + if (prefs.deco_mode == VPMB) { + deco = "VPM-B"; + } else { + deco = "BUHLMANN"; + } + + snprintf(buf, sizeof(buf), translate("gettextFromC", "DISCLAIMER / WARNING: THIS IS A NEW IMPLEMENTATION OF THE %s " + "ALGORITHM AND A DIVE PLANNER IMPLEMENTATION BASED ON THAT WHICH HAS " + "RECEIVED ONLY A LIMITED AMOUNT OF TESTING. WE STRONGLY RECOMMEND NOT TO " + "PLAN DIVES SIMPLY BASED ON THE RESULTS GIVEN HERE."), deco); + disclaimer = buf; + + if (!dp) { + free((void *)buffer); + free((void *)temp); + return; + } + + if (error) { + snprintf(temp, sz_temp, "%s", + translate("gettextFromC", "Decompression calculation aborted due to excessive time")); + snprintf(buffer, sz_buffer, "%s %s
", + translate("gettextFromC", "Warning:"), temp); + dive->notes = strdup(buffer); + + free((void *)buffer); + free((void *)temp); + return; + } + + len = show_disclaimer ? snprintf(buffer, sz_buffer, "
%s

", disclaimer) : 0; + if (prefs.deco_mode == BUEHLMANN){ + snprintf(temp, sz_temp, translate("gettextFromC", "based on Bühlmann ZHL-16B with GFlow = %d and GFhigh = %d"), + diveplan->gflow, diveplan->gfhigh); + } else if (prefs.deco_mode == VPMB){ + if (prefs.conservatism_level == 0) + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "based on VPM-B at nominal conservatism")); + else + snprintf(temp, sz_temp, translate("gettextFromC", "based on VPM-B at +%d conservatism"), prefs.conservatism_level); + } else if (prefs.deco_mode == RECREATIONAL){ + snprintf(temp, sz_temp, translate("gettextFromC", "recreational mode based on Bühlmann ZHL-16B with GFlow = %d and GFhigh = %d"), + diveplan->gflow, diveplan->gfhigh); + } + len += snprintf(buffer + len, sz_buffer - len, "
%s
%s

", + translate("gettextFromC", "Subsurface dive plan"), temp); + + if (!plan_verbatim) { + len += snprintf(buffer + len, sz_buffer - len, "
", + translate("gettextFromC", "depth")); + if (plan_display_duration) + len += snprintf(buffer + len, sz_buffer - len, "", + translate("gettextFromC", "duration")); + if (plan_display_runtime) + len += snprintf(buffer + len, sz_buffer - len, "", + translate("gettextFromC", "runtime")); + len += snprintf(buffer + len, sz_buffer - len, + "", + translate("gettextFromC", "gas")); + } + do { + struct gasmix gasmix, newgasmix = {}; + const char *depth_unit; + double depthvalue; + int decimals; + bool isascent = (dp->depth < lastdepth); + + nextdp = dp->next; + if (dp->time == 0) + continue; + gasmix = dp->gasmix; + depthvalue = get_depth_units(dp->depth, &decimals, &depth_unit); + /* analyze the dive points ahead */ + while (nextdp && nextdp->time == 0) + nextdp = nextdp->next; + if (nextdp) + newgasmix = nextdp->gasmix; + gaschange_after = (nextdp && (gasmix_distance(&gasmix, &newgasmix) || dp->setpoint != nextdp->setpoint)); + gaschange_before = (gasmix_distance(&lastprintgasmix, &gasmix) || lastprintsetpoint != dp->setpoint); + /* do we want to skip this leg as it is devoid of anything useful? */ + if (!dp->entered && + nextdp && + dp->depth != lastdepth && + nextdp->depth != dp->depth && + !gaschange_before && + !gaschange_after) + continue; + if (dp->time - lasttime < 10 && !(gaschange_after && dp->next && dp->depth != dp->next->depth)) + continue; + + len = strlen(buffer); + if (plan_verbatim) { + /* When displaying a verbatim plan, we output a waypoint for every gas change. + * Therefore, we do not need to test for difficult cases that mean we need to + * print a segment just so we don't miss a gas change. This makes the logic + * to determine whether or not to print a segment much simpler than with the + * non-verbatim plan. + */ + if (dp->depth != lastprintdepth) { + if (plan_display_transitions || dp->entered || !dp->next || (gaschange_after && dp->next && dp->depth != nextdp->depth)) { + if (dp->setpoint) + snprintf(temp, sz_temp, translate("gettextFromC", "Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s (SP = %.1fbar)"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix), + (double) dp->setpoint / 1000.0); + + else + snprintf(temp, sz_temp, translate("gettextFromC", "Transition to %.*f %s in %d:%02d min - runtime %d:%02u on %s"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix)); + + len += snprintf(buffer + len, sz_buffer - len, "%s
", temp); + } + newdepth = dp->depth; + lasttime = dp->time; + } else { + if ((nextdp && dp->depth != nextdp->depth) || gaschange_after) { + if (dp->setpoint) + snprintf(temp, sz_temp, translate("gettextFromC", "Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s (SP = %.1fbar)"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix), + (double) dp->setpoint / 1000.0); + else + snprintf(temp, sz_temp, translate("gettextFromC", "Stay at %.*f %s for %d:%02d min - runtime %d:%02u on %s"), + decimals, depthvalue, depth_unit, + FRACTION(dp->time - lasttime, 60), + FRACTION(dp->time, 60), + gasname(&gasmix)); + + len += snprintf(buffer + len, sz_buffer - len, "%s
", temp); + newdepth = dp->depth; + lasttime = dp->time; + } + } + } else { + /* When not displaying the verbatim dive plan, we typically ignore ascents between deco stops, + * unless the display transitions option has been selected. We output a segment if any of the + * following conditions are met. + * 1) Display transitions is selected + * 2) The segment was manually entered + * 3) It is the last segment of the dive + * 4) The segment is not an ascent, there was a gas change at the start of the segment and the next segment + * is a change in depth (typical deco stop) + * 5) There is a gas change at the end of the segment and the last segment was entered (first calculated + * segment if it ends in a gas change) + * 6) There is a gaschange after but no ascent. This should only occur when backgas breaks option is selected + * 7) It is an ascent ending with a gas change, but is not followed by a stop. As case 5 already matches + * the first calculated ascent if it ends with a gas change, this should only occur if a travel gas is + * used for a calculated ascent, there is a subsequent gas change before the first deco stop, and zero + * time has been allowed for a gas switch. + */ + if (plan_display_transitions || dp->entered || !dp->next || + (nextdp && dp->depth != nextdp->depth) || + (!isascent && gaschange_before && nextdp && dp->depth != nextdp->depth) || + (gaschange_after && lastentered) || (gaschange_after && !isascent) || + (isascent && gaschange_after && nextdp && dp->depth != nextdp->depth )) { + snprintf(temp, sz_temp, translate("gettextFromC", "%3.0f%s"), depthvalue, depth_unit); + len += snprintf(buffer + len, sz_buffer - len, "", temp); + if (plan_display_duration) { + snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time - lasttime + 30) / 60); + len += snprintf(buffer + len, sz_buffer - len, "", temp); + } + if (plan_display_runtime) { + snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time + 30) / 60); + len += snprintf(buffer + len, sz_buffer - len, "", temp); + } + + /* Normally a gas change is displayed on the stopping segment, so only display a gas change at the end of + * an ascent segment if it is not followed by a stop + */ + if (isascent && gaschange_after && dp->next && nextdp && dp->depth != nextdp->depth) { + if (dp->setpoint) { + snprintf(temp, sz_temp, translate("gettextFromC", "(SP = %.1fbar)"), (double) nextdp->setpoint / 1000.0); + len += snprintf(buffer + len, sz_buffer - len, "", gasname(&newgasmix), + temp); + } else { + len += snprintf(buffer + len, sz_buffer - len, "", gasname(&newgasmix)); + } + lastprintsetpoint = nextdp->setpoint; + lastprintgasmix = newgasmix; + gaschange_after = false; + } else if (gaschange_before) { + // If a new gas has been used for this segment, now is the time to show it + if (dp->setpoint) { + snprintf(temp, sz_temp, translate("gettextFromC", "(SP = %.1fbar)"), (double) dp->setpoint / 1000.0); + len += snprintf(buffer + len, sz_buffer - len, "", gasname(&gasmix), + temp); + } else { + len += snprintf(buffer + len, sz_buffer - len, "", gasname(&gasmix)); + } + // Set variables so subsequent iterations can test against the last gas printed + lastprintsetpoint = dp->setpoint; + lastprintgasmix = gasmix; + gaschange_after = false; + } else { + len += snprintf(buffer + len, sz_buffer - len, ""); + } + len += snprintf(buffer + len, sz_buffer - len, ""); + newdepth = dp->depth; + lasttime = dp->time; + } + } + if (gaschange_after) { + // gas switch at this waypoint + if (plan_verbatim) { + if (lastsetpoint >= 0) { + if (nextdp && nextdp->setpoint) + snprintf(temp, sz_temp, translate("gettextFromC", "Switch gas to %s (SP = %.1fbar)"), gasname(&newgasmix), (double) nextdp->setpoint / 1000.0); + else + snprintf(temp, sz_temp, translate("gettextFromC", "Switch gas to %s"), gasname(&newgasmix)); + + len += snprintf(buffer + len, sz_buffer - len, "%s
", temp); + } + gaschange_after = false; + gasmix = newgasmix; + } + } + lastprintdepth = newdepth; + lastdepth = dp->depth; + lastsetpoint = dp->setpoint; + lastentered = dp->entered; + } while ((dp = nextdp) != NULL); + len += snprintf(buffer + len, sz_buffer - len, "
%s%s%s%s
%s%s%s%s %s%s%s %s%s 
"); + + dive->cns = 0; + dive->maxcns = 0; + update_cylinder_related_info(dive); + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "CNS")); + len += snprintf(buffer + len, sz_buffer - len, "

%s: %i%%", temp, dive->cns); + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "OTU")); + len += snprintf(buffer + len, sz_buffer - len, "
%s: %i
", temp, dive->otu); + + if (dive->dc.divemode == CCR) + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "Gas consumption (CCR legs excluded):")); + else + snprintf(temp, sz_temp, "%s", translate("gettextFromC", "Gas consumption:")); + len += snprintf(buffer + len, sz_buffer - len, "

%s
", temp); + for (int gasidx = 0; gasidx < MAX_CYLINDERS; gasidx++) { + double volume, pressure, deco_volume, deco_pressure; + const char *unit, *pressure_unit; + char warning[1000] = ""; + cylinder_t *cyl = &dive->cylinder[gasidx]; + if (cylinder_none(cyl)) + break; + + volume = get_volume_units(cyl->gas_used.mliter, NULL, &unit); + deco_volume = get_volume_units(cyl->deco_gas_used.mliter, NULL, &unit); + if (cyl->type.size.mliter) { + deco_pressure = get_pressure_units(1000.0 * cyl->deco_gas_used.mliter / cyl->type.size.mliter, &pressure_unit); + pressure = get_pressure_units(1000.0 * cyl->gas_used.mliter / cyl->type.size.mliter, &pressure_unit); + /* Warn if the plan uses more gas than is available in a cylinder + * This only works if we have working pressure for the cylinder + * 10bar is a made up number - but it seemed silly to pretend you could breathe cylinder down to 0 */ + if (cyl->end.mbar < 10000) + snprintf(warning, sizeof(warning), " — %s %s", + translate("gettextFromC", "Warning:"), + translate("gettextFromC", "this is more gas than available in the specified cylinder!")); + else + if ((float) cyl->end.mbar * cyl->type.size.mliter / 1000.0 < (float) cyl->deco_gas_used.mliter) + snprintf(warning, sizeof(warning), " — %s %s", + translate("gettextFromC", "Warning:"), + translate("gettextFromC", "not enough reserve for gas sharing on ascent!")); + + snprintf(temp, sz_temp, translate("gettextFromC", "%.0f%s/%.0f%s of %s (%.0f%s/%.0f%s in planned ascent)"), volume, unit, pressure, pressure_unit, gasname(&cyl->gasmix), deco_volume, unit, deco_pressure, pressure_unit); + } else { + snprintf(temp, sz_temp, translate("gettextFromC", "%.0f%s (%.0f%s during planned ascent) of %s"), volume, unit, deco_volume, unit, gasname(&cyl->gasmix)); + } + len += snprintf(buffer + len, sz_buffer - len, "%s%s
", temp, warning); + } + dp = diveplan->dp; + if (dive->dc.divemode != CCR) { + while (dp) { + if (dp->time != 0) { + struct gas_pressures pressures; + fill_pressures(&pressures, depth_to_atm(dp->depth, dive), &dp->gasmix, 0.0, dive->dc.divemode); + + if (pressures.o2 > (dp->entered ? prefs.bottompo2 : prefs.decopo2) / 1000.0) { + const char *depth_unit; + int decimals; + double depth_value = get_depth_units(dp->depth, &decimals, &depth_unit); + len = strlen(buffer); + snprintf(temp, sz_temp, + translate("gettextFromC", "high pOâ‚‚ value %.2f at %d:%02u with gas %s at depth %.*f %s"), + pressures.o2, FRACTION(dp->time, 60), gasname(&dp->gasmix), decimals, depth_value, depth_unit); + len += snprintf(buffer + len, sz_buffer - len, "%s %s
", + translate("gettextFromC", "Warning:"), temp); + } else if (pressures.o2 < 0.16) { + const char *depth_unit; + int decimals; + double depth_value = get_depth_units(dp->depth, &decimals, &depth_unit); + len = strlen(buffer); + snprintf(temp, sz_temp, + translate("gettextFromC", "low pOâ‚‚ value %.2f at %d:%02u with gas %s at depth %.*f %s"), + pressures.o2, FRACTION(dp->time, 60), gasname(&dp->gasmix), decimals, depth_value, depth_unit); + len += snprintf(buffer + len, sz_buffer - len, "%s %s
", + translate("gettextFromC", "Warning:"), temp); + + } + } + dp = dp->next; + } + } + snprintf(buffer + len, sz_buffer - len, "
"); + dive->notes = strdup(buffer); + + free((void *)buffer); + free((void *)temp); +} + +int ascent_velocity(int depth, int avg_depth, int bottom_time) +{ + /* We need to make this configurable */ + + /* As an example (and possibly reasonable default) this is the Tech 1 provedure according + * to http://www.globalunderwaterexplorers.org/files/Standards_and_Procedures/SOP_Manual_Ver2.0.2.pdf */ + + if (depth * 4 > avg_depth * 3) { + return prefs.ascrate75; + } else { + if (depth * 2 > avg_depth) { + return prefs.ascrate50; + } else { + if (depth > 6000) + return prefs.ascratestops; + else + return prefs.ascratelast6m; + } + } +} + +void track_ascent_gas(int depth, cylinder_t *cylinder, int avg_depth, int bottom_time, bool safety_stop) +{ + while (depth > 0) { + int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; + if (deltad > depth) + deltad = depth; + update_cylinder_pressure(&displayed_dive, depth, depth - deltad, TIMESTEP, prefs.decosac, cylinder, true); + if (depth <= 5000 && depth >= (5000 - deltad) && safety_stop) { + update_cylinder_pressure(&displayed_dive, 5000, 5000, 180, prefs.decosac, cylinder, true); + safety_stop = false; + } + depth -= deltad; + } +} + +// Determine whether ascending to the next stop will break the ceiling. Return true if the ascent is ok, false if it isn't. +bool trial_ascent(int trial_depth, int stoplevel, int avg_depth, int bottom_time, struct gasmix *gasmix, int po2, double surface_pressure) +{ + + bool clear_to_ascend = true; + char *trial_cache = NULL; + + // For consistency with other VPM-B implementations, we should not start the ascent while the ceiling is + // deeper than the next stop (thus the offgasing during the ascent is ignored). + // However, we still need to make sure we don't break the ceiling due to on-gassing during ascent. + if (prefs.deco_mode == VPMB && (deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, + depth_to_bar(stoplevel, &displayed_dive)), + surface_pressure, &displayed_dive, 1) > stoplevel)) + return false; + + cache_deco_state(&trial_cache); + while (trial_depth > stoplevel) { + int deltad = ascent_velocity(trial_depth, avg_depth, bottom_time) * TIMESTEP; + if (deltad > trial_depth) /* don't test against depth above surface */ + deltad = trial_depth; + add_segment(depth_to_bar(trial_depth, &displayed_dive), + gasmix, + TIMESTEP, po2, &displayed_dive, prefs.decosac); + if (deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, depth_to_bar(trial_depth, &displayed_dive)), + surface_pressure, &displayed_dive, 1) > trial_depth - deltad) { + /* We should have stopped */ + clear_to_ascend = false; + break; + } + trial_depth -= deltad; + } + restore_deco_state(trial_cache); + free(trial_cache); + return clear_to_ascend; +} + +/* Determine if there is enough gas for the dive. Return true if there is enough. + * Also return true if this cannot be calculated because the cylinder doesn't have + * size or a starting pressure. + */ +bool enough_gas(int current_cylinder) +{ + cylinder_t *cyl; + cyl = &displayed_dive.cylinder[current_cylinder]; + + if (!cyl->start.mbar) + return true; + if (cyl->type.size.mliter) + return (float) (cyl->end.mbar - prefs.reserve_gas) * cyl->type.size.mliter / 1000.0 > (float) cyl->deco_gas_used.mliter; + else + return true; +} + +// Work out the stops. Return value is if there were any mandatory stops. + +bool plan(struct diveplan *diveplan, char **cached_datap, bool is_planner, bool show_disclaimer) +{ + int bottom_depth; + int bottom_gi; + int bottom_stopidx; + bool is_final_plan = true; + int deco_time; + int previous_deco_time; + char *bottom_cache = NULL; + struct sample *sample; + int po2; + int transitiontime, gi; + int current_cylinder; + unsigned int stopidx; + int depth; + struct gaschanges *gaschanges = NULL; + int gaschangenr; + int *decostoplevels; + int decostoplevelcount; + unsigned int *stoplevels = NULL; + bool stopping = false; + bool pendinggaschange = false; + int clock, previous_point_time; + int avg_depth, max_depth, bottom_time = 0; + int last_ascend_rate; + int best_first_ascend_cylinder; + struct gasmix gas, bottom_gas; + int o2time = 0; + int breaktime = -1; + int breakcylinder = 0; + int error = 0; + bool decodive = false; + + set_gf(diveplan->gflow, diveplan->gfhigh, prefs.gf_low_at_maxdepth); + if (!diveplan->surface_pressure) + diveplan->surface_pressure = SURFACE_PRESSURE; + displayed_dive.surface_pressure.mbar = diveplan->surface_pressure; + clear_deco(displayed_dive.surface_pressure.mbar / 1000.0); + max_bottom_ceiling_pressure.mbar = first_ceiling_pressure.mbar = 0; + create_dive_from_plan(diveplan, is_planner); + + // Do we want deco stop array in metres or feet? + if (prefs.units.length == METERS ) { + decostoplevels = decostoplevels_metric; + decostoplevelcount = sizeof(decostoplevels_metric) / sizeof(int); + } else { + decostoplevels = decostoplevels_imperial; + decostoplevelcount = sizeof(decostoplevels_imperial) / sizeof(int); + } + + /* If the user has selected last stop to be at 6m/20', we need to get rid of the 3m/10' stop. + * Otherwise reinstate the last stop 3m/10' stop. + */ + if (prefs.last_stop) + *(decostoplevels + 1) = 0; + else + *(decostoplevels + 1) = M_OR_FT(3,10); + + /* Let's start at the last 'sample', i.e. the last manually entered waypoint. */ + sample = &displayed_dive.dc.sample[displayed_dive.dc.samples - 1]; + + get_gas_at_time(&displayed_dive, &displayed_dive.dc, sample->time, &gas); + + po2 = sample->setpoint.mbar; + if ((current_cylinder = get_gasidx(&displayed_dive, &gas)) == -1) { + report_error(translate("gettextFromC", "Can't find gas %s"), gasname(&gas)); + current_cylinder = 0; + } + depth = displayed_dive.dc.sample[displayed_dive.dc.samples - 1].depth.mm; + average_max_depth(diveplan, &avg_depth, &max_depth); + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + + /* if all we wanted was the dive just get us back to the surface */ + if (!is_planner) { + transitiontime = depth / 75; /* this still needs to be made configurable */ + plan_add_segment(diveplan, transitiontime, 0, gas, po2, false); + create_dive_from_plan(diveplan, is_planner); + return(false); + } + +#if DEBUG_PLAN & 4 + printf("gas %s\n", gasname(&gas)); + printf("depth %5.2lfm \n", depth / 1000.0); +#endif + + best_first_ascend_cylinder = current_cylinder; + /* Find the gases available for deco */ + + if (po2) { // Don't change gas in CCR mode + gaschanges = NULL; + gaschangenr = 0; + } else { + gaschanges = analyze_gaslist(diveplan, &gaschangenr, depth, &best_first_ascend_cylinder); + } + /* Find the first potential decostopdepth above current depth */ + for (stopidx = 0; stopidx < decostoplevelcount; stopidx++) + if (*(decostoplevels + stopidx) >= depth) + break; + if (stopidx > 0) + stopidx--; + /* Stoplevels are either depths of gas changes or potential deco stop depths. */ + stoplevels = sort_stops(decostoplevels, stopidx + 1, gaschanges, gaschangenr); + stopidx += gaschangenr; + + /* Keep time during the ascend */ + bottom_time = clock = previous_point_time = displayed_dive.dc.sample[displayed_dive.dc.samples - 1].time.seconds; + gi = gaschangenr - 1; + + /* Set tissue tolerance and initial vpmb gradient at start of ascent phase */ + tissue_at_end(&displayed_dive, cached_datap); + nuclear_regeneration(clock); + vpmb_start_gradient(); + + if(prefs.deco_mode == RECREATIONAL) { + bool safety_stop = prefs.safetystop && max_depth >= 10000; + track_ascent_gas(depth, &displayed_dive.cylinder[current_cylinder], avg_depth, bottom_time, safety_stop); + // How long can we stay at the current depth and still directly ascent to the surface? + do { + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + DECOTIMESTEP, po2, &displayed_dive, prefs.bottomsac); + update_cylinder_pressure(&displayed_dive, depth, depth, DECOTIMESTEP, prefs.bottomsac, &displayed_dive.cylinder[current_cylinder], false); + clock += DECOTIMESTEP; + } while (trial_ascent(depth, 0, avg_depth, bottom_time, &displayed_dive.cylinder[current_cylinder].gasmix, + po2, diveplan->surface_pressure / 1000.0) && + enough_gas(current_cylinder)); + + // We did stay one DECOTIMESTEP too many. + // In the best of all worlds, we would roll back also the last add_segment in terms of caching deco state, but + // let's ignore that since for the eventual ascent in recreational mode, nobody looks at the ceiling anymore, + // so we don't really have to compute the deco state. + update_cylinder_pressure(&displayed_dive, depth, depth, -DECOTIMESTEP, prefs.bottomsac, &displayed_dive.cylinder[current_cylinder], false); + clock -= DECOTIMESTEP; + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, true); + previous_point_time = clock; + do { + /* Ascend to surface */ + int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; + if (ascent_velocity(depth, avg_depth, bottom_time) != last_ascend_rate) { + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + } + if (depth - deltad < 0) + deltad = depth; + + clock += TIMESTEP; + depth -= deltad; + if (depth <= 5000 && depth >= (5000 - deltad) && safety_stop) { + plan_add_segment(diveplan, clock - previous_point_time, 5000, gas, po2, false); + previous_point_time = clock; + clock += 180; + plan_add_segment(diveplan, clock - previous_point_time, 5000, gas, po2, false); + previous_point_time = clock; + safety_stop = false; + } + } while (depth > 0); + plan_add_segment(diveplan, clock - previous_point_time, 0, gas, po2, false); + create_dive_from_plan(diveplan, is_planner); + add_plan_to_notes(diveplan, &displayed_dive, show_disclaimer, error); + fixup_dc_duration(&displayed_dive.dc); + + free(stoplevels); + free(gaschanges); + return(false); + } + + if (best_first_ascend_cylinder != current_cylinder) { + current_cylinder = best_first_ascend_cylinder; + gas = displayed_dive.cylinder[current_cylinder].gasmix; + +#if DEBUG_PLAN & 16 + printf("switch to gas %d (%d/%d) @ %5.2lfm\n", best_first_ascend_cylinder, + (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[best_first_ascend_cylinder].depth / 1000.0); +#endif + } + + // VPM-B or Buehlmann Deco + tissue_at_end(&displayed_dive, cached_datap); + previous_deco_time = 100000000; + deco_time = 10000000; + cache_deco_state(&bottom_cache); // Lets us make several iterations + bottom_depth = depth; + bottom_gi = gi; + bottom_gas = gas; + bottom_stopidx = stopidx; + + //CVA + do { + is_final_plan = (prefs.deco_mode == BUEHLMANN) || (previous_deco_time - deco_time < 10); // CVA time converges + if (deco_time != 10000000) + vpmb_next_gradient(deco_time, diveplan->surface_pressure / 1000.0); + + previous_deco_time = deco_time; + restore_deco_state(bottom_cache); + + depth = bottom_depth; + gi = bottom_gi; + clock = previous_point_time = bottom_time; + gas = bottom_gas; + stopping = false; + decodive = false; + stopidx = bottom_stopidx; + breaktime = -1; + breakcylinder = 0; + o2time = 0; + first_ceiling_pressure.mbar = depth_to_mbar(deco_allowed_depth(tissue_tolerance_calc(&displayed_dive, + depth_to_bar(depth, &displayed_dive)), + diveplan->surface_pressure / 1000.0, + &displayed_dive, + 1), + &displayed_dive); + if (max_bottom_ceiling_pressure.mbar > first_ceiling_pressure.mbar) + first_ceiling_pressure.mbar = max_bottom_ceiling_pressure.mbar; + + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + if ((current_cylinder = get_gasidx(&displayed_dive, &gas)) == -1) { + report_error(translate("gettextFromC", "Can't find gas %s"), gasname(&gas)); + current_cylinder = 0; + } + + while (1) { + /* We will break out when we hit the surface */ + do { + /* Ascend to next stop depth */ + int deltad = ascent_velocity(depth, avg_depth, bottom_time) * TIMESTEP; + if (ascent_velocity(depth, avg_depth, bottom_time) != last_ascend_rate) { + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = false; + last_ascend_rate = ascent_velocity(depth, avg_depth, bottom_time); + } + if (depth - deltad < stoplevels[stopidx]) + deltad = depth - stoplevels[stopidx]; + + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + TIMESTEP, po2, &displayed_dive, prefs.decosac); + clock += TIMESTEP; + depth -= deltad; + } while (depth > 0 && depth > stoplevels[stopidx]); + + if (depth <= 0) + break; /* We are at the surface */ + + if (gi >= 0 && stoplevels[stopidx] <= gaschanges[gi].depth) { + /* We have reached a gas change. + * Record this in the dive plan */ + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = true; + + /* Check we need to change cylinder. + * We might not if the cylinder was chosen by the user + * or user has selected only to switch only at required stops. + * If current gas is hypoxic, we want to switch asap */ + + if (current_cylinder != gaschanges[gi].gasidx) { + if (!prefs.switch_at_req_stop || + !trial_ascent(depth, stoplevels[stopidx - 1], avg_depth, bottom_time, + &displayed_dive.cylinder[current_cylinder].gasmix, po2, diveplan->surface_pressure / 1000.0) || get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) < 160) { + current_cylinder = gaschanges[gi].gasidx; + gas = displayed_dive.cylinder[current_cylinder].gasmix; +#if DEBUG_PLAN & 16 + printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi].gasidx, + (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[gi].depth / 1000.0); +#endif + /* Stop for the minimum duration to switch gas */ + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + prefs.min_switch_duration, po2, &displayed_dive, prefs.decosac); + clock += prefs.min_switch_duration; + if (prefs.doo2breaks && get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) + o2time += prefs.min_switch_duration; + } else { + /* The user has selected the option to switch gas only at required stops. + * Remember that we are waiting to switch gas + */ + pendinggaschange = true; + } + } + gi--; + } + --stopidx; + + /* Save the current state and try to ascend to the next stopdepth */ + while (1) { + /* Check if ascending to next stop is clear, go back and wait if we hit the ceiling on the way */ + if (trial_ascent(depth, stoplevels[stopidx], avg_depth, bottom_time, + &displayed_dive.cylinder[current_cylinder].gasmix, po2, diveplan->surface_pressure / 1000.0)) + break; /* We did not hit the ceiling */ + + /* Add a minute of deco time and then try again */ + decodive = true; + if (!stopping) { + /* The last segment was an ascend segment. + * Add a waypoint for start of this deco stop */ + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = true; + } + + /* Are we waiting to switch gas? + * Occurs when the user has selected the option to switch only at required stops + */ + if (pendinggaschange) { + current_cylinder = gaschanges[gi + 1].gasidx; + gas = displayed_dive.cylinder[current_cylinder].gasmix; +#if DEBUG_PLAN & 16 + printf("switch to gas %d (%d/%d) @ %5.2lfm\n", gaschanges[gi + 1].gasidx, + (get_o2(&gas) + 5) / 10, (get_he(&gas) + 5) / 10, gaschanges[gi + 1].depth / 1000.0); +#endif + /* Stop for the minimum duration to switch gas */ + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + prefs.min_switch_duration, po2, &displayed_dive, prefs.decosac); + clock += prefs.min_switch_duration; + if (prefs.doo2breaks && get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) + o2time += prefs.min_switch_duration; + pendinggaschange = false; + } + + /* Deco stop should end when runtime is at a whole minute */ + int this_decotimestep; + this_decotimestep = DECOTIMESTEP - clock % DECOTIMESTEP; + + add_segment(depth_to_bar(depth, &displayed_dive), + &displayed_dive.cylinder[current_cylinder].gasmix, + this_decotimestep, po2, &displayed_dive, prefs.decosac); + clock += this_decotimestep; + /* Finish infinite deco */ + if(clock >= 48 * 3600 && depth >= 6000) { + error = LONGDECO; + break; + } + if (prefs.doo2breaks) { + /* The backgas breaks option limits time on oxygen to 12 minutes, followed by 6 minutes on + * backgas (first defined gas). This could be customized if there were demand. + */ + if (get_o2(&displayed_dive.cylinder[current_cylinder].gasmix) == 1000) { + o2time += DECOTIMESTEP; + if (o2time >= 12 * 60) { + breaktime = 0; + breakcylinder = current_cylinder; + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + current_cylinder = 0; + gas = displayed_dive.cylinder[current_cylinder].gasmix; + } + } else { + if (breaktime >= 0) { + breaktime += DECOTIMESTEP; + if (breaktime >= 6 * 60) { + o2time = 0; + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + current_cylinder = breakcylinder; + gas = displayed_dive.cylinder[current_cylinder].gasmix; + breaktime = -1; + } + } + } + } + } + if (stopping) { + /* Next we will ascend again. Add a waypoint if we have spend deco time */ + if (is_final_plan) + plan_add_segment(diveplan, clock - previous_point_time, depth, gas, po2, false); + previous_point_time = clock; + stopping = false; + } + } + + deco_time = clock - bottom_time; + } while (!is_final_plan); + + plan_add_segment(diveplan, clock - previous_point_time, 0, gas, po2, false); + create_dive_from_plan(diveplan, is_planner); + add_plan_to_notes(diveplan, &displayed_dive, show_disclaimer, error); + fixup_dc_duration(&displayed_dive.dc); + + free(stoplevels); + free(gaschanges); + free(bottom_cache); + return decodive; +} + +/* + * Get a value in tenths (so "10.2" == 102, "9" = 90) + * + * Return negative for errors. + */ +static int get_tenths(const char *begin, const char **endp) +{ + char *end; + int value = strtol(begin, &end, 10); + + if (begin == end) + return -1; + value *= 10; + + /* Fraction? We only look at the first digit */ + if (*end == '.') { + end++; + if (!isdigit(*end)) + return -1; + value += *end - '0'; + do { + end++; + } while (isdigit(*end)); + } + *endp = end; + return value; +} + +static int get_permille(const char *begin, const char **end) +{ + int value = get_tenths(begin, end); + if (value >= 0) { + /* Allow a percentage sign */ + if (**end == '%') + ++*end; + } + return value; +} + +int validate_gas(const char *text, struct gasmix *gas) +{ + int o2, he; + + if (!text) + return 0; + + while (isspace(*text)) + text++; + + if (!*text) + return 0; + + if (!strcasecmp(text, translate("gettextFromC", "air"))) { + o2 = O2_IN_AIR; + he = 0; + text += strlen(translate("gettextFromC", "air")); + } else if (!strcasecmp(text, translate("gettextFromC", "oxygen"))) { + o2 = 1000; + he = 0; + text += strlen(translate("gettextFromC", "oxygen")); + } else if (!strncasecmp(text, translate("gettextFromC", "ean"), 3)) { + o2 = get_permille(text + 3, &text); + he = 0; + } else { + o2 = get_permille(text, &text); + he = 0; + if (*text == '/') + he = get_permille(text + 1, &text); + } + + /* We don't want any extra crud */ + while (isspace(*text)) + text++; + if (*text) + return 0; + + /* Validate the gas mix */ + if (*text || o2 < 1 || o2 > 1000 || he < 0 || o2 + he > 1000) + return 0; + + /* Let it rip */ + gas->o2.permille = o2; + gas->he.permille = he; + return 1; +} + +int validate_po2(const char *text, int *mbar_po2) +{ + int po2; + + if (!text) + return 0; + + po2 = get_tenths(text, &text); + if (po2 < 0) + return 0; + + while (isspace(*text)) + text++; + + while (isspace(*text)) + text++; + if (*text) + return 0; + + *mbar_po2 = po2 * 100; + return 1; +} diff --git a/subsurface-core/planner.h b/subsurface-core/planner.h new file mode 100644 index 000000000..a675989e0 --- /dev/null +++ b/subsurface-core/planner.h @@ -0,0 +1,32 @@ +#ifndef PLANNER_H +#define PLANNER_H + +#define LONGDECO 1 +#define NOT_RECREATIONAL 2 + +#ifdef __cplusplus +extern "C" { +#endif + +extern int validate_gas(const char *text, struct gasmix *gas); +extern int validate_po2(const char *text, int *mbar_po2); +extern timestamp_t current_time_notz(void); +extern void set_last_stop(bool last_stop_6m); +extern void set_verbatim(bool verbatim); +extern void set_display_runtime(bool display); +extern void set_display_duration(bool display); +extern void set_display_transitions(bool display); +extern void get_gas_at_time(struct dive *dive, struct divecomputer *dc, duration_t time, struct gasmix *gas); +extern int get_gasidx(struct dive *dive, struct gasmix *mix); +extern bool diveplan_empty(struct diveplan *diveplan); + +extern void free_dps(struct diveplan *diveplan); +extern struct dive *planned_dive; +extern char *cache_data; +extern const char *disclaimer; +extern double plangflow, plangfhigh; + +#ifdef __cplusplus +} +#endif +#endif // PLANNER_H diff --git a/subsurface-core/pref.h b/subsurface-core/pref.h new file mode 100644 index 000000000..84975aaaa --- /dev/null +++ b/subsurface-core/pref.h @@ -0,0 +1,158 @@ +#ifndef PREF_H +#define PREF_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "units.h" +#include "taxonomy.h" + +/* can't use 'bool' for the boolean values - different size in C and C++ */ +typedef struct +{ + short po2; + short pn2; + short phe; + double po2_threshold; + double pn2_threshold; + double phe_threshold; +} partial_pressure_graphs_t; + +typedef struct { + char *access_token; + char *user_id; + char *album_id; +} facebook_prefs_t; + +typedef struct { + bool enable_geocoding; + bool parse_dive_without_gps; + bool tag_existing_dives; + enum taxonomy_category category[3]; +} geocoding_prefs_t; + +enum deco_mode { + BUEHLMANN, + RECREATIONAL, + VPMB +}; + +struct preferences { + const char *divelist_font; + const char *default_filename; + const char *default_cylinder; + const char *cloud_base_url; + const char *cloud_git_url; + double font_size; + partial_pressure_graphs_t pp_graphs; + short mod; + double modpO2; + short ead; + short dcceiling; + short redceiling; + short calcceiling; + short calcceiling3m; + short calcalltissues; + short calcndltts; + short gflow; + short gfhigh; + int animation_speed; + bool gf_low_at_maxdepth; + bool show_ccr_setpoint; + bool show_ccr_sensors; + short display_invalid_dives; + short unit_system; + struct units units; + bool coordinates_traditional; + short show_sac; + short display_unused_tanks; + short show_average_depth; + short zoomed_plot; + short hrgraph; + short percentagegraph; + short rulergraph; + short tankbar; + short save_userid_local; + char *userid; + int ascrate75; // All rates in mm / sec + int ascrate50; + int ascratestops; + int ascratelast6m; + int descrate; + int bottompo2; + int decopo2; + int proxy_type; + char *proxy_host; + int proxy_port; + short proxy_auth; + char *proxy_user; + char *proxy_pass; + bool doo2breaks; + bool drop_stone_mode; + bool last_stop; // At 6m? + bool verbatim_plan; + bool display_runtime; + bool display_duration; + bool display_transitions; + bool safetystop; + bool switch_at_req_stop; + int reserve_gas; + int min_switch_duration; // seconds + int bottomsac; + int decosac; + int o2consumption; // ml per min + int pscr_ratio; // dump ratio times 1000 + int defaultsetpoint; // default setpoint in mbar + bool show_pictures_in_profile; + bool use_default_file; + short default_file_behavior; + facebook_prefs_t facebook; + char *cloud_storage_password; + char *cloud_storage_newpassword; + char *cloud_storage_email; + char *cloud_storage_email_encoded; + bool save_password_local; + short cloud_verification_status; + bool cloud_background_sync; + geocoding_prefs_t geocoding; + enum deco_mode deco_mode; + short conservatism_level; +}; +enum unit_system_values { + METRIC, + IMPERIAL, + PERSONALIZE +}; + +enum def_file_behavior { + UNDEFINED_DEFAULT_FILE, + LOCAL_DEFAULT_FILE, + NO_DEFAULT_FILE, + CLOUD_DEFAULT_FILE +}; + +enum cloud_status { + CS_UNKNOWN, + CS_INCORRECT_USER_PASSWD, + CS_NEED_TO_VERIFY, + CS_VERIFIED +}; + +extern struct preferences prefs, default_prefs, informational_prefs; + +#define PP_GRAPHS_ENABLED (prefs.pp_graphs.po2 || prefs.pp_graphs.pn2 || prefs.pp_graphs.phe) + +extern const char *system_divelist_default_font; +extern double system_divelist_default_font_size; + +extern const char *system_default_directory(void); +extern const char *system_default_filename(); +extern bool subsurface_ignore_font(const char *font); +extern void subsurface_OS_pref_setup(); + +#ifdef __cplusplus +} +#endif + +#endif // PREF_H diff --git a/subsurface-core/prefs-macros.h b/subsurface-core/prefs-macros.h new file mode 100644 index 000000000..fe459d3da --- /dev/null +++ b/subsurface-core/prefs-macros.h @@ -0,0 +1,68 @@ +#ifndef PREFSMACROS_H +#define PREFSMACROS_H + +#define SB(V, B) s.setValue(V, (int)(B->isChecked() ? 1 : 0)) + +#define GET_UNIT(name, field, f, t) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.units.field = (v.toInt() == (t)) ? (t) : (f); \ + else \ + prefs.units.field = default_prefs.units.field + +#define GET_BOOL(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toBool(); \ + else \ + prefs.field = default_prefs.field + +#define GET_DOUBLE(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toDouble(); \ + else \ + prefs.field = default_prefs.field + +#define GET_INT(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toInt(); \ + else \ + prefs.field = default_prefs.field + +#define GET_ENUM(name, type, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = (enum type)v.toInt(); \ + else \ + prefs.field = default_prefs.field + +#define GET_INT_DEF(name, field, defval) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = v.toInt(); \ + else \ + prefs.field = defval + +#define GET_TXT(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = strdup(v.toString().toUtf8().constData()); \ + else \ + prefs.field = default_prefs.field + +#define SAVE_OR_REMOVE_SPECIAL(_setting, _default, _compare, _value) \ + if (_compare != _default) \ + s.setValue(_setting, _value); \ + else \ + s.remove(_setting) + +#define SAVE_OR_REMOVE(_setting, _default, _value) \ + if (_value != _default) \ + s.setValue(_setting, _value); \ + else \ + s.remove(_setting) + +#endif // PREFSMACROS_H + diff --git a/subsurface-core/profile.c b/subsurface-core/profile.c new file mode 100644 index 000000000..d39133c21 --- /dev/null +++ b/subsurface-core/profile.c @@ -0,0 +1,1463 @@ +/* profile.c */ +/* creates all the necessary data for drawing the dive profile + */ +#include "gettext.h" +#include +#include +#include + +#include "dive.h" +#include "display.h" +#include "divelist.h" + +#include "profile.h" +#include "gaspressures.h" +#include "deco.h" +#include "libdivecomputer/parser.h" +#include "libdivecomputer/version.h" +#include "membuffer.h" + +//#define DEBUG_GAS 1 + +#define MAX_PROFILE_DECO 7200 + + +int selected_dive = -1; /* careful: 0 is a valid value */ +unsigned int dc_number = 0; + +static struct plot_data *last_pi_entry_new = NULL; +void populate_pressure_information(struct dive *, struct divecomputer *, struct plot_info *, int); + +#ifdef DEBUG_PI +/* debugging tool - not normally used */ +static void dump_pi(struct plot_info *pi) +{ + int i; + + printf("pi:{nr:%d maxtime:%d meandepth:%d maxdepth:%d \n" + " maxpressure:%d mintemp:%d maxtemp:%d\n", + pi->nr, pi->maxtime, pi->meandepth, pi->maxdepth, + pi->maxpressure, pi->mintemp, pi->maxtemp); + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = &pi->entry[i]; + printf(" entry[%d]:{cylinderindex:%d sec:%d pressure:{%d,%d}\n" + " time:%d:%02d temperature:%d depth:%d stopdepth:%d stoptime:%d ndl:%d smoothed:%d po2:%lf phe:%lf pn2:%lf sum-pp %lf}\n", + i, entry->cylinderindex, entry->sec, + entry->pressure[0], entry->pressure[1], + entry->sec / 60, entry->sec % 60, + entry->temperature, entry->depth, entry->stopdepth, entry->stoptime, entry->ndl, entry->smoothed, + entry->pressures.o2, entry->pressures.he, entry->pressures.n2, + entry->pressures.o2 + entry->pressures.he + entry->pressures.n2); + } + printf(" }\n"); +} +#endif + +#define ROUND_UP(x, y) ((((x) + (y) - 1) / (y)) * (y)) +#define DIV_UP(x, y) (((x) + (y) - 1) / (y)) + +/* + * When showing dive profiles, we scale things to the + * current dive. However, we don't scale past less than + * 30 minutes or 90 ft, just so that small dives show + * up as such unless zoom is enabled. + * We also need to add 180 seconds at the end so the min/max + * plots correctly + */ +int get_maxtime(struct plot_info *pi) +{ + int seconds = pi->maxtime; + + int DURATION_THR = (pi->dive_type == FREEDIVING ? 60 : 600); + int CEILING = (pi->dive_type == FREEDIVING ? 30 : 60); + + if (prefs.zoomed_plot) { + /* Rounded up to one minute, with at least 2.5 minutes to + * spare. + * For dive times shorter than 10 minutes, we use seconds/4 to + * calculate the space dynamically. + * This is seamless since 600/4 = 150. + */ + if (seconds < DURATION_THR) + return ROUND_UP(seconds + seconds / 4, CEILING); + else + return ROUND_UP(seconds + DURATION_THR/4, CEILING); + } else { + /* min 30 minutes, rounded up to 5 minutes, with at least 2.5 minutes to spare */ + return MAX(30 * 60, ROUND_UP(seconds + DURATION_THR/4, CEILING * 5)); + } +} + +/* get the maximum depth to which we want to plot + * take into account the additional vertical space needed to plot + * partial pressure graphs */ +int get_maxdepth(struct plot_info *pi) +{ + unsigned mm = pi->maxdepth; + int md; + + if (prefs.zoomed_plot) { + /* Rounded up to 10m, with at least 3m to spare */ + md = ROUND_UP(mm + 3000, 10000); + } else { + /* Minimum 30m, rounded up to 10m, with at least 3m to spare */ + md = MAX((unsigned)30000, ROUND_UP(mm + 3000, 10000)); + } + md += pi->maxpp * 9000; + return md; +} + +/* collect all event names and whether we display them */ +struct ev_select *ev_namelist; +int evn_allocated; +int evn_used; + +#if WE_DONT_USE_THIS /* we need to implement event filters in Qt */ +int evn_foreach (void (*callback)(const char *, bool *, void *), void *data) { + int i; + + for (i = 0; i < evn_used; i++) { + /* here we display an event name on screen - so translate */ + callback(translate("gettextFromC", ev_namelist[i].ev_name), &ev_namelist[i].plot_ev, data); + } + return i; +} +#endif /* WE_DONT_USE_THIS */ + +void clear_events(void) +{ + for (int i = 0; i < evn_used; i++) + free(ev_namelist[i].ev_name); + evn_used = 0; +} + +void remember_event(const char *eventname) +{ + int i = 0, len; + + if (!eventname || (len = strlen(eventname)) == 0) + return; + while (i < evn_used) { + if (!strncmp(eventname, ev_namelist[i].ev_name, len)) + return; + i++; + } + if (evn_used == evn_allocated) { + evn_allocated += 10; + ev_namelist = realloc(ev_namelist, evn_allocated * sizeof(struct ev_select)); + if (!ev_namelist) + /* we are screwed, but let's just bail out */ + return; + } + ev_namelist[evn_used].ev_name = strdup(eventname); + ev_namelist[evn_used].plot_ev = true; + evn_used++; +} + +/* Get local sac-rate (in ml/min) between entry1 and entry2 */ +static int get_local_sac(struct plot_data *entry1, struct plot_data *entry2, struct dive *dive) +{ + int index = entry1->cylinderindex; + cylinder_t *cyl; + int duration = entry2->sec - entry1->sec; + int depth, airuse; + pressure_t a, b; + double atm; + + if (entry2->cylinderindex != index) + return 0; + if (duration <= 0) + return 0; + a.mbar = GET_PRESSURE(entry1); + b.mbar = GET_PRESSURE(entry2); + if (!b.mbar || a.mbar <= b.mbar) + return 0; + + /* Mean pressure in ATM */ + depth = (entry1->depth + entry2->depth) / 2; + atm = depth_to_atm(depth, dive); + + cyl = dive->cylinder + index; + + airuse = gas_volume(cyl, a) - gas_volume(cyl, b); + + /* milliliters per minute */ + return airuse / atm * 60 / duration; +} + +static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index) +{ + struct plot_data *p = entry; + int time = entry->sec; + int seconds = 90 * (index + 1); + struct plot_data *min, *max; + int avg, nr; + + /* Go back 'seconds' in time */ + while (p > first) { + if (p[-1].sec < time - seconds) + break; + p--; + } + + /* Then go forward until we hit an entry past the time */ + min = max = p; + avg = p->depth; + nr = 1; + while (++p < last) { + int depth = p->depth; + if (p->sec > time + seconds) + break; + avg += depth; + nr++; + if (depth < min->depth) + min = p; + if (depth > max->depth) + max = p; + } + entry->min[index] = min; + entry->max[index] = max; + entry->avg[index] = (avg + nr / 2) / nr; +} + +static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last) +{ + analyze_plot_info_minmax_minute(entry, first, last, 0); + analyze_plot_info_minmax_minute(entry, first, last, 1); + analyze_plot_info_minmax_minute(entry, first, last, 2); +} + +static velocity_t velocity(int speed) +{ + velocity_t v; + + if (speed < -304) /* ascent faster than -60ft/min */ + v = CRAZY; + else if (speed < -152) /* above -30ft/min */ + v = FAST; + else if (speed < -76) /* -15ft/min */ + v = MODERATE; + else if (speed < -25) /* -5ft/min */ + v = SLOW; + else if (speed < 25) /* very hard to find data, but it appears that the recommendations + for descent are usually about 2x ascent rate; still, we want + stable to mean stable */ + v = STABLE; + else if (speed < 152) /* between 5 and 30ft/min is considered slow */ + v = SLOW; + else if (speed < 304) /* up to 60ft/min is moderate */ + v = MODERATE; + else if (speed < 507) /* up to 100ft/min is fast */ + v = FAST; + else /* more than that is just crazy - you'll blow your ears out */ + v = CRAZY; + + return v; +} + +struct plot_info *analyze_plot_info(struct plot_info *pi) +{ + int i; + int nr = pi->nr; + + /* Smoothing function: 5-point triangular smooth */ + for (i = 2; i < nr; i++) { + struct plot_data *entry = pi->entry + i; + int depth; + + if (i < nr - 2) { + depth = entry[-2].depth + 2 * entry[-1].depth + 3 * entry[0].depth + 2 * entry[1].depth + entry[2].depth; + entry->smoothed = (depth + 4) / 9; + } + /* vertical velocity in mm/sec */ + /* Linus wants to smooth this - let's at least look at the samples that aren't FAST or CRAZY */ + if (entry[0].sec - entry[-1].sec) { + entry->speed = (entry[0].depth - entry[-1].depth) / (entry[0].sec - entry[-1].sec); + entry->velocity = velocity(entry->speed); + /* if our samples are short and we aren't too FAST*/ + if (entry[0].sec - entry[-1].sec < 15 && entry->velocity < FAST) { + int past = -2; + while (i + past > 0 && entry[0].sec - entry[past].sec < 15) + past--; + entry->velocity = velocity((entry[0].depth - entry[past].depth) / + (entry[0].sec - entry[past].sec)); + } + } else { + entry->velocity = STABLE; + entry->speed = 0; + } + } + + /* One-, two- and three-minute minmax data */ + for (i = 0; i < nr; i++) { + struct plot_data *entry = pi->entry + i; + analyze_plot_info_minmax(entry, pi->entry, pi->entry + nr); + } + + return pi; +} + +/* + * If the event has an explicit cylinder index, + * we return that. If it doesn't, we return the best + * match based on the gasmix. + * + * Some dive computers give cylinder indexes, some + * give just the gas mix. + */ +int get_cylinder_index(struct dive *dive, struct event *ev) +{ + int i; + int best = 0, score = INT_MAX; + int target_o2, target_he; + struct gasmix *g; + + if (ev->gas.index >= 0) + return ev->gas.index; + + g = get_gasmix_from_event(ev); + target_o2 = get_o2(g); + target_he = get_he(g); + + /* + * Try to find a cylinder that best matches the target gas + * mix. + */ + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + int delta_o2, delta_he, distance; + + if (cylinder_nodata(cyl)) + continue; + + delta_o2 = get_o2(&cyl->gasmix) - target_o2; + delta_he = get_he(&cyl->gasmix) - target_he; + distance = delta_o2 * delta_o2; + distance += delta_he * delta_he; + + if (distance >= score) + continue; + score = distance; + best = i; + } + return best; +} + +struct event *get_next_event(struct event *event, const char *name) +{ + if (!name || !*name) + return NULL; + while (event) { + if (!strcmp(event->name, name)) + return event; + event = event->next; + } + return event; +} + +static int count_events(struct divecomputer *dc) +{ + int result = 0; + struct event *ev = dc->events; + while (ev != NULL) { + result++; + ev = ev->next; + } + return result; +} + +static int set_cylinder_index(struct plot_info *pi, int i, int cylinderindex, unsigned int end) +{ + while (i < pi->nr) { + struct plot_data *entry = pi->entry + i; + if (entry->sec > end) + break; + if (entry->cylinderindex != cylinderindex) { + entry->cylinderindex = cylinderindex; + entry->pressure[0] = 0; + } + i++; + } + return i; +} + +static int set_setpoint(struct plot_info *pi, int i, int setpoint, unsigned int end) +{ + while (i < pi->nr) { + struct plot_data *entry = pi->entry + i; + if (entry->sec > end) + break; + entry->o2pressure.mbar = setpoint; + i++; + } + return i; +} + +/* normally the first cylinder has index 0... if not, we need to fix this up here */ +static int set_first_cylinder_index(struct plot_info *pi, int i, int cylinderindex, unsigned int end) +{ + while (i < pi->nr) { + struct plot_data *entry = pi->entry + i; + if (entry->sec > end) + break; + entry->cylinderindex = cylinderindex; + i++; + } + return i; +} + +static void check_gas_change_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int i = 0, cylinderindex = 0; + struct event *ev = get_next_event(dc->events, "gaschange"); + + // for dive computers that tell us their first gas as an event on the first sample + // we need to make sure things are setup correctly + cylinderindex = explicit_first_cylinder(dive, dc); + set_first_cylinder_index(pi, 0, cylinderindex, ~0u); + + if (!ev) + return; + + do { + i = set_cylinder_index(pi, i, cylinderindex, ev->time.seconds); + cylinderindex = get_cylinder_index(dive, ev); + ev = get_next_event(ev->next, "gaschange"); + } while (ev); + set_cylinder_index(pi, i, cylinderindex, ~0u); +} + +static void check_setpoint_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int i = 0; + pressure_t setpoint; + + setpoint.mbar = 0; + struct event *ev = get_next_event(dc->events, "SP change"); + + if (!ev) + return; + + do { + i = set_setpoint(pi, i, setpoint.mbar, ev->time.seconds); + setpoint.mbar = ev->value; + if (setpoint.mbar) + dc->divemode = CCR; + ev = get_next_event(ev->next, "SP change"); + } while (ev); + set_setpoint(pi, i, setpoint.mbar, ~0u); +} + + +struct plot_info calculate_max_limits_new(struct dive *dive, struct divecomputer *given_dc) +{ + struct divecomputer *dc = &(dive->dc); + bool seen = false; + static struct plot_info pi; + int maxdepth = dive->maxdepth.mm; + int maxtime = 0; + int maxpressure = 0, minpressure = INT_MAX; + int maxhr = 0, minhr = INT_MAX; + int mintemp = dive->mintemp.mkelvin; + int maxtemp = dive->maxtemp.mkelvin; + int cyl; + + /* Get the per-cylinder maximum pressure if they are manual */ + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { + int mbar = dive->cylinder[cyl].start.mbar; + if (mbar > maxpressure) + maxpressure = mbar; + if (mbar < minpressure) + minpressure = mbar; + } + + /* Then do all the samples from all the dive computers */ + do { + if (dc == given_dc) + seen = true; + int i = dc->samples; + int lastdepth = 0; + struct sample *s = dc->sample; + + while (--i >= 0) { + int depth = s->depth.mm; + int pressure = s->cylinderpressure.mbar; + int temperature = s->temperature.mkelvin; + int heartbeat = s->heartbeat; + + if (!mintemp && temperature < mintemp) + mintemp = temperature; + if (temperature > maxtemp) + maxtemp = temperature; + + if (pressure && pressure < minpressure) + minpressure = pressure; + if (pressure > maxpressure) + maxpressure = pressure; + if (heartbeat > maxhr) + maxhr = heartbeat; + if (heartbeat < minhr) + minhr = heartbeat; + + if (depth > maxdepth) + maxdepth = s->depth.mm; + if ((depth > SURFACE_THRESHOLD || lastdepth > SURFACE_THRESHOLD) && + s->time.seconds > maxtime) + maxtime = s->time.seconds; + lastdepth = depth; + s++; + } + dc = dc->next; + if (dc == NULL && !seen) { + dc = given_dc; + seen = true; + } + } while (dc != NULL); + + if (minpressure > maxpressure) + minpressure = 0; + if (minhr > maxhr) + minhr = 0; + + memset(&pi, 0, sizeof(pi)); + pi.maxdepth = maxdepth; + pi.maxtime = maxtime; + pi.maxpressure = maxpressure; + pi.minpressure = minpressure; + pi.minhr = minhr; + pi.maxhr = maxhr; + pi.mintemp = mintemp; + pi.maxtemp = maxtemp; + return pi; +} + +/* copy the previous entry (we know this exists), update time and depth + * and zero out the sensor pressure (since this is a synthetic entry) + * increment the entry pointer and the count of synthetic entries. */ +#define INSERT_ENTRY(_time, _depth, _sac) \ + *entry = entry[-1]; \ + entry->sec = _time; \ + entry->depth = _depth; \ + entry->running_sum = (entry - 1)->running_sum + (_time - (entry - 1)->sec) * (_depth + (entry - 1)->depth) / 2; \ + SENSOR_PRESSURE(entry) = 0; \ + entry->sac = _sac; \ + entry++; \ + idx++ + +struct plot_data *populate_plot_entries(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int idx, maxtime, nr, i; + int lastdepth, lasttime, lasttemp = 0; + struct plot_data *plot_data; + struct event *ev = dc->events; + + maxtime = pi->maxtime; + + /* + * We want to have a plot_info event at least every 10s (so "maxtime/10+1"), + * but samples could be more dense than that (so add in dc->samples). We also + * need to have one for every event (so count events and add that) and + * additionally we want two surface events around the whole thing (thus the + * additional 4). There is also one extra space for a final entry + * that has time > maxtime (because there can be surface samples + * past "maxtime" in the original sample data) + */ + nr = dc->samples + 6 + maxtime / 10 + count_events(dc); + plot_data = calloc(nr, sizeof(struct plot_data)); + pi->entry = plot_data; + if (!plot_data) + return NULL; + pi->nr = nr; + idx = 2; /* the two extra events at the start */ + + lastdepth = 0; + lasttime = 0; + /* skip events at time = 0 */ + while (ev && ev->time.seconds == 0) + ev = ev->next; + for (i = 0; i < dc->samples; i++) { + struct plot_data *entry = plot_data + idx; + struct sample *sample = dc->sample + i; + int time = sample->time.seconds; + int offset, delta; + int depth = sample->depth.mm; + int sac = sample->sac.mliter; + + /* Add intermediate plot entries if required */ + delta = time - lasttime; + if (delta <= 0) { + time = lasttime; + delta = 1; // avoid divide by 0 + } + for (offset = 10; offset < delta; offset += 10) { + if (lasttime + offset > maxtime) + break; + + /* Add events if they are between plot entries */ + while (ev && ev->time.seconds < lasttime + offset) { + INSERT_ENTRY(ev->time.seconds, interpolate(lastdepth, depth, ev->time.seconds - lasttime, delta), sac); + ev = ev->next; + } + + /* now insert the time interpolated entry */ + INSERT_ENTRY(lasttime + offset, interpolate(lastdepth, depth, offset, delta), sac); + + /* skip events that happened at this time */ + while (ev && ev->time.seconds == lasttime + offset) + ev = ev->next; + } + + /* Add events if they are between plot entries */ + while (ev && ev->time.seconds < time) { + INSERT_ENTRY(ev->time.seconds, interpolate(lastdepth, depth, ev->time.seconds - lasttime, delta), sac); + ev = ev->next; + } + + + entry->sec = time; + entry->depth = depth; + + entry->running_sum = (entry - 1)->running_sum + (time - (entry - 1)->sec) * (depth + (entry - 1)->depth) / 2; + entry->stopdepth = sample->stopdepth.mm; + entry->stoptime = sample->stoptime.seconds; + entry->ndl = sample->ndl.seconds; + entry->tts = sample->tts.seconds; + pi->has_ndl |= sample->ndl.seconds; + entry->in_deco = sample->in_deco; + entry->cns = sample->cns; + if (dc->divemode == CCR) { + entry->o2pressure.mbar = entry->o2setpoint.mbar = sample->setpoint.mbar; // for rebreathers + entry->o2sensor[0].mbar = sample->o2sensor[0].mbar; // for up to three rebreather O2 sensors + entry->o2sensor[1].mbar = sample->o2sensor[1].mbar; + entry->o2sensor[2].mbar = sample->o2sensor[2].mbar; + } else { + entry->pressures.o2 = sample->setpoint.mbar / 1000.0; + } + /* FIXME! sensor index -> cylinder index translation! */ + // entry->cylinderindex = sample->sensor; + SENSOR_PRESSURE(entry) = sample->cylinderpressure.mbar; + O2CYLINDER_PRESSURE(entry) = sample->o2cylinderpressure.mbar; + if (sample->temperature.mkelvin) + entry->temperature = lasttemp = sample->temperature.mkelvin; + else + entry->temperature = lasttemp; + entry->heartbeat = sample->heartbeat; + entry->bearing = sample->bearing.degrees; + entry->sac = sample->sac.mliter; + if (sample->rbt.seconds) + entry->rbt = sample->rbt.seconds; + /* skip events that happened at this time */ + while (ev && ev->time.seconds == time) + ev = ev->next; + lasttime = time; + lastdepth = depth; + idx++; + + if (time > maxtime) + break; + } + + /* Add two final surface events */ + plot_data[idx++].sec = lasttime + 1; + plot_data[idx++].sec = lasttime + 2; + pi->nr = idx; + + return plot_data; +} + +#undef INSERT_ENTRY + +static void populate_cylinder_pressure_data(int idx, int start, int end, struct plot_info *pi, bool o2flag) +{ + int i; + + /* First: check that none of the entries has sensor pressure for this cylinder index */ + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + if (entry->cylinderindex != idx && !o2flag) + continue; + if (CYLINDER_PRESSURE(o2flag, entry)) + return; + } + + /* Then: populate the first entry with the beginning cylinder pressure */ + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + if (entry->cylinderindex != idx && !o2flag) + continue; + if (o2flag) + O2CYLINDER_PRESSURE(entry) = start; + else + SENSOR_PRESSURE(entry) = start; + break; + } + + /* .. and the last entry with the ending cylinder pressure */ + for (i = pi->nr; --i >= 0; /* nothing */) { + struct plot_data *entry = pi->entry + i; + if (entry->cylinderindex != idx && !o2flag) + continue; + if (o2flag) + O2CYLINDER_PRESSURE(entry) = end; + else + SENSOR_PRESSURE(entry) = end; + break; + } +} + +/* + * Calculate the sac rate between the two plot entries 'first' and 'last'. + * + * Everything in between has a cylinder pressure, and it's all the same + * cylinder. + */ +static int sac_between(struct dive *dive, struct plot_data *first, struct plot_data *last) +{ + int airuse; + double pressuretime; + pressure_t a, b; + cylinder_t *cyl; + int duration; + + if (first == last) + return 0; + + /* Calculate air use - trivial */ + a.mbar = GET_PRESSURE(first); + b.mbar = GET_PRESSURE(last); + cyl = dive->cylinder + first->cylinderindex; + airuse = gas_volume(cyl, a) - gas_volume(cyl, b); + if (airuse <= 0) + return 0; + + /* Calculate depthpressure integrated over time */ + pressuretime = 0.0; + do { + int depth = (first[0].depth + first[1].depth) / 2; + int time = first[1].sec - first[0].sec; + double atm = depth_to_atm(depth, dive); + + pressuretime += atm * time; + } while (++first < last); + + /* Turn "atmseconds" into "atmminutes" */ + pressuretime /= 60; + + /* SAC = mliter per minute */ + return rint(airuse / pressuretime); +} + +/* + * Try to do the momentary sac rate for this entry, averaging over one + * minute. + */ +static void fill_sac(struct dive *dive, struct plot_info *pi, int idx) +{ + struct plot_data *entry = pi->entry + idx; + struct plot_data *first, *last; + int time; + + if (entry->sac) + return; + + if (!GET_PRESSURE(entry)) + return; + + /* + * Try to go back 30 seconds to get 'first'. + * Stop if the sensor changed, or if we went back too far. + */ + first = entry; + time = entry->sec - 30; + while (idx > 0) { + struct plot_data *prev = first-1; + if (prev->cylinderindex != first->cylinderindex) + break; + if (prev->depth < SURFACE_THRESHOLD && first->depth < SURFACE_THRESHOLD) + break; + if (prev->sec < time) + break; + if (!GET_PRESSURE(prev)) + break; + idx--; + first = prev; + } + + /* Now find an entry a minute after the first one */ + last = first; + time = first->sec + 60; + while (++idx < pi->nr) { + struct plot_data *next = last+1; + if (next->cylinderindex != last->cylinderindex) + break; + if (next->depth < SURFACE_THRESHOLD && last->depth < SURFACE_THRESHOLD) + break; + if (next->sec > time) + break; + if (!GET_PRESSURE(next)) + break; + last = next; + } + + /* Ok, now calculate the SAC between 'first' and 'last' */ + entry->sac = sac_between(dive, first, last); +} + +static void calculate_sac(struct dive *dive, struct plot_info *pi) +{ + int i = 0, last = 0; + struct plot_data *last_entry = NULL; + + for (i = 0; i < pi->nr; i++) + fill_sac(dive, pi, i); +} + +static void populate_secondary_sensor_data(struct divecomputer *dc, struct plot_info *pi) +{ + /* We should try to see if it has interesting pressure data here */ +} + +static void setup_gas_sensor_pressure(struct dive *dive, struct divecomputer *dc, struct plot_info *pi) +{ + int i; + struct divecomputer *secondary; + + /* First, populate the pressures with the manual cylinder data.. */ + for (i = 0; i < MAX_CYLINDERS; i++) { + cylinder_t *cyl = dive->cylinder + i; + int start = cyl->start.mbar ?: cyl->sample_start.mbar; + int end = cyl->end.mbar ?: cyl->sample_end.mbar; + + if (!start || !end) + continue; + + populate_cylinder_pressure_data(i, start, end, pi, dive->cylinder[i].cylinder_use == OXYGEN); + } + + /* + * Here, we should try to walk through all the dive computers, + * and try to see if they have sensor data different from the + * primary dive computer (dc). + */ + secondary = &dive->dc; + do { + if (secondary == dc) + continue; + populate_secondary_sensor_data(dc, pi); + } while ((secondary = secondary->next) != NULL); +} + +/* calculate DECO STOP / TTS / NDL */ +static void calculate_ndl_tts(struct plot_data *entry, struct dive *dive, double surface_pressure) +{ + /* FIXME: This should be configurable */ + /* ascent speed up to first deco stop */ + const int ascent_s_per_step = 1; + const int ascent_mm_per_step = 200; /* 12 m/min */ + /* ascent speed between deco stops */ + const int ascent_s_per_deco_step = 1; + const int ascent_mm_per_deco_step = 16; /* 1 m/min */ + /* how long time steps in deco calculations? */ + const int time_stepsize = 60; + const int deco_stepsize = 3000; + /* at what depth is the current deco-step? */ + int next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), + surface_pressure, dive, 1), deco_stepsize); + int ascent_depth = entry->depth; + /* at what time should we give up and say that we got enuff NDL? */ + int cylinderindex = entry->cylinderindex; + + /* If we don't have a ceiling yet, calculate ndl. Don't try to calculate + * a ndl for lower values than 3m it would take forever */ + if (next_stop == 0) { + if (entry->depth < 3000) { + entry->ndl = MAX_PROFILE_DECO; + return; + } + /* stop if the ndl is above max_ndl seconds, and call it plenty of time */ + while (entry->ndl_calc < MAX_PROFILE_DECO && deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, 1) <= 0) { + entry->ndl_calc += time_stepsize; + add_segment(depth_to_bar(entry->depth, dive), + &dive->cylinder[cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, prefs.bottomsac); + } + /* we don't need to calculate anything else */ + return; + } + + /* We are in deco */ + entry->in_deco_calc = true; + + /* Add segments for movement to stopdepth */ + for (; ascent_depth > next_stop; ascent_depth -= ascent_mm_per_step, entry->tts_calc += ascent_s_per_step) { + add_segment(depth_to_bar(ascent_depth, dive), + &dive->cylinder[cylinderindex].gasmix, ascent_s_per_step, entry->o2pressure.mbar, dive, prefs.decosac); + next_stop = ROUND_UP(deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(ascent_depth, dive)), surface_pressure, dive, 1), deco_stepsize); + } + ascent_depth = next_stop; + + /* And how long is the current deco-step? */ + entry->stoptime_calc = 0; + entry->stopdepth_calc = next_stop; + next_stop -= deco_stepsize; + + /* And how long is the total TTS */ + while (next_stop >= 0) { + /* save the time for the first stop to show in the graph */ + if (ascent_depth == entry->stopdepth_calc) + entry->stoptime_calc += time_stepsize; + + entry->tts_calc += time_stepsize; + if (entry->tts_calc > MAX_PROFILE_DECO) + break; + add_segment(depth_to_bar(ascent_depth, dive), + &dive->cylinder[cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, prefs.decosac); + + if (deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(ascent_depth,dive)), surface_pressure, dive, 1) <= next_stop) { + /* move to the next stop and add the travel between stops */ + for (; ascent_depth > next_stop; ascent_depth -= ascent_mm_per_deco_step, entry->tts_calc += ascent_s_per_deco_step) + add_segment(depth_to_bar(ascent_depth, dive), + &dive->cylinder[cylinderindex].gasmix, ascent_s_per_deco_step, entry->o2pressure.mbar, dive, prefs.decosac); + ascent_depth = next_stop; + next_stop -= deco_stepsize; + } + } +} + +/* Let's try to do some deco calculations. + */ +void calculate_deco_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool print_mode) +{ + int i; + double surface_pressure = (dc->surface_pressure.mbar ? dc->surface_pressure.mbar : get_surface_pressure_in_mbar(dive, true)) / 1000.0; + int last_ndl_tts_calc_time = 0; + for (i = 1; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + int j, t0 = (entry - 1)->sec, t1 = entry->sec; + int time_stepsize = 20; + + entry->ambpressure = depth_to_bar(entry->depth, dive); + entry->gfline = MAX((double)prefs.gflow, (entry->ambpressure - surface_pressure) / (gf_low_pressure_this_dive - surface_pressure) * + (prefs.gflow - prefs.gfhigh) + + prefs.gfhigh) * + (100.0 - AMB_PERCENTAGE) / 100.0 + AMB_PERCENTAGE; + if (t0 > t1) { + fprintf(stderr, "non-monotonous dive stamps %d %d\n", t0, t1); + int xchg = t1; + t1 = t0; + t0 = xchg; + } + if (t0 != t1 && t1 - t0 < time_stepsize) + time_stepsize = t1 - t0; + for (j = t0 + time_stepsize; j <= t1; j += time_stepsize) { + int depth = interpolate(entry[-1].depth, entry[0].depth, j - t0, t1 - t0); + add_segment(depth_to_bar(depth, dive), + &dive->cylinder[entry->cylinderindex].gasmix, time_stepsize, entry->o2pressure.mbar, dive, entry->sac); + if ((t1 - j < time_stepsize) && (j < t1)) + time_stepsize = t1 - j; + } + if (t0 == t1) + entry->ceiling = (entry - 1)->ceiling; + else + entry->ceiling = deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, !prefs.calcceiling3m); + for (j = 0; j < 16; j++) { + double m_value = buehlmann_inertgas_a[j] + entry->ambpressure / buehlmann_inertgas_b[j]; + entry->ceilings[j] = deco_allowed_depth(tolerated_by_tissue[j], surface_pressure, dive, 1); + entry->percentages[j] = tissue_inertgas_saturation[j] < entry->ambpressure ? + tissue_inertgas_saturation[j] / entry->ambpressure * AMB_PERCENTAGE : + AMB_PERCENTAGE + (tissue_inertgas_saturation[j] - entry->ambpressure) / (m_value - entry->ambpressure) * (100.0 - AMB_PERCENTAGE); + } + + /* should we do more calculations? + * We don't for print-mode because this info doesn't show up there */ + if (prefs.calcndltts && !print_mode) { + /* only calculate ndl/tts on every 30 seconds */ + if ((entry->sec - last_ndl_tts_calc_time) < 30) { + struct plot_data *prev_entry = (entry - 1); + entry->stoptime_calc = prev_entry->stoptime_calc; + entry->stopdepth_calc = prev_entry->stopdepth_calc; + entry->tts_calc = prev_entry->tts_calc; + entry->ndl_calc = prev_entry->ndl_calc; + continue; + } + last_ndl_tts_calc_time = entry->sec; + + /* We are going to mess up deco state, so store it for later restore */ + char *cache_data = NULL; + cache_deco_state(&cache_data); + calculate_ndl_tts(entry, dive, surface_pressure); + /* Restore "real" deco state for next real time step */ + restore_deco_state(cache_data); + free(cache_data); + } + } +#if DECO_CALC_DEBUG & 1 + dump_tissues(); +#endif +} + + +/* Function calculate_ccr_po2: This function takes information from one plot_data structure (i.e. one point on + * the dive profile), containing the oxygen sensor values of a CCR system and, for that plot_data structure, + * calculates the po2 value from the sensor data. Several rules are applied, depending on how many o2 sensors + * there are and the differences among the readings from these sensors. + */ +static int calculate_ccr_po2(struct plot_data *entry, struct divecomputer *dc) +{ + int sump = 0, minp = 999999, maxp = -999999; + int diff_limit = 100; // The limit beyond which O2 sensor differences are considered significant (default = 100 mbar) + int i, np = 0; + + for (i = 0; i < dc->no_o2sensors; i++) + if (entry->o2sensor[i].mbar) { // Valid reading + ++np; + sump += entry->o2sensor[i].mbar; + minp = MIN(minp, entry->o2sensor[i].mbar); + maxp = MAX(maxp, entry->o2sensor[i].mbar); + } + switch (np) { + case 0: // Uhoh + return entry->o2pressure.mbar; + case 1: // Return what we have + return sump; + case 2: // Take the average + return sump / 2; + case 3: // Voting logic + if (2 * maxp - sump + minp < diff_limit) { // Upper difference acceptable... + if (2 * minp - sump + maxp) // ...and lower difference acceptable + return sump / 3; + else + return (sump - minp) / 2; + } else { + if (2 * minp - sump + maxp) // ...but lower difference acceptable + return (sump - maxp) / 2; + else + return sump / 3; + } + default: // This should not happen + assert(np <= 3); + return 0; + } +} + +static void calculate_gas_information_new(struct dive *dive, struct plot_info *pi) +{ + int i; + double amb_pressure; + + for (i = 1; i < pi->nr; i++) { + int fn2, fhe; + struct plot_data *entry = pi->entry + i; + int cylinderindex = entry->cylinderindex; + + amb_pressure = depth_to_bar(entry->depth, dive); + + fill_pressures(&entry->pressures, amb_pressure, &dive->cylinder[cylinderindex].gasmix, entry->o2pressure.mbar / 1000.0, dive->dc.divemode); + fn2 = (int)(1000.0 * entry->pressures.n2 / amb_pressure); + fhe = (int)(1000.0 * entry->pressures.he / amb_pressure); + + /* Calculate MOD, EAD, END and EADD based on partial pressures calculated before + * so there is no difference in calculating between OC and CC + * END takes O₂ + N₂ (air) into account ("Narcotic" for trimix dives) + * EAD just uses N₂ ("Air" for nitrox dives) */ + pressure_t modpO2 = { .mbar = (int)(prefs.modpO2 * 1000) }; + entry->mod = (double)gas_mod(&dive->cylinder[cylinderindex].gasmix, modpO2, dive, 1).mm; + entry->end = (entry->depth + 10000) * (1000 - fhe) / 1000.0 - 10000; + entry->ead = (entry->depth + 10000) * fn2 / (double)N2_IN_AIR - 10000; + entry->eadd = (entry->depth + 10000) * + (entry->pressures.o2 / amb_pressure * O2_DENSITY + + entry->pressures.n2 / amb_pressure * N2_DENSITY + + entry->pressures.he / amb_pressure * HE_DENSITY) / + (O2_IN_AIR * O2_DENSITY + N2_IN_AIR * N2_DENSITY) * 1000 - 10000; + if (entry->mod < 0) + entry->mod = 0; + if (entry->ead < 0) + entry->ead = 0; + if (entry->end < 0) + entry->end = 0; + if (entry->eadd < 0) + entry->eadd = 0; + } +} + +void fill_o2_values(struct divecomputer *dc, struct plot_info *pi, struct dive *dive) +/* In the samples from each dive computer, there may be uninitialised oxygen + * sensor or setpoint values, e.g. when events were inserted into the dive log + * or if the dive computer does not report o2 values with every sample. But + * for drawing the profile a complete series of valid o2 pressure values is + * required. This function takes the oxygen sensor data and setpoint values + * from the structures of plotinfo and replaces the zero values with their + * last known values so that the oxygen sensor data are complete and ready + * for plotting. This function called by: create_plot_info_new() */ +{ + int i, j; + pressure_t last_sensor[3], o2pressure; + pressure_t amb_pressure; + + for (i = 0; i < pi->nr; i++) { + struct plot_data *entry = pi->entry + i; + + if (dc->divemode == CCR) { + if (i == 0) { // For 1st iteration, initialise the last_sensor values + for (j = 0; j < dc->no_o2sensors; j++) + last_sensor[j].mbar = pi->entry->o2sensor[j].mbar; + } else { // Now re-insert the missing oxygen pressure values + for (j = 0; j < dc->no_o2sensors; j++) + if (entry->o2sensor[j].mbar) + last_sensor[j].mbar = entry->o2sensor[j].mbar; + else + entry->o2sensor[j].mbar = last_sensor[j].mbar; + } // having initialised the empty o2 sensor values for this point on the profile, + amb_pressure.mbar = depth_to_mbar(entry->depth, dive); + o2pressure.mbar = calculate_ccr_po2(entry, dc); // ...calculate the po2 based on the sensor data + entry->o2pressure.mbar = MIN(o2pressure.mbar, amb_pressure.mbar); + } else { + entry->o2pressure.mbar = 0; // initialise po2 to zero for dctype = OC + } + } +} + +#ifdef DEBUG_GAS +/* A CCR debug function that writes the cylinder pressure and the oxygen values to the file debug_print_profiledata.dat: + * Called in create_plot_info_new() + */ +static void debug_print_profiledata(struct plot_info *pi) +{ + FILE *f1; + struct plot_data *entry; + int i; + if (!(f1 = fopen("debug_print_profiledata.dat", "w"))) { + printf("File open error for: debug_print_profiledata.dat\n"); + } else { + fprintf(f1, "id t1 gas gasint t2 t3 dil dilint t4 t5 setpoint sensor1 sensor2 sensor3 t6 po2 fo2\n"); + for (i = 0; i < pi->nr; i++) { + entry = pi->entry + i; + fprintf(f1, "%d gas=%8d %8d ; dil=%8d %8d ; o2_sp= %d %d %d %d PO2= %f\n", i, SENSOR_PRESSURE(entry), + INTERPOLATED_PRESSURE(entry), O2CYLINDER_PRESSURE(entry), INTERPOLATED_O2CYLINDER_PRESSURE(entry), + entry->o2pressure.mbar, entry->o2sensor[0].mbar, entry->o2sensor[1].mbar, entry->o2sensor[2].mbar, entry->pressures.o2); + } + fclose(f1); + } +} +#endif + +/* + * Create a plot-info with smoothing and ranged min/max + * + * This also makes sure that we have extra empty events on both + * sides, so that you can do end-points without having to worry + * about it. + */ +void create_plot_info_new(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool fast) +{ + int o2, he, o2max; + init_decompression(dive); + /* Create the new plot data */ + free((void *)last_pi_entry_new); + + get_dive_gas(dive, &o2, &he, &o2max); + if (dc->divemode == FREEDIVE){ + pi->dive_type = FREEDIVE; + } else if (he > 0) { + pi->dive_type = TRIMIX; + } else { + if (o2) + pi->dive_type = NITROX; + else + pi->dive_type = AIR; + } + + last_pi_entry_new = populate_plot_entries(dive, dc, pi); + + check_gas_change_events(dive, dc, pi); /* Populate the gas index from the gas change events */ + check_setpoint_events(dive, dc, pi); /* Populate setpoints */ + setup_gas_sensor_pressure(dive, dc, pi); /* Try to populate our gas pressure knowledge */ + if (!fast) { + populate_pressure_information(dive, dc, pi, false); /* .. calculate missing pressure entries for all gasses except o2 */ + if (dc->divemode == CCR) /* For CCR dives.. */ + populate_pressure_information(dive, dc, pi, true); /* .. calculate missing o2 gas pressure entries */ + } + fill_o2_values(dc, pi, dive); /* .. and insert the O2 sensor data having 0 values. */ + calculate_sac(dive, pi); /* Calculate sac */ + calculate_deco_information(dive, dc, pi, false); /* and ceiling information, using gradient factor values in Preferences) */ + calculate_gas_information_new(dive, pi); /* Calculate gas partial pressures */ + +#ifdef DEBUG_GAS + debug_print_profiledata(pi); +#endif + + pi->meandepth = dive->dc.meandepth.mm; + analyze_plot_info(pi); +} + +struct divecomputer *select_dc(struct dive *dive) +{ + unsigned int max = number_of_computers(dive); + unsigned int i = dc_number; + + /* Reset 'dc_number' if we've switched dives and it is now out of range */ + if (i >= max) + dc_number = i = 0; + + return get_dive_dc(dive, i); +} + +static void plot_string(struct plot_info *pi, struct plot_data *entry, struct membuffer *b, bool has_ndl) +{ + int pressurevalue, mod, ead, end, eadd; + const char *depth_unit, *pressure_unit, *temp_unit, *vertical_speed_unit; + double depthvalue, tempvalue, speedvalue, sacvalue; + int decimals; + const char *unit; + + depthvalue = get_depth_units(entry->depth, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "@: %d:%02d\nD: %.1f%s\n"), FRACTION(entry->sec, 60), depthvalue, depth_unit); + if (GET_PRESSURE(entry)) { + pressurevalue = get_pressure_units(GET_PRESSURE(entry), &pressure_unit); + put_format(b, translate("gettextFromC", "P: %d%s\n"), pressurevalue, pressure_unit); + } + if (entry->temperature) { + tempvalue = get_temp_units(entry->temperature, &temp_unit); + put_format(b, translate("gettextFromC", "T: %.1f%s\n"), tempvalue, temp_unit); + } + speedvalue = get_vertical_speed_units(abs(entry->speed), NULL, &vertical_speed_unit); + /* Ascending speeds are positive, descending are negative */ + if (entry->speed > 0) + speedvalue *= -1; + put_format(b, translate("gettextFromC", "V: %.1f%s\n"), speedvalue, vertical_speed_unit); + sacvalue = get_volume_units(entry->sac, &decimals, &unit); + if (entry->sac && prefs.show_sac) + put_format(b, translate("gettextFromC", "SAC: %.*f%s/min\n"), decimals, sacvalue, unit); + if (entry->cns) + put_format(b, translate("gettextFromC", "CNS: %u%%\n"), entry->cns); + if (prefs.pp_graphs.po2) + put_format(b, translate("gettextFromC", "pO%s: %.2fbar\n"), UTF8_SUBSCRIPT_2, entry->pressures.o2); + if (prefs.pp_graphs.pn2) + put_format(b, translate("gettextFromC", "pN%s: %.2fbar\n"), UTF8_SUBSCRIPT_2, entry->pressures.n2); + if (prefs.pp_graphs.phe) + put_format(b, translate("gettextFromC", "pHe: %.2fbar\n"), entry->pressures.he); + if (prefs.mod) { + mod = (int)get_depth_units(entry->mod, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "MOD: %d%s\n"), mod, depth_unit); + } + eadd = (int)get_depth_units(entry->eadd, NULL, &depth_unit); + if (prefs.ead) { + switch (pi->dive_type) { + case NITROX: + ead = (int)get_depth_units(entry->ead, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "EAD: %d%s\nEADD: %d%s\n"), ead, depth_unit, eadd, depth_unit); + break; + case TRIMIX: + end = (int)get_depth_units(entry->end, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "END: %d%s\nEADD: %d%s\n"), end, depth_unit, eadd, depth_unit); + break; + case AIR: + case FREEDIVING: + /* nothing */ + break; + } + } + if (entry->stopdepth) { + depthvalue = get_depth_units(entry->stopdepth, NULL, &depth_unit); + if (entry->ndl) { + /* this is a safety stop as we still have ndl */ + if (entry->stoptime) + put_format(b, translate("gettextFromC", "Safetystop: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), + depthvalue, depth_unit); + else + put_format(b, translate("gettextFromC", "Safetystop: unkn time @ %.0f%s\n"), + depthvalue, depth_unit); + } else { + /* actual deco stop */ + if (entry->stoptime) + put_format(b, translate("gettextFromC", "Deco: %umin @ %.0f%s\n"), DIV_UP(entry->stoptime, 60), + depthvalue, depth_unit); + else + put_format(b, translate("gettextFromC", "Deco: unkn time @ %.0f%s\n"), + depthvalue, depth_unit); + } + } else if (entry->in_deco) { + put_string(b, translate("gettextFromC", "In deco\n")); + } else if (has_ndl) { + put_format(b, translate("gettextFromC", "NDL: %umin\n"), DIV_UP(entry->ndl, 60)); + } + if (entry->tts) + put_format(b, translate("gettextFromC", "TTS: %umin\n"), DIV_UP(entry->tts, 60)); + if (entry->stopdepth_calc && entry->stoptime_calc) { + depthvalue = get_depth_units(entry->stopdepth_calc, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "Deco: %umin @ %.0f%s (calc)\n"), DIV_UP(entry->stoptime_calc, 60), + depthvalue, depth_unit); + } else if (entry->in_deco_calc) { + /* This means that we have no NDL left, + * and we have no deco stop, + * so if we just accend to the surface slowly + * (ascent_mm_per_step / ascent_s_per_step) + * everything will be ok. */ + put_string(b, translate("gettextFromC", "In deco (calc)\n")); + } else if (prefs.calcndltts && entry->ndl_calc != 0) { + if(entry->ndl_calc < MAX_PROFILE_DECO) + put_format(b, translate("gettextFromC", "NDL: %umin (calc)\n"), DIV_UP(entry->ndl_calc, 60)); + else + put_format(b, "%s", translate("gettextFromC", "NDL: >2h (calc)\n")); + } + if (entry->tts_calc) { + if (entry->tts_calc < MAX_PROFILE_DECO) + put_format(b, translate("gettextFromC", "TTS: %umin (calc)\n"), DIV_UP(entry->tts_calc, 60)); + else + put_format(b, "%s", translate("gettextFromC", "TTS: >2h (calc)\n")); + } + if (entry->rbt) + put_format(b, translate("gettextFromC", "RBT: %umin\n"), DIV_UP(entry->rbt, 60)); + if (entry->ceiling) { + depthvalue = get_depth_units(entry->ceiling, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "Calculated ceiling %.0f%s\n"), depthvalue, depth_unit); + if (prefs.calcalltissues) { + int k; + for (k = 0; k < 16; k++) { + if (entry->ceilings[k]) { + depthvalue = get_depth_units(entry->ceilings[k], NULL, &depth_unit); + put_format(b, translate("gettextFromC", "Tissue %.0fmin: %.1f%s\n"), buehlmann_N2_t_halflife[k], depthvalue, depth_unit); + } + } + } + } + if (entry->heartbeat && prefs.hrgraph) + put_format(b, translate("gettextFromC", "heartbeat: %d\n"), entry->heartbeat); + if (entry->bearing) + put_format(b, translate("gettextFromC", "bearing: %d\n"), entry->bearing); + if (entry->running_sum) { + depthvalue = get_depth_units(entry->running_sum / entry->sec, NULL, &depth_unit); + put_format(b, translate("gettextFromC", "mean depth to here %.1f%s\n"), depthvalue, depth_unit); + } + + strip_mb(b); +} + +struct plot_data *get_plot_details_new(struct plot_info *pi, int time, struct membuffer *mb) +{ + struct plot_data *entry = NULL; + int i; + + for (i = 0; i < pi->nr; i++) { + entry = pi->entry + i; + if (entry->sec >= time) + break; + } + if (entry) + plot_string(pi, entry, mb, pi->has_ndl); + return (entry); +} + +/* Compare two plot_data entries and writes the results into a string */ +void compare_samples(struct plot_data *e1, struct plot_data *e2, char *buf, int bufsize, int sum) +{ + struct plot_data *start, *stop, *data; + const char *depth_unit, *pressure_unit, *vertical_speed_unit; + char *buf2 = malloc(bufsize); + int avg_speed, max_asc_speed, max_desc_speed; + int delta_depth, avg_depth, max_depth, min_depth; + int bar_used, last_pressure, pressurevalue; + int count, last_sec, delta_time; + + double depthvalue, speedvalue; + + if (bufsize > 0) + buf[0] = '\0'; + if (e1 == NULL || e2 == NULL) { + free(buf2); + return; + } + + if (e1->sec < e2->sec) { + start = e1; + stop = e2; + } else if (e1->sec > e2->sec) { + start = e2; + stop = e1; + } else { + free(buf2); + return; + } + count = 0; + avg_speed = 0; + max_asc_speed = 0; + max_desc_speed = 0; + + delta_depth = abs(start->depth - stop->depth); + delta_time = abs(start->sec - stop->sec); + avg_depth = 0; + max_depth = 0; + min_depth = INT_MAX; + bar_used = 0; + + last_sec = start->sec; + last_pressure = GET_PRESSURE(start); + + data = start; + while (data != stop) { + data = start + count; + if (sum) + avg_speed += abs(data->speed) * (data->sec - last_sec); + else + avg_speed += data->speed * (data->sec - last_sec); + avg_depth += data->depth * (data->sec - last_sec); + + if (data->speed > max_desc_speed) + max_desc_speed = data->speed; + if (data->speed < max_asc_speed) + max_asc_speed = data->speed; + + if (data->depth < min_depth) + min_depth = data->depth; + if (data->depth > max_depth) + max_depth = data->depth; + /* Try to detect gas changes */ + if (GET_PRESSURE(data) < last_pressure + 2000) + bar_used += last_pressure - GET_PRESSURE(data); + + count += 1; + last_sec = data->sec; + last_pressure = GET_PRESSURE(data); + } + avg_depth /= stop->sec - start->sec; + avg_speed /= stop->sec - start->sec; + + snprintf(buf, bufsize, translate("gettextFromC", "%sT: %d:%02d min"), UTF8_DELTA, delta_time / 60, delta_time % 60); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(delta_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_DELTA, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(min_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_DOWNWARDS_ARROW, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(max_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s"), buf2, UTF8_UPWARDS_ARROW, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + depthvalue = get_depth_units(avg_depth, NULL, &depth_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sD:%.1f%s\n"), buf2, UTF8_AVERAGE, depthvalue, depth_unit); + memcpy(buf2, buf, bufsize); + + speedvalue = get_vertical_speed_units(abs(max_desc_speed), NULL, &vertical_speed_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s%sV:%.2f%s"), buf2, UTF8_DOWNWARDS_ARROW, speedvalue, vertical_speed_unit); + memcpy(buf2, buf, bufsize); + + speedvalue = get_vertical_speed_units(abs(max_asc_speed), NULL, &vertical_speed_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sV:%.2f%s"), buf2, UTF8_UPWARDS_ARROW, speedvalue, vertical_speed_unit); + memcpy(buf2, buf, bufsize); + + speedvalue = get_vertical_speed_units(abs(avg_speed), NULL, &vertical_speed_unit); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sV:%.2f%s"), buf2, UTF8_AVERAGE, speedvalue, vertical_speed_unit); + memcpy(buf2, buf, bufsize); + + /* Only print if gas has been used */ + if (bar_used) { + pressurevalue = get_pressure_units(bar_used, &pressure_unit); + memcpy(buf2, buf, bufsize); + snprintf(buf, bufsize, translate("gettextFromC", "%s %sP:%d %s"), buf2, UTF8_DELTA, pressurevalue, pressure_unit); + } + + free(buf2); +} diff --git a/subsurface-core/profile.h b/subsurface-core/profile.h new file mode 100644 index 000000000..a6dbfcf5f --- /dev/null +++ b/subsurface-core/profile.h @@ -0,0 +1,109 @@ +#ifndef PROFILE_H +#define PROFILE_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + STABLE, + SLOW, + MODERATE, + FAST, + CRAZY +} velocity_t; + +struct membuffer; +struct divecomputer; +struct plot_info; +struct plot_data { + unsigned int in_deco : 1; + int cylinderindex; + int sec; + /* pressure[0] is sensor cylinder pressure [when CCR, the pressure of the diluent cylinder] + * pressure[1] is interpolated cylinder pressure */ + int pressure[2]; + /* o2pressure[0] is o2 cylinder pressure [CCR] + * o2pressure[1] is interpolated o2 cylinder pressure [CCR] */ + int o2cylinderpressure[2]; + int temperature; + /* Depth info */ + int depth; + int ceiling; + int ceilings[16]; + int percentages[16]; + int ndl; + int tts; + int rbt; + int stoptime; + int stopdepth; + int cns; + int smoothed; + int sac; + int running_sum; + struct gas_pressures pressures; + pressure_t o2pressure; // for rebreathers, this is consensus measured po2, or setpoint otherwise. 0 for OC. + pressure_t o2sensor[3]; //for rebreathers with up to 3 PO2 sensors + pressure_t o2setpoint; + double mod, ead, end, eadd; + velocity_t velocity; + int speed; + struct plot_data *min[3]; + struct plot_data *max[3]; + int avg[3]; + /* values calculated by us */ + unsigned int in_deco_calc : 1; + int ndl_calc; + int tts_calc; + int stoptime_calc; + int stopdepth_calc; + int pressure_time; + int heartbeat; + int bearing; + double ambpressure; + double gfline; +}; + +struct ev_select { + char *ev_name; + bool plot_ev; +}; + +struct plot_info calculate_max_limits_new(struct dive *dive, struct divecomputer *given_dc); +void compare_samples(struct plot_data *e1, struct plot_data *e2, char *buf, int bufsize, int sum); +struct plot_data *populate_plot_entries(struct dive *dive, struct divecomputer *dc, struct plot_info *pi); +struct plot_info *analyze_plot_info(struct plot_info *pi); +void create_plot_info_new(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool fast); +void calculate_deco_information(struct dive *dive, struct divecomputer *dc, struct plot_info *pi, bool print_mode); +struct plot_data *get_plot_details_new(struct plot_info *pi, int time, struct membuffer *); + +/* + * When showing dive profiles, we scale things to the + * current dive. However, we don't scale past less than + * 30 minutes or 90 ft, just so that small dives show + * up as such unless zoom is enabled. + * We also need to add 180 seconds at the end so the min/max + * plots correctly + */ +int get_maxtime(struct plot_info *pi); + +/* get the maximum depth to which we want to plot + * take into account the additional verical space needed to plot + * partial pressure graphs */ +int get_maxdepth(struct plot_info *pi); + +#define SENSOR_PR 0 +#define INTERPOLATED_PR 1 +#define SENSOR_PRESSURE(_entry) (_entry)->pressure[SENSOR_PR] +#define O2CYLINDER_PRESSURE(_entry) (_entry)->o2cylinderpressure[SENSOR_PR] +#define CYLINDER_PRESSURE(_o2, _entry) (_o2 ? O2CYLINDER_PRESSURE(_entry) : SENSOR_PRESSURE(_entry)) +#define INTERPOLATED_PRESSURE(_entry) (_entry)->pressure[INTERPOLATED_PR] +#define INTERPOLATED_O2CYLINDER_PRESSURE(_entry) (_entry)->o2cylinderpressure[INTERPOLATED_PR] +#define GET_PRESSURE(_entry) (SENSOR_PRESSURE(_entry) ? SENSOR_PRESSURE(_entry) : INTERPOLATED_PRESSURE(_entry)) +#define GET_O2CYLINDER_PRESSURE(_entry) (O2CYLINDER_PRESSURE(_entry) ? O2CYLINDER_PRESSURE(_entry) : INTERPOLATED_O2CYLINDER_PRESSURE(_entry)) +#define SAC_WINDOW 45 /* sliding window in seconds for current SAC calculation */ + +#ifdef __cplusplus +} +#endif +#endif // PROFILE_H diff --git a/subsurface-core/qt-gui.h b/subsurface-core/qt-gui.h new file mode 100644 index 000000000..ca038b145 --- /dev/null +++ b/subsurface-core/qt-gui.h @@ -0,0 +1,17 @@ +#ifndef QT_GUI_H +#define QT_GUI_H + +#include + +void init_qt_late(); +void init_ui(); + +void run_ui(); +void exit_ui(); + +#if defined(SUBSURFACE_MOBILE) +#include +extern QObject *qqWindowObject; +#endif + +#endif // QT_GUI_H diff --git a/subsurface-core/qt-init.cpp b/subsurface-core/qt-init.cpp new file mode 100644 index 000000000..b52dfd970 --- /dev/null +++ b/subsurface-core/qt-init.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include "helpers.h" + +void init_qt_late() +{ + QApplication *application = qApp; + // tell Qt to use system proxies + // note: on Linux, "system" == "environment variables" + QNetworkProxyFactory::setUseSystemConfiguration(true); + + // for Win32 and Qt5 we try to set the locale codec to UTF-8. + // this makes QFile::encodeName() work. +#ifdef Q_OS_WIN + QTextCodec::setCodecForLocale(QTextCodec::codecForMib(106)); +#endif + + QCoreApplication::setOrganizationName("Subsurface"); + QCoreApplication::setOrganizationDomain("subsurface.hohndel.org"); + QCoreApplication::setApplicationName("Subsurface"); + // find plugins installed in the application directory (without this SVGs don't work on Windows) + QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); + QLocale loc; + QString uiLang = uiLanguage(&loc); + QLocale::setDefault(loc); + + // we don't have translations for English - if we don't check for this + // Qt will proceed to load the second language in preference order - not what we want + // on Linux this tends to be en-US, but on the Mac it's just en + if (!uiLang.startsWith("en") || uiLang.startsWith("en-GB")) { + qtTranslator = new QTranslator; + if (qtTranslator->load(loc, "qt", "_", QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + application->installTranslator(qtTranslator); + } else { + qDebug() << "can't find Qt localization for locale" << uiLang << "searching in" << QLibraryInfo::location(QLibraryInfo::TranslationsPath); + } + ssrfTranslator = new QTranslator; + if (ssrfTranslator->load(loc, "subsurface", "_") || + ssrfTranslator->load(loc, "subsurface", "_", getSubsurfaceDataPath("translations")) || + ssrfTranslator->load(loc, "subsurface", "_", getSubsurfaceDataPath("../translations"))) { + application->installTranslator(ssrfTranslator); + } else { + qDebug() << "can't find Subsurface localization for locale" << uiLang; + } + } +} diff --git a/subsurface-core/qthelper.cpp b/subsurface-core/qthelper.cpp new file mode 100644 index 000000000..a12d25333 --- /dev/null +++ b/subsurface-core/qthelper.cpp @@ -0,0 +1,1653 @@ +#include "qthelper.h" +#include "helpers.h" +#include "gettextfromc.h" +#include "statistics.h" +#include "membuffer.h" +#include "subsurfacesysinfo.h" +#include "version.h" +#include "divecomputer.h" +#include "time.h" +#include "gettextfromc.h" +#include +#include +#include "file.h" +#include "prefs-macros.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +const char *existing_filename; +static QString shortDateFormat; +static QString dateFormat; +static QString timeFormat; +static QLocale loc; + +#define translate(_context, arg) trGettext(arg) +static const QString DEGREE_SIGNS("dD" UTF8_DEGREE); + +Dive::Dive() : + m_number(-1), + dive(NULL) +{ +} + +Dive::~Dive() +{ +} + +int Dive::number() const +{ + return m_number; +} + +int Dive::id() const +{ + return m_id; +} + +QString Dive::date() const +{ + return m_date; +} + +timestamp_t Dive::timestamp() const +{ + return m_timestamp; +} + +QString Dive::time() const +{ + return m_time; +} + +QString Dive::location() const +{ + return m_location; +} + +QString Dive::duration() const +{ + return m_duration; +} + +QString Dive::depth() const +{ + return m_depth; +} + +QString Dive::divemaster() const +{ + return m_divemaster; +} + +QString Dive::buddy() const +{ + return m_buddy; +} + +QString Dive::airTemp() const +{ + return m_airTemp; +} + +QString Dive::waterTemp() const +{ + return m_waterTemp; +} + +QString Dive::notes() const +{ + return m_notes; +} + +QString Dive::tags() const +{ + return m_tags; +} + +QString Dive::gas() const +{ + return m_gas; +} + +QString Dive::sac() const +{ + return m_sac; +} + +QString Dive::weight() const +{ + return m_weight; +} + +QString Dive::suit() const +{ + return m_suit; +} + +QString Dive::cylinder() const +{ + return m_cylinder; +} + +QString Dive::trip() const +{ + return m_trip; +} + +int Dive::rating() const +{ + return m_rating; +} + +void Dive::put_divemaster() +{ + if (!dive->divemaster) + m_divemaster = "--"; + else + m_divemaster = dive->divemaster; +} + +void Dive::put_date_time() +{ + QDateTime localTime = QDateTime::fromTime_t(dive->when - gettimezoneoffset(displayed_dive.when)); + localTime.setTimeSpec(Qt::UTC); + m_date = localTime.date().toString(dateFormat); + m_time = localTime.time().toString(timeFormat); +} + +void Dive::put_timestamp() +{ + m_timestamp = dive->when; +} + +void Dive::put_location() +{ + m_location = QString::fromUtf8(get_dive_location(dive)); + if (m_location.isEmpty()) { + m_location = "--"; + } +} + +void Dive::put_depth() +{ + m_depth = get_depth_string(dive->dc.maxdepth.mm, true, true); +} + +void Dive::put_duration() +{ + m_duration = get_dive_duration_string(dive->duration.seconds, QObject::tr("h:"), QObject::tr("min")); +} + +void Dive::put_buddy() +{ + if (!dive->buddy) + m_buddy = "--"; + else + m_buddy = dive->buddy; +} + +void Dive::put_temp() +{ + m_airTemp = get_temperature_string(dive->airtemp, true); + m_waterTemp = get_temperature_string(dive->watertemp, true); + if (m_airTemp.isEmpty()) { + m_airTemp = "--"; + } + if (m_waterTemp.isEmpty()) { + m_waterTemp = "--"; + } +} + +void Dive::put_notes() +{ + if (same_string(dive->dc.model, "planned dive")) { + QTextDocument notes; + notes.setHtml(QString::fromUtf8(dive->notes)); + m_notes = notes.toPlainText(); + } else { + m_notes = QString::fromUtf8(dive->notes); + } + if (m_notes.isEmpty()) { + m_notes = "--"; + } +} + +void Dive::put_tags() +{ + char buffer[256]; + taglist_get_tagstring(dive->tag_list, buffer, 256); + m_tags = QString(buffer); +} + +void Dive::put_gas() +{ + int added = 0; + QString gas, gases; + for (int i = 0; i < MAX_CYLINDERS; i++) { + if (!is_cylinder_used(dive, i)) + continue; + gas = dive->cylinder[i].type.description; + gas += QString(!gas.isEmpty() ? " " : "") + gasname(&dive->cylinder[i].gasmix); + // if has a description and if such gas is not already present + if (!gas.isEmpty() && gases.indexOf(gas) == -1) { + if (added > 0) + gases += QString(" / "); + gases += gas; + added++; + } + } + m_gas = gases; +} + +void Dive::put_sac() +{ + if (dive->sac) { + const char *unit; + int decimal; + double value = get_volume_units(dive->sac, &decimal, &unit); + m_sac = QString::number(value, 'f', decimal).append(unit); + } +} + +void Dive::put_weight() +{ + weight_t tw = { total_weight(dive) }; + m_weight = weight_string(tw.grams); +} + +void Dive::put_suit() +{ + m_suit = QString(dive->suit); +} + +void Dive::put_cylinder() +{ + m_cylinder = QString(dive->cylinder[0].type.description); +} + +void Dive::put_trip() +{ + dive_trip *trip = dive->divetrip; + if (trip) { + m_trip = QString(trip->location); + } +} + +QString weight_string(int weight_in_grams) +{ + QString str; + if (get_units()->weight == units::KG) { + int gr = weight_in_grams % 1000; + int kg = weight_in_grams / 1000; + if (kg >= 20.0) { + str = QString("0"); + } else { + str = QString("%1.%2").arg(kg).arg((unsigned)(gr) / 100); + } + } else { + double lbs = grams_to_lbs(weight_in_grams); + str = QString("%1").arg(lbs, 0, 'f', lbs >= 40.0 ? 0 : 1); + } + return (str); +} + +QString distance_string(int distanceInMeters) +{ + QString str; + if(get_units()->length == units::METERS) { + if (distanceInMeters >= 1000) + str = QString(translate("gettextFromC", "%1km")).arg(distanceInMeters / 1000); + else + str = QString(translate("gettextFromC", "%1m")).arg(distanceInMeters); + } else { + double miles = m_to_mile(distanceInMeters); + if (miles >= 1.0) + str = QString(translate("gettextFromC", "%1mi")).arg((int)miles); + else + str = QString(translate("gettextFromC", "%1yd")).arg((int)(miles * 1760)); + } + return str; +} + +extern "C" const char *printGPSCoords(int lat, int lon) +{ + unsigned int latdeg, londeg; + unsigned int latmin, lonmin; + double latsec, lonsec; + QString lath, lonh, result; + + if (!lat && !lon) + return strdup(""); + + if (prefs.coordinates_traditional) { + lath = lat >= 0 ? translate("gettextFromC", "N") : translate("gettextFromC", "S"); + lonh = lon >= 0 ? translate("gettextFromC", "E") : translate("gettextFromC", "W"); + lat = abs(lat); + lon = abs(lon); + latdeg = lat / 1000000U; + londeg = lon / 1000000U; + latmin = (lat % 1000000U) * 60U; + lonmin = (lon % 1000000U) * 60U; + latsec = (latmin % 1000000) * 60; + lonsec = (lonmin % 1000000) * 60; + result.sprintf("%u%s%02d\'%06.3f\"%s %u%s%02d\'%06.3f\"%s", + latdeg, UTF8_DEGREE, latmin / 1000000, latsec / 1000000, lath.toUtf8().data(), + londeg, UTF8_DEGREE, lonmin / 1000000, lonsec / 1000000, lonh.toUtf8().data()); + } else { + result.sprintf("%f %f", (double) lat / 1000000.0, (double) lon / 1000000.0); + } + return strdup(result.toUtf8().data()); +} + +/** +* Try to parse in a generic manner a coordinate. +*/ +static bool parseCoord(const QString& txt, int& pos, const QString& positives, + const QString& negatives, const QString& others, + double& value) +{ + bool numberDefined = false, degreesDefined = false, + minutesDefined = false, secondsDefined = false; + double number = 0.0; + int posBeforeNumber = pos; + int sign = 0; + value = 0.0; + while (pos < txt.size()) { + if (txt[pos].isDigit()) { + if (numberDefined) + return false; + QRegExp numberRe("(\\d+(?:[\\.,]\\d+)?).*"); + if (!numberRe.exactMatch(txt.mid(pos))) + return false; + number = numberRe.cap(1).toDouble(); + numberDefined = true; + posBeforeNumber = pos; + pos += numberRe.cap(1).size() - 1; + } else if (positives.indexOf(txt[pos]) >= 0) { + if (sign != 0) + return false; + sign = 1; + if (degreesDefined || numberDefined) { + //sign after the degrees => + //at the end of the coordinate + ++pos; + break; + } + } else if (negatives.indexOf(txt[pos]) >= 0) { + if (sign != 0) { + if (others.indexOf(txt[pos]) >= 0) + //special case for the '-' sign => next coordinate + break; + return false; + } + sign = -1; + if (degreesDefined || numberDefined) { + //sign after the degrees => + //at the end of the coordinate + ++pos; + break; + } + } else if (others.indexOf(txt[pos]) >= 0) { + //we are at the next coordinate. + break; + } else if (DEGREE_SIGNS.indexOf(txt[pos]) >= 0 || + (txt[pos].isSpace() && !degreesDefined && numberDefined)) { + if (!numberDefined) + return false; + if (degreesDefined) { + //next coordinate => need to put back the number + pos = posBeforeNumber; + numberDefined = false; + break; + } + value += number; + numberDefined = false; + degreesDefined = true; + } else if (txt[pos] == '\'' || (txt[pos].isSpace() && !minutesDefined && numberDefined)) { + if (!numberDefined || minutesDefined) + return false; + value += number / 60.0; + numberDefined = false; + minutesDefined = true; + } else if (txt[pos] == '"' || (txt[pos].isSpace() && !secondsDefined && numberDefined)) { + if (!numberDefined || secondsDefined) + return false; + value += number / 3600.0; + numberDefined = false; + secondsDefined = true; + } else if ((numberDefined || minutesDefined || secondsDefined) && + (txt[pos] == ',' || txt[pos] == ';')) { + // next coordinate coming up + // eat the ',' and any subsequent white space + while (txt[++pos].isSpace()) + /* nothing */ ; + break; + } else { + return false; + } + ++pos; + } + if (!degreesDefined && numberDefined) { + value = number; //just a single number => degrees + } else if (!minutesDefined && numberDefined) { + value += number / 60.0; + } else if (!secondsDefined && numberDefined) { + value += number / 3600.0; + } else if (numberDefined) { + return false; + } + if (sign == -1) value *= -1.0; + return true; +} + +/** +* Parse special coordinate formats that cannot be handled by parseCoord. +*/ +static bool parseSpecialCoords(const QString& txt, double& latitude, double& longitude) { + QRegExp xmlFormat("(-?\\d+(?:\\.\\d+)?),?\\s+(-?\\d+(?:\\.\\d+)?)"); + if (xmlFormat.exactMatch(txt)) { + latitude = xmlFormat.cap(1).toDouble(); + longitude = xmlFormat.cap(2).toDouble(); + return true; + } + return false; +} + +bool parseGpsText(const QString &gps_text, double *latitude, double *longitude) +{ + static const QString POS_LAT = QString("+N") + translate("gettextFromC", "N"); + static const QString NEG_LAT = QString("-S") + translate("gettextFromC", "S"); + static const QString POS_LON = QString("+E") + translate("gettextFromC", "E"); + static const QString NEG_LON = QString("-W") + translate("gettextFromC", "W"); + + //remove the useless spaces (but keep the ones separating numbers) + static const QRegExp SPACE_CLEANER("\\s*([" + POS_LAT + NEG_LAT + POS_LON + + NEG_LON + DEGREE_SIGNS + "'\"\\s])\\s*"); + const QString normalized = gps_text.trimmed().toUpper().replace(SPACE_CLEANER, "\\1"); + + if (normalized.isEmpty()) { + *latitude = 0.0; + *longitude = 0.0; + return true; + } + if (parseSpecialCoords(normalized, *latitude, *longitude)) + return true; + int pos = 0; + return parseCoord(normalized, pos, POS_LAT, NEG_LAT, POS_LON + NEG_LON, *latitude) && + parseCoord(normalized, pos, POS_LON, NEG_LON, "", *longitude) && + pos == normalized.size(); +} + +#if 0 // we'll need something like this for the dive site management, eventually +bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out) +{ + double latitude, longitude; + int latudeg, longudeg; + bool ignore; + bool *parsed = parsed_out ?: &ignore; + *parsed = true; + + /* if we have a master and the dive's gps address is different from it, + * don't change the dive */ + if (master && (master->latitude.udeg != dive->latitude.udeg || + master->longitude.udeg != dive->longitude.udeg)) + return false; + + if (!(*parsed = parseGpsText(gps_text, &latitude, &longitude))) + return false; + + latudeg = rint(1000000 * latitude); + longudeg = rint(1000000 * longitude); + + /* if dive gps didn't change, nothing changed */ + if (dive->latitude.udeg == latudeg && dive->longitude.udeg == longudeg) + return false; + /* ok, update the dive and mark things changed */ + dive->latitude.udeg = latudeg; + dive->longitude.udeg = longudeg; + return true; +} +#endif + +QList getDivesInTrip(dive_trip_t *trip) +{ + QList ret; + int i; + struct dive *d; + for_each_dive (i, d) { + if (d->divetrip == trip) { + ret.push_back(get_divenr(d)); + } + } + return ret; +} + +// we need this to be uniq, but also make sure +// it doesn't change during the life time of a Subsurface session +// oh, and it has no meaning whatsoever - that's why we have the +// silly initial number and increment by 3 :-) +int dive_getUniqID(struct dive *d) +{ + static QSet ids; + static int maxId = 83529; + + int id = d->id; + if (id) { + if (!ids.contains(id)) { + qDebug() << "WTF - only I am allowed to create IDs"; + ids.insert(id); + } + return id; + } + maxId += 3; + id = maxId; + Q_ASSERT(!ids.contains(id)); + ids.insert(id); + return id; +} + + +static xmlDocPtr get_stylesheet_doc(const xmlChar *uri, xmlDictPtr, int, void *, xsltLoadType) +{ + QFile f(QLatin1String(":/xslt/") + (const char *)uri); + if (!f.open(QIODevice::ReadOnly)) { + if (verbose > 0) { + qDebug() << "cannot open stylesheet" << QLatin1String(":/xslt/") + (const char *)uri; + return NULL; + } + } + /* Load and parse the data */ + QByteArray source = f.readAll(); + + xmlDocPtr doc = xmlParseMemory(source, source.size()); + return doc; +} + +extern "C" xsltStylesheetPtr get_stylesheet(const char *name) +{ + // this needs to be done only once, but doesn't hurt to run every time + xsltSetLoaderFunc(get_stylesheet_doc); + + // get main document: + xmlDocPtr doc = get_stylesheet_doc((const xmlChar *)name, NULL, 0, NULL, XSLT_LOAD_START); + if (!doc) + return NULL; + + // xsltSetGenericErrorFunc(stderr, NULL); + xsltStylesheetPtr xslt = xsltParseStylesheetDoc(doc); + if (!xslt) { + xmlFreeDoc(doc); + return NULL; + } + + return xslt; +} + + +extern "C" timestamp_t picture_get_timestamp(char *filename) +{ + EXIFInfo exif; + memblock mem; + int retval; + + // filename might not be the actual filename, so let's go via the hash. + if (readfile(localFilePath(QString(filename)).toUtf8().data(), &mem) <= 0) + return 0; + retval = exif.parseFrom((const unsigned char *)mem.buffer, (unsigned)mem.size); + free(mem.buffer); + if (retval != PARSE_EXIF_SUCCESS) + return 0; + return exif.epoch(); +} + +extern "C" char *move_away(const char *old_path) +{ + if (verbose > 1) + qDebug() << "move away" << old_path; + QDir oldDir(old_path); + QDir newDir; + QString newPath; + int i = 0; + do { + newPath = QString(old_path) + QString(".%1").arg(++i); + newDir.setPath(newPath); + } while(newDir.exists()); + if (verbose > 1) + qDebug() << "renaming to" << newPath; + if (!oldDir.rename(old_path, newPath)) { + if (verbose) + qDebug() << "rename of" << old_path << "to" << newPath << "failed"; + // this next one we only try on Windows... if we are on a different platform + // we simply give up and return an empty string +#ifdef WIN32 + if (subsurface_dir_rename(old_path, qPrintable(newPath)) == 0) +#endif + return strdup(""); + } + return strdup(qPrintable(newPath)); +} + +extern "C" char *get_file_name(const char *fileName) +{ + QFileInfo fileInfo(fileName); + return strdup(fileInfo.fileName().toUtf8()); +} + +extern "C" void copy_image_and_overwrite(const char *cfileName, const char *path, const char *cnewName) +{ + QString fileName(cfileName); + QString newName(path); + newName += cnewName; + QFile file(newName); + if (file.exists()) + file.remove(); + if (!QFile::copy(fileName, newName)) + qDebug() << "copy of" << fileName << "to" << newName << "failed"; +} + +extern "C" bool string_sequence_contains(const char *string_sequence, const char *text) +{ + if (same_string(text, "") || same_string(string_sequence, "")) + return false; + + QString stringSequence(string_sequence); + QStringList strings = stringSequence.split(",", QString::SkipEmptyParts); + Q_FOREACH (const QString& string, strings) { + if (string.trimmed().compare(QString(text).trimmed(), Qt::CaseInsensitive) == 0) + return true; + } + return false; +} + +static bool lessThan(const QPair &a, const QPair &b) +{ + return a.second < b.second; +} + +void selectedDivesGasUsed(QVector > &gasUsedOrdered) +{ + int i, j; + struct dive *d; + QMap gasUsed; + for_each_dive (i, d) { + if (!d->selected) + continue; + volume_t diveGases[MAX_CYLINDERS] = {}; + get_gas_used(d, diveGases); + for (j = 0; j < MAX_CYLINDERS; j++) + if (diveGases[j].mliter) { + QString gasName = gasname(&d->cylinder[j].gasmix); + gasUsed[gasName] += diveGases[j].mliter; + } + } + Q_FOREACH(const QString& gas, gasUsed.keys()) { + gasUsedOrdered.append(qMakePair(gas, gasUsed[gas])); + } + qSort(gasUsedOrdered.begin(), gasUsedOrdered.end(), lessThan); +} + +QString getUserAgent() +{ + QString arch; + // fill in the system data - use ':' as separator + // replace all other ':' with ' ' so that this is easy to parse + QString userAgent = QString("Subsurface:%1:").arg(subsurface_version()); + userAgent.append(SubsurfaceSysInfo::prettyOsName().replace(':', ' ') + ":"); + arch = SubsurfaceSysInfo::buildCpuArchitecture().replace(':', ' '); + userAgent.append(arch); + if (arch == "i386") + userAgent.append("/" + SubsurfaceSysInfo::currentCpuArchitecture()); + userAgent.append(":" + uiLanguage(NULL)); + return userAgent; + +} + +QString uiLanguage(QLocale *callerLoc) +{ + QSettings s; + s.beginGroup("Language"); + + if (!s.value("UseSystemLanguage", true).toBool()) { + loc = QLocale(s.value("UiLanguage", QLocale().uiLanguages().first()).toString()); + } else { + loc = QLocale(QLocale().uiLanguages().first()); + } + + QString uiLang = loc.uiLanguages().first(); + s.endGroup(); + + // there's a stupid Qt bug on MacOS where uiLanguages doesn't give us the country info + if (!uiLang.contains('-') && uiLang != loc.bcp47Name()) { + QLocale loc2(loc.bcp47Name()); + loc = loc2; + uiLang = loc2.uiLanguages().first(); + } + if (callerLoc) + *callerLoc = loc; + + // the short format is fine + // the long format uses long weekday and month names, so replace those with the short ones + // for time we don't want the time zone designator and don't want leading zeroes on the hours + shortDateFormat = loc.dateFormat(QLocale::ShortFormat); + dateFormat = loc.dateFormat(QLocale::LongFormat); + dateFormat.replace("dddd,", "ddd").replace("dddd", "ddd").replace("MMMM", "MMM"); + // special hack for Swedish as our switching from long weekday names to short weekday names + // messes things up there + dateFormat.replace("'en' 'den' d:'e'", " d"); + timeFormat = loc.timeFormat(); + timeFormat.replace("(t)", "").replace(" t", "").replace("t", "").replace("hh", "h").replace("HH", "H").replace("'kl'.", ""); + timeFormat.replace(".ss", "").replace(":ss", "").replace("ss", ""); + return uiLang; +} + +QLocale getLocale() +{ + return loc; +} + +QString getDateFormat() +{ + return dateFormat; +} +void set_filename(const char *filename, bool force) +{ + if (!force && existing_filename) + return; + free((void *)existing_filename); + if (filename) + existing_filename = strdup(filename); + else + existing_filename = NULL; +} + +const QString get_dc_nickname(const char *model, uint32_t deviceid) +{ + const DiveComputerNode *existNode = dcList.getExact(model, deviceid); + + if (existNode && !existNode->nickName.isEmpty()) + return existNode->nickName; + else + return model; +} + +QString get_depth_string(int mm, bool showunit, bool showdecimal) +{ + if (prefs.units.length == units::METERS) { + double meters = mm / 1000.0; + return QString("%1%2").arg(meters, 0, 'f', (showdecimal && meters < 20.0) ? 1 : 0).arg(showunit ? translate("gettextFromC", "m") : ""); + } else { + double feet = mm_to_feet(mm); + return QString("%1%2").arg(feet, 0, 'f', 0).arg(showunit ? translate("gettextFromC", "ft") : ""); + } +} + +QString get_depth_string(depth_t depth, bool showunit, bool showdecimal) +{ + return get_depth_string(depth.mm, showunit, showdecimal); +} + +QString get_depth_unit() +{ + if (prefs.units.length == units::METERS) + return QString("%1").arg(translate("gettextFromC", "m")); + else + return QString("%1").arg(translate("gettextFromC", "ft")); +} + +QString get_weight_string(weight_t weight, bool showunit) +{ + QString str = weight_string(weight.grams); + if (get_units()->weight == units::KG) { + str = QString("%1%2").arg(str).arg(showunit ? translate("gettextFromC", "kg") : ""); + } else { + str = QString("%1%2").arg(str).arg(showunit ? translate("gettextFromC", "lbs") : ""); + } + return (str); +} + +QString get_weight_unit() +{ + if (prefs.units.weight == units::KG) + return QString("%1").arg(translate("gettextFromC", "kg")); + else + return QString("%1").arg(translate("gettextFromC", "lbs")); +} + +/* these methods retrieve used gas per cylinder */ +static unsigned start_pressure(cylinder_t *cyl) +{ + return cyl->start.mbar ?: cyl->sample_start.mbar; +} + +static unsigned end_pressure(cylinder_t *cyl) +{ + return cyl->end.mbar ?: cyl->sample_end.mbar; +} + +QString get_cylinder_used_gas_string(cylinder_t *cyl, bool showunit) +{ + int decimals; + const char *unit; + double gas_usage; + /* Get the cylinder gas use in mbar */ + gas_usage = start_pressure(cyl) - end_pressure(cyl); + /* Can we turn it into a volume? */ + if (cyl->type.size.mliter) { + gas_usage = bar_to_atm(gas_usage / 1000); + gas_usage *= cyl->type.size.mliter; + gas_usage = get_volume_units(gas_usage, &decimals, &unit); + } else { + gas_usage = get_pressure_units(gas_usage, &unit); + decimals = 0; + } + // translate("gettextFromC","%.*f %s" + return QString("%1 %2").arg(gas_usage, 0, 'f', decimals).arg(showunit ? unit : ""); +} + +QString get_temperature_string(temperature_t temp, bool showunit) +{ + if (temp.mkelvin == 0) { + return ""; //temperature not defined + } else if (prefs.units.temperature == units::CELSIUS) { + double celsius = mkelvin_to_C(temp.mkelvin); + return QString("%1%2%3").arg(celsius, 0, 'f', 1).arg(showunit ? (UTF8_DEGREE) : "").arg(showunit ? translate("gettextFromC", "C") : ""); + } else { + double fahrenheit = mkelvin_to_F(temp.mkelvin); + return QString("%1%2%3").arg(fahrenheit, 0, 'f', 1).arg(showunit ? (UTF8_DEGREE) : "").arg(showunit ? translate("gettextFromC", "F") : ""); + } +} + +QString get_temp_unit() +{ + if (prefs.units.temperature == units::CELSIUS) + return QString(UTF8_DEGREE "C"); + else + return QString(UTF8_DEGREE "F"); +} + +QString get_volume_string(volume_t volume, bool showunit, int mbar) +{ + const char *unit; + int decimals; + double value = get_volume_units(volume.mliter, &decimals, &unit); + if (mbar) { + // we are showing a tank size + // fix the weird imperial way of denominating size and provide + // reasonable number of decimals + if (prefs.units.volume == units::CUFT) + value *= bar_to_atm(mbar / 1000.0); + decimals = (value > 20.0) ? 0 : (value > 2.0) ? 1 : 2; + } + return QString("%1%2").arg(value, 0, 'f', decimals).arg(showunit ? unit : ""); +} + +QString get_volume_unit() +{ + const char *unit; + (void) get_volume_units(0, NULL, &unit); + return QString(unit); +} + +QString get_pressure_string(pressure_t pressure, bool showunit) +{ + if (prefs.units.pressure == units::BAR) { + double bar = pressure.mbar / 1000.0; + return QString("%1%2").arg(bar, 0, 'f', 1).arg(showunit ? translate("gettextFromC", "bar") : ""); + } else { + double psi = mbar_to_PSI(pressure.mbar); + return QString("%1%2").arg(psi, 0, 'f', 0).arg(showunit ? translate("gettextFromC", "psi") : ""); + } +} + +QString getSubsurfaceDataPath(QString folderToFind) +{ + QString execdir; + QDir folder; + + // first check if we are running in the build dir, so the path that we + // are looking for is just a subdirectory of the execution path; + // this also works on Windows as there we install the dirs + // under the application path + execdir = QCoreApplication::applicationDirPath(); + folder = QDir(execdir.append(QDir::separator()).append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + + // next check for the Linux typical $(prefix)/share/subsurface + execdir = QCoreApplication::applicationDirPath(); + if (execdir.contains("bin")) { + folder = QDir(execdir.replace("bin", "share/subsurface/").append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + } + // then look for the usual locations on a Mac + execdir = QCoreApplication::applicationDirPath(); + folder = QDir(execdir.append("/../Resources/share/").append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + execdir = QCoreApplication::applicationDirPath(); + folder = QDir(execdir.append("/../Resources/").append(folderToFind)); + if (folder.exists()) + return folder.absolutePath(); + return QString(""); +} + +static const char *printing_templates = "printing_templates"; + +QString getPrintingTemplatePathUser() +{ + static QString path = QString(); + if (path.isEmpty()) + path = QString(system_default_directory()) + QDir::separator() + QString(printing_templates); + return path; +} + +QString getPrintingTemplatePathBundle() +{ + static QString path = QString(); + if (path.isEmpty()) + path = getSubsurfaceDataPath(printing_templates); + return path; +} + +void copyPath(QString src, QString dst) +{ + QDir dir(src); + if (!dir.exists()) + return; + foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + QString dst_path = dst + QDir::separator() + d; + dir.mkpath(dst_path); + copyPath(src + QDir::separator() + d, dst_path); + } + foreach (QString f, dir.entryList(QDir::Files)) + QFile::copy(src + QDir::separator() + f, dst + QDir::separator() + f); +} + +int gettimezoneoffset(timestamp_t when) +{ + QDateTime dt1, dt2; + if (when == 0) + dt1 = QDateTime::currentDateTime(); + else + dt1 = QDateTime::fromMSecsSinceEpoch(when * 1000); + dt2 = dt1.toUTC(); + dt1.setTimeSpec(Qt::UTC); + return dt2.secsTo(dt1); +} + +int parseTemperatureToMkelvin(const QString &text) +{ + int mkelvin; + QString numOnly = text; + numOnly.replace(",", ".").remove(QRegExp("[^-0-9.]")); + if (numOnly.isEmpty()) + return 0; + double number = numOnly.toDouble(); + switch (prefs.units.temperature) { + case units::CELSIUS: + mkelvin = C_to_mkelvin(number); + break; + case units::FAHRENHEIT: + mkelvin = F_to_mkelvin(number); + break; + default: + mkelvin = 0; + } + return mkelvin; +} + +QString get_dive_duration_string(timestamp_t when, QString hourText, QString minutesText) +{ + int hrs, mins; + mins = (when + 59) / 60; + hrs = mins / 60; + mins -= hrs * 60; + + QString displayTime; + if (hrs) + displayTime = QString("%1%2%3%4").arg(hrs).arg(hourText).arg(mins, 2, 10, QChar('0')).arg(minutesText); + else + displayTime = QString("%1%2").arg(mins).arg(minutesText); + + return displayTime; +} + +QString get_dive_date_string(timestamp_t when) +{ + QDateTime ts; + ts.setMSecsSinceEpoch(when * 1000L); + return loc.toString(ts.toUTC(), dateFormat + " " + timeFormat); +} + +QString get_short_dive_date_string(timestamp_t when) +{ + QDateTime ts; + ts.setMSecsSinceEpoch(when * 1000L); + return loc.toString(ts.toUTC(), shortDateFormat + " " + timeFormat); +} + +const char *get_dive_date_c_string(timestamp_t when) +{ + QString text = get_dive_date_string(when); + return strdup(text.toUtf8().data()); +} + +bool is_same_day(timestamp_t trip_when, timestamp_t dive_when) +{ + static timestamp_t twhen = (timestamp_t) 0; + static struct tm tmt; + struct tm tmd; + + utc_mkdate(dive_when, &tmd); + + if (twhen != trip_when) { + twhen = trip_when; + utc_mkdate(twhen, &tmt); + } + + return ((tmd.tm_mday == tmt.tm_mday) && (tmd.tm_mon == tmt.tm_mon) && (tmd.tm_year == tmt.tm_year)); +} + +QString get_trip_date_string(timestamp_t when, int nr, bool getday) +{ + struct tm tm; + utc_mkdate(when, &tm); + QDateTime localTime = QDateTime::fromTime_t(when); + localTime.setTimeSpec(Qt::UTC); + QString ret ; + + QString suffix = " " + QObject::tr("(%n dive(s))", "", nr); + if (getday) { + ret = localTime.date().toString(dateFormat) + suffix; + } else { + ret = localTime.date().toString("MMM yy") + suffix; + } + return ret; + +} + +extern "C" void reverseGeoLookup(degrees_t latitude, degrees_t longitude, uint32_t uuid) +{ + QNetworkRequest request; + QNetworkAccessManager *rgl = new QNetworkAccessManager(); + request.setUrl(QString("http://open.mapquestapi.com/nominatim/v1/reverse.php?format=json&accept-language=%1&lat=%2&lon=%3") + .arg(uiLanguage(NULL)).arg(latitude.udeg / 1000000.0).arg(longitude.udeg / 1000000.0)); + request.setRawHeader("Accept", "text/json"); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + QNetworkReply *reply = rgl->get(request); + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + QJsonParseError errorObject; + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &errorObject); + if (errorObject.error != QJsonParseError::NoError) { + qDebug() << errorObject.errorString(); + } else { + QJsonObject obj = jsonDoc.object(); + QJsonObject address = obj.value("address").toObject(); + qDebug() << "found country:" << address.value("country").toString(); + struct dive_site *ds = get_dive_site_by_uuid(uuid); + ds->notes = add_to_string(ds->notes, "countrytag: %s", address.value("country").toString().toUtf8().data()); + } +} + +QHash hashOf; +QMutex hashOfMutex; +QHash localFilenameOf; + +extern "C" char * hashstring(char * filename) +{ + return hashOf[QString(filename)].toHex().data(); +} + +const QString hashfile_name() +{ + return QString(system_default_directory()).append("/hashes"); +} + +extern "C" char *hashfile_name_string() +{ + return strdup(hashfile_name().toUtf8().data()); +} + +void read_hashes() +{ + QFile hashfile(hashfile_name()); + if (hashfile.open(QIODevice::ReadOnly)) { + QDataStream stream(&hashfile); + stream >> localFilenameOf; + stream >> hashOf; + hashfile.close(); + } +} + +void write_hashes() +{ + QSaveFile hashfile(hashfile_name()); + if (hashfile.open(QIODevice::WriteOnly)) { + QDataStream stream(&hashfile); + stream << localFilenameOf; + stream << hashOf; + hashfile.commit(); + } else { + qDebug() << "cannot open" << hashfile.fileName(); + } +} + +void add_hash(const QString filename, QByteArray hash) +{ + QMutexLocker locker(&hashOfMutex); + hashOf[filename] = hash; + localFilenameOf[hash] = filename; +} + +QByteArray hashFile(const QString filename) +{ + QCryptographicHash hash(QCryptographicHash::Sha1); + QFile imagefile(filename); + if (imagefile.exists() && imagefile.open(QIODevice::ReadOnly)) { + hash.addData(&imagefile); + add_hash(filename, hash.result()); + return hash.result(); + } else { + return QByteArray(); + } +} + +void learnHash(struct picture *picture, QByteArray hash) +{ + if (picture->hash) + free(picture->hash); + QMutexLocker locker(&hashOfMutex); + hashOf[QString(picture->filename)] = hash; + picture->hash = strdup(hash.toHex()); +} + +QString localFilePath(const QString originalFilename) +{ + if (hashOf.contains(originalFilename) && localFilenameOf.contains(hashOf[originalFilename])) + return localFilenameOf[hashOf[originalFilename]]; + else + return originalFilename; +} + +QString fileFromHash(char *hash) +{ + return localFilenameOf[QByteArray::fromHex(hash)]; +} + +void updateHash(struct picture *picture) { + QByteArray hash = hashFile(fileFromHash(picture->hash)); + QMutexLocker locker(&hashOfMutex); + hashOf[QString(picture->filename)] = hash; + char *old = picture->hash; + picture->hash = strdup(hash.toHex()); + free(old); +} + +void hashPicture(struct picture *picture) +{ + char *oldHash = copy_string(picture->hash); + learnHash(picture, hashFile(QString(picture->filename))); + if (!same_string(picture->hash, "") && !same_string(picture->hash, oldHash)) + mark_divelist_changed((true)); + free(oldHash); +} + +extern "C" void cache_picture(struct picture *picture) +{ + QString filename = picture->filename; + if (!hashOf.contains(filename)) + QtConcurrent::run(hashPicture, picture); +} + +void learnImages(const QDir dir, int max_recursions, bool recursed) +{ + QDir current(dir); + QStringList filters, files; + + if (max_recursions) { + foreach (QString dirname, dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs)) { + learnImages(QDir(dir.filePath(dirname)), max_recursions - 1, true); + } + } + + foreach (QString format, QImageReader::supportedImageFormats()) { + filters.append(QString("*.").append(format)); + } + + foreach (QString file, dir.entryList(filters, QDir::Files)) { + files.append(dir.absoluteFilePath(file)); + } + + QtConcurrent::blockingMap(files, hashFile); +} + +extern "C" const char *local_file_path(struct picture *picture) +{ + QString hashString = picture->hash; + if (hashString.isEmpty()) { + QByteArray hash = hashFile(picture->filename); + free(picture->hash); + picture->hash = strdup(hash.toHex().data()); + } + QString localFileName = fileFromHash(picture->hash); + if (localFileName.isEmpty()) + localFileName = picture->filename; + return strdup(qPrintable(localFileName)); +} + +extern "C" bool picture_exists(struct picture *picture) +{ + QString localFilename = fileFromHash(picture->hash); + QByteArray hash = hashFile(localFilename); + return same_string(hash.toHex().data(), picture->hash); +} + +const QString picturedir() +{ + return QString(system_default_directory()).append("/picturedata/"); +} + +extern "C" char *picturedir_string() +{ + return strdup(picturedir().toUtf8().data()); +} + +/* when we get a picture from git storage (local or remote) and can't find the picture + * based on its hash, we create a local copy with the hash as filename and the appropriate + * suffix */ +extern "C" void savePictureLocal(struct picture *picture, const char *data, int len) +{ + QString dirname = picturedir(); + QDir localPictureDir(dirname); + localPictureDir.mkpath(dirname); + QString suffix(picture->filename); + suffix.replace(QRegularExpression(".*\\."), ""); + QString filename(dirname + picture->hash + "." + suffix); + QSaveFile out(filename); + if (out.open(QIODevice::WriteOnly)) { + out.write(data, len); + out.commit(); + add_hash(filename, QByteArray::fromHex(picture->hash)); + } +} + +extern "C" void picture_load_exif_data(struct picture *p) +{ + EXIFInfo exif; + memblock mem; + + if (readfile(localFilePath(QString(p->filename)).toUtf8().data(), &mem) <= 0) + goto picture_load_exit; + if (exif.parseFrom((const unsigned char *)mem.buffer, (unsigned)mem.size) != PARSE_EXIF_SUCCESS) + goto picture_load_exit; + p->longitude.udeg= lrint(1000000.0 * exif.GeoLocation.Longitude); + p->latitude.udeg = lrint(1000000.0 * exif.GeoLocation.Latitude); + +picture_load_exit: + free(mem.buffer); + return; +} + +QString get_gas_string(struct gasmix gas) +{ + uint o2 = (get_o2(&gas) + 5) / 10, he = (get_he(&gas) + 5) / 10; + QString result = gasmix_is_air(&gas) ? QObject::tr("AIR") : he == 0 ? (o2 == 100 ? QObject::tr("OXYGEN") : QString("EAN%1").arg(o2, 2, 10, QChar('0'))) : QString("%1/%2").arg(o2).arg(he); + return result; +} + +QString get_divepoint_gas_string(const divedatapoint &p) +{ + return get_gas_string(p.gasmix); +} + +weight_t string_to_weight(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_kg = QObject::tr("kg"); + QString local_lbs = QObject::tr("lbs"); + weight_t weight; + + if (rest.startsWith("kg") || rest.startsWith(local_kg)) + goto kg; + // using just "lb" instead of "lbs" is intentional - some people might enter the singular + if (rest.startsWith("lb") || rest.startsWith(local_lbs)) + goto lbs; + if (prefs.units.weight == prefs.units.LBS) + goto lbs; +kg: + weight.grams = rint(value * 1000); + return weight; +lbs: + weight.grams = lbs_to_grams(value); + return weight; +} + +depth_t string_to_depth(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_ft = QObject::tr("ft"); + QString local_m = QObject::tr("m"); + depth_t depth; + + if (rest.startsWith("m") || rest.startsWith(local_m)) + goto m; + if (rest.startsWith("ft") || rest.startsWith(local_ft)) + goto ft; + if (prefs.units.length == prefs.units.FEET) + goto ft; +m: + depth.mm = rint(value * 1000); + return depth; +ft: + depth.mm = feet_to_mm(value); + return depth; +} + +pressure_t string_to_pressure(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_psi = QObject::tr("psi"); + QString local_bar = QObject::tr("bar"); + pressure_t pressure; + + if (rest.startsWith("bar") || rest.startsWith(local_bar)) + goto bar; + if (rest.startsWith("psi") || rest.startsWith(local_psi)) + goto psi; + if (prefs.units.pressure == prefs.units.PSI) + goto psi; +bar: + pressure.mbar = rint(value * 1000); + return pressure; +psi: + pressure.mbar = psi_to_mbar(value); + return pressure; +} + +volume_t string_to_volume(const char *str, pressure_t workp) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + QString rest = QString(end).trimmed(); + QString local_l = QObject::tr("l"); + QString local_cuft = QObject::tr("cuft"); + volume_t volume; + + if (rest.startsWith("l") || rest.startsWith("ℓ") || rest.startsWith(local_l)) + goto l; + if (rest.startsWith("cuft") || rest.startsWith(local_cuft)) + goto cuft; + /* + * If we don't have explicit units, and there is no working + * pressure, we're going to assume "liter" even in imperial + * measurements. + */ + if (!workp.mbar) + goto l; + if (prefs.units.volume == prefs.units.LITER) + goto l; +cuft: + if (workp.mbar) + value /= bar_to_atm(workp.mbar / 1000.0); + value = cuft_to_l(value); +l: + volume.mliter = rint(value * 1000); + return volume; +} + +fraction_t string_to_fraction(const char *str) +{ + const char *end; + double value = strtod_flags(str, &end, 0); + fraction_t fraction; + + fraction.permille = rint(value * 10); + return fraction; +} + +int getCloudURL(QString &filename) +{ + QString email = QString(prefs.cloud_storage_email); + email.replace(QRegularExpression("[^a-zA-Z0-9@._+-]"), ""); + if (email.isEmpty() || same_string(prefs.cloud_storage_password, "")) + return report_error("Please configure Cloud storage email and password in the preferences"); + if (email != prefs.cloud_storage_email_encoded) { + free(prefs.cloud_storage_email_encoded); + prefs.cloud_storage_email_encoded = strdup(qPrintable(email)); + } + filename = QString(QString(prefs.cloud_git_url) + "/%1[%1]").arg(email); + if (verbose) + qDebug() << "cloud URL set as" << filename; + return 0; +} + +extern "C" char *cloud_url() +{ + QString filename; + getCloudURL(filename); + return strdup(filename.toUtf8().data()); +} + +void loadPreferences() +{ + QSettings s; + QVariant v; + + s.beginGroup("Units"); + if (s.value("unit_system").toString() == "metric") { + prefs.unit_system = METRIC; + prefs.units = SI_units; + } else if (s.value("unit_system").toString() == "imperial") { + prefs.unit_system = IMPERIAL; + prefs.units = IMPERIAL_units; + } else { + prefs.unit_system = PERSONALIZE; + GET_UNIT("length", length, units::FEET, units::METERS); + GET_UNIT("pressure", pressure, units::PSI, units::BAR); + GET_UNIT("volume", volume, units::CUFT, units::LITER); + GET_UNIT("temperature", temperature, units::FAHRENHEIT, units::CELSIUS); + GET_UNIT("weight", weight, units::LBS, units::KG); + } + GET_UNIT("vertical_speed_time", vertical_speed_time, units::MINUTES, units::SECONDS); + GET_BOOL("coordinates", coordinates_traditional); + s.endGroup(); + s.beginGroup("TecDetails"); + GET_BOOL("po2graph", pp_graphs.po2); + GET_BOOL("pn2graph", pp_graphs.pn2); + GET_BOOL("phegraph", pp_graphs.phe); + GET_DOUBLE("po2threshold", pp_graphs.po2_threshold); + GET_DOUBLE("pn2threshold", pp_graphs.pn2_threshold); + GET_DOUBLE("phethreshold", pp_graphs.phe_threshold); + GET_BOOL("mod", mod); + GET_DOUBLE("modpO2", modpO2); + GET_BOOL("ead", ead); + GET_BOOL("redceiling", redceiling); + GET_BOOL("dcceiling", dcceiling); + GET_BOOL("calcceiling", calcceiling); + GET_BOOL("calcceiling3m", calcceiling3m); + GET_BOOL("calcndltts", calcndltts); + GET_BOOL("calcalltissues", calcalltissues); + GET_BOOL("hrgraph", hrgraph); + GET_BOOL("tankbar", tankbar); + GET_BOOL("percentagegraph", percentagegraph); + GET_INT("gflow", gflow); + GET_INT("gfhigh", gfhigh); + GET_BOOL("gf_low_at_maxdepth", gf_low_at_maxdepth); + GET_BOOL("show_ccr_setpoint",show_ccr_setpoint); + GET_BOOL("show_ccr_sensors",show_ccr_sensors); + GET_BOOL("zoomed_plot", zoomed_plot); + set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth); + GET_BOOL("show_sac", show_sac); + GET_BOOL("display_unused_tanks", display_unused_tanks); + GET_BOOL("show_average_depth", show_average_depth); + s.endGroup(); + + s.beginGroup("GeneralSettings"); + GET_TXT("default_filename", default_filename); + GET_INT("default_file_behavior", default_file_behavior); + if (prefs.default_file_behavior == UNDEFINED_DEFAULT_FILE) { + // undefined, so check if there's a filename set and + // use that, otherwise go with no default file + if (QString(prefs.default_filename).isEmpty()) + prefs.default_file_behavior = NO_DEFAULT_FILE; + else + prefs.default_file_behavior = LOCAL_DEFAULT_FILE; + } + GET_TXT("default_cylinder", default_cylinder); + GET_BOOL("use_default_file", use_default_file); + GET_INT("defaultsetpoint", defaultsetpoint); + GET_INT("o2consumption", o2consumption); + GET_INT("pscr_ratio", pscr_ratio); + s.endGroup(); + + s.beginGroup("Display"); + // get the font from the settings or our defaults + // respect the system default font size if none is explicitly set + QFont defaultFont = s.value("divelist_font", prefs.divelist_font).value(); + if (IS_FP_SAME(system_divelist_default_font_size, -1.0)) { + prefs.font_size = qApp->font().pointSizeF(); + system_divelist_default_font_size = prefs.font_size; // this way we don't save it on exit + } + prefs.font_size = s.value("font_size", prefs.font_size).toFloat(); + // painful effort to ignore previous default fonts on Windows - ridiculous + QString fontName = defaultFont.toString(); + if (fontName.contains(",")) + fontName = fontName.left(fontName.indexOf(",")); + if (subsurface_ignore_font(fontName.toUtf8().constData())) { + defaultFont = QFont(prefs.divelist_font); + } else { + free((void *)prefs.divelist_font); + prefs.divelist_font = strdup(fontName.toUtf8().constData()); + } + defaultFont.setPointSizeF(prefs.font_size); + qApp->setFont(defaultFont); + GET_INT("displayinvalid", display_invalid_dives); + s.endGroup(); + + s.beginGroup("Animations"); + GET_INT("animation_speed", animation_speed); + s.endGroup(); + + s.beginGroup("Network"); + GET_INT_DEF("proxy_type", proxy_type, QNetworkProxy::DefaultProxy); + GET_TXT("proxy_host", proxy_host); + GET_INT("proxy_port", proxy_port); + GET_BOOL("proxy_auth", proxy_auth); + GET_TXT("proxy_user", proxy_user); + GET_TXT("proxy_pass", proxy_pass); + s.endGroup(); + + s.beginGroup("CloudStorage"); + GET_TXT("email", cloud_storage_email); + GET_BOOL("save_password_local", save_password_local); + if (prefs.save_password_local) { // GET_TEXT macro is not a single statement + GET_TXT("password", cloud_storage_password); + } + GET_INT("cloud_verification_status", cloud_verification_status); + GET_BOOL("cloud_background_sync", cloud_background_sync); + + // creating the git url here is simply a convenience when C code wants + // to compare against that git URL - it's always derived from the base URL + GET_TXT("cloud_base_url", cloud_base_url); + prefs.cloud_git_url = strdup(qPrintable(QString(prefs.cloud_base_url) + "/git")); + s.endGroup(); + + // Subsurface webservice id is stored outside of the groups + GET_TXT("subsurface_webservice_uid", userid); + + // GeoManagement + s.beginGroup("geocoding"); +#ifdef DISABLED + GET_BOOL("enable_geocoding", geocoding.enable_geocoding); + GET_BOOL("parse_dive_without_gps", geocoding.parse_dive_without_gps); + GET_BOOL("tag_existing_dives", geocoding.tag_existing_dives); +#else + prefs.geocoding.enable_geocoding = true; +#endif + GET_ENUM("cat0", taxonomy_category, geocoding.category[0]); + GET_ENUM("cat1", taxonomy_category, geocoding.category[1]); + GET_ENUM("cat2", taxonomy_category, geocoding.category[2]); + s.endGroup(); + +} + +extern "C" bool isCloudUrl(const char *filename) +{ + QString email = QString(prefs.cloud_storage_email); + email.replace(QRegularExpression("[^a-zA-Z0-9@._+-]"), ""); + if (!email.isEmpty() && + QString(QString(prefs.cloud_git_url) + "/%1[%1]").arg(email) == filename) + return true; + return false; +} + +extern "C" bool getProxyString(char **buffer) +{ + if (prefs.proxy_type == QNetworkProxy::HttpProxy) { + QString proxy; + if (prefs.proxy_auth) + proxy = QString("http://%1:%2@%3:%4").arg(prefs.proxy_user).arg(prefs.proxy_pass) + .arg(prefs.proxy_host).arg(prefs.proxy_port); + else + proxy = QString("http://%1:%2").arg(prefs.proxy_host).arg(prefs.proxy_port); + if (buffer) + *buffer = strdup(qPrintable(proxy)); + return true; + } + return false; +} + +extern "C" void subsurface_mkdir(const char *dir) +{ + QDir directory; + if (!directory.mkpath(QString(dir))) + qDebug() << "failed to create path" << dir; +} + +extern "C" void parse_display_units(char *line) +{ + qDebug() << line; +} + +static QByteArray currentApplicationState; + +QByteArray getCurrentAppState() +{ + return currentApplicationState; +} + +void setCurrentAppState(QByteArray state) +{ + currentApplicationState = state; +} + +extern "C" bool in_planner() +{ + return (currentApplicationState == "PlanDive" || currentApplicationState == "EditPlannedDive"); +} diff --git a/subsurface-core/qthelper.h b/subsurface-core/qthelper.h new file mode 100644 index 000000000..a2b7b6c39 --- /dev/null +++ b/subsurface-core/qthelper.h @@ -0,0 +1,135 @@ +#ifndef QTHELPER_H +#define QTHELPER_H + +#include +#include +#include +#include "dive.h" +#include "divelist.h" +#include +#include + +class Dive { +private: + int m_number; + int m_id; + int m_rating; + QString m_date; + timestamp_t m_timestamp; + QString m_time; + QString m_location; + QString m_duration; + QString m_depth; + QString m_divemaster; + QString m_buddy; + QString m_airTemp; + QString m_waterTemp; + QString m_notes; + QString m_tags; + QString m_gas; + QString m_sac; + QString m_weight; + QString m_suit; + QString m_cylinder; + QString m_trip; + struct dive *dive; + void put_date_time(); + void put_timestamp(); + void put_location(); + void put_duration(); + void put_depth(); + void put_divemaster(); + void put_buddy(); + void put_temp(); + void put_notes(); + void put_tags(); + void put_gas(); + void put_sac(); + void put_weight(); + void put_suit(); + void put_cylinder(); + void put_trip(); + +public: + Dive(struct dive *dive) + : dive(dive) + { + m_number = dive->number; + m_id = dive->id; + m_rating = dive->rating; + put_date_time(); + put_location(); + put_duration(); + put_depth(); + put_divemaster(); + put_buddy(); + put_temp(); + put_notes(); + put_tags(); + put_gas(); + put_sac(); + put_timestamp(); + put_weight(); + put_suit(); + put_cylinder(); + put_trip(); + } + Dive(); + ~Dive(); + int number() const; + int id() const; + int rating() const; + QString date() const; + timestamp_t timestamp() const; + QString time() const; + QString location() const; + QString duration() const; + QString depth() const; + QString divemaster() const; + QString buddy() const; + QString airTemp() const; + QString waterTemp() const; + QString notes() const; + QString tags() const; + QString gas() const; + QString sac() const; + QString weight() const; + QString suit() const; + QString cylinder() const; + QString trip() const; +}; + +// global pointers for our translation +extern QTranslator *qtTranslator, *ssrfTranslator; + +QString weight_string(int weight_in_grams); +QString distance_string(int distanceInMeters); +bool gpsHasChanged(struct dive *dive, struct dive *master, const QString &gps_text, bool *parsed_out = 0); +extern "C" const char *printGPSCoords(int lat, int lon); +QList getDivesInTrip(dive_trip_t *trip); +QString get_gas_string(struct gasmix gas); +QString get_divepoint_gas_string(const divedatapoint& dp); +void read_hashes(); +void write_hashes(); +void updateHash(struct picture *picture); +QByteArray hashFile(const QString filename); +void learnImages(const QDir dir, int max_recursions, bool recursed); +void add_hash(const QString filename, QByteArray hash); +QString localFilePath(const QString originalFilename); +QString fileFromHash(char *hash); +void learnHash(struct picture *picture, QByteArray hash); +extern "C" void cache_picture(struct picture *picture); +weight_t string_to_weight(const char *str); +depth_t string_to_depth(const char *str); +pressure_t string_to_pressure(const char *str); +volume_t string_to_volume(const char *str, pressure_t workp); +fraction_t string_to_fraction(const char *str); +int getCloudURL(QString &filename); +void loadPreferences(); +bool parseGpsText(const QString &gps_text, double *latitude, double *longitude); +QByteArray getCurrentAppState(); +void setCurrentAppState(QByteArray state); +extern "C" bool in_planner(); +extern "C" void subsurface_mkdir(const char *dir); + +#endif // QTHELPER_H diff --git a/subsurface-core/qthelperfromc.h b/subsurface-core/qthelperfromc.h new file mode 100644 index 000000000..d2e80144c --- /dev/null +++ b/subsurface-core/qthelperfromc.h @@ -0,0 +1,21 @@ +#ifndef QTHELPERFROMC_H +#define QTHELPERFROMC_H + +bool getProxyString(char **buffer); +bool canReachCloudServer(); +void updateWindowTitle(); +bool isCloudUrl(const char *filename); +void subsurface_mkdir(const char *dir); +char *get_file_name(const char *fileName); +void copy_image_and_overwrite(const char *cfileName, const char *path, const char *cnewName); +char *hashstring(char *filename); +bool picture_exists(struct picture *picture); +char *move_away(const char *path); +const char *local_file_path(struct picture *picture); +void savePictureLocal(struct picture *picture, const char *data, int len); +void cache_picture(struct picture *picture); +char *cloud_url(); +char *hashfile_name_string(); +char *picturedir_string(); + +#endif // QTHELPERFROMC_H diff --git a/subsurface-core/qtserialbluetooth.cpp b/subsurface-core/qtserialbluetooth.cpp new file mode 100644 index 000000000..025ab8c34 --- /dev/null +++ b/subsurface-core/qtserialbluetooth.cpp @@ -0,0 +1,415 @@ +#include + +#include +#include +#include +#include +#include + +#include + +#if defined(SSRF_CUSTOM_SERIAL) + +#if defined(Q_OS_WIN) + #include + #include + #include +#endif + +#include + +extern "C" { +typedef struct serial_t { + /* Library context. */ + dc_context_t *context; + /* + * RFCOMM socket used for Bluetooth Serial communication. + */ +#if defined(Q_OS_WIN) + SOCKET socket; +#else + QBluetoothSocket *socket; +#endif + long timeout; +} serial_t; + +static int qt_serial_open(serial_t **out, dc_context_t *context, const char* devaddr) +{ + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + serial_t *serial_port = (serial_t *) malloc (sizeof (serial_t)); + if (serial_port == NULL) { + return DC_STATUS_NOMEMORY; + } + + // Library context. + serial_port->context = context; + + // Default to blocking reads. + serial_port->timeout = -1; + +#if defined(Q_OS_WIN) + // Create a RFCOMM socket + serial_port->socket = ::socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM); + + if (serial_port->socket == INVALID_SOCKET) { + free(serial_port); + return DC_STATUS_IO; + } + + SOCKADDR_BTH socketBthAddress; + int socketBthAddressBth = sizeof (socketBthAddress); + char *address = strdup(devaddr); + + ZeroMemory(&socketBthAddress, socketBthAddressBth); + qDebug() << "Trying to connect to address " << devaddr; + + if (WSAStringToAddressA(address, + AF_BTH, + NULL, + (LPSOCKADDR) &socketBthAddress, + &socketBthAddressBth + ) != 0) { + qDebug() << "FAiled to convert the address " << address; + free(address); + + return DC_STATUS_IO; + } + + free(address); + + socketBthAddress.addressFamily = AF_BTH; + socketBthAddress.port = BT_PORT_ANY; + memset(&socketBthAddress.serviceClassId, 0, sizeof(socketBthAddress.serviceClassId)); + socketBthAddress.serviceClassId = SerialPortServiceClass_UUID; + + // Try to connect to the device + if (::connect(serial_port->socket, + (struct sockaddr *) &socketBthAddress, + socketBthAddressBth + ) != 0) { + qDebug() << "Failed to connect to device"; + + return DC_STATUS_NODEVICE; + } + + qDebug() << "Succesfully connected to device"; +#else + // Create a RFCOMM socket + serial_port->socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); + + // Wait until the connection succeeds or until an error occurs + QEventLoop loop; + loop.connect(serial_port->socket, SIGNAL(connected()), SLOT(quit())); + loop.connect(serial_port->socket, SIGNAL(error(QBluetoothSocket::SocketError)), SLOT(quit())); + + // Create a timer. If the connection doesn't succeed after five seconds or no error occurs then stop the opening step + QTimer timer; + int msec = 5000; + timer.setSingleShot(true); + loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + // First try to connect on RFCOMM channel 1. This is the default channel for most devices + QBluetoothAddress remoteDeviceAddress(devaddr); + serial_port->socket->connectToService(remoteDeviceAddress, 1, QIODevice::ReadWrite | QIODevice::Unbuffered); + timer.start(msec); + loop.exec(); + + if (serial_port->socket->state() == QBluetoothSocket::ConnectingState) { + // It seems that the connection on channel 1 took more than expected. Wait another 15 seconds + qDebug() << "The connection on RFCOMM channel number 1 took more than expected. Wait another 15 seconds."; + timer.start(3 * msec); + loop.exec(); + } else if (serial_port->socket->state() == QBluetoothSocket::UnconnectedState) { + // Try to connect on channel number 5. Maybe this is a Shearwater Petrel2 device. + qDebug() << "Connection on channel 1 failed. Trying on channel number 5."; + serial_port->socket->connectToService(remoteDeviceAddress, 5, QIODevice::ReadWrite | QIODevice::Unbuffered); + timer.start(msec); + loop.exec(); + + if (serial_port->socket->state() == QBluetoothSocket::ConnectingState) { + // It seems that the connection on channel 5 took more than expected. Wait another 15 seconds + qDebug() << "The connection on RFCOMM channel number 5 took more than expected. Wait another 15 seconds."; + timer.start(3 * msec); + loop.exec(); + } + } +#elif defined(Q_OS_ANDROID) || (QT_VERSION >= 0x050500 && defined(Q_OS_MAC)) + // Try to connect to the device using the uuid of the Serial Port Profile service + QBluetoothAddress remoteDeviceAddress(devaddr); + serial_port->socket->connectToService(remoteDeviceAddress, QBluetoothUuid(QBluetoothUuid::SerialPort)); + timer.start(msec); + loop.exec(); + + if (serial_port->socket->state() == QBluetoothSocket::ConnectingState || + serial_port->socket->state() == QBluetoothSocket::ServiceLookupState) { + // It seems that the connection step took more than expected. Wait another 20 seconds. + qDebug() << "The connection step took more than expected. Wait another 20 seconds"; + timer.start(4 * msec); + loop.exec(); + } +#endif + if (serial_port->socket->state() != QBluetoothSocket::ConnectedState) { + + // Get the latest error and try to match it with one from libdivecomputer + QBluetoothSocket::SocketError err = serial_port->socket->error(); + qDebug() << "Failed to connect to device " << devaddr << ". Device state " << serial_port->socket->state() << ". Error: " << err; + + free (serial_port); + switch(err) { + case QBluetoothSocket::HostNotFoundError: + case QBluetoothSocket::ServiceNotFoundError: + return DC_STATUS_NODEVICE; + case QBluetoothSocket::UnsupportedProtocolError: + return DC_STATUS_PROTOCOL; +#if QT_VERSION >= 0x050400 + case QBluetoothSocket::OperationError: + return DC_STATUS_UNSUPPORTED; +#endif + case QBluetoothSocket::NetworkError: + return DC_STATUS_IO; + default: + return QBluetoothSocket::UnknownSocketError; + } + } +#endif + *out = serial_port; + + return DC_STATUS_SUCCESS; +} + +static int qt_serial_close(serial_t *device) +{ + if (device == NULL) + return DC_STATUS_SUCCESS; + +#if defined(Q_OS_WIN) + // Cleanup + closesocket(device->socket); + free(device); +#else + if (device->socket == NULL) { + free(device); + return DC_STATUS_SUCCESS; + } + + device->socket->close(); + + delete device->socket; + free(device); +#endif + + return DC_STATUS_SUCCESS; +} + +static int qt_serial_read(serial_t *device, void* data, unsigned int size) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while (nbytes < size) { + rc = recv (device->socket, (char *) data + nbytes, size - nbytes, 0); + + if (rc < 0) { + return -1; // Error during recv call. + } else if (rc == 0) { + break; // EOF reached. + } + + nbytes += rc; + } + + return nbytes; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while(nbytes < size && device->socket->state() == QBluetoothSocket::ConnectedState) + { + rc = device->socket->read((char *) data + nbytes, size - nbytes); + + if (rc < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; // Retry. + + return -1; // Something really bad happened :-( + } else if (rc == 0) { + // Wait until the device is available for read operations + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + loop.connect(device->socket, SIGNAL(readyRead()), SLOT(quit())); + timer.start(device->timeout); + loop.exec(); + + if (!timer.isActive()) + return nbytes; + } + + nbytes += rc; + } + + return nbytes; +#endif +} + +static int qt_serial_write(serial_t *device, const void* data, unsigned int size) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while (nbytes < size) { + rc = send(device->socket, (char *) data + nbytes, size - nbytes, 0); + + if (rc < 0) { + return -1; // Error during send call. + } + + nbytes += rc; + } + + return nbytes; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + unsigned int nbytes = 0; + int rc; + + while(nbytes < size && device->socket->state() == QBluetoothSocket::ConnectedState) + { + rc = device->socket->write((char *) data + nbytes, size - nbytes); + + if (rc < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; // Retry. + + return -1; // Something really bad happened :-( + } else if (rc == 0) { + break; + } + + nbytes += rc; + } + + return nbytes; +#endif +} + +static int qt_serial_flush(serial_t *device, int queue) +{ + if (device == NULL) + return DC_STATUS_INVALIDARGS; +#if !defined(Q_OS_WIN) + if (device->socket == NULL) + return DC_STATUS_INVALIDARGS; +#endif + // TODO: add implementation + + return DC_STATUS_SUCCESS; +} + +static int qt_serial_get_received(serial_t *device) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + // TODO use WSAIoctl to get the information + + return 0; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + return device->socket->bytesAvailable(); +#endif +} + +static int qt_serial_get_transmitted(serial_t *device) +{ +#if defined(Q_OS_WIN) + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + // TODO add implementation + + return 0; +#else + if (device == NULL || device->socket == NULL) + return DC_STATUS_INVALIDARGS; + + return device->socket->bytesToWrite(); +#endif +} + +static int qt_serial_set_timeout(serial_t *device, long timeout) +{ + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + device->timeout = timeout; + + return DC_STATUS_SUCCESS; +} + + +const dc_serial_operations_t qt_serial_ops = { + .open = qt_serial_open, + .close = qt_serial_close, + .read = qt_serial_read, + .write = qt_serial_write, + .flush = qt_serial_flush, + .get_received = qt_serial_get_received, + .get_transmitted = qt_serial_get_transmitted, + .set_timeout = qt_serial_set_timeout +}; + +extern void dc_serial_init (dc_serial_t *serial, void *data, const dc_serial_operations_t *ops); + +dc_status_t dc_serial_qt_open(dc_serial_t **out, dc_context_t *context, const char *devaddr) +{ + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + dc_serial_t *serial_device = (dc_serial_t *) malloc (sizeof (dc_serial_t)); + + if (serial_device == NULL) { + return DC_STATUS_NOMEMORY; + } + + // Initialize data and function pointers + dc_serial_init(serial_device, NULL, &qt_serial_ops); + + // Open the serial device. + dc_status_t rc = (dc_status_t)qt_serial_open (&serial_device->port, context, devaddr); + if (rc != DC_STATUS_SUCCESS) { + free (serial_device); + return rc; + } + + // Set the type of the device + serial_device->type = DC_TRANSPORT_BLUETOOTH; + + *out = serial_device; + + return DC_STATUS_SUCCESS; +} +} +#endif diff --git a/subsurface-core/save-git.c b/subsurface-core/save-git.c new file mode 100644 index 000000000..69ad0726d --- /dev/null +++ b/subsurface-core/save-git.c @@ -0,0 +1,1235 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "git-access.h" +#include "version.h" +#include "qthelperfromc.h" + +/* + * handle libgit2 revision 0.20 and earlier + */ +#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR <= 20 && !defined(USE_LIBGIT21_API) + #define GIT_CHECKOUT_OPTIONS_INIT GIT_CHECKOUT_OPTS_INIT + #define git_checkout_options git_checkout_opts + #define git_branch_create(out,repo,branch_name,target,force,sig,msg) \ + git_branch_create(out,repo,branch_name,target,force) + #define git_reference_set_target(out,ref,target,signature,log_message) \ + git_reference_set_target(out,ref,target) +#endif +/* + * api break in libgit2 revision 0.22 + */ +#if !LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR < 22 + #define git_treebuilder_new(out, repo, source) git_treebuilder_create(out, source) +#else + #define git_treebuilder_write(id, repo, bld) git_treebuilder_write(id, bld) +#endif +/* + * api break introduced in libgit2 master after 0.22 - let's guess this is the v0.23 API + */ +#if USE_LIBGIT23_API || (!LIBGIT2_VER_MAJOR && LIBGIT2_VER_MINOR >= 23) + #define git_branch_create(out, repo, branch_name, target, force, signature, log_message) \ + git_branch_create(out, repo, branch_name, target, force) + #define git_reference_set_target(out, ref, id, author, log_message) \ + git_reference_set_target(out, ref, id, log_message) +#endif + +#define VA_BUF(b, fmt) do { va_list args; va_start(args, fmt); put_vformat(b, fmt, args); va_end(args); } while (0) + +static void cond_put_format(int cond, struct membuffer *b, const char *fmt, ...) +{ + if (cond) { + VA_BUF(b, fmt); + } +} + +#define SAVE(str, x) cond_put_format(dive->x, b, str " %d\n", dive->x) + +static void show_gps(struct membuffer *b, degrees_t latitude, degrees_t longitude) +{ + if (latitude.udeg || longitude.udeg) { + put_degrees(b, latitude, "gps ", " "); + put_degrees(b, longitude, "", "\n"); + } +} + +static void quote(struct membuffer *b, const char *text) +{ + const char *p = text; + + for (;;) { + const char *escape; + + switch (*p++) { + default: + continue; + case 0: + escape = NULL; + break; + case 1 ... 8: + case 11: + case 12: + case 14 ... 31: + escape = "?"; + break; + case '\\': + escape = "\\\\"; + break; + case '"': + escape = "\\\""; + break; + case '\n': + escape = "\n\t"; + if (*p == '\n') + escape = "\n"; + break; + } + put_bytes(b, text, (p - text - 1)); + if (!escape) + break; + put_string(b, escape); + text = p; + } +} + +static void show_utf8(struct membuffer *b, const char *prefix, const char *value, const char *postfix) +{ + if (value) { + put_format(b, "%s\"", prefix); + quote(b, value); + put_format(b, "\"%s", postfix); + } +} + +static void save_overview(struct membuffer *b, struct dive *dive) +{ + show_utf8(b, "divemaster ", dive->divemaster, "\n"); + show_utf8(b, "buddy ", dive->buddy, "\n"); + show_utf8(b, "suit ", dive->suit, "\n"); + show_utf8(b, "notes ", dive->notes, "\n"); +} + +static void save_tags(struct membuffer *b, struct tag_entry *tags) +{ + const char *sep = " "; + + if (!tags) + return; + put_string(b, "tags"); + while (tags) { + show_utf8(b, sep, tags->tag->source ? : tags->tag->name, ""); + sep = ", "; + tags = tags->next; + } + put_string(b, "\n"); +} + +static void save_extra_data(struct membuffer *b, struct extra_data *ed) +{ + while (ed) { + if (ed->key && ed->value) + put_format(b, "keyvalue \"%s\" \"%s\"\n", ed->key ? : "", ed->value ? : ""); + ed = ed->next; + } +} + +static void put_gasmix(struct membuffer *b, struct gasmix *mix) +{ + int o2 = mix->o2.permille; + int he = mix->he.permille; + + if (o2) { + put_format(b, " o2=%u.%u%%", FRACTION(o2, 10)); + if (he) + put_format(b, " he=%u.%u%%", FRACTION(he, 10)); + } +} + +static void save_cylinder_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_cylinders(dive); + for (i = 0; i < nr; i++) { + cylinder_t *cylinder = dive->cylinder + i; + int volume = cylinder->type.size.mliter; + const char *description = cylinder->type.description; + + put_string(b, "cylinder"); + if (volume) + put_milli(b, " vol=", volume, "l"); + put_pressure(b, cylinder->type.workingpressure, " workpressure=", "bar"); + show_utf8(b, " description=", description, ""); + strip_mb(b); + put_gasmix(b, &cylinder->gasmix); + put_pressure(b, cylinder->start, " start=", "bar"); + put_pressure(b, cylinder->end, " end=", "bar"); + if (cylinder->cylinder_use != OC_GAS) + put_format(b, " use=%s", cylinderuse_text[cylinder->cylinder_use]); + + put_string(b, "\n"); + } +} + +static void save_weightsystem_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_weightsystems(dive); + for (i = 0; i < nr; i++) { + weightsystem_t *ws = dive->weightsystem + i; + int grams = ws->weight.grams; + const char *description = ws->description; + + put_string(b, "weightsystem"); + put_milli(b, " weight=", grams, "kg"); + show_utf8(b, " description=", description, ""); + put_string(b, "\n"); + } +} + +static void save_dive_temperature(struct membuffer *b, struct dive *dive) +{ + if (dive->airtemp.mkelvin != dc_airtemp(&dive->dc)) + put_temperature(b, dive->airtemp, "airtemp ", "°C\n"); + if (dive->watertemp.mkelvin != dc_watertemp(&dive->dc)) + put_temperature(b, dive->watertemp, "watertemp ", "°C\n"); +} + +static void save_depths(struct membuffer *b, struct divecomputer *dc) +{ + put_depth(b, dc->maxdepth, "maxdepth ", "m\n"); + put_depth(b, dc->meandepth, "meandepth ", "m\n"); +} + +static void save_temperatures(struct membuffer *b, struct divecomputer *dc) +{ + put_temperature(b, dc->airtemp, "airtemp ", "°C\n"); + put_temperature(b, dc->watertemp, "watertemp ", "°C\n"); +} + +static void save_airpressure(struct membuffer *b, struct divecomputer *dc) +{ + put_pressure(b, dc->surface_pressure, "surfacepressure ", "bar\n"); +} + +static void save_salinity(struct membuffer *b, struct divecomputer *dc) +{ + /* only save if we have a value that isn't the default of sea water */ + if (!dc->salinity || dc->salinity == SEAWATER_SALINITY) + return; + put_salinity(b, dc->salinity, "salinity ", "g/l\n"); +} + +static void show_date(struct membuffer *b, timestamp_t when) +{ + struct tm tm; + + utc_mkdate(when, &tm); + + put_format(b, "date %04u-%02u-%02u\n", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); + put_format(b, "time %02u:%02u:%02u\n", + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void show_index(struct membuffer *b, int value, const char *pre, const char *post) +{ + if (value) + put_format(b, " %s%d%s", pre, value, post); +} + +/* + * Samples are saved as densely as possible while still being readable, + * since they are the bulk of the data. + * + * For parsing, look at the units to figure out what the numbers are. + */ +static void save_sample(struct membuffer *b, struct sample *sample, struct sample *old) +{ + put_format(b, "%3u:%02u", FRACTION(sample->time.seconds, 60)); + put_milli(b, " ", sample->depth.mm, "m"); + put_temperature(b, sample->temperature, " ", "°C"); + put_pressure(b, sample->cylinderpressure, " ", "bar"); + put_pressure(b, sample->o2cylinderpressure," o2pressure=","bar"); + + /* + * We only show sensor information for samples with pressure, and only if it + * changed from the previous sensor we showed. + */ + if (sample->cylinderpressure.mbar && sample->sensor != old->sensor) { + put_format(b, " sensor=%d", sample->sensor); + old->sensor = sample->sensor; + } + + /* the deco/ndl values are stored whenever they change */ + if (sample->ndl.seconds != old->ndl.seconds) { + put_format(b, " ndl=%u:%02u", FRACTION(sample->ndl.seconds, 60)); + old->ndl = sample->ndl; + } + if (sample->tts.seconds != old->tts.seconds) { + put_format(b, " tts=%u:%02u", FRACTION(sample->tts.seconds, 60)); + old->tts = sample->tts; + } + if (sample->in_deco != old->in_deco) { + put_format(b, " in_deco=%d", sample->in_deco ? 1 : 0); + old->in_deco = sample->in_deco; + } + if (sample->stoptime.seconds != old->stoptime.seconds) { + put_format(b, " stoptime=%u:%02u", FRACTION(sample->stoptime.seconds, 60)); + old->stoptime = sample->stoptime; + } + + if (sample->stopdepth.mm != old->stopdepth.mm) { + put_milli(b, " stopdepth=", sample->stopdepth.mm, "m"); + old->stopdepth = sample->stopdepth; + } + + if (sample->cns != old->cns) { + put_format(b, " cns=%u%%", sample->cns); + old->cns = sample->cns; + } + + if (sample->rbt.seconds) + put_format(b, " rbt=%u:%02u", FRACTION(sample->rbt.seconds, 60)); + + if (sample->o2sensor[0].mbar != old->o2sensor[0].mbar) { + put_milli(b, " sensor1=", sample->o2sensor[0].mbar, "bar"); + old->o2sensor[0] = sample->o2sensor[0]; + } + + if ((sample->o2sensor[1].mbar) && (sample->o2sensor[1].mbar != old->o2sensor[1].mbar)) { + put_milli(b, " sensor2=", sample->o2sensor[1].mbar, "bar"); + old->o2sensor[1] = sample->o2sensor[1]; + } + + if ((sample->o2sensor[2].mbar) && (sample->o2sensor[2].mbar != old->o2sensor[2].mbar)) { + put_milli(b, " sensor3=", sample->o2sensor[2].mbar, "bar"); + old->o2sensor[2] = sample->o2sensor[2]; + } + + if (sample->setpoint.mbar != old->setpoint.mbar) { + put_milli(b, " po2=", sample->setpoint.mbar, "bar"); + old->setpoint = sample->setpoint; + } + show_index(b, sample->heartbeat, "heartbeat=", ""); + show_index(b, sample->bearing.degrees, "bearing=", "°"); + put_format(b, "\n"); +} + +static void save_samples(struct membuffer *b, int nr, struct sample *s) +{ + struct sample dummy = {}; + + while (--nr >= 0) { + save_sample(b, s, &dummy); + s++; + } +} + +static void save_one_event(struct membuffer *b, struct event *ev) +{ + put_format(b, "event %d:%02d", FRACTION(ev->time.seconds, 60)); + show_index(b, ev->type, "type=", ""); + show_index(b, ev->flags, "flags=", ""); + show_index(b, ev->value, "value=", ""); + show_utf8(b, " name=", ev->name, ""); + if (event_is_gaschange(ev)) { + if (ev->gas.index >= 0) { + show_index(b, ev->gas.index, "cylinder=", ""); + put_gasmix(b, &ev->gas.mix); + } else if (!event_gasmix_redundant(ev)) + put_gasmix(b, &ev->gas.mix); + } + put_string(b, "\n"); +} + +static void save_events(struct membuffer *b, struct event *ev) +{ + while (ev) { + save_one_event(b, ev); + ev = ev->next; + } +} + +static void save_dc(struct membuffer *b, struct dive *dive, struct divecomputer *dc) +{ + show_utf8(b, "model ", dc->model, "\n"); + if (dc->deviceid) + put_format(b, "deviceid %08x\n", dc->deviceid); + if (dc->diveid) + put_format(b, "diveid %08x\n", dc->diveid); + if (dc->when && dc->when != dive->when) + show_date(b, dc->when); + if (dc->duration.seconds && dc->duration.seconds != dive->dc.duration.seconds) + put_duration(b, dc->duration, "duration ", "min\n"); + if (dc->divemode != OC) { + put_format(b, "dctype %s\n", divemode_text[dc->divemode]); + put_format(b, "numberofoxygensensors %d\n",dc->no_o2sensors); + } + + save_depths(b, dc); + save_temperatures(b, dc); + save_airpressure(b, dc); + save_salinity(b, dc); + put_duration(b, dc->surfacetime, "surfacetime ", "min\n"); + + save_extra_data(b, dc->extra_data); + save_events(b, dc->events); + save_samples(b, dc->samples, dc->sample); +} + +/* + * Note that we don't save the date and time or dive + * number: they are encoded in the filename. + */ +static void create_dive_buffer(struct dive *dive, struct membuffer *b) +{ + put_format(b, "duration %u:%02u min\n", FRACTION(dive->dc.duration.seconds, 60)); + SAVE("rating", rating); + SAVE("visibility", visibility); + cond_put_format(dive->tripflag == NO_TRIP, b, "notrip\n"); + save_tags(b, dive->tag_list); + cond_put_format(dive->dive_site_uuid && get_dive_site_by_uuid(dive->dive_site_uuid), + b, "divesiteid %08x\n", dive->dive_site_uuid); + if (verbose && dive->dive_site_uuid && !get_dive_site_by_uuid(dive->dive_site_uuid)) + fprintf(stderr, "removed reference to non-existant dive site with uuid %08x\n", dive->dive_site_uuid); + save_overview(b, dive); + save_cylinder_info(b, dive); + save_weightsystem_info(b, dive); + save_dive_temperature(b, dive); +} + +static struct membuffer error_string_buffer = { 0 }; + +/* + * Note that the act of "getting" the error string + * buffer doesn't de-allocate the buffer, but it does + * set the buffer length to zero, so that any future + * error reports will overwrite the string rather than + * append to it. + */ +const char *get_error_string(void) +{ + const char *str; + + if (!error_string_buffer.len) + return ""; + str = mb_cstring(&error_string_buffer); + error_string_buffer.len = 0; + return str; +} + +int report_error(const char *fmt, ...) +{ + struct membuffer *buf = &error_string_buffer; + + /* Previous unprinted errors? Add a newline in between */ + if (buf->len) + put_bytes(buf, "\n", 1); + VA_BUF(buf, fmt); + mb_cstring(buf); + return -1; +} + +/* + * libgit2 has a "git_treebuilder" concept, but it's broken, and can not + * be used to do a flat tree (like the git "index") nor a recursive tree. + * Stupid. + * + * So we have to do that "keep track of recursive treebuilder entries" + * ourselves. We use 'git_treebuilder' for any regular files, and our own + * data structures for recursive trees. + * + * When finally writing it out, we traverse the subdirectories depth- + * first, writing them out, and then adding the written-out trees to + * the git_treebuilder they existed in. + */ +struct dir { + git_treebuilder *files; + struct dir *subdirs, *sibling; + char unique, name[1]; +}; + +static int tree_insert(git_treebuilder *dir, const char *name, int mkunique, git_oid *id, unsigned mode) +{ + int ret; + struct membuffer uniquename = { 0 }; + + if (mkunique && git_treebuilder_get(dir, name)) { + char hex[8]; + git_oid_nfmt(hex, 7, id); + hex[7] = 0; + put_format(&uniquename, "%s~%s", name, hex); + name = mb_cstring(&uniquename); + } + ret = git_treebuilder_insert(NULL, dir, name, id, mode); + free_buffer(&uniquename); + return ret; +} + +/* + * This does *not* make sure the new subdirectory doesn't + * alias some existing name. That is actually useful: you + * can create multiple directories with the same name, and + * set the "unique" flag, which will then append the SHA1 + * of the directory to the name when it is written. + */ +static struct dir *new_directory(git_repository *repo, struct dir *parent, struct membuffer *namebuf) +{ + struct dir *subdir; + const char *name = mb_cstring(namebuf); + int len = namebuf->len; + + subdir = malloc(sizeof(*subdir)+len); + + /* + * It starts out empty: no subdirectories of its own, + * and an empty treebuilder list of files. + */ + subdir->subdirs = NULL; + git_treebuilder_new(&subdir->files, repo, NULL); + memcpy(subdir->name, name, len); + subdir->unique = 0; + subdir->name[len] = 0; + + /* Add it to the list of subdirs of the parent */ + subdir->sibling = parent->subdirs; + parent->subdirs = subdir; + + return subdir; +} + +static struct dir *mktree(git_repository *repo, struct dir *dir, const char *fmt, ...) +{ + struct membuffer buf = { 0 }; + struct dir *subdir; + + VA_BUF(&buf, fmt); + for (subdir = dir->subdirs; subdir; subdir = subdir->sibling) { + if (subdir->unique) + continue; + if (strncmp(subdir->name, buf.buffer, buf.len)) + continue; + if (!subdir->name[buf.len]) + break; + } + if (!subdir) + subdir = new_directory(repo, dir, &buf); + free_buffer(&buf); + return subdir; +} + +/* + * The name of a dive is the date and the dive number (and possibly + * the uniqueness suffix). + * + * Note that the time of the dive may not be the same as the + * time of the directory structure it is created in: the dive + * might be part of a trip that straddles a month (or even a + * year). + * + * We do *not* want to use localized weekdays and cause peoples save + * formats to depend on their locale. + */ +static void create_dive_name(struct dive *dive, struct membuffer *name, struct tm *dirtm) +{ + struct tm tm; + static const char weekday[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + utc_mkdate(dive->when, &tm); + if (tm.tm_year != dirtm->tm_year) + put_format(name, "%04u-", tm.tm_year + 1900); + if (tm.tm_mon != dirtm->tm_mon) + put_format(name, "%02u-", tm.tm_mon+1); + + /* a colon is an illegal char in a file name on Windows - use an '=' instead */ + put_format(name, "%02u-%s-%02u=%02u=%02u", + tm.tm_mday, weekday[tm.tm_wday], + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +/* Write file at filepath to the git repo with given filename */ +static int blob_insert_fromdisk(git_repository *repo, struct dir *tree, const char *filepath, const char *filename) +{ + int ret; + git_oid blob_id; + + ret = git_blob_create_fromdisk(&blob_id, repo, filepath); + if (ret) + return ret; + return tree_insert(tree->files, filename, 1, &blob_id, GIT_FILEMODE_BLOB); +} + +/* + * Write a membuffer to the git repo, and free it + */ +static int blob_insert(git_repository *repo, struct dir *tree, struct membuffer *b, const char *fmt, ...) +{ + int ret; + git_oid blob_id; + struct membuffer name = { 0 }; + + ret = git_blob_create_frombuffer(&blob_id, repo, b->buffer, b->len); + free_buffer(b); + if (ret) + return ret; + + VA_BUF(&name, fmt); + ret = tree_insert(tree->files, mb_cstring(&name), 1, &blob_id, GIT_FILEMODE_BLOB); + free_buffer(&name); + return ret; +} + +static int save_one_divecomputer(git_repository *repo, struct dir *tree, struct dive *dive, struct divecomputer *dc, int idx) +{ + int ret; + struct membuffer buf = { 0 }; + + save_dc(&buf, dive, dc); + ret = blob_insert(repo, tree, &buf, "Divecomputer%c%03u", idx ? '-' : 0, idx); + if (ret) + report_error("divecomputer tree insert failed"); + return ret; +} + +static int save_one_picture(git_repository *repo, struct dir *dir, struct picture *pic) +{ + int offset = pic->offset.seconds; + struct membuffer buf = { 0 }; + char sign = '+'; + unsigned h; + int error; + + show_utf8(&buf, "filename ", pic->filename, "\n"); + show_gps(&buf, pic->latitude, pic->longitude); + show_utf8(&buf, "hash ", pic->hash, "\n"); + + /* Picture loading will load even negative offsets.. */ + if (offset < 0) { + offset = -offset; + sign = '-'; + } + + /* Use full hh:mm:ss format to make it all sort nicely */ + h = offset / 3600; + offset -= h *3600; + error = blob_insert(repo, dir, &buf, "%c%02u=%02u=%02u", + sign, h, FRACTION(offset, 60)); + if (!error) { + /* next store the actual picture; we prefix all picture names + * with "PIC-" to make things easier on the parsing side */ + struct membuffer namebuf = { 0 }; + const char *localfn = local_file_path(pic); + put_format(&namebuf, "PIC-%s", pic->hash); + error = blob_insert_fromdisk(repo, dir, localfn, mb_cstring(&namebuf)); + free((void *)localfn); + } + return error; +} + +static int save_pictures(git_repository *repo, struct dir *dir, struct dive *dive) +{ + if (dive->picture_list) { + dir = mktree(repo, dir, "Pictures"); + FOR_EACH_PICTURE(dive) { + save_one_picture(repo, dir, picture); + } + } + return 0; +} + +static int save_one_dive(git_repository *repo, struct dir *tree, struct dive *dive, struct tm *tm) +{ + struct divecomputer *dc; + struct membuffer buf = { 0 }, name = { 0 }; + struct dir *subdir; + int ret, nr; + + /* Create dive directory */ + create_dive_name(dive, &name, tm); + subdir = new_directory(repo, tree, &name); + subdir->unique = 1; + free_buffer(&name); + + create_dive_buffer(dive, &buf); + nr = dive->number; + ret = blob_insert(repo, subdir, &buf, + "Dive%c%d", nr ? '-' : 0, nr); + if (ret) + return report_error("dive save-file tree insert failed"); + + /* + * Save the dive computer data. If there is only one dive + * computer, use index 0 for that (which disables the index + * generation when naming it). + */ + dc = &dive->dc; + nr = dc->next ? 1 : 0; + do { + save_one_divecomputer(repo, subdir, dive, dc, nr++); + dc = dc->next; + } while (dc); + + /* Save the picture data, if any */ + save_pictures(repo, subdir, dive); + return 0; +} + +/* + * We'll mark the trip directories unique, so this does not + * need to be unique per se. It could be just "trip". But + * to make things a bit more user-friendly, we try to take + * the trip location into account. + * + * But no special characters, and no numbers (numbers in the + * name could be construed as a date). + * + * So we might end up with "02-Maui", and then the unique + * flag will make us write it out as "02-Maui~54b4" or + * similar. + */ +#define MAXTRIPNAME 15 +static void create_trip_name(dive_trip_t *trip, struct membuffer *name, struct tm *tm) +{ + put_format(name, "%02u-", tm->tm_mday); + if (trip->location) { + char ascii_loc[MAXTRIPNAME+1], *p = trip->location; + int i; + + for (i = 0; i < MAXTRIPNAME; ) { + char c = *p++; + switch (c) { + case 0: + case ',': + case '.': + break; + + case 'a' ... 'z': + case 'A' ... 'Z': + ascii_loc[i++] = c; + continue; + default: + continue; + } + break; + } + if (i > 1) { + put_bytes(name, ascii_loc, i); + return; + } + } + + /* No useful name? */ + put_string(name, "trip"); +} + +static int save_trip_description(git_repository *repo, struct dir *dir, dive_trip_t *trip, struct tm *tm) +{ + int ret; + git_oid blob_id; + struct membuffer desc = { 0 }; + + put_format(&desc, "date %04u-%02u-%02u\n", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + put_format(&desc, "time %02u:%02u:%02u\n", + tm->tm_hour, tm->tm_min, tm->tm_sec); + + show_utf8(&desc, "location ", trip->location, "\n"); + show_utf8(&desc, "notes ", trip->notes, "\n"); + + ret = git_blob_create_frombuffer(&blob_id, repo, desc.buffer, desc.len); + free_buffer(&desc); + if (ret) + return report_error("trip blob creation failed"); + ret = tree_insert(dir->files, "00-Trip", 0, &blob_id, GIT_FILEMODE_BLOB); + if (ret) + return report_error("trip description tree insert failed"); + return 0; +} + +static void verify_shared_date(timestamp_t when, struct tm *tm) +{ + struct tm tmp_tm; + + utc_mkdate(when, &tmp_tm); + if (tmp_tm.tm_year != tm->tm_year) { + tm->tm_year = -1; + tm->tm_mon = -1; + } + if (tmp_tm.tm_mon != tm->tm_mon) + tm->tm_mon = -1; +} + +#define MIN_TIMESTAMP (0) +#define MAX_TIMESTAMP (0x7fffffffffffffff) + +static int save_one_trip(git_repository *repo, struct dir *tree, dive_trip_t *trip, struct tm *tm) +{ + int i; + struct dive *dive; + struct dir *subdir; + struct membuffer name = { 0 }; + timestamp_t first, last; + + /* Create trip directory */ + create_trip_name(trip, &name, tm); + subdir = new_directory(repo, tree, &name); + subdir->unique = 1; + free_buffer(&name); + + /* Trip description file */ + save_trip_description(repo, subdir, trip, tm); + + /* Make sure we write out the dates to the dives consistently */ + first = MAX_TIMESTAMP; + last = MIN_TIMESTAMP; + for_each_dive(i, dive) { + if (dive->divetrip != trip) + continue; + if (dive->when < first) + first = dive->when; + if (dive->when > last) + last = dive->when; + } + verify_shared_date(first, tm); + verify_shared_date(last, tm); + + /* Save each dive in the directory */ + for_each_dive(i, dive) { + if (dive->divetrip == trip) + save_one_dive(repo, subdir, dive, tm); + } + + return 0; +} + +static void save_units(void *_b) +{ + struct membuffer *b =_b; + if (prefs.unit_system == METRIC) + put_string(b, "units METRIC\n"); + else if (prefs.unit_system == IMPERIAL) + put_string(b, "units IMPERIAL\n"); + else + put_format(b, "units PERSONALIZE %s %s %s %s %s %s", + prefs.units.length == METERS ? "METERS" : "FEET", + prefs.units.volume == LITER ? "LITER" : "CUFT", + prefs.units.pressure == BAR ? "BAR" : prefs.units.pressure == PSI ? "PSI" : "PASCAL", + prefs.units.temperature == CELSIUS ? "CELSIUS" : prefs.units.temperature == FAHRENHEIT ? "FAHRENHEIT" : "KELVIN", + prefs.units.weight == KG ? "KG" : "LBS", + prefs.units.vertical_speed_time == SECONDS ? "SECONDS" : "MINUTES"); +} + +static void save_userid(void *_b) +{ + struct membuffer *b = _b; + if (prefs.save_userid_local) + put_format(b, "userid %30s\n", prefs.userid); +} + +static void save_one_device(void *_b, const char *model, uint32_t deviceid, + const char *nickname, const char *serial, const char *firmware) +{ + struct membuffer *b = _b; + + if (nickname && !strcmp(model, nickname)) + nickname = NULL; + if (serial && !*serial) serial = NULL; + if (firmware && !*firmware) firmware = NULL; + if (nickname && !*nickname) nickname = NULL; + if (!nickname && !serial && !firmware) + return; + + show_utf8(b, "divecomputerid ", model, ""); + put_format(b, " deviceid=%08x", deviceid); + show_utf8(b, " serial=", serial, ""); + show_utf8(b, " firmware=", firmware, ""); + show_utf8(b, " nickname=", nickname, ""); + put_string(b, "\n"); +} + +static void save_settings(git_repository *repo, struct dir *tree) +{ + struct membuffer b = { 0 }; + + put_format(&b, "version %d\n", DATAFORMAT_VERSION); + save_userid(&b); + call_for_each_dc(&b, save_one_device, false); + cond_put_format(autogroup, &b, "autogroup\n"); + save_units(&b); + + blob_insert(repo, tree, &b, "00-Subsurface"); +} + +static void save_divesites(git_repository *repo, struct dir *tree) +{ + struct dir *subdir; + struct membuffer dirname = { 0 }; + put_format(&dirname, "01-Divesites"); + subdir = new_directory(repo, tree, &dirname); + + for (int i = 0; i < dive_site_table.nr; i++) { + struct membuffer b = { 0 }; + struct dive_site *ds = get_dive_site(i); + if (dive_site_is_empty(ds)) { + int j; + struct dive *d; + for_each_dive(j, d) { + if (d->dive_site_uuid == ds->uuid) + d->dive_site_uuid = 0; + } + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } else if (ds->name && + (strncmp(ds->name, "Auto-created dive", 17) == 0 || + strncmp(ds->name, "New Dive", 8) == 0)) { + fprintf(stderr, "found an auto divesite %s\n", ds->name); + // these are the two default names for sites from + // the web service; if the site isn't used in any + // dive (really? you didn't rename it?), delete it + if (!is_dive_site_used(ds->uuid, false)) { + if (verbose) + fprintf(stderr, "Deleted unused auto-created dive site %s\n", ds->name); + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } + } + struct membuffer site_file_name = { 0 }; + put_format(&site_file_name, "Site-%08x", ds->uuid); + show_utf8(&b, "name ", ds->name, "\n"); + show_utf8(&b, "description ", ds->description, "\n"); + show_utf8(&b, "notes ", ds->notes, "\n"); + show_gps(&b, ds->latitude, ds->longitude); + for (int j = 0; j < ds->taxonomy.nr; j++) { + struct taxonomy *t = &ds->taxonomy.category[j]; + if (t->category != TC_NONE && t->value) { + put_format(&b, "geo cat %d origin %d ", t->category, t->origin); + show_utf8(&b, "", t->value, "\n" ); + } + } + blob_insert(repo, subdir, &b, mb_cstring(&site_file_name)); + } +} + +static int create_git_tree(git_repository *repo, struct dir *root, bool select_only) +{ + int i; + struct dive *dive; + dive_trip_t *trip; + + save_settings(repo, root); + + save_divesites(repo, root); + + for (trip = dive_trip_list; trip != NULL; trip = trip->next) + trip->index = 0; + + /* save the dives */ + for_each_dive(i, dive) { + struct tm tm; + struct dir *tree; + + trip = dive->divetrip; + + if (select_only) { + if (!dive->selected) + continue; + /* We don't save trips when doing selected dive saves */ + trip = NULL; + } + + /* Create the date-based hierarchy */ + utc_mkdate(trip ? trip->when : dive->when, &tm); + tree = mktree(repo, root, "%04d", tm.tm_year + 1900); + tree = mktree(repo, tree, "%02d", tm.tm_mon + 1); + + if (trip) { + /* Did we already save this trip? */ + if (trip->index) + continue; + trip->index = 1; + + /* Pass that new subdirectory in for save-trip */ + save_one_trip(repo, tree, trip, &tm); + continue; + } + + save_one_dive(repo, tree, dive, &tm); + } + return 0; +} + +/* + * See if we can find the parent ID that the git data came from + */ +static git_object *try_to_find_parent(const char *hex_id, git_repository *repo) +{ + git_oid object_id; + git_commit *commit; + + if (!hex_id) + return NULL; + if (git_oid_fromstr(&object_id, hex_id)) + return NULL; + if (git_commit_lookup(&commit, repo, &object_id)) + return NULL; + return (git_object *)commit; +} + +static int notify_cb(git_checkout_notify_t why, + const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, + void *payload) +{ + report_error("File '%s' does not match in working tree", path); + return 0; /* Continue with checkout */ +} + +static git_tree *get_git_tree(git_repository *repo, git_object *parent) +{ + git_tree *tree; + if (!parent) + return NULL; + if (git_tree_lookup(&tree, repo, git_commit_tree_id((const git_commit *) parent))) + return NULL; + return tree; +} + +int update_git_checkout(git_repository *repo, git_object *parent, git_tree *tree) +{ + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + + opts.checkout_strategy = GIT_CHECKOUT_SAFE; + opts.notify_flags = GIT_CHECKOUT_NOTIFY_CONFLICT | GIT_CHECKOUT_NOTIFY_DIRTY; + opts.notify_cb = notify_cb; + opts.baseline = get_git_tree(repo, parent); + return git_checkout_tree(repo, (git_object *) tree, &opts); +} + +static int get_authorship(git_repository *repo, git_signature **authorp) +{ +#if LIBGIT2_VER_MAJOR || LIBGIT2_VER_MINOR >= 20 + if (git_signature_default(authorp, repo) == 0) + return 0; +#endif + /* Default name information, with potential OS overrides */ + struct user_info user = { + .name = "Subsurface", + .email = "subsurace@subsurface-divelog.org" + }; + + subsurface_user_info(&user); + + /* git_signature_default() is too recent */ + return git_signature_now(authorp, user.name, user.email); +} + +static void create_commit_message(struct membuffer *msg) +{ + int nr = dive_table.nr; + struct dive *dive = get_dive(nr-1); + + if (dive) { + dive_trip_t *trip = dive->divetrip; + const char *location = get_dive_location(dive) ? : "no location"; + struct divecomputer *dc = &dive->dc; + const char *sep = "\n"; + + if (dive->number) + nr = dive->number; + + put_format(msg, "dive %d: %s", nr, location); + if (trip && trip->location && *trip->location && strcmp(trip->location, location)) + put_format(msg, " (%s)", trip->location); + put_format(msg, "\n"); + do { + if (dc->model && *dc->model) { + put_format(msg, "%s%s", sep, dc->model); + sep = ", "; + } + } while ((dc = dc->next) != NULL); + put_format(msg, "\n"); + } + put_format(msg, "Created by subsurface %s\n", subsurface_version()); +} + +static int create_new_commit(git_repository *repo, const char *remote, const char *branch, git_oid *tree_id) +{ + int ret; + git_reference *ref; + git_object *parent; + git_oid commit_id; + git_signature *author; + git_commit *commit; + git_tree *tree; + + ret = git_branch_lookup(&ref, repo, branch, GIT_BRANCH_LOCAL); + switch (ret) { + default: + return report_error("Bad branch '%s' (%s)", branch, strerror(errno)); + case GIT_EINVALIDSPEC: + return report_error("Invalid branch name '%s'", branch); + case GIT_ENOTFOUND: /* We'll happily create it */ + ref = NULL; + parent = try_to_find_parent(saved_git_id, repo); + break; + case 0: + if (git_reference_peel(&parent, ref, GIT_OBJ_COMMIT)) + return report_error("Unable to look up parent in branch '%s'", branch); + + if (saved_git_id) { + if (existing_filename) + fprintf(stderr, "existing filename %s\n", existing_filename); + const git_oid *id = git_commit_id((const git_commit *) parent); + /* if we are saving to the same git tree we got this from, let's make + * sure there is no confusion */ + if (same_string(existing_filename, remote) && git_oid_strcmp(id, saved_git_id)) + return report_error("The git branch does not match the git parent of the source"); + } + + /* all good */ + break; + } + + if (git_tree_lookup(&tree, repo, tree_id)) + return report_error("Could not look up newly created tree"); + + if (get_authorship(repo, &author)) + return report_error("No user name configuration in git repo"); + + /* If the parent commit has the same tree ID, do not create a new commit */ + if (parent && git_oid_equal(tree_id, git_commit_tree_id((const git_commit *) parent))) { + /* If the parent already came from the ref, the commit is already there */ + if (ref) + return 0; + /* Else we do want to create the new branch, but with the old commit */ + commit = (git_commit *) parent; + } else { + struct membuffer commit_msg = { 0 }; + + create_commit_message(&commit_msg); + if (git_commit_create_v(&commit_id, repo, NULL, author, author, NULL, mb_cstring(&commit_msg), tree, parent != NULL, parent)) + return report_error("Git commit create failed (%s)", strerror(errno)); + free_buffer(&commit_msg); + + if (git_commit_lookup(&commit, repo, &commit_id)) + return report_error("Could not look up newly created commit"); + } + + if (!ref) { + if (git_branch_create(&ref, repo, branch, commit, 0, author, "Create branch")) + return report_error("Failed to create branch '%s'", branch); + } + /* + * If it's a checked-out branch, try to also update the working + * tree and index. If that fails (dirty working tree or whatever), + * this is not technically a save error (we did save things in + * the object database), but it can cause extreme confusion, so + * warn about it. + */ + if (git_branch_is_head(ref) && !git_repository_is_bare(repo)) { + if (update_git_checkout(repo, parent, tree)) { + const git_error *err = giterr_last(); + const char *errstr = err ? err->message : strerror(errno); + report_error("Git branch '%s' is checked out, but worktree is dirty (%s)", + branch, errstr); + } + } + + if (git_reference_set_target(&ref, ref, &commit_id, author, "Subsurface save event")) + return report_error("Failed to update branch '%s'", branch); + set_git_id(&commit_id); + + git_signature_free(author); + + return 0; +} + +static int write_git_tree(git_repository *repo, struct dir *tree, git_oid *result) +{ + int ret; + struct dir *subdir; + + /* Write out our subdirectories, add them to the treebuilder, and free them */ + while ((subdir = tree->subdirs) != NULL) { + git_oid id; + + if (!write_git_tree(repo, subdir, &id)) + tree_insert(tree->files, subdir->name, subdir->unique, &id, GIT_FILEMODE_TREE); + tree->subdirs = subdir->sibling; + free(subdir); + }; + + /* .. write out the resulting treebuilder */ + ret = git_treebuilder_write(result, repo, tree->files); + + /* .. and free the now useless treebuilder */ + git_treebuilder_free(tree->files); + + return ret; +} + +int do_git_save(git_repository *repo, const char *branch, const char *remote, bool select_only, bool create_empty) +{ + struct dir tree; + git_oid id; + + if (verbose) + fprintf(stderr, "git storage: do git save\n"); + + /* Start with an empty tree: no subdirectories, no files */ + tree.name[0] = 0; + tree.subdirs = NULL; + if (git_treebuilder_new(&tree.files, repo, NULL)) + return report_error("git treebuilder failed"); + + if (!create_empty) + /* Populate our tree data structure */ + if (create_git_tree(repo, &tree, select_only)) + return -1; + + if (write_git_tree(repo, &tree, &id)) + return report_error("git tree write failed"); + + /* And save the tree! */ + if (create_new_commit(repo, remote, branch, &id)) + return report_error("creating commit failed"); + + if (remote && prefs.cloud_background_sync) { + /* now sync the tree with the cloud server */ + if (strstr(remote, prefs.cloud_git_url)) { + return sync_with_remote(repo, remote, branch, RT_HTTPS); + } + } + return 0; +} + +int git_save_dives(struct git_repository *repo, const char *branch, const char *remote, bool select_only) +{ + int ret; + + if (repo == dummy_git_repository) + return report_error("Unable to open git repository '%s'", branch); + ret = do_git_save(repo, branch, remote, select_only, false); + git_repository_free(repo); + free((void *)branch); + return ret; +} diff --git a/subsurface-core/save-html.c b/subsurface-core/save-html.c new file mode 100644 index 000000000..64ce94f66 --- /dev/null +++ b/subsurface-core/save-html.c @@ -0,0 +1,533 @@ +#include "save-html.h" +#include "qthelperfromc.h" +#include "gettext.h" +#include "stdio.h" + +void write_attribute(struct membuffer *b, const char *att_name, const char *value, const char *separator) +{ + if (!value) + value = "--"; + put_format(b, "\"%s\":\"", att_name); + put_HTML_quoted(b, value); + put_format(b, "\"%s", separator); +} + +void save_photos(struct membuffer *b, const char *photos_dir, struct dive *dive) +{ + struct picture *pic = dive->picture_list; + + if (!pic) + return; + + char *separator = "\"photos\":["; + do { + put_string(b, separator); + separator = ", "; + char *fname = get_file_name(local_file_path(pic)); + put_format(b, "{\"filename\":\"%s\"}", fname); + copy_image_and_overwrite(local_file_path(pic), photos_dir, fname); + free(fname); + pic = pic->next; + } while (pic); + put_string(b, "],"); +} + +void write_divecomputers(struct membuffer *b, struct dive *dive) +{ + put_string(b, "\"divecomputers\":["); + struct divecomputer *dc; + char *separator = ""; + for_each_dc (dive, dc) { + put_string(b, separator); + separator = ", "; + put_format(b, "{"); + write_attribute(b, "model", dc->model, ", "); + if (dc->deviceid) + put_format(b, "\"deviceid\":\"%08x\", ", dc->deviceid); + else + put_string(b, "\"deviceid\":\"--\", "); + if (dc->diveid) + put_format(b, "\"diveid\":\"%08x\" ", dc->diveid); + else + put_string(b, "\"diveid\":\"--\" "); + put_format(b, "}"); + } + put_string(b, "],"); +} + +void write_dive_status(struct membuffer *b, struct dive *dive) +{ + put_format(b, "\"sac\":\"%d\",", dive->sac); + put_format(b, "\"otu\":\"%d\",", dive->otu); + put_format(b, "\"cns\":\"%d\",", dive->cns); +} + +void put_HTML_bookmarks(struct membuffer *b, struct dive *dive) +{ + struct event *ev = dive->dc.events; + + if (!ev) + return; + + char *separator = "\"events\":["; + do { + put_string(b, separator); + separator = ", "; + put_format(b, "{\"name\":\"%s\",", ev->name); + put_format(b, "\"value\":\"%d\",", ev->value); + put_format(b, "\"type\":\"%d\",", ev->type); + put_format(b, "\"time\":\"%d\"}", ev->time.seconds); + ev = ev->next; + } while (ev); + put_string(b, "],"); +} + +static void put_weightsystem_HTML(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_weightsystems(dive); + + put_string(b, "\"Weights\":["); + + char *separator = ""; + + for (i = 0; i < nr; i++) { + weightsystem_t *ws = dive->weightsystem + i; + int grams = ws->weight.grams; + const char *description = ws->description; + + put_string(b, separator); + separator = ", "; + put_string(b, "{"); + put_HTML_weight_units(b, grams, "\"weight\":\"", "\","); + write_attribute(b, "description", description, " "); + put_string(b, "}"); + } + put_string(b, "],"); +} + +static void put_cylinder_HTML(struct membuffer *b, struct dive *dive) +{ + int i, nr; + char *separator = "\"Cylinders\":["; + nr = nr_cylinders(dive); + + if (!nr) + put_string(b, separator); + + for (i = 0; i < nr; i++) { + cylinder_t *cylinder = dive->cylinder + i; + put_format(b, "%s{", separator); + separator = ", "; + write_attribute(b, "Type", cylinder->type.description, ", "); + if (cylinder->type.size.mliter) { + int volume = cylinder->type.size.mliter; + if (prefs.units.volume == CUFT && cylinder->type.workingpressure.mbar) + volume *= bar_to_atm(cylinder->type.workingpressure.mbar / 1000.0); + put_HTML_volume_units(b, volume, "\"Size\":\"", " \", "); + } else { + write_attribute(b, "Size", "--", ", "); + } + put_HTML_pressure_units(b, cylinder->type.workingpressure, "\"WPressure\":\"", " \", "); + + if (cylinder->start.mbar) { + put_HTML_pressure_units(b, cylinder->start, "\"SPressure\":\"", " \", "); + } else { + write_attribute(b, "SPressure", "--", ", "); + } + + if (cylinder->end.mbar) { + put_HTML_pressure_units(b, cylinder->end, "\"EPressure\":\"", " \", "); + } else { + write_attribute(b, "EPressure", "--", ", "); + } + + if (cylinder->gasmix.o2.permille) { + put_format(b, "\"O2\":\"%u.%u%%\",", FRACTION(cylinder->gasmix.o2.permille, 10)); + put_format(b, "\"He\":\"%u.%u%%\"", FRACTION(cylinder->gasmix.he.permille, 10)); + } else { + write_attribute(b, "O2", "Air", ""); + } + + put_string(b, "}"); + } + + put_string(b, "],"); +} + + +void put_HTML_samples(struct membuffer *b, struct dive *dive) +{ + int i; + put_format(b, "\"maxdepth\":%d,", dive->dc.maxdepth.mm); + put_format(b, "\"duration\":%d,", dive->dc.duration.seconds); + struct sample *s = dive->dc.sample; + + if (!dive->dc.samples) + return; + + char *separator = "\"samples\":["; + for (i = 0; i < dive->dc.samples; i++) { + put_format(b, "%s[%d,%d,%d,%d]", separator, s->time.seconds, s->depth.mm, s->cylinderpressure.mbar, s->temperature.mkelvin); + separator = ", "; + s++; + } + put_string(b, "],"); +} + +void put_HTML_coordinates(struct membuffer *b, struct dive *dive) +{ + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds) + return; + degrees_t latitude = ds->latitude; + degrees_t longitude = ds->longitude; + + //don't put coordinates if in (0,0) + if (!latitude.udeg && !longitude.udeg) + return; + + put_string(b, "\"coordinates\":{"); + put_degrees(b, latitude, "\"lat\":\"", "\","); + put_degrees(b, longitude, "\"lon\":\"", "\""); + put_string(b, "},"); +} + +void put_HTML_date(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + struct tm tm; + utc_mkdate(dive->when, &tm); + put_format(b, "%s%04u-%02u-%02u%s", pre, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, post); +} + +void put_HTML_quoted(struct membuffer *b, const char *text) +{ + int is_html = 1, is_attribute = 1; + put_quoted(b, text, is_attribute, is_html); +} + +void put_HTML_notes(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + put_string(b, pre); + if (dive->notes) { + put_HTML_quoted(b, dive->notes); + } else { + put_string(b, "--"); + } + put_string(b, post); +} + +void put_HTML_pressure_units(struct membuffer *b, pressure_t pressure, const char *pre, const char *post) +{ + const char *unit; + double value; + + if (!pressure.mbar) { + put_format(b, "%s%s", pre, post); + return; + } + + value = get_pressure_units(pressure.mbar, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_volume_units(struct membuffer *b, unsigned int ml, const char *pre, const char *post) +{ + const char *unit; + double value; + int frac; + + value = get_volume_units(ml, &frac, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_weight_units(struct membuffer *b, unsigned int grams, const char *pre, const char *post) +{ + const char *unit; + double value; + int frac; + + value = get_weight_units(grams, &frac, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_time(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + struct tm tm; + utc_mkdate(dive->when, &tm); + put_format(b, "%s%02u:%02u:%02u%s", pre, tm.tm_hour, tm.tm_min, tm.tm_sec, post); +} + +void put_HTML_airtemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + const char *unit; + double value; + + if (!dive->airtemp.mkelvin) { + put_format(b, "%s--%s", pre, post); + return; + } + value = get_temp_units(dive->airtemp.mkelvin, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_watertemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + const char *unit; + double value; + + if (!dive->watertemp.mkelvin) { + put_format(b, "%s--%s", pre, post); + return; + } + value = get_temp_units(dive->watertemp.mkelvin, &unit); + put_format(b, "%s%.1f %s%s", pre, value, unit, post); +} + +void put_HTML_tags(struct membuffer *b, struct dive *dive, const char *pre, const char *post) +{ + put_string(b, pre); + struct tag_entry *tag = dive->tag_list; + + if (!tag) + put_string(b, "[\"--\""); + + char *separator = "["; + while (tag) { + put_format(b, "%s\"", separator); + separator = ", "; + put_HTML_quoted(b, tag->tag->name); + put_string(b, "\""); + tag = tag->next; + } + put_string(b, "]"); + put_string(b, post); +} + +/* if exporting list_only mode, we neglect exporting the samples, bookmarks and cylinders */ +void write_one_dive(struct membuffer *b, struct dive *dive, const char *photos_dir, int *dive_no, const bool list_only) +{ + put_string(b, "{"); + put_format(b, "\"number\":%d,", *dive_no); + put_format(b, "\"subsurface_number\":%d,", dive->number); + put_HTML_date(b, dive, "\"date\":\"", "\","); + put_HTML_time(b, dive, "\"time\":\"", "\","); + write_attribute(b, "location", get_dive_location(dive), ", "); + put_HTML_coordinates(b, dive); + put_format(b, "\"rating\":%d,", dive->rating); + put_format(b, "\"visibility\":%d,", dive->visibility); + put_format(b, "\"dive_duration\":\"%u:%02u min\",", + FRACTION(dive->duration.seconds, 60)); + put_string(b, "\"temperature\":{"); + put_HTML_airtemp(b, dive, "\"air\":\"", "\","); + put_HTML_watertemp(b, dive, "\"water\":\"", "\""); + put_string(b, " },"); + write_attribute(b, "buddy", dive->buddy, ", "); + write_attribute(b, "divemaster", dive->divemaster, ", "); + write_attribute(b, "suit", dive->suit, ", "); + put_HTML_tags(b, dive, "\"tags\":", ","); + if (!list_only) { + put_cylinder_HTML(b, dive); + put_weightsystem_HTML(b, dive); + put_HTML_samples(b, dive); + put_HTML_bookmarks(b, dive); + write_dive_status(b, dive); + if (photos_dir && strcmp(photos_dir, "")) + save_photos(b, photos_dir, dive); + write_divecomputers(b, dive); + } + put_HTML_notes(b, dive, "\"notes\":\"", "\""); + put_string(b, "}\n"); + (*dive_no)++; +} + +void write_no_trip(struct membuffer *b, int *dive_no, bool selected_only, const char *photos_dir, const bool list_only, char *sep) +{ + int i; + struct dive *dive; + char *separator = ""; + bool found_sel_dive = 0; + + for_each_dive (i, dive) { + // write dive if it doesn't belong to any trip and the dive is selected + // or we are in exporting all dives mode. + if (!dive->divetrip && (dive->selected || !selected_only)) { + if (!found_sel_dive) { + put_format(b, "%c{", *sep); + (*sep) = ','; + put_format(b, "\"name\":\"Other\","); + put_format(b, "\"dives\":["); + found_sel_dive = 1; + } + put_string(b, separator); + separator = ", "; + write_one_dive(b, dive, photos_dir, dive_no, list_only); + } + } + if (found_sel_dive) + put_format(b, "]}\n\n"); +} + +void write_trip(struct membuffer *b, dive_trip_t *trip, int *dive_no, bool selected_only, const char *photos_dir, const bool list_only, char *sep) +{ + struct dive *dive; + char *separator = ""; + bool found_sel_dive = 0; + + for (dive = trip->dives; dive != NULL; dive = dive->next) { + if (!dive->selected && selected_only) + continue; + + // save trip if found at least one selected dive. + if (!found_sel_dive) { + found_sel_dive = 1; + put_format(b, "%c {", *sep); + (*sep) = ','; + put_format(b, "\"name\":\"%s\",", trip->location); + put_format(b, "\"dives\":["); + } + put_string(b, separator); + separator = ", "; + write_one_dive(b, dive, photos_dir, dive_no, list_only); + } + + // close the trip object if contain dives. + if (found_sel_dive) + put_format(b, "]}\n\n"); +} + +void write_trips(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only) +{ + int i, dive_no = 0; + struct dive *dive; + dive_trip_t *trip; + char sep_ = ' '; + char *sep = &sep_; + + for (trip = dive_trip_list; trip != NULL; trip = trip->next) + trip->index = 0; + + for_each_dive (i, dive) { + trip = dive->divetrip; + + /*Continue if the dive have no trips or we have seen this trip before*/ + if (!trip || trip->index) + continue; + + /* We haven't seen this trip before - save it and all dives */ + trip->index = 1; + write_trip(b, trip, &dive_no, selected_only, photos_dir, list_only, sep); + } + + /*Save all remaining trips into Others*/ + write_no_trip(b, &dive_no, selected_only, photos_dir, list_only, sep); +} + +void export_list(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only) +{ + put_string(b, "trips=["); + write_trips(b, photos_dir, selected_only, list_only); + put_string(b, "]"); +} + +void export_HTML(const char *file_name, const char *photos_dir, const bool selected_only, const bool list_only) +{ + FILE *f; + + struct membuffer buf = { 0 }; + export_list(&buf, photos_dir, selected_only, list_only); + + f = subsurface_fopen(file_name, "w+"); + if (!f) { + report_error(translate("gettextFromC", "Can't open file %s"), file_name); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} + +void export_translation(const char *file_name) +{ + FILE *f; + + struct membuffer buf = { 0 }; + struct membuffer *b = &buf; + + //export translated words here + put_format(b, "translate={"); + + //Dive list view + write_attribute(b, "Number", translate("gettextFromC", "Number"), ", "); + write_attribute(b, "Date", translate("gettextFromC", "Date"), ", "); + write_attribute(b, "Time", translate("gettextFromC", "Time"), ", "); + write_attribute(b, "Location", translate("gettextFromC", "Location"), ", "); + write_attribute(b, "Air_Temp", translate("gettextFromC", "Air temp."), ", "); + write_attribute(b, "Water_Temp", translate("gettextFromC", "Water temp."), ", "); + write_attribute(b, "dives", translate("gettextFromC", "Dives"), ", "); + write_attribute(b, "Expand_All", translate("gettextFromC", "Expand all"), ", "); + write_attribute(b, "Collapse_All", translate("gettextFromC", "Collapse all"), ", "); + write_attribute(b, "trips", translate("gettextFromC", "Trips"), ", "); + write_attribute(b, "Statistics", translate("gettextFromC", "Statistics"), ", "); + write_attribute(b, "Advanced_Search", translate("gettextFromC", "Advanced search"), ", "); + + //Dive expanded view + write_attribute(b, "Rating", translate("gettextFromC", "Rating"), ", "); + write_attribute(b, "Visibility", translate("gettextFromC", "Visibility"), ", "); + write_attribute(b, "Duration", translate("gettextFromC", "Duration"), ", "); + write_attribute(b, "DiveMaster", translate("gettextFromC", "Divemaster"), ", "); + write_attribute(b, "Buddy", translate("gettextFromC", "Buddy"), ", "); + write_attribute(b, "Suit", translate("gettextFromC", "Suit"), ", "); + write_attribute(b, "Tags", translate("gettextFromC", "Tags"), ", "); + write_attribute(b, "Notes", translate("gettextFromC", "Notes"), ", "); + write_attribute(b, "Show_more_details", translate("gettextFromC", "Show more details"), ", "); + + //Yearly statistics view + write_attribute(b, "Yearly_statistics", translate("gettextFromC", "Yearly statistics"), ", "); + write_attribute(b, "Year", translate("gettextFromC", "Year"), ", "); + write_attribute(b, "Total_Time", translate("gettextFromC", "Total time"), ", "); + write_attribute(b, "Average_Time", translate("gettextFromC", "Average time"), ", "); + write_attribute(b, "Shortest_Time", translate("gettextFromC", "Shortest time"), ", "); + write_attribute(b, "Longest_Time", translate("gettextFromC", "Longest time"), ", "); + write_attribute(b, "Average_Depth", translate("gettextFromC", "Average depth"), ", "); + write_attribute(b, "Min_Depth", translate("gettextFromC", "Min. depth"), ", "); + write_attribute(b, "Max_Depth", translate("gettextFromC", "Max. depth"), ", "); + write_attribute(b, "Average_SAC", translate("gettextFromC", "Average SAC"), ", "); + write_attribute(b, "Min_SAC", translate("gettextFromC", "Min. SAC"), ", "); + write_attribute(b, "Max_SAC", translate("gettextFromC", "Max. SAC"), ", "); + write_attribute(b, "Average_Temp", translate("gettextFromC", "Average temp."), ", "); + write_attribute(b, "Min_Temp", translate("gettextFromC", "Min. temp."), ", "); + write_attribute(b, "Max_Temp", translate("gettextFromC", "Max. temp."), ", "); + write_attribute(b, "Back_to_List", translate("gettextFromC", "Back to list"), ", "); + + //dive detailed view + write_attribute(b, "Dive_No", translate("gettextFromC", "Dive No."), ", "); + write_attribute(b, "Dive_profile", translate("gettextFromC", "Dive profile"), ", "); + write_attribute(b, "Dive_information", translate("gettextFromC", "Dive information"), ", "); + write_attribute(b, "Dive_equipment", translate("gettextFromC", "Dive equipment"), ", "); + write_attribute(b, "Type", translate("gettextFromC", "Type"), ", "); + write_attribute(b, "Size", translate("gettextFromC", "Size"), ", "); + write_attribute(b, "Work_Pressure", translate("gettextFromC", "Work pressure"), ", "); + write_attribute(b, "Start_Pressure", translate("gettextFromC", "Start pressure"), ", "); + write_attribute(b, "End_Pressure", translate("gettextFromC", "End pressure"), ", "); + write_attribute(b, "Gas", translate("gettextFromC", "Gas"), ", "); + write_attribute(b, "Weight", translate("gettextFromC", "Weight"), ", "); + write_attribute(b, "Type", translate("gettextFromC", "Type"), ", "); + write_attribute(b, "Events", translate("gettextFromC", "Events"), ", "); + write_attribute(b, "Name", translate("gettextFromC", "Name"), ", "); + write_attribute(b, "Value", translate("gettextFromC", "Value"), ", "); + write_attribute(b, "Coordinates", translate("gettextFromC", "Coordinates"), ", "); + write_attribute(b, "Dive_Status", translate("gettextFromC", "Dive status"), " "); + + put_format(b, "}"); + + f = subsurface_fopen(file_name, "w+"); + if (!f) { + report_error(translate("gettextFromC", "Can't open file %s"), file_name); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} diff --git a/subsurface-core/save-html.h b/subsurface-core/save-html.h new file mode 100644 index 000000000..20743e90a --- /dev/null +++ b/subsurface-core/save-html.h @@ -0,0 +1,30 @@ +#ifndef HTML_SAVE_H +#define HTML_SAVE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "dive.h" +#include "membuffer.h" + +void put_HTML_date(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_airtemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_watertemp(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_time(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_notes(struct membuffer *b, struct dive *dive, const char *pre, const char *post); +void put_HTML_quoted(struct membuffer *b, const char *text); +void put_HTML_pressure_units(struct membuffer *b, pressure_t pressure, const char *pre, const char *post); +void put_HTML_weight_units(struct membuffer *b, unsigned int grams, const char *pre, const char *post); +void put_HTML_volume_units(struct membuffer *b, unsigned int ml, const char *pre, const char *post); + +void export_HTML(const char *file_name, const char *photos_dir, const bool selected_only, const bool list_only); +void export_list(struct membuffer *b, const char *photos_dir, bool selected_only, const bool list_only); + +void export_translation(const char *file_name); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/subsurface-core/save-xml.c b/subsurface-core/save-xml.c new file mode 100644 index 000000000..166885861 --- /dev/null +++ b/subsurface-core/save-xml.c @@ -0,0 +1,743 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dive.h" +#include "divelist.h" +#include "device.h" +#include "membuffer.h" +#include "strndup.h" +#include "git-access.h" +#include "qthelperfromc.h" + +/* + * We're outputting utf8 in xml. + * We need to quote the characters <, >, &. + * + * Technically I don't think we'd necessarily need to quote the control + * characters, but at least libxml2 doesn't like them. It doesn't even + * allow them quoted. So we just skip them and replace them with '?'. + * + * If we do this for attributes, we need to quote the quotes we use too. + */ +static void quote(struct membuffer *b, const char *text, int is_attribute) +{ + int is_html = 0; + put_quoted(b, text, is_attribute, is_html); +} + +static void show_utf8(struct membuffer *b, const char *text, const char *pre, const char *post, int is_attribute) +{ + int len; + char *cleaned; + + if (!text) + return; + /* remove leading and trailing space */ + /* We need to combine isascii() with isspace(), + * because we can only trust isspace() with 7-bit ascii, + * on windows for example */ + while (isascii(*text) && isspace(*text)) + text++; + len = strlen(text); + if (!len) + return; + while (len && isascii(text[len - 1]) && isspace(text[len - 1])) + len--; + cleaned = strndup(text, len); + put_string(b, pre); + quote(b, cleaned, is_attribute); + put_string(b, post); + free(cleaned); +} + +static void save_depths(struct membuffer *b, struct divecomputer *dc) +{ + /* What's the point of this dive entry again? */ + if (!dc->maxdepth.mm && !dc->meandepth.mm) + return; + + put_string(b, " maxdepth, " max='", " m'"); + put_depth(b, dc->meandepth, " mean='", " m'"); + put_string(b, " />\n"); +} + +static void save_dive_temperature(struct membuffer *b, struct dive *dive) +{ + if (!dive->airtemp.mkelvin && !dive->watertemp.mkelvin) + return; + if (dive->airtemp.mkelvin == dc_airtemp(&dive->dc) && dive->watertemp.mkelvin == dc_watertemp(&dive->dc)) + return; + + put_string(b, " airtemp.mkelvin != dc_airtemp(&dive->dc)) + put_temperature(b, dive->airtemp, " air='", " C'"); + if (dive->watertemp.mkelvin != dc_watertemp(&dive->dc)) + put_temperature(b, dive->watertemp, " water='", " C'"); + put_string(b, "/>\n"); +} + +static void save_temperatures(struct membuffer *b, struct divecomputer *dc) +{ + if (!dc->airtemp.mkelvin && !dc->watertemp.mkelvin) + return; + put_string(b, " airtemp, " air='", " C'"); + put_temperature(b, dc->watertemp, " water='", " C'"); + put_string(b, " />\n"); +} + +static void save_airpressure(struct membuffer *b, struct divecomputer *dc) +{ + if (!dc->surface_pressure.mbar) + return; + put_string(b, " surface_pressure, " pressure='", " bar'"); + put_string(b, " />\n"); +} + +static void save_salinity(struct membuffer *b, struct divecomputer *dc) +{ + /* only save if we have a value that isn't the default of sea water */ + if (!dc->salinity || dc->salinity == SEAWATER_SALINITY) + return; + put_string(b, " salinity, " salinity='", " g/l'"); + put_string(b, " />\n"); +} + +static void save_overview(struct membuffer *b, struct dive *dive) +{ + show_utf8(b, dive->divemaster, " ", "\n", 0); + show_utf8(b, dive->buddy, " ", "\n", 0); + show_utf8(b, dive->notes, " ", "\n", 0); + show_utf8(b, dive->suit, " ", "\n", 0); +} + +static void put_gasmix(struct membuffer *b, struct gasmix *mix) +{ + int o2 = mix->o2.permille; + int he = mix->he.permille; + + if (o2) { + put_format(b, " o2='%u.%u%%'", FRACTION(o2, 10)); + if (he) + put_format(b, " he='%u.%u%%'", FRACTION(he, 10)); + } +} + +static void save_cylinder_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_cylinders(dive); + + for (i = 0; i < nr; i++) { + cylinder_t *cylinder = dive->cylinder + i; + int volume = cylinder->type.size.mliter; + const char *description = cylinder->type.description; + + put_format(b, " type.workingpressure, " workpressure='", " bar'"); + show_utf8(b, description, " description='", "'", 1); + put_gasmix(b, &cylinder->gasmix); + put_pressure(b, cylinder->start, " start='", " bar'"); + put_pressure(b, cylinder->end, " end='", " bar'"); + if (cylinder->cylinder_use != OC_GAS) + show_utf8(b, cylinderuse_text[cylinder->cylinder_use], " use='", "'", 1); + put_format(b, " />\n"); + } +} + +static void save_weightsystem_info(struct membuffer *b, struct dive *dive) +{ + int i, nr; + + nr = nr_weightsystems(dive); + + for (i = 0; i < nr; i++) { + weightsystem_t *ws = dive->weightsystem + i; + int grams = ws->weight.grams; + const char *description = ws->description; + + put_format(b, " \n"); + } +} + +static void show_index(struct membuffer *b, int value, const char *pre, const char *post) +{ + if (value) + put_format(b, " %s%d%s", pre, value, post); +} + +static void save_sample(struct membuffer *b, struct sample *sample, struct sample *old) +{ + put_format(b, " time.seconds, 60)); + put_milli(b, " depth='", sample->depth.mm, " m'"); + if (sample->temperature.mkelvin && sample->temperature.mkelvin != old->temperature.mkelvin) { + put_temperature(b, sample->temperature, " temp='", " C'"); + old->temperature = sample->temperature; + } + put_pressure(b, sample->cylinderpressure, " pressure='", " bar'"); + put_pressure(b, sample->o2cylinderpressure, " o2pressure='", " bar'"); + + /* + * We only show sensor information for samples with pressure, and only if it + * changed from the previous sensor we showed. + */ + if (sample->cylinderpressure.mbar && sample->sensor != old->sensor) { + put_format(b, " sensor='%d'", sample->sensor); + old->sensor = sample->sensor; + } + + /* the deco/ndl values are stored whenever they change */ + if (sample->ndl.seconds != old->ndl.seconds) { + put_format(b, " ndl='%u:%02u min'", FRACTION(sample->ndl.seconds, 60)); + old->ndl = sample->ndl; + } + if (sample->tts.seconds != old->tts.seconds) { + put_format(b, " tts='%u:%02u min'", FRACTION(sample->tts.seconds, 60)); + old->tts = sample->tts; + } + if (sample->rbt.seconds) + put_format(b, " rbt='%u:%02u min'", FRACTION(sample->rbt.seconds, 60)); + if (sample->in_deco != old->in_deco) { + put_format(b, " in_deco='%d'", sample->in_deco ? 1 : 0); + old->in_deco = sample->in_deco; + } + if (sample->stoptime.seconds != old->stoptime.seconds) { + put_format(b, " stoptime='%u:%02u min'", FRACTION(sample->stoptime.seconds, 60)); + old->stoptime = sample->stoptime; + } + + if (sample->stopdepth.mm != old->stopdepth.mm) { + put_milli(b, " stopdepth='", sample->stopdepth.mm, " m'"); + old->stopdepth = sample->stopdepth; + } + + if (sample->cns != old->cns) { + put_format(b, " cns='%u%%'", sample->cns); + old->cns = sample->cns; + } + + if ((sample->o2sensor[0].mbar) && (sample->o2sensor[0].mbar != old->o2sensor[0].mbar)) { + put_milli(b, " sensor1='", sample->o2sensor[0].mbar, " bar'"); + old->o2sensor[0] = sample->o2sensor[0]; + } + + if ((sample->o2sensor[1].mbar) && (sample->o2sensor[1].mbar != old->o2sensor[1].mbar)) { + put_milli(b, " sensor2='", sample->o2sensor[1].mbar, " bar'"); + old->o2sensor[1] = sample->o2sensor[1]; + } + + if ((sample->o2sensor[2].mbar) && (sample->o2sensor[2].mbar != old->o2sensor[2].mbar)) { + put_milli(b, " sensor3='", sample->o2sensor[2].mbar, " bar'"); + old->o2sensor[2] = sample->o2sensor[2]; + } + + if (sample->setpoint.mbar != old->setpoint.mbar) { + put_milli(b, " po2='", sample->setpoint.mbar, " bar'"); + old->setpoint = sample->setpoint; + } + show_index(b, sample->heartbeat, "heartbeat='", "'"); + show_index(b, sample->bearing.degrees, "bearing='", "'"); + put_format(b, " />\n"); +} + +static void save_one_event(struct membuffer *b, struct event *ev) +{ + put_format(b, " time.seconds, 60)); + show_index(b, ev->type, "type='", "'"); + show_index(b, ev->flags, "flags='", "'"); + show_index(b, ev->value, "value='", "'"); + show_utf8(b, ev->name, " name='", "'", 1); + if (event_is_gaschange(ev)) { + if (ev->gas.index >= 0) { + show_index(b, ev->gas.index, "cylinder='", "'"); + put_gasmix(b, &ev->gas.mix); + } else if (!event_gasmix_redundant(ev)) + put_gasmix(b, &ev->gas.mix); + } + put_format(b, " />\n"); +} + + +static void save_events(struct membuffer *b, struct event *ev) +{ + while (ev) { + save_one_event(b, ev); + ev = ev->next; + } +} + +static void save_tags(struct membuffer *b, struct tag_entry *entry) +{ + if (entry) { + const char *sep = " tags='"; + do { + struct divetag *tag = entry->tag; + put_string(b, sep); + /* If the tag has been translated, write the source to the xml file */ + quote(b, tag->source ?: tag->name, 1); + sep = ", "; + } while ((entry = entry->next) != NULL); + put_string(b, "'"); + } +} + +static void save_extra_data(struct membuffer *b, struct extra_data *ed) +{ + while (ed) { + if (ed->key && ed->value) { + put_string(b, " key, " key='", "'", 1); + show_utf8(b, ed->value, " value='", "'", 1); + put_string(b, " />\n"); + } + ed = ed->next; + } +} + +static void show_date(struct membuffer *b, timestamp_t when) +{ + struct tm tm; + + utc_mkdate(when, &tm); + + put_format(b, " date='%04u-%02u-%02u'", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); + put_format(b, " time='%02u:%02u:%02u'", + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void save_samples(struct membuffer *b, int nr, struct sample *s) +{ + struct sample dummy = {}; + + while (--nr >= 0) { + save_sample(b, s, &dummy); + s++; + } +} + +static void save_dc(struct membuffer *b, struct dive *dive, struct divecomputer *dc) +{ + put_format(b, " model, " model='", "'", 1); + if (dc->deviceid) + put_format(b, " deviceid='%08x'", dc->deviceid); + if (dc->diveid) + put_format(b, " diveid='%08x'", dc->diveid); + if (dc->when && dc->when != dive->when) + show_date(b, dc->when); + if (dc->duration.seconds && dc->duration.seconds != dive->dc.duration.seconds) + put_duration(b, dc->duration, " duration='", " min'"); + if (dc->divemode != OC) { + for (enum dive_comp_type i = 0; i < NUM_DC_TYPE; i++) + if (dc->divemode == i) + show_utf8(b, divemode_text[i], " dctype='", "'", 1); + if (dc->no_o2sensors) + put_format(b," no_o2sensors='%d'", dc->no_o2sensors); + } + put_format(b, ">\n"); + save_depths(b, dc); + save_temperatures(b, dc); + save_airpressure(b, dc); + save_salinity(b, dc); + put_duration(b, dc->surfacetime, " ", " min\n"); + save_extra_data(b, dc->extra_data); + save_events(b, dc->events); + save_samples(b, dc->samples, dc->sample); + + put_format(b, " \n"); +} + +static void save_picture(struct membuffer *b, struct picture *pic) +{ + put_string(b, " offset.seconds) { + int offset = pic->offset.seconds; + char sign = '+'; + if (offset < 0) { + sign = '-'; + offset = -offset; + } + put_format(b, " offset='%c%u:%02u min'", sign, FRACTION(offset, 60)); + } + if (pic->latitude.udeg || pic->longitude.udeg) { + put_degrees(b, pic->latitude, " gps='", " "); + put_degrees(b, pic->longitude, "", "'"); + } + if (hashstring(pic->filename)) + put_format(b, " hash='%s'", hashstring(pic->filename)); + + put_string(b, "/>\n"); +} + +void save_one_dive_to_mb(struct membuffer *b, struct dive *dive) +{ + struct divecomputer *dc; + + put_string(b, "number) + put_format(b, " number='%d'", dive->number); + if (dive->tripflag == NO_TRIP) + put_format(b, " tripflag='NOTRIP'"); + if (dive->rating) + put_format(b, " rating='%d'", dive->rating); + if (dive->visibility) + put_format(b, " visibility='%d'", dive->visibility); + save_tags(b, dive->tag_list); + if (dive->dive_site_uuid) { + if (get_dive_site_by_uuid(dive->dive_site_uuid) != NULL) + put_format(b, " divesiteid='%8x'", dive->dive_site_uuid); + else if (verbose) + fprintf(stderr, "removed reference to non-existant dive site with uuid %08x\n", dive->dive_site_uuid); + } + show_date(b, dive->when); + put_format(b, " duration='%u:%02u min'>\n", + FRACTION(dive->dc.duration.seconds, 60)); + save_overview(b, dive); + save_cylinder_info(b, dive); + save_weightsystem_info(b, dive); + save_dive_temperature(b, dive); + /* Save the dive computer data */ + for_each_dc(dive, dc) + save_dc(b, dive, dc); + FOR_EACH_PICTURE(dive) + save_picture(b, picture); + put_format(b, "\n"); +} + +int save_dive(FILE *f, struct dive *dive) +{ + struct membuffer buf = { 0 }; + + save_one_dive_to_mb(&buf, dive); + flush_buffer(&buf, f); + /* Error handling? */ + return 0; +} + +static void save_trip(struct membuffer *b, dive_trip_t *trip) +{ + int i; + struct dive *dive; + + put_format(b, "when); + show_utf8(b, trip->location, " location=\'", "\'", 1); + put_format(b, ">\n"); + show_utf8(b, trip->notes, "", "\n", 0); + + /* + * Incredibly cheesy: we want to save the dives sorted, and they + * are sorted in the dive array.. So instead of using the dive + * list in the trip, we just traverse the global dive array and + * check the divetrip pointer.. + */ + for_each_dive(i, dive) { + if (dive->divetrip == trip) + save_one_dive_to_mb(b, dive); + } + + put_format(b, "\n"); +} + +static void save_one_device(void *_f, const char *model, uint32_t deviceid, + const char *nickname, const char *serial_nr, const char *firmware) +{ + struct membuffer *b = _f; + + /* Nicknames that are empty or the same as the device model are not interesting */ + if (nickname) { + if (!*nickname || !strcmp(model, nickname)) + nickname = NULL; + } + + /* Serial numbers that are empty are not interesting */ + if (serial_nr && !*serial_nr) + serial_nr = NULL; + + /* Firmware strings that are empty are not interesting */ + if (firmware && !*firmware) + firmware = NULL; + + /* Do we have anything interesting about this dive computer to save? */ + if (!serial_nr && !nickname && !firmware) + return; + + put_format(b, "\n"); +} + +int save_dives(const char *filename) +{ + return save_dives_logic(filename, false); +} + +void save_dives_buffer(struct membuffer *b, const bool select_only) +{ + int i; + struct dive *dive; + dive_trip_t *trip; + + put_format(b, "\n\n", DATAFORMAT_VERSION); + + if (prefs.save_userid_local) + put_format(b, " %30s\n", prefs.userid); + + /* save the dive computer nicknames, if any */ + call_for_each_dc(b, save_one_device, select_only); + if (autogroup) + put_format(b, " \n"); + put_format(b, "\n"); + + /* save the dive sites - to make the output consistent let's sort the table, first */ + dive_site_table_sort(); + put_format(b, "\n"); + for (i = 0; i < dive_site_table.nr; i++) { + int j; + struct dive *d; + struct dive_site *ds = get_dive_site(i); + if (dive_site_is_empty(ds)) { + for_each_dive(j, d) { + if (d->dive_site_uuid == ds->uuid) + d->dive_site_uuid = 0; + } + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } else if (ds->name && + (strncmp(ds->name, "Auto-created dive", 17) == 0 || + strncmp(ds->name, "New Dive", 8) == 0)) { + // these are the two default names for sites from + // the web service; if the site isn't used in any + // dive (really? you didn't rename it?), delete it + if (!is_dive_site_used(ds->uuid, false)) { + if (verbose) + fprintf(stderr, "Deleted unused auto-created dive site %s\n", ds->name); + delete_dive_site(ds->uuid); + i--; // since we just deleted that one + continue; + } + } + if (select_only && !is_dive_site_used(ds->uuid, true)) + continue; + + put_format(b, "uuid); + show_utf8(b, ds->name, " name='", "'", 1); + if (ds->latitude.udeg || ds->longitude.udeg) { + put_degrees(b, ds->latitude, " gps='", " "); + put_degrees(b, ds->longitude, "", "'"); + } + show_utf8(b, ds->description, " description='", "'", 1); + put_format(b, ">\n"); + show_utf8(b, ds->notes, " ", " \n", 0); + if (ds->taxonomy.nr) { + for (int j = 0; j < ds->taxonomy.nr; j++) { + struct taxonomy *t = &ds->taxonomy.category[j]; + if (t->category != TC_NONE && t->value) { + put_format(b, " category); + put_format(b, " origin='%d'", t->origin); + show_utf8(b, t->value, " value='", "'/>\n", 1); + } + } + } + put_format(b, "\n"); + } + put_format(b, "\n\n"); + for (trip = dive_trip_list; trip != NULL; trip = trip->next) + trip->index = 0; + + /* save the dives */ + for_each_dive(i, dive) { + if (select_only) { + + if (!dive->selected) + continue; + save_one_dive_to_mb(b, dive); + + } else { + trip = dive->divetrip; + + /* Bare dive without a trip? */ + if (!trip) { + save_one_dive_to_mb(b, dive); + continue; + } + + /* Have we already seen this trip (and thus saved this dive?) */ + if (trip->index) + continue; + + /* We haven't seen this trip before - save it and all dives */ + trip->index = 1; + save_trip(b, trip); + } + } + put_format(b, "\n\n"); +} + +static void save_backup(const char *name, const char *ext, const char *new_ext) +{ + int len = strlen(name); + int a = strlen(ext), b = strlen(new_ext); + char *newname; + + /* len up to and including the final '.' */ + len -= a; + if (len <= 1) + return; + if (name[len - 1] != '.') + return; + /* msvc doesn't have strncasecmp, has _strnicmp instead - crazy */ + if (strncasecmp(name + len, ext, a)) + return; + + newname = malloc(len + b + 1); + if (!newname) + return; + + memcpy(newname, name, len); + memcpy(newname + len, new_ext, b + 1); + + /* + * Ignore errors. Maybe we can't create the backup file, + * maybe no old file existed. Regardless, we'll write the + * new file. + */ + (void) subsurface_rename(name, newname); + free(newname); +} + +static void try_to_backup(const char *filename) +{ + char extension[][5] = { "xml", "ssrf", "" }; + int i = 0; + int flen = strlen(filename); + + /* Maybe we might want to make this configurable? */ + while (extension[i][0] != '\0') { + int elen = strlen(extension[i]); + if (strcasecmp(filename + flen - elen, extension[i]) == 0) { + if (last_xml_version < DATAFORMAT_VERSION) { + int se_len = strlen(extension[i]) + 5; + char *special_ext = malloc(se_len); + snprintf(special_ext, se_len, "%s.v%d", extension[i], last_xml_version); + save_backup(filename, extension[i], special_ext); + free(special_ext); + } else { + save_backup(filename, extension[i], "bak"); + } + break; + } + i++; + } +} + +int save_dives_logic(const char *filename, const bool select_only) +{ + struct membuffer buf = { 0 }; + FILE *f; + void *git; + const char *branch, *remote; + int error; + + git = is_git_repository(filename, &branch, &remote, false); + if (git) + return git_save_dives(git, branch, remote, select_only); + + try_to_backup(filename); + + save_dives_buffer(&buf, select_only); + + error = -1; + f = subsurface_fopen(filename, "w"); + if (f) { + flush_buffer(&buf, f); + error = fclose(f); + } + if (error) + report_error("Save failed (%s)", strerror(errno)); + + free_buffer(&buf); + return error; +} + +int export_dives_xslt(const char *filename, const bool selected, const int units, const char *export_xslt) +{ + FILE *f; + struct membuffer buf = { 0 }; + xmlDoc *doc; + xsltStylesheetPtr xslt = NULL; + xmlDoc *transformed; + int res = 0; + char *params[3]; + int pnr = 0; + char unitstr[3]; + + if (verbose) + fprintf(stderr, "export_dives_xslt with stylesheet %s\n", export_xslt); + + if (!filename) + return report_error("No filename for export"); + + /* Save XML to file and convert it into a memory buffer */ + save_dives_buffer(&buf, selected); + + /* + * Parse the memory buffer into XML document and + * transform it to selected export format, finally dumping + * the XML into a character buffer. + */ + doc = xmlReadMemory(buf.buffer, buf.len, "divelog", NULL, 0); + free_buffer(&buf); + if (!doc) + return report_error("Failed to read XML memory"); + + /* Convert to export format */ + xslt = get_stylesheet(export_xslt); + if (!xslt) + return report_error("Failed to open export conversion stylesheet"); + + snprintf(unitstr, 3, "%d", units); + params[pnr++] = "units"; + params[pnr++] = unitstr; + params[pnr++] = NULL; + + transformed = xsltApplyStylesheet(xslt, doc, (const char **)params); + xmlFreeDoc(doc); + + /* Write the transformed export to file */ + f = subsurface_fopen(filename, "w"); + if (f) { + xsltSaveResultToFile(f, transformed, xslt); + fclose(f); + /* Check write errors? */ + } else { + res = report_error("Failed to open %s for writing (%s)", filename, strerror(errno)); + } + xsltFreeStylesheet(xslt); + xmlFreeDoc(transformed); + + return res; +} diff --git a/subsurface-core/serial_ftdi.c b/subsurface-core/serial_ftdi.c new file mode 100644 index 000000000..cbac026cf --- /dev/null +++ b/subsurface-core/serial_ftdi.c @@ -0,0 +1,664 @@ +/* + * libdivecomputer + * + * Copyright (C) 2008 Jef Driesen + * Copyright (C) 2014 Venkatesh Shukla + * Copyright (C) 2015 Anton Lundin + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include // malloc, free +#include // strerror +#include // errno +#include // gettimeofday +#include // nanosleep +#include + +#include +#include + +#ifndef __ANDROID__ +#define INFO(context, fmt, ...) fprintf(stderr, "INFO: " fmt "\n", ##__VA_ARGS__) +#define ERROR(context, fmt, ...) fprintf(stderr, "ERROR: " fmt "\n", ##__VA_ARGS__) +#else +#include +#define INFO(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "INFO: " fmt "\n", ##__VA_ARGS__) +#define ERROR(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "ERROR: " fmt "\n", ##__VA_ARGS__) +#endif +//#define SYSERROR(context, errcode) ERROR(__FILE__ ":" __LINE__ ": %s", strerror(errcode)) +#define SYSERROR(context, errcode) ; + +#include + +/* Verbatim copied libdivecomputer enums to support configure */ +typedef enum serial_parity_t { + SERIAL_PARITY_NONE, + SERIAL_PARITY_EVEN, + SERIAL_PARITY_ODD +} serial_parity_t; + +typedef enum serial_flowcontrol_t { + SERIAL_FLOWCONTROL_NONE, + SERIAL_FLOWCONTROL_HARDWARE, + SERIAL_FLOWCONTROL_SOFTWARE +} serial_flowcontrol_t; + +typedef enum serial_queue_t { + SERIAL_QUEUE_INPUT = 0x01, + SERIAL_QUEUE_OUTPUT = 0x02, + SERIAL_QUEUE_BOTH = SERIAL_QUEUE_INPUT | SERIAL_QUEUE_OUTPUT +} serial_queue_t; + +typedef enum serial_line_t { + SERIAL_LINE_DCD, // Data carrier detect + SERIAL_LINE_CTS, // Clear to send + SERIAL_LINE_DSR, // Data set ready + SERIAL_LINE_RNG, // Ring indicator +} serial_line_t; + +#define VID 0x0403 // Vendor ID of FTDI + +#define MAX_BACKOFF 500 // Max milliseconds to wait before timing out. + +typedef struct serial_t { + /* Library context. */ + dc_context_t *context; + /* + * The file descriptor corresponding to the serial port. + * Also a libftdi_ftdi_ctx could be used? + */ + struct ftdi_context *ftdi_ctx; + long timeout; + /* + * Serial port settings are saved into this variable immediately + * after the port is opened. These settings are restored when the + * serial port is closed. + * Saving this using libftdi context or libusb. Search further. + * Custom implementation using libftdi functions could be done. + */ + + /* Half-duplex settings */ + int halfduplex; + unsigned int baudrate; + unsigned int nbits; +} serial_t; + +static int serial_ftdi_get_received (serial_t *device) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // Direct access is not encouraged. But function implementation + // is not available. The return quantity might be anything. + // Find out further about its possible values and correct way of + // access. + int bytes = device->ftdi_ctx->readbuffer_remaining; + + return bytes; +} + +static int serial_ftdi_get_transmitted (serial_t *device) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // This is not possible using libftdi. Look further into it. + return -1; +} + +static int serial_ftdi_sleep (serial_t *device, unsigned long timeout) +{ + if (device == NULL) + return -1; + + INFO (device->context, "Sleep: value=%lu", timeout); + + struct timespec ts; + ts.tv_sec = (timeout / 1000); + ts.tv_nsec = (timeout % 1000) * 1000000; + + while (nanosleep (&ts, &ts) != 0) { + if (errno != EINTR ) { + SYSERROR (device->context, errno); + return -1; + } + } + + return 0; +} + + +// Used internally for opening ftdi devices +static int serial_ftdi_open_device (struct ftdi_context *ftdi_ctx) +{ + int accepted_pids[] = { 0x6001, 0x6010, 0x6011, // Suunto (Smart Interface), Heinrichs Weikamp + 0xF460, // Oceanic + 0xF680, // Suunto + 0x87D0, // Cressi (Leonardo) + }; + int num_accepted_pids = 6; + int i, pid, ret; + for (i = 0; i < num_accepted_pids; i++) { + pid = accepted_pids[i]; + ret = ftdi_usb_open (ftdi_ctx, VID, pid); + if (ret == -3) // Device not found + continue; + else + return ret; + } + // No supported devices are attached. + return ret; +} + +// +// Open the serial port. +// Initialise ftdi_context and use it to open the device +// +//FIXME: ugly forward declaration of serial_ftdi_configure, util we support configure for real... +static dc_status_t serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol); +static dc_status_t serial_ftdi_open (serial_t **out, dc_context_t *context, const char* name) +{ + if (out == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (context, "Open: name=%s", name ? name : ""); + + // Allocate memory. + serial_t *device = (serial_t *) malloc (sizeof (serial_t)); + if (device == NULL) { + SYSERROR (context, errno); + return DC_STATUS_NOMEMORY; + } + + struct ftdi_context *ftdi_ctx = ftdi_new(); + if (ftdi_ctx == NULL) { + free(device); + SYSERROR (context, errno); + return DC_STATUS_NOMEMORY; + } + + // Library context. + device->context = context; + + // Default to blocking reads. + device->timeout = -1; + + // Default to full-duplex. + device->halfduplex = 0; + device->baudrate = 0; + device->nbits = 0; + + // Initialize device ftdi context + ftdi_init(ftdi_ctx); + + if (ftdi_set_interface(ftdi_ctx,INTERFACE_ANY)) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + if (serial_ftdi_open_device(ftdi_ctx) < 0) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + if (ftdi_usb_reset(ftdi_ctx)) { + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + if (ftdi_usb_purge_buffers(ftdi_ctx)) { + free(device); + ERROR (context, "%s", ftdi_get_error_string(ftdi_ctx)); + return DC_STATUS_IO; + } + + device->ftdi_ctx = ftdi_ctx; + + //FIXME: remove this when custom-serial have support for configure calls + serial_ftdi_configure (device, 115200, 8, 0, 1, 0); + + *out = device; + + return DC_STATUS_SUCCESS; +} + +// +// Close the serial port. +// +static int serial_ftdi_close (serial_t *device) +{ + if (device == NULL) + return 0; + + // Restore the initial terminal attributes. + // See if it is possible using libusb or libftdi + + int ret = ftdi_usb_close(device->ftdi_ctx); + if (ret < 0) { + ERROR (device->context, "Unable to close the ftdi device : %d (%s)\n", + ret, ftdi_get_error_string(device->ftdi_ctx)); + return ret; + } + + ftdi_free(device->ftdi_ctx); + + // Free memory. + free (device); + + return 0; +} + +// +// Configure the serial port (baudrate, databits, parity, stopbits and flowcontrol). +// +static dc_status_t serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Configure: baudrate=%i, databits=%i, parity=%i, stopbits=%i, flowcontrol=%i", + baudrate, databits, parity, stopbits, flowcontrol); + + enum ftdi_bits_type ft_bits; + enum ftdi_stopbits_type ft_stopbits; + enum ftdi_parity_type ft_parity; + + if (ftdi_set_baudrate(device->ftdi_ctx, baudrate) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + + // Set the character size. + switch (databits) { + case 7: + ft_bits = BITS_7; + break; + case 8: + ft_bits = BITS_8; + break; + default: + return DC_STATUS_INVALIDARGS; + } + + // Set the parity type. + switch (parity) { + case SERIAL_PARITY_NONE: // No parity + ft_parity = NONE; + break; + case SERIAL_PARITY_EVEN: // Even parity + ft_parity = EVEN; + break; + case SERIAL_PARITY_ODD: // Odd parity + ft_parity = ODD; + break; + default: + return DC_STATUS_INVALIDARGS; + } + + // Set the number of stop bits. + switch (stopbits) { + case 1: // One stopbit + ft_stopbits = STOP_BIT_1; + break; + case 2: // Two stopbits + ft_stopbits = STOP_BIT_2; + break; + default: + return DC_STATUS_INVALIDARGS; + } + + // Set the attributes + if (ftdi_set_line_property(device->ftdi_ctx, ft_bits, ft_stopbits, ft_parity)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + + // Set the flow control. + switch (flowcontrol) { + case SERIAL_FLOWCONTROL_NONE: // No flow control. + if (ftdi_setflowctrl(device->ftdi_ctx, SIO_DISABLE_FLOW_CTRL) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + break; + case SERIAL_FLOWCONTROL_HARDWARE: // Hardware (RTS/CTS) flow control. + if (ftdi_setflowctrl(device->ftdi_ctx, SIO_RTS_CTS_HS) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + break; + case SERIAL_FLOWCONTROL_SOFTWARE: // Software (XON/XOFF) flow control. + if (ftdi_setflowctrl(device->ftdi_ctx, SIO_XON_XOFF_HS) < 0) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return DC_STATUS_IO; + } + break; + default: + return DC_STATUS_INVALIDARGS; + } + + device->baudrate = baudrate; + device->nbits = 1 + databits + stopbits + (parity ? 1 : 0); + + return DC_STATUS_SUCCESS; +} + +// +// Configure the serial port (timeouts). +// +static int serial_ftdi_set_timeout (serial_t *device, long timeout) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Timeout: value=%li", timeout); + + device->timeout = timeout; + + return 0; +} + +static int serial_ftdi_set_halfduplex (serial_t *device, int value) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // Most ftdi chips support full duplex operation. ft232rl does. + // Crosscheck other chips. + + device->halfduplex = value; + + return 0; +} + +static int serial_ftdi_read (serial_t *device, void *data, unsigned int size) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + // The total timeout. + long timeout = device->timeout; + + // The absolute target time. + struct timeval tve; + + static int backoff = 1; + int init = 1; + unsigned int nbytes = 0; + while (nbytes < size) { + struct timeval tvt; + if (timeout > 0) { + struct timeval now; + if (gettimeofday (&now, NULL) != 0) { + SYSERROR (device->context, errno); + return -1; + } + + if (init) { + // Calculate the initial timeout. + tvt.tv_sec = (timeout / 1000); + tvt.tv_usec = (timeout % 1000) * 1000; + // Calculate the target time. + timeradd (&now, &tvt, &tve); + } else { + // Calculate the remaining timeout. + if (timercmp (&now, &tve, <)) + timersub (&tve, &now, &tvt); + else + timerclear (&tvt); + } + init = 0; + } else if (timeout == 0) { + timerclear (&tvt); + } + + int n = ftdi_read_data (device->ftdi_ctx, (char *) data + nbytes, size - nbytes); + if (n < 0) { + if (n == LIBUSB_ERROR_INTERRUPTED) + continue; //Retry. + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; //Error during read call. + } else if (n == 0) { + // Exponential backoff. + if (backoff > MAX_BACKOFF) { + ERROR(device->context, "%s", "FTDI read timed out."); + return -1; + } + serial_ftdi_sleep (device, backoff); + backoff *= 2; + } else { + // Reset backoff to 1 on success. + backoff = 1; + } + + nbytes += n; + } + + INFO (device->context, "Read %d bytes", nbytes); + + return nbytes; +} + +static int serial_ftdi_write (serial_t *device, const void *data, unsigned int size) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + struct timeval tve, tvb; + if (device->halfduplex) { + // Get the current time. + if (gettimeofday (&tvb, NULL) != 0) { + SYSERROR (device->context, errno); + return -1; + } + } + + unsigned int nbytes = 0; + while (nbytes < size) { + + int n = ftdi_write_data (device->ftdi_ctx, (char *) data + nbytes, size - nbytes); + if (n < 0) { + if (n == LIBUSB_ERROR_INTERRUPTED) + continue; // Retry. + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; // Error during write call. + } else if (n == 0) { + break; // EOF. + } + + nbytes += n; + } + + if (device->halfduplex) { + // Get the current time. + if (gettimeofday (&tve, NULL) != 0) { + SYSERROR (device->context, errno); + return -1; + } + + // Calculate the elapsed time (microseconds). + struct timeval tvt; + timersub (&tve, &tvb, &tvt); + unsigned long elapsed = tvt.tv_sec * 1000000 + tvt.tv_usec; + + // Calculate the expected duration (microseconds). A 2 millisecond fudge + // factor is added because it improves the success rate significantly. + unsigned long expected = 1000000.0 * device->nbits / device->baudrate * size + 0.5 + 2000; + + // Wait for the remaining time. + if (elapsed < expected) { + unsigned long remaining = expected - elapsed; + + // The remaining time is rounded up to the nearest millisecond to + // match the Windows implementation. The higher resolution is + // pointless anyway, since we already added a fudge factor above. + serial_ftdi_sleep (device, (remaining + 999) / 1000); + } + } + + INFO (device->context, "Wrote %d bytes", nbytes); + + return nbytes; +} + +static int serial_ftdi_flush (serial_t *device, int queue) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Flush: queue=%u, input=%i, output=%i", queue, + serial_ftdi_get_received (device), + serial_ftdi_get_transmitted (device)); + + switch (queue) { + case SERIAL_QUEUE_INPUT: + if (ftdi_usb_purge_tx_buffer(device->ftdi_ctx)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + break; + case SERIAL_QUEUE_OUTPUT: + if (ftdi_usb_purge_rx_buffer(device->ftdi_ctx)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + break; + default: + if (ftdi_usb_purge_buffers(device->ftdi_ctx)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + break; + } + + return 0; +} + +static int serial_ftdi_send_break (serial_t *device) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument)a + + INFO (device->context, "Break : One time period."); + + // no direct functions for sending break signals in libftdi. + // there is a suggestion to lower the baudrate and sending NUL + // and resetting the baudrate up again. But it has flaws. + // Not implementing it before researching more. + + return -1; +} + +static int serial_ftdi_set_break (serial_t *device, int level) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "Break: value=%i", level); + + // Not implemented in libftdi yet. Research it further. + + return -1; +} + +static int serial_ftdi_set_dtr (serial_t *device, int level) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "DTR: value=%i", level); + + if (ftdi_setdtr(device->ftdi_ctx, level)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + + return 0; +} + +static int serial_ftdi_set_rts (serial_t *device, int level) +{ + if (device == NULL) + return -1; // EINVAL (Invalid argument) + + INFO (device->context, "RTS: value=%i", level); + + if (ftdi_setrts(device->ftdi_ctx, level)) { + ERROR (device->context, "%s", ftdi_get_error_string(device->ftdi_ctx)); + return -1; + } + + return 0; +} + +const dc_serial_operations_t serial_ftdi_ops = { + .open = serial_ftdi_open, + .close = serial_ftdi_close, + .read = serial_ftdi_read, + .write = serial_ftdi_write, + .flush = serial_ftdi_flush, + .get_received = serial_ftdi_get_received, + .get_transmitted = NULL, /*NOT USED ANYWHERE! serial_ftdi_get_transmitted */ + .set_timeout = serial_ftdi_set_timeout +#ifdef FIXED_SSRF_CUSTOM_SERIAL + , + .configure = serial_ftdi_configure, +//static int serial_ftdi_configure (serial_t *device, int baudrate, int databits, int parity, int stopbits, int flowcontrol) + .set_halfduplex = serial_ftdi_set_halfduplex, +//static int serial_ftdi_set_halfduplex (serial_t *device, int value) + .send_break = serial_ftdi_send_break, +//static int serial_ftdi_send_break (serial_t *device) + .set_break = serial_ftdi_set_break, +//static int serial_ftdi_set_break (serial_t *device, int level) + .set_dtr = serial_ftdi_set_dtr, +//static int serial_ftdi_set_dtr (serial_t *device, int level) + .set_rts = serial_ftdi_set_rts +//static int serial_ftdi_set_rts (serial_t *device, int level) +#endif +}; + +dc_status_t dc_serial_ftdi_open(dc_serial_t **out, dc_context_t *context) +{ + if (out == NULL) + return DC_STATUS_INVALIDARGS; + + // Allocate memory. + dc_serial_t *serial_device = (dc_serial_t *) malloc (sizeof (dc_serial_t)); + + if (serial_device == NULL) { + return DC_STATUS_NOMEMORY; + } + + // Initialize data and function pointers + dc_serial_init(serial_device, NULL, &serial_ftdi_ops); + + // Open the serial device. + dc_status_t rc = (dc_status_t) serial_ftdi_open (&serial_device->port, context, NULL); + if (rc != DC_STATUS_SUCCESS) { + free (serial_device); + return rc; + } + + // Set the type of the device + serial_device->type = DC_TRANSPORT_USB;; + + *out = serial_device; + + return DC_STATUS_SUCCESS; +} diff --git a/subsurface-core/sha1.c b/subsurface-core/sha1.c new file mode 100644 index 000000000..acf8c5d9f --- /dev/null +++ b/subsurface-core/sha1.c @@ -0,0 +1,300 @@ +/* + * SHA1 routine optimized to do word accesses rather than byte accesses, + * and to avoid unnecessary copies into the context array. + * + * This was initially based on the Mozilla SHA1 implementation, although + * none of the original Mozilla code remains. + */ + +/* this is only to get definitions for memcpy(), ntohl() and htonl() */ +#include +#include +#ifdef WIN32 +#include +#else +#include +#endif +#include "sha1.h" + +#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) + +/* + * Force usage of rol or ror by selecting the one with the smaller constant. + * It _can_ generate slightly smaller code (a constant of 1 is special), but + * perhaps more importantly it's possibly faster on any uarch that does a + * rotate with a loop. + */ + +#define SHA_ASM(op, x, n) ({ unsigned int __res; __asm__(op " %1,%0":"=r" (__res):"i" (n), "0" (x)); __res; }) +#define SHA_ROL(x, n) SHA_ASM("rol", x, n) +#define SHA_ROR(x, n) SHA_ASM("ror", x, n) + +#else + +#define SHA_ROT(X, l, r) (((X) << (l)) | ((X) >> (r))) +#define SHA_ROL(X, n) SHA_ROT(X, n, 32 - (n)) +#define SHA_ROR(X, n) SHA_ROT(X, 32 - (n), n) + +#endif + +/* + * If you have 32 registers or more, the compiler can (and should) + * try to change the array[] accesses into registers. However, on + * machines with less than ~25 registers, that won't really work, + * and at least gcc will make an unholy mess of it. + * + * So to avoid that mess which just slows things down, we force + * the stores to memory to actually happen (we might be better off + * with a 'W(t)=(val);asm("":"+m" (W(t))' there instead, as + * suggested by Artur Skawina - that will also make gcc unable to + * try to do the silly "optimize away loads" part because it won't + * see what the value will be). + * + * Ben Herrenschmidt reports that on PPC, the C version comes close + * to the optimized asm with this (ie on PPC you don't want that + * 'volatile', since there are lots of registers). + * + * On ARM we get the best code generation by forcing a full memory barrier + * between each SHA_ROUND, otherwise gcc happily get wild with spilling and + * the stack frame size simply explode and performance goes down the drain. + */ + +#if defined(__i386__) || defined(__x86_64__) +#define setW(x, val) (*(volatile unsigned int *)&W(x) = (val)) +#elif defined(__GNUC__) && defined(__arm__) +#define setW(x, val) \ + do { \ + W(x) = (val); \ + __asm__("" :: : "memory"); \ + } while (0) +#else +#define setW(x, val) (W(x) = (val)) +#endif + +/* + * Performance might be improved if the CPU architecture is OK with + * unaligned 32-bit loads and a fast ntohl() is available. + * Otherwise fall back to byte loads and shifts which is portable, + * and is faster on architectures with memory alignment issues. + */ + +#if defined(__i386__) || defined(__x86_64__) || \ + defined(_M_IX86) || defined(_M_X64) || \ + defined(__ppc__) || defined(__ppc64__) || \ + defined(__powerpc__) || defined(__powerpc64__) || \ + defined(__s390__) || defined(__s390x__) + +#define get_be32(p) ntohl(*(unsigned int *)(p)) +#define put_be32(p, v) \ + do { \ + *(unsigned int *)(p) = htonl(v); \ + } while (0) + +#else + +#define get_be32(p) ( \ + (*((unsigned char *)(p) + 0) << 24) | \ + (*((unsigned char *)(p) + 1) << 16) | \ + (*((unsigned char *)(p) + 2) << 8) | \ + (*((unsigned char *)(p) + 3) << 0)) +#define put_be32(p, v) \ + do { \ + unsigned int __v = (v); \ + *((unsigned char *)(p) + 0) = __v >> 24; \ + *((unsigned char *)(p) + 1) = __v >> 16; \ + *((unsigned char *)(p) + 2) = __v >> 8; \ + *((unsigned char *)(p) + 3) = __v >> 0; \ + } while (0) + +#endif + +/* This "rolls" over the 512-bit array */ +#define W(x) (array[(x) & 15]) + +/* + * Where do we get the source from? The first 16 iterations get it from + * the input data, the next mix it from the 512-bit array. + */ +#define SHA_SRC(t) get_be32((unsigned char *)block + (t) * 4) +#define SHA_MIX(t) SHA_ROL(W((t) + 13) ^ W((t) + 8) ^ W((t) + 2) ^ W(t), 1); + +#define SHA_ROUND(t, input, fn, constant, A, B, C, D, E) \ + do { \ + unsigned int TEMP = input(t); \ + setW(t, TEMP); \ + E += TEMP + SHA_ROL(A, 5) + (fn) + (constant); \ + B = SHA_ROR(B, 2); \ + } while (0) + +#define T_0_15(t, A, B, C, D, E) SHA_ROUND(t, SHA_SRC, (((C ^ D) & B) ^ D), 0x5a827999, A, B, C, D, E) +#define T_16_19(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (((C ^ D) & B) ^ D), 0x5a827999, A, B, C, D, E) +#define T_20_39(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B ^ C ^ D), 0x6ed9eba1, A, B, C, D, E) +#define T_40_59(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, ((B &C) + (D &(B ^ C))), 0x8f1bbcdc, A, B, C, D, E) +#define T_60_79(t, A, B, C, D, E) SHA_ROUND(t, SHA_MIX, (B ^ C ^ D), 0xca62c1d6, A, B, C, D, E) + +static void blk_SHA1_Block(blk_SHA_CTX *ctx, const void *block) +{ + unsigned int A, B, C, D, E; + unsigned int array[16]; + + A = ctx->H[0]; + B = ctx->H[1]; + C = ctx->H[2]; + D = ctx->H[3]; + E = ctx->H[4]; + + /* Round 1 - iterations 0-16 take their input from 'block' */ + T_0_15(0, A, B, C, D, E); + T_0_15(1, E, A, B, C, D); + T_0_15(2, D, E, A, B, C); + T_0_15(3, C, D, E, A, B); + T_0_15(4, B, C, D, E, A); + T_0_15(5, A, B, C, D, E); + T_0_15(6, E, A, B, C, D); + T_0_15(7, D, E, A, B, C); + T_0_15(8, C, D, E, A, B); + T_0_15(9, B, C, D, E, A); + T_0_15(10, A, B, C, D, E); + T_0_15(11, E, A, B, C, D); + T_0_15(12, D, E, A, B, C); + T_0_15(13, C, D, E, A, B); + T_0_15(14, B, C, D, E, A); + T_0_15(15, A, B, C, D, E); + + /* Round 1 - tail. Input from 512-bit mixing array */ + T_16_19(16, E, A, B, C, D); + T_16_19(17, D, E, A, B, C); + T_16_19(18, C, D, E, A, B); + T_16_19(19, B, C, D, E, A); + + /* Round 2 */ + T_20_39(20, A, B, C, D, E); + T_20_39(21, E, A, B, C, D); + T_20_39(22, D, E, A, B, C); + T_20_39(23, C, D, E, A, B); + T_20_39(24, B, C, D, E, A); + T_20_39(25, A, B, C, D, E); + T_20_39(26, E, A, B, C, D); + T_20_39(27, D, E, A, B, C); + T_20_39(28, C, D, E, A, B); + T_20_39(29, B, C, D, E, A); + T_20_39(30, A, B, C, D, E); + T_20_39(31, E, A, B, C, D); + T_20_39(32, D, E, A, B, C); + T_20_39(33, C, D, E, A, B); + T_20_39(34, B, C, D, E, A); + T_20_39(35, A, B, C, D, E); + T_20_39(36, E, A, B, C, D); + T_20_39(37, D, E, A, B, C); + T_20_39(38, C, D, E, A, B); + T_20_39(39, B, C, D, E, A); + + /* Round 3 */ + T_40_59(40, A, B, C, D, E); + T_40_59(41, E, A, B, C, D); + T_40_59(42, D, E, A, B, C); + T_40_59(43, C, D, E, A, B); + T_40_59(44, B, C, D, E, A); + T_40_59(45, A, B, C, D, E); + T_40_59(46, E, A, B, C, D); + T_40_59(47, D, E, A, B, C); + T_40_59(48, C, D, E, A, B); + T_40_59(49, B, C, D, E, A); + T_40_59(50, A, B, C, D, E); + T_40_59(51, E, A, B, C, D); + T_40_59(52, D, E, A, B, C); + T_40_59(53, C, D, E, A, B); + T_40_59(54, B, C, D, E, A); + T_40_59(55, A, B, C, D, E); + T_40_59(56, E, A, B, C, D); + T_40_59(57, D, E, A, B, C); + T_40_59(58, C, D, E, A, B); + T_40_59(59, B, C, D, E, A); + + /* Round 4 */ + T_60_79(60, A, B, C, D, E); + T_60_79(61, E, A, B, C, D); + T_60_79(62, D, E, A, B, C); + T_60_79(63, C, D, E, A, B); + T_60_79(64, B, C, D, E, A); + T_60_79(65, A, B, C, D, E); + T_60_79(66, E, A, B, C, D); + T_60_79(67, D, E, A, B, C); + T_60_79(68, C, D, E, A, B); + T_60_79(69, B, C, D, E, A); + T_60_79(70, A, B, C, D, E); + T_60_79(71, E, A, B, C, D); + T_60_79(72, D, E, A, B, C); + T_60_79(73, C, D, E, A, B); + T_60_79(74, B, C, D, E, A); + T_60_79(75, A, B, C, D, E); + T_60_79(76, E, A, B, C, D); + T_60_79(77, D, E, A, B, C); + T_60_79(78, C, D, E, A, B); + T_60_79(79, B, C, D, E, A); + + ctx->H[0] += A; + ctx->H[1] += B; + ctx->H[2] += C; + ctx->H[3] += D; + ctx->H[4] += E; +} + +void blk_SHA1_Init(blk_SHA_CTX *ctx) +{ + ctx->size = 0; + + /* Initialize H with the magic constants (see FIPS180 for constants) */ + ctx->H[0] = 0x67452301; + ctx->H[1] = 0xefcdab89; + ctx->H[2] = 0x98badcfe; + ctx->H[3] = 0x10325476; + ctx->H[4] = 0xc3d2e1f0; +} + +void blk_SHA1_Update(blk_SHA_CTX *ctx, const void *data, unsigned long len) +{ + unsigned int lenW = ctx->size & 63; + + ctx->size += len; + + /* Read the data into W and process blocks as they get full */ + if (lenW) { + unsigned int left = 64 - lenW; + if (len < left) + left = len; + memcpy(lenW + (char *)ctx->W, data, left); + lenW = (lenW + left) & 63; + len -= left; + data = ((const char *)data + left); + if (lenW) + return; + blk_SHA1_Block(ctx, ctx->W); + } + while (len >= 64) { + blk_SHA1_Block(ctx, data); + data = ((const char *)data + 64); + len -= 64; + } + if (len) + memcpy(ctx->W, data, len); +} + +void blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx) +{ + static const unsigned char pad[64] = { 0x80 }; + unsigned int padlen[2]; + int i; + + /* Pad with a binary 1 (ie 0x80), then zeroes, then length */ + padlen[0] = htonl((uint32_t)(ctx->size >> 29)); + padlen[1] = htonl((uint32_t)(ctx->size << 3)); + + i = ctx->size & 63; + blk_SHA1_Update(ctx, pad, 1 + (63 & (55 - i))); + blk_SHA1_Update(ctx, padlen, 8); + + /* Output hash */ + for (i = 0; i < 5; i++) + put_be32(hashout + i * 4, ctx->H[i]); +} diff --git a/subsurface-core/sha1.h b/subsurface-core/sha1.h new file mode 100644 index 000000000..cab6ff77d --- /dev/null +++ b/subsurface-core/sha1.h @@ -0,0 +1,38 @@ +/* + * SHA1 routine optimized to do word accesses rather than byte accesses, + * and to avoid unnecessary copies into the context array. + * + * This was initially based on the Mozilla SHA1 implementation, although + * none of the original Mozilla code remains. + */ +#ifndef SHA1_H +#define SHA1_H + +typedef struct +{ + unsigned long long size; + unsigned int H[5]; + unsigned int W[16]; +} blk_SHA_CTX; + +void blk_SHA1_Init(blk_SHA_CTX *ctx); +void blk_SHA1_Update(blk_SHA_CTX *ctx, const void *dataIn, unsigned long len); +void blk_SHA1_Final(unsigned char hashout[20], blk_SHA_CTX *ctx); + +/* Make us use the standard names */ +#define SHA_CTX blk_SHA_CTX +#define SHA1_Init blk_SHA1_Init +#define SHA1_Update blk_SHA1_Update +#define SHA1_Final blk_SHA1_Final + +/* Trivial helper function */ +static inline void SHA1(const void *dataIn, unsigned long len, unsigned char hashout[20]) +{ + SHA_CTX ctx; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, dataIn, len); + SHA1_Final(hashout, &ctx); +} + +#endif // SHA1_H diff --git a/subsurface-core/statistics.c b/subsurface-core/statistics.c new file mode 100644 index 000000000..19fd350eb --- /dev/null +++ b/subsurface-core/statistics.c @@ -0,0 +1,379 @@ +/* statistics.c + * + * core logic for the Info & Stats page - + * char *get_time_string(int seconds, int maxdays); + * char *get_minutes(int seconds); + * void process_all_dives(struct dive *dive, struct dive **prev_dive); + * void get_selected_dives_text(char *buffer, int size); + */ +#include "gettext.h" +#include +#include + +#include "dive.h" +#include "display.h" +#include "divelist.h" +#include "statistics.h" + +static stats_t stats; +stats_t stats_selection; +stats_t *stats_monthly = NULL; +stats_t *stats_yearly = NULL; +stats_t *stats_by_trip = NULL; + +static void process_temperatures(struct dive *dp, stats_t *stats) +{ + int min_temp, mean_temp, max_temp = 0; + + max_temp = dp->maxtemp.mkelvin; + if (max_temp && (!stats->max_temp || max_temp > stats->max_temp)) + stats->max_temp = max_temp; + + min_temp = dp->mintemp.mkelvin; + if (min_temp && (!stats->min_temp || min_temp < stats->min_temp)) + stats->min_temp = min_temp; + + if (min_temp || max_temp) { + mean_temp = min_temp; + if (mean_temp) + mean_temp = (mean_temp + max_temp) / 2; + else + mean_temp = max_temp; + stats->combined_temp += get_temp_units(mean_temp, NULL); + stats->combined_count++; + } +} + +static void process_dive(struct dive *dp, stats_t *stats) +{ + int old_tt, sac_time = 0; + int duration = dp->duration.seconds; + + old_tt = stats->total_time.seconds; + stats->total_time.seconds += duration; + if (duration > stats->longest_time.seconds) + stats->longest_time.seconds = duration; + if (stats->shortest_time.seconds == 0 || duration < stats->shortest_time.seconds) + stats->shortest_time.seconds = duration; + if (dp->maxdepth.mm > stats->max_depth.mm) + stats->max_depth.mm = dp->maxdepth.mm; + if (stats->min_depth.mm == 0 || dp->maxdepth.mm < stats->min_depth.mm) + stats->min_depth.mm = dp->maxdepth.mm; + + process_temperatures(dp, stats); + + /* Maybe we should drop zero-duration dives */ + if (!duration) + return; + stats->avg_depth.mm = (1.0 * old_tt * stats->avg_depth.mm + + duration * dp->meandepth.mm) / + stats->total_time.seconds; + if (dp->sac > 100) { /* less than .1 l/min is bogus, even with a pSCR */ + sac_time = stats->total_sac_time + duration; + stats->avg_sac.mliter = (1.0 * stats->total_sac_time * stats->avg_sac.mliter + + duration * dp->sac) / + sac_time; + if (dp->sac > stats->max_sac.mliter) + stats->max_sac.mliter = dp->sac; + if (stats->min_sac.mliter == 0 || dp->sac < stats->min_sac.mliter) + stats->min_sac.mliter = dp->sac; + stats->total_sac_time = sac_time; + } +} + +char *get_minutes(int seconds) +{ + static char buf[80]; + snprintf(buf, sizeof(buf), "%d:%.2d", FRACTION(seconds, 60)); + return buf; +} + +void process_all_dives(struct dive *dive, struct dive **prev_dive) +{ + int idx; + struct dive *dp; + struct tm tm; + int current_year = 0; + int current_month = 0; + int year_iter = 0; + int month_iter = 0; + int prev_month = 0, prev_year = 0; + int trip_iter = 0; + dive_trip_t *trip_ptr = 0; + unsigned int size; + + *prev_dive = NULL; + memset(&stats, 0, sizeof(stats)); + if (dive_table.nr > 0) { + stats.shortest_time.seconds = dive_table.dives[0]->duration.seconds; + stats.min_depth.mm = dive_table.dives[0]->maxdepth.mm; + stats.selection_size = dive_table.nr; + } + + /* allocate sufficient space to hold the worst + * case (one dive per year or all dives during + * one month) for yearly and monthly statistics*/ + + free(stats_yearly); + free(stats_monthly); + free(stats_by_trip); + + size = sizeof(stats_t) * (dive_table.nr + 1); + stats_yearly = malloc(size); + stats_monthly = malloc(size); + stats_by_trip = malloc(size); + if (!stats_yearly || !stats_monthly || !stats_by_trip) + return; + memset(stats_yearly, 0, size); + memset(stats_monthly, 0, size); + memset(stats_by_trip, 0, size); + stats_yearly[0].is_year = true; + + /* this relies on the fact that the dives in the dive_table + * are in chronological order */ + for_each_dive (idx, dp) { + if (dive && dp->when == dive->when) { + /* that's the one we are showing */ + if (idx > 0) + *prev_dive = dive_table.dives[idx - 1]; + } + process_dive(dp, &stats); + + /* yearly statistics */ + utc_mkdate(dp->when, &tm); + if (current_year == 0) + current_year = tm.tm_year + 1900; + + if (current_year != tm.tm_year + 1900) { + current_year = tm.tm_year + 1900; + process_dive(dp, &(stats_yearly[++year_iter])); + stats_yearly[year_iter].is_year = true; + } else { + process_dive(dp, &(stats_yearly[year_iter])); + } + stats_yearly[year_iter].selection_size++; + stats_yearly[year_iter].period = current_year; + + if (dp->divetrip != NULL) { + if (trip_ptr != dp->divetrip) { + trip_ptr = dp->divetrip; + trip_iter++; + } + + /* stats_by_trip[0] is all the dives combined */ + stats_by_trip[0].selection_size++; + process_dive(dp, &(stats_by_trip[0])); + stats_by_trip[0].is_trip = true; + stats_by_trip[0].location = strdup("All (by trip stats)"); + + process_dive(dp, &(stats_by_trip[trip_iter])); + stats_by_trip[trip_iter].selection_size++; + stats_by_trip[trip_iter].is_trip = true; + stats_by_trip[trip_iter].location = dp->divetrip->location; + } + + /* monthly statistics */ + if (current_month == 0) { + current_month = tm.tm_mon + 1; + } else { + if (current_month != tm.tm_mon + 1) + current_month = tm.tm_mon + 1; + if (prev_month != current_month || prev_year != current_year) + month_iter++; + } + process_dive(dp, &(stats_monthly[month_iter])); + stats_monthly[month_iter].selection_size++; + stats_monthly[month_iter].period = current_month; + prev_month = current_month; + prev_year = current_year; + } +} + +/* make sure we skip the selected summary entries */ +void process_selected_dives(void) +{ + struct dive *dive; + unsigned int i, nr; + + memset(&stats_selection, 0, sizeof(stats_selection)); + + nr = 0; + for_each_dive(i, dive) { + if (dive->selected) { + process_dive(dive, &stats_selection); + nr++; + } + } + stats_selection.selection_size = nr; +} + +char *get_time_string_s(int seconds, int maxdays, bool freediving) +{ + static char buf[80]; + if (maxdays && seconds > 3600 * 24 * maxdays) { + snprintf(buf, sizeof(buf), translate("gettextFromC", "more than %d days"), maxdays); + } else { + int days = seconds / 3600 / 24; + int hours = (seconds - days * 3600 * 24) / 3600; + int minutes = (seconds - days * 3600 * 24 - hours * 3600) / 60; + int secs = (seconds - days * 3600 * 24 - hours * 3600 - minutes*60); + if (days > 0) + snprintf(buf, sizeof(buf), translate("gettextFromC", "%dd %dh %dmin"), days, hours, minutes); + else + if (freediving && seconds < 3600) + snprintf(buf, sizeof(buf), translate("gettextFromC", "%dmin %dsecs"), minutes, secs); + else + snprintf(buf, sizeof(buf), translate("gettextFromC", "%dh %dmin"), hours, minutes); + } + return buf; +} + +/* this gets called when at least two but not all dives are selected */ +static void get_ranges(char *buffer, int size) +{ + int i, len; + int first = -1, last = -1; + struct dive *dive; + + snprintf(buffer, size, "%s", translate("gettextFromC", "for dives #")); + for_each_dive (i, dive) { + if (!dive->selected) + continue; + if (dive->number < 1) { + /* uhh - weird numbers - bail */ + snprintf(buffer, size, "%s", translate("gettextFromC", "for selected dives")); + return; + } + len = strlen(buffer); + if (last == -1) { + snprintf(buffer + len, size - len, "%d", dive->number); + first = last = dive->number; + } else { + if (dive->number == last + 1) { + last++; + continue; + } else { + if (first == last) + snprintf(buffer + len, size - len, ", %d", dive->number); + else if (first + 1 == last) + snprintf(buffer + len, size - len, ", %d, %d", last, dive->number); + else + snprintf(buffer + len, size - len, "-%d, %d", last, dive->number); + first = last = dive->number; + } + } + } + len = strlen(buffer); + if (first != last) { + if (first + 1 == last) + snprintf(buffer + len, size - len, ", %d", last); + else + snprintf(buffer + len, size - len, "-%d", last); + } +} + +void get_selected_dives_text(char *buffer, int size) +{ + if (amount_selected == 1) { + if (current_dive) + snprintf(buffer, size, translate("gettextFromC", "for dive #%d"), current_dive->number); + else + snprintf(buffer, size, "%s", translate("gettextFromC", "for selected dive")); + } else if (amount_selected == dive_table.nr) { + snprintf(buffer, size, "%s", translate("gettextFromC", "for all dives")); + } else if (amount_selected == 0) { + snprintf(buffer, size, "%s", translate("gettextFromC", "(no dives)")); + } else { + get_ranges(buffer, size); + if (strlen(buffer) == size - 1) { + /* add our own ellipse... the way Pango does this is ugly + * as it will leave partial numbers there which I don't like */ + int offset = 4; + while (offset < size && isdigit(buffer[size - offset])) + offset++; + strcpy(buffer + size - offset, "..."); + } + } +} + +#define SOME_GAS 5000 // 5bar drop in cylinder pressure makes cylinder used + +bool is_cylinder_used(struct dive *dive, int idx) +{ + struct divecomputer *dc; + bool firstGasExplicit = false; + if (cylinder_none(&dive->cylinder[idx])) + return false; + + if ((dive->cylinder[idx].start.mbar - dive->cylinder[idx].end.mbar) > SOME_GAS) + return true; + for_each_dc(dive, dc) { + struct event *event = get_next_event(dc->events, "gaschange"); + while (event) { + if (dc->sample && (event->time.seconds == 0 || + (dc->samples && dc->sample[0].time.seconds == event->time.seconds))) + firstGasExplicit = true; + if (get_cylinder_index(dive, event) == idx) + return true; + event = get_next_event(event->next, "gaschange"); + } + if (dc->divemode == CCR && (idx == dive->diluent_cylinder_index || idx == dive->oxygen_cylinder_index)) + return true; + } + if (idx == 0 && !firstGasExplicit) + return true; + return false; +} + +void get_gas_used(struct dive *dive, volume_t gases[MAX_CYLINDERS]) +{ + int idx; + for (idx = 0; idx < MAX_CYLINDERS; idx++) { + cylinder_t *cyl = &dive->cylinder[idx]; + pressure_t start, end; + + if (!is_cylinder_used(dive, idx)) + continue; + + start = cyl->start.mbar ? cyl->start : cyl->sample_start; + end = cyl->end.mbar ? cyl->end : cyl->sample_end; + if (end.mbar && start.mbar > end.mbar) + gases[idx].mliter = gas_volume(cyl, start) - gas_volume(cyl, end); + } +} + +/* Quite crude reverse-blender-function, but it produces a approx result */ +static void get_gas_parts(struct gasmix mix, volume_t vol, int o2_in_topup, volume_t *o2, volume_t *he) +{ + volume_t air = {}; + + if (gasmix_is_air(&mix)) { + o2->mliter = 0; + he->mliter = 0; + return; + } + + air.mliter = rint(((double)vol.mliter * (1000 - get_he(&mix) - get_o2(&mix))) / (1000 - o2_in_topup)); + he->mliter = rint(((double)vol.mliter * get_he(&mix)) / 1000.0); + o2->mliter += vol.mliter - he->mliter - air.mliter; +} + +void selected_dives_gas_parts(volume_t *o2_tot, volume_t *he_tot) +{ + int i, j; + struct dive *d; + for_each_dive (i, d) { + if (!d->selected) + continue; + volume_t diveGases[MAX_CYLINDERS] = {}; + get_gas_used(d, diveGases); + for (j = 0; j < MAX_CYLINDERS; j++) { + if (diveGases[j].mliter) { + volume_t o2 = {}, he = {}; + get_gas_parts(d->cylinder[j].gasmix, diveGases[j], O2_IN_AIR, &o2, &he); + o2_tot->mliter += o2.mliter; + he_tot->mliter += he.mliter; + } + } + } +} diff --git a/subsurface-core/statistics.h b/subsurface-core/statistics.h new file mode 100644 index 000000000..dbab25761 --- /dev/null +++ b/subsurface-core/statistics.h @@ -0,0 +1,58 @@ +/* + * statistics.h + * + * core logic functions called from statistics UI + * common types and variables + */ + +#ifndef STATISTICS_H +#define STATISTICS_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct +{ + int period; + duration_t total_time; + /* avg_time is simply total_time / nr -- let's not keep this */ + duration_t shortest_time; + duration_t longest_time; + depth_t max_depth; + depth_t min_depth; + depth_t avg_depth; + volume_t max_sac; + volume_t min_sac; + volume_t avg_sac; + int max_temp; + int min_temp; + double combined_temp; + unsigned int combined_count; + unsigned int selection_size; + unsigned int total_sac_time; + bool is_year; + bool is_trip; + char *location; +} stats_t; +extern stats_t stats_selection; +extern stats_t *stats_yearly; +extern stats_t *stats_monthly; +extern stats_t *stats_by_trip; + +extern char *get_time_string_s(int seconds, int maxdays, bool freediving); +extern char *get_minutes(int seconds); +extern void process_all_dives(struct dive *dive, struct dive **prev_dive); +extern void get_selected_dives_text(char *buffer, int size); +extern void get_gas_used(struct dive *dive, volume_t gases[MAX_CYLINDERS]); +extern void process_selected_dives(void); +void selected_dives_gas_parts(volume_t *o2_tot, volume_t *he_tot); + +inline char *get_time_string(int seconds, int maxdays) { + return get_time_string_s( seconds, maxdays, false); +} +#ifdef __cplusplus +} +#endif + +#endif // STATISTICS_H diff --git a/subsurface-core/strndup.h b/subsurface-core/strndup.h new file mode 100644 index 000000000..84e18b60f --- /dev/null +++ b/subsurface-core/strndup.h @@ -0,0 +1,21 @@ +#ifndef STRNDUP_H +#define STRNDUP_H +#if __WIN32__ +static char *strndup (const char *s, size_t n) +{ + char *cpy; + size_t len = strlen(s); + if (n < len) + len = n; + if ((cpy = malloc(len + 1)) != + NULL) { + cpy[len] = + '\0'; + memcpy(cpy, + s, + len); + } + return cpy; +} +#endif +#endif /* STRNDUP_H */ diff --git a/subsurface-core/strtod.c b/subsurface-core/strtod.c new file mode 100644 index 000000000..81e5d42d1 --- /dev/null +++ b/subsurface-core/strtod.c @@ -0,0 +1,128 @@ +/* + * Sane helper for 'strtod()'. + * + * Sad that we even need this, but the C library version has + * insane locale behavior, and while the Qt "doDouble()" routines + * are better in that regard, they don't have an end pointer + * (having replaced it with the completely idiotic "ok" boolean + * pointer instead). + * + * I wonder what drugs people are on sometimes. + * + * Right now we support the following flags to limit the + * parsing some ways: + * + * STRTOD_NO_SIGN - don't accept signs + * STRTOD_NO_DOT - no decimal dots, I'm European + * STRTOD_NO_COMMA - no comma, please, I'm C locale + * STRTOD_NO_EXPONENT - no exponent parsing, I'm human + * + * The "negative" flags are so that the common case can just + * use a flag value of 0, and only if you have some special + * requirements do you need to state those with explicit flags. + * + * So if you want the C locale kind of parsing, you'd use the + * STRTOD_NO_COMMA flag to disallow a decimal comma. But if you + * want a more relaxed "Hey, Europeans are people too, even if + * they have locales with commas", just pass in a zero flag. + */ +#include +#include "dive.h" + +double strtod_flags(const char *str, const char **ptr, unsigned int flags) +{ + char c; + const char *p = str, *ep; + double val = 0.0; + double decimal = 1.0; + int sign = 0, esign = 0; + int numbers = 0, dot = 0; + + /* skip spaces */ + while (isspace(c = *p++)) + /* */; + + /* optional sign */ + if (!(flags & STRTOD_NO_SIGN)) { + switch (c) { + case '-': + sign = 1; + /* fallthrough */ + case '+': + c = *p++; + } + } + + /* Mantissa */ + for (;; c = *p++) { + if ((c == '.' && !(flags & STRTOD_NO_DOT)) || + (c == ',' && !(flags & STRTOD_NO_COMMA))) { + if (dot) + goto done; + dot = 1; + continue; + } + if (c >= '0' && c <= '9') { + numbers++; + val = (val * 10) + (c - '0'); + if (dot) + decimal *= 10; + continue; + } + if (c != 'e' && c != 'E') + goto done; + if (flags & STRTOD_NO_EXPONENT) + goto done; + break; + } + + if (!numbers) + goto done; + + /* Exponent */ + ep = p; + c = *ep++; + switch (c) { + case '-': + esign = 1; + /* fallthrough */ + case '+': + c = *ep++; + } + + if (c >= '0' && c <= '9') { + p = ep; + int exponent = c - '0'; + + for (;;) { + c = *p++; + if (c < '0' || c > '9') + break; + exponent *= 10; + exponent += c - '0'; + } + + /* We're not going to bother playing games */ + if (exponent > 308) + exponent = 308; + + while (exponent-- > 0) { + if (esign) + decimal *= 10; + else + decimal /= 10; + } + } + +done: + if (!numbers) + goto no_conversion; + if (ptr) + *ptr = p - 1; + return (sign ? -val : val) / decimal; + +no_conversion: + if (ptr) + *ptr = str; + return 0.0; +} diff --git a/subsurface-core/subsurfacestartup.c b/subsurface-core/subsurfacestartup.c new file mode 100644 index 000000000..1286885ee --- /dev/null +++ b/subsurface-core/subsurfacestartup.c @@ -0,0 +1,319 @@ +#include "subsurfacestartup.h" +#include "version.h" +#include +#include +#include "gettext.h" +#include "qthelperfromc.h" +#include "git-access.h" + +struct preferences prefs, informational_prefs; +struct preferences default_prefs = { + .cloud_base_url = "https://cloud.subsurface-divelog.org/", + .units = SI_UNITS, + .unit_system = METRIC, + .coordinates_traditional = true, + .pp_graphs = { + .po2 = false, + .pn2 = false, + .phe = false, + .po2_threshold = 1.6, + .pn2_threshold = 4.0, + .phe_threshold = 13.0, + }, + .mod = false, + .modpO2 = 1.6, + .ead = false, + .hrgraph = false, + .percentagegraph = false, + .dcceiling = true, + .redceiling = false, + .calcceiling = false, + .calcceiling3m = false, + .calcndltts = false, + .gflow = 30, + .gfhigh = 75, + .animation_speed = 500, + .gf_low_at_maxdepth = false, + .show_ccr_setpoint = false, + .show_ccr_sensors = false, + .font_size = -1, + .display_invalid_dives = false, + .show_sac = false, + .display_unused_tanks = false, + .show_average_depth = true, + .ascrate75 = 9000 / 60, + .ascrate50 = 6000 / 60, + .ascratestops = 6000 / 60, + .ascratelast6m = 1000 / 60, + .descrate = 18000 / 60, + .bottompo2 = 1400, + .decopo2 = 1600, + .doo2breaks = false, + .drop_stone_mode = false, + .switch_at_req_stop = false, + .min_switch_duration = 60, + .last_stop = false, + .verbatim_plan = false, + .display_runtime = true, + .display_duration = true, + .display_transitions = true, + .safetystop = true, + .bottomsac = 20000, + .decosac = 17000, + .reserve_gas=40000, + .o2consumption = 720, + .pscr_ratio = 100, + .show_pictures_in_profile = true, + .tankbar = false, + .facebook = { + .user_id = NULL, + .album_id = NULL, + .access_token = NULL + }, + .defaultsetpoint = 1100, + .cloud_background_sync = true, + .geocoding = { + .enable_geocoding = true, + .parse_dive_without_gps = false, + .tag_existing_dives = false, + .category = { 0 } + }, + .deco_mode = BUEHLMANN, + .conservatism_level = 3 +}; + +int run_survey; + +struct units *get_units() +{ + return &prefs.units; +} + +/* random helper functions, used here or elsewhere */ +static int sortfn(const void *_a, const void *_b) +{ + const struct dive *a = (const struct dive *)*(void **)_a; + const struct dive *b = (const struct dive *)*(void **)_b; + + if (a->when < b->when) + return -1; + if (a->when > b->when) + return 1; + return 0; +} + +void sort_table(struct dive_table *table) +{ + qsort(table->dives, table->nr, sizeof(struct dive *), sortfn); +} + +const char *weekday(int wday) +{ + static const char wday_array[7][7] = { + /*++GETTEXT: these are three letter days - we allow up to six code bytes */ + QT_TRANSLATE_NOOP("gettextFromC", "Sun"), QT_TRANSLATE_NOOP("gettextFromC", "Mon"), QT_TRANSLATE_NOOP("gettextFromC", "Tue"), QT_TRANSLATE_NOOP("gettextFromC", "Wed"), QT_TRANSLATE_NOOP("gettextFromC", "Thu"), QT_TRANSLATE_NOOP("gettextFromC", "Fri"), QT_TRANSLATE_NOOP("gettextFromC", "Sat") + }; + return translate("gettextFromC", wday_array[wday]); +} + +const char *monthname(int mon) +{ + static const char month_array[12][7] = { + /*++GETTEXT: these are three letter months - we allow up to six code bytes*/ + QT_TRANSLATE_NOOP("gettextFromC", "Jan"), QT_TRANSLATE_NOOP("gettextFromC", "Feb"), QT_TRANSLATE_NOOP("gettextFromC", "Mar"), QT_TRANSLATE_NOOP("gettextFromC", "Apr"), QT_TRANSLATE_NOOP("gettextFromC", "May"), QT_TRANSLATE_NOOP("gettextFromC", "Jun"), + QT_TRANSLATE_NOOP("gettextFromC", "Jul"), QT_TRANSLATE_NOOP("gettextFromC", "Aug"), QT_TRANSLATE_NOOP("gettextFromC", "Sep"), QT_TRANSLATE_NOOP("gettextFromC", "Oct"), QT_TRANSLATE_NOOP("gettextFromC", "Nov"), QT_TRANSLATE_NOOP("gettextFromC", "Dec"), + }; + return translate("gettextFromC", month_array[mon]); +} + +/* + * track whether we switched to importing dives + */ +bool imported = false; + +static void print_version() +{ + printf("Subsurface v%s, ", subsurface_git_version()); + printf("built with libdivecomputer v%s\n", dc_version(NULL)); +} + +void print_files() +{ + const char *branch = 0; + const char *remote = 0; + const char *filename, *local_git; + + filename = cloud_url(); + + is_git_repository(filename, &branch, &remote, true); + printf("\nFile locations:\n\n"); + if (branch && remote) { + local_git = get_local_dir(remote, branch); + printf("Local git storage: %s\n", local_git); + } else { + printf("Unable to get local git directory\n"); + } + char *tmp = cloud_url(); + printf("Cloud URL: %s\n", tmp); + free(tmp); + tmp = hashfile_name_string(); + printf("Image hashes: %s\n", tmp); + free(tmp); + tmp = picturedir_string(); + printf("Local picture directory: %s\n\n", tmp); + free(tmp); +} + +static void print_help() +{ + print_version(); + printf("\nUsage: subsurface [options] [logfile ...] [--import logfile ...]"); + printf("\n\noptions include:"); + printf("\n --help|-h This help text"); + printf("\n --import logfile ... Logs before this option is treated as base, everything after is imported"); + printf("\n --verbose|-v Verbose debug (repeat to increase verbosity)"); + printf("\n --version Prints current version"); + printf("\n --survey Offer to submit a user survey"); + printf("\n --win32console Create a dedicated console if needed (Windows only). Add option before everything else\n\n"); +} + +void parse_argument(const char *arg) +{ + const char *p = arg + 1; + + do { + switch (*p) { + case 'h': + print_help(); + exit(0); + case 'v': + verbose++; + continue; + case 'q': + quit++; + continue; + case '-': + /* long options with -- */ + if (strcmp(arg, "--help") == 0) { + print_help(); + exit(0); + } + if (strcmp(arg, "--import") == 0) { + imported = true; /* mark the dives so far as the base, * everything after is imported */ + return; + } + if (strcmp(arg, "--verbose") == 0) { + verbose++; + return; + } + if (strcmp(arg, "--version") == 0) { + print_version(); + exit(0); + } + if (strcmp(arg, "--survey") == 0) { + run_survey = true; + return; + } + if (strcmp(arg, "--win32console") == 0) + return; + /* fallthrough */ + case 'p': + /* ignore process serial number argument when run as native macosx app */ + if (strncmp(arg, "-psQT_TR_NOOP(", 5) == 0) { + return; + } + /* fallthrough */ + default: + fprintf(stderr, "Bad argument '%s'\n", arg); + exit(1); + } + } while (*++p); +} + +void renumber_dives(int start_nr, bool selected_only) +{ + int i, nr = start_nr; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive->selected) + dive->number = nr++; + } + mark_divelist_changed(true); +} + +/* + * Under a POSIX setup, the locale string should have a format + * like [language[_territory][.codeset][@modifier]]. + * + * So search for the underscore, and see if the "territory" is + * US, and turn on imperial units by default. + * + * I guess Burma and Liberia should trigger this too. I'm too + * lazy to look up the territory names, though. + */ +void setup_system_prefs(void) +{ + const char *env; + + subsurface_OS_pref_setup(); + default_prefs.divelist_font = strdup(system_divelist_default_font); + default_prefs.font_size = system_divelist_default_font_size; + default_prefs.default_filename = system_default_filename(); + + env = getenv("LC_MEASUREMENT"); + if (!env) + env = getenv("LC_ALL"); + if (!env) + env = getenv("LANG"); + if (!env) + return; + env = strchr(env, '_'); + if (!env) + return; + env++; + if (strncmp(env, "US", 2)) + return; + + default_prefs.units = IMPERIAL_units; +} + +/* copy a preferences block, including making copies of all included strings */ +void copy_prefs(struct preferences *src, struct preferences *dest) +{ + *dest = *src; + dest->divelist_font = copy_string(src->divelist_font); + dest->default_filename = copy_string(src->default_filename); + dest->default_cylinder = copy_string(src->default_cylinder); + dest->cloud_base_url = copy_string(src->cloud_base_url); + dest->cloud_git_url = copy_string(src->cloud_git_url); + dest->userid = copy_string(src->userid); + dest->proxy_host = copy_string(src->proxy_host); + dest->proxy_user = copy_string(src->proxy_user); + dest->proxy_pass = copy_string(src->proxy_pass); + dest->cloud_storage_password = copy_string(src->cloud_storage_password); + dest->cloud_storage_newpassword = copy_string(src->cloud_storage_newpassword); + dest->cloud_storage_email = copy_string(src->cloud_storage_email); + dest->cloud_storage_email_encoded = copy_string(src->cloud_storage_email_encoded); + dest->facebook.access_token = copy_string(src->facebook.access_token); + dest->facebook.user_id = copy_string(src->facebook.user_id); + dest->facebook.album_id = copy_string(src->facebook.album_id); +} + +/* + * Free strduped prefs before exit. + * + * These are not real leaks but they plug the holes found by eg. + * valgrind so you can find the real leaks. + */ +void free_prefs(void) +{ + free((void*)prefs.default_filename); + free((void*)prefs.default_cylinder); + free((void*)prefs.divelist_font); + free((void*)prefs.cloud_storage_password); + free(prefs.proxy_host); + free(prefs.proxy_user); + free(prefs.proxy_pass); + free(prefs.userid); +} diff --git a/subsurface-core/subsurfacestartup.h b/subsurface-core/subsurfacestartup.h new file mode 100644 index 000000000..3ccc24aa4 --- /dev/null +++ b/subsurface-core/subsurfacestartup.h @@ -0,0 +1,26 @@ +#ifndef SUBSURFACESTARTUP_H +#define SUBSURFACESTARTUP_H + +#include "dive.h" +#include "divelist.h" +#include "libdivecomputer.h" + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +extern bool imported; + +void setup_system_prefs(void); +void parse_argument(const char *arg); +void free_prefs(void); +void copy_prefs(struct preferences *src, struct preferences *dest); +void print_files(void); + +#ifdef __cplusplus +} +#endif + +#endif // SUBSURFACESTARTUP_H diff --git a/subsurface-core/subsurfacesysinfo.cpp b/subsurface-core/subsurfacesysinfo.cpp new file mode 100644 index 000000000..a7173b169 --- /dev/null +++ b/subsurface-core/subsurfacesysinfo.cpp @@ -0,0 +1,620 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 Intel Corporation +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "subsurfacesysinfo.h" +#include + +#ifdef Q_OS_UNIX +#include +#endif + +#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) + +#ifndef QStringLiteral +# define QStringLiteral QString::fromUtf8 +#endif + +#ifdef Q_OS_UNIX +#include +#endif + +#ifdef __APPLE__ +#include +#endif + +// --- this is a copy of Qt 5.4's src/corelib/global/archdetect.cpp --- + +// main part: processor type +#if defined(Q_PROCESSOR_ALPHA) +# define ARCH_PROCESSOR "alpha" +#elif defined(Q_PROCESSOR_ARM) +# define ARCH_PROCESSOR "arm" +#elif defined(Q_PROCESSOR_AVR32) +# define ARCH_PROCESSOR "avr32" +#elif defined(Q_PROCESSOR_BLACKFIN) +# define ARCH_PROCESSOR "bfin" +#elif defined(Q_PROCESSOR_X86_32) +# define ARCH_PROCESSOR "i386" +#elif defined(Q_PROCESSOR_X86_64) +# define ARCH_PROCESSOR "x86_64" +#elif defined(Q_PROCESSOR_IA64) +# define ARCH_PROCESSOR "ia64" +#elif defined(Q_PROCESSOR_MIPS) +# define ARCH_PROCESSOR "mips" +#elif defined(Q_PROCESSOR_POWER) +# define ARCH_PROCESSOR "power" +#elif defined(Q_PROCESSOR_S390) +# define ARCH_PROCESSOR "s390" +#elif defined(Q_PROCESSOR_SH) +# define ARCH_PROCESSOR "sh" +#elif defined(Q_PROCESSOR_SPARC) +# define ARCH_PROCESSOR "sparc" +#else +# define ARCH_PROCESSOR "unknown" +#endif + +// endinanness +#if defined(Q_LITTLE_ENDIAN) +# define ARCH_ENDIANNESS "little_endian" +#elif defined(Q_BIG_ENDIAN) +# define ARCH_ENDIANNESS "big_endian" +#endif + +// pointer type +#if defined(Q_OS_WIN64) || (defined(Q_OS_WINRT) && defined(_M_X64)) +# define ARCH_POINTER "llp64" +#elif defined(__LP64__) || QT_POINTER_SIZE - 0 == 8 +# define ARCH_POINTER "lp64" +#else +# define ARCH_POINTER "ilp32" +#endif + +// secondary: ABI string (includes the dash) +#if defined(__ARM_EABI__) || defined(__mips_eabi) +# define ARCH_ABI1 "-eabi" +#elif defined(_MIPS_SIM) +# if _MIPS_SIM == _ABIO32 +# define ARCH_ABI1 "-o32" +# elif _MIPS_SIM == _ABIN32 +# define ARCH_ABI1 "-n32" +# elif _MIPS_SIM == _ABI64 +# define ARCH_ABI1 "-n64" +# elif _MIPS_SIM == _ABIO64 +# define ARCH_ABI1 "-o64" +# endif +#else +# define ARCH_ABI1 "" +#endif +#if defined(__ARM_PCS_VFP) || defined(__mips_hard_float) +# define ARCH_ABI2 "-hardfloat" +#else +# define ARCH_ABI2 "" +#endif + +#define ARCH_ABI ARCH_ABI1 ARCH_ABI2 + +#define ARCH_FULL ARCH_PROCESSOR "-" ARCH_ENDIANNESS "-" ARCH_POINTER ARCH_ABI + +// --- end of archdetect.cpp --- +// copied from Qt 5.4.1's src/corelib/global/qglobal.cpp + +#if defined(Q_OS_WIN) || defined(Q_OS_CYGWIN) || defined(Q_OS_WINCE) || defined(Q_OS_WINRT) + +QT_BEGIN_INCLUDE_NAMESPACE +#include "qt_windows.h" +QT_END_INCLUDE_NAMESPACE + +#ifndef Q_OS_WINRT +# ifndef Q_OS_WINCE +// Fallback for determining Windows versions >= 8 by looping using the +// version check macros. Note that it will return build number=0 to avoid +// inefficient looping. +static inline void determineWinOsVersionFallbackPost8(OSVERSIONINFO *result) +{ + result->dwBuildNumber = 0; + DWORDLONG conditionMask = 0; + VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditionMask, VER_PLATFORMID, VER_EQUAL); + OSVERSIONINFOEX checkVersion = { sizeof(OSVERSIONINFOEX), result->dwMajorVersion, 0, + result->dwBuildNumber, result->dwPlatformId, {'\0'}, 0, 0, 0, 0, 0 }; + for ( ; VerifyVersionInfo(&checkVersion, VER_MAJORVERSION | VER_PLATFORMID, conditionMask); ++checkVersion.dwMajorVersion) + result->dwMajorVersion = checkVersion.dwMajorVersion; + conditionMask = 0; + checkVersion.dwMajorVersion = result->dwMajorVersion; + checkVersion.dwMinorVersion = 0; + VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_EQUAL); + VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditionMask, VER_PLATFORMID, VER_EQUAL); + for ( ; VerifyVersionInfo(&checkVersion, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID, conditionMask); ++checkVersion.dwMinorVersion) + result->dwMinorVersion = checkVersion.dwMinorVersion; +} + +# endif // !Q_OS_WINCE + +static inline OSVERSIONINFO winOsVersion() +{ + OSVERSIONINFO result = { sizeof(OSVERSIONINFO), 0, 0, 0, 0, {'\0'}}; + // GetVersionEx() has been deprecated in Windows 8.1 and will return + // only Windows 8 from that version on. +# if defined(_MSC_VER) && _MSC_VER >= 1800 +# pragma warning( push ) +# pragma warning( disable : 4996 ) +# endif + GetVersionEx(&result); +# if defined(_MSC_VER) && _MSC_VER >= 1800 +# pragma warning( pop ) +# endif +# ifndef Q_OS_WINCE + if (result.dwMajorVersion == 6 && result.dwMinorVersion == 2) { + determineWinOsVersionFallbackPost8(&result); + } +# endif // !Q_OS_WINCE + return result; +} +#endif // !Q_OS_WINRT + +static const char *winVer_helper() +{ + switch (int(SubsurfaceSysInfo::WindowsVersion)) { + case SubsurfaceSysInfo::WV_NT: + return "NT"; + case SubsurfaceSysInfo::WV_2000: + return "2000"; + case SubsurfaceSysInfo::WV_XP: + return "XP"; + case SubsurfaceSysInfo::WV_2003: + return "2003"; + case SubsurfaceSysInfo::WV_VISTA: + return "Vista"; + case SubsurfaceSysInfo::WV_WINDOWS7: + return "7"; + case SubsurfaceSysInfo::WV_WINDOWS8: + return "8"; + case SubsurfaceSysInfo::WV_WINDOWS8_1: + return "8.1"; + + case SubsurfaceSysInfo::WV_CE: + return "CE"; + case SubsurfaceSysInfo::WV_CENET: + return "CENET"; + case SubsurfaceSysInfo::WV_CE_5: + return "CE5"; + case SubsurfaceSysInfo::WV_CE_6: + return "CE6"; + } + // unknown, future version + return 0; +} +#endif + +#if defined(Q_OS_UNIX) +# if (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) || defined(Q_OS_FREEBSD) +# define USE_ETC_OS_RELEASE +struct QUnixOSVersion +{ + // from /etc/os-release + QString productType; // $ID + QString productVersion; // $VERSION_ID + QString prettyName; // $PRETTY_NAME +}; + +static QString unquote(const char *begin, const char *end) +{ + if (*begin == '"') { + Q_ASSERT(end[-1] == '"'); + return QString::fromLatin1(begin + 1, end - begin - 2); + } + return QString::fromLatin1(begin, end - begin); +} + +static bool readEtcOsRelease(QUnixOSVersion &v) +{ + // we're avoiding QFile here + int fd = QT_OPEN("/etc/os-release", O_RDONLY); + if (fd == -1) + return false; + + QT_STATBUF sbuf; + if (QT_FSTAT(fd, &sbuf) == -1) { + QT_CLOSE(fd); + return false; + } + + QByteArray buffer(sbuf.st_size, Qt::Uninitialized); + buffer.resize(QT_READ(fd, buffer.data(), sbuf.st_size)); + QT_CLOSE(fd); + + const char *ptr = buffer.constData(); + const char *end = buffer.constEnd(); + const char *eol; + for ( ; ptr != end; ptr = eol + 1) { + static const char idString[] = "ID="; + static const char prettyNameString[] = "PRETTY_NAME="; + static const char versionIdString[] = "VERSION_ID="; + + // find the end of the line after ptr + eol = static_cast(memchr(ptr, '\n', end - ptr)); + if (!eol) + eol = end - 1; + + // note: we're doing a binary search here, so comparison + // must always be sorted + int cmp = strncmp(ptr, idString, strlen(idString)); + if (cmp < 0) + continue; + if (cmp == 0) { + ptr += strlen(idString); + v.productType = unquote(ptr, eol); + continue; + } + + cmp = strncmp(ptr, prettyNameString, strlen(prettyNameString)); + if (cmp < 0) + continue; + if (cmp == 0) { + ptr += strlen(prettyNameString); + v.prettyName = unquote(ptr, eol); + continue; + } + + cmp = strncmp(ptr, versionIdString, strlen(versionIdString)); + if (cmp < 0) + continue; + if (cmp == 0) { + ptr += strlen(versionIdString); + v.productVersion = unquote(ptr, eol); + continue; + } + } + + return true; +} +# endif // USE_ETC_OS_RELEASE +#endif // Q_OS_UNIX + +static QString unknownText() +{ + return QStringLiteral("unknown"); +} + +QString SubsurfaceSysInfo::buildCpuArchitecture() +{ + return QStringLiteral(ARCH_PROCESSOR); +} + +QString SubsurfaceSysInfo::currentCpuArchitecture() +{ +#if defined(Q_OS_WIN) && !defined(Q_OS_WINCE) + // We don't need to catch all the CPU architectures in this function; + // only those where the host CPU might be different than the build target + // (usually, 64-bit platforms). + SYSTEM_INFO info; + GetNativeSystemInfo(&info); + switch (info.wProcessorArchitecture) { +# ifdef PROCESSOR_ARCHITECTURE_AMD64 + case PROCESSOR_ARCHITECTURE_AMD64: + return QStringLiteral("x86_64"); +# endif +# ifdef PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 + case PROCESSOR_ARCHITECTURE_IA32_ON_WIN64: +# endif + case PROCESSOR_ARCHITECTURE_IA64: + return QStringLiteral("ia64"); + } +#elif defined(Q_OS_UNIX) + long ret = -1; + struct utsname u; + +# if defined(Q_OS_SOLARIS) + // We need a special call for Solaris because uname(2) on x86 returns "i86pc" for + // both 32- and 64-bit CPUs. Reference: + // http://docs.oracle.com/cd/E18752_01/html/816-5167/sysinfo-2.html#REFMAN2sysinfo-2 + // http://fxr.watson.org/fxr/source/common/syscall/systeminfo.c?v=OPENSOLARIS + // http://fxr.watson.org/fxr/source/common/conf/param.c?v=OPENSOLARIS;im=10#L530 + if (ret == -1) + ret = sysinfo(SI_ARCHITECTURE_64, u.machine, sizeof u.machine); +# endif + + if (ret == -1) + ret = uname(&u); + + // we could use detectUnixVersion() above, but we only need a field no other function does + if (ret != -1) { + // the use of QT_BUILD_INTERNAL here is simply to ensure all branches build + // as we don't often build on some of the less common platforms +# if defined(Q_PROCESSOR_ARM) || defined(QT_BUILD_INTERNAL) + if (strcmp(u.machine, "aarch64") == 0) + return QStringLiteral("arm64"); + if (strncmp(u.machine, "armv", 4) == 0) + return QStringLiteral("arm"); +# endif +# if defined(Q_PROCESSOR_POWER) || defined(QT_BUILD_INTERNAL) + // harmonize "powerpc" and "ppc" to "power" + if (strncmp(u.machine, "ppc", 3) == 0) + return QLatin1String("power") + QLatin1String(u.machine + 3); + if (strncmp(u.machine, "powerpc", 7) == 0) + return QLatin1String("power") + QLatin1String(u.machine + 7); + if (strcmp(u.machine, "Power Macintosh") == 0) + return QLatin1String("power"); +# endif +# if defined(Q_PROCESSOR_SPARC) || defined(QT_BUILD_INTERNAL) + // Solaris sysinfo(2) (above) uses "sparcv9", but uname -m says "sun4u"; + // Linux says "sparc64" + if (strcmp(u.machine, "sun4u") == 0 || strcmp(u.machine, "sparc64") == 0) + return QStringLiteral("sparcv9"); + if (strcmp(u.machine, "sparc32") == 0) + return QStringLiteral("sparc"); +# endif +# if defined(Q_PROCESSOR_X86) || defined(QT_BUILD_INTERNAL) + // harmonize all "i?86" to "i386" + if (strlen(u.machine) == 4 && u.machine[0] == 'i' + && u.machine[2] == '8' && u.machine[3] == '6') + return QStringLiteral("i386"); + if (strcmp(u.machine, "amd64") == 0) // Solaris + return QStringLiteral("x86_64"); +# endif + return QString::fromLatin1(u.machine); + } +#endif + return buildCpuArchitecture(); +} + + +QString SubsurfaceSysInfo::buildAbi() +{ + return QLatin1String(ARCH_FULL); +} + +QString SubsurfaceSysInfo::kernelType() +{ +#if defined(Q_OS_WINCE) + return QStringLiteral("wince"); +#elif defined(Q_OS_WIN) + return QStringLiteral("winnt"); +#elif defined(Q_OS_UNIX) + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.sysname).toLower(); +#endif + return unknownText(); +} + +QString SubsurfaceSysInfo::kernelVersion() +{ +#ifdef Q_OS_WINRT + // TBD + return QString(); +#elif defined(Q_OS_WIN) + const OSVERSIONINFO osver = winOsVersion(); + return QString::number(int(osver.dwMajorVersion)) + QLatin1Char('.') + QString::number(int(osver.dwMinorVersion)) + + QLatin1Char('.') + QString::number(int(osver.dwBuildNumber)); +#else + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.release); + return QString(); +#endif +} + +QString SubsurfaceSysInfo::productType() +{ + // similar, but not identical to QFileSelectorPrivate::platformSelectors +#if defined(Q_OS_WINPHONE) + return QStringLiteral("winphone"); +#elif defined(Q_OS_WINRT) + return QStringLiteral("winrt"); +#elif defined(Q_OS_WINCE) + return QStringLiteral("wince"); +#elif defined(Q_OS_WIN) + return QStringLiteral("windows"); + +#elif defined(Q_OS_BLACKBERRY) + return QStringLiteral("blackberry"); +#elif defined(Q_OS_QNX) + return QStringLiteral("qnx"); + +#elif defined(Q_OS_ANDROID) + return QStringLiteral("android"); + +#elif defined(Q_OS_IOS) + return QStringLiteral("ios"); +#elif defined(Q_OS_OSX) + return QStringLiteral("osx"); +#elif defined(Q_OS_DARWIN) + return QStringLiteral("darwin"); + +#elif defined(USE_ETC_OS_RELEASE) // Q_OS_UNIX + QUnixOSVersion unixOsVersion; + readEtcOsRelease(unixOsVersion); + if (!unixOsVersion.productType.isEmpty()) + return unixOsVersion.productType; +#endif + return unknownText(); +} + +QString SubsurfaceSysInfo::productVersion() +{ +#if defined(Q_OS_IOS) + int major = (int(MacintoshVersion) >> 4) & 0xf; + int minor = int(MacintoshVersion) & 0xf; + if (Q_LIKELY(major < 10 && minor < 10)) { + char buf[4] = { char(major + '0'), '.', char(minor + '0'), '\0' }; + return QString::fromLatin1(buf, 3); + } + return QString::number(major) + QLatin1Char('.') + QString::number(minor); +#elif defined(Q_OS_OSX) + int minor = int(MacintoshVersion) - 2; // we're not running on Mac OS 9 + Q_ASSERT(minor < 100); + char buf[] = "10.0\0"; + if (Q_LIKELY(minor < 10)) { + buf[3] += minor; + } else { + buf[3] += minor / 10; + buf[4] = '0' + minor % 10; + } + return QString::fromLatin1(buf); +#elif defined(Q_OS_WIN) + const char *version = winVer_helper(); + if (version) + return QString::fromLatin1(version).toLower(); + // fall through + + // Android and Blackberry should not fall through to the Unix code +#elif defined(Q_OS_ANDROID) + // TBD +#elif defined(Q_OS_BLACKBERRY) + deviceinfo_details_t *deviceInfo; + if (deviceinfo_get_details(&deviceInfo) == BPS_SUCCESS) { + QString bbVersion = QString::fromLatin1(deviceinfo_details_get_device_os_version(deviceInfo)); + deviceinfo_free_details(&deviceInfo); + return bbVersion; + } +#elif defined(USE_ETC_OS_RELEASE) // Q_OS_UNIX + QUnixOSVersion unixOsVersion; + readEtcOsRelease(unixOsVersion); + if (!unixOsVersion.productVersion.isEmpty()) + return unixOsVersion.productVersion; +#endif + + // fallback + return unknownText(); +} + +QString SubsurfaceSysInfo::prettyProductName() +{ +#if defined(Q_OS_IOS) + return QLatin1String("iOS ") + productVersion(); +#elif defined(Q_OS_OSX) + // get the known codenames + const char *basename = 0; + switch (int(MacintoshVersion)) { + case MV_CHEETAH: + case MV_PUMA: + case MV_JAGUAR: + case MV_PANTHER: + case MV_TIGER: + // This version of Qt does not run on those versions of OS X + // so this case label will never be reached + Q_UNREACHABLE(); + break; + case MV_LEOPARD: + basename = "Mac OS X Leopard ("; + break; + case MV_SNOWLEOPARD: + basename = "Mac OS X Snow Leopard ("; + break; + case MV_LION: + basename = "Mac OS X Lion ("; + break; + case MV_MOUNTAINLION: + basename = "OS X Mountain Lion ("; + break; + case MV_MAVERICKS: + basename = "OS X Mavericks ("; + break; +#ifdef MV_YOSEMITE + case MV_YOSEMITE: +#else + case 0x000C: // MV_YOSEMITE +#endif + basename = "OS X Yosemite ("; + break; +#ifdef MV_ELCAPITAN + case MV_ELCAPITAN : +#else + case 0x000D: // MV_ELCAPITAN +#endif + basename = "OS X El Capitan ("; + break; + } + if (basename) + return QLatin1String(basename) + productVersion() + QLatin1Char(')'); + + // a future version of OS X + return QLatin1String("OS X ") + productVersion(); +#elif defined(Q_OS_WINPHONE) + return QLatin1String("Windows Phone ") + QLatin1String(winVer_helper()); +#elif defined(Q_OS_WIN) + return QLatin1String("Windows ") + QLatin1String(winVer_helper()); +#elif defined(Q_OS_ANDROID) + return QLatin1String("Android ") + productVersion(); +#elif defined(Q_OS_BLACKBERRY) + return QLatin1String("BlackBerry ") + productVersion(); +#elif defined(Q_OS_UNIX) +# ifdef USE_ETC_OS_RELEASE + QUnixOSVersion unixOsVersion; + readEtcOsRelease(unixOsVersion); + if (!unixOsVersion.prettyName.isEmpty()) + return unixOsVersion.prettyName; +# endif + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.sysname) + QLatin1Char(' ') + QString::fromLatin1(u.release); +#endif + return unknownText(); +} + +#endif // Qt >= 5.4 + +QString SubsurfaceSysInfo::prettyOsName() +{ + // Matches the pre-release version of Qt 5.4 + QString pretty = prettyProductName(); +#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID) + // QSysInfo::kernelType() returns lowercase ("linux" instead of "Linux") + struct utsname u; + if (uname(&u) == 0) + return QString::fromLatin1(u.sysname) + QLatin1String(" (") + pretty + QLatin1Char(')'); +#endif + return pretty; +} + +extern "C" { +bool isWin7Or8() +{ +#ifdef Q_OS_WIN + return (QSysInfo::WindowsVersion & QSysInfo::WV_NT_based) >= QSysInfo::WV_WINDOWS7; +#else + return false; +#endif +} +} diff --git a/subsurface-core/subsurfacesysinfo.h b/subsurface-core/subsurfacesysinfo.h new file mode 100644 index 000000000..b2c267b83 --- /dev/null +++ b/subsurface-core/subsurfacesysinfo.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 Intel Corporation +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SUBSURFACESYSINFO_H +#define SUBSURFACESYSINFO_H + +#include + +class SubsurfaceSysInfo : public QSysInfo { +public: +#if QT_VERSION < 0x050400 + static QString buildCpuArchitecture(); + static QString currentCpuArchitecture(); + static QString buildAbi(); + + static QString kernelType(); + static QString kernelVersion(); + static QString productType(); + static QString productVersion(); + static QString prettyProductName(); +#endif + static QString prettyOsName(); +}; + + +#endif // SUBSURFACESYSINFO_H diff --git a/subsurface-core/taxonomy.c b/subsurface-core/taxonomy.c new file mode 100644 index 000000000..670d85ad0 --- /dev/null +++ b/subsurface-core/taxonomy.c @@ -0,0 +1,48 @@ +#include "taxonomy.h" +#include "gettext.h" +#include + +char *taxonomy_category_names[TC_NR_CATEGORIES] = { + QT_TRANSLATE_NOOP("getTextFromC", "None"), + QT_TRANSLATE_NOOP("getTextFromC", "Ocean"), + QT_TRANSLATE_NOOP("getTextFromC", "Country"), + QT_TRANSLATE_NOOP("getTextFromC", "State"), + QT_TRANSLATE_NOOP("getTextFromC", "County"), + QT_TRANSLATE_NOOP("getTextFromC", "Town"), + QT_TRANSLATE_NOOP("getTextFromC", "City") +}; + +// these are the names for geoname.org +char *taxonomy_api_names[TC_NR_CATEGORIES] = { + "none", + "name", + "countryName", + "adminName1", + "adminName2", + "toponymName", + "adminName3" +}; + +struct taxonomy *alloc_taxonomy() +{ + return calloc(TC_NR_CATEGORIES, sizeof(struct taxonomy)); +} + +void free_taxonomy(struct taxonomy_data *t) +{ + if (t) { + for (int i = 0; i < t->nr; i++) + free((void *)t->category[i].value); + free(t->category); + t->category = NULL; + t->nr = 0; + } +} + +int taxonomy_index_for_category(struct taxonomy_data *t, enum taxonomy_category cat) +{ + for (int i = 0; i < t->nr; i++) + if (t->category[i].category == cat) + return i; + return -1; +} diff --git a/subsurface-core/taxonomy.h b/subsurface-core/taxonomy.h new file mode 100644 index 000000000..51245d562 --- /dev/null +++ b/subsurface-core/taxonomy.h @@ -0,0 +1,41 @@ +#ifndef TAXONOMY_H +#define TAXONOMY_H + +#ifdef __cplusplus +extern "C" { +#endif + +enum taxonomy_category { + TC_NONE, + TC_OCEAN, + TC_COUNTRY, + TC_ADMIN_L1, + TC_ADMIN_L2, + TC_LOCALNAME, + TC_ADMIN_L3, + TC_NR_CATEGORIES +}; + +extern char *taxonomy_category_names[TC_NR_CATEGORIES]; +extern char *taxonomy_api_names[TC_NR_CATEGORIES]; + +struct taxonomy { + int category; /* the category for this tag: ocean, country, admin_l1, admin_l2, localname, etc */ + const char *value; /* the value returned, parsed, or manually entered for that category */ + enum { GEOCODED, PARSED, MANUAL, COPIED } origin; +}; + +/* the data block contains 3 taxonomy structures - unused ones have a tag value of NONE */ +struct taxonomy_data { + int nr; + struct taxonomy *category; +}; + +struct taxonomy *alloc_taxonomy(); +void free_taxonomy(struct taxonomy_data *t); +int taxonomy_index_for_category(struct taxonomy_data *t, enum taxonomy_category cat); + +#ifdef __cplusplus +} +#endif +#endif // TAXONOMY_H diff --git a/subsurface-core/time.c b/subsurface-core/time.c new file mode 100644 index 000000000..b658954bc --- /dev/null +++ b/subsurface-core/time.c @@ -0,0 +1,98 @@ +#include +#include "dive.h" + +/* + * Convert 64-bit timestamp to 'struct tm' in UTC. + * + * On 32-bit machines, only do 64-bit arithmetic for the seconds + * part, after that we do everything in 'long'. 64-bit divides + * are unnecessary once you're counting minutes (32-bit minutes: + * 8000+ years). + */ +void utc_mkdate(timestamp_t timestamp, struct tm *tm) +{ + static const int mdays[] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, + }; + static const int mdays_leap[] = { + 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, + }; + unsigned long val; + unsigned int leapyears; + int m; + const int *mp; + + memset(tm, 0, sizeof(*tm)); + + /* seconds since 1970 -> minutes since 1970 */ + tm->tm_sec = timestamp % 60; + val = timestamp /= 60; + + /* Do the simple stuff */ + tm->tm_min = val % 60; + val /= 60; + tm->tm_hour = val % 24; + val /= 24; + + /* Jan 1, 1970 was a Thursday (tm_wday=4) */ + tm->tm_wday = (val + 4) % 7; + + /* + * Now we're in "days since Jan 1, 1970". To make things easier, + * let's make it "days since Jan 1, 1968", since that's a leap-year + */ + val += 365 + 366; + + /* This only works up until 2099 (2100 isn't a leap-year) */ + leapyears = val / (365 * 4 + 1); + val %= (365 * 4 + 1); + tm->tm_year = 68 + leapyears * 4; + + /* Handle the leap-year itself */ + mp = mdays_leap; + if (val > 365) { + tm->tm_year++; + val -= 366; + tm->tm_year += val / 365; + val %= 365; + mp = mdays; + } + + for (m = 0; m < 12; m++) { + if (val < *mp) + break; + val -= *mp++; + } + tm->tm_mday = val + 1; + tm->tm_mon = m; +} + +timestamp_t utc_mktime(struct tm *tm) +{ + static const int mdays[] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 + }; + int year = tm->tm_year; + int month = tm->tm_mon; + int day = tm->tm_mday; + + /* First normalize relative to 1900 */ + if (year < 70) + year += 100; + else if (year > 1900) + year -= 1900; + + /* Normalized to Jan 1, 1970: unix time */ + year -= 70; + + if (year < 0 || year > 129) /* algo only works for 1970-2099 */ + return -1; + if (month < 0 || month > 11) /* array bounds */ + return -1; + if (month < 2 || (year + 2) % 4) + day--; + if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0) + return -1; + return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24 * 60 * 60UL + + tm->tm_hour * 60 * 60 + tm->tm_min * 60 + tm->tm_sec; +} diff --git a/subsurface-core/uemis-downloader.c b/subsurface-core/uemis-downloader.c new file mode 100644 index 000000000..2a6e5178c --- /dev/null +++ b/subsurface-core/uemis-downloader.c @@ -0,0 +1,1359 @@ +/* + * uemis-downloader.c + * + * Copyright (c) Dirk Hohndel + * released under GPL2 + * + * very (VERY) loosely based on the algorithms found in Java code by Fabian Gast + * which was released under the BSD-STYLE BEER WARE LICENSE + * I believe that I only used the information about HOW to do this (download data from the Uemis + * Zurich) but did not actually use any of his copyrighted code, therefore the license under which + * he released his code does not apply to this new implementation in C + * + * Modified by Guido Lerch guido.lerch@gmail.com in August 2015 + */ +#include +#include +#include +#include +#include +#include + +#include "gettext.h" +#include "libdivecomputer.h" +#include "uemis.h" +#include "divelist.h" + +#define ERR_FS_ALMOST_FULL QT_TRANSLATE_NOOP("gettextFromC", "Uemis Zurich: the file system is almost full.\nDisconnect/reconnect the dive computer\nand click \'Retry\'") +#define ERR_FS_FULL QT_TRANSLATE_NOOP("gettextFromC", "Uemis Zurich: the file system is full.\nDisconnect/reconnect the dive computer\nand click Retry") +#define ERR_FS_SHORT_WRITE QT_TRANSLATE_NOOP("gettextFromC", "Short write to req.txt file.\nIs the Uemis Zurich plugged in correctly?") +#define ERR_NO_FILES QT_TRANSLATE_NOOP("gettextFromC", "No dives to download.") +#define BUFLEN 2048 +#define BUFLEN 2048 +#define NUM_PARAM_BUFS 10 + +// debugging setup +// #define UEMIS_DEBUG 1 + 2 + 4 + 8 + 16 + 32 + +#define UEMIS_MAX_FILES 4000 +#define UEMIS_MEM_FULL 1 +#define UEMIS_MEM_OK 0 +#define UEMIS_SPOT_BLOCK_SIZE 1 +#define UEMIS_DIVE_DETAILS_SIZE 2 +#define UEMIS_LOG_BLOCK_SIZE 10 +#define UEMIS_CHECK_LOG 1 +#define UEMIS_CHECK_DETAILS 2 +#define UEMIS_CHECK_SINGLE_DIVE 3 + +#if UEMIS_DEBUG +const char *home, *user, *d_time; +static int debug_round = 0; +#define debugfile stderr +#endif + +#if UEMIS_DEBUG & 64 /* we are reading from a copy of the filesystem, not the device - no need to wait */ +#define UEMIS_TIMEOUT 50 /* 50ns */ +#define UEMIS_LONG_TIMEOUT 500 /* 500ns */ +#define UEMIS_MAX_TIMEOUT 2000 /* 2ms */ +#else +#define UEMIS_TIMEOUT 50000 /* 50ms */ +#define UEMIS_LONG_TIMEOUT 500000 /* 500ms */ +#define UEMIS_MAX_TIMEOUT 2000000 /* 2s */ +#endif + +static char *param_buff[NUM_PARAM_BUFS]; +static char *reqtxt_path; +static int reqtxt_file; +static int filenr; +static int number_of_files; +static char *mbuf = NULL; +static int mbuf_size = 0; + +static int max_mem_used = -1; +static int next_table_index = 0; +static int dive_to_read = 0; + +/* helper function to parse the Uemis data structures */ +static void uemis_ts(char *buffer, void *_when) +{ + struct tm tm; + timestamp_t *when = _when; + + memset(&tm, 0, sizeof(tm)); + sscanf(buffer, "%d-%d-%dT%d:%d:%d", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec); + tm.tm_mon -= 1; + tm.tm_year -= 1900; + *when = utc_mktime(&tm); +} + +/* float minutes */ +static void uemis_duration(char *buffer, duration_t *duration) +{ + duration->seconds = rint(ascii_strtod(buffer, NULL) * 60); +} + +/* int cm */ +static void uemis_depth(char *buffer, depth_t *depth) +{ + depth->mm = atoi(buffer) * 10; +} + +static void uemis_get_index(char *buffer, int *idx) +{ + *idx = atoi(buffer); +} + +/* space separated */ +static void uemis_add_string(const char *buffer, char **text, const char *delimit) +{ + /* do nothing if this is an empty buffer (Uemis sometimes returns a single + * space for empty buffers) */ + if (!buffer || !*buffer || (*buffer == ' ' && *(buffer + 1) == '\0')) + return; + if (!*text) { + *text = strdup(buffer); + } else { + char *buf = malloc(strlen(buffer) + strlen(*text) + 2); + strcpy(buf, *text); + strcat(buf, delimit); + strcat(buf, buffer); + free(*text); + *text = buf; + } +} + +/* still unclear if it ever reports lbs */ +static void uemis_get_weight(char *buffer, weightsystem_t *weight, int diveid) +{ + weight->weight.grams = uemis_get_weight_unit(diveid) ? + lbs_to_grams(ascii_strtod(buffer, NULL)) : + ascii_strtod(buffer, NULL) * 1000; + weight->description = strdup(translate("gettextFromC", "unknown")); +} + +static struct dive *uemis_start_dive(uint32_t deviceid) +{ + struct dive *dive = alloc_dive(); + dive->downloaded = true; + dive->dc.model = strdup("Uemis Zurich"); + dive->dc.deviceid = deviceid; + return dive; +} + +static struct dive *get_dive_by_uemis_diveid(device_data_t *devdata, uint32_t object_id) +{ + for (int i = 0; i < devdata->download_table->nr; i++) { + if (object_id == devdata->download_table->dives[i]->dc.diveid) + return devdata->download_table->dives[i]; + } + return NULL; +} + +static void record_uemis_dive(device_data_t *devdata, struct dive *dive) +{ + if (devdata->create_new_trip) { + if (!devdata->trip) + devdata->trip = create_and_hookup_trip_from_dive(dive); + else + add_dive_to_trip(dive, devdata->trip); + } + record_dive_to_table(dive, devdata->download_table); +} + +/* send text to the importer progress bar */ +static void uemis_info(const char *fmt, ...) +{ + static char buffer[256]; + va_list ap; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + progress_bar_text = buffer; +} + +static long bytes_available(int file) +{ + long result; + long now = lseek(file, 0, SEEK_CUR); + if (now == -1) + return 0; + result = lseek(file, 0, SEEK_END); + lseek(file, now, SEEK_SET); + if (now == -1 || result == -1) + return 0; + return result; +} + +static int number_of_file(char *path) +{ + int count = 0; +#ifdef WIN32 + struct _wdirent *entry; + _WDIR *dirp = (_WDIR *)subsurface_opendir(path); +#else + struct dirent *entry; + DIR *dirp = (DIR *)subsurface_opendir(path); +#endif + + while (dirp) { +#ifdef WIN32 + entry = _wreaddir(dirp); + if (!entry) + break; +#else + entry = readdir(dirp); + if (!entry) + break; + if (strstr(entry->d_name, ".TXT") || strstr(entry->d_name, ".txt")) /* If the entry is a regular file */ +#endif + count++; + } +#ifdef WIN32 + _wclosedir(dirp); +#else + closedir(dirp); +#endif + return count; +} + +static char *build_filename(const char *path, const char *name) +{ + int len = strlen(path) + strlen(name) + 2; + char *buf = malloc(len); +#if WIN32 + snprintf(buf, len, "%s\\%s", path, name); +#else + snprintf(buf, len, "%s/%s", path, name); +#endif + return buf; +} + +/* Check if there's a req.txt file and get the starting filenr from it. + * Test for the maximum number of ANS files (I believe this is always + * 4000 but in case there are differences depending on firmware, this + * code is easy enough */ +static bool uemis_init(const char *path) +{ + char *ans_path; + int i; + + if (!path) + return false; + /* let's check if this is indeed a Uemis DC */ + reqtxt_path = build_filename(path, "req.txt"); + reqtxt_file = subsurface_open(reqtxt_path, O_RDONLY | O_CREAT, 0666); + if (reqtxt_file < 0) { +#if UEMIS_DEBUG & 1 + fprintf(debugfile, ":EE req.txt can't be opened\n"); +#endif + return false; + } + if (bytes_available(reqtxt_file) > 5) { + char tmp[6]; + read(reqtxt_file, tmp, 5); + tmp[5] = '\0'; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "::r req.txt \"%s\"\n", tmp); +#endif + if (sscanf(tmp + 1, "%d", &filenr) != 1) + return false; + } else { + filenr = 0; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "::r req.txt skipped as there were fewer than 5 bytes\n"); +#endif + } + close(reqtxt_file); + + /* It would be nice if we could simply go back to the first set of + * ANS files. But with a FAT filesystem that isn't possible */ + ans_path = build_filename(path, "ANS"); + number_of_files = number_of_file(ans_path); + free(ans_path); + /* initialize the array in which we collect the answers */ + for (i = 0; i < NUM_PARAM_BUFS; i++) + param_buff[i] = ""; + return true; +} + +static void str_append_with_delim(char *s, char *t) +{ + int len = strlen(s); + snprintf(s + len, BUFLEN - len, "%s{", t); +} + +/* The communication protocoll with the DC is truly funky. + * After you write your request to the req.txt file you call this function. + * It writes the number of the next ANS file at the beginning of the req.txt + * file (prefixed by 'n' or 'r') and then again at the very end of it, after + * the full request (this time without the prefix). + * Then it syncs (not needed on Windows) and closes the file. */ +static void trigger_response(int file, char *command, int nr, long tailpos) +{ + char fl[10]; + + snprintf(fl, 8, "%s%04d", command, nr); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, ":tr %s (after seeks)\n", fl); +#endif + if (lseek(file, 0, SEEK_SET) == -1) + goto fs_error; + if (write(file, fl, strlen(fl)) == -1) + goto fs_error; + if (lseek(file, tailpos, SEEK_SET) == -1) + goto fs_error; + if (write(file, fl + 1, strlen(fl + 1)) == -1) + goto fs_error; +#ifndef WIN32 + fsync(file); +#endif +fs_error: + close(file); +} + +static char *next_token(char **buf) +{ + char *q, *p = strchr(*buf, '{'); + if (p) + *p = '\0'; + else + p = *buf + strlen(*buf) - 1; + q = *buf; + *buf = p + 1; + return q; +} + +/* poor man's tokenizer that understands a quoted delimter ('{') */ +static char *next_segment(char *buf, int *offset, int size) +{ + int i = *offset; + int seg_size; + bool done = false; + char *segment; + + while (!done) { + if (i < size) { + if (i < size - 1 && buf[i] == '\\' && + (buf[i + 1] == '\\' || buf[i + 1] == '{')) + memcpy(buf + i, buf + i + 1, size - i - 1); + else if (buf[i] == '{') + done = true; + i++; + } else { + done = true; + } + } + seg_size = i - *offset - 1; + if (seg_size < 0) + seg_size = 0; + segment = malloc(seg_size + 1); + memcpy(segment, buf + *offset, seg_size); + segment[seg_size] = '\0'; + *offset = i; + return segment; +} + +/* a dynamically growing buffer to store the potentially massive responses. + * The binary data block can be more than 100k in size (base64 encoded) */ +static void buffer_add(char **buffer, int *buffer_size, char *buf) +{ + if (!buf) + return; + if (!*buffer) { + *buffer = strdup(buf); + *buffer_size = strlen(*buffer) + 1; + } else { + *buffer_size += strlen(buf); + *buffer = realloc(*buffer, *buffer_size); + strcat(*buffer, buf); + } +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "added \"%s\" to buffer - new length %d\n", buf, *buffer_size); +#endif +} + +/* are there more ANS files we can check? */ +static bool next_file(int max) +{ + if (filenr >= max) + return false; + filenr++; + return true; +} + +/* try and do a quick decode - without trying to get to fancy in case the data + * straddles a block boundary... + * we are parsing something that looks like this: + * object_id{int{2{date{ts{2011-04-05T12:38:04{duration{float{12.000... + */ +static char *first_object_id_val(char *buf) +{ + char *object, *bufend; + if (!buf) + return NULL; + bufend = buf + strlen(buf); + object = strstr(buf, "object_id"); + if (object && object + 14 < bufend) { + /* get the value */ + char tmp[36]; + char *p = object + 14; + char *t = tmp; + +#if UEMIS_DEBUG & 4 + char debugbuf[50]; + strncpy(debugbuf, object, 49); + debugbuf[49] = '\0'; + fprintf(debugfile, "buf |%s|\n", debugbuf); +#endif + while (p < bufend && *p != '{' && t < tmp + 9) + *t++ = *p++; + if (*p == '{') { + /* found the object_id - let's quickly look for the date */ + if (strncmp(p, "{date{ts{", 9) == 0 && strstr(p, "{duration{") != NULL) { + /* cool - that was easy */ + *t++ = ','; + *t++ = ' '; + /* skip the 9 characters we just found and take the date, ignoring the seconds + * and replace the silly 'T' in the middle with a space */ + strncpy(t, p + 9, 16); + if (*(t + 10) == 'T') + *(t + 10) = ' '; + t += 16; + } + *t = '\0'; + return strdup(tmp); + } + } + return NULL; +} + +/* ultra-simplistic; it doesn't deal with the case when the object_id is + * split across two chunks. It also doesn't deal with the discrepancy between + * object_id and dive number as understood by the dive computer */ +static void show_progress(char *buf, const char *what) +{ + char *val = first_object_id_val(buf); + if (val) { +/* let the user know what we are working on */ +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "reading %s\n %s\n %s\n", what, val, buf); +#endif + uemis_info(translate("gettextFromC", "%s %s"), what, val); + free(val); + } +} + +static void uemis_increased_timeout(int *timeout) +{ + if (*timeout < UEMIS_MAX_TIMEOUT) + *timeout += UEMIS_LONG_TIMEOUT; + usleep(*timeout); +} + +/* send a request to the dive computer and collect the answer */ +static bool uemis_get_answer(const char *path, char *request, int n_param_in, + int n_param_out, const char **error_text) +{ + int i = 0, file_length; + char sb[BUFLEN]; + char fl[13]; + char tmp[101]; + const char *what = translate("gettextFromC", "data"); + bool searching = true; + bool assembling_mbuf = false; + bool ismulti = false; + bool found_answer = false; + bool more_files = true; + bool answer_in_mbuf = false; + char *ans_path; + int ans_file; + int timeout = UEMIS_LONG_TIMEOUT; + + reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); + if (reqtxt_file == -1) { + *error_text = "can't open req.txt"; +#ifdef UEMIS_DEBUG + fprintf(debugfile, "open %s failed with errno %d\n", reqtxt_path, errno); +#endif + return false; + } + snprintf(sb, BUFLEN, "n%04d12345678", filenr); + str_append_with_delim(sb, request); + for (i = 0; i < n_param_in; i++) + str_append_with_delim(sb, param_buff[i]); + if (!strcmp(request, "getDivelogs") || !strcmp(request, "getDeviceData") || !strcmp(request, "getDirectory") || + !strcmp(request, "getDivespot") || !strcmp(request, "getDive")) { + answer_in_mbuf = true; + str_append_with_delim(sb, ""); + if (!strcmp(request, "getDivelogs")) + what = translate("gettextFromC", "divelog #"); + else if (!strcmp(request, "getDivespot")) + what = translate("gettextFromC", "divespot #"); + else if (!strcmp(request, "getDive")) + what = translate("gettextFromC", "details for #"); + } + str_append_with_delim(sb, ""); + file_length = strlen(sb); + snprintf(fl, 10, "%08d", file_length - 13); + memcpy(sb + 5, fl, strlen(fl)); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "::w req.txt \"%s\"\n", sb); +#endif + if (write(reqtxt_file, sb, strlen(sb)) != strlen(sb)) { + *error_text = translate("gettextFromC", ERR_FS_SHORT_WRITE); + return false; + } + if (!next_file(number_of_files)) { + *error_text = translate("gettextFromC", ERR_FS_FULL); + more_files = false; + } + trigger_response(reqtxt_file, "n", filenr, file_length); + usleep(timeout); + mbuf = NULL; + mbuf_size = 0; + while (searching || assembling_mbuf) { + if (import_thread_cancelled) + return false; + progress_bar_fraction = filenr / (double)UEMIS_MAX_FILES; + snprintf(fl, 13, "ANS%d.TXT", filenr - 1); + ans_path = build_filename(build_filename(path, "ANS"), fl); + ans_file = subsurface_open(ans_path, O_RDONLY, 0666); + read(ans_file, tmp, 100); + close(ans_file); +#if UEMIS_DEBUG & 8 + tmp[100] = '\0'; + fprintf(debugfile, "::t %s \"%s\"\n", ans_path, tmp); +#elif UEMIS_DEBUG & 4 + char pbuf[4]; + pbuf[0] = tmp[0]; + pbuf[1] = tmp[1]; + pbuf[2] = tmp[2]; + pbuf[3] = 0; + fprintf(debugfile, "::t %s \"%s...\"\n", ans_path, pbuf); +#endif + free(ans_path); + if (tmp[0] == '1') { + searching = false; + if (tmp[1] == 'm') { + assembling_mbuf = true; + ismulti = true; + } + if (tmp[2] == 'e') + assembling_mbuf = false; + if (assembling_mbuf) { + if (!next_file(number_of_files)) { + *error_text = translate("gettextFromC", ERR_FS_FULL); + more_files = false; + assembling_mbuf = false; + } + reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); + if (reqtxt_file == -1) { + *error_text = "can't open req.txt"; + fprintf(stderr, "open %s failed with errno %d\n", reqtxt_path, errno); + return false; + } + trigger_response(reqtxt_file, "n", filenr, file_length); + } + } else { + if (!next_file(number_of_files - 1)) { + *error_text = translate("gettextFromC", ERR_FS_FULL); + more_files = false; + assembling_mbuf = false; + searching = false; + } + reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); + if (reqtxt_file == -1) { + *error_text = "can't open req.txt"; + fprintf(stderr, "open %s failed with errno %d\n", reqtxt_path, errno); + return false; + } + trigger_response(reqtxt_file, "r", filenr, file_length); + uemis_increased_timeout(&timeout); + } + if (ismulti && more_files && tmp[0] == '1') { + int size; + snprintf(fl, 13, "ANS%d.TXT", assembling_mbuf ? filenr - 2 : filenr - 1); + ans_path = build_filename(build_filename(path, "ANS"), fl); + ans_file = subsurface_open(ans_path, O_RDONLY, 0666); + free(ans_path); + size = bytes_available(ans_file); + if (size > 3) { + char *buf; + int r; + if (lseek(ans_file, 3, SEEK_CUR) == -1) + goto fs_error; + buf = malloc(size - 2); + if ((r = read(ans_file, buf, size - 3)) != size - 3) { + free(buf); + goto fs_error; + } + buf[r] = '\0'; + buffer_add(&mbuf, &mbuf_size, buf); + show_progress(buf, what); + free(buf); + param_buff[3]++; + } + close(ans_file); + timeout = UEMIS_TIMEOUT; + usleep(UEMIS_TIMEOUT); + } + } + if (more_files) { + int size = 0, j = 0; + char *buf = NULL; + + if (!ismulti) { + snprintf(fl, 13, "ANS%d.TXT", filenr - 1); + ans_path = build_filename(build_filename(path, "ANS"), fl); + ans_file = subsurface_open(ans_path, O_RDONLY, 0666); + free(ans_path); + size = bytes_available(ans_file); + if (size > 3) { + int r; + if (lseek(ans_file, 3, SEEK_CUR) == -1) + goto fs_error; + buf = malloc(size - 2); + if ((r = read(ans_file, buf, size - 3)) != size - 3) { + free(buf); + goto fs_error; + } + buf[r] = '\0'; + buffer_add(&mbuf, &mbuf_size, buf); + show_progress(buf, what); +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "::r %s \"%s\"\n", fl, buf); +#endif + } + size -= 3; + close(ans_file); + } else { + ismulti = false; + } +#if UEMIS_DEBUG & 8 + fprintf(debugfile, ":r: %s\n", buf); +#endif + if (!answer_in_mbuf) + for (i = 0; i < n_param_out && j < size; i++) + param_buff[i] = next_segment(buf, &j, size); + found_answer = true; + free(buf); + } +#if UEMIS_DEBUG & 1 + for (i = 0; i < n_param_out; i++) + fprintf(debugfile, "::: %d: %s\n", i, param_buff[i]); +#endif + return found_answer; +fs_error: + close(ans_file); + return false; +} + +static bool parse_divespot(char *buf) +{ + char *bp = buf + 1; + char *tp = next_token(&bp); + char *tag, *type, *val; + char locationstring[1024] = ""; + int divespot, len; + double latitude = 0.0, longitude = 0.0; + + // dive spot got deleted, so fail here + if (strstr(bp, "deleted{bool{true")) + return false; + // not a dive spot, fail here + if (strcmp(tp, "divespot")) + return false; + do + tag = next_token(&bp); + while (*tag && strcmp(tag, "object_id")); + if (!*tag) + return false; + next_token(&bp); + val = next_token(&bp); + divespot = atoi(val); + do { + tag = next_token(&bp); + type = next_token(&bp); + val = next_token(&bp); + if (!strcmp(type, "string") && *val && strcmp(val, " ")) { + len = strlen(locationstring); + snprintf(locationstring + len, sizeof(locationstring) - len, + "%s%s", len ? ", " : "", val); + } else if (!strcmp(type, "float")) { + if (!strcmp(tag, "longitude")) + longitude = ascii_strtod(val, NULL); + else if (!strcmp(tag, "latitude")) + latitude = ascii_strtod(val, NULL); + } + } while (tag && *tag); + + uemis_set_divelocation(divespot, locationstring, longitude, latitude); + return true; +} + +static char *suit[] = {"", QT_TRANSLATE_NOOP("gettextFromC", "wetsuit"), QT_TRANSLATE_NOOP("gettextFromC", "semidry"), QT_TRANSLATE_NOOP("gettextFromC", "drysuit")}; +static char *suit_type[] = {"", QT_TRANSLATE_NOOP("gettextFromC", "shorty"), QT_TRANSLATE_NOOP("gettextFromC", "vest"), QT_TRANSLATE_NOOP("gettextFromC", "long john"), QT_TRANSLATE_NOOP("gettextFromC", "jacket"), QT_TRANSLATE_NOOP("gettextFromC", "full suit"), QT_TRANSLATE_NOOP("gettextFromC", "2 pcs full suit")}; +static char *suit_thickness[] = {"", "0.5-2mm", "2-3mm", "3-5mm", "5-7mm", "8mm+", QT_TRANSLATE_NOOP("gettextFromC", "membrane")}; + +static void parse_tag(struct dive *dive, char *tag, char *val) +{ + /* we can ignore computer_id, water and gas as those are redundant + * with the binary data and would just get overwritten */ +#if UEMIS_DEBUG & 4 + if (strcmp(tag, "file_content")) + fprintf(debugfile, "Adding to dive %d : %s = %s\n", dive->dc.diveid, tag, val); +#endif + if (!strcmp(tag, "date")) { + uemis_ts(val, &dive->when); + } else if (!strcmp(tag, "duration")) { + uemis_duration(val, &dive->dc.duration); + } else if (!strcmp(tag, "depth")) { + uemis_depth(val, &dive->dc.maxdepth); + } else if (!strcmp(tag, "file_content")) { + uemis_parse_divelog_binary(val, dive); + } else if (!strcmp(tag, "altitude")) { + uemis_get_index(val, &dive->dc.surface_pressure.mbar); + } else if (!strcmp(tag, "f32Weight")) { + uemis_get_weight(val, &dive->weightsystem[0], dive->dc.diveid); + } else if (!strcmp(tag, "notes")) { + uemis_add_string(val, &dive->notes, " "); + } else if (!strcmp(tag, "u8DiveSuit")) { + if (*suit[atoi(val)]) + uemis_add_string(translate("gettextFromC", suit[atoi(val)]), &dive->suit, " "); + } else if (!strcmp(tag, "u8DiveSuitType")) { + if (*suit_type[atoi(val)]) + uemis_add_string(translate("gettextFromC", suit_type[atoi(val)]), &dive->suit, " "); + } else if (!strcmp(tag, "u8SuitThickness")) { + if (*suit_thickness[atoi(val)]) + uemis_add_string(translate("gettextFromC", suit_thickness[atoi(val)]), &dive->suit, " "); + } else if (!strcmp(tag, "nickname")) { + uemis_add_string(val, &dive->buddy, ","); + } +} + +static bool uemis_delete_dive(device_data_t *devdata, uint32_t diveid) +{ + struct dive *dive = NULL; + + if (devdata->download_table->dives[devdata->download_table->nr - 1]->dc.diveid == diveid) { + /* we hit the last one in the array */ + dive = devdata->download_table->dives[devdata->download_table->nr - 1]; + } else { + for (int i = 0; i < devdata->download_table->nr - 1; i++) { + if (devdata->download_table->dives[i]->dc.diveid == diveid) { + dive = devdata->download_table->dives[i]; + for (int x = i; x < devdata->download_table->nr - 1; x++) + devdata->download_table->dives[i] = devdata->download_table->dives[x + 1]; + } + } + } + if (dive) { + devdata->download_table->dives[--devdata->download_table->nr] = NULL; + if (dive->tripflag != TF_NONE) + remove_dive_from_trip(dive, false); + + free(dive->dc.sample); + free((void *)dive->notes); + free((void *)dive->divemaster); + free((void *)dive->buddy); + free((void *)dive->suit); + taglist_free(dive->tag_list); + free(dive); + + return true; + } + return false; +} + +/* This function is called for both divelog and dive information that we get + * from the SDA (what an insane design, btw). The object_id in the divelog + * matches the logfilenr in the dive information (which has its own, often + * different object_id) - we use this as the diveid. + * We create the dive when parsing the divelog and then later, when we parse + * the dive information we locate the already created dive via its diveid. + * Most things just get parsed and converted into our internal data structures, + * but the dive location API is even more crazy. We just get an id that is an + * index into yet another data store that we read out later. In order to + * correctly populate the location and gps data from that we need to remember + * the adresses of those fields for every dive that references the divespot. */ +static bool process_raw_buffer(device_data_t *devdata, uint32_t deviceid, char *inbuf, char **max_divenr, bool keep_number, int *for_dive) +{ + char *buf = strdup(inbuf); + char *tp, *bp, *tag, *type, *val; + bool done = false; + int inbuflen = strlen(inbuf); + char *endptr = buf + inbuflen; + bool is_log = false, is_dive = false; + char *sections[10]; + int s, nr_sections = 0; + struct dive *dive = NULL; + char dive_no[10]; + +#if UEMIS_DEBUG & 8 + fprintf(debugfile, "p_r_b %s\n", inbuf); +#endif + if (for_dive) + *for_dive = -1; + bp = buf + 1; + tp = next_token(&bp); + if (strcmp(tp, "divelog") == 0) { + /* this is a divelog */ + is_log = true; + tp = next_token(&bp); + /* is it a valid entry or nothing ? */ + if (strcmp(tp, "1.0") != 0 || strstr(inbuf, "divelog{1.0{{{{")) { + free(buf); + return false; + } + } else if (strcmp(tp, "dive") == 0) { + /* this is dive detail */ + is_dive = true; + tp = next_token(&bp); + if (strcmp(tp, "1.0") != 0) { + free(buf); + return false; + } + } else { + /* don't understand the buffer */ + free(buf); + return false; + } + if (is_log) { + dive = uemis_start_dive(deviceid); + } else { + /* remember, we don't know if this is the right entry, + * so first test if this is even a valid entry */ + if (strstr(inbuf, "deleted{bool{true")) { +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "p_r_b entry deleted\n"); +#endif + /* oops, this one isn't valid, suggest to try the previous one */ + free(buf); + return false; + } + /* quickhack and workaround to capture the original dive_no + * i am doing this so I dont have to change the original design + * but when parsing a dive we never parse the dive number because + * at the time it's being read the *dive varible is not set because + * the dive_no tag comes before the object_id in the uemis ans file + */ + dive_no[0] = '\0'; + char *dive_no_buf = strdup(inbuf); + char *dive_no_ptr = strstr(dive_no_buf, "dive_no{int{") + 12; + if (dive_no_ptr) { + char *dive_no_end = strstr(dive_no_ptr, "{"); + if (dive_no_end) { + *dive_no_end = '\0'; + strncpy(dive_no, dive_no_ptr, 9); + dive_no[9] = '\0'; + } + } + free(dive_no_buf); + } + while (!done) { + /* the valid buffer ends with a series of delimiters */ + if (bp >= endptr - 2 || !strcmp(bp, "{{")) + break; + tag = next_token(&bp); + /* we also end if we get an empty tag */ + if (*tag == '\0') + break; + for (s = 0; s < nr_sections; s++) + if (!strcmp(tag, sections[s])) { + tag = next_token(&bp); + break; + } + type = next_token(&bp); + if (!strcmp(type, "1.0")) { + /* this tells us the sections that will follow; the tag here + * is of the format dive-
*/ + sections[nr_sections] = strchr(tag, '-') + 1; +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "Expect to find section %s\n", sections[nr_sections]); +#endif + if (nr_sections < sizeof(sections) - 1) + nr_sections++; + continue; + } + val = next_token(&bp); +#if UEMIS_DEBUG & 8 + if (strlen(val) < 20) + fprintf(debugfile, "Parsed %s, %s, %s\n*************************\n", tag, type, val); +#endif + if (is_log && strcmp(tag, "object_id") == 0) { + free(*max_divenr); + *max_divenr = strdup(val); + dive->dc.diveid = atoi(val); +#if UEMIS_DEBUG % 2 + fprintf(debugfile, "Adding new dive from log with object_id %d.\n", atoi(val)); +#endif + } else if (is_dive && strcmp(tag, "logfilenr") == 0) { + /* this one tells us which dive we are adding data to */ + dive = get_dive_by_uemis_diveid(devdata, atoi(val)); + if (strcmp(dive_no, "0")) + dive->number = atoi(dive_no); + if (for_dive) + *for_dive = atoi(val); + } else if (!is_log && dive && !strcmp(tag, "divespot_id")) { + int divespot_id = atoi(val); + if (divespot_id != -1) { + dive->dive_site_uuid = create_dive_site("from Uemis", dive->when); + uemis_mark_divelocation(dive->dc.diveid, divespot_id, dive->dive_site_uuid); + } +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Created divesite %d for diveid : %d\n", dive->dive_site_uuid, dive->dc.diveid); +#endif + } else if (dive) { + parse_tag(dive, tag, val); + } + if (is_log && !strcmp(tag, "file_content")) + done = true; + /* done with one dive (got the file_content tag), but there could be more: + * a '{' indicates the end of the record - but we need to see another "{{" + * later in the buffer to know that the next record is complete (it could + * be a short read because of some error */ + if (done && ++bp < endptr && *bp != '{' && strstr(bp, "{{")) { + done = false; + record_uemis_dive(devdata, dive); + mark_divelist_changed(true); + dive = uemis_start_dive(deviceid); + } + } + if (is_log) { + if (dive->dc.diveid) { + record_uemis_dive(devdata, dive); + mark_divelist_changed(true); + } else { /* partial dive */ + free(dive); + free(buf); + return false; + } + } + free(buf); + return true; +} + +static char *uemis_get_divenr(char *deviceidstr, int force) +{ + uint32_t deviceid, maxdiveid = 0; + int i; + char divenr[10]; + struct dive_table *table; + deviceid = atoi(deviceidstr); + + /* + * If we are are retrying after a disconnect/reconnect, we + * will look up the highest dive number in the dives we + * already have. + * + * Also, if "force_download" is true, do this even if we + * don't have any dives (maxdiveid will remain zero) + */ + if (force || downloadTable.nr) + table = &downloadTable; + else + table = &dive_table; + + for (i = 0; i < table->nr; i++) { + struct dive *d = table->dives[i]; + struct divecomputer *dc; + if (!d) + continue; + for_each_dc (d, dc) { + if (dc->model && !strcmp(dc->model, "Uemis Zurich") && + (dc->deviceid == 0 || dc->deviceid == 0x7fffffff || dc->deviceid == deviceid) && + dc->diveid > maxdiveid) + maxdiveid = dc->diveid; + } + } + snprintf(divenr, 10, "%d", maxdiveid); + return strdup(divenr); +} + +#if UEMIS_DEBUG +static int bufCnt = 0; +static bool do_dump_buffer_to_file(char *buf, char *prefix) +{ + char path[100]; + char date[40]; + char obid[40]; + if (!buf) + return false; + + if (strstr(buf, "date{ts{")) + strncpy(date, strstr(buf, "date{ts{"), sizeof(date)); + else + strncpy(date, "date{ts{no-date{", sizeof(date)); + + if (!strstr(buf, "object_id{int{")) + return false; + + strncpy(obid, strstr(buf, "object_id{int{"), sizeof(obid)); + char *ptr1 = strstr(date, "date{ts{"); + char *ptr2 = strstr(obid, "object_id{int{"); + char *pdate = next_token(&ptr1); + pdate = next_token(&ptr1); + pdate = next_token(&ptr1); + char *pobid = next_token(&ptr2); + pobid = next_token(&ptr2); + pobid = next_token(&ptr2); + snprintf(path, sizeof(path), "/%s/%s/UEMIS Dump/%s_%s_Uemis_dump_%s_in_round_%d_%d.txt", home, user, prefix, pdate, pobid, debug_round, bufCnt); + int dumpFile = subsurface_open(path, O_RDWR | O_CREAT, 0666); + if (dumpFile == -1) + return false; + write(dumpFile, buf, strlen(buf)); + close(dumpFile); + bufCnt++; + return true; +} +#endif + +/* do some more sophisticated calculations here to try and predict if the next round of + * divelog/divedetail reads will fit into the UEMIS buffer, + * filenr holds now the uemis filenr after having read several logs including the dive details, + * fCapacity will five us the average number of files needed for all currently loaded data + * remember the maximum file usage per dive + * return : UEMIS_MEM_OK if there is enough memeory for a full round + * UEMIS_MEM_CRITICAL if the memory is good for reading the dive logs + * UEMIS_MEM_FULL if the memory is exhaused + */ +static int get_memory(struct dive_table *td, int checkpoint) +{ + if (td->nr <= 0) + return UEMIS_MEM_OK; + + switch (checkpoint) { + case UEMIS_CHECK_LOG: + if (filenr / td->nr > max_mem_used) + max_mem_used = filenr / td->nr; + + /* check if a full block of dive logs + dive details and dive spot fit into the UEMIS buffer */ + if (max_mem_used * UEMIS_LOG_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) + return UEMIS_MEM_FULL; + break; + case UEMIS_CHECK_DETAILS: + /* check if the next set of dive details and dive spot fit into the UEMIS buffer */ + if ((UEMIS_DIVE_DETAILS_SIZE + UEMIS_SPOT_BLOCK_SIZE) * UEMIS_LOG_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) + return UEMIS_MEM_FULL; + break; + case UEMIS_CHECK_SINGLE_DIVE: + if (UEMIS_DIVE_DETAILS_SIZE + UEMIS_SPOT_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) + return UEMIS_MEM_FULL; + break; + } + return UEMIS_MEM_OK; +} + +/* mark a dive as deleted by setting download to false + * this will be picked up by some cleaning statement later */ +static void do_delete_dives(struct dive_table *td, int idx) +{ + for (int x = idx; x < td->nr; x++) + td->dives[x]->downloaded = false; +} + +static bool load_uemis_divespot(const char *mountpath, int divespot_id) +{ + char divespotnr[10]; + snprintf(divespotnr, sizeof(divespotnr), "%d", divespot_id); + param_buff[2] = divespotnr; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "getDivespot %d\n", divespot_id); +#endif + bool success = uemis_get_answer(mountpath, "getDivespot", 3, 0, NULL); + if (mbuf && success) { +#if UEMIS_DEBUG & 16 + do_dump_buffer_to_file(mbuf, "Spot"); +#endif + return parse_divespot(mbuf); + } + return false; +} + +static void get_uemis_divespot(const char *mountpath, int divespot_id, struct dive *dive) +{ + struct dive_site *nds = get_dive_site_by_uuid(dive->dive_site_uuid); + if (nds && nds->name && strstr(nds->name,"from Uemis")) { + if (load_uemis_divespot(mountpath, divespot_id)) { + /* get the divesite based on the diveid, this should give us + * the newly created site + */ + struct dive_site *ods = NULL; + /* with the divesite name we got from parse_dive, that is called on load_uemis_divespot + * we search all existing divesites if we have one with the same name already. The function + * returns the first found which is luckily not the newly created. + */ + (void)get_dive_site_uuid_by_name(nds->name, &ods); + if (ods) { + /* if the uuid's are the same, the new site is a duplicat and can be deleted */ + if (nds->uuid != ods->uuid) { + delete_dive_site(nds->uuid); + dive->dive_site_uuid = ods->uuid; + } + } + } else { + /* if we cant load the dive site details, delete the site we + * created in process_raw_buffer + */ + delete_dive_site(dive->dive_site_uuid); + } + } +} + +static bool get_matching_dive(int idx, char *newmax, int *uemis_mem_status, struct device_data_t *data, const char *mountpath, const char deviceidnr) +{ + struct dive *dive = data->download_table->dives[idx]; + char log_file_no_to_find[20]; + char dive_to_read_buf[10]; + bool found = false; + int deleted_files = 0; + + snprintf(log_file_no_to_find, sizeof(log_file_no_to_find), "logfilenr{int{%d", dive->dc.diveid); + while (!found) { + if (import_thread_cancelled) + break; + snprintf(dive_to_read_buf, sizeof(dive_to_read_buf), "%d", dive_to_read); + param_buff[2] = dive_to_read_buf; + (void)uemis_get_answer(mountpath, "getDive", 3, 0, NULL); +#if UEMIS_DEBUG & 16 + do_dump_buffer_to_file(mbuf, "Dive"); +#endif + *uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_SINGLE_DIVE); + if (*uemis_mem_status == UEMIS_MEM_OK) { + /* if the memory isn's completely full we can try to read more divelog vs. dive details + * UEMIS_MEM_CRITICAL means not enough space for a full round but the dive details + * and the divespots should fit into the UEMIS memory + * The match we do here is to map the object_id to the logfilenr, we do this + * by iterating through the last set of loaded divelogs and then find the corresponding + * dive with the matching logfilenr */ + if (mbuf) { + if (strstr(mbuf, log_file_no_to_find)) { + /* we found the logfilenr that matches our object_id from the divelog we were looking for + * we mark the search sucessfull even if the dive has been deleted. */ + found = true; + if (strstr(mbuf, "deleted{bool{true") == NULL) { + process_raw_buffer(data, deviceidnr, mbuf, &newmax, false, NULL); + /* remember the last log file number as it is very likely that subsequent dives + * have the same or higher logfile number. + * UEMIS unfortunately deletes dives by deleting the dive details and not the logs. */ +#if UEMIS_DEBUG & 2 + d_time = get_dive_date_c_string(dive->when); + fprintf(debugfile, "Matching divelog id %d from %s with dive details %d\n", dive->dc.diveid, d_time, dive_to_read); +#endif + int divespot_id = uemis_get_divespot_id_by_diveid(dive->dc.diveid); + if (divespot_id >= 0) + get_uemis_divespot(mountpath, divespot_id, dive); + + } else { + /* in this case we found a deleted file, so let's increment */ +#if UEMIS_DEBUG & 2 + d_time = get_dive_date_c_string(dive->when); + fprintf(debugfile, "TRY matching divelog id %d from %s with dive details %d but details are deleted\n", dive->dc.diveid, d_time, dive_to_read); +#endif + deleted_files++; + /* mark this log entry as deleted and cleanup later, otherwise we mess up our array */ + dive->downloaded = false; +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Deleted dive from %s, with id %d from table\n", d_time, dive->dc.diveid); +#endif + } + } else { + uint32_t nr_found = 0; + char *logfilenr = strstr(mbuf, "logfilenr"); + if (logfilenr) { + sscanf(logfilenr, "logfilenr{int{%u", &nr_found); + if (nr_found >= dive->dc.diveid) + dive_to_read = dive_to_read - 2; + if (dive_to_read < -1) + dive_to_read = -1; + } + } + } + dive_to_read++; + } else { + /* At this point the memory of the UEMIS is full, let's cleanup all divelog files were + * we could not match the details to. */ + do_delete_dives(data->download_table, idx); + return false; + } + } + /* decrement iDiveToRead by the amount of deleted entries found to assure + * we are not missing any valid matches when processing subsequent logs */ + dive_to_read = (dive_to_read - deleted_files > 0 ? dive_to_read - deleted_files : 0); + deleted_files = 0; + return true; +} + +const char *do_uemis_import(device_data_t *data) +{ + const char *mountpath = data->devname; + short force_download = data->force_download; + char *newmax = NULL; + int first, start, end = -2; + uint32_t deviceidnr; + char *deviceid = NULL; + const char *result = NULL; + char *endptr; + bool success, keep_number = false, once = true; + int match_dive_and_log = 0; + int uemis_mem_status = UEMIS_MEM_OK; + +#if UEMIS_DEBUG + home = getenv("HOME"); + user = getenv("LOGNAME"); +#endif + if (dive_table.nr == 0) + keep_number = true; + + uemis_info(translate("gettextFromC", "Initialise communication")); + if (!uemis_init(mountpath)) { + free(reqtxt_path); + return translate("gettextFromC", "Uemis init failed"); + } + + if (!uemis_get_answer(mountpath, "getDeviceId", 0, 1, &result)) + goto bail; + deviceid = strdup(param_buff[0]); + deviceidnr = atoi(deviceid); + + /* param_buff[0] is still valid */ + if (!uemis_get_answer(mountpath, "initSession", 1, 6, &result)) + goto bail; + + uemis_info(translate("gettextFromC", "Start download")); + if (!uemis_get_answer(mountpath, "processSync", 0, 2, &result)) + goto bail; + + /* before starting the long download, check if user pressed cancel */ + if (import_thread_cancelled) + goto bail; + + param_buff[1] = "notempty"; + newmax = uemis_get_divenr(deviceid, force_download); + + first = start = atoi(newmax); + dive_to_read = first; + for (;;) { +#if UEMIS_DEBUG & 2 + debug_round++; +#endif +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i inner loop start %d end %d newmax %s\n", start, end, newmax); +#endif + /* start at the last filled download table index */ + match_dive_and_log = data->download_table->nr; + sprintf(newmax, "%d", start); + param_buff[2] = newmax; + param_buff[3] = 0; + success = uemis_get_answer(mountpath, "getDivelogs", 3, 0, &result); + uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_DETAILS); + if (success && mbuf && uemis_mem_status != UEMIS_MEM_FULL) { +#if UEMIS_DEBUG & 16 + do_dump_buffer_to_file(mbuf, "Divelogs"); +#endif + /* process the buffer we have assembled */ + + if (!process_raw_buffer(data, deviceidnr, mbuf, &newmax, keep_number, NULL)) { + /* if no dives were downloaded, mark end appropriately */ + if (end == -2) + end = start - 1; + success = false; + } + if (once) { + char *t = first_object_id_val(mbuf); + if (t && atoi(t) > start) + start = atoi(t); + free(t); + once = false; + } + /* clean up mbuf */ + endptr = strstr(mbuf, "{{{"); + if (endptr) + *(endptr + 2) = '\0'; + /* last object_id we parsed */ + sscanf(newmax, "%d", &end); +#if UEMIS_DEBUG & 4 + fprintf(debugfile, "d_u_i after download and parse start %d end %d newmax %s progress %4.2f\n", start, end, newmax, progress_bar_fraction); +#endif + /* The way this works is that I am reading the current dive from what has been loaded during the getDiveLogs call to the UEMIS. + * As the object_id of the divelog entry and the object_id of the dive details are not necessarily the same, the match needs + * to happen based on the logfilenr. + * What the following part does is to optimize the mapping by using + * dive_to_read = the dive deatils entry that need to be read using the object_id + * logFileNoToFind = map the logfilenr of the dive details with the object_id = diveid from the get dive logs */ + for (int i = match_dive_and_log; i < data->download_table->nr; i++) { + bool success = get_matching_dive(i, newmax, &uemis_mem_status, data, mountpath, deviceidnr); + if (!success) + break; + if (import_thread_cancelled) + break; + } + + start = end; + + /* Do some memory checking here */ + uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_LOG); + if (uemis_mem_status != UEMIS_MEM_OK) + break; + + /* if the user clicked cancel, exit gracefully */ + if (import_thread_cancelled) + break; + + /* if we got an error or got nothing back, stop trying */ + if (!success || !param_buff[3]) + break; +#if UEMIS_DEBUG & 2 + if (debug_round != -1) + if (debug_round-- == 0) + goto bail; +#endif + } else { + /* some of the loading from the UEMIS failed at the divelog level + * if the memory status = full, we cant even load the divespots and/or buddys. + * The loaded block of divelogs is useless and all new loaded divelogs need to + * be deleted from the download_table. + */ + if (uemis_mem_status == UEMIS_MEM_FULL) + do_delete_dives(data->download_table, match_dive_and_log); + break; + } + } + + if (end == -2 && sscanf(newmax, "%d", &end) != 1) + end = start; + +#if UEMIS_DEBUG & 2 + fprintf(debugfile, "Done: read from object_id %d to %d\n", first, end); +#endif + + /* Regardless on where we are with the memory situation, it's time now + * to see if we have to clean some dead bodies from our download table */ + next_table_index = 0; + while (next_table_index < data->download_table->nr) { + if (!data->download_table->dives[next_table_index]->downloaded) + uemis_delete_dive(data, data->download_table->dives[next_table_index]->dc.diveid); + else + next_table_index++; + } + + if (uemis_mem_status != UEMIS_MEM_OK) + result = translate("gettextFromC", ERR_FS_ALMOST_FULL); + +bail: + (void)uemis_get_answer(mountpath, "terminateSync", 0, 3, &result); + if (!strcmp(param_buff[0], "error")) { + if (!strcmp(param_buff[2], "Out of Memory")) + result = translate("gettextFromC", ERR_FS_FULL); + else + result = param_buff[2]; + } + free(deviceid); + free(reqtxt_path); + if (!data->download_table->nr) + result = translate("gettextFromC", ERR_NO_FILES); + return result; +} diff --git a/subsurface-core/uemis.c b/subsurface-core/uemis.c new file mode 100644 index 000000000..4135e0cfe --- /dev/null +++ b/subsurface-core/uemis.c @@ -0,0 +1,392 @@ +/* + * uemis.c + * + * UEMIS SDA file importer + * AUTHOR: Dirk Hohndel - Copyright 2011 + * + * Licensed under the MIT license. + */ +#include +#include + +#include "gettext.h" + +#include "dive.h" +#include "uemis.h" +#include +#include + +/* + * following code is based on code found in at base64.sourceforge.net/b64.c + * AUTHOR: Bob Trower 08/04/01 + * COPYRIGHT: Copyright (c) Trantor Standard Systems Inc., 2001 + * NOTE: This source code may be used as you wish, subject to + * the MIT license. + */ +/* + * Translation Table to decode (created by Bob Trower) + */ +static const char cd64[] = "|$$$}rstuvwxyz{$$$$$$$>?@ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ[\\]^_`abcdefghijklmnopq"; + +/* + * decodeblock -- decode 4 '6-bit' characters into 3 8-bit binary bytes + */ +static void decodeblock(unsigned char in[4], unsigned char out[3]) +{ + out[0] = (unsigned char)(in[0] << 2 | in[1] >> 4); + out[1] = (unsigned char)(in[1] << 4 | in[2] >> 2); + out[2] = (unsigned char)(((in[2] << 6) & 0xc0) | in[3]); +} + +/* + * decode a base64 encoded stream discarding padding, line breaks and noise + */ +static void decode(uint8_t *inbuf, uint8_t *outbuf, int inbuf_len) +{ + uint8_t in[4], out[3], v; + int i, len, indx_in = 0, indx_out = 0; + + while (indx_in < inbuf_len) { + for (len = 0, i = 0; i < 4 && (indx_in < inbuf_len); i++) { + v = 0; + while ((indx_in < inbuf_len) && v == 0) { + v = inbuf[indx_in++]; + v = ((v < 43 || v > 122) ? 0 : cd64[v - 43]); + if (v) + v = ((v == '$') ? 0 : v - 61); + } + if (indx_in < inbuf_len) { + len++; + if (v) + in[i] = (v - 1); + } else + in[i] = 0; + } + if (len) { + decodeblock(in, out); + for (i = 0; i < len - 1; i++) + outbuf[indx_out++] = out[i]; + } + } +} +/* end code from Bob Trower */ + +/* + * convert the base64 data blog + */ +static int uemis_convert_base64(char *base64, uint8_t **data) +{ + int len, datalen; + + len = strlen(base64); + datalen = (len / 4 + 1) * 3; + if (datalen < 0x123 + 0x25) + /* less than header + 1 sample??? */ + fprintf(stderr, "suspiciously short data block %d\n", datalen); + + *data = malloc(datalen); + if (!*data) { + fprintf(stderr, "Out of memory\n"); + return 0; + } + decode((unsigned char *)base64, *data, len); + + if (memcmp(*data, "Dive\01\00\00", 7)) + fprintf(stderr, "Missing Dive100 header\n"); + + return datalen; +} + +struct uemis_helper { + int diveid; + int lbs; + int divespot; + int dive_site_uuid; + struct uemis_helper *next; +}; +static struct uemis_helper *uemis_helper = NULL; + +static struct uemis_helper *uemis_get_helper(int diveid) +{ + struct uemis_helper **php = &uemis_helper; + struct uemis_helper *hp = *php; + + while (hp) { + if (hp->diveid == diveid) + return hp; + if (hp->next) { + hp = hp->next; + continue; + } + php = &hp->next; + break; + } + hp = *php = calloc(1, sizeof(struct uemis_helper)); + hp->diveid = diveid; + hp->next = NULL; + return hp; +} + +static void uemis_weight_unit(int diveid, int lbs) +{ + struct uemis_helper *hp = uemis_get_helper(diveid); + if (hp) + hp->lbs = lbs; +} + +int uemis_get_weight_unit(int diveid) +{ + struct uemis_helper *hp = uemis_helper; + while (hp) { + if (hp->diveid == diveid) + return hp->lbs; + hp = hp->next; + } + /* odd - we should have found this; default to kg */ + return 0; +} + +void uemis_mark_divelocation(int diveid, int divespot, uint32_t dive_site_uuid) +{ + struct uemis_helper *hp = uemis_get_helper(diveid); + hp->divespot = divespot; + hp->dive_site_uuid = dive_site_uuid; +} + +/* support finding a dive spot based on the diveid */ +int uemis_get_divespot_id_by_diveid(uint32_t diveid) +{ + struct uemis_helper *hp = uemis_helper; + while (hp) { + if (hp->diveid == diveid) + return hp->divespot; + hp = hp->next; + } + return -1; +} + +void uemis_set_divelocation(int divespot, char *text, double longitude, double latitude) +{ + struct uemis_helper *hp = uemis_helper; + while (hp) { + if (hp->divespot == divespot) { + struct dive_site *ds = get_dive_site_by_uuid(hp->dive_site_uuid); + if (ds) { + ds->name = strdup(text); + ds->longitude.udeg = round(longitude * 1000000); + ds->latitude.udeg = round(latitude * 1000000); + } + } + hp = hp->next; + } +} + +/* Create events from the flag bits and other data in the sample; + * These bits basically represent what is displayed on screen at sample time. + * Many of these 'warnings' are way hyper-active and seriously clutter the + * profile plot - so these are disabled by default + * + * we mark all the strings for translation, but we store the untranslated + * strings and only convert them when displaying them on screen - this way + * when we write them to the XML file we'll always have the English strings, + * regardless of locale + */ +static void uemis_event(struct dive *dive, struct divecomputer *dc, struct sample *sample, uemis_sample_t *u_sample) +{ + uint8_t *flags = u_sample->flags; + int stopdepth; + static int lastndl; + + if (flags[1] & 0x01) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Safety stop violation")); + if (flags[1] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Speed alarm")); +#if WANT_CRAZY_WARNINGS + if (flags[1] & 0x06) /* both bits 1 and 2 are a warning */ + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Speed warning")); + if (flags[1] & 0x10) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ green warning")); +#endif + if (flags[1] & 0x20) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ ascend warning")); + if (flags[1] & 0x40) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ ascend alarm")); + /* flags[2] reflects the deco / time bar + * flags[3] reflects more display details on deco and pO2 */ + if (flags[4] & 0x01) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Tank pressure info")); + if (flags[4] & 0x04) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "RGT warning")); + if (flags[4] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "RGT alert")); + if (flags[4] & 0x40) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Tank change suggested")); + if (flags[4] & 0x80) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Depth limit exceeded")); + if (flags[5] & 0x01) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Max deco time warning")); + if (flags[5] & 0x04) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Dive time info")); + if (flags[5] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Dive time alert")); + if (flags[5] & 0x10) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Marker")); + if (flags[6] & 0x02) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "No tank data")); + if (flags[6] & 0x04) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Low battery warning")); + if (flags[6] & 0x08) + add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Low battery alert")); +/* flags[7] reflects the little on screen icons that remind of previous + * warnings / alerts - not useful for events */ + +#if UEMIS_DEBUG & 32 + int i, j; + for (i = 0; i < 8; i++) { + printf(" %d: ", 29 + i); + for (j = 7; j >= 0; j--) + printf("%c", flags[i] & 1 << j ? '1' : '0'); + } + printf("\n"); +#endif + /* now add deco / NDL + * we don't use events but store this in the sample - that makes much more sense + * for the way we display this information + * What we know about the encoding so far: + * flags[3].bit0 | flags[5].bit1 != 0 ==> in deco + * flags[0].bit7 == 1 ==> Safety Stop + * otherwise NDL */ + stopdepth = rel_mbar_to_depth(u_sample->hold_depth, dive); + if ((flags[3] & 1) | (flags[5] & 2)) { + /* deco */ + sample->in_deco = true; + sample->stopdepth.mm = stopdepth; + sample->stoptime.seconds = u_sample->hold_time * 60; + sample->ndl.seconds = 0; + } else if (flags[0] & 128) { + /* safety stop - distinguished from deco stop by having + * both ndl and stop information */ + sample->in_deco = false; + sample->stopdepth.mm = stopdepth; + sample->stoptime.seconds = u_sample->hold_time * 60; + sample->ndl.seconds = lastndl; + } else { + /* NDL */ + sample->in_deco = false; + lastndl = sample->ndl.seconds = u_sample->hold_time * 60; + sample->stopdepth.mm = 0; + sample->stoptime.seconds = 0; + } +#if UEMIS_DEBUG & 32 + printf("%dm:%ds: p_amb_tol:%d surface:%d holdtime:%d holddepth:%d/%d ---> stopdepth:%d stoptime:%d ndl:%d\n", + sample->time.seconds / 60, sample->time.seconds % 60, u_sample->p_amb_tol, dive->dc.surface_pressure.mbar, + u_sample->hold_time, u_sample->hold_depth, stopdepth, sample->stopdepth.mm, sample->stoptime.seconds, sample->ndl.seconds); +#endif +} + +/* + * parse uemis base64 data blob into struct dive + */ +void uemis_parse_divelog_binary(char *base64, void *datap) +{ + int datalen; + int i; + uint8_t *data; + struct sample *sample = NULL; + uemis_sample_t *u_sample; + struct dive *dive = datap; + struct divecomputer *dc = &dive->dc; + int template, gasoffset; + uint8_t active = 0; + char version[5]; + + datalen = uemis_convert_base64(base64, &data); + dive->dc.airtemp.mkelvin = C_to_mkelvin((*(uint16_t *)(data + 45)) / 10.0); + dive->dc.surface_pressure.mbar = *(uint16_t *)(data + 43); + if (*(uint8_t *)(data + 19)) + dive->dc.salinity = SEAWATER_SALINITY; /* avg grams per 10l sea water */ + else + dive->dc.salinity = FRESHWATER_SALINITY; /* grams per 10l fresh water */ + + /* this will allow us to find the last dive read so far from this computer */ + dc->model = strdup("Uemis Zurich"); + dc->deviceid = *(uint32_t *)(data + 9); + dc->diveid = *(uint16_t *)(data + 7); + /* remember the weight units used in this dive - we may need this later when + * parsing the weight */ + uemis_weight_unit(dc->diveid, *(uint8_t *)(data + 24)); + /* dive template in use: + 0 = air + 1 = nitrox (B) + 2 = nitrox (B+D) + 3 = nitrox (B+T+D) + uemis cylinder data is insane - it stores seven tank settings in a block + and the template tells us which of the four groups of tanks we need to look at + */ + gasoffset = template = *(uint8_t *)(data + 115); + if (template == 3) + gasoffset = 4; + if (template == 0) + template = 1; + for (i = 0; i < template; i++) { + float volume = *(float *)(data + 116 + 25 * (gasoffset + i)) * 1000.0; + /* uemis always assumes a working pressure of 202.6bar (!?!?) - I first thought + * it was 3000psi, but testing against all my dives gets me that strange number. + * Still, that's of course completely bogus and shows they don't get how + * cylinders are named in non-metric parts of the world... + * we store the incorrect working pressure to get the SAC calculations "close" + * but the user will have to correct this manually + */ + dive->cylinder[i].type.size.mliter = rint(volume); + dive->cylinder[i].type.workingpressure.mbar = 202600; + dive->cylinder[i].gasmix.o2.permille = *(uint8_t *)(data + 120 + 25 * (gasoffset + i)) * 10; + dive->cylinder[i].gasmix.he.permille = 0; + } + /* first byte of divelog data is at offset 0x123 */ + i = 0x123; + u_sample = (uemis_sample_t *)(data + i); + while ((i <= datalen) && (data[i] != 0 || data[i + 1] != 0)) { + if (u_sample->active_tank != active) { + if (u_sample->active_tank >= MAX_CYLINDERS) { + fprintf(stderr, "got invalid sensor #%d was #%d\n", u_sample->active_tank, active); + } else { + active = u_sample->active_tank; + add_gas_switch_event(dive, dc, u_sample->dive_time, active); + } + } + sample = prepare_sample(dc); + sample->time.seconds = u_sample->dive_time; + sample->depth.mm = rel_mbar_to_depth(u_sample->water_pressure, dive); + sample->temperature.mkelvin = C_to_mkelvin(u_sample->dive_temperature / 10.0); + sample->sensor = active; + sample->cylinderpressure.mbar = + (u_sample->tank_pressure_high * 256 + u_sample->tank_pressure_low) * 10; + sample->cns = u_sample->cns; + uemis_event(dive, dc, sample, u_sample); + finish_sample(dc); + i += 0x25; + u_sample++; + } + if (sample) + dive->dc.duration.seconds = sample->time.seconds - 1; + + /* get data from the footer */ + char buffer[24]; + + snprintf(version, sizeof(version), "%1u.%02u", data[18], data[17]); + add_extra_data(dc, "FW Version", version); + snprintf(buffer, sizeof(buffer), "%08x", *(uint32_t *)(data + 9)); + add_extra_data(dc, "Serial", buffer); + snprintf(buffer, sizeof(buffer), "%d", *(uint16_t *)(data + i + 35)); + add_extra_data(dc, "main battery after dive", buffer); + snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 24), 60)); + add_extra_data(dc, "no fly time", buffer); + snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 26), 60)); + add_extra_data(dc, "no dive time", buffer); + snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 28), 60)); + add_extra_data(dc, "desat time", buffer); + snprintf(buffer, sizeof(buffer), "%u", *(uint16_t *)(data + i + 30)); + add_extra_data(dc, "allowed altitude", buffer); + + return; +} diff --git a/subsurface-core/uemis.h b/subsurface-core/uemis.h new file mode 100644 index 000000000..5f32fe76c --- /dev/null +++ b/subsurface-core/uemis.h @@ -0,0 +1,54 @@ +/* + * defines and prototypes for the uemis Zurich SDA file parser + */ + +#ifndef UEMIS_H +#define UEMIS_H + +#include +#include "dive.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void uemis_parse_divelog_binary(char *base64, void *divep); +int uemis_get_weight_unit(int diveid); +void uemis_mark_divelocation(int diveid, int divespot, uint32_t dive_site_uuid); +void uemis_set_divelocation(int divespot, char *text, double longitude, double latitude); +int uemis_get_divespot_id_by_diveid(uint32_t diveid); + +typedef struct +{ + uint16_t dive_time; + uint16_t water_pressure; // (in cbar) + uint16_t dive_temperature; // (in dC) + uint8_t ascent_speed; // (units unclear) + uint8_t work_fact; + uint8_t cold_fact; + uint8_t bubble_fact; + uint16_t ascent_time; + uint16_t ascent_time_opt; + uint16_t p_amb_tol; + uint16_t satt; + uint16_t hold_depth; + uint16_t hold_time; + uint8_t active_tank; + // bloody glib, when compiled for Windows, forces the whole program to use + // the Windows packing rules. So to avoid problems on Windows (and since + // only tank_pressure is currently used and that exactly once) I give in and + // make this silly low byte / high byte 8bit entries + uint8_t tank_pressure_low; // (in cbar) + uint8_t tank_pressure_high; + uint8_t consumption_low; // (units unclear) + uint8_t consumption_high; + uint8_t rgt; // (remaining gas time in minutes) + uint8_t cns; + uint8_t flags[8]; +} __attribute((packed)) uemis_sample_t; + +#ifdef __cplusplus +} +#endif + +#endif // UEMIS_H diff --git a/subsurface-core/units.h b/subsurface-core/units.h new file mode 100644 index 000000000..1273bd9bb --- /dev/null +++ b/subsurface-core/units.h @@ -0,0 +1,277 @@ +#ifndef UNITS_H +#define UNITS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define O2_IN_AIR 209 // permille +#define N2_IN_AIR 781 +#define O2_DENSITY 1429 // mg/Liter +#define N2_DENSITY 1251 +#define HE_DENSITY 179 +#define SURFACE_PRESSURE 1013 // mbar +#define SURFACE_PRESSURE_STRING "1013" +#define ZERO_C_IN_MKELVIN 273150 // mKelvin + +#ifdef __cplusplus +#define M_OR_FT(_m, _f) ((prefs.units.length == units::METERS) ? ((_m) * 1000) : (feet_to_mm(_f))) +#else +#define M_OR_FT(_m, _f) ((prefs.units.length == METERS) ? ((_m) * 1000) : (feet_to_mm(_f))) +#endif + +/* Salinity is expressed in weight in grams per 10l */ +#define SEAWATER_SALINITY 10300 +#define FRESHWATER_SALINITY 10000 + +#include +/* + * Some silly typedefs to make our units very explicit. + * + * Also, the units are chosen so that values can be expressible as + * integers, so that we never have FP rounding issues. And they + * are small enough that converting to/from imperial units doesn't + * really matter. + * + * We also strive to make '0' a meaningless number saying "not + * initialized", since many values are things that may not have + * been reported (eg cylinder pressure or temperature from dive + * computers that don't support them). But sometimes -1 is an even + * more explicit way of saying "not there". + * + * Thus "millibar" for pressure, for example, or "millikelvin" for + * temperatures. Doing temperatures in celsius or fahrenheit would + * make for loss of precision when converting from one to the other, + * and using millikelvin is SI-like but also means that a temperature + * of '0' is clearly just a missing temperature or cylinder pressure. + * + * Also strive to use units that can not possibly be mistaken for a + * valid value in a "normal" system without conversion. If the max + * depth of a dive is '20000', you probably didn't convert from mm on + * output, or if the max depth gets reported as "0.2ft" it was either + * a really boring dive, or there was some missing input conversion, + * and a 60-ft dive got recorded as 60mm. + * + * Doing these as "structs containing value" means that we always + * have to explicitly write out those units in order to get at the + * actual value. So there is hopefully little fear of using a value + * in millikelvin as Fahrenheit by mistake. + * + * We don't actually use these all yet, so maybe they'll change, but + * I made a number of types as guidelines. + */ +typedef int64_t timestamp_t; + +typedef struct +{ + uint32_t seconds; // durations up to 68 yrs +} duration_t; + +typedef struct +{ + int32_t seconds; // offsets up to +/- 34 yrs +} offset_t; + +typedef struct +{ + int32_t mm; +} depth_t; // depth to 2000 km + +typedef struct +{ + int32_t mbar; // pressure up to 2000 bar +} pressure_t; + +typedef struct +{ + uint16_t mbar; +} o2pressure_t; // pressure up to 65 bar + +typedef struct +{ + int16_t degrees; +} bearing_t; // compass bearing + +typedef struct +{ + int32_t mkelvin; // up to 1750 degrees K +} temperature_t; + +typedef struct +{ + int mliter; +} volume_t; + +typedef struct +{ + int permille; +} fraction_t; + +typedef struct +{ + int grams; +} weight_t; + +typedef struct +{ + int udeg; +} degrees_t; + +static inline double udeg_to_radians(int udeg) +{ + return (udeg * M_PI) / (1000000.0 * 180.0); +} + +static inline double grams_to_lbs(int grams) +{ + return grams / 453.6; +} + +static inline int lbs_to_grams(double lbs) +{ + return rint(lbs * 453.6); +} + +static inline double ml_to_cuft(int ml) +{ + return ml / 28316.8466; +} + +static inline double cuft_to_l(double cuft) +{ + return cuft * 28.3168466; +} + +static inline double mm_to_feet(int mm) +{ + return mm * 0.00328084; +} + +static inline double m_to_mile(int m) +{ + return m / 1609.344; +} + +static inline unsigned long feet_to_mm(double feet) +{ + return rint(feet * 304.8); +} + +static inline int to_feet(depth_t depth) +{ + return rint(mm_to_feet(depth.mm)); +} + +static inline double mkelvin_to_C(int mkelvin) +{ + return (mkelvin - ZERO_C_IN_MKELVIN) / 1000.0; +} + +static inline double mkelvin_to_F(int mkelvin) +{ + return mkelvin * 9 / 5000.0 - 459.670; +} + +static inline unsigned long F_to_mkelvin(double f) +{ + return rint((f - 32) * 1000 / 1.8 + ZERO_C_IN_MKELVIN); +} + +static inline unsigned long C_to_mkelvin(double c) +{ + return rint(c * 1000 + ZERO_C_IN_MKELVIN); +} + +static inline double psi_to_bar(double psi) +{ + return psi / 14.5037738; +} + +static inline long psi_to_mbar(double psi) +{ + return rint(psi_to_bar(psi) * 1000); +} + +static inline int to_PSI(pressure_t pressure) +{ + return rint(pressure.mbar * 0.0145037738); +} + +static inline double bar_to_atm(double bar) +{ + return bar / SURFACE_PRESSURE * 1000; +} + +static inline double mbar_to_atm(int mbar) +{ + return (double)mbar / SURFACE_PRESSURE; +} + +static inline int mbar_to_PSI(int mbar) +{ + pressure_t p = { mbar }; + return to_PSI(p); +} + +/* + * We keep our internal data in well-specified units, but + * the input and output may come in some random format. This + * keeps track of those units. + */ +/* turns out in Win32 PASCAL is defined as a calling convention */ +#ifdef WIN32 +#undef PASCAL +#endif +struct units { + enum { + METERS, + FEET + } length; + enum { + LITER, + CUFT + } volume; + enum { + BAR, + PSI, + PASCAL + } pressure; + enum { + CELSIUS, + FAHRENHEIT, + KELVIN + } temperature; + enum { + KG, + LBS + } weight; + enum { + SECONDS, + MINUTES + } vertical_speed_time; +}; + +/* + * We're going to default to SI units for input. Yes, + * technically the SI unit for pressure is Pascal, but + * we default to bar (10^5 pascal), which people + * actually use. Similarly, C instead of Kelvin. + * And kg instead of g. + */ +#define SI_UNITS \ + { \ + .length = METERS, .volume = LITER, .pressure = BAR, .temperature = CELSIUS, .weight = KG, .vertical_speed_time = MINUTES \ + } + +#define IMPERIAL_UNITS \ + { \ + .length = FEET, .volume = CUFT, .pressure = PSI, .temperature = FAHRENHEIT, .weight = LBS, .vertical_speed_time = MINUTES \ + } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/subsurface-core/version.c b/subsurface-core/version.c new file mode 100644 index 000000000..5b54bf4c7 --- /dev/null +++ b/subsurface-core/version.c @@ -0,0 +1,16 @@ +#include "ssrf-version.h" + +const char *subsurface_version(void) +{ + return VERSION_STRING; +} + +const char *subsurface_git_version(void) +{ + return GIT_VERSION_STRING; +} + +const char *subsurface_canonical_version(void) +{ + return CANONICAL_VERSION_STRING; +} diff --git a/subsurface-core/version.h b/subsurface-core/version.h new file mode 100644 index 000000000..bc0aac00d --- /dev/null +++ b/subsurface-core/version.h @@ -0,0 +1,16 @@ +#ifndef VERSION_H +#define VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +const char *subsurface_version(void); +const char *subsurface_git_version(void); +const char *subsurface_canonical_version(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/subsurface-core/webservice.h b/subsurface-core/webservice.h new file mode 100644 index 000000000..052b8aae7 --- /dev/null +++ b/subsurface-core/webservice.h @@ -0,0 +1,24 @@ +#ifndef WEBSERVICE_H +#define WEBSERVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +//extern void webservice_download_dialog(void); +//extern bool webservice_request_user_xml(const gchar *, gchar **, unsigned int *, unsigned int *); +extern int divelogde_upload(char *fn, char **error); +extern unsigned int download_dialog_parse_response(char *xmldata, unsigned int len); + +enum { + DD_STATUS_OK, + DD_STATUS_ERROR_CONNECT, + DD_STATUS_ERROR_ID, + DD_STATUS_ERROR_PARSE, +}; + + +#ifdef __cplusplus +} +#endif +#endif // WEBSERVICE_H diff --git a/subsurface-core/windows.c b/subsurface-core/windows.c new file mode 100644 index 000000000..a2386fd83 --- /dev/null +++ b/subsurface-core/windows.c @@ -0,0 +1,448 @@ +/* windows.c */ +/* implements Windows specific functions */ +#include +#include "dive.h" +#include "display.h" +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x500 +#include +#include +#include +#include +#include +#include +#include +#include + +const char non_standard_system_divelist_default_font[] = "Calibri"; +const char current_system_divelist_default_font[] = "Segoe UI"; +const char *system_divelist_default_font = non_standard_system_divelist_default_font; +double system_divelist_default_font_size = -1; + +void subsurface_user_info(struct user_info *user) +{ /* Encourage use of at least libgit2-0.20 */ } + +extern bool isWin7Or8(); + +void subsurface_OS_pref_setup(void) +{ + if (isWin7Or8()) + system_divelist_default_font = current_system_divelist_default_font; +} + +bool subsurface_ignore_font(const char *font) +{ + // if this is running on a recent enough version of Windows and the font + // passed in is the pre 4.3 default font, ignore it + if (isWin7Or8() && strcmp(font, non_standard_system_divelist_default_font) == 0) + return true; + return false; +} + +/* this function returns the Win32 Roaming path for the current user as UTF-8. + * it never returns NULL but fallsback to .\ instead! + * the append argument will append a wchar_t string to the end of the path. + */ +static const char *system_default_path_append(const wchar_t *append) +{ + wchar_t wpath[MAX_PATH] = { 0 }; + const char *fname = "system_default_path_append()"; + + /* obtain the user path via SHGetFolderPathW. + * this API is deprecated but still supported on modern Win32. + * fallback to .\ if it fails. + */ + if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, wpath))) { + fprintf(stderr, "%s: cannot obtain path!\n", fname); + wpath[0] = L'.'; + wpath[1] = L'\0'; + } + + wcscat(wpath, L"\\Subsurface"); + if (append) { + wcscat(wpath, L"\\"); + wcscat(wpath, append); + } + + /* attempt to convert the UTF-16 string to UTF-8. + * resize the buffer and fallback to .\Subsurface if it fails. + */ + const int wsz = wcslen(wpath); + const int sz = WideCharToMultiByte(CP_UTF8, 0, wpath, wsz, NULL, 0, NULL, NULL); + char *path = (char *)malloc(sz + 1); + if (!sz) + goto fallback; + if (WideCharToMultiByte(CP_UTF8, 0, wpath, wsz, path, sz, NULL, NULL)) { + path[sz] = '\0'; + return path; + } + +fallback: + fprintf(stderr, "%s: cannot obtain path as UTF-8!\n", fname); + const char *local = ".\\Subsurface"; + const int len = strlen(local) + 1; + path = (char *)realloc(path, len); + memset(path, 0, len); + strcat(path, local); + return path; +} + +/* by passing NULL to system_default_path_append() we obtain the pure path. + * '\' not included at the end. + */ +const char *system_default_directory(void) +{ + static const char *path = NULL; + if (!path) + path = system_default_path_append(NULL); + return path; +} + +/* obtain the Roaming path and append "\\.xml" to it. + */ +const char *system_default_filename(void) +{ + static wchar_t filename[UNLEN + 5] = { 0 }; + if (!*filename) { + wchar_t username[UNLEN + 1] = { 0 }; + DWORD username_len = UNLEN + 1; + GetUserNameW(username, &username_len); + wcscat(filename, username); + wcscat(filename, L".xml"); + } + static const char *path = NULL; + if (!path) + path = system_default_path_append(filename); + return path; +} + +int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) +{ + int index = -1; + DWORD i; + if (dc_type != DC_TYPE_UEMIS) { + // Open the registry key. + HKEY hKey; + LONG rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", 0, KEY_QUERY_VALUE, &hKey); + if (rc != ERROR_SUCCESS) { + return -1; + } + + // Get the number of values. + DWORD count = 0; + rc = RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &count, NULL, NULL, NULL, NULL); + if (rc != ERROR_SUCCESS) { + RegCloseKey(hKey); + return -1; + } + for (i = 0; i < count; ++i) { + // Get the value name, data and type. + char name[512], data[512]; + DWORD name_len = sizeof(name); + DWORD data_len = sizeof(data); + DWORD type = 0; + rc = RegEnumValue(hKey, i, name, &name_len, NULL, &type, (LPBYTE)data, &data_len); + if (rc != ERROR_SUCCESS) { + RegCloseKey(hKey); + return -1; + } + + // Ignore non-string values. + if (type != REG_SZ) + continue; + + // Prevent a possible buffer overflow. + if (data_len >= sizeof(data)) { + RegCloseKey(hKey); + return -1; + } + + // Null terminate the string. + data[data_len] = 0; + + callback(data, userdata); + index++; + if (is_default_dive_computer_device(name)) + index = i; + } + + RegCloseKey(hKey); + } + if (dc_type != DC_TYPE_SERIAL) { + int i; + int count_drives = 0; + const int bufdef = 512; + const char *dlabels[] = {"UEMISSDA", NULL}; + char bufname[bufdef], bufval[bufdef], *p; + DWORD bufname_len; + + /* add drive letters that match labels */ + memset(bufname, 0, bufdef); + bufname_len = bufdef; + if (GetLogicalDriveStringsA(bufname_len, bufname)) { + p = bufname; + + while (*p) { + memset(bufval, 0, bufdef); + if (GetVolumeInformationA(p, bufval, bufdef, NULL, NULL, NULL, NULL, 0)) { + for (i = 0; dlabels[i] != NULL; i++) + if (!strcmp(bufval, dlabels[i])) { + char data[512]; + snprintf(data, sizeof(data), "%s (%s)", p, dlabels[i]); + callback(data, userdata); + if (is_default_dive_computer_device(p)) + index = count_drives; + count_drives++; + } + } + p = &p[strlen(p) + 1]; + } + if (count_drives == 1) /* we found exactly one Uemis "drive" */ + index = 0; /* make it the selected "device" */ + } + } + return index; +} + +/* this function converts a utf-8 string to win32's utf-16 2 byte string. + * the caller function should manage the allocated memory. + */ +static wchar_t *utf8_to_utf16_fl(const char *utf8, char *file, int line) +{ + assert(utf8 != NULL); + assert(file != NULL); + assert(line); + /* estimate buffer size */ + const int sz = strlen(utf8) + 1; + wchar_t *utf16 = (wchar_t *)malloc(sizeof(wchar_t) * sz); + if (!utf16) { + fprintf(stderr, "%s:%d: %s %d.", file, line, "cannot allocate buffer of size", sz); + return NULL; + } + if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16, sz)) + return utf16; + fprintf(stderr, "%s:%d: %s", file, line, "cannot convert string."); + free((void *)utf16); + return NULL; +} + +#define utf8_to_utf16(s) utf8_to_utf16_fl(s, __FILE__, __LINE__) + +/* bellow we provide a set of wrappers for some I/O functions to use wchar_t. + * on win32 this solves the issue that we need paths to be utf-16 encoded. + */ +int subsurface_rename(const char *path, const char *newpath) +{ + int ret = -1; + if (!path || !newpath) + return ret; + + wchar_t *wpath = utf8_to_utf16(path); + wchar_t *wnewpath = utf8_to_utf16(newpath); + + if (wpath && wnewpath) + ret = _wrename(wpath, wnewpath); + free((void *)wpath); + free((void *)wnewpath); + return ret; +} + +// if the QDir based rename fails, we try this one +int subsurface_dir_rename(const char *path, const char *newpath) +{ + // check if the folder exists + BOOL exists = FALSE; + DWORD attrib = GetFileAttributes(path); + if (attrib != INVALID_FILE_ATTRIBUTES && attrib & FILE_ATTRIBUTE_DIRECTORY) + exists = TRUE; + if (!exists && verbose) { + fprintf(stderr, "folder not found or path is not a folder: %s\n", path); + return EXIT_FAILURE; + } + + // list of error codes: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx + DWORD errorCode; + + // if this fails something has already obatained (more) exclusive access to the folder + HANDLE h = CreateFile(path, GENERIC_WRITE, FILE_SHARE_WRITE | + FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + if (h == INVALID_HANDLE_VALUE) { + errorCode = GetLastError(); + if (verbose) + fprintf(stderr, "cannot obtain exclusive write access for folder: %u\n", (unsigned int)errorCode ); + return EXIT_FAILURE; + } else { + if (verbose) + fprintf(stderr, "exclusive write access obtained...closing handle!"); + CloseHandle(h); + + // attempt to rename + BOOL result = MoveFile(path, newpath); + if (!result) { + errorCode = GetLastError(); + if (verbose) + fprintf(stderr, "rename failed: %u\n", (unsigned int)errorCode); + return EXIT_FAILURE; + } + if (verbose > 1) + fprintf(stderr, "folder rename success: %s ---> %s\n", path, newpath); + } + return EXIT_SUCCESS; +} + +int subsurface_open(const char *path, int oflags, mode_t mode) +{ + int ret = -1; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) + ret = _wopen(wpath, oflags, mode); + free((void *)wpath); + return ret; +} + +FILE *subsurface_fopen(const char *path, const char *mode) +{ + FILE *ret = NULL; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) { + const int len = strlen(mode); + wchar_t wmode[len + 1]; + for (int i = 0; i < len; i++) + wmode[i] = (wchar_t)mode[i]; + wmode[len] = 0; + ret = _wfopen(wpath, wmode); + } + free((void *)wpath); + return ret; +} + +/* here we return a void pointer instead of _WDIR or DIR pointer */ +void *subsurface_opendir(const char *path) +{ + _WDIR *ret = NULL; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) + ret = _wopendir(wpath); + free((void *)wpath); + return (void *)ret; +} + +int subsurface_access(const char *path, int mode) +{ + int ret = -1; + if (!path) + return ret; + wchar_t *wpath = utf8_to_utf16(path); + if (wpath) + ret = _waccess(wpath, mode); + free((void *)wpath); + return ret; +} + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) +{ +#if defined(LIBZIP_VERSION_MAJOR) + /* libzip 0.10 has zip_fdopen, let's use it since zip_open doesn't have a + * wchar_t version */ + int fd = subsurface_open(path, O_RDONLY | O_BINARY, 0); + struct zip *ret = zip_fdopen(fd, flags, errorp); + if (!ret) + close(fd); + return ret; +#else + return zip_open(path, flags, errorp); +#endif +} + +int subsurface_zip_close(struct zip *zip) +{ + return zip_close(zip); +} + +/* win32 console */ +static struct { + bool allocated; + UINT cp; + FILE *out, *err; +} console_desc; + +void subsurface_console_init(bool dedicated) +{ + (void)console_desc; + /* if this is a console app already, do nothing */ +#ifndef WIN32_CONSOLE_APP + /* just in case of multiple calls */ + memset((void *)&console_desc, 0, sizeof(console_desc)); + /* the AttachConsole(..) call can be used to determine if the parent process + * is a terminal. if it succeeds, there is no need for a dedicated console + * window and we don't need to call the AllocConsole() function. on the other + * hand if the user has set the 'dedicated' flag to 'true' and if AttachConsole() + * has failed, we create a dedicated console window. + */ + console_desc.allocated = AttachConsole(ATTACH_PARENT_PROCESS); + if (console_desc.allocated) + dedicated = false; + if (!console_desc.allocated && dedicated) + console_desc.allocated = AllocConsole(); + if (!console_desc.allocated) + return; + + console_desc.cp = GetConsoleCP(); + SetConsoleOutputCP(CP_UTF8); /* make the ouput utf8 */ + + /* set some console modes; we don't need to reset these back. + * ENABLE_EXTENDED_FLAGS = 0x0080, ENABLE_QUICK_EDIT_MODE = 0x0040 */ + HANDLE h_in = GetStdHandle(STD_INPUT_HANDLE); + if (h_in) { + SetConsoleMode(h_in, 0x0080 | 0x0040); + CloseHandle(h_in); + } + + /* dedicated only; disable the 'x' button as it will close the main process as well */ + HWND h_cw = GetConsoleWindow(); + if (h_cw && dedicated) { + SetWindowTextA(h_cw, "Subsurface Console"); + HMENU h_menu = GetSystemMenu(h_cw, 0); + if (h_menu) { + EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED); + DrawMenuBar(h_cw); + } + SetConsoleCtrlHandler(NULL, TRUE); /* disable the CTRL handler */ + } + + /* redirect; on win32, CON is a reserved pipe target, like NUL */ + console_desc.out = freopen("CON", "w", stdout); + console_desc.err = freopen("CON", "w", stderr); + if (!dedicated) + puts(""); /* add an empty line */ +#endif +} + +void subsurface_console_exit(void) +{ +#ifndef WIN32_CONSOLE_APP + if (!console_desc.allocated) + return; + + /* close handles */ + if (console_desc.out) + fclose(console_desc.out); + if (console_desc.err) + fclose(console_desc.err); + + /* reset code page and free */ + SetConsoleOutputCP(console_desc.cp); + FreeConsole(); +#endif +} diff --git a/subsurface-core/windowtitleupdate.cpp b/subsurface-core/windowtitleupdate.cpp new file mode 100644 index 000000000..eec324c68 --- /dev/null +++ b/subsurface-core/windowtitleupdate.cpp @@ -0,0 +1,31 @@ +#include "windowtitleupdate.h" + +WindowTitleUpdate *WindowTitleUpdate::m_instance = NULL; + +WindowTitleUpdate::WindowTitleUpdate(QObject *parent) : QObject(parent) +{ + Q_ASSERT_X(m_instance == NULL, "WindowTitleUpdate", "WindowTitleUpdate recreated!"); + + m_instance = this; +} + +WindowTitleUpdate *WindowTitleUpdate::instance() +{ + return m_instance; +} + +WindowTitleUpdate::~WindowTitleUpdate() +{ + m_instance = NULL; +} + +void WindowTitleUpdate::emitSignal() +{ + emit updateTitle(); +} + +extern "C" void updateWindowTitle() +{ + WindowTitleUpdate *wt = WindowTitleUpdate::instance(); + wt->emitSignal(); +} diff --git a/subsurface-core/windowtitleupdate.h b/subsurface-core/windowtitleupdate.h new file mode 100644 index 000000000..8650e5868 --- /dev/null +++ b/subsurface-core/windowtitleupdate.h @@ -0,0 +1,20 @@ +#ifndef WINDOWTITLEUPDATE_H +#define WINDOWTITLEUPDATE_H + +#include + +class WindowTitleUpdate : public QObject +{ + Q_OBJECT +public: + explicit WindowTitleUpdate(QObject *parent = 0); + ~WindowTitleUpdate(); + static WindowTitleUpdate *instance(); + void emitSignal(); +signals: + void updateTitle(); +private: + static WindowTitleUpdate *m_instance; +}; + +#endif // WINDOWTITLEUPDATE_H diff --git a/subsurface-core/worldmap-options.h b/subsurface-core/worldmap-options.h new file mode 100644 index 000000000..177443563 --- /dev/null +++ b/subsurface-core/worldmap-options.h @@ -0,0 +1,7 @@ +#ifndef WORLDMAP_OPTIONS_H +#define WORLDMAP_OPTIONS_H + +const char *map_options = "center: new google.maps.LatLng(0,0),\n\tzoom: 3,\n\tminZoom: 2,\n\tmapTypeId: google.maps.MapTypeId.SATELLITE\n\t"; +const char *css = "\n\thtml { height: 100% }\n\tbody { height: 100%; margin: 0; padding: 0 }\n\t#map-canvas { height: 100% }\n"; + +#endif // WORLDMAP-OPTIONS_H diff --git a/subsurface-core/worldmap-save.c b/subsurface-core/worldmap-save.c new file mode 100644 index 000000000..25c5ee33f --- /dev/null +++ b/subsurface-core/worldmap-save.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +#include "dive.h" +#include "membuffer.h" +#include "save-html.h" +#include "worldmap-save.h" +#include "worldmap-options.h" +#include "gettext.h" + +char *getGoogleApi() +{ + /* google maps api auth*/ + return "https://maps.googleapis.com/maps/api/js?key=AIzaSyDzo9PWsqYDDSddVswg_13rpD9oH_dLuoQ"; +} + +void writeMarkers(struct membuffer *b, const bool selected_only) +{ + int i, dive_no = 0; + struct dive *dive; + char pre[1000], post[1000]; + + for_each_dive (i, dive) { + if (selected_only) { + if (!dive->selected) + continue; + } + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!ds || !dive_site_has_gps_location(ds)) + continue; + put_degrees(b, ds->latitude, "temp = new google.maps.Marker({position: new google.maps.LatLng(", ""); + put_degrees(b, ds->longitude, ",", ")});\n"); + put_string(b, "markers.push(temp);\ntempinfowindow = new google.maps.InfoWindow({content: '
'+'
'+'
'+'
"); + snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Date:")); + put_HTML_date(b, dive, pre, "

"); + snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Time:")); + put_HTML_time(b, dive, pre, "

"); + snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Duration:")); + snprintf(post, sizeof(post), " %s

", translate("gettextFromC", "min")); + put_duration(b, dive->duration, pre, post); + snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Max. depth:")); + snprintf(post, sizeof(post), " %s

", translate("gettextFromC", "m")); + put_depth(b, dive->maxdepth, pre, post); + put_string(b, "

"); + put_HTML_quoted(b, translate("gettextFromC", "Air temp.:")); + put_HTML_airtemp(b, dive, " ", "

"); + put_string(b, "

"); + put_HTML_quoted(b, translate("gettextFromC", "Water temp.:")); + put_HTML_watertemp(b, dive, " ", "

"); + snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Location:")); + put_string(b, pre); + put_HTML_quoted(b, get_dive_location(dive)); + put_string(b, "

"); + snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Notes:")); + put_HTML_notes(b, dive, pre, "

"); + put_string(b, "

'+'
'+'
'});\ninfowindows.push(tempinfowindow);\n"); + put_format(b, "google.maps.event.addListener(markers[%d], 'mouseover', function() {\ninfowindows[%d].open(map,markers[%d]);}", dive_no, dive_no, dive_no); + put_format(b, ");google.maps.event.addListener(markers[%d], 'mouseout', function() {\ninfowindows[%d].close();});\n", dive_no, dive_no); + dive_no++; + } +} + +void insert_html_header(struct membuffer *b) +{ + put_string(b, "\n\n\n"); + put_string(b, "\nWorld Map\n"); + put_string(b, ""); +} + +void insert_css(struct membuffer *b) +{ + put_format(b, "\n", css); +} + +void insert_javascript(struct membuffer *b, const bool selected_only) +{ + put_string(b, "\n\n"); +} + +void export(struct membuffer *b, const bool selected_only) +{ + insert_html_header(b); + insert_css(b); + insert_javascript(b, selected_only); + put_string(b, "\t\n\n
\n\n"); +} + +void export_worldmap_HTML(const char *file_name, const bool selected_only) +{ + FILE *f; + + struct membuffer buf = { 0 }; + export(&buf, selected_only); + + f = subsurface_fopen(file_name, "w+"); + if (!f) { + report_error(translate("gettextFromC", "Can't open file %s"), file_name); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} diff --git a/subsurface-core/worldmap-save.h b/subsurface-core/worldmap-save.h new file mode 100644 index 000000000..102ea40e5 --- /dev/null +++ b/subsurface-core/worldmap-save.h @@ -0,0 +1,15 @@ +#ifndef WORLDMAP_SAVE_H +#define WORLDMAP_SAVE_H + +#ifdef __cplusplus +extern "C" { +#endif + +extern void export_worldmap_HTML(const char *file_name, const bool selected_only); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/subsurfacestartup.c b/subsurfacestartup.c deleted file mode 100644 index 1286885ee..000000000 --- a/subsurfacestartup.c +++ /dev/null @@ -1,319 +0,0 @@ -#include "subsurfacestartup.h" -#include "version.h" -#include -#include -#include "gettext.h" -#include "qthelperfromc.h" -#include "git-access.h" - -struct preferences prefs, informational_prefs; -struct preferences default_prefs = { - .cloud_base_url = "https://cloud.subsurface-divelog.org/", - .units = SI_UNITS, - .unit_system = METRIC, - .coordinates_traditional = true, - .pp_graphs = { - .po2 = false, - .pn2 = false, - .phe = false, - .po2_threshold = 1.6, - .pn2_threshold = 4.0, - .phe_threshold = 13.0, - }, - .mod = false, - .modpO2 = 1.6, - .ead = false, - .hrgraph = false, - .percentagegraph = false, - .dcceiling = true, - .redceiling = false, - .calcceiling = false, - .calcceiling3m = false, - .calcndltts = false, - .gflow = 30, - .gfhigh = 75, - .animation_speed = 500, - .gf_low_at_maxdepth = false, - .show_ccr_setpoint = false, - .show_ccr_sensors = false, - .font_size = -1, - .display_invalid_dives = false, - .show_sac = false, - .display_unused_tanks = false, - .show_average_depth = true, - .ascrate75 = 9000 / 60, - .ascrate50 = 6000 / 60, - .ascratestops = 6000 / 60, - .ascratelast6m = 1000 / 60, - .descrate = 18000 / 60, - .bottompo2 = 1400, - .decopo2 = 1600, - .doo2breaks = false, - .drop_stone_mode = false, - .switch_at_req_stop = false, - .min_switch_duration = 60, - .last_stop = false, - .verbatim_plan = false, - .display_runtime = true, - .display_duration = true, - .display_transitions = true, - .safetystop = true, - .bottomsac = 20000, - .decosac = 17000, - .reserve_gas=40000, - .o2consumption = 720, - .pscr_ratio = 100, - .show_pictures_in_profile = true, - .tankbar = false, - .facebook = { - .user_id = NULL, - .album_id = NULL, - .access_token = NULL - }, - .defaultsetpoint = 1100, - .cloud_background_sync = true, - .geocoding = { - .enable_geocoding = true, - .parse_dive_without_gps = false, - .tag_existing_dives = false, - .category = { 0 } - }, - .deco_mode = BUEHLMANN, - .conservatism_level = 3 -}; - -int run_survey; - -struct units *get_units() -{ - return &prefs.units; -} - -/* random helper functions, used here or elsewhere */ -static int sortfn(const void *_a, const void *_b) -{ - const struct dive *a = (const struct dive *)*(void **)_a; - const struct dive *b = (const struct dive *)*(void **)_b; - - if (a->when < b->when) - return -1; - if (a->when > b->when) - return 1; - return 0; -} - -void sort_table(struct dive_table *table) -{ - qsort(table->dives, table->nr, sizeof(struct dive *), sortfn); -} - -const char *weekday(int wday) -{ - static const char wday_array[7][7] = { - /*++GETTEXT: these are three letter days - we allow up to six code bytes */ - QT_TRANSLATE_NOOP("gettextFromC", "Sun"), QT_TRANSLATE_NOOP("gettextFromC", "Mon"), QT_TRANSLATE_NOOP("gettextFromC", "Tue"), QT_TRANSLATE_NOOP("gettextFromC", "Wed"), QT_TRANSLATE_NOOP("gettextFromC", "Thu"), QT_TRANSLATE_NOOP("gettextFromC", "Fri"), QT_TRANSLATE_NOOP("gettextFromC", "Sat") - }; - return translate("gettextFromC", wday_array[wday]); -} - -const char *monthname(int mon) -{ - static const char month_array[12][7] = { - /*++GETTEXT: these are three letter months - we allow up to six code bytes*/ - QT_TRANSLATE_NOOP("gettextFromC", "Jan"), QT_TRANSLATE_NOOP("gettextFromC", "Feb"), QT_TRANSLATE_NOOP("gettextFromC", "Mar"), QT_TRANSLATE_NOOP("gettextFromC", "Apr"), QT_TRANSLATE_NOOP("gettextFromC", "May"), QT_TRANSLATE_NOOP("gettextFromC", "Jun"), - QT_TRANSLATE_NOOP("gettextFromC", "Jul"), QT_TRANSLATE_NOOP("gettextFromC", "Aug"), QT_TRANSLATE_NOOP("gettextFromC", "Sep"), QT_TRANSLATE_NOOP("gettextFromC", "Oct"), QT_TRANSLATE_NOOP("gettextFromC", "Nov"), QT_TRANSLATE_NOOP("gettextFromC", "Dec"), - }; - return translate("gettextFromC", month_array[mon]); -} - -/* - * track whether we switched to importing dives - */ -bool imported = false; - -static void print_version() -{ - printf("Subsurface v%s, ", subsurface_git_version()); - printf("built with libdivecomputer v%s\n", dc_version(NULL)); -} - -void print_files() -{ - const char *branch = 0; - const char *remote = 0; - const char *filename, *local_git; - - filename = cloud_url(); - - is_git_repository(filename, &branch, &remote, true); - printf("\nFile locations:\n\n"); - if (branch && remote) { - local_git = get_local_dir(remote, branch); - printf("Local git storage: %s\n", local_git); - } else { - printf("Unable to get local git directory\n"); - } - char *tmp = cloud_url(); - printf("Cloud URL: %s\n", tmp); - free(tmp); - tmp = hashfile_name_string(); - printf("Image hashes: %s\n", tmp); - free(tmp); - tmp = picturedir_string(); - printf("Local picture directory: %s\n\n", tmp); - free(tmp); -} - -static void print_help() -{ - print_version(); - printf("\nUsage: subsurface [options] [logfile ...] [--import logfile ...]"); - printf("\n\noptions include:"); - printf("\n --help|-h This help text"); - printf("\n --import logfile ... Logs before this option is treated as base, everything after is imported"); - printf("\n --verbose|-v Verbose debug (repeat to increase verbosity)"); - printf("\n --version Prints current version"); - printf("\n --survey Offer to submit a user survey"); - printf("\n --win32console Create a dedicated console if needed (Windows only). Add option before everything else\n\n"); -} - -void parse_argument(const char *arg) -{ - const char *p = arg + 1; - - do { - switch (*p) { - case 'h': - print_help(); - exit(0); - case 'v': - verbose++; - continue; - case 'q': - quit++; - continue; - case '-': - /* long options with -- */ - if (strcmp(arg, "--help") == 0) { - print_help(); - exit(0); - } - if (strcmp(arg, "--import") == 0) { - imported = true; /* mark the dives so far as the base, * everything after is imported */ - return; - } - if (strcmp(arg, "--verbose") == 0) { - verbose++; - return; - } - if (strcmp(arg, "--version") == 0) { - print_version(); - exit(0); - } - if (strcmp(arg, "--survey") == 0) { - run_survey = true; - return; - } - if (strcmp(arg, "--win32console") == 0) - return; - /* fallthrough */ - case 'p': - /* ignore process serial number argument when run as native macosx app */ - if (strncmp(arg, "-psQT_TR_NOOP(", 5) == 0) { - return; - } - /* fallthrough */ - default: - fprintf(stderr, "Bad argument '%s'\n", arg); - exit(1); - } - } while (*++p); -} - -void renumber_dives(int start_nr, bool selected_only) -{ - int i, nr = start_nr; - struct dive *dive; - - for_each_dive (i, dive) { - if (dive->selected) - dive->number = nr++; - } - mark_divelist_changed(true); -} - -/* - * Under a POSIX setup, the locale string should have a format - * like [language[_territory][.codeset][@modifier]]. - * - * So search for the underscore, and see if the "territory" is - * US, and turn on imperial units by default. - * - * I guess Burma and Liberia should trigger this too. I'm too - * lazy to look up the territory names, though. - */ -void setup_system_prefs(void) -{ - const char *env; - - subsurface_OS_pref_setup(); - default_prefs.divelist_font = strdup(system_divelist_default_font); - default_prefs.font_size = system_divelist_default_font_size; - default_prefs.default_filename = system_default_filename(); - - env = getenv("LC_MEASUREMENT"); - if (!env) - env = getenv("LC_ALL"); - if (!env) - env = getenv("LANG"); - if (!env) - return; - env = strchr(env, '_'); - if (!env) - return; - env++; - if (strncmp(env, "US", 2)) - return; - - default_prefs.units = IMPERIAL_units; -} - -/* copy a preferences block, including making copies of all included strings */ -void copy_prefs(struct preferences *src, struct preferences *dest) -{ - *dest = *src; - dest->divelist_font = copy_string(src->divelist_font); - dest->default_filename = copy_string(src->default_filename); - dest->default_cylinder = copy_string(src->default_cylinder); - dest->cloud_base_url = copy_string(src->cloud_base_url); - dest->cloud_git_url = copy_string(src->cloud_git_url); - dest->userid = copy_string(src->userid); - dest->proxy_host = copy_string(src->proxy_host); - dest->proxy_user = copy_string(src->proxy_user); - dest->proxy_pass = copy_string(src->proxy_pass); - dest->cloud_storage_password = copy_string(src->cloud_storage_password); - dest->cloud_storage_newpassword = copy_string(src->cloud_storage_newpassword); - dest->cloud_storage_email = copy_string(src->cloud_storage_email); - dest->cloud_storage_email_encoded = copy_string(src->cloud_storage_email_encoded); - dest->facebook.access_token = copy_string(src->facebook.access_token); - dest->facebook.user_id = copy_string(src->facebook.user_id); - dest->facebook.album_id = copy_string(src->facebook.album_id); -} - -/* - * Free strduped prefs before exit. - * - * These are not real leaks but they plug the holes found by eg. - * valgrind so you can find the real leaks. - */ -void free_prefs(void) -{ - free((void*)prefs.default_filename); - free((void*)prefs.default_cylinder); - free((void*)prefs.divelist_font); - free((void*)prefs.cloud_storage_password); - free(prefs.proxy_host); - free(prefs.proxy_user); - free(prefs.proxy_pass); - free(prefs.userid); -} diff --git a/subsurfacestartup.h b/subsurfacestartup.h deleted file mode 100644 index 3ccc24aa4..000000000 --- a/subsurfacestartup.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef SUBSURFACESTARTUP_H -#define SUBSURFACESTARTUP_H - -#include "dive.h" -#include "divelist.h" -#include "libdivecomputer.h" - -#ifdef __cplusplus -extern "C" { -#else -#include -#endif - -extern bool imported; - -void setup_system_prefs(void); -void parse_argument(const char *arg); -void free_prefs(void); -void copy_prefs(struct preferences *src, struct preferences *dest); -void print_files(void); - -#ifdef __cplusplus -} -#endif - -#endif // SUBSURFACESTARTUP_H diff --git a/subsurfacesysinfo.cpp b/subsurfacesysinfo.cpp deleted file mode 100644 index a7173b169..000000000 --- a/subsurfacesysinfo.cpp +++ /dev/null @@ -1,620 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Copyright (C) 2014 Intel Corporation -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "subsurfacesysinfo.h" -#include - -#ifdef Q_OS_UNIX -#include -#endif - -#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) - -#ifndef QStringLiteral -# define QStringLiteral QString::fromUtf8 -#endif - -#ifdef Q_OS_UNIX -#include -#endif - -#ifdef __APPLE__ -#include -#endif - -// --- this is a copy of Qt 5.4's src/corelib/global/archdetect.cpp --- - -// main part: processor type -#if defined(Q_PROCESSOR_ALPHA) -# define ARCH_PROCESSOR "alpha" -#elif defined(Q_PROCESSOR_ARM) -# define ARCH_PROCESSOR "arm" -#elif defined(Q_PROCESSOR_AVR32) -# define ARCH_PROCESSOR "avr32" -#elif defined(Q_PROCESSOR_BLACKFIN) -# define ARCH_PROCESSOR "bfin" -#elif defined(Q_PROCESSOR_X86_32) -# define ARCH_PROCESSOR "i386" -#elif defined(Q_PROCESSOR_X86_64) -# define ARCH_PROCESSOR "x86_64" -#elif defined(Q_PROCESSOR_IA64) -# define ARCH_PROCESSOR "ia64" -#elif defined(Q_PROCESSOR_MIPS) -# define ARCH_PROCESSOR "mips" -#elif defined(Q_PROCESSOR_POWER) -# define ARCH_PROCESSOR "power" -#elif defined(Q_PROCESSOR_S390) -# define ARCH_PROCESSOR "s390" -#elif defined(Q_PROCESSOR_SH) -# define ARCH_PROCESSOR "sh" -#elif defined(Q_PROCESSOR_SPARC) -# define ARCH_PROCESSOR "sparc" -#else -# define ARCH_PROCESSOR "unknown" -#endif - -// endinanness -#if defined(Q_LITTLE_ENDIAN) -# define ARCH_ENDIANNESS "little_endian" -#elif defined(Q_BIG_ENDIAN) -# define ARCH_ENDIANNESS "big_endian" -#endif - -// pointer type -#if defined(Q_OS_WIN64) || (defined(Q_OS_WINRT) && defined(_M_X64)) -# define ARCH_POINTER "llp64" -#elif defined(__LP64__) || QT_POINTER_SIZE - 0 == 8 -# define ARCH_POINTER "lp64" -#else -# define ARCH_POINTER "ilp32" -#endif - -// secondary: ABI string (includes the dash) -#if defined(__ARM_EABI__) || defined(__mips_eabi) -# define ARCH_ABI1 "-eabi" -#elif defined(_MIPS_SIM) -# if _MIPS_SIM == _ABIO32 -# define ARCH_ABI1 "-o32" -# elif _MIPS_SIM == _ABIN32 -# define ARCH_ABI1 "-n32" -# elif _MIPS_SIM == _ABI64 -# define ARCH_ABI1 "-n64" -# elif _MIPS_SIM == _ABIO64 -# define ARCH_ABI1 "-o64" -# endif -#else -# define ARCH_ABI1 "" -#endif -#if defined(__ARM_PCS_VFP) || defined(__mips_hard_float) -# define ARCH_ABI2 "-hardfloat" -#else -# define ARCH_ABI2 "" -#endif - -#define ARCH_ABI ARCH_ABI1 ARCH_ABI2 - -#define ARCH_FULL ARCH_PROCESSOR "-" ARCH_ENDIANNESS "-" ARCH_POINTER ARCH_ABI - -// --- end of archdetect.cpp --- -// copied from Qt 5.4.1's src/corelib/global/qglobal.cpp - -#if defined(Q_OS_WIN) || defined(Q_OS_CYGWIN) || defined(Q_OS_WINCE) || defined(Q_OS_WINRT) - -QT_BEGIN_INCLUDE_NAMESPACE -#include "qt_windows.h" -QT_END_INCLUDE_NAMESPACE - -#ifndef Q_OS_WINRT -# ifndef Q_OS_WINCE -// Fallback for determining Windows versions >= 8 by looping using the -// version check macros. Note that it will return build number=0 to avoid -// inefficient looping. -static inline void determineWinOsVersionFallbackPost8(OSVERSIONINFO *result) -{ - result->dwBuildNumber = 0; - DWORDLONG conditionMask = 0; - VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL); - VER_SET_CONDITION(conditionMask, VER_PLATFORMID, VER_EQUAL); - OSVERSIONINFOEX checkVersion = { sizeof(OSVERSIONINFOEX), result->dwMajorVersion, 0, - result->dwBuildNumber, result->dwPlatformId, {'\0'}, 0, 0, 0, 0, 0 }; - for ( ; VerifyVersionInfo(&checkVersion, VER_MAJORVERSION | VER_PLATFORMID, conditionMask); ++checkVersion.dwMajorVersion) - result->dwMajorVersion = checkVersion.dwMajorVersion; - conditionMask = 0; - checkVersion.dwMajorVersion = result->dwMajorVersion; - checkVersion.dwMinorVersion = 0; - VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_EQUAL); - VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL); - VER_SET_CONDITION(conditionMask, VER_PLATFORMID, VER_EQUAL); - for ( ; VerifyVersionInfo(&checkVersion, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID, conditionMask); ++checkVersion.dwMinorVersion) - result->dwMinorVersion = checkVersion.dwMinorVersion; -} - -# endif // !Q_OS_WINCE - -static inline OSVERSIONINFO winOsVersion() -{ - OSVERSIONINFO result = { sizeof(OSVERSIONINFO), 0, 0, 0, 0, {'\0'}}; - // GetVersionEx() has been deprecated in Windows 8.1 and will return - // only Windows 8 from that version on. -# if defined(_MSC_VER) && _MSC_VER >= 1800 -# pragma warning( push ) -# pragma warning( disable : 4996 ) -# endif - GetVersionEx(&result); -# if defined(_MSC_VER) && _MSC_VER >= 1800 -# pragma warning( pop ) -# endif -# ifndef Q_OS_WINCE - if (result.dwMajorVersion == 6 && result.dwMinorVersion == 2) { - determineWinOsVersionFallbackPost8(&result); - } -# endif // !Q_OS_WINCE - return result; -} -#endif // !Q_OS_WINRT - -static const char *winVer_helper() -{ - switch (int(SubsurfaceSysInfo::WindowsVersion)) { - case SubsurfaceSysInfo::WV_NT: - return "NT"; - case SubsurfaceSysInfo::WV_2000: - return "2000"; - case SubsurfaceSysInfo::WV_XP: - return "XP"; - case SubsurfaceSysInfo::WV_2003: - return "2003"; - case SubsurfaceSysInfo::WV_VISTA: - return "Vista"; - case SubsurfaceSysInfo::WV_WINDOWS7: - return "7"; - case SubsurfaceSysInfo::WV_WINDOWS8: - return "8"; - case SubsurfaceSysInfo::WV_WINDOWS8_1: - return "8.1"; - - case SubsurfaceSysInfo::WV_CE: - return "CE"; - case SubsurfaceSysInfo::WV_CENET: - return "CENET"; - case SubsurfaceSysInfo::WV_CE_5: - return "CE5"; - case SubsurfaceSysInfo::WV_CE_6: - return "CE6"; - } - // unknown, future version - return 0; -} -#endif - -#if defined(Q_OS_UNIX) -# if (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) || defined(Q_OS_FREEBSD) -# define USE_ETC_OS_RELEASE -struct QUnixOSVersion -{ - // from /etc/os-release - QString productType; // $ID - QString productVersion; // $VERSION_ID - QString prettyName; // $PRETTY_NAME -}; - -static QString unquote(const char *begin, const char *end) -{ - if (*begin == '"') { - Q_ASSERT(end[-1] == '"'); - return QString::fromLatin1(begin + 1, end - begin - 2); - } - return QString::fromLatin1(begin, end - begin); -} - -static bool readEtcOsRelease(QUnixOSVersion &v) -{ - // we're avoiding QFile here - int fd = QT_OPEN("/etc/os-release", O_RDONLY); - if (fd == -1) - return false; - - QT_STATBUF sbuf; - if (QT_FSTAT(fd, &sbuf) == -1) { - QT_CLOSE(fd); - return false; - } - - QByteArray buffer(sbuf.st_size, Qt::Uninitialized); - buffer.resize(QT_READ(fd, buffer.data(), sbuf.st_size)); - QT_CLOSE(fd); - - const char *ptr = buffer.constData(); - const char *end = buffer.constEnd(); - const char *eol; - for ( ; ptr != end; ptr = eol + 1) { - static const char idString[] = "ID="; - static const char prettyNameString[] = "PRETTY_NAME="; - static const char versionIdString[] = "VERSION_ID="; - - // find the end of the line after ptr - eol = static_cast(memchr(ptr, '\n', end - ptr)); - if (!eol) - eol = end - 1; - - // note: we're doing a binary search here, so comparison - // must always be sorted - int cmp = strncmp(ptr, idString, strlen(idString)); - if (cmp < 0) - continue; - if (cmp == 0) { - ptr += strlen(idString); - v.productType = unquote(ptr, eol); - continue; - } - - cmp = strncmp(ptr, prettyNameString, strlen(prettyNameString)); - if (cmp < 0) - continue; - if (cmp == 0) { - ptr += strlen(prettyNameString); - v.prettyName = unquote(ptr, eol); - continue; - } - - cmp = strncmp(ptr, versionIdString, strlen(versionIdString)); - if (cmp < 0) - continue; - if (cmp == 0) { - ptr += strlen(versionIdString); - v.productVersion = unquote(ptr, eol); - continue; - } - } - - return true; -} -# endif // USE_ETC_OS_RELEASE -#endif // Q_OS_UNIX - -static QString unknownText() -{ - return QStringLiteral("unknown"); -} - -QString SubsurfaceSysInfo::buildCpuArchitecture() -{ - return QStringLiteral(ARCH_PROCESSOR); -} - -QString SubsurfaceSysInfo::currentCpuArchitecture() -{ -#if defined(Q_OS_WIN) && !defined(Q_OS_WINCE) - // We don't need to catch all the CPU architectures in this function; - // only those where the host CPU might be different than the build target - // (usually, 64-bit platforms). - SYSTEM_INFO info; - GetNativeSystemInfo(&info); - switch (info.wProcessorArchitecture) { -# ifdef PROCESSOR_ARCHITECTURE_AMD64 - case PROCESSOR_ARCHITECTURE_AMD64: - return QStringLiteral("x86_64"); -# endif -# ifdef PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 - case PROCESSOR_ARCHITECTURE_IA32_ON_WIN64: -# endif - case PROCESSOR_ARCHITECTURE_IA64: - return QStringLiteral("ia64"); - } -#elif defined(Q_OS_UNIX) - long ret = -1; - struct utsname u; - -# if defined(Q_OS_SOLARIS) - // We need a special call for Solaris because uname(2) on x86 returns "i86pc" for - // both 32- and 64-bit CPUs. Reference: - // http://docs.oracle.com/cd/E18752_01/html/816-5167/sysinfo-2.html#REFMAN2sysinfo-2 - // http://fxr.watson.org/fxr/source/common/syscall/systeminfo.c?v=OPENSOLARIS - // http://fxr.watson.org/fxr/source/common/conf/param.c?v=OPENSOLARIS;im=10#L530 - if (ret == -1) - ret = sysinfo(SI_ARCHITECTURE_64, u.machine, sizeof u.machine); -# endif - - if (ret == -1) - ret = uname(&u); - - // we could use detectUnixVersion() above, but we only need a field no other function does - if (ret != -1) { - // the use of QT_BUILD_INTERNAL here is simply to ensure all branches build - // as we don't often build on some of the less common platforms -# if defined(Q_PROCESSOR_ARM) || defined(QT_BUILD_INTERNAL) - if (strcmp(u.machine, "aarch64") == 0) - return QStringLiteral("arm64"); - if (strncmp(u.machine, "armv", 4) == 0) - return QStringLiteral("arm"); -# endif -# if defined(Q_PROCESSOR_POWER) || defined(QT_BUILD_INTERNAL) - // harmonize "powerpc" and "ppc" to "power" - if (strncmp(u.machine, "ppc", 3) == 0) - return QLatin1String("power") + QLatin1String(u.machine + 3); - if (strncmp(u.machine, "powerpc", 7) == 0) - return QLatin1String("power") + QLatin1String(u.machine + 7); - if (strcmp(u.machine, "Power Macintosh") == 0) - return QLatin1String("power"); -# endif -# if defined(Q_PROCESSOR_SPARC) || defined(QT_BUILD_INTERNAL) - // Solaris sysinfo(2) (above) uses "sparcv9", but uname -m says "sun4u"; - // Linux says "sparc64" - if (strcmp(u.machine, "sun4u") == 0 || strcmp(u.machine, "sparc64") == 0) - return QStringLiteral("sparcv9"); - if (strcmp(u.machine, "sparc32") == 0) - return QStringLiteral("sparc"); -# endif -# if defined(Q_PROCESSOR_X86) || defined(QT_BUILD_INTERNAL) - // harmonize all "i?86" to "i386" - if (strlen(u.machine) == 4 && u.machine[0] == 'i' - && u.machine[2] == '8' && u.machine[3] == '6') - return QStringLiteral("i386"); - if (strcmp(u.machine, "amd64") == 0) // Solaris - return QStringLiteral("x86_64"); -# endif - return QString::fromLatin1(u.machine); - } -#endif - return buildCpuArchitecture(); -} - - -QString SubsurfaceSysInfo::buildAbi() -{ - return QLatin1String(ARCH_FULL); -} - -QString SubsurfaceSysInfo::kernelType() -{ -#if defined(Q_OS_WINCE) - return QStringLiteral("wince"); -#elif defined(Q_OS_WIN) - return QStringLiteral("winnt"); -#elif defined(Q_OS_UNIX) - struct utsname u; - if (uname(&u) == 0) - return QString::fromLatin1(u.sysname).toLower(); -#endif - return unknownText(); -} - -QString SubsurfaceSysInfo::kernelVersion() -{ -#ifdef Q_OS_WINRT - // TBD - return QString(); -#elif defined(Q_OS_WIN) - const OSVERSIONINFO osver = winOsVersion(); - return QString::number(int(osver.dwMajorVersion)) + QLatin1Char('.') + QString::number(int(osver.dwMinorVersion)) - + QLatin1Char('.') + QString::number(int(osver.dwBuildNumber)); -#else - struct utsname u; - if (uname(&u) == 0) - return QString::fromLatin1(u.release); - return QString(); -#endif -} - -QString SubsurfaceSysInfo::productType() -{ - // similar, but not identical to QFileSelectorPrivate::platformSelectors -#if defined(Q_OS_WINPHONE) - return QStringLiteral("winphone"); -#elif defined(Q_OS_WINRT) - return QStringLiteral("winrt"); -#elif defined(Q_OS_WINCE) - return QStringLiteral("wince"); -#elif defined(Q_OS_WIN) - return QStringLiteral("windows"); - -#elif defined(Q_OS_BLACKBERRY) - return QStringLiteral("blackberry"); -#elif defined(Q_OS_QNX) - return QStringLiteral("qnx"); - -#elif defined(Q_OS_ANDROID) - return QStringLiteral("android"); - -#elif defined(Q_OS_IOS) - return QStringLiteral("ios"); -#elif defined(Q_OS_OSX) - return QStringLiteral("osx"); -#elif defined(Q_OS_DARWIN) - return QStringLiteral("darwin"); - -#elif defined(USE_ETC_OS_RELEASE) // Q_OS_UNIX - QUnixOSVersion unixOsVersion; - readEtcOsRelease(unixOsVersion); - if (!unixOsVersion.productType.isEmpty()) - return unixOsVersion.productType; -#endif - return unknownText(); -} - -QString SubsurfaceSysInfo::productVersion() -{ -#if defined(Q_OS_IOS) - int major = (int(MacintoshVersion) >> 4) & 0xf; - int minor = int(MacintoshVersion) & 0xf; - if (Q_LIKELY(major < 10 && minor < 10)) { - char buf[4] = { char(major + '0'), '.', char(minor + '0'), '\0' }; - return QString::fromLatin1(buf, 3); - } - return QString::number(major) + QLatin1Char('.') + QString::number(minor); -#elif defined(Q_OS_OSX) - int minor = int(MacintoshVersion) - 2; // we're not running on Mac OS 9 - Q_ASSERT(minor < 100); - char buf[] = "10.0\0"; - if (Q_LIKELY(minor < 10)) { - buf[3] += minor; - } else { - buf[3] += minor / 10; - buf[4] = '0' + minor % 10; - } - return QString::fromLatin1(buf); -#elif defined(Q_OS_WIN) - const char *version = winVer_helper(); - if (version) - return QString::fromLatin1(version).toLower(); - // fall through - - // Android and Blackberry should not fall through to the Unix code -#elif defined(Q_OS_ANDROID) - // TBD -#elif defined(Q_OS_BLACKBERRY) - deviceinfo_details_t *deviceInfo; - if (deviceinfo_get_details(&deviceInfo) == BPS_SUCCESS) { - QString bbVersion = QString::fromLatin1(deviceinfo_details_get_device_os_version(deviceInfo)); - deviceinfo_free_details(&deviceInfo); - return bbVersion; - } -#elif defined(USE_ETC_OS_RELEASE) // Q_OS_UNIX - QUnixOSVersion unixOsVersion; - readEtcOsRelease(unixOsVersion); - if (!unixOsVersion.productVersion.isEmpty()) - return unixOsVersion.productVersion; -#endif - - // fallback - return unknownText(); -} - -QString SubsurfaceSysInfo::prettyProductName() -{ -#if defined(Q_OS_IOS) - return QLatin1String("iOS ") + productVersion(); -#elif defined(Q_OS_OSX) - // get the known codenames - const char *basename = 0; - switch (int(MacintoshVersion)) { - case MV_CHEETAH: - case MV_PUMA: - case MV_JAGUAR: - case MV_PANTHER: - case MV_TIGER: - // This version of Qt does not run on those versions of OS X - // so this case label will never be reached - Q_UNREACHABLE(); - break; - case MV_LEOPARD: - basename = "Mac OS X Leopard ("; - break; - case MV_SNOWLEOPARD: - basename = "Mac OS X Snow Leopard ("; - break; - case MV_LION: - basename = "Mac OS X Lion ("; - break; - case MV_MOUNTAINLION: - basename = "OS X Mountain Lion ("; - break; - case MV_MAVERICKS: - basename = "OS X Mavericks ("; - break; -#ifdef MV_YOSEMITE - case MV_YOSEMITE: -#else - case 0x000C: // MV_YOSEMITE -#endif - basename = "OS X Yosemite ("; - break; -#ifdef MV_ELCAPITAN - case MV_ELCAPITAN : -#else - case 0x000D: // MV_ELCAPITAN -#endif - basename = "OS X El Capitan ("; - break; - } - if (basename) - return QLatin1String(basename) + productVersion() + QLatin1Char(')'); - - // a future version of OS X - return QLatin1String("OS X ") + productVersion(); -#elif defined(Q_OS_WINPHONE) - return QLatin1String("Windows Phone ") + QLatin1String(winVer_helper()); -#elif defined(Q_OS_WIN) - return QLatin1String("Windows ") + QLatin1String(winVer_helper()); -#elif defined(Q_OS_ANDROID) - return QLatin1String("Android ") + productVersion(); -#elif defined(Q_OS_BLACKBERRY) - return QLatin1String("BlackBerry ") + productVersion(); -#elif defined(Q_OS_UNIX) -# ifdef USE_ETC_OS_RELEASE - QUnixOSVersion unixOsVersion; - readEtcOsRelease(unixOsVersion); - if (!unixOsVersion.prettyName.isEmpty()) - return unixOsVersion.prettyName; -# endif - struct utsname u; - if (uname(&u) == 0) - return QString::fromLatin1(u.sysname) + QLatin1Char(' ') + QString::fromLatin1(u.release); -#endif - return unknownText(); -} - -#endif // Qt >= 5.4 - -QString SubsurfaceSysInfo::prettyOsName() -{ - // Matches the pre-release version of Qt 5.4 - QString pretty = prettyProductName(); -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID) - // QSysInfo::kernelType() returns lowercase ("linux" instead of "Linux") - struct utsname u; - if (uname(&u) == 0) - return QString::fromLatin1(u.sysname) + QLatin1String(" (") + pretty + QLatin1Char(')'); -#endif - return pretty; -} - -extern "C" { -bool isWin7Or8() -{ -#ifdef Q_OS_WIN - return (QSysInfo::WindowsVersion & QSysInfo::WV_NT_based) >= QSysInfo::WV_WINDOWS7; -#else - return false; -#endif -} -} diff --git a/subsurfacesysinfo.h b/subsurfacesysinfo.h deleted file mode 100644 index b2c267b83..000000000 --- a/subsurfacesysinfo.h +++ /dev/null @@ -1,65 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Copyright (C) 2014 Intel Corporation -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef SUBSURFACESYSINFO_H -#define SUBSURFACESYSINFO_H - -#include - -class SubsurfaceSysInfo : public QSysInfo { -public: -#if QT_VERSION < 0x050400 - static QString buildCpuArchitecture(); - static QString currentCpuArchitecture(); - static QString buildAbi(); - - static QString kernelType(); - static QString kernelVersion(); - static QString productType(); - static QString productVersion(); - static QString prettyProductName(); -#endif - static QString prettyOsName(); -}; - - -#endif // SUBSURFACESYSINFO_H diff --git a/taxonomy.c b/taxonomy.c deleted file mode 100644 index 670d85ad0..000000000 --- a/taxonomy.c +++ /dev/null @@ -1,48 +0,0 @@ -#include "taxonomy.h" -#include "gettext.h" -#include - -char *taxonomy_category_names[TC_NR_CATEGORIES] = { - QT_TRANSLATE_NOOP("getTextFromC", "None"), - QT_TRANSLATE_NOOP("getTextFromC", "Ocean"), - QT_TRANSLATE_NOOP("getTextFromC", "Country"), - QT_TRANSLATE_NOOP("getTextFromC", "State"), - QT_TRANSLATE_NOOP("getTextFromC", "County"), - QT_TRANSLATE_NOOP("getTextFromC", "Town"), - QT_TRANSLATE_NOOP("getTextFromC", "City") -}; - -// these are the names for geoname.org -char *taxonomy_api_names[TC_NR_CATEGORIES] = { - "none", - "name", - "countryName", - "adminName1", - "adminName2", - "toponymName", - "adminName3" -}; - -struct taxonomy *alloc_taxonomy() -{ - return calloc(TC_NR_CATEGORIES, sizeof(struct taxonomy)); -} - -void free_taxonomy(struct taxonomy_data *t) -{ - if (t) { - for (int i = 0; i < t->nr; i++) - free((void *)t->category[i].value); - free(t->category); - t->category = NULL; - t->nr = 0; - } -} - -int taxonomy_index_for_category(struct taxonomy_data *t, enum taxonomy_category cat) -{ - for (int i = 0; i < t->nr; i++) - if (t->category[i].category == cat) - return i; - return -1; -} diff --git a/taxonomy.h b/taxonomy.h deleted file mode 100644 index 51245d562..000000000 --- a/taxonomy.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef TAXONOMY_H -#define TAXONOMY_H - -#ifdef __cplusplus -extern "C" { -#endif - -enum taxonomy_category { - TC_NONE, - TC_OCEAN, - TC_COUNTRY, - TC_ADMIN_L1, - TC_ADMIN_L2, - TC_LOCALNAME, - TC_ADMIN_L3, - TC_NR_CATEGORIES -}; - -extern char *taxonomy_category_names[TC_NR_CATEGORIES]; -extern char *taxonomy_api_names[TC_NR_CATEGORIES]; - -struct taxonomy { - int category; /* the category for this tag: ocean, country, admin_l1, admin_l2, localname, etc */ - const char *value; /* the value returned, parsed, or manually entered for that category */ - enum { GEOCODED, PARSED, MANUAL, COPIED } origin; -}; - -/* the data block contains 3 taxonomy structures - unused ones have a tag value of NONE */ -struct taxonomy_data { - int nr; - struct taxonomy *category; -}; - -struct taxonomy *alloc_taxonomy(); -void free_taxonomy(struct taxonomy_data *t); -int taxonomy_index_for_category(struct taxonomy_data *t, enum taxonomy_category cat); - -#ifdef __cplusplus -} -#endif -#endif // TAXONOMY_H diff --git a/time.c b/time.c deleted file mode 100644 index b658954bc..000000000 --- a/time.c +++ /dev/null @@ -1,98 +0,0 @@ -#include -#include "dive.h" - -/* - * Convert 64-bit timestamp to 'struct tm' in UTC. - * - * On 32-bit machines, only do 64-bit arithmetic for the seconds - * part, after that we do everything in 'long'. 64-bit divides - * are unnecessary once you're counting minutes (32-bit minutes: - * 8000+ years). - */ -void utc_mkdate(timestamp_t timestamp, struct tm *tm) -{ - static const int mdays[] = { - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - }; - static const int mdays_leap[] = { - 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - }; - unsigned long val; - unsigned int leapyears; - int m; - const int *mp; - - memset(tm, 0, sizeof(*tm)); - - /* seconds since 1970 -> minutes since 1970 */ - tm->tm_sec = timestamp % 60; - val = timestamp /= 60; - - /* Do the simple stuff */ - tm->tm_min = val % 60; - val /= 60; - tm->tm_hour = val % 24; - val /= 24; - - /* Jan 1, 1970 was a Thursday (tm_wday=4) */ - tm->tm_wday = (val + 4) % 7; - - /* - * Now we're in "days since Jan 1, 1970". To make things easier, - * let's make it "days since Jan 1, 1968", since that's a leap-year - */ - val += 365 + 366; - - /* This only works up until 2099 (2100 isn't a leap-year) */ - leapyears = val / (365 * 4 + 1); - val %= (365 * 4 + 1); - tm->tm_year = 68 + leapyears * 4; - - /* Handle the leap-year itself */ - mp = mdays_leap; - if (val > 365) { - tm->tm_year++; - val -= 366; - tm->tm_year += val / 365; - val %= 365; - mp = mdays; - } - - for (m = 0; m < 12; m++) { - if (val < *mp) - break; - val -= *mp++; - } - tm->tm_mday = val + 1; - tm->tm_mon = m; -} - -timestamp_t utc_mktime(struct tm *tm) -{ - static const int mdays[] = { - 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 - }; - int year = tm->tm_year; - int month = tm->tm_mon; - int day = tm->tm_mday; - - /* First normalize relative to 1900 */ - if (year < 70) - year += 100; - else if (year > 1900) - year -= 1900; - - /* Normalized to Jan 1, 1970: unix time */ - year -= 70; - - if (year < 0 || year > 129) /* algo only works for 1970-2099 */ - return -1; - if (month < 0 || month > 11) /* array bounds */ - return -1; - if (month < 2 || (year + 2) % 4) - day--; - if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0) - return -1; - return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24 * 60 * 60UL + - tm->tm_hour * 60 * 60 + tm->tm_min * 60 + tm->tm_sec; -} diff --git a/uemis-downloader.c b/uemis-downloader.c deleted file mode 100644 index 2a6e5178c..000000000 --- a/uemis-downloader.c +++ /dev/null @@ -1,1359 +0,0 @@ -/* - * uemis-downloader.c - * - * Copyright (c) Dirk Hohndel - * released under GPL2 - * - * very (VERY) loosely based on the algorithms found in Java code by Fabian Gast - * which was released under the BSD-STYLE BEER WARE LICENSE - * I believe that I only used the information about HOW to do this (download data from the Uemis - * Zurich) but did not actually use any of his copyrighted code, therefore the license under which - * he released his code does not apply to this new implementation in C - * - * Modified by Guido Lerch guido.lerch@gmail.com in August 2015 - */ -#include -#include -#include -#include -#include -#include - -#include "gettext.h" -#include "libdivecomputer.h" -#include "uemis.h" -#include "divelist.h" - -#define ERR_FS_ALMOST_FULL QT_TRANSLATE_NOOP("gettextFromC", "Uemis Zurich: the file system is almost full.\nDisconnect/reconnect the dive computer\nand click \'Retry\'") -#define ERR_FS_FULL QT_TRANSLATE_NOOP("gettextFromC", "Uemis Zurich: the file system is full.\nDisconnect/reconnect the dive computer\nand click Retry") -#define ERR_FS_SHORT_WRITE QT_TRANSLATE_NOOP("gettextFromC", "Short write to req.txt file.\nIs the Uemis Zurich plugged in correctly?") -#define ERR_NO_FILES QT_TRANSLATE_NOOP("gettextFromC", "No dives to download.") -#define BUFLEN 2048 -#define BUFLEN 2048 -#define NUM_PARAM_BUFS 10 - -// debugging setup -// #define UEMIS_DEBUG 1 + 2 + 4 + 8 + 16 + 32 - -#define UEMIS_MAX_FILES 4000 -#define UEMIS_MEM_FULL 1 -#define UEMIS_MEM_OK 0 -#define UEMIS_SPOT_BLOCK_SIZE 1 -#define UEMIS_DIVE_DETAILS_SIZE 2 -#define UEMIS_LOG_BLOCK_SIZE 10 -#define UEMIS_CHECK_LOG 1 -#define UEMIS_CHECK_DETAILS 2 -#define UEMIS_CHECK_SINGLE_DIVE 3 - -#if UEMIS_DEBUG -const char *home, *user, *d_time; -static int debug_round = 0; -#define debugfile stderr -#endif - -#if UEMIS_DEBUG & 64 /* we are reading from a copy of the filesystem, not the device - no need to wait */ -#define UEMIS_TIMEOUT 50 /* 50ns */ -#define UEMIS_LONG_TIMEOUT 500 /* 500ns */ -#define UEMIS_MAX_TIMEOUT 2000 /* 2ms */ -#else -#define UEMIS_TIMEOUT 50000 /* 50ms */ -#define UEMIS_LONG_TIMEOUT 500000 /* 500ms */ -#define UEMIS_MAX_TIMEOUT 2000000 /* 2s */ -#endif - -static char *param_buff[NUM_PARAM_BUFS]; -static char *reqtxt_path; -static int reqtxt_file; -static int filenr; -static int number_of_files; -static char *mbuf = NULL; -static int mbuf_size = 0; - -static int max_mem_used = -1; -static int next_table_index = 0; -static int dive_to_read = 0; - -/* helper function to parse the Uemis data structures */ -static void uemis_ts(char *buffer, void *_when) -{ - struct tm tm; - timestamp_t *when = _when; - - memset(&tm, 0, sizeof(tm)); - sscanf(buffer, "%d-%d-%dT%d:%d:%d", - &tm.tm_year, &tm.tm_mon, &tm.tm_mday, - &tm.tm_hour, &tm.tm_min, &tm.tm_sec); - tm.tm_mon -= 1; - tm.tm_year -= 1900; - *when = utc_mktime(&tm); -} - -/* float minutes */ -static void uemis_duration(char *buffer, duration_t *duration) -{ - duration->seconds = rint(ascii_strtod(buffer, NULL) * 60); -} - -/* int cm */ -static void uemis_depth(char *buffer, depth_t *depth) -{ - depth->mm = atoi(buffer) * 10; -} - -static void uemis_get_index(char *buffer, int *idx) -{ - *idx = atoi(buffer); -} - -/* space separated */ -static void uemis_add_string(const char *buffer, char **text, const char *delimit) -{ - /* do nothing if this is an empty buffer (Uemis sometimes returns a single - * space for empty buffers) */ - if (!buffer || !*buffer || (*buffer == ' ' && *(buffer + 1) == '\0')) - return; - if (!*text) { - *text = strdup(buffer); - } else { - char *buf = malloc(strlen(buffer) + strlen(*text) + 2); - strcpy(buf, *text); - strcat(buf, delimit); - strcat(buf, buffer); - free(*text); - *text = buf; - } -} - -/* still unclear if it ever reports lbs */ -static void uemis_get_weight(char *buffer, weightsystem_t *weight, int diveid) -{ - weight->weight.grams = uemis_get_weight_unit(diveid) ? - lbs_to_grams(ascii_strtod(buffer, NULL)) : - ascii_strtod(buffer, NULL) * 1000; - weight->description = strdup(translate("gettextFromC", "unknown")); -} - -static struct dive *uemis_start_dive(uint32_t deviceid) -{ - struct dive *dive = alloc_dive(); - dive->downloaded = true; - dive->dc.model = strdup("Uemis Zurich"); - dive->dc.deviceid = deviceid; - return dive; -} - -static struct dive *get_dive_by_uemis_diveid(device_data_t *devdata, uint32_t object_id) -{ - for (int i = 0; i < devdata->download_table->nr; i++) { - if (object_id == devdata->download_table->dives[i]->dc.diveid) - return devdata->download_table->dives[i]; - } - return NULL; -} - -static void record_uemis_dive(device_data_t *devdata, struct dive *dive) -{ - if (devdata->create_new_trip) { - if (!devdata->trip) - devdata->trip = create_and_hookup_trip_from_dive(dive); - else - add_dive_to_trip(dive, devdata->trip); - } - record_dive_to_table(dive, devdata->download_table); -} - -/* send text to the importer progress bar */ -static void uemis_info(const char *fmt, ...) -{ - static char buffer[256]; - va_list ap; - - va_start(ap, fmt); - vsnprintf(buffer, sizeof(buffer), fmt, ap); - va_end(ap); - progress_bar_text = buffer; -} - -static long bytes_available(int file) -{ - long result; - long now = lseek(file, 0, SEEK_CUR); - if (now == -1) - return 0; - result = lseek(file, 0, SEEK_END); - lseek(file, now, SEEK_SET); - if (now == -1 || result == -1) - return 0; - return result; -} - -static int number_of_file(char *path) -{ - int count = 0; -#ifdef WIN32 - struct _wdirent *entry; - _WDIR *dirp = (_WDIR *)subsurface_opendir(path); -#else - struct dirent *entry; - DIR *dirp = (DIR *)subsurface_opendir(path); -#endif - - while (dirp) { -#ifdef WIN32 - entry = _wreaddir(dirp); - if (!entry) - break; -#else - entry = readdir(dirp); - if (!entry) - break; - if (strstr(entry->d_name, ".TXT") || strstr(entry->d_name, ".txt")) /* If the entry is a regular file */ -#endif - count++; - } -#ifdef WIN32 - _wclosedir(dirp); -#else - closedir(dirp); -#endif - return count; -} - -static char *build_filename(const char *path, const char *name) -{ - int len = strlen(path) + strlen(name) + 2; - char *buf = malloc(len); -#if WIN32 - snprintf(buf, len, "%s\\%s", path, name); -#else - snprintf(buf, len, "%s/%s", path, name); -#endif - return buf; -} - -/* Check if there's a req.txt file and get the starting filenr from it. - * Test for the maximum number of ANS files (I believe this is always - * 4000 but in case there are differences depending on firmware, this - * code is easy enough */ -static bool uemis_init(const char *path) -{ - char *ans_path; - int i; - - if (!path) - return false; - /* let's check if this is indeed a Uemis DC */ - reqtxt_path = build_filename(path, "req.txt"); - reqtxt_file = subsurface_open(reqtxt_path, O_RDONLY | O_CREAT, 0666); - if (reqtxt_file < 0) { -#if UEMIS_DEBUG & 1 - fprintf(debugfile, ":EE req.txt can't be opened\n"); -#endif - return false; - } - if (bytes_available(reqtxt_file) > 5) { - char tmp[6]; - read(reqtxt_file, tmp, 5); - tmp[5] = '\0'; -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "::r req.txt \"%s\"\n", tmp); -#endif - if (sscanf(tmp + 1, "%d", &filenr) != 1) - return false; - } else { - filenr = 0; -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "::r req.txt skipped as there were fewer than 5 bytes\n"); -#endif - } - close(reqtxt_file); - - /* It would be nice if we could simply go back to the first set of - * ANS files. But with a FAT filesystem that isn't possible */ - ans_path = build_filename(path, "ANS"); - number_of_files = number_of_file(ans_path); - free(ans_path); - /* initialize the array in which we collect the answers */ - for (i = 0; i < NUM_PARAM_BUFS; i++) - param_buff[i] = ""; - return true; -} - -static void str_append_with_delim(char *s, char *t) -{ - int len = strlen(s); - snprintf(s + len, BUFLEN - len, "%s{", t); -} - -/* The communication protocoll with the DC is truly funky. - * After you write your request to the req.txt file you call this function. - * It writes the number of the next ANS file at the beginning of the req.txt - * file (prefixed by 'n' or 'r') and then again at the very end of it, after - * the full request (this time without the prefix). - * Then it syncs (not needed on Windows) and closes the file. */ -static void trigger_response(int file, char *command, int nr, long tailpos) -{ - char fl[10]; - - snprintf(fl, 8, "%s%04d", command, nr); -#if UEMIS_DEBUG & 4 - fprintf(debugfile, ":tr %s (after seeks)\n", fl); -#endif - if (lseek(file, 0, SEEK_SET) == -1) - goto fs_error; - if (write(file, fl, strlen(fl)) == -1) - goto fs_error; - if (lseek(file, tailpos, SEEK_SET) == -1) - goto fs_error; - if (write(file, fl + 1, strlen(fl + 1)) == -1) - goto fs_error; -#ifndef WIN32 - fsync(file); -#endif -fs_error: - close(file); -} - -static char *next_token(char **buf) -{ - char *q, *p = strchr(*buf, '{'); - if (p) - *p = '\0'; - else - p = *buf + strlen(*buf) - 1; - q = *buf; - *buf = p + 1; - return q; -} - -/* poor man's tokenizer that understands a quoted delimter ('{') */ -static char *next_segment(char *buf, int *offset, int size) -{ - int i = *offset; - int seg_size; - bool done = false; - char *segment; - - while (!done) { - if (i < size) { - if (i < size - 1 && buf[i] == '\\' && - (buf[i + 1] == '\\' || buf[i + 1] == '{')) - memcpy(buf + i, buf + i + 1, size - i - 1); - else if (buf[i] == '{') - done = true; - i++; - } else { - done = true; - } - } - seg_size = i - *offset - 1; - if (seg_size < 0) - seg_size = 0; - segment = malloc(seg_size + 1); - memcpy(segment, buf + *offset, seg_size); - segment[seg_size] = '\0'; - *offset = i; - return segment; -} - -/* a dynamically growing buffer to store the potentially massive responses. - * The binary data block can be more than 100k in size (base64 encoded) */ -static void buffer_add(char **buffer, int *buffer_size, char *buf) -{ - if (!buf) - return; - if (!*buffer) { - *buffer = strdup(buf); - *buffer_size = strlen(*buffer) + 1; - } else { - *buffer_size += strlen(buf); - *buffer = realloc(*buffer, *buffer_size); - strcat(*buffer, buf); - } -#if UEMIS_DEBUG & 8 - fprintf(debugfile, "added \"%s\" to buffer - new length %d\n", buf, *buffer_size); -#endif -} - -/* are there more ANS files we can check? */ -static bool next_file(int max) -{ - if (filenr >= max) - return false; - filenr++; - return true; -} - -/* try and do a quick decode - without trying to get to fancy in case the data - * straddles a block boundary... - * we are parsing something that looks like this: - * object_id{int{2{date{ts{2011-04-05T12:38:04{duration{float{12.000... - */ -static char *first_object_id_val(char *buf) -{ - char *object, *bufend; - if (!buf) - return NULL; - bufend = buf + strlen(buf); - object = strstr(buf, "object_id"); - if (object && object + 14 < bufend) { - /* get the value */ - char tmp[36]; - char *p = object + 14; - char *t = tmp; - -#if UEMIS_DEBUG & 4 - char debugbuf[50]; - strncpy(debugbuf, object, 49); - debugbuf[49] = '\0'; - fprintf(debugfile, "buf |%s|\n", debugbuf); -#endif - while (p < bufend && *p != '{' && t < tmp + 9) - *t++ = *p++; - if (*p == '{') { - /* found the object_id - let's quickly look for the date */ - if (strncmp(p, "{date{ts{", 9) == 0 && strstr(p, "{duration{") != NULL) { - /* cool - that was easy */ - *t++ = ','; - *t++ = ' '; - /* skip the 9 characters we just found and take the date, ignoring the seconds - * and replace the silly 'T' in the middle with a space */ - strncpy(t, p + 9, 16); - if (*(t + 10) == 'T') - *(t + 10) = ' '; - t += 16; - } - *t = '\0'; - return strdup(tmp); - } - } - return NULL; -} - -/* ultra-simplistic; it doesn't deal with the case when the object_id is - * split across two chunks. It also doesn't deal with the discrepancy between - * object_id and dive number as understood by the dive computer */ -static void show_progress(char *buf, const char *what) -{ - char *val = first_object_id_val(buf); - if (val) { -/* let the user know what we are working on */ -#if UEMIS_DEBUG & 8 - fprintf(debugfile, "reading %s\n %s\n %s\n", what, val, buf); -#endif - uemis_info(translate("gettextFromC", "%s %s"), what, val); - free(val); - } -} - -static void uemis_increased_timeout(int *timeout) -{ - if (*timeout < UEMIS_MAX_TIMEOUT) - *timeout += UEMIS_LONG_TIMEOUT; - usleep(*timeout); -} - -/* send a request to the dive computer and collect the answer */ -static bool uemis_get_answer(const char *path, char *request, int n_param_in, - int n_param_out, const char **error_text) -{ - int i = 0, file_length; - char sb[BUFLEN]; - char fl[13]; - char tmp[101]; - const char *what = translate("gettextFromC", "data"); - bool searching = true; - bool assembling_mbuf = false; - bool ismulti = false; - bool found_answer = false; - bool more_files = true; - bool answer_in_mbuf = false; - char *ans_path; - int ans_file; - int timeout = UEMIS_LONG_TIMEOUT; - - reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); - if (reqtxt_file == -1) { - *error_text = "can't open req.txt"; -#ifdef UEMIS_DEBUG - fprintf(debugfile, "open %s failed with errno %d\n", reqtxt_path, errno); -#endif - return false; - } - snprintf(sb, BUFLEN, "n%04d12345678", filenr); - str_append_with_delim(sb, request); - for (i = 0; i < n_param_in; i++) - str_append_with_delim(sb, param_buff[i]); - if (!strcmp(request, "getDivelogs") || !strcmp(request, "getDeviceData") || !strcmp(request, "getDirectory") || - !strcmp(request, "getDivespot") || !strcmp(request, "getDive")) { - answer_in_mbuf = true; - str_append_with_delim(sb, ""); - if (!strcmp(request, "getDivelogs")) - what = translate("gettextFromC", "divelog #"); - else if (!strcmp(request, "getDivespot")) - what = translate("gettextFromC", "divespot #"); - else if (!strcmp(request, "getDive")) - what = translate("gettextFromC", "details for #"); - } - str_append_with_delim(sb, ""); - file_length = strlen(sb); - snprintf(fl, 10, "%08d", file_length - 13); - memcpy(sb + 5, fl, strlen(fl)); -#if UEMIS_DEBUG & 4 - fprintf(debugfile, "::w req.txt \"%s\"\n", sb); -#endif - if (write(reqtxt_file, sb, strlen(sb)) != strlen(sb)) { - *error_text = translate("gettextFromC", ERR_FS_SHORT_WRITE); - return false; - } - if (!next_file(number_of_files)) { - *error_text = translate("gettextFromC", ERR_FS_FULL); - more_files = false; - } - trigger_response(reqtxt_file, "n", filenr, file_length); - usleep(timeout); - mbuf = NULL; - mbuf_size = 0; - while (searching || assembling_mbuf) { - if (import_thread_cancelled) - return false; - progress_bar_fraction = filenr / (double)UEMIS_MAX_FILES; - snprintf(fl, 13, "ANS%d.TXT", filenr - 1); - ans_path = build_filename(build_filename(path, "ANS"), fl); - ans_file = subsurface_open(ans_path, O_RDONLY, 0666); - read(ans_file, tmp, 100); - close(ans_file); -#if UEMIS_DEBUG & 8 - tmp[100] = '\0'; - fprintf(debugfile, "::t %s \"%s\"\n", ans_path, tmp); -#elif UEMIS_DEBUG & 4 - char pbuf[4]; - pbuf[0] = tmp[0]; - pbuf[1] = tmp[1]; - pbuf[2] = tmp[2]; - pbuf[3] = 0; - fprintf(debugfile, "::t %s \"%s...\"\n", ans_path, pbuf); -#endif - free(ans_path); - if (tmp[0] == '1') { - searching = false; - if (tmp[1] == 'm') { - assembling_mbuf = true; - ismulti = true; - } - if (tmp[2] == 'e') - assembling_mbuf = false; - if (assembling_mbuf) { - if (!next_file(number_of_files)) { - *error_text = translate("gettextFromC", ERR_FS_FULL); - more_files = false; - assembling_mbuf = false; - } - reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); - if (reqtxt_file == -1) { - *error_text = "can't open req.txt"; - fprintf(stderr, "open %s failed with errno %d\n", reqtxt_path, errno); - return false; - } - trigger_response(reqtxt_file, "n", filenr, file_length); - } - } else { - if (!next_file(number_of_files - 1)) { - *error_text = translate("gettextFromC", ERR_FS_FULL); - more_files = false; - assembling_mbuf = false; - searching = false; - } - reqtxt_file = subsurface_open(reqtxt_path, O_RDWR | O_CREAT, 0666); - if (reqtxt_file == -1) { - *error_text = "can't open req.txt"; - fprintf(stderr, "open %s failed with errno %d\n", reqtxt_path, errno); - return false; - } - trigger_response(reqtxt_file, "r", filenr, file_length); - uemis_increased_timeout(&timeout); - } - if (ismulti && more_files && tmp[0] == '1') { - int size; - snprintf(fl, 13, "ANS%d.TXT", assembling_mbuf ? filenr - 2 : filenr - 1); - ans_path = build_filename(build_filename(path, "ANS"), fl); - ans_file = subsurface_open(ans_path, O_RDONLY, 0666); - free(ans_path); - size = bytes_available(ans_file); - if (size > 3) { - char *buf; - int r; - if (lseek(ans_file, 3, SEEK_CUR) == -1) - goto fs_error; - buf = malloc(size - 2); - if ((r = read(ans_file, buf, size - 3)) != size - 3) { - free(buf); - goto fs_error; - } - buf[r] = '\0'; - buffer_add(&mbuf, &mbuf_size, buf); - show_progress(buf, what); - free(buf); - param_buff[3]++; - } - close(ans_file); - timeout = UEMIS_TIMEOUT; - usleep(UEMIS_TIMEOUT); - } - } - if (more_files) { - int size = 0, j = 0; - char *buf = NULL; - - if (!ismulti) { - snprintf(fl, 13, "ANS%d.TXT", filenr - 1); - ans_path = build_filename(build_filename(path, "ANS"), fl); - ans_file = subsurface_open(ans_path, O_RDONLY, 0666); - free(ans_path); - size = bytes_available(ans_file); - if (size > 3) { - int r; - if (lseek(ans_file, 3, SEEK_CUR) == -1) - goto fs_error; - buf = malloc(size - 2); - if ((r = read(ans_file, buf, size - 3)) != size - 3) { - free(buf); - goto fs_error; - } - buf[r] = '\0'; - buffer_add(&mbuf, &mbuf_size, buf); - show_progress(buf, what); -#if UEMIS_DEBUG & 8 - fprintf(debugfile, "::r %s \"%s\"\n", fl, buf); -#endif - } - size -= 3; - close(ans_file); - } else { - ismulti = false; - } -#if UEMIS_DEBUG & 8 - fprintf(debugfile, ":r: %s\n", buf); -#endif - if (!answer_in_mbuf) - for (i = 0; i < n_param_out && j < size; i++) - param_buff[i] = next_segment(buf, &j, size); - found_answer = true; - free(buf); - } -#if UEMIS_DEBUG & 1 - for (i = 0; i < n_param_out; i++) - fprintf(debugfile, "::: %d: %s\n", i, param_buff[i]); -#endif - return found_answer; -fs_error: - close(ans_file); - return false; -} - -static bool parse_divespot(char *buf) -{ - char *bp = buf + 1; - char *tp = next_token(&bp); - char *tag, *type, *val; - char locationstring[1024] = ""; - int divespot, len; - double latitude = 0.0, longitude = 0.0; - - // dive spot got deleted, so fail here - if (strstr(bp, "deleted{bool{true")) - return false; - // not a dive spot, fail here - if (strcmp(tp, "divespot")) - return false; - do - tag = next_token(&bp); - while (*tag && strcmp(tag, "object_id")); - if (!*tag) - return false; - next_token(&bp); - val = next_token(&bp); - divespot = atoi(val); - do { - tag = next_token(&bp); - type = next_token(&bp); - val = next_token(&bp); - if (!strcmp(type, "string") && *val && strcmp(val, " ")) { - len = strlen(locationstring); - snprintf(locationstring + len, sizeof(locationstring) - len, - "%s%s", len ? ", " : "", val); - } else if (!strcmp(type, "float")) { - if (!strcmp(tag, "longitude")) - longitude = ascii_strtod(val, NULL); - else if (!strcmp(tag, "latitude")) - latitude = ascii_strtod(val, NULL); - } - } while (tag && *tag); - - uemis_set_divelocation(divespot, locationstring, longitude, latitude); - return true; -} - -static char *suit[] = {"", QT_TRANSLATE_NOOP("gettextFromC", "wetsuit"), QT_TRANSLATE_NOOP("gettextFromC", "semidry"), QT_TRANSLATE_NOOP("gettextFromC", "drysuit")}; -static char *suit_type[] = {"", QT_TRANSLATE_NOOP("gettextFromC", "shorty"), QT_TRANSLATE_NOOP("gettextFromC", "vest"), QT_TRANSLATE_NOOP("gettextFromC", "long john"), QT_TRANSLATE_NOOP("gettextFromC", "jacket"), QT_TRANSLATE_NOOP("gettextFromC", "full suit"), QT_TRANSLATE_NOOP("gettextFromC", "2 pcs full suit")}; -static char *suit_thickness[] = {"", "0.5-2mm", "2-3mm", "3-5mm", "5-7mm", "8mm+", QT_TRANSLATE_NOOP("gettextFromC", "membrane")}; - -static void parse_tag(struct dive *dive, char *tag, char *val) -{ - /* we can ignore computer_id, water and gas as those are redundant - * with the binary data and would just get overwritten */ -#if UEMIS_DEBUG & 4 - if (strcmp(tag, "file_content")) - fprintf(debugfile, "Adding to dive %d : %s = %s\n", dive->dc.diveid, tag, val); -#endif - if (!strcmp(tag, "date")) { - uemis_ts(val, &dive->when); - } else if (!strcmp(tag, "duration")) { - uemis_duration(val, &dive->dc.duration); - } else if (!strcmp(tag, "depth")) { - uemis_depth(val, &dive->dc.maxdepth); - } else if (!strcmp(tag, "file_content")) { - uemis_parse_divelog_binary(val, dive); - } else if (!strcmp(tag, "altitude")) { - uemis_get_index(val, &dive->dc.surface_pressure.mbar); - } else if (!strcmp(tag, "f32Weight")) { - uemis_get_weight(val, &dive->weightsystem[0], dive->dc.diveid); - } else if (!strcmp(tag, "notes")) { - uemis_add_string(val, &dive->notes, " "); - } else if (!strcmp(tag, "u8DiveSuit")) { - if (*suit[atoi(val)]) - uemis_add_string(translate("gettextFromC", suit[atoi(val)]), &dive->suit, " "); - } else if (!strcmp(tag, "u8DiveSuitType")) { - if (*suit_type[atoi(val)]) - uemis_add_string(translate("gettextFromC", suit_type[atoi(val)]), &dive->suit, " "); - } else if (!strcmp(tag, "u8SuitThickness")) { - if (*suit_thickness[atoi(val)]) - uemis_add_string(translate("gettextFromC", suit_thickness[atoi(val)]), &dive->suit, " "); - } else if (!strcmp(tag, "nickname")) { - uemis_add_string(val, &dive->buddy, ","); - } -} - -static bool uemis_delete_dive(device_data_t *devdata, uint32_t diveid) -{ - struct dive *dive = NULL; - - if (devdata->download_table->dives[devdata->download_table->nr - 1]->dc.diveid == diveid) { - /* we hit the last one in the array */ - dive = devdata->download_table->dives[devdata->download_table->nr - 1]; - } else { - for (int i = 0; i < devdata->download_table->nr - 1; i++) { - if (devdata->download_table->dives[i]->dc.diveid == diveid) { - dive = devdata->download_table->dives[i]; - for (int x = i; x < devdata->download_table->nr - 1; x++) - devdata->download_table->dives[i] = devdata->download_table->dives[x + 1]; - } - } - } - if (dive) { - devdata->download_table->dives[--devdata->download_table->nr] = NULL; - if (dive->tripflag != TF_NONE) - remove_dive_from_trip(dive, false); - - free(dive->dc.sample); - free((void *)dive->notes); - free((void *)dive->divemaster); - free((void *)dive->buddy); - free((void *)dive->suit); - taglist_free(dive->tag_list); - free(dive); - - return true; - } - return false; -} - -/* This function is called for both divelog and dive information that we get - * from the SDA (what an insane design, btw). The object_id in the divelog - * matches the logfilenr in the dive information (which has its own, often - * different object_id) - we use this as the diveid. - * We create the dive when parsing the divelog and then later, when we parse - * the dive information we locate the already created dive via its diveid. - * Most things just get parsed and converted into our internal data structures, - * but the dive location API is even more crazy. We just get an id that is an - * index into yet another data store that we read out later. In order to - * correctly populate the location and gps data from that we need to remember - * the adresses of those fields for every dive that references the divespot. */ -static bool process_raw_buffer(device_data_t *devdata, uint32_t deviceid, char *inbuf, char **max_divenr, bool keep_number, int *for_dive) -{ - char *buf = strdup(inbuf); - char *tp, *bp, *tag, *type, *val; - bool done = false; - int inbuflen = strlen(inbuf); - char *endptr = buf + inbuflen; - bool is_log = false, is_dive = false; - char *sections[10]; - int s, nr_sections = 0; - struct dive *dive = NULL; - char dive_no[10]; - -#if UEMIS_DEBUG & 8 - fprintf(debugfile, "p_r_b %s\n", inbuf); -#endif - if (for_dive) - *for_dive = -1; - bp = buf + 1; - tp = next_token(&bp); - if (strcmp(tp, "divelog") == 0) { - /* this is a divelog */ - is_log = true; - tp = next_token(&bp); - /* is it a valid entry or nothing ? */ - if (strcmp(tp, "1.0") != 0 || strstr(inbuf, "divelog{1.0{{{{")) { - free(buf); - return false; - } - } else if (strcmp(tp, "dive") == 0) { - /* this is dive detail */ - is_dive = true; - tp = next_token(&bp); - if (strcmp(tp, "1.0") != 0) { - free(buf); - return false; - } - } else { - /* don't understand the buffer */ - free(buf); - return false; - } - if (is_log) { - dive = uemis_start_dive(deviceid); - } else { - /* remember, we don't know if this is the right entry, - * so first test if this is even a valid entry */ - if (strstr(inbuf, "deleted{bool{true")) { -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "p_r_b entry deleted\n"); -#endif - /* oops, this one isn't valid, suggest to try the previous one */ - free(buf); - return false; - } - /* quickhack and workaround to capture the original dive_no - * i am doing this so I dont have to change the original design - * but when parsing a dive we never parse the dive number because - * at the time it's being read the *dive varible is not set because - * the dive_no tag comes before the object_id in the uemis ans file - */ - dive_no[0] = '\0'; - char *dive_no_buf = strdup(inbuf); - char *dive_no_ptr = strstr(dive_no_buf, "dive_no{int{") + 12; - if (dive_no_ptr) { - char *dive_no_end = strstr(dive_no_ptr, "{"); - if (dive_no_end) { - *dive_no_end = '\0'; - strncpy(dive_no, dive_no_ptr, 9); - dive_no[9] = '\0'; - } - } - free(dive_no_buf); - } - while (!done) { - /* the valid buffer ends with a series of delimiters */ - if (bp >= endptr - 2 || !strcmp(bp, "{{")) - break; - tag = next_token(&bp); - /* we also end if we get an empty tag */ - if (*tag == '\0') - break; - for (s = 0; s < nr_sections; s++) - if (!strcmp(tag, sections[s])) { - tag = next_token(&bp); - break; - } - type = next_token(&bp); - if (!strcmp(type, "1.0")) { - /* this tells us the sections that will follow; the tag here - * is of the format dive-
*/ - sections[nr_sections] = strchr(tag, '-') + 1; -#if UEMIS_DEBUG & 4 - fprintf(debugfile, "Expect to find section %s\n", sections[nr_sections]); -#endif - if (nr_sections < sizeof(sections) - 1) - nr_sections++; - continue; - } - val = next_token(&bp); -#if UEMIS_DEBUG & 8 - if (strlen(val) < 20) - fprintf(debugfile, "Parsed %s, %s, %s\n*************************\n", tag, type, val); -#endif - if (is_log && strcmp(tag, "object_id") == 0) { - free(*max_divenr); - *max_divenr = strdup(val); - dive->dc.diveid = atoi(val); -#if UEMIS_DEBUG % 2 - fprintf(debugfile, "Adding new dive from log with object_id %d.\n", atoi(val)); -#endif - } else if (is_dive && strcmp(tag, "logfilenr") == 0) { - /* this one tells us which dive we are adding data to */ - dive = get_dive_by_uemis_diveid(devdata, atoi(val)); - if (strcmp(dive_no, "0")) - dive->number = atoi(dive_no); - if (for_dive) - *for_dive = atoi(val); - } else if (!is_log && dive && !strcmp(tag, "divespot_id")) { - int divespot_id = atoi(val); - if (divespot_id != -1) { - dive->dive_site_uuid = create_dive_site("from Uemis", dive->when); - uemis_mark_divelocation(dive->dc.diveid, divespot_id, dive->dive_site_uuid); - } -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "Created divesite %d for diveid : %d\n", dive->dive_site_uuid, dive->dc.diveid); -#endif - } else if (dive) { - parse_tag(dive, tag, val); - } - if (is_log && !strcmp(tag, "file_content")) - done = true; - /* done with one dive (got the file_content tag), but there could be more: - * a '{' indicates the end of the record - but we need to see another "{{" - * later in the buffer to know that the next record is complete (it could - * be a short read because of some error */ - if (done && ++bp < endptr && *bp != '{' && strstr(bp, "{{")) { - done = false; - record_uemis_dive(devdata, dive); - mark_divelist_changed(true); - dive = uemis_start_dive(deviceid); - } - } - if (is_log) { - if (dive->dc.diveid) { - record_uemis_dive(devdata, dive); - mark_divelist_changed(true); - } else { /* partial dive */ - free(dive); - free(buf); - return false; - } - } - free(buf); - return true; -} - -static char *uemis_get_divenr(char *deviceidstr, int force) -{ - uint32_t deviceid, maxdiveid = 0; - int i; - char divenr[10]; - struct dive_table *table; - deviceid = atoi(deviceidstr); - - /* - * If we are are retrying after a disconnect/reconnect, we - * will look up the highest dive number in the dives we - * already have. - * - * Also, if "force_download" is true, do this even if we - * don't have any dives (maxdiveid will remain zero) - */ - if (force || downloadTable.nr) - table = &downloadTable; - else - table = &dive_table; - - for (i = 0; i < table->nr; i++) { - struct dive *d = table->dives[i]; - struct divecomputer *dc; - if (!d) - continue; - for_each_dc (d, dc) { - if (dc->model && !strcmp(dc->model, "Uemis Zurich") && - (dc->deviceid == 0 || dc->deviceid == 0x7fffffff || dc->deviceid == deviceid) && - dc->diveid > maxdiveid) - maxdiveid = dc->diveid; - } - } - snprintf(divenr, 10, "%d", maxdiveid); - return strdup(divenr); -} - -#if UEMIS_DEBUG -static int bufCnt = 0; -static bool do_dump_buffer_to_file(char *buf, char *prefix) -{ - char path[100]; - char date[40]; - char obid[40]; - if (!buf) - return false; - - if (strstr(buf, "date{ts{")) - strncpy(date, strstr(buf, "date{ts{"), sizeof(date)); - else - strncpy(date, "date{ts{no-date{", sizeof(date)); - - if (!strstr(buf, "object_id{int{")) - return false; - - strncpy(obid, strstr(buf, "object_id{int{"), sizeof(obid)); - char *ptr1 = strstr(date, "date{ts{"); - char *ptr2 = strstr(obid, "object_id{int{"); - char *pdate = next_token(&ptr1); - pdate = next_token(&ptr1); - pdate = next_token(&ptr1); - char *pobid = next_token(&ptr2); - pobid = next_token(&ptr2); - pobid = next_token(&ptr2); - snprintf(path, sizeof(path), "/%s/%s/UEMIS Dump/%s_%s_Uemis_dump_%s_in_round_%d_%d.txt", home, user, prefix, pdate, pobid, debug_round, bufCnt); - int dumpFile = subsurface_open(path, O_RDWR | O_CREAT, 0666); - if (dumpFile == -1) - return false; - write(dumpFile, buf, strlen(buf)); - close(dumpFile); - bufCnt++; - return true; -} -#endif - -/* do some more sophisticated calculations here to try and predict if the next round of - * divelog/divedetail reads will fit into the UEMIS buffer, - * filenr holds now the uemis filenr after having read several logs including the dive details, - * fCapacity will five us the average number of files needed for all currently loaded data - * remember the maximum file usage per dive - * return : UEMIS_MEM_OK if there is enough memeory for a full round - * UEMIS_MEM_CRITICAL if the memory is good for reading the dive logs - * UEMIS_MEM_FULL if the memory is exhaused - */ -static int get_memory(struct dive_table *td, int checkpoint) -{ - if (td->nr <= 0) - return UEMIS_MEM_OK; - - switch (checkpoint) { - case UEMIS_CHECK_LOG: - if (filenr / td->nr > max_mem_used) - max_mem_used = filenr / td->nr; - - /* check if a full block of dive logs + dive details and dive spot fit into the UEMIS buffer */ - if (max_mem_used * UEMIS_LOG_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) - return UEMIS_MEM_FULL; - break; - case UEMIS_CHECK_DETAILS: - /* check if the next set of dive details and dive spot fit into the UEMIS buffer */ - if ((UEMIS_DIVE_DETAILS_SIZE + UEMIS_SPOT_BLOCK_SIZE) * UEMIS_LOG_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) - return UEMIS_MEM_FULL; - break; - case UEMIS_CHECK_SINGLE_DIVE: - if (UEMIS_DIVE_DETAILS_SIZE + UEMIS_SPOT_BLOCK_SIZE > UEMIS_MAX_FILES - filenr) - return UEMIS_MEM_FULL; - break; - } - return UEMIS_MEM_OK; -} - -/* mark a dive as deleted by setting download to false - * this will be picked up by some cleaning statement later */ -static void do_delete_dives(struct dive_table *td, int idx) -{ - for (int x = idx; x < td->nr; x++) - td->dives[x]->downloaded = false; -} - -static bool load_uemis_divespot(const char *mountpath, int divespot_id) -{ - char divespotnr[10]; - snprintf(divespotnr, sizeof(divespotnr), "%d", divespot_id); - param_buff[2] = divespotnr; -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "getDivespot %d\n", divespot_id); -#endif - bool success = uemis_get_answer(mountpath, "getDivespot", 3, 0, NULL); - if (mbuf && success) { -#if UEMIS_DEBUG & 16 - do_dump_buffer_to_file(mbuf, "Spot"); -#endif - return parse_divespot(mbuf); - } - return false; -} - -static void get_uemis_divespot(const char *mountpath, int divespot_id, struct dive *dive) -{ - struct dive_site *nds = get_dive_site_by_uuid(dive->dive_site_uuid); - if (nds && nds->name && strstr(nds->name,"from Uemis")) { - if (load_uemis_divespot(mountpath, divespot_id)) { - /* get the divesite based on the diveid, this should give us - * the newly created site - */ - struct dive_site *ods = NULL; - /* with the divesite name we got from parse_dive, that is called on load_uemis_divespot - * we search all existing divesites if we have one with the same name already. The function - * returns the first found which is luckily not the newly created. - */ - (void)get_dive_site_uuid_by_name(nds->name, &ods); - if (ods) { - /* if the uuid's are the same, the new site is a duplicat and can be deleted */ - if (nds->uuid != ods->uuid) { - delete_dive_site(nds->uuid); - dive->dive_site_uuid = ods->uuid; - } - } - } else { - /* if we cant load the dive site details, delete the site we - * created in process_raw_buffer - */ - delete_dive_site(dive->dive_site_uuid); - } - } -} - -static bool get_matching_dive(int idx, char *newmax, int *uemis_mem_status, struct device_data_t *data, const char *mountpath, const char deviceidnr) -{ - struct dive *dive = data->download_table->dives[idx]; - char log_file_no_to_find[20]; - char dive_to_read_buf[10]; - bool found = false; - int deleted_files = 0; - - snprintf(log_file_no_to_find, sizeof(log_file_no_to_find), "logfilenr{int{%d", dive->dc.diveid); - while (!found) { - if (import_thread_cancelled) - break; - snprintf(dive_to_read_buf, sizeof(dive_to_read_buf), "%d", dive_to_read); - param_buff[2] = dive_to_read_buf; - (void)uemis_get_answer(mountpath, "getDive", 3, 0, NULL); -#if UEMIS_DEBUG & 16 - do_dump_buffer_to_file(mbuf, "Dive"); -#endif - *uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_SINGLE_DIVE); - if (*uemis_mem_status == UEMIS_MEM_OK) { - /* if the memory isn's completely full we can try to read more divelog vs. dive details - * UEMIS_MEM_CRITICAL means not enough space for a full round but the dive details - * and the divespots should fit into the UEMIS memory - * The match we do here is to map the object_id to the logfilenr, we do this - * by iterating through the last set of loaded divelogs and then find the corresponding - * dive with the matching logfilenr */ - if (mbuf) { - if (strstr(mbuf, log_file_no_to_find)) { - /* we found the logfilenr that matches our object_id from the divelog we were looking for - * we mark the search sucessfull even if the dive has been deleted. */ - found = true; - if (strstr(mbuf, "deleted{bool{true") == NULL) { - process_raw_buffer(data, deviceidnr, mbuf, &newmax, false, NULL); - /* remember the last log file number as it is very likely that subsequent dives - * have the same or higher logfile number. - * UEMIS unfortunately deletes dives by deleting the dive details and not the logs. */ -#if UEMIS_DEBUG & 2 - d_time = get_dive_date_c_string(dive->when); - fprintf(debugfile, "Matching divelog id %d from %s with dive details %d\n", dive->dc.diveid, d_time, dive_to_read); -#endif - int divespot_id = uemis_get_divespot_id_by_diveid(dive->dc.diveid); - if (divespot_id >= 0) - get_uemis_divespot(mountpath, divespot_id, dive); - - } else { - /* in this case we found a deleted file, so let's increment */ -#if UEMIS_DEBUG & 2 - d_time = get_dive_date_c_string(dive->when); - fprintf(debugfile, "TRY matching divelog id %d from %s with dive details %d but details are deleted\n", dive->dc.diveid, d_time, dive_to_read); -#endif - deleted_files++; - /* mark this log entry as deleted and cleanup later, otherwise we mess up our array */ - dive->downloaded = false; -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "Deleted dive from %s, with id %d from table\n", d_time, dive->dc.diveid); -#endif - } - } else { - uint32_t nr_found = 0; - char *logfilenr = strstr(mbuf, "logfilenr"); - if (logfilenr) { - sscanf(logfilenr, "logfilenr{int{%u", &nr_found); - if (nr_found >= dive->dc.diveid) - dive_to_read = dive_to_read - 2; - if (dive_to_read < -1) - dive_to_read = -1; - } - } - } - dive_to_read++; - } else { - /* At this point the memory of the UEMIS is full, let's cleanup all divelog files were - * we could not match the details to. */ - do_delete_dives(data->download_table, idx); - return false; - } - } - /* decrement iDiveToRead by the amount of deleted entries found to assure - * we are not missing any valid matches when processing subsequent logs */ - dive_to_read = (dive_to_read - deleted_files > 0 ? dive_to_read - deleted_files : 0); - deleted_files = 0; - return true; -} - -const char *do_uemis_import(device_data_t *data) -{ - const char *mountpath = data->devname; - short force_download = data->force_download; - char *newmax = NULL; - int first, start, end = -2; - uint32_t deviceidnr; - char *deviceid = NULL; - const char *result = NULL; - char *endptr; - bool success, keep_number = false, once = true; - int match_dive_and_log = 0; - int uemis_mem_status = UEMIS_MEM_OK; - -#if UEMIS_DEBUG - home = getenv("HOME"); - user = getenv("LOGNAME"); -#endif - if (dive_table.nr == 0) - keep_number = true; - - uemis_info(translate("gettextFromC", "Initialise communication")); - if (!uemis_init(mountpath)) { - free(reqtxt_path); - return translate("gettextFromC", "Uemis init failed"); - } - - if (!uemis_get_answer(mountpath, "getDeviceId", 0, 1, &result)) - goto bail; - deviceid = strdup(param_buff[0]); - deviceidnr = atoi(deviceid); - - /* param_buff[0] is still valid */ - if (!uemis_get_answer(mountpath, "initSession", 1, 6, &result)) - goto bail; - - uemis_info(translate("gettextFromC", "Start download")); - if (!uemis_get_answer(mountpath, "processSync", 0, 2, &result)) - goto bail; - - /* before starting the long download, check if user pressed cancel */ - if (import_thread_cancelled) - goto bail; - - param_buff[1] = "notempty"; - newmax = uemis_get_divenr(deviceid, force_download); - - first = start = atoi(newmax); - dive_to_read = first; - for (;;) { -#if UEMIS_DEBUG & 2 - debug_round++; -#endif -#if UEMIS_DEBUG & 4 - fprintf(debugfile, "d_u_i inner loop start %d end %d newmax %s\n", start, end, newmax); -#endif - /* start at the last filled download table index */ - match_dive_and_log = data->download_table->nr; - sprintf(newmax, "%d", start); - param_buff[2] = newmax; - param_buff[3] = 0; - success = uemis_get_answer(mountpath, "getDivelogs", 3, 0, &result); - uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_DETAILS); - if (success && mbuf && uemis_mem_status != UEMIS_MEM_FULL) { -#if UEMIS_DEBUG & 16 - do_dump_buffer_to_file(mbuf, "Divelogs"); -#endif - /* process the buffer we have assembled */ - - if (!process_raw_buffer(data, deviceidnr, mbuf, &newmax, keep_number, NULL)) { - /* if no dives were downloaded, mark end appropriately */ - if (end == -2) - end = start - 1; - success = false; - } - if (once) { - char *t = first_object_id_val(mbuf); - if (t && atoi(t) > start) - start = atoi(t); - free(t); - once = false; - } - /* clean up mbuf */ - endptr = strstr(mbuf, "{{{"); - if (endptr) - *(endptr + 2) = '\0'; - /* last object_id we parsed */ - sscanf(newmax, "%d", &end); -#if UEMIS_DEBUG & 4 - fprintf(debugfile, "d_u_i after download and parse start %d end %d newmax %s progress %4.2f\n", start, end, newmax, progress_bar_fraction); -#endif - /* The way this works is that I am reading the current dive from what has been loaded during the getDiveLogs call to the UEMIS. - * As the object_id of the divelog entry and the object_id of the dive details are not necessarily the same, the match needs - * to happen based on the logfilenr. - * What the following part does is to optimize the mapping by using - * dive_to_read = the dive deatils entry that need to be read using the object_id - * logFileNoToFind = map the logfilenr of the dive details with the object_id = diveid from the get dive logs */ - for (int i = match_dive_and_log; i < data->download_table->nr; i++) { - bool success = get_matching_dive(i, newmax, &uemis_mem_status, data, mountpath, deviceidnr); - if (!success) - break; - if (import_thread_cancelled) - break; - } - - start = end; - - /* Do some memory checking here */ - uemis_mem_status = get_memory(data->download_table, UEMIS_CHECK_LOG); - if (uemis_mem_status != UEMIS_MEM_OK) - break; - - /* if the user clicked cancel, exit gracefully */ - if (import_thread_cancelled) - break; - - /* if we got an error or got nothing back, stop trying */ - if (!success || !param_buff[3]) - break; -#if UEMIS_DEBUG & 2 - if (debug_round != -1) - if (debug_round-- == 0) - goto bail; -#endif - } else { - /* some of the loading from the UEMIS failed at the divelog level - * if the memory status = full, we cant even load the divespots and/or buddys. - * The loaded block of divelogs is useless and all new loaded divelogs need to - * be deleted from the download_table. - */ - if (uemis_mem_status == UEMIS_MEM_FULL) - do_delete_dives(data->download_table, match_dive_and_log); - break; - } - } - - if (end == -2 && sscanf(newmax, "%d", &end) != 1) - end = start; - -#if UEMIS_DEBUG & 2 - fprintf(debugfile, "Done: read from object_id %d to %d\n", first, end); -#endif - - /* Regardless on where we are with the memory situation, it's time now - * to see if we have to clean some dead bodies from our download table */ - next_table_index = 0; - while (next_table_index < data->download_table->nr) { - if (!data->download_table->dives[next_table_index]->downloaded) - uemis_delete_dive(data, data->download_table->dives[next_table_index]->dc.diveid); - else - next_table_index++; - } - - if (uemis_mem_status != UEMIS_MEM_OK) - result = translate("gettextFromC", ERR_FS_ALMOST_FULL); - -bail: - (void)uemis_get_answer(mountpath, "terminateSync", 0, 3, &result); - if (!strcmp(param_buff[0], "error")) { - if (!strcmp(param_buff[2], "Out of Memory")) - result = translate("gettextFromC", ERR_FS_FULL); - else - result = param_buff[2]; - } - free(deviceid); - free(reqtxt_path); - if (!data->download_table->nr) - result = translate("gettextFromC", ERR_NO_FILES); - return result; -} diff --git a/uemis.c b/uemis.c deleted file mode 100644 index 4135e0cfe..000000000 --- a/uemis.c +++ /dev/null @@ -1,392 +0,0 @@ -/* - * uemis.c - * - * UEMIS SDA file importer - * AUTHOR: Dirk Hohndel - Copyright 2011 - * - * Licensed under the MIT license. - */ -#include -#include - -#include "gettext.h" - -#include "dive.h" -#include "uemis.h" -#include -#include - -/* - * following code is based on code found in at base64.sourceforge.net/b64.c - * AUTHOR: Bob Trower 08/04/01 - * COPYRIGHT: Copyright (c) Trantor Standard Systems Inc., 2001 - * NOTE: This source code may be used as you wish, subject to - * the MIT license. - */ -/* - * Translation Table to decode (created by Bob Trower) - */ -static const char cd64[] = "|$$$}rstuvwxyz{$$$$$$$>?@ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ[\\]^_`abcdefghijklmnopq"; - -/* - * decodeblock -- decode 4 '6-bit' characters into 3 8-bit binary bytes - */ -static void decodeblock(unsigned char in[4], unsigned char out[3]) -{ - out[0] = (unsigned char)(in[0] << 2 | in[1] >> 4); - out[1] = (unsigned char)(in[1] << 4 | in[2] >> 2); - out[2] = (unsigned char)(((in[2] << 6) & 0xc0) | in[3]); -} - -/* - * decode a base64 encoded stream discarding padding, line breaks and noise - */ -static void decode(uint8_t *inbuf, uint8_t *outbuf, int inbuf_len) -{ - uint8_t in[4], out[3], v; - int i, len, indx_in = 0, indx_out = 0; - - while (indx_in < inbuf_len) { - for (len = 0, i = 0; i < 4 && (indx_in < inbuf_len); i++) { - v = 0; - while ((indx_in < inbuf_len) && v == 0) { - v = inbuf[indx_in++]; - v = ((v < 43 || v > 122) ? 0 : cd64[v - 43]); - if (v) - v = ((v == '$') ? 0 : v - 61); - } - if (indx_in < inbuf_len) { - len++; - if (v) - in[i] = (v - 1); - } else - in[i] = 0; - } - if (len) { - decodeblock(in, out); - for (i = 0; i < len - 1; i++) - outbuf[indx_out++] = out[i]; - } - } -} -/* end code from Bob Trower */ - -/* - * convert the base64 data blog - */ -static int uemis_convert_base64(char *base64, uint8_t **data) -{ - int len, datalen; - - len = strlen(base64); - datalen = (len / 4 + 1) * 3; - if (datalen < 0x123 + 0x25) - /* less than header + 1 sample??? */ - fprintf(stderr, "suspiciously short data block %d\n", datalen); - - *data = malloc(datalen); - if (!*data) { - fprintf(stderr, "Out of memory\n"); - return 0; - } - decode((unsigned char *)base64, *data, len); - - if (memcmp(*data, "Dive\01\00\00", 7)) - fprintf(stderr, "Missing Dive100 header\n"); - - return datalen; -} - -struct uemis_helper { - int diveid; - int lbs; - int divespot; - int dive_site_uuid; - struct uemis_helper *next; -}; -static struct uemis_helper *uemis_helper = NULL; - -static struct uemis_helper *uemis_get_helper(int diveid) -{ - struct uemis_helper **php = &uemis_helper; - struct uemis_helper *hp = *php; - - while (hp) { - if (hp->diveid == diveid) - return hp; - if (hp->next) { - hp = hp->next; - continue; - } - php = &hp->next; - break; - } - hp = *php = calloc(1, sizeof(struct uemis_helper)); - hp->diveid = diveid; - hp->next = NULL; - return hp; -} - -static void uemis_weight_unit(int diveid, int lbs) -{ - struct uemis_helper *hp = uemis_get_helper(diveid); - if (hp) - hp->lbs = lbs; -} - -int uemis_get_weight_unit(int diveid) -{ - struct uemis_helper *hp = uemis_helper; - while (hp) { - if (hp->diveid == diveid) - return hp->lbs; - hp = hp->next; - } - /* odd - we should have found this; default to kg */ - return 0; -} - -void uemis_mark_divelocation(int diveid, int divespot, uint32_t dive_site_uuid) -{ - struct uemis_helper *hp = uemis_get_helper(diveid); - hp->divespot = divespot; - hp->dive_site_uuid = dive_site_uuid; -} - -/* support finding a dive spot based on the diveid */ -int uemis_get_divespot_id_by_diveid(uint32_t diveid) -{ - struct uemis_helper *hp = uemis_helper; - while (hp) { - if (hp->diveid == diveid) - return hp->divespot; - hp = hp->next; - } - return -1; -} - -void uemis_set_divelocation(int divespot, char *text, double longitude, double latitude) -{ - struct uemis_helper *hp = uemis_helper; - while (hp) { - if (hp->divespot == divespot) { - struct dive_site *ds = get_dive_site_by_uuid(hp->dive_site_uuid); - if (ds) { - ds->name = strdup(text); - ds->longitude.udeg = round(longitude * 1000000); - ds->latitude.udeg = round(latitude * 1000000); - } - } - hp = hp->next; - } -} - -/* Create events from the flag bits and other data in the sample; - * These bits basically represent what is displayed on screen at sample time. - * Many of these 'warnings' are way hyper-active and seriously clutter the - * profile plot - so these are disabled by default - * - * we mark all the strings for translation, but we store the untranslated - * strings and only convert them when displaying them on screen - this way - * when we write them to the XML file we'll always have the English strings, - * regardless of locale - */ -static void uemis_event(struct dive *dive, struct divecomputer *dc, struct sample *sample, uemis_sample_t *u_sample) -{ - uint8_t *flags = u_sample->flags; - int stopdepth; - static int lastndl; - - if (flags[1] & 0x01) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Safety stop violation")); - if (flags[1] & 0x08) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Speed alarm")); -#if WANT_CRAZY_WARNINGS - if (flags[1] & 0x06) /* both bits 1 and 2 are a warning */ - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Speed warning")); - if (flags[1] & 0x10) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ green warning")); -#endif - if (flags[1] & 0x20) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ ascend warning")); - if (flags[1] & 0x40) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "pOâ‚‚ ascend alarm")); - /* flags[2] reflects the deco / time bar - * flags[3] reflects more display details on deco and pO2 */ - if (flags[4] & 0x01) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Tank pressure info")); - if (flags[4] & 0x04) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "RGT warning")); - if (flags[4] & 0x08) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "RGT alert")); - if (flags[4] & 0x40) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Tank change suggested")); - if (flags[4] & 0x80) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Depth limit exceeded")); - if (flags[5] & 0x01) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Max deco time warning")); - if (flags[5] & 0x04) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Dive time info")); - if (flags[5] & 0x08) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Dive time alert")); - if (flags[5] & 0x10) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Marker")); - if (flags[6] & 0x02) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "No tank data")); - if (flags[6] & 0x04) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Low battery warning")); - if (flags[6] & 0x08) - add_event(dc, sample->time.seconds, 0, 0, 0, QT_TRANSLATE_NOOP("gettextFromC", "Low battery alert")); -/* flags[7] reflects the little on screen icons that remind of previous - * warnings / alerts - not useful for events */ - -#if UEMIS_DEBUG & 32 - int i, j; - for (i = 0; i < 8; i++) { - printf(" %d: ", 29 + i); - for (j = 7; j >= 0; j--) - printf("%c", flags[i] & 1 << j ? '1' : '0'); - } - printf("\n"); -#endif - /* now add deco / NDL - * we don't use events but store this in the sample - that makes much more sense - * for the way we display this information - * What we know about the encoding so far: - * flags[3].bit0 | flags[5].bit1 != 0 ==> in deco - * flags[0].bit7 == 1 ==> Safety Stop - * otherwise NDL */ - stopdepth = rel_mbar_to_depth(u_sample->hold_depth, dive); - if ((flags[3] & 1) | (flags[5] & 2)) { - /* deco */ - sample->in_deco = true; - sample->stopdepth.mm = stopdepth; - sample->stoptime.seconds = u_sample->hold_time * 60; - sample->ndl.seconds = 0; - } else if (flags[0] & 128) { - /* safety stop - distinguished from deco stop by having - * both ndl and stop information */ - sample->in_deco = false; - sample->stopdepth.mm = stopdepth; - sample->stoptime.seconds = u_sample->hold_time * 60; - sample->ndl.seconds = lastndl; - } else { - /* NDL */ - sample->in_deco = false; - lastndl = sample->ndl.seconds = u_sample->hold_time * 60; - sample->stopdepth.mm = 0; - sample->stoptime.seconds = 0; - } -#if UEMIS_DEBUG & 32 - printf("%dm:%ds: p_amb_tol:%d surface:%d holdtime:%d holddepth:%d/%d ---> stopdepth:%d stoptime:%d ndl:%d\n", - sample->time.seconds / 60, sample->time.seconds % 60, u_sample->p_amb_tol, dive->dc.surface_pressure.mbar, - u_sample->hold_time, u_sample->hold_depth, stopdepth, sample->stopdepth.mm, sample->stoptime.seconds, sample->ndl.seconds); -#endif -} - -/* - * parse uemis base64 data blob into struct dive - */ -void uemis_parse_divelog_binary(char *base64, void *datap) -{ - int datalen; - int i; - uint8_t *data; - struct sample *sample = NULL; - uemis_sample_t *u_sample; - struct dive *dive = datap; - struct divecomputer *dc = &dive->dc; - int template, gasoffset; - uint8_t active = 0; - char version[5]; - - datalen = uemis_convert_base64(base64, &data); - dive->dc.airtemp.mkelvin = C_to_mkelvin((*(uint16_t *)(data + 45)) / 10.0); - dive->dc.surface_pressure.mbar = *(uint16_t *)(data + 43); - if (*(uint8_t *)(data + 19)) - dive->dc.salinity = SEAWATER_SALINITY; /* avg grams per 10l sea water */ - else - dive->dc.salinity = FRESHWATER_SALINITY; /* grams per 10l fresh water */ - - /* this will allow us to find the last dive read so far from this computer */ - dc->model = strdup("Uemis Zurich"); - dc->deviceid = *(uint32_t *)(data + 9); - dc->diveid = *(uint16_t *)(data + 7); - /* remember the weight units used in this dive - we may need this later when - * parsing the weight */ - uemis_weight_unit(dc->diveid, *(uint8_t *)(data + 24)); - /* dive template in use: - 0 = air - 1 = nitrox (B) - 2 = nitrox (B+D) - 3 = nitrox (B+T+D) - uemis cylinder data is insane - it stores seven tank settings in a block - and the template tells us which of the four groups of tanks we need to look at - */ - gasoffset = template = *(uint8_t *)(data + 115); - if (template == 3) - gasoffset = 4; - if (template == 0) - template = 1; - for (i = 0; i < template; i++) { - float volume = *(float *)(data + 116 + 25 * (gasoffset + i)) * 1000.0; - /* uemis always assumes a working pressure of 202.6bar (!?!?) - I first thought - * it was 3000psi, but testing against all my dives gets me that strange number. - * Still, that's of course completely bogus and shows they don't get how - * cylinders are named in non-metric parts of the world... - * we store the incorrect working pressure to get the SAC calculations "close" - * but the user will have to correct this manually - */ - dive->cylinder[i].type.size.mliter = rint(volume); - dive->cylinder[i].type.workingpressure.mbar = 202600; - dive->cylinder[i].gasmix.o2.permille = *(uint8_t *)(data + 120 + 25 * (gasoffset + i)) * 10; - dive->cylinder[i].gasmix.he.permille = 0; - } - /* first byte of divelog data is at offset 0x123 */ - i = 0x123; - u_sample = (uemis_sample_t *)(data + i); - while ((i <= datalen) && (data[i] != 0 || data[i + 1] != 0)) { - if (u_sample->active_tank != active) { - if (u_sample->active_tank >= MAX_CYLINDERS) { - fprintf(stderr, "got invalid sensor #%d was #%d\n", u_sample->active_tank, active); - } else { - active = u_sample->active_tank; - add_gas_switch_event(dive, dc, u_sample->dive_time, active); - } - } - sample = prepare_sample(dc); - sample->time.seconds = u_sample->dive_time; - sample->depth.mm = rel_mbar_to_depth(u_sample->water_pressure, dive); - sample->temperature.mkelvin = C_to_mkelvin(u_sample->dive_temperature / 10.0); - sample->sensor = active; - sample->cylinderpressure.mbar = - (u_sample->tank_pressure_high * 256 + u_sample->tank_pressure_low) * 10; - sample->cns = u_sample->cns; - uemis_event(dive, dc, sample, u_sample); - finish_sample(dc); - i += 0x25; - u_sample++; - } - if (sample) - dive->dc.duration.seconds = sample->time.seconds - 1; - - /* get data from the footer */ - char buffer[24]; - - snprintf(version, sizeof(version), "%1u.%02u", data[18], data[17]); - add_extra_data(dc, "FW Version", version); - snprintf(buffer, sizeof(buffer), "%08x", *(uint32_t *)(data + 9)); - add_extra_data(dc, "Serial", buffer); - snprintf(buffer, sizeof(buffer), "%d", *(uint16_t *)(data + i + 35)); - add_extra_data(dc, "main battery after dive", buffer); - snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 24), 60)); - add_extra_data(dc, "no fly time", buffer); - snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 26), 60)); - add_extra_data(dc, "no dive time", buffer); - snprintf(buffer, sizeof(buffer), "%0u:%02u", FRACTION(*(uint16_t *)(data + i + 28), 60)); - add_extra_data(dc, "desat time", buffer); - snprintf(buffer, sizeof(buffer), "%u", *(uint16_t *)(data + i + 30)); - add_extra_data(dc, "allowed altitude", buffer); - - return; -} diff --git a/uemis.h b/uemis.h deleted file mode 100644 index 5f32fe76c..000000000 --- a/uemis.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * defines and prototypes for the uemis Zurich SDA file parser - */ - -#ifndef UEMIS_H -#define UEMIS_H - -#include -#include "dive.h" - -#ifdef __cplusplus -extern "C" { -#endif - -void uemis_parse_divelog_binary(char *base64, void *divep); -int uemis_get_weight_unit(int diveid); -void uemis_mark_divelocation(int diveid, int divespot, uint32_t dive_site_uuid); -void uemis_set_divelocation(int divespot, char *text, double longitude, double latitude); -int uemis_get_divespot_id_by_diveid(uint32_t diveid); - -typedef struct -{ - uint16_t dive_time; - uint16_t water_pressure; // (in cbar) - uint16_t dive_temperature; // (in dC) - uint8_t ascent_speed; // (units unclear) - uint8_t work_fact; - uint8_t cold_fact; - uint8_t bubble_fact; - uint16_t ascent_time; - uint16_t ascent_time_opt; - uint16_t p_amb_tol; - uint16_t satt; - uint16_t hold_depth; - uint16_t hold_time; - uint8_t active_tank; - // bloody glib, when compiled for Windows, forces the whole program to use - // the Windows packing rules. So to avoid problems on Windows (and since - // only tank_pressure is currently used and that exactly once) I give in and - // make this silly low byte / high byte 8bit entries - uint8_t tank_pressure_low; // (in cbar) - uint8_t tank_pressure_high; - uint8_t consumption_low; // (units unclear) - uint8_t consumption_high; - uint8_t rgt; // (remaining gas time in minutes) - uint8_t cns; - uint8_t flags[8]; -} __attribute((packed)) uemis_sample_t; - -#ifdef __cplusplus -} -#endif - -#endif // UEMIS_H diff --git a/units.h b/units.h deleted file mode 100644 index 1273bd9bb..000000000 --- a/units.h +++ /dev/null @@ -1,277 +0,0 @@ -#ifndef UNITS_H -#define UNITS_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define O2_IN_AIR 209 // permille -#define N2_IN_AIR 781 -#define O2_DENSITY 1429 // mg/Liter -#define N2_DENSITY 1251 -#define HE_DENSITY 179 -#define SURFACE_PRESSURE 1013 // mbar -#define SURFACE_PRESSURE_STRING "1013" -#define ZERO_C_IN_MKELVIN 273150 // mKelvin - -#ifdef __cplusplus -#define M_OR_FT(_m, _f) ((prefs.units.length == units::METERS) ? ((_m) * 1000) : (feet_to_mm(_f))) -#else -#define M_OR_FT(_m, _f) ((prefs.units.length == METERS) ? ((_m) * 1000) : (feet_to_mm(_f))) -#endif - -/* Salinity is expressed in weight in grams per 10l */ -#define SEAWATER_SALINITY 10300 -#define FRESHWATER_SALINITY 10000 - -#include -/* - * Some silly typedefs to make our units very explicit. - * - * Also, the units are chosen so that values can be expressible as - * integers, so that we never have FP rounding issues. And they - * are small enough that converting to/from imperial units doesn't - * really matter. - * - * We also strive to make '0' a meaningless number saying "not - * initialized", since many values are things that may not have - * been reported (eg cylinder pressure or temperature from dive - * computers that don't support them). But sometimes -1 is an even - * more explicit way of saying "not there". - * - * Thus "millibar" for pressure, for example, or "millikelvin" for - * temperatures. Doing temperatures in celsius or fahrenheit would - * make for loss of precision when converting from one to the other, - * and using millikelvin is SI-like but also means that a temperature - * of '0' is clearly just a missing temperature or cylinder pressure. - * - * Also strive to use units that can not possibly be mistaken for a - * valid value in a "normal" system without conversion. If the max - * depth of a dive is '20000', you probably didn't convert from mm on - * output, or if the max depth gets reported as "0.2ft" it was either - * a really boring dive, or there was some missing input conversion, - * and a 60-ft dive got recorded as 60mm. - * - * Doing these as "structs containing value" means that we always - * have to explicitly write out those units in order to get at the - * actual value. So there is hopefully little fear of using a value - * in millikelvin as Fahrenheit by mistake. - * - * We don't actually use these all yet, so maybe they'll change, but - * I made a number of types as guidelines. - */ -typedef int64_t timestamp_t; - -typedef struct -{ - uint32_t seconds; // durations up to 68 yrs -} duration_t; - -typedef struct -{ - int32_t seconds; // offsets up to +/- 34 yrs -} offset_t; - -typedef struct -{ - int32_t mm; -} depth_t; // depth to 2000 km - -typedef struct -{ - int32_t mbar; // pressure up to 2000 bar -} pressure_t; - -typedef struct -{ - uint16_t mbar; -} o2pressure_t; // pressure up to 65 bar - -typedef struct -{ - int16_t degrees; -} bearing_t; // compass bearing - -typedef struct -{ - int32_t mkelvin; // up to 1750 degrees K -} temperature_t; - -typedef struct -{ - int mliter; -} volume_t; - -typedef struct -{ - int permille; -} fraction_t; - -typedef struct -{ - int grams; -} weight_t; - -typedef struct -{ - int udeg; -} degrees_t; - -static inline double udeg_to_radians(int udeg) -{ - return (udeg * M_PI) / (1000000.0 * 180.0); -} - -static inline double grams_to_lbs(int grams) -{ - return grams / 453.6; -} - -static inline int lbs_to_grams(double lbs) -{ - return rint(lbs * 453.6); -} - -static inline double ml_to_cuft(int ml) -{ - return ml / 28316.8466; -} - -static inline double cuft_to_l(double cuft) -{ - return cuft * 28.3168466; -} - -static inline double mm_to_feet(int mm) -{ - return mm * 0.00328084; -} - -static inline double m_to_mile(int m) -{ - return m / 1609.344; -} - -static inline unsigned long feet_to_mm(double feet) -{ - return rint(feet * 304.8); -} - -static inline int to_feet(depth_t depth) -{ - return rint(mm_to_feet(depth.mm)); -} - -static inline double mkelvin_to_C(int mkelvin) -{ - return (mkelvin - ZERO_C_IN_MKELVIN) / 1000.0; -} - -static inline double mkelvin_to_F(int mkelvin) -{ - return mkelvin * 9 / 5000.0 - 459.670; -} - -static inline unsigned long F_to_mkelvin(double f) -{ - return rint((f - 32) * 1000 / 1.8 + ZERO_C_IN_MKELVIN); -} - -static inline unsigned long C_to_mkelvin(double c) -{ - return rint(c * 1000 + ZERO_C_IN_MKELVIN); -} - -static inline double psi_to_bar(double psi) -{ - return psi / 14.5037738; -} - -static inline long psi_to_mbar(double psi) -{ - return rint(psi_to_bar(psi) * 1000); -} - -static inline int to_PSI(pressure_t pressure) -{ - return rint(pressure.mbar * 0.0145037738); -} - -static inline double bar_to_atm(double bar) -{ - return bar / SURFACE_PRESSURE * 1000; -} - -static inline double mbar_to_atm(int mbar) -{ - return (double)mbar / SURFACE_PRESSURE; -} - -static inline int mbar_to_PSI(int mbar) -{ - pressure_t p = { mbar }; - return to_PSI(p); -} - -/* - * We keep our internal data in well-specified units, but - * the input and output may come in some random format. This - * keeps track of those units. - */ -/* turns out in Win32 PASCAL is defined as a calling convention */ -#ifdef WIN32 -#undef PASCAL -#endif -struct units { - enum { - METERS, - FEET - } length; - enum { - LITER, - CUFT - } volume; - enum { - BAR, - PSI, - PASCAL - } pressure; - enum { - CELSIUS, - FAHRENHEIT, - KELVIN - } temperature; - enum { - KG, - LBS - } weight; - enum { - SECONDS, - MINUTES - } vertical_speed_time; -}; - -/* - * We're going to default to SI units for input. Yes, - * technically the SI unit for pressure is Pascal, but - * we default to bar (10^5 pascal), which people - * actually use. Similarly, C instead of Kelvin. - * And kg instead of g. - */ -#define SI_UNITS \ - { \ - .length = METERS, .volume = LITER, .pressure = BAR, .temperature = CELSIUS, .weight = KG, .vertical_speed_time = MINUTES \ - } - -#define IMPERIAL_UNITS \ - { \ - .length = FEET, .volume = CUFT, .pressure = PSI, .temperature = FAHRENHEIT, .weight = LBS, .vertical_speed_time = MINUTES \ - } - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/version.c b/version.c deleted file mode 100644 index 5b54bf4c7..000000000 --- a/version.c +++ /dev/null @@ -1,16 +0,0 @@ -#include "ssrf-version.h" - -const char *subsurface_version(void) -{ - return VERSION_STRING; -} - -const char *subsurface_git_version(void) -{ - return GIT_VERSION_STRING; -} - -const char *subsurface_canonical_version(void) -{ - return CANONICAL_VERSION_STRING; -} diff --git a/version.h b/version.h deleted file mode 100644 index bc0aac00d..000000000 --- a/version.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef VERSION_H -#define VERSION_H - -#ifdef __cplusplus -extern "C" { -#endif - -const char *subsurface_version(void); -const char *subsurface_git_version(void); -const char *subsurface_canonical_version(void); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/webservice.h b/webservice.h deleted file mode 100644 index 052b8aae7..000000000 --- a/webservice.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef WEBSERVICE_H -#define WEBSERVICE_H - -#ifdef __cplusplus -extern "C" { -#endif - -//extern void webservice_download_dialog(void); -//extern bool webservice_request_user_xml(const gchar *, gchar **, unsigned int *, unsigned int *); -extern int divelogde_upload(char *fn, char **error); -extern unsigned int download_dialog_parse_response(char *xmldata, unsigned int len); - -enum { - DD_STATUS_OK, - DD_STATUS_ERROR_CONNECT, - DD_STATUS_ERROR_ID, - DD_STATUS_ERROR_PARSE, -}; - - -#ifdef __cplusplus -} -#endif -#endif // WEBSERVICE_H diff --git a/windows.c b/windows.c deleted file mode 100644 index a2386fd83..000000000 --- a/windows.c +++ /dev/null @@ -1,448 +0,0 @@ -/* windows.c */ -/* implements Windows specific functions */ -#include -#include "dive.h" -#include "display.h" -#undef _WIN32_WINNT -#define _WIN32_WINNT 0x500 -#include -#include -#include -#include -#include -#include -#include -#include - -const char non_standard_system_divelist_default_font[] = "Calibri"; -const char current_system_divelist_default_font[] = "Segoe UI"; -const char *system_divelist_default_font = non_standard_system_divelist_default_font; -double system_divelist_default_font_size = -1; - -void subsurface_user_info(struct user_info *user) -{ /* Encourage use of at least libgit2-0.20 */ } - -extern bool isWin7Or8(); - -void subsurface_OS_pref_setup(void) -{ - if (isWin7Or8()) - system_divelist_default_font = current_system_divelist_default_font; -} - -bool subsurface_ignore_font(const char *font) -{ - // if this is running on a recent enough version of Windows and the font - // passed in is the pre 4.3 default font, ignore it - if (isWin7Or8() && strcmp(font, non_standard_system_divelist_default_font) == 0) - return true; - return false; -} - -/* this function returns the Win32 Roaming path for the current user as UTF-8. - * it never returns NULL but fallsback to .\ instead! - * the append argument will append a wchar_t string to the end of the path. - */ -static const char *system_default_path_append(const wchar_t *append) -{ - wchar_t wpath[MAX_PATH] = { 0 }; - const char *fname = "system_default_path_append()"; - - /* obtain the user path via SHGetFolderPathW. - * this API is deprecated but still supported on modern Win32. - * fallback to .\ if it fails. - */ - if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, wpath))) { - fprintf(stderr, "%s: cannot obtain path!\n", fname); - wpath[0] = L'.'; - wpath[1] = L'\0'; - } - - wcscat(wpath, L"\\Subsurface"); - if (append) { - wcscat(wpath, L"\\"); - wcscat(wpath, append); - } - - /* attempt to convert the UTF-16 string to UTF-8. - * resize the buffer and fallback to .\Subsurface if it fails. - */ - const int wsz = wcslen(wpath); - const int sz = WideCharToMultiByte(CP_UTF8, 0, wpath, wsz, NULL, 0, NULL, NULL); - char *path = (char *)malloc(sz + 1); - if (!sz) - goto fallback; - if (WideCharToMultiByte(CP_UTF8, 0, wpath, wsz, path, sz, NULL, NULL)) { - path[sz] = '\0'; - return path; - } - -fallback: - fprintf(stderr, "%s: cannot obtain path as UTF-8!\n", fname); - const char *local = ".\\Subsurface"; - const int len = strlen(local) + 1; - path = (char *)realloc(path, len); - memset(path, 0, len); - strcat(path, local); - return path; -} - -/* by passing NULL to system_default_path_append() we obtain the pure path. - * '\' not included at the end. - */ -const char *system_default_directory(void) -{ - static const char *path = NULL; - if (!path) - path = system_default_path_append(NULL); - return path; -} - -/* obtain the Roaming path and append "\\.xml" to it. - */ -const char *system_default_filename(void) -{ - static wchar_t filename[UNLEN + 5] = { 0 }; - if (!*filename) { - wchar_t username[UNLEN + 1] = { 0 }; - DWORD username_len = UNLEN + 1; - GetUserNameW(username, &username_len); - wcscat(filename, username); - wcscat(filename, L".xml"); - } - static const char *path = NULL; - if (!path) - path = system_default_path_append(filename); - return path; -} - -int enumerate_devices(device_callback_t callback, void *userdata, int dc_type) -{ - int index = -1; - DWORD i; - if (dc_type != DC_TYPE_UEMIS) { - // Open the registry key. - HKEY hKey; - LONG rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM", 0, KEY_QUERY_VALUE, &hKey); - if (rc != ERROR_SUCCESS) { - return -1; - } - - // Get the number of values. - DWORD count = 0; - rc = RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &count, NULL, NULL, NULL, NULL); - if (rc != ERROR_SUCCESS) { - RegCloseKey(hKey); - return -1; - } - for (i = 0; i < count; ++i) { - // Get the value name, data and type. - char name[512], data[512]; - DWORD name_len = sizeof(name); - DWORD data_len = sizeof(data); - DWORD type = 0; - rc = RegEnumValue(hKey, i, name, &name_len, NULL, &type, (LPBYTE)data, &data_len); - if (rc != ERROR_SUCCESS) { - RegCloseKey(hKey); - return -1; - } - - // Ignore non-string values. - if (type != REG_SZ) - continue; - - // Prevent a possible buffer overflow. - if (data_len >= sizeof(data)) { - RegCloseKey(hKey); - return -1; - } - - // Null terminate the string. - data[data_len] = 0; - - callback(data, userdata); - index++; - if (is_default_dive_computer_device(name)) - index = i; - } - - RegCloseKey(hKey); - } - if (dc_type != DC_TYPE_SERIAL) { - int i; - int count_drives = 0; - const int bufdef = 512; - const char *dlabels[] = {"UEMISSDA", NULL}; - char bufname[bufdef], bufval[bufdef], *p; - DWORD bufname_len; - - /* add drive letters that match labels */ - memset(bufname, 0, bufdef); - bufname_len = bufdef; - if (GetLogicalDriveStringsA(bufname_len, bufname)) { - p = bufname; - - while (*p) { - memset(bufval, 0, bufdef); - if (GetVolumeInformationA(p, bufval, bufdef, NULL, NULL, NULL, NULL, 0)) { - for (i = 0; dlabels[i] != NULL; i++) - if (!strcmp(bufval, dlabels[i])) { - char data[512]; - snprintf(data, sizeof(data), "%s (%s)", p, dlabels[i]); - callback(data, userdata); - if (is_default_dive_computer_device(p)) - index = count_drives; - count_drives++; - } - } - p = &p[strlen(p) + 1]; - } - if (count_drives == 1) /* we found exactly one Uemis "drive" */ - index = 0; /* make it the selected "device" */ - } - } - return index; -} - -/* this function converts a utf-8 string to win32's utf-16 2 byte string. - * the caller function should manage the allocated memory. - */ -static wchar_t *utf8_to_utf16_fl(const char *utf8, char *file, int line) -{ - assert(utf8 != NULL); - assert(file != NULL); - assert(line); - /* estimate buffer size */ - const int sz = strlen(utf8) + 1; - wchar_t *utf16 = (wchar_t *)malloc(sizeof(wchar_t) * sz); - if (!utf16) { - fprintf(stderr, "%s:%d: %s %d.", file, line, "cannot allocate buffer of size", sz); - return NULL; - } - if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16, sz)) - return utf16; - fprintf(stderr, "%s:%d: %s", file, line, "cannot convert string."); - free((void *)utf16); - return NULL; -} - -#define utf8_to_utf16(s) utf8_to_utf16_fl(s, __FILE__, __LINE__) - -/* bellow we provide a set of wrappers for some I/O functions to use wchar_t. - * on win32 this solves the issue that we need paths to be utf-16 encoded. - */ -int subsurface_rename(const char *path, const char *newpath) -{ - int ret = -1; - if (!path || !newpath) - return ret; - - wchar_t *wpath = utf8_to_utf16(path); - wchar_t *wnewpath = utf8_to_utf16(newpath); - - if (wpath && wnewpath) - ret = _wrename(wpath, wnewpath); - free((void *)wpath); - free((void *)wnewpath); - return ret; -} - -// if the QDir based rename fails, we try this one -int subsurface_dir_rename(const char *path, const char *newpath) -{ - // check if the folder exists - BOOL exists = FALSE; - DWORD attrib = GetFileAttributes(path); - if (attrib != INVALID_FILE_ATTRIBUTES && attrib & FILE_ATTRIBUTE_DIRECTORY) - exists = TRUE; - if (!exists && verbose) { - fprintf(stderr, "folder not found or path is not a folder: %s\n", path); - return EXIT_FAILURE; - } - - // list of error codes: - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx - DWORD errorCode; - - // if this fails something has already obatained (more) exclusive access to the folder - HANDLE h = CreateFile(path, GENERIC_WRITE, FILE_SHARE_WRITE | - FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - if (h == INVALID_HANDLE_VALUE) { - errorCode = GetLastError(); - if (verbose) - fprintf(stderr, "cannot obtain exclusive write access for folder: %u\n", (unsigned int)errorCode ); - return EXIT_FAILURE; - } else { - if (verbose) - fprintf(stderr, "exclusive write access obtained...closing handle!"); - CloseHandle(h); - - // attempt to rename - BOOL result = MoveFile(path, newpath); - if (!result) { - errorCode = GetLastError(); - if (verbose) - fprintf(stderr, "rename failed: %u\n", (unsigned int)errorCode); - return EXIT_FAILURE; - } - if (verbose > 1) - fprintf(stderr, "folder rename success: %s ---> %s\n", path, newpath); - } - return EXIT_SUCCESS; -} - -int subsurface_open(const char *path, int oflags, mode_t mode) -{ - int ret = -1; - if (!path) - return ret; - wchar_t *wpath = utf8_to_utf16(path); - if (wpath) - ret = _wopen(wpath, oflags, mode); - free((void *)wpath); - return ret; -} - -FILE *subsurface_fopen(const char *path, const char *mode) -{ - FILE *ret = NULL; - if (!path) - return ret; - wchar_t *wpath = utf8_to_utf16(path); - if (wpath) { - const int len = strlen(mode); - wchar_t wmode[len + 1]; - for (int i = 0; i < len; i++) - wmode[i] = (wchar_t)mode[i]; - wmode[len] = 0; - ret = _wfopen(wpath, wmode); - } - free((void *)wpath); - return ret; -} - -/* here we return a void pointer instead of _WDIR or DIR pointer */ -void *subsurface_opendir(const char *path) -{ - _WDIR *ret = NULL; - if (!path) - return ret; - wchar_t *wpath = utf8_to_utf16(path); - if (wpath) - ret = _wopendir(wpath); - free((void *)wpath); - return (void *)ret; -} - -int subsurface_access(const char *path, int mode) -{ - int ret = -1; - if (!path) - return ret; - wchar_t *wpath = utf8_to_utf16(path); - if (wpath) - ret = _waccess(wpath, mode); - free((void *)wpath); - return ret; -} - -#ifndef O_BINARY -#define O_BINARY 0 -#endif - -struct zip *subsurface_zip_open_readonly(const char *path, int flags, int *errorp) -{ -#if defined(LIBZIP_VERSION_MAJOR) - /* libzip 0.10 has zip_fdopen, let's use it since zip_open doesn't have a - * wchar_t version */ - int fd = subsurface_open(path, O_RDONLY | O_BINARY, 0); - struct zip *ret = zip_fdopen(fd, flags, errorp); - if (!ret) - close(fd); - return ret; -#else - return zip_open(path, flags, errorp); -#endif -} - -int subsurface_zip_close(struct zip *zip) -{ - return zip_close(zip); -} - -/* win32 console */ -static struct { - bool allocated; - UINT cp; - FILE *out, *err; -} console_desc; - -void subsurface_console_init(bool dedicated) -{ - (void)console_desc; - /* if this is a console app already, do nothing */ -#ifndef WIN32_CONSOLE_APP - /* just in case of multiple calls */ - memset((void *)&console_desc, 0, sizeof(console_desc)); - /* the AttachConsole(..) call can be used to determine if the parent process - * is a terminal. if it succeeds, there is no need for a dedicated console - * window and we don't need to call the AllocConsole() function. on the other - * hand if the user has set the 'dedicated' flag to 'true' and if AttachConsole() - * has failed, we create a dedicated console window. - */ - console_desc.allocated = AttachConsole(ATTACH_PARENT_PROCESS); - if (console_desc.allocated) - dedicated = false; - if (!console_desc.allocated && dedicated) - console_desc.allocated = AllocConsole(); - if (!console_desc.allocated) - return; - - console_desc.cp = GetConsoleCP(); - SetConsoleOutputCP(CP_UTF8); /* make the ouput utf8 */ - - /* set some console modes; we don't need to reset these back. - * ENABLE_EXTENDED_FLAGS = 0x0080, ENABLE_QUICK_EDIT_MODE = 0x0040 */ - HANDLE h_in = GetStdHandle(STD_INPUT_HANDLE); - if (h_in) { - SetConsoleMode(h_in, 0x0080 | 0x0040); - CloseHandle(h_in); - } - - /* dedicated only; disable the 'x' button as it will close the main process as well */ - HWND h_cw = GetConsoleWindow(); - if (h_cw && dedicated) { - SetWindowTextA(h_cw, "Subsurface Console"); - HMENU h_menu = GetSystemMenu(h_cw, 0); - if (h_menu) { - EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED); - DrawMenuBar(h_cw); - } - SetConsoleCtrlHandler(NULL, TRUE); /* disable the CTRL handler */ - } - - /* redirect; on win32, CON is a reserved pipe target, like NUL */ - console_desc.out = freopen("CON", "w", stdout); - console_desc.err = freopen("CON", "w", stderr); - if (!dedicated) - puts(""); /* add an empty line */ -#endif -} - -void subsurface_console_exit(void) -{ -#ifndef WIN32_CONSOLE_APP - if (!console_desc.allocated) - return; - - /* close handles */ - if (console_desc.out) - fclose(console_desc.out); - if (console_desc.err) - fclose(console_desc.err); - - /* reset code page and free */ - SetConsoleOutputCP(console_desc.cp); - FreeConsole(); -#endif -} diff --git a/windowtitleupdate.cpp b/windowtitleupdate.cpp deleted file mode 100644 index eec324c68..000000000 --- a/windowtitleupdate.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "windowtitleupdate.h" - -WindowTitleUpdate *WindowTitleUpdate::m_instance = NULL; - -WindowTitleUpdate::WindowTitleUpdate(QObject *parent) : QObject(parent) -{ - Q_ASSERT_X(m_instance == NULL, "WindowTitleUpdate", "WindowTitleUpdate recreated!"); - - m_instance = this; -} - -WindowTitleUpdate *WindowTitleUpdate::instance() -{ - return m_instance; -} - -WindowTitleUpdate::~WindowTitleUpdate() -{ - m_instance = NULL; -} - -void WindowTitleUpdate::emitSignal() -{ - emit updateTitle(); -} - -extern "C" void updateWindowTitle() -{ - WindowTitleUpdate *wt = WindowTitleUpdate::instance(); - wt->emitSignal(); -} diff --git a/windowtitleupdate.h b/windowtitleupdate.h deleted file mode 100644 index 8650e5868..000000000 --- a/windowtitleupdate.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef WINDOWTITLEUPDATE_H -#define WINDOWTITLEUPDATE_H - -#include - -class WindowTitleUpdate : public QObject -{ - Q_OBJECT -public: - explicit WindowTitleUpdate(QObject *parent = 0); - ~WindowTitleUpdate(); - static WindowTitleUpdate *instance(); - void emitSignal(); -signals: - void updateTitle(); -private: - static WindowTitleUpdate *m_instance; -}; - -#endif // WINDOWTITLEUPDATE_H diff --git a/worldmap-options.h b/worldmap-options.h deleted file mode 100644 index 177443563..000000000 --- a/worldmap-options.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef WORLDMAP_OPTIONS_H -#define WORLDMAP_OPTIONS_H - -const char *map_options = "center: new google.maps.LatLng(0,0),\n\tzoom: 3,\n\tminZoom: 2,\n\tmapTypeId: google.maps.MapTypeId.SATELLITE\n\t"; -const char *css = "\n\thtml { height: 100% }\n\tbody { height: 100%; margin: 0; padding: 0 }\n\t#map-canvas { height: 100% }\n"; - -#endif // WORLDMAP-OPTIONS_H diff --git a/worldmap-save.c b/worldmap-save.c deleted file mode 100644 index 25c5ee33f..000000000 --- a/worldmap-save.c +++ /dev/null @@ -1,114 +0,0 @@ -#include -#include -#include -#include - -#include "dive.h" -#include "membuffer.h" -#include "save-html.h" -#include "worldmap-save.h" -#include "worldmap-options.h" -#include "gettext.h" - -char *getGoogleApi() -{ - /* google maps api auth*/ - return "https://maps.googleapis.com/maps/api/js?key=AIzaSyDzo9PWsqYDDSddVswg_13rpD9oH_dLuoQ"; -} - -void writeMarkers(struct membuffer *b, const bool selected_only) -{ - int i, dive_no = 0; - struct dive *dive; - char pre[1000], post[1000]; - - for_each_dive (i, dive) { - if (selected_only) { - if (!dive->selected) - continue; - } - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!ds || !dive_site_has_gps_location(ds)) - continue; - put_degrees(b, ds->latitude, "temp = new google.maps.Marker({position: new google.maps.LatLng(", ""); - put_degrees(b, ds->longitude, ",", ")});\n"); - put_string(b, "markers.push(temp);\ntempinfowindow = new google.maps.InfoWindow({content: '
'+'
'+'
'+'
"); - snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Date:")); - put_HTML_date(b, dive, pre, "

"); - snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Time:")); - put_HTML_time(b, dive, pre, "

"); - snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Duration:")); - snprintf(post, sizeof(post), " %s

", translate("gettextFromC", "min")); - put_duration(b, dive->duration, pre, post); - snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Max. depth:")); - snprintf(post, sizeof(post), " %s

", translate("gettextFromC", "m")); - put_depth(b, dive->maxdepth, pre, post); - put_string(b, "

"); - put_HTML_quoted(b, translate("gettextFromC", "Air temp.:")); - put_HTML_airtemp(b, dive, " ", "

"); - put_string(b, "

"); - put_HTML_quoted(b, translate("gettextFromC", "Water temp.:")); - put_HTML_watertemp(b, dive, " ", "

"); - snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Location:")); - put_string(b, pre); - put_HTML_quoted(b, get_dive_location(dive)); - put_string(b, "

"); - snprintf(pre, sizeof(pre), "

%s ", translate("gettextFromC", "Notes:")); - put_HTML_notes(b, dive, pre, "

"); - put_string(b, "

'+'
'+'
'});\ninfowindows.push(tempinfowindow);\n"); - put_format(b, "google.maps.event.addListener(markers[%d], 'mouseover', function() {\ninfowindows[%d].open(map,markers[%d]);}", dive_no, dive_no, dive_no); - put_format(b, ");google.maps.event.addListener(markers[%d], 'mouseout', function() {\ninfowindows[%d].close();});\n", dive_no, dive_no); - dive_no++; - } -} - -void insert_html_header(struct membuffer *b) -{ - put_string(b, "\n\n\n"); - put_string(b, "\nWorld Map\n"); - put_string(b, ""); -} - -void insert_css(struct membuffer *b) -{ - put_format(b, "\n", css); -} - -void insert_javascript(struct membuffer *b, const bool selected_only) -{ - put_string(b, "\n\n"); -} - -void export(struct membuffer *b, const bool selected_only) -{ - insert_html_header(b); - insert_css(b); - insert_javascript(b, selected_only); - put_string(b, "\t\n\n
\n\n"); -} - -void export_worldmap_HTML(const char *file_name, const bool selected_only) -{ - FILE *f; - - struct membuffer buf = { 0 }; - export(&buf, selected_only); - - f = subsurface_fopen(file_name, "w+"); - if (!f) { - report_error(translate("gettextFromC", "Can't open file %s"), file_name); - } else { - flush_buffer(&buf, f); /*check for writing errors? */ - fclose(f); - } - free_buffer(&buf); -} diff --git a/worldmap-save.h b/worldmap-save.h deleted file mode 100644 index 102ea40e5..000000000 --- a/worldmap-save.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef WORLDMAP_SAVE_H -#define WORLDMAP_SAVE_H - -#ifdef __cplusplus -extern "C" { -#endif - -extern void export_worldmap_HTML(const char *file_name, const bool selected_only); - - -#ifdef __cplusplus -} -#endif - -#endif -- cgit v1.2.3-70-g09d2 From 6cd711a11b1a2d9484eb05915de0bd5dc60907b2 Mon Sep 17 00:00:00 2001 From: Tomaz Canabrava Date: Wed, 2 Sep 2015 21:22:29 -0300 Subject: Modify code to make it compile after rebase Did a git rebase and some stuff changed in the meantime; This is a compatibility commit: Add a few include directories on the cmake to quiet some ui_headers.h not being found (the ones that are created automatically by uic) and a few noiseances like models requiring interface functionality. Signed-off-by: Tomaz Canabrava Signed-off-by: Dirk Hohndel --- CMakeLists.txt | 1 + qt-models/completionmodels.cpp | 3 ++- qt-models/filtermodels.cpp | 11 +++++++++-- qt-ui/CMakeLists.txt | 5 +++++ 4 files changed, 17 insertions(+), 3 deletions(-) (limited to 'qt-ui') diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a14db6de..f1c143b36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set(CMAKE_MODULE_PATH include_directories(. ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_BINARY_DIR} + ${CMAKE_BINARY_DIR}/qt-ui qt-ui qt-models qt-ui/profile diff --git a/qt-models/completionmodels.cpp b/qt-models/completionmodels.cpp index 838d239d2..a8b61aed5 100644 --- a/qt-models/completionmodels.cpp +++ b/qt-models/completionmodels.cpp @@ -1,6 +1,7 @@ #include "completionmodels.h" #include "dive.h" -#include "mainwindow.h" +#include +#include #define CREATE_UPDATE_METHOD(Class, diveStructMember) \ void Class::updateModel() \ diff --git a/qt-models/filtermodels.cpp b/qt-models/filtermodels.cpp index 80ed0cfd5..29853cb3d 100644 --- a/qt-models/filtermodels.cpp +++ b/qt-models/filtermodels.cpp @@ -1,5 +1,4 @@ #include "filtermodels.h" -#include "mainwindow.h" #include "models.h" #include "divelistview.h" #include "display.h" @@ -355,9 +354,16 @@ bool MultiFilterSortModel::filterAcceptsRow(int source_row, const QModelIndex &s void MultiFilterSortModel::myInvalidate() { + //WARNING: + //TODO: + // THIS CODE BELOW IS COMPLETELY BROKEN. I KNOW, I WROTE IT. + // REMOVE THIS, MAKE IT SANE. + // GRRRRR. + +#if 0 int i; struct dive *d; - DiveListView *dlv = MainWindow::instance()->dive_list(); + // DiveListView *dlv = MainWindow::instance()->dive_list(); divesDisplayed = 0; @@ -395,6 +401,7 @@ void MultiFilterSortModel::myInvalidate() if (curr_dive_site) { dlv->expandAll(); } +#endif } void MultiFilterSortModel::addFilterModel(MultiFilterInterface *model) diff --git a/qt-ui/CMakeLists.txt b/qt-ui/CMakeLists.txt index a860faed5..45447771a 100644 --- a/qt-ui/CMakeLists.txt +++ b/qt-ui/CMakeLists.txt @@ -8,6 +8,11 @@ if(BTSUPPORT) set(BT_SRC_FILES btdeviceselectiondialog.cpp) endif() +include_directories(. + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_BINARY_DIR} +) + # the interface, in C++ set(SUBSURFACE_INTERFACE updatemanager.cpp -- cgit v1.2.3-70-g09d2 From e49d6213ad129284a45d53c3fcdc03249e84efe2 Mon Sep 17 00:00:00 2001 From: Tomaz Canabrava Date: Thu, 3 Sep 2015 14:20:19 -0300 Subject: Move qt-ui to desktop-widgets Since we have now destkop and mobile versions, 'qt-ui' was a very poor name choice for a folder that contains only destkop-enabled widgets. Also, move the graphicsview-common.h/cpp to subsurface-core because it doesn't depend on qgraphicsview, it merely implements all the colors that we use throughout Subsurface, and we will use colors on both desktop and mobile versions Same thing applies for metrics.h/cpp Signed-off-by: Tomaz Canabrava Signed-off-by: Dirk Hohndel --- CMakeLists.txt | 8 +- desktop-widgets/CMakeLists.txt | 111 + desktop-widgets/about.cpp | 40 + desktop-widgets/about.h | 21 + desktop-widgets/about.ui | 136 ++ desktop-widgets/btdeviceselectiondialog.cpp | 656 +++++ desktop-widgets/btdeviceselectiondialog.h | 91 + desktop-widgets/btdeviceselectiondialog.ui | 224 ++ desktop-widgets/configuredivecomputerdialog.cpp | 1257 ++++++++++ desktop-widgets/configuredivecomputerdialog.h | 149 ++ desktop-widgets/configuredivecomputerdialog.ui | 2758 ++++++++++++++++++++++ desktop-widgets/css/tableviews.css | 0 desktop-widgets/divecomponentselection.ui | 190 ++ desktop-widgets/divecomputermanagementdialog.cpp | 69 + desktop-widgets/divecomputermanagementdialog.h | 29 + desktop-widgets/divecomputermanagementdialog.ui | 81 + desktop-widgets/divelistview.cpp | 1035 ++++++++ desktop-widgets/divelistview.h | 89 + desktop-widgets/divelogexportdialog.cpp | 240 ++ desktop-widgets/divelogexportdialog.h | 39 + desktop-widgets/divelogexportdialog.ui | 606 +++++ desktop-widgets/divelogimportdialog.cpp | 861 +++++++ desktop-widgets/divelogimportdialog.h | 131 + desktop-widgets/divelogimportdialog.ui | 249 ++ desktop-widgets/divepicturewidget.cpp | 100 + desktop-widgets/divepicturewidget.h | 36 + desktop-widgets/diveplanner.cpp | 513 ++++ desktop-widgets/diveplanner.h | 104 + desktop-widgets/diveplanner.ui | 257 ++ desktop-widgets/diveshareexportdialog.cpp | 141 ++ desktop-widgets/diveshareexportdialog.h | 34 + desktop-widgets/diveshareexportdialog.ui | 291 +++ desktop-widgets/downloadfromdivecomputer.cpp | 727 ++++++ desktop-widgets/downloadfromdivecomputer.h | 125 + desktop-widgets/downloadfromdivecomputer.ui | 301 +++ desktop-widgets/filterwidget.ui | 140 ++ desktop-widgets/globe.cpp | 431 ++++ desktop-widgets/globe.h | 84 + desktop-widgets/groupedlineedit.cpp | 197 ++ desktop-widgets/groupedlineedit.h | 72 + desktop-widgets/kmessagewidget.cpp | 480 ++++ desktop-widgets/kmessagewidget.h | 342 +++ desktop-widgets/listfilter.ui | 63 + desktop-widgets/locationInformation.ui | 156 ++ desktop-widgets/locationinformation.cpp | 618 +++++ desktop-widgets/locationinformation.h | 114 + desktop-widgets/maintab.cpp | 1612 +++++++++++++ desktop-widgets/maintab.h | 129 + desktop-widgets/maintab.ui | 1267 ++++++++++ desktop-widgets/mainwindow.cpp | 1923 +++++++++++++++ desktop-widgets/mainwindow.h | 258 ++ desktop-widgets/mainwindow.ui | 761 ++++++ desktop-widgets/marble/GeoDataTreeModel.h | 118 + desktop-widgets/modeldelegates.cpp | 617 +++++ desktop-widgets/modeldelegates.h | 141 ++ desktop-widgets/notificationwidget.cpp | 42 + desktop-widgets/notificationwidget.h | 32 + desktop-widgets/plannerDetails.ui | 104 + desktop-widgets/plannerSettings.ui | 749 ++++++ desktop-widgets/preferences.cpp | 559 +++++ desktop-widgets/preferences.h | 54 + desktop-widgets/preferences.ui | 1883 +++++++++++++++ desktop-widgets/printdialog.cpp | 194 ++ desktop-widgets/printdialog.h | 38 + desktop-widgets/printer.cpp | 273 +++ desktop-widgets/printer.h | 48 + desktop-widgets/printoptions.cpp | 189 ++ desktop-widgets/printoptions.h | 88 + desktop-widgets/printoptions.ui | 168 ++ desktop-widgets/profile/animationfunctions.cpp | 75 + desktop-widgets/profile/animationfunctions.h | 18 + desktop-widgets/profile/divecartesianaxis.cpp | 459 ++++ desktop-widgets/profile/divecartesianaxis.h | 122 + desktop-widgets/profile/diveeventitem.cpp | 172 ++ desktop-widgets/profile/diveeventitem.h | 34 + desktop-widgets/profile/divelineitem.cpp | 5 + desktop-widgets/profile/divelineitem.h | 15 + desktop-widgets/profile/divepixmapitem.cpp | 130 + desktop-widgets/profile/divepixmapitem.h | 57 + desktop-widgets/profile/diveprofileitem.cpp | 979 ++++++++ desktop-widgets/profile/diveprofileitem.h | 225 ++ desktop-widgets/profile/diverectitem.cpp | 5 + desktop-widgets/profile/diverectitem.h | 17 + desktop-widgets/profile/divetextitem.cpp | 113 + desktop-widgets/profile/divetextitem.h | 38 + desktop-widgets/profile/divetooltipitem.cpp | 285 +++ desktop-widgets/profile/divetooltipitem.h | 67 + desktop-widgets/profile/profilewidget2.cpp | 1836 ++++++++++++++ desktop-widgets/profile/profilewidget2.h | 211 ++ desktop-widgets/profile/ruleritem.cpp | 179 ++ desktop-widgets/profile/ruleritem.h | 59 + desktop-widgets/profile/tankitem.cpp | 120 + desktop-widgets/profile/tankitem.h | 39 + desktop-widgets/qtwaitingspinner.cpp | 288 +++ desktop-widgets/qtwaitingspinner.h | 103 + desktop-widgets/renumber.ui | 120 + desktop-widgets/searchbar.ui | 134 ++ desktop-widgets/setpoint.ui | 130 + desktop-widgets/shiftimagetimes.ui | 293 +++ desktop-widgets/shifttimes.ui | 214 ++ desktop-widgets/simplewidgets.cpp | 736 ++++++ desktop-widgets/simplewidgets.h | 237 ++ desktop-widgets/socialnetworks.cpp | 326 +++ desktop-widgets/socialnetworks.h | 49 + desktop-widgets/socialnetworksdialog.ui | 184 ++ desktop-widgets/starwidget.cpp | 164 ++ desktop-widgets/starwidget.h | 44 + desktop-widgets/statistics/monthstatistics.cpp | 0 desktop-widgets/statistics/monthstatistics.h | 0 desktop-widgets/statistics/statisticsbar.cpp | 0 desktop-widgets/statistics/statisticsbar.h | 0 desktop-widgets/statistics/statisticswidget.cpp | 41 + desktop-widgets/statistics/statisticswidget.h | 23 + desktop-widgets/statistics/yearstatistics.cpp | 0 desktop-widgets/statistics/yearstatistics.h | 0 desktop-widgets/subsurfacewebservices.cpp | 1121 +++++++++ desktop-widgets/subsurfacewebservices.h | 142 ++ desktop-widgets/tableview.cpp | 145 ++ desktop-widgets/tableview.h | 54 + desktop-widgets/tableview.ui | 27 + desktop-widgets/tagwidget.cpp | 210 ++ desktop-widgets/tagwidget.h | 34 + desktop-widgets/templateedit.cpp | 227 ++ desktop-widgets/templateedit.h | 44 + desktop-widgets/templateedit.ui | 577 +++++ desktop-widgets/templatelayout.cpp | 182 ++ desktop-widgets/templatelayout.h | 168 ++ desktop-widgets/undocommands.cpp | 156 ++ desktop-widgets/undocommands.h | 50 + desktop-widgets/updatemanager.cpp | 154 ++ desktop-widgets/updatemanager.h | 24 + desktop-widgets/urldialog.ui | 91 + desktop-widgets/usermanual.cpp | 151 ++ desktop-widgets/usermanual.h | 50 + desktop-widgets/usersurvey.cpp | 133 ++ desktop-widgets/usersurvey.h | 30 + desktop-widgets/usersurvey.ui | 301 +++ desktop-widgets/webservices.ui | 156 ++ main.cpp | 6 +- qt-gui.cpp | 2 +- qt-models/diveplotdatamodel.cpp | 2 +- qt-models/filtermodels.cpp | 4 +- qt-ui/CMakeLists.txt | 113 - qt-ui/about.cpp | 40 - qt-ui/about.h | 21 - qt-ui/about.ui | 136 -- qt-ui/btdeviceselectiondialog.cpp | 656 ----- qt-ui/btdeviceselectiondialog.h | 91 - qt-ui/btdeviceselectiondialog.ui | 224 -- qt-ui/configuredivecomputerdialog.cpp | 1257 ---------- qt-ui/configuredivecomputerdialog.h | 149 -- qt-ui/configuredivecomputerdialog.ui | 2758 ---------------------- qt-ui/css/tableviews.css | 0 qt-ui/divecomponentselection.ui | 190 -- qt-ui/divecomputermanagementdialog.cpp | 69 - qt-ui/divecomputermanagementdialog.h | 29 - qt-ui/divecomputermanagementdialog.ui | 81 - qt-ui/divelistview.cpp | 1035 -------- qt-ui/divelistview.h | 89 - qt-ui/divelogexportdialog.cpp | 240 -- qt-ui/divelogexportdialog.h | 39 - qt-ui/divelogexportdialog.ui | 606 ----- qt-ui/divelogimportdialog.cpp | 861 ------- qt-ui/divelogimportdialog.h | 131 - qt-ui/divelogimportdialog.ui | 249 -- qt-ui/divepicturewidget.cpp | 100 - qt-ui/divepicturewidget.h | 36 - qt-ui/diveplanner.cpp | 513 ---- qt-ui/diveplanner.h | 104 - qt-ui/diveplanner.ui | 257 -- qt-ui/diveshareexportdialog.cpp | 141 -- qt-ui/diveshareexportdialog.h | 34 - qt-ui/diveshareexportdialog.ui | 291 --- qt-ui/downloadfromdivecomputer.cpp | 727 ------ qt-ui/downloadfromdivecomputer.h | 125 - qt-ui/downloadfromdivecomputer.ui | 301 --- qt-ui/filterwidget.ui | 140 -- qt-ui/globe.cpp | 431 ---- qt-ui/globe.h | 84 - qt-ui/graphicsview-common.cpp | 88 - qt-ui/graphicsview-common.h | 94 - qt-ui/groupedlineedit.cpp | 197 -- qt-ui/groupedlineedit.h | 72 - qt-ui/kmessagewidget.cpp | 480 ---- qt-ui/kmessagewidget.h | 342 --- qt-ui/listfilter.ui | 63 - qt-ui/locationInformation.ui | 156 -- qt-ui/locationinformation.cpp | 618 ----- qt-ui/locationinformation.h | 114 - qt-ui/maintab.cpp | 1612 ------------- qt-ui/maintab.h | 129 - qt-ui/maintab.ui | 1267 ---------- qt-ui/mainwindow.cpp | 1922 --------------- qt-ui/mainwindow.h | 258 -- qt-ui/mainwindow.ui | 761 ------ qt-ui/marble/GeoDataTreeModel.h | 118 - qt-ui/metrics.cpp | 50 - qt-ui/metrics.h | 32 - qt-ui/modeldelegates.cpp | 617 ----- qt-ui/modeldelegates.h | 141 -- qt-ui/notificationwidget.cpp | 42 - qt-ui/notificationwidget.h | 32 - qt-ui/plannerDetails.ui | 104 - qt-ui/plannerSettings.ui | 749 ------ qt-ui/preferences.cpp | 559 ----- qt-ui/preferences.h | 54 - qt-ui/preferences.ui | 1883 --------------- qt-ui/printdialog.cpp | 194 -- qt-ui/printdialog.h | 38 - qt-ui/printer.cpp | 273 --- qt-ui/printer.h | 48 - qt-ui/printoptions.cpp | 189 -- qt-ui/printoptions.h | 88 - qt-ui/printoptions.ui | 168 -- qt-ui/profile/animationfunctions.cpp | 75 - qt-ui/profile/animationfunctions.h | 18 - qt-ui/profile/divecartesianaxis.cpp | 459 ---- qt-ui/profile/divecartesianaxis.h | 122 - qt-ui/profile/diveeventitem.cpp | 172 -- qt-ui/profile/diveeventitem.h | 34 - qt-ui/profile/divelineitem.cpp | 5 - qt-ui/profile/divelineitem.h | 15 - qt-ui/profile/divepixmapitem.cpp | 130 - qt-ui/profile/divepixmapitem.h | 57 - qt-ui/profile/diveprofileitem.cpp | 979 -------- qt-ui/profile/diveprofileitem.h | 226 -- qt-ui/profile/diverectitem.cpp | 5 - qt-ui/profile/diverectitem.h | 17 - qt-ui/profile/divetextitem.cpp | 110 - qt-ui/profile/divetextitem.h | 38 - qt-ui/profile/divetooltipitem.cpp | 285 --- qt-ui/profile/divetooltipitem.h | 67 - qt-ui/profile/profilewidget2.cpp | 1836 -------------- qt-ui/profile/profilewidget2.h | 212 -- qt-ui/profile/ruleritem.cpp | 179 -- qt-ui/profile/ruleritem.h | 59 - qt-ui/profile/tankitem.cpp | 120 - qt-ui/profile/tankitem.h | 39 - qt-ui/qtwaitingspinner.cpp | 288 --- qt-ui/qtwaitingspinner.h | 103 - qt-ui/renumber.ui | 120 - qt-ui/searchbar.ui | 134 -- qt-ui/setpoint.ui | 130 - qt-ui/shiftimagetimes.ui | 293 --- qt-ui/shifttimes.ui | 214 -- qt-ui/simplewidgets.cpp | 736 ------ qt-ui/simplewidgets.h | 237 -- qt-ui/socialnetworks.cpp | 326 --- qt-ui/socialnetworks.h | 49 - qt-ui/socialnetworksdialog.ui | 184 -- qt-ui/starwidget.cpp | 164 -- qt-ui/starwidget.h | 44 - qt-ui/statistics/monthstatistics.cpp | 0 qt-ui/statistics/monthstatistics.h | 0 qt-ui/statistics/statisticsbar.cpp | 0 qt-ui/statistics/statisticsbar.h | 0 qt-ui/statistics/statisticswidget.cpp | 41 - qt-ui/statistics/statisticswidget.h | 23 - qt-ui/statistics/yearstatistics.cpp | 0 qt-ui/statistics/yearstatistics.h | 0 qt-ui/subsurfacewebservices.cpp | 1121 --------- qt-ui/subsurfacewebservices.h | 142 -- qt-ui/tableview.cpp | 145 -- qt-ui/tableview.h | 54 - qt-ui/tableview.ui | 27 - qt-ui/tagwidget.cpp | 210 -- qt-ui/tagwidget.h | 34 - qt-ui/templateedit.cpp | 227 -- qt-ui/templateedit.h | 44 - qt-ui/templateedit.ui | 577 ----- qt-ui/templatelayout.cpp | 182 -- qt-ui/templatelayout.h | 168 -- qt-ui/undocommands.cpp | 156 -- qt-ui/undocommands.h | 50 - qt-ui/updatemanager.cpp | 154 -- qt-ui/updatemanager.h | 24 - qt-ui/urldialog.ui | 91 - qt-ui/usermanual.cpp | 151 -- qt-ui/usermanual.h | 50 - qt-ui/usersurvey.cpp | 133 -- qt-ui/usersurvey.h | 30 - qt-ui/usersurvey.ui | 301 --- qt-ui/webservices.ui | 156 -- subsurface-core/CMakeLists.txt | 2 + subsurface-core/color.cpp | 88 + subsurface-core/color.h | 85 + subsurface-core/metrics.cpp | 50 + subsurface-core/metrics.h | 32 + 288 files changed, 38477 insertions(+), 38482 deletions(-) create mode 100644 desktop-widgets/CMakeLists.txt create mode 100644 desktop-widgets/about.cpp create mode 100644 desktop-widgets/about.h create mode 100644 desktop-widgets/about.ui create mode 100644 desktop-widgets/btdeviceselectiondialog.cpp create mode 100644 desktop-widgets/btdeviceselectiondialog.h create mode 100644 desktop-widgets/btdeviceselectiondialog.ui create mode 100644 desktop-widgets/configuredivecomputerdialog.cpp create mode 100644 desktop-widgets/configuredivecomputerdialog.h create mode 100644 desktop-widgets/configuredivecomputerdialog.ui create mode 100644 desktop-widgets/css/tableviews.css create mode 100644 desktop-widgets/divecomponentselection.ui create mode 100644 desktop-widgets/divecomputermanagementdialog.cpp create mode 100644 desktop-widgets/divecomputermanagementdialog.h create mode 100644 desktop-widgets/divecomputermanagementdialog.ui create mode 100644 desktop-widgets/divelistview.cpp create mode 100644 desktop-widgets/divelistview.h create mode 100644 desktop-widgets/divelogexportdialog.cpp create mode 100644 desktop-widgets/divelogexportdialog.h create mode 100644 desktop-widgets/divelogexportdialog.ui create mode 100644 desktop-widgets/divelogimportdialog.cpp create mode 100644 desktop-widgets/divelogimportdialog.h create mode 100644 desktop-widgets/divelogimportdialog.ui create mode 100644 desktop-widgets/divepicturewidget.cpp create mode 100644 desktop-widgets/divepicturewidget.h create mode 100644 desktop-widgets/diveplanner.cpp create mode 100644 desktop-widgets/diveplanner.h create mode 100644 desktop-widgets/diveplanner.ui create mode 100644 desktop-widgets/diveshareexportdialog.cpp create mode 100644 desktop-widgets/diveshareexportdialog.h create mode 100644 desktop-widgets/diveshareexportdialog.ui create mode 100644 desktop-widgets/downloadfromdivecomputer.cpp create mode 100644 desktop-widgets/downloadfromdivecomputer.h create mode 100644 desktop-widgets/downloadfromdivecomputer.ui create mode 100644 desktop-widgets/filterwidget.ui create mode 100644 desktop-widgets/globe.cpp create mode 100644 desktop-widgets/globe.h create mode 100644 desktop-widgets/groupedlineedit.cpp create mode 100644 desktop-widgets/groupedlineedit.h create mode 100644 desktop-widgets/kmessagewidget.cpp create mode 100644 desktop-widgets/kmessagewidget.h create mode 100644 desktop-widgets/listfilter.ui create mode 100644 desktop-widgets/locationInformation.ui create mode 100644 desktop-widgets/locationinformation.cpp create mode 100644 desktop-widgets/locationinformation.h create mode 100644 desktop-widgets/maintab.cpp create mode 100644 desktop-widgets/maintab.h create mode 100644 desktop-widgets/maintab.ui create mode 100644 desktop-widgets/mainwindow.cpp create mode 100644 desktop-widgets/mainwindow.h create mode 100644 desktop-widgets/mainwindow.ui create mode 100644 desktop-widgets/marble/GeoDataTreeModel.h create mode 100644 desktop-widgets/modeldelegates.cpp create mode 100644 desktop-widgets/modeldelegates.h create mode 100644 desktop-widgets/notificationwidget.cpp create mode 100644 desktop-widgets/notificationwidget.h create mode 100644 desktop-widgets/plannerDetails.ui create mode 100644 desktop-widgets/plannerSettings.ui create mode 100644 desktop-widgets/preferences.cpp create mode 100644 desktop-widgets/preferences.h create mode 100644 desktop-widgets/preferences.ui create mode 100644 desktop-widgets/printdialog.cpp create mode 100644 desktop-widgets/printdialog.h create mode 100644 desktop-widgets/printer.cpp create mode 100644 desktop-widgets/printer.h create mode 100644 desktop-widgets/printoptions.cpp create mode 100644 desktop-widgets/printoptions.h create mode 100644 desktop-widgets/printoptions.ui create mode 100644 desktop-widgets/profile/animationfunctions.cpp create mode 100644 desktop-widgets/profile/animationfunctions.h create mode 100644 desktop-widgets/profile/divecartesianaxis.cpp create mode 100644 desktop-widgets/profile/divecartesianaxis.h create mode 100644 desktop-widgets/profile/diveeventitem.cpp create mode 100644 desktop-widgets/profile/diveeventitem.h create mode 100644 desktop-widgets/profile/divelineitem.cpp create mode 100644 desktop-widgets/profile/divelineitem.h create mode 100644 desktop-widgets/profile/divepixmapitem.cpp create mode 100644 desktop-widgets/profile/divepixmapitem.h create mode 100644 desktop-widgets/profile/diveprofileitem.cpp create mode 100644 desktop-widgets/profile/diveprofileitem.h create mode 100644 desktop-widgets/profile/diverectitem.cpp create mode 100644 desktop-widgets/profile/diverectitem.h create mode 100644 desktop-widgets/profile/divetextitem.cpp create mode 100644 desktop-widgets/profile/divetextitem.h create mode 100644 desktop-widgets/profile/divetooltipitem.cpp create mode 100644 desktop-widgets/profile/divetooltipitem.h create mode 100644 desktop-widgets/profile/profilewidget2.cpp create mode 100644 desktop-widgets/profile/profilewidget2.h create mode 100644 desktop-widgets/profile/ruleritem.cpp create mode 100644 desktop-widgets/profile/ruleritem.h create mode 100644 desktop-widgets/profile/tankitem.cpp create mode 100644 desktop-widgets/profile/tankitem.h create mode 100644 desktop-widgets/qtwaitingspinner.cpp create mode 100644 desktop-widgets/qtwaitingspinner.h create mode 100644 desktop-widgets/renumber.ui create mode 100644 desktop-widgets/searchbar.ui create mode 100644 desktop-widgets/setpoint.ui create mode 100644 desktop-widgets/shiftimagetimes.ui create mode 100644 desktop-widgets/shifttimes.ui create mode 100644 desktop-widgets/simplewidgets.cpp create mode 100644 desktop-widgets/simplewidgets.h create mode 100644 desktop-widgets/socialnetworks.cpp create mode 100644 desktop-widgets/socialnetworks.h create mode 100644 desktop-widgets/socialnetworksdialog.ui create mode 100644 desktop-widgets/starwidget.cpp create mode 100644 desktop-widgets/starwidget.h create mode 100644 desktop-widgets/statistics/monthstatistics.cpp create mode 100644 desktop-widgets/statistics/monthstatistics.h create mode 100644 desktop-widgets/statistics/statisticsbar.cpp create mode 100644 desktop-widgets/statistics/statisticsbar.h create mode 100644 desktop-widgets/statistics/statisticswidget.cpp create mode 100644 desktop-widgets/statistics/statisticswidget.h create mode 100644 desktop-widgets/statistics/yearstatistics.cpp create mode 100644 desktop-widgets/statistics/yearstatistics.h create mode 100644 desktop-widgets/subsurfacewebservices.cpp create mode 100644 desktop-widgets/subsurfacewebservices.h create mode 100644 desktop-widgets/tableview.cpp create mode 100644 desktop-widgets/tableview.h create mode 100644 desktop-widgets/tableview.ui create mode 100644 desktop-widgets/tagwidget.cpp create mode 100644 desktop-widgets/tagwidget.h create mode 100644 desktop-widgets/templateedit.cpp create mode 100644 desktop-widgets/templateedit.h create mode 100644 desktop-widgets/templateedit.ui create mode 100644 desktop-widgets/templatelayout.cpp create mode 100644 desktop-widgets/templatelayout.h create mode 100644 desktop-widgets/undocommands.cpp create mode 100644 desktop-widgets/undocommands.h create mode 100644 desktop-widgets/updatemanager.cpp create mode 100644 desktop-widgets/updatemanager.h create mode 100644 desktop-widgets/urldialog.ui create mode 100644 desktop-widgets/usermanual.cpp create mode 100644 desktop-widgets/usermanual.h create mode 100644 desktop-widgets/usersurvey.cpp create mode 100644 desktop-widgets/usersurvey.h create mode 100644 desktop-widgets/usersurvey.ui create mode 100644 desktop-widgets/webservices.ui delete mode 100644 qt-ui/CMakeLists.txt delete mode 100644 qt-ui/about.cpp delete mode 100644 qt-ui/about.h delete mode 100644 qt-ui/about.ui delete mode 100644 qt-ui/btdeviceselectiondialog.cpp delete mode 100644 qt-ui/btdeviceselectiondialog.h delete mode 100644 qt-ui/btdeviceselectiondialog.ui delete mode 100644 qt-ui/configuredivecomputerdialog.cpp delete mode 100644 qt-ui/configuredivecomputerdialog.h delete mode 100644 qt-ui/configuredivecomputerdialog.ui delete mode 100644 qt-ui/css/tableviews.css delete mode 100644 qt-ui/divecomponentselection.ui delete mode 100644 qt-ui/divecomputermanagementdialog.cpp delete mode 100644 qt-ui/divecomputermanagementdialog.h delete mode 100644 qt-ui/divecomputermanagementdialog.ui delete mode 100644 qt-ui/divelistview.cpp delete mode 100644 qt-ui/divelistview.h delete mode 100644 qt-ui/divelogexportdialog.cpp delete mode 100644 qt-ui/divelogexportdialog.h delete mode 100644 qt-ui/divelogexportdialog.ui delete mode 100644 qt-ui/divelogimportdialog.cpp delete mode 100644 qt-ui/divelogimportdialog.h delete mode 100644 qt-ui/divelogimportdialog.ui delete mode 100644 qt-ui/divepicturewidget.cpp delete mode 100644 qt-ui/divepicturewidget.h delete mode 100644 qt-ui/diveplanner.cpp delete mode 100644 qt-ui/diveplanner.h delete mode 100644 qt-ui/diveplanner.ui delete mode 100644 qt-ui/diveshareexportdialog.cpp delete mode 100644 qt-ui/diveshareexportdialog.h delete mode 100644 qt-ui/diveshareexportdialog.ui delete mode 100644 qt-ui/downloadfromdivecomputer.cpp delete mode 100644 qt-ui/downloadfromdivecomputer.h delete mode 100644 qt-ui/downloadfromdivecomputer.ui delete mode 100644 qt-ui/filterwidget.ui delete mode 100644 qt-ui/globe.cpp delete mode 100644 qt-ui/globe.h delete mode 100644 qt-ui/graphicsview-common.cpp delete mode 100644 qt-ui/graphicsview-common.h delete mode 100644 qt-ui/groupedlineedit.cpp delete mode 100644 qt-ui/groupedlineedit.h delete mode 100644 qt-ui/kmessagewidget.cpp delete mode 100644 qt-ui/kmessagewidget.h delete mode 100644 qt-ui/listfilter.ui delete mode 100644 qt-ui/locationInformation.ui delete mode 100644 qt-ui/locationinformation.cpp delete mode 100644 qt-ui/locationinformation.h delete mode 100644 qt-ui/maintab.cpp delete mode 100644 qt-ui/maintab.h delete mode 100644 qt-ui/maintab.ui delete mode 100644 qt-ui/mainwindow.cpp delete mode 100644 qt-ui/mainwindow.h delete mode 100644 qt-ui/mainwindow.ui delete mode 100644 qt-ui/marble/GeoDataTreeModel.h delete mode 100644 qt-ui/metrics.cpp delete mode 100644 qt-ui/metrics.h delete mode 100644 qt-ui/modeldelegates.cpp delete mode 100644 qt-ui/modeldelegates.h delete mode 100644 qt-ui/notificationwidget.cpp delete mode 100644 qt-ui/notificationwidget.h delete mode 100644 qt-ui/plannerDetails.ui delete mode 100644 qt-ui/plannerSettings.ui delete mode 100644 qt-ui/preferences.cpp delete mode 100644 qt-ui/preferences.h delete mode 100644 qt-ui/preferences.ui delete mode 100644 qt-ui/printdialog.cpp delete mode 100644 qt-ui/printdialog.h delete mode 100644 qt-ui/printer.cpp delete mode 100644 qt-ui/printer.h delete mode 100644 qt-ui/printoptions.cpp delete mode 100644 qt-ui/printoptions.h delete mode 100644 qt-ui/printoptions.ui delete mode 100644 qt-ui/profile/animationfunctions.cpp delete mode 100644 qt-ui/profile/animationfunctions.h delete mode 100644 qt-ui/profile/divecartesianaxis.cpp delete mode 100644 qt-ui/profile/divecartesianaxis.h delete mode 100644 qt-ui/profile/diveeventitem.cpp delete mode 100644 qt-ui/profile/diveeventitem.h delete mode 100644 qt-ui/profile/divelineitem.cpp delete mode 100644 qt-ui/profile/divelineitem.h delete mode 100644 qt-ui/profile/divepixmapitem.cpp delete mode 100644 qt-ui/profile/divepixmapitem.h delete mode 100644 qt-ui/profile/diveprofileitem.cpp delete mode 100644 qt-ui/profile/diveprofileitem.h delete mode 100644 qt-ui/profile/diverectitem.cpp delete mode 100644 qt-ui/profile/diverectitem.h delete mode 100644 qt-ui/profile/divetextitem.cpp delete mode 100644 qt-ui/profile/divetextitem.h delete mode 100644 qt-ui/profile/divetooltipitem.cpp delete mode 100644 qt-ui/profile/divetooltipitem.h delete mode 100644 qt-ui/profile/profilewidget2.cpp delete mode 100644 qt-ui/profile/profilewidget2.h delete mode 100644 qt-ui/profile/ruleritem.cpp delete mode 100644 qt-ui/profile/ruleritem.h delete mode 100644 qt-ui/profile/tankitem.cpp delete mode 100644 qt-ui/profile/tankitem.h delete mode 100644 qt-ui/qtwaitingspinner.cpp delete mode 100644 qt-ui/qtwaitingspinner.h delete mode 100644 qt-ui/renumber.ui delete mode 100644 qt-ui/searchbar.ui delete mode 100644 qt-ui/setpoint.ui delete mode 100644 qt-ui/shiftimagetimes.ui delete mode 100644 qt-ui/shifttimes.ui delete mode 100644 qt-ui/simplewidgets.cpp delete mode 100644 qt-ui/simplewidgets.h delete mode 100644 qt-ui/socialnetworks.cpp delete mode 100644 qt-ui/socialnetworks.h delete mode 100644 qt-ui/socialnetworksdialog.ui delete mode 100644 qt-ui/starwidget.cpp delete mode 100644 qt-ui/starwidget.h delete mode 100644 qt-ui/statistics/monthstatistics.cpp delete mode 100644 qt-ui/statistics/monthstatistics.h delete mode 100644 qt-ui/statistics/statisticsbar.cpp delete mode 100644 qt-ui/statistics/statisticsbar.h delete mode 100644 qt-ui/statistics/statisticswidget.cpp delete mode 100644 qt-ui/statistics/statisticswidget.h delete mode 100644 qt-ui/statistics/yearstatistics.cpp delete mode 100644 qt-ui/statistics/yearstatistics.h delete mode 100644 qt-ui/subsurfacewebservices.cpp delete mode 100644 qt-ui/subsurfacewebservices.h delete mode 100644 qt-ui/tableview.cpp delete mode 100644 qt-ui/tableview.h delete mode 100644 qt-ui/tableview.ui delete mode 100644 qt-ui/tagwidget.cpp delete mode 100644 qt-ui/tagwidget.h delete mode 100644 qt-ui/templateedit.cpp delete mode 100644 qt-ui/templateedit.h delete mode 100644 qt-ui/templateedit.ui delete mode 100644 qt-ui/templatelayout.cpp delete mode 100644 qt-ui/templatelayout.h delete mode 100644 qt-ui/undocommands.cpp delete mode 100644 qt-ui/undocommands.h delete mode 100644 qt-ui/updatemanager.cpp delete mode 100644 qt-ui/updatemanager.h delete mode 100644 qt-ui/urldialog.ui delete mode 100644 qt-ui/usermanual.cpp delete mode 100644 qt-ui/usermanual.h delete mode 100644 qt-ui/usersurvey.cpp delete mode 100644 qt-ui/usersurvey.h delete mode 100644 qt-ui/usersurvey.ui delete mode 100644 qt-ui/webservices.ui create mode 100644 subsurface-core/color.cpp create mode 100644 subsurface-core/metrics.cpp create mode 100644 subsurface-core/metrics.h (limited to 'qt-ui') diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e4e5f53d..74b9d0d94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,10 +37,10 @@ set(CMAKE_MODULE_PATH include_directories(. ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_BINARY_DIR} - ${CMAKE_BINARY_DIR}/qt-ui - qt-ui + ${CMAKE_BINARY_DIR}/desktop-widgets + desktop-widgets/ qt-models - qt-ui/profile + desktop-widgets/profile subsurface-core/ ) @@ -362,7 +362,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT ANDROID) set(SUBSURFACE_LINK_LIBRARIES ${SUBSURFACE_LINK_LIBRARIES} -lpthread) endif() -add_subdirectory(qt-ui) +add_subdirectory(desktop-widgets) # create the executables if(SUBSURFACE_MOBILE) diff --git a/desktop-widgets/CMakeLists.txt b/desktop-widgets/CMakeLists.txt new file mode 100644 index 000000000..2c373b83f --- /dev/null +++ b/desktop-widgets/CMakeLists.txt @@ -0,0 +1,111 @@ +# create the libraries +file(GLOB SUBSURFACE_UI *.ui) +qt5_wrap_ui(SUBSURFACE_UI_HDRS ${SUBSURFACE_UI}) +qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc) +source_group("Subsurface Interface Files" FILES ${SUBSURFACE_UI}) + +if(BTSUPPORT) + set(BT_SRC_FILES btdeviceselectiondialog.cpp) +endif() + +include_directories(. + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_BINARY_DIR} +) + +# the interface, in C++ +set(SUBSURFACE_INTERFACE + updatemanager.cpp + about.cpp + divecomputermanagementdialog.cpp + divelistview.cpp + diveplanner.cpp + diveshareexportdialog.cpp + downloadfromdivecomputer.cpp + globe.cpp + kmessagewidget.cpp + maintab.cpp + mainwindow.cpp + modeldelegates.cpp + notificationwidget.cpp + preferences.cpp + simplewidgets.cpp + starwidget.cpp + subsurfacewebservices.cpp + tableview.cpp + divelogimportdialog.cpp + tagwidget.cpp + groupedlineedit.cpp + divelogexportdialog.cpp + divepicturewidget.cpp + usersurvey.cpp + configuredivecomputerdialog.cpp + undocommands.cpp + locationinformation.cpp + qtwaitingspinner.cpp +) + +if(NOT NO_USERMANUAL) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + usermanual.cpp + ) +endif() + +if(NOT NO_PRINTING) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + templateedit.cpp + printdialog.cpp + printoptions.cpp + printer.cpp + templatelayout.cpp + ) +endif() + +if (FBSUPPORT) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + socialnetworks.cpp + ) +endif() + +if (BTSUPPORT) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + btdeviceselectiondialog.cpp + ) +endif() + +source_group("Subsurface Interface" FILES ${SUBSURFACE_INTERFACE}) + +# the profile widget +set(SUBSURFACE_PROFILE_LIB_SRCS + profile/profilewidget2.cpp + profile/diverectitem.cpp + profile/divepixmapitem.cpp + profile/divelineitem.cpp + profile/divetextitem.cpp + profile/animationfunctions.cpp + profile/divecartesianaxis.cpp + profile/diveprofileitem.cpp + profile/diveeventitem.cpp + profile/divetooltipitem.cpp + profile/ruleritem.cpp + profile/tankitem.cpp +) +source_group("Subsurface Profile" FILES ${SUBSURFACE_PROFILE_LIB_SRCS}) + +# the yearly statistics widget. +set(SUBSURFACE_STATISTICS_LIB_SRCS + statistics/statisticswidget.cpp + statistics/yearstatistics.cpp + statistics/statisticsbar.cpp + statistics/monthstatistics.cpp +) +source_group("Subsurface Statistics" FILES ${SUBSURFACE_STATISTICS_LIB_SRCS}) + +add_library(subsurface_profile STATIC ${SUBSURFACE_PROFILE_LIB_SRCS}) +target_link_libraries(subsurface_profile ${QT_LIBRARIES}) +add_library(subsurface_statistics STATIC ${SUBSURFACE_STATISTICS_LIB_SRCS}) +target_link_libraries(subsurface_statistics ${QT_LIBRARIES}) +add_library(subsurface_generated_ui STATIC ${SUBSURFACE_UI_HDRS}) +target_link_libraries(subsurface_generated_ui ${QT_LIBRARIES}) +add_library(subsurface_interface STATIC ${SUBSURFACE_INTERFACE}) +target_link_libraries(subsurface_interface ${QT_LIBRARIES} ${MARBLE_LIBRARIES}) diff --git a/desktop-widgets/about.cpp b/desktop-widgets/about.cpp new file mode 100644 index 000000000..e0df55980 --- /dev/null +++ b/desktop-widgets/about.cpp @@ -0,0 +1,40 @@ +#include "about.h" +#include "version.h" +#include +#include +#include + +SubsurfaceAbout::SubsurfaceAbout(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + ui.setupUi(this); + + setWindowModality(Qt::ApplicationModal); + QString versionString(subsurface_git_version()); + QStringList readableVersions = QStringList() << "4.4.96" << "4.5 Beta 1" << + "4.4.97" << "4.5 Beta 2" << + "4.4.98" << "4.5 Beta 3"; + if (readableVersions.contains(versionString)) + versionString = readableVersions[readableVersions.indexOf(versionString) + 1]; + + ui.aboutLabel->setText(tr("" + "Subsurface %1

" + "Multi-platform divelog software
" + "" + "Linus Torvalds, Dirk Hohndel, Tomaz Canabrava, and others, 2011-2015" + "").arg(versionString)); + + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void SubsurfaceAbout::on_licenseButton_clicked() +{ + QDesktopServices::openUrl(QUrl("http://www.gnu.org/licenses/gpl-2.0.txt")); +} + +void SubsurfaceAbout::on_websiteButton_clicked() +{ + QDesktopServices::openUrl(QUrl("http://subsurface-divelog.org")); +} diff --git a/desktop-widgets/about.h b/desktop-widgets/about.h new file mode 100644 index 000000000..47423aea2 --- /dev/null +++ b/desktop-widgets/about.h @@ -0,0 +1,21 @@ +#ifndef ABOUT_H +#define ABOUT_H + +#include +#include "ui_about.h" + +class SubsurfaceAbout : public QDialog { + Q_OBJECT + +public: + explicit SubsurfaceAbout(QWidget *parent = 0, Qt::WindowFlags f = 0); +private +slots: + void on_licenseButton_clicked(); + void on_websiteButton_clicked(); + +private: + Ui::SubsurfaceAbout ui; +}; + +#endif // ABOUT_H diff --git a/desktop-widgets/about.ui b/desktop-widgets/about.ui new file mode 100644 index 000000000..0c1735e26 --- /dev/null +++ b/desktop-widgets/about.ui @@ -0,0 +1,136 @@ + + + SubsurfaceAbout + + + Qt::WindowModal + + + + 0 + 0 + 359 + 423 + + + + + 0 + 0 + + + + About Subsurface + + + + :/subsurface-icon:/subsurface-icon + + + true + + + + + + + + + :/subsurface-icon + + + Qt::AlignCenter + + + + + + + + + + Qt::RichText + + + Qt::AlignCenter + + + 10 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &License + + + + + + + &Website + + + + + + + &Close + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + closeButton + clicked() + SubsurfaceAbout + accept() + + + 290 + 411 + + + 340 + 409 + + + + + diff --git a/desktop-widgets/btdeviceselectiondialog.cpp b/desktop-widgets/btdeviceselectiondialog.cpp new file mode 100644 index 000000000..fb2cbc1f3 --- /dev/null +++ b/desktop-widgets/btdeviceselectiondialog.cpp @@ -0,0 +1,656 @@ +#include +#include +#include +#include + +#include "ui_btdeviceselectiondialog.h" +#include "btdeviceselectiondialog.h" + +#if defined(Q_OS_WIN) +Q_DECLARE_METATYPE(QBluetoothDeviceDiscoveryAgent::Error) +#endif +#if QT_VERSION < 0x050500 +Q_DECLARE_METATYPE(QBluetoothDeviceInfo) +#endif + +BtDeviceSelectionDialog::BtDeviceSelectionDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::BtDeviceSelectionDialog), + remoteDeviceDiscoveryAgent(0) +{ + ui->setupUi(this); + + // Quit button callbacks + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), this, SLOT(reject())); + connect(ui->quit, SIGNAL(clicked()), this, SLOT(reject())); + + // Translate the UI labels + ui->localDeviceDetails->setTitle(tr("Local Bluetooth device details")); + ui->selectDeviceLabel->setText(tr("Select device:")); + ui->deviceAddressLabel->setText(tr("Address:")); + ui->deviceNameLabel->setText(tr("Name:")); + ui->deviceState->setText(tr("Bluetooth powered on")); + ui->changeDeviceState->setText(tr("Turn on/off")); + ui->discoveredDevicesLabel->setText(tr("Discovered devices")); + ui->scan->setText(tr("Scan")); + ui->clear->setText(tr("Clear")); + ui->save->setText(tr("Save")); + ui->quit->setText(tr("Quit")); + + // Disable the save button because there is no device selected + ui->save->setEnabled(false); + + // Add event for item selection + connect(ui->discoveredDevicesList, SIGNAL(itemClicked(QListWidgetItem*)), + this, SLOT(itemClicked(QListWidgetItem*))); + +#if defined(Q_OS_WIN) + ULONG ulRetCode = SUCCESS; + WSADATA WSAData = { 0 }; + + // Initialize WinSock and ask for version 2.2. + ulRetCode = WSAStartup(MAKEWORD(2, 2), &WSAData); + if (ulRetCode != SUCCESS) { + QMessageBox::StandardButton warningBox; + warningBox = QMessageBox::critical(this, "Bluetooth", + tr("Could not initialize Winsock version 2.2"), QMessageBox::Ok); + return; + } + + // Initialize the device discovery agent + initializeDeviceDiscoveryAgent(); + + // On Windows we cannot select a device or show information about the local device + ui->localDeviceDetails->hide(); +#else + // Initialize the local Bluetooth device + localDevice = new QBluetoothLocalDevice(); + + // Populate the list with local bluetooth devices + QList localAvailableDevices = localDevice->allDevices(); + int availableDevicesSize = localAvailableDevices.size(); + + if (availableDevicesSize > 1) { + int defaultDeviceIndex = -1; + + for (int it = 0; it < availableDevicesSize; it++) { + QBluetoothHostInfo localAvailableDevice = localAvailableDevices.at(it); + ui->localSelectedDevice->addItem(localAvailableDevice.name(), + QVariant::fromValue(localAvailableDevice.address())); + + if (localDevice->address() == localAvailableDevice.address()) + defaultDeviceIndex = it; + } + + // Positionate the current index to the default device and register to index changes events + ui->localSelectedDevice->setCurrentIndex(defaultDeviceIndex); + connect(ui->localSelectedDevice, SIGNAL(currentIndexChanged(int)), + this, SLOT(localDeviceChanged(int))); + } else { + // If there is only one local Bluetooth adapter hide the combobox and the label + ui->selectDeviceLabel->hide(); + ui->localSelectedDevice->hide(); + } + + // Update the UI information about the local device + updateLocalDeviceInformation(); + + // Initialize the device discovery agent + if (localDevice->isValid()) + initializeDeviceDiscoveryAgent(); +#endif +} + +BtDeviceSelectionDialog::~BtDeviceSelectionDialog() +{ + delete ui; + +#if defined(Q_OS_WIN) + // Terminate the use of Winsock 2 DLL + WSACleanup(); +#else + // Clean the local device + delete localDevice; +#endif + if (remoteDeviceDiscoveryAgent) { + // Clean the device discovery agent + if (remoteDeviceDiscoveryAgent->isActive()) { + remoteDeviceDiscoveryAgent->stop(); +#if defined(Q_OS_WIN) + remoteDeviceDiscoveryAgent->wait(); +#endif + } + + delete remoteDeviceDiscoveryAgent; + } +} + +void BtDeviceSelectionDialog::on_changeDeviceState_clicked() +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + if (localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) { + ui->dialogStatus->setText(tr("Trying to turn on the local Bluetooth device...")); + localDevice->powerOn(); + } else { + ui->dialogStatus->setText(tr("Trying to turn off the local Bluetooth device...")); + localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); + } +#endif +} + +void BtDeviceSelectionDialog::on_save_clicked() +{ + // Get the selected device. There will be always a selected device if the save button is enabled. + QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); + QBluetoothDeviceInfo remoteDeviceInfo = currentItem->data(Qt::UserRole).value(); + + // Save the selected device + selectedRemoteDeviceInfo = QSharedPointer(new QBluetoothDeviceInfo(remoteDeviceInfo)); + + if (remoteDeviceDiscoveryAgent->isActive()) { + // Stop the SDP agent if the clear button is pressed and enable the Scan button + remoteDeviceDiscoveryAgent->stop(); +#if defined(Q_OS_WIN) + remoteDeviceDiscoveryAgent->wait(); +#endif + ui->scan->setEnabled(true); + } + + // Close the device selection dialog and set the result code to Accepted + accept(); +} + +void BtDeviceSelectionDialog::on_clear_clicked() +{ + ui->dialogStatus->setText(tr("Remote devices list was cleared.")); + ui->discoveredDevicesList->clear(); + ui->save->setEnabled(false); + + if (remoteDeviceDiscoveryAgent->isActive()) { + // Stop the SDP agent if the clear button is pressed and enable the Scan button + remoteDeviceDiscoveryAgent->stop(); +#if defined(Q_OS_WIN) + remoteDeviceDiscoveryAgent->wait(); +#endif + ui->scan->setEnabled(true); + } +} + +void BtDeviceSelectionDialog::on_scan_clicked() +{ + ui->dialogStatus->setText(tr("Scanning for remote devices...")); + ui->discoveredDevicesList->clear(); + remoteDeviceDiscoveryAgent->start(); + ui->scan->setEnabled(false); +} + +void BtDeviceSelectionDialog::remoteDeviceScanFinished() +{ + if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::NoError) { + ui->dialogStatus->setText(tr("Scanning finished successfully.")); + } else { + deviceDiscoveryError(remoteDeviceDiscoveryAgent->error()); + } + + ui->scan->setEnabled(true); +} + +void BtDeviceSelectionDialog::hostModeStateChanged(QBluetoothLocalDevice::HostMode mode) +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + bool on = !(mode == QBluetoothLocalDevice::HostPoweredOff); + + //: %1 will be replaced with "turned on" or "turned off" + ui->dialogStatus->setText(tr("The local Bluetooth device was %1.") + .arg(on? tr("turned on") : tr("turned off"))); + ui->deviceState->setChecked(on); + ui->scan->setEnabled(on); +#endif +} + +void BtDeviceSelectionDialog::addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo) +{ +#if defined(Q_OS_WIN) + // On Windows we cannot obtain the pairing status so we set only the name and the address of the device + QString deviceLabel = QString("%1 (%2)").arg(remoteDeviceInfo.name(), + remoteDeviceInfo.address().toString()); + QColor pairingColor = QColor(Qt::white); +#else + // By default we use the status label and the color for the UNPAIRED state + QColor pairingColor = QColor(Qt::red); + QString pairingStatusLabel = tr("UNPAIRED"); + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); + + if (pairingStatus == QBluetoothLocalDevice::Paired) { + pairingStatusLabel = tr("PAIRED"); + pairingColor = QColor(Qt::gray); + } else if (pairingStatus == QBluetoothLocalDevice::AuthorizedPaired) { + pairingStatusLabel = tr("AUTHORIZED_PAIRED"); + pairingColor = QColor(Qt::blue); + } + + QString deviceLabel = tr("%1 (%2) [State: %3]").arg(remoteDeviceInfo.name(), + remoteDeviceInfo.address().toString(), + pairingStatusLabel); +#endif + // Create the new item, set its information and add it to the list + QListWidgetItem *item = new QListWidgetItem(deviceLabel); + + item->setData(Qt::UserRole, QVariant::fromValue(remoteDeviceInfo)); + item->setBackgroundColor(pairingColor); + + ui->discoveredDevicesList->addItem(item); +} + +void BtDeviceSelectionDialog::itemClicked(QListWidgetItem *item) +{ + // By default we assume that the devices are paired + QBluetoothDeviceInfo remoteDeviceInfo = item->data(Qt::UserRole).value(); + QString statusMessage = tr("The device %1 can be used for connection. You can press the Save button.") + .arg(remoteDeviceInfo.address().toString()); + bool enableSaveButton = true; + +#if !defined(Q_OS_WIN) + // On other platforms than Windows we can obtain the pairing status so if the devices are not paired we disable the button + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); + + if (pairingStatus == QBluetoothLocalDevice::Unpaired) { + statusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") + .arg(remoteDeviceInfo.address().toString()); + enableSaveButton = false; + } +#endif + // Update the status message and the save button + ui->dialogStatus->setText(statusMessage); + ui->save->setEnabled(enableSaveButton); +} + +void BtDeviceSelectionDialog::localDeviceChanged(int index) +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + QBluetoothAddress localDeviceSelectedAddress = ui->localSelectedDevice->itemData(index, Qt::UserRole).value(); + + // Delete the old localDevice + if (localDevice) + delete localDevice; + + // Create a new local device using the selected address + localDevice = new QBluetoothLocalDevice(localDeviceSelectedAddress); + + ui->dialogStatus->setText(tr("The local device was changed.")); + + // Clear the discovered devices list + on_clear_clicked(); + + // Update the UI information about the local device + updateLocalDeviceInformation(); + + // Initialize the device discovery agent + if (localDevice->isValid()) + initializeDeviceDiscoveryAgent(); +#endif +} + +void BtDeviceSelectionDialog::displayPairingMenu(const QPoint &pos) +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + QMenu menu(this); + QAction *pairAction = menu.addAction(tr("Pair")); + QAction *removePairAction = menu.addAction(tr("Remove pairing")); + QAction *chosenAction = menu.exec(ui->discoveredDevicesList->viewport()->mapToGlobal(pos)); + QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); + QBluetoothDeviceInfo currentRemoteDeviceInfo = currentItem->data(Qt::UserRole).value(); + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(currentRemoteDeviceInfo.address()); + + //TODO: disable the actions + if (pairingStatus == QBluetoothLocalDevice::Unpaired) { + pairAction->setEnabled(true); + removePairAction->setEnabled(false); + } else { + pairAction->setEnabled(false); + removePairAction->setEnabled(true); + } + + if (chosenAction == pairAction) { + ui->dialogStatus->setText(tr("Trying to pair device %1") + .arg(currentRemoteDeviceInfo.address().toString())); + localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Paired); + } else if (chosenAction == removePairAction) { + ui->dialogStatus->setText(tr("Trying to unpair device %1") + .arg(currentRemoteDeviceInfo.address().toString())); + localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Unpaired); + } +#endif +} + +void BtDeviceSelectionDialog::pairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) +{ + // Determine the color, the new pairing status and the log message. By default we assume that the devices are UNPAIRED. + QString remoteDeviceStringAddress = address.toString(); + QColor pairingColor = QColor(Qt::red); + QString pairingStatusLabel = tr("UNPAIRED"); + QString dialogStatusMessage = tr("Device %1 was unpaired.").arg(remoteDeviceStringAddress); + bool enableSaveButton = false; + + if (pairing == QBluetoothLocalDevice::Paired) { + pairingStatusLabel = tr("PAIRED"); + pairingColor = QColor(Qt::gray); + enableSaveButton = true; + dialogStatusMessage = tr("Device %1 was paired.").arg(remoteDeviceStringAddress); + } else if (pairing == QBluetoothLocalDevice::AuthorizedPaired) { + pairingStatusLabel = tr("AUTHORIZED_PAIRED"); + pairingColor = QColor(Qt::blue); + enableSaveButton = true; + dialogStatusMessage = tr("Device %1 was paired and is authorized.").arg(remoteDeviceStringAddress); + } + + // Find the items which represent the BTH device and update their state + QList items = ui->discoveredDevicesList->findItems(remoteDeviceStringAddress, Qt::MatchContains); + QRegularExpression pairingExpression = QRegularExpression(QString("%1|%2|%3").arg(tr("PAIRED"), + tr("AUTHORIZED_PAIRED"), + tr("UNPAIRED"))); + + for (int i = 0; i < items.count(); ++i) { + QListWidgetItem *item = items.at(i); + QString updatedDeviceLabel = item->text().replace(QRegularExpression(pairingExpression), + pairingStatusLabel); + + item->setText(updatedDeviceLabel); + item->setBackgroundColor(pairingColor); + } + + // Check if the updated device is the selected one from the list and inform the user that it can/cannot start the download mode + QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); + + if (currentItem != NULL && currentItem->text().contains(remoteDeviceStringAddress, Qt::CaseInsensitive)) { + if (pairing == QBluetoothLocalDevice::Unpaired) { + dialogStatusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") + .arg(remoteDeviceStringAddress); + } else { + dialogStatusMessage = tr("The device %1 can now be used for connection. You can press the Save button.") + .arg(remoteDeviceStringAddress); + } + } + + // Update the save button and the dialog status message + ui->save->setEnabled(enableSaveButton); + ui->dialogStatus->setText(dialogStatusMessage); +} + +void BtDeviceSelectionDialog::error(QBluetoothLocalDevice::Error error) +{ + ui->dialogStatus->setText(tr("Local device error: %1.") + .arg((error == QBluetoothLocalDevice::PairingError)? tr("Pairing error. If the remote device requires a custom PIN code, " + "please try to pair the devices using your operating system. ") + : tr("Unknown error"))); +} + +void BtDeviceSelectionDialog::deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error) +{ + QString errorDescription; + + switch (error) { + case QBluetoothDeviceDiscoveryAgent::PoweredOffError: + errorDescription = tr("The Bluetooth adaptor is powered off, power it on before doing discovery."); + break; + case QBluetoothDeviceDiscoveryAgent::InputOutputError: + errorDescription = tr("Writing to or reading from the device resulted in an error."); + break; + default: +#if defined(Q_OS_WIN) + errorDescription = remoteDeviceDiscoveryAgent->errorToString(); +#else + errorDescription = tr("An unknown error has occurred."); +#endif + break; + } + + ui->dialogStatus->setText(tr("Device discovery error: %1.").arg(errorDescription)); +} + +QString BtDeviceSelectionDialog::getSelectedDeviceAddress() +{ + if (selectedRemoteDeviceInfo) { + return selectedRemoteDeviceInfo.data()->address().toString(); + } + + return QString(); +} + +QString BtDeviceSelectionDialog::getSelectedDeviceName() +{ + if (selectedRemoteDeviceInfo) { + return selectedRemoteDeviceInfo.data()->name(); + } + + return QString(); +} + +void BtDeviceSelectionDialog::updateLocalDeviceInformation() +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + // Check if the selected Bluetooth device can be accessed + if (!localDevice->isValid()) { + QString na = tr("Not available"); + + // Update the UI information + ui->deviceAddress->setText(na); + ui->deviceName->setText(na); + + // Announce the user that there is a problem with the selected local Bluetooth adapter + ui->dialogStatus->setText(tr("The local Bluetooth adapter cannot be accessed.")); + + // Disable the buttons + ui->save->setEnabled(false); + ui->scan->setEnabled(false); + ui->clear->setEnabled(false); + ui->changeDeviceState->setEnabled(false); + + return; + } + + // Set UI information about the local device + ui->deviceAddress->setText(localDevice->address().toString()); + ui->deviceName->setText(localDevice->name()); + + connect(localDevice, SIGNAL(hostModeStateChanged(QBluetoothLocalDevice::HostMode)), + this, SLOT(hostModeStateChanged(QBluetoothLocalDevice::HostMode))); + + // Initialize the state of the local device and activate/deactive the scan button + hostModeStateChanged(localDevice->hostMode()); + + // Add context menu for devices to be able to pair them + ui->discoveredDevicesList->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->discoveredDevicesList, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(displayPairingMenu(QPoint))); + connect(localDevice, SIGNAL(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing)), + this, SLOT(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing))); + + connect(localDevice, SIGNAL(error(QBluetoothLocalDevice::Error)), + this, SLOT(error(QBluetoothLocalDevice::Error))); +#endif +} + +void BtDeviceSelectionDialog::initializeDeviceDiscoveryAgent() +{ +#if defined(Q_OS_WIN) + // Register QBluetoothDeviceInfo metatype + qRegisterMetaType(); + + // Register QBluetoothDeviceDiscoveryAgent metatype (Needed for QBluetoothDeviceDiscoveryAgent::Error) + qRegisterMetaType(); + + // Intialize the discovery agent + remoteDeviceDiscoveryAgent = new WinBluetoothDeviceDiscoveryAgent(this); +#else + // Intialize the discovery agent + remoteDeviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(localDevice->address()); + + // Test if the discovery agent was successfully created + if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError) { + ui->dialogStatus->setText(tr("The device discovery agent was not created because the %1 address does not " + "match the physical adapter address of any local Bluetooth device.") + .arg(localDevice->address().toString())); + ui->scan->setEnabled(false); + ui->clear->setEnabled(false); + return; + } +#endif + connect(remoteDeviceDiscoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)), + this, SLOT(addRemoteDevice(QBluetoothDeviceInfo))); + connect(remoteDeviceDiscoveryAgent, SIGNAL(finished()), + this, SLOT(remoteDeviceScanFinished())); + connect(remoteDeviceDiscoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), + this, SLOT(deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error))); +} + +#if defined(Q_OS_WIN) +WinBluetoothDeviceDiscoveryAgent::WinBluetoothDeviceDiscoveryAgent(QObject *parent) : QThread(parent) +{ + // Initialize the internal flags by their default values + running = false; + stopped = false; + lastError = QBluetoothDeviceDiscoveryAgent::NoError; + lastErrorToString = tr("No error"); +} + +WinBluetoothDeviceDiscoveryAgent::~WinBluetoothDeviceDiscoveryAgent() +{ +} + +bool WinBluetoothDeviceDiscoveryAgent::isActive() const +{ + return running; +} + +QString WinBluetoothDeviceDiscoveryAgent::errorToString() const +{ + return lastErrorToString; +} + +QBluetoothDeviceDiscoveryAgent::Error WinBluetoothDeviceDiscoveryAgent::error() const +{ + return lastError; +} + +void WinBluetoothDeviceDiscoveryAgent::run() +{ + // Initialize query for device and start the lookup service + WSAQUERYSET queryset; + HANDLE hLookup; + int result = SUCCESS; + + running = true; + lastError = QBluetoothDeviceDiscoveryAgent::NoError; + lastErrorToString = tr("No error"); + + memset(&queryset, 0, sizeof(WSAQUERYSET)); + queryset.dwSize = sizeof(WSAQUERYSET); + queryset.dwNameSpace = NS_BTH; + + // The LUP_CONTAINERS flag is used to signal that we are doing a device inquiry + // while LUP_FLUSHCACHE flag is used to flush the device cache for all inquiries + // and to do a fresh lookup instead. + result = WSALookupServiceBegin(&queryset, LUP_CONTAINERS | LUP_FLUSHCACHE, &hLookup); + + if (result != SUCCESS) { + // Get the last error and emit a signal + lastErrorToString = qt_error_string(); + lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError; + emit error(lastError); + + // Announce that the inquiry finished and restore the stopped flag + running = false; + stopped = false; + + return; + } + + // Declare the necessary variables to collect the information + BYTE buffer[4096]; + DWORD bufferLength = sizeof(buffer); + WSAQUERYSET *pResults = (WSAQUERYSET*)&buffer; + + memset(buffer, 0, sizeof(buffer)); + + pResults->dwSize = sizeof(WSAQUERYSET); + pResults->dwNameSpace = NS_BTH; + pResults->lpBlob = NULL; + + //Start looking for devices + while (result == SUCCESS && !stopped){ + // LUP_RETURN_NAME and LUP_RETURN_ADDR flags are used to return the name and the address of the discovered device + result = WSALookupServiceNext(hLookup, LUP_RETURN_NAME | LUP_RETURN_ADDR, &bufferLength, pResults); + + if (result == SUCCESS) { + // Found a device + QString deviceAddress(BTH_ADDR_BUF_LEN, Qt::Uninitialized); + DWORD addressSize = BTH_ADDR_BUF_LEN; + + // Collect the address of the device from the WSAQUERYSET + SOCKADDR_BTH *socketBthAddress = (SOCKADDR_BTH *) pResults->lpcsaBuffer->RemoteAddr.lpSockaddr; + + // Convert the BTH_ADDR to string + if (WSAAddressToStringW((LPSOCKADDR) socketBthAddress, + sizeof (*socketBthAddress), + NULL, + reinterpret_cast(deviceAddress.data()), + &addressSize + ) != 0) { + // Get the last error and emit a signal + lastErrorToString = qt_error_string(); + lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; + emit(lastError); + + break; + } + + // Remove the round parentheses + deviceAddress.remove(')'); + deviceAddress.remove('('); + + // Save the name of the discovered device and truncate the address + QString deviceName = QString(pResults->lpszServiceInstanceName); + deviceAddress.truncate(BTH_ADDR_PRETTY_STRING_LEN); + + // Create an object with information about the discovered device + QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(QBluetoothAddress(deviceAddress), deviceName, 0); + + // Raise a signal with information about the found remote device + emit deviceDiscovered(deviceInfo); + } else { + // Get the last error and emit a signal + lastErrorToString = qt_error_string(); + lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; + emit(lastError); + } + } + + // Announce that the inquiry finished and restore the stopped flag + running = false; + stopped = false; + + // Restore the error status + lastError = QBluetoothDeviceDiscoveryAgent::NoError; + + // End the lookup service + WSALookupServiceEnd(hLookup); +} + +void WinBluetoothDeviceDiscoveryAgent::stop() +{ + // Stop the inqury + stopped = true; +} +#endif diff --git a/desktop-widgets/btdeviceselectiondialog.h b/desktop-widgets/btdeviceselectiondialog.h new file mode 100644 index 000000000..7651f164b --- /dev/null +++ b/desktop-widgets/btdeviceselectiondialog.h @@ -0,0 +1,91 @@ +#ifndef BTDEVICESELECTIONDIALOG_H +#define BTDEVICESELECTIONDIALOG_H + +#include +#include +#include +#include +#include +#include + +#if defined(Q_OS_WIN) + #include + #include + #include + + #define SUCCESS 0 + #define BTH_ADDR_BUF_LEN 40 + #define BTH_ADDR_PRETTY_STRING_LEN 17 // there are 6 two-digit hex values and 5 colons + + #undef ERROR // this is already declared in our headers + #undef IGNORE // this is already declared in our headers + #undef DC_VERSION // this is already declared in libdivecomputer header +#endif + +namespace Ui { + class BtDeviceSelectionDialog; +} + +#if defined(Q_OS_WIN) +class WinBluetoothDeviceDiscoveryAgent : public QThread { + Q_OBJECT +signals: + void deviceDiscovered(const QBluetoothDeviceInfo &info); + void error(QBluetoothDeviceDiscoveryAgent::Error error); + +public: + WinBluetoothDeviceDiscoveryAgent(QObject *parent); + ~WinBluetoothDeviceDiscoveryAgent(); + bool isActive() const; + QString errorToString() const; + QBluetoothDeviceDiscoveryAgent::Error error() const; + virtual void run(); + virtual void stop(); + +private: + bool running; + bool stopped; + QString lastErrorToString; + QBluetoothDeviceDiscoveryAgent::Error lastError; +}; +#endif + +class BtDeviceSelectionDialog : public QDialog { + Q_OBJECT + +public: + explicit BtDeviceSelectionDialog(QWidget *parent = 0); + ~BtDeviceSelectionDialog(); + QString getSelectedDeviceAddress(); + QString getSelectedDeviceName(); + +private slots: + void on_changeDeviceState_clicked(); + void on_save_clicked(); + void on_clear_clicked(); + void on_scan_clicked(); + void remoteDeviceScanFinished(); + void hostModeStateChanged(QBluetoothLocalDevice::HostMode mode); + void addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo); + void itemClicked(QListWidgetItem *item); + void displayPairingMenu(const QPoint &pos); + void pairingFinished(const QBluetoothAddress &address,QBluetoothLocalDevice::Pairing pairing); + void error(QBluetoothLocalDevice::Error error); + void deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error); + void localDeviceChanged(int); + +private: + Ui::BtDeviceSelectionDialog *ui; +#if defined(Q_OS_WIN) + WinBluetoothDeviceDiscoveryAgent *remoteDeviceDiscoveryAgent; +#else + QBluetoothLocalDevice *localDevice; + QBluetoothDeviceDiscoveryAgent *remoteDeviceDiscoveryAgent; +#endif + QSharedPointer selectedRemoteDeviceInfo; + + void updateLocalDeviceInformation(); + void initializeDeviceDiscoveryAgent(); +}; + +#endif // BTDEVICESELECTIONDIALOG_H diff --git a/desktop-widgets/btdeviceselectiondialog.ui b/desktop-widgets/btdeviceselectiondialog.ui new file mode 100644 index 000000000..4aa83cf1c --- /dev/null +++ b/desktop-widgets/btdeviceselectiondialog.ui @@ -0,0 +1,224 @@ + + + BtDeviceSelectionDialog + + + + 0 + 0 + 735 + 460 + + + + Remote Bluetooth device selection + + + + + + + 0 + 0 + + + + + 75 + true + + + + Discovered devices + + + + + + + + + Save + + + + + + + + 0 + 0 + + + + Quit + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Scan + + + + + + + + 0 + 0 + + + + Clear + + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Local Bluetooth device details + + + false + + + + + + Name: + + + + + + + true + + + + + + + Address: + + + + + + + true + + + + + + + false + + + + 0 + 0 + + + + + 75 + true + + + + Bluetooth powered on + + + true + + + + + + + + 0 + 0 + + + + + 50 + false + + + + Turn on/off + + + + + + + + + + Select device: + + + + + + + + + + + + + true + + + + + + + + diff --git a/desktop-widgets/configuredivecomputerdialog.cpp b/desktop-widgets/configuredivecomputerdialog.cpp new file mode 100644 index 000000000..ddb9450de --- /dev/null +++ b/desktop-widgets/configuredivecomputerdialog.cpp @@ -0,0 +1,1257 @@ +#include "configuredivecomputerdialog.h" + +#include "helpers.h" +#include "mainwindow.h" +#include "display.h" + +#include +#include +#include +#include +#include + +struct product { + const char *product; + dc_descriptor_t *descriptor; + struct product *next; +}; + +struct vendor { + const char *vendor; + struct product *productlist; + struct vendor *next; +}; + +struct mydescriptor { + const char *vendor; + const char *product; + dc_family_t type; + unsigned int model; +}; + +GasSpinBoxItemDelegate::GasSpinBoxItemDelegate(QObject *parent, column_type type) : QStyledItemDelegate(parent), type(type) +{ +} +GasSpinBoxItemDelegate::~GasSpinBoxItemDelegate() +{ +} + +QWidget *GasSpinBoxItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // Create the spinbox and give it it's settings + QSpinBox *sb = new QSpinBox(parent); + if (type == PERCENT) { + sb->setMinimum(0); + sb->setMaximum(100); + sb->setSuffix("%"); + } else if (type == DEPTH) { + sb->setMinimum(0); + sb->setMaximum(255); + sb->setSuffix(" m"); + } else if (type == SETPOINT) { + sb->setMinimum(0); + sb->setMaximum(255); + sb->setSuffix(" cbar"); + } + return sb; +} + +void GasSpinBoxItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if (QSpinBox *sb = qobject_cast(editor)) + sb->setValue(index.data(Qt::EditRole).toInt()); + else + QStyledItemDelegate::setEditorData(editor, index); +} + + +void GasSpinBoxItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if (QSpinBox *sb = qobject_cast(editor)) + model->setData(index, sb->value(), Qt::EditRole); + else + QStyledItemDelegate::setModelData(editor, model, index); +} + +GasTypeComboBoxItemDelegate::GasTypeComboBoxItemDelegate(QObject *parent, computer_type type) : QStyledItemDelegate(parent), type(type) +{ +} +GasTypeComboBoxItemDelegate::~GasTypeComboBoxItemDelegate() +{ +} + +QWidget *GasTypeComboBoxItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // Create the combobox and populate it + QComboBox *cb = new QComboBox(parent); + cb->addItem(QString("Disabled")); + if (type == OSTC3) { + cb->addItem(QString("First")); + cb->addItem(QString("Travel")); + cb->addItem(QString("Deco")); + } else if (type == OSTC) { + cb->addItem(QString("Active")); + cb->addItem(QString("First")); + } + return cb; +} + +void GasTypeComboBoxItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if (QComboBox *cb = qobject_cast(editor)) + cb->setCurrentIndex(index.data(Qt::EditRole).toInt()); + else + QStyledItemDelegate::setEditorData(editor, index); +} + + +void GasTypeComboBoxItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if (QComboBox *cb = qobject_cast(editor)) + model->setData(index, cb->currentIndex(), Qt::EditRole); + else + QStyledItemDelegate::setModelData(editor, model, index); +} + +ConfigureDiveComputerDialog::ConfigureDiveComputerDialog(QWidget *parent) : QDialog(parent), + config(0), +#ifdef BT_SUPPORT + deviceDetails(0), + btDeviceSelectionDialog(0) +#else + deviceDetails(0) +#endif +{ + ui.setupUi(this); + + deviceDetails = new DeviceDetails(this); + config = new ConfigureDiveComputer(); + connect(config, SIGNAL(progress(int)), ui.progressBar, SLOT(setValue(int))); + connect(config, SIGNAL(error(QString)), this, SLOT(configError(QString))); + connect(config, SIGNAL(message(QString)), this, SLOT(configMessage(QString))); + connect(config, SIGNAL(deviceDetailsChanged(DeviceDetails *)), + this, SLOT(deviceDetailsReceived(DeviceDetails *))); + connect(ui.retrieveDetails, SIGNAL(clicked()), this, SLOT(readSettings())); + connect(ui.resetButton, SIGNAL(clicked()), this, SLOT(resetSettings())); + ui.chooseLogFile->setEnabled(ui.logToFile->isChecked()); + connect(ui.chooseLogFile, SIGNAL(clicked()), this, SLOT(pickLogFile())); + connect(ui.logToFile, SIGNAL(stateChanged(int)), this, SLOT(checkLogFile(int))); + connect(ui.connectButton, SIGNAL(clicked()), this, SLOT(dc_open())); + connect(ui.disconnectButton, SIGNAL(clicked()), this, SLOT(dc_close())); +#ifdef BT_SUPPORT + connect(ui.bluetoothMode, SIGNAL(clicked(bool)), this, SLOT(selectRemoteBluetoothDevice())); +#else + ui.bluetoothMode->setVisible(false); +#endif + + memset(&device_data, 0, sizeof(device_data)); + fill_computer_list(); + if (default_dive_computer_device) + ui.device->setEditText(default_dive_computer_device); + + ui.DiveComputerList->setCurrentRow(0); + on_DiveComputerList_currentRowChanged(0); + + ui.ostc3GasTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostc3GasTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostc3GasTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC3)); + ui.ostc3GasTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostc3DilTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC3)); + ui.ostc3DilTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostc3SetPointTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::SETPOINT)); + ui.ostc3SetPointTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostcGasTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostcGasTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostcGasTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC)); + ui.ostcGasTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostcDilTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC)); + ui.ostcDilTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostcSetPointTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::SETPOINT)); + ui.ostcSetPointTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + + QSettings settings; + settings.beginGroup("ConfigureDiveComputerDialog"); + settings.beginGroup("ostc3GasTable"); + for (int i = 0; i < ui.ostc3GasTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostc3GasTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostc3DilTable"); + for (int i = 0; i < ui.ostc3DilTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostc3DilTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostc3SetPointTable"); + for (int i = 0; i < ui.ostc3SetPointTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostc3SetPointTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + + settings.beginGroup("ostcGasTable"); + for (int i = 0; i < ui.ostcGasTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostcGasTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostcDilTable"); + for (int i = 0; i < ui.ostcDilTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostcDilTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostcSetPointTable"); + for (int i = 0; i < ui.ostcSetPointTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostcSetPointTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.endGroup(); +} + +OstcFirmwareCheck::OstcFirmwareCheck(QString product) : parent(0) +{ + QUrl url; + memset(&devData, 1, sizeof(devData)); + if (product == "OSTC 3") { + url = QUrl("http://www.heinrichsweikamp.net/autofirmware/ostc3_changelog.txt"); + latestFirmwareHexFile = QString("http://www.heinrichsweikamp.net/autofirmware/ostc3_firmware.hex"); + } else if (product == "OSTC Sport") { + url = QUrl("http://www.heinrichsweikamp.net/autofirmware/ostc_sport_changelog.txt"); + latestFirmwareHexFile = QString("http://www.heinrichsweikamp.net/autofirmware/ostc_sport_firmware.hex"); + } else { // not one of the known dive computers + return; + } + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(parseOstcFwVersion(QNetworkReply *))); + QNetworkRequest download(url); + manager.get(download); +} + +void OstcFirmwareCheck::parseOstcFwVersion(QNetworkReply *reply) +{ + QString parse = reply->readAll(); + int firstOpenBracket = parse.indexOf('['); + int firstCloseBracket = parse.indexOf(']'); + latestFirmwareAvailable = parse.mid(firstOpenBracket + 1, firstCloseBracket - firstOpenBracket - 1); + disconnect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(parseOstcFwVersion(QNetworkReply *))); +} + +void OstcFirmwareCheck::checkLatest(QWidget *_parent, device_data_t *data) +{ + devData = *data; + parent = _parent; + // If we didn't find a current firmware version stop this hole thing here. + if (latestFirmwareAvailable.isEmpty()) + return; + + // for now libdivecomputer gives us the firmware on device undecoded as integer + // for the OSTC that means highbyte.lowbyte is the version number + int firmwareOnDevice = devData.libdc_firmware; + QString firmwareOnDeviceString = QString("%1.%2").arg(firmwareOnDevice / 256).arg(firmwareOnDevice % 256); + + // Convert the latestFirmwareAvailable to a integear we can compare with + QStringList fwParts = latestFirmwareAvailable.split("."); + int latestFirmwareAvailableNumber = fwParts[0].toInt() * 256 + fwParts[1].toInt(); + if (latestFirmwareAvailableNumber > firmwareOnDevice) { + QMessageBox response(parent); + QString message = tr("You should update the firmware on your dive computer: you have version %1 but the latest stable version is %2") + .arg(firmwareOnDeviceString) + .arg(latestFirmwareAvailable); + if (strcmp(data->product, "OSTC Sport") == 0) + message += tr("\n\nPlease start Bluetooth on your OSTC Sport and do the same preparations as for a logbook download before continuing with the update"); + response.addButton(tr("Not now"), QMessageBox::RejectRole); + response.addButton(tr("Update firmware"), QMessageBox::AcceptRole); + response.setText(message); + response.setWindowTitle(tr("Firmware upgrade notice")); + response.setIcon(QMessageBox::Question); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + if (ret == QMessageBox::Accepted) + upgradeFirmware(); + } +} + +void OstcFirmwareCheck::upgradeFirmware() +{ + // start download of latestFirmwareHexFile + QString saveFileName = latestFirmwareHexFile; + saveFileName.replace("http://www.heinrichsweikamp.net/autofirmware/", ""); + saveFileName.replace("firmware", latestFirmwareAvailable); + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append(saveFileName); + storeFirmware = QFileDialog::getSaveFileName(parent, tr("Save the downloaded firmware as"), + filename, tr("HEX files (*.hex)")); + if (storeFirmware.isEmpty()) + return; + + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveOstcFirmware(QNetworkReply *))); + QNetworkRequest download(latestFirmwareHexFile); + manager.get(download); +} + +void OstcFirmwareCheck::saveOstcFirmware(QNetworkReply *reply) +{ + // firmware is downloaded + // call config->startFirmwareUpdate() with that file and the device data + + QByteArray firmwareData = reply->readAll(); + QFile file(storeFirmware); + file.open(QIODevice::WriteOnly); + file.write(firmwareData); + file.close(); + QProgressDialog *dialog = new QProgressDialog("Updating firmware", "", 0, 100); + dialog->setCancelButton(0); + dialog->setAutoClose(true); + ConfigureDiveComputer *config = new ConfigureDiveComputer(); + connect(config, SIGNAL(message(QString)), dialog, SLOT(setLabelText(QString))); + connect(config, SIGNAL(error(QString)), dialog, SLOT(setLabelText(QString))); + connect(config, SIGNAL(progress(int)), dialog, SLOT(setValue(int))); + connect(dialog, SIGNAL(finished(int)), config, SLOT(dc_close())); + config->dc_open(&devData); + config->startFirmwareUpdate(storeFirmware, &devData); +} + +ConfigureDiveComputerDialog::~ConfigureDiveComputerDialog() +{ + delete config; +} + +void ConfigureDiveComputerDialog::closeEvent(QCloseEvent *event) +{ + dc_close(); + + QSettings settings; + settings.beginGroup("ConfigureDiveComputerDialog"); + settings.beginGroup("ostc3GasTable"); + for (int i = 0; i < ui.ostc3GasTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostc3GasTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostc3DilTable"); + for (int i = 0; i < ui.ostc3DilTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostc3DilTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostc3SetPointTable"); + for (int i = 0; i < ui.ostc3SetPointTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostc3SetPointTable->columnWidth(i)); + settings.endGroup(); + + settings.beginGroup("ostcGasTable"); + for (int i = 0; i < ui.ostcGasTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostcGasTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostcDilTable"); + for (int i = 0; i < ui.ostcDilTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostcDilTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostcSetPointTable"); + for (int i = 0; i < ui.ostcSetPointTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostcSetPointTable->columnWidth(i)); + settings.endGroup(); + settings.endGroup(); +} + + +static void fillDeviceList(const char *name, void *data) +{ + QComboBox *comboBox = (QComboBox *)data; + comboBox->addItem(name); +} + +void ConfigureDiveComputerDialog::fill_device_list(int dc_type) +{ + int deviceIndex; + ui.device->clear(); + deviceIndex = enumerate_devices(fillDeviceList, ui.device, dc_type); + if (deviceIndex >= 0) + ui.device->setCurrentIndex(deviceIndex); +} + +void ConfigureDiveComputerDialog::fill_computer_list() +{ + dc_iterator_t *iterator = NULL; + dc_descriptor_t *descriptor = NULL; + + struct mydescriptor *mydescriptor; + + dc_descriptor_iterator(&iterator); + while (dc_iterator_next(iterator, &descriptor) == DC_STATUS_SUCCESS) { + const char *vendor = dc_descriptor_get_vendor(descriptor); + const char *product = dc_descriptor_get_product(descriptor); + + if (!vendorList.contains(vendor)) + vendorList.append(vendor); + + if (!productList[vendor].contains(product)) + productList[vendor].push_back(product); + + descriptorLookup[QString(vendor) + QString(product)] = descriptor; + } + dc_iterator_free(iterator); + + mydescriptor = (struct mydescriptor *)malloc(sizeof(struct mydescriptor)); + mydescriptor->vendor = "Uemis"; + mydescriptor->product = "Zurich"; + mydescriptor->type = DC_FAMILY_NULL; + mydescriptor->model = 0; + + if (!vendorList.contains("Uemis")) + vendorList.append("Uemis"); + + if (!productList["Uemis"].contains("Zurich")) + productList["Uemis"].push_back("Zurich"); + + descriptorLookup["UemisZurich"] = (dc_descriptor_t *)mydescriptor; + + qSort(vendorList); +} + +void ConfigureDiveComputerDialog::populateDeviceDetails() +{ + switch (ui.dcStackedWidget->currentIndex()) { + case 0: + populateDeviceDetailsOSTC3(); + break; + case 1: + populateDeviceDetailsSuuntoVyper(); + break; + case 2: + populateDeviceDetailsOSTC(); + break; + } +} + +#define GET_INT_FROM(_field, _default) ((_field) != NULL) ? (_field)->data(Qt::EditRole).toInt() : (_default) + +void ConfigureDiveComputerDialog::populateDeviceDetailsOSTC3() +{ + deviceDetails->customText = ui.customTextLlineEdit->text(); + deviceDetails->diveMode = ui.diveModeComboBox->currentIndex(); + deviceDetails->saturation = ui.saturationSpinBox->value(); + deviceDetails->desaturation = ui.desaturationSpinBox->value(); + deviceDetails->lastDeco = ui.lastDecoSpinBox->value(); + deviceDetails->brightness = ui.brightnessComboBox->currentIndex(); + deviceDetails->units = ui.unitsComboBox->currentIndex(); + deviceDetails->samplingRate = ui.samplingRateComboBox->currentIndex(); + deviceDetails->salinity = ui.salinitySpinBox->value(); + deviceDetails->diveModeColor = ui.diveModeColour->currentIndex(); + deviceDetails->language = ui.languageComboBox->currentIndex(); + deviceDetails->dateFormat = ui.dateFormatComboBox->currentIndex(); + deviceDetails->compassGain = ui.compassGainComboBox->currentIndex(); + deviceDetails->syncTime = ui.dateTimeSyncCheckBox->isChecked(); + deviceDetails->safetyStop = ui.safetyStopCheckBox->isChecked(); + deviceDetails->gfHigh = ui.gfHighSpinBox->value(); + deviceDetails->gfLow = ui.gfLowSpinBox->value(); + deviceDetails->pressureSensorOffset = ui.pressureSensorOffsetSpinBox->value(); + deviceDetails->ppO2Min = ui.ppO2MinSpinBox->value(); + deviceDetails->ppO2Max = ui.ppO2MaxSpinBox->value(); + deviceDetails->futureTTS = ui.futureTTSSpinBox->value(); + deviceDetails->ccrMode = ui.ccrModeComboBox->currentIndex(); + deviceDetails->decoType = ui.decoTypeComboBox->currentIndex(); + deviceDetails->aGFSelectable = ui.aGFSelectableCheckBox->isChecked(); + deviceDetails->aGFHigh = ui.aGFHighSpinBox->value(); + deviceDetails->aGFLow = ui.aGFLowSpinBox->value(); + deviceDetails->calibrationGas = ui.calibrationGasSpinBox->value(); + deviceDetails->flipScreen = ui.flipScreenCheckBox->isChecked(); + deviceDetails->setPointFallback = ui.setPointFallbackCheckBox->isChecked(); + deviceDetails->leftButtonSensitivity = ui.leftButtonSensitivity->value(); + deviceDetails->rightButtonSensitivity = ui.rightButtonSensitivity->value(); + deviceDetails->bottomGasConsumption = ui.bottomGasConsumption->value(); + deviceDetails->decoGasConsumption = ui.decoGasConsumption->value(); + deviceDetails->modWarning = ui.modWarning->isChecked(); + deviceDetails->dynamicAscendRate = ui.dynamicAscendRate->isChecked(); + deviceDetails->graphicalSpeedIndicator = ui.graphicalSpeedIndicator->isChecked(); + deviceDetails->alwaysShowppO2 = ui.alwaysShowppO2->isChecked(); + + //set gas values + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + + gas1.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(0, 1), 21); + gas1.helium = GET_INT_FROM(ui.ostc3GasTable->item(0, 2), 0); + gas1.type = GET_INT_FROM(ui.ostc3GasTable->item(0, 3), 0); + gas1.depth = GET_INT_FROM(ui.ostc3GasTable->item(0, 4), 0); + + gas2.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(1, 1), 21); + gas2.helium = GET_INT_FROM(ui.ostc3GasTable->item(1, 2), 0); + gas2.type = GET_INT_FROM(ui.ostc3GasTable->item(1, 3), 0); + gas2.depth = GET_INT_FROM(ui.ostc3GasTable->item(1, 4), 0); + + gas3.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(2, 1), 21); + gas3.helium = GET_INT_FROM(ui.ostc3GasTable->item(2, 2), 0); + gas3.type = GET_INT_FROM(ui.ostc3GasTable->item(2, 3), 0); + gas3.depth = GET_INT_FROM(ui.ostc3GasTable->item(2, 4), 0); + + gas4.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(3, 1), 21); + gas4.helium = GET_INT_FROM(ui.ostc3GasTable->item(3, 2), 0); + gas4.type = GET_INT_FROM(ui.ostc3GasTable->item(3, 3), 0); + gas4.depth = GET_INT_FROM(ui.ostc3GasTable->item(3, 4), 0); + + gas5.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(4, 1), 21); + gas5.helium = GET_INT_FROM(ui.ostc3GasTable->item(4, 2), 0); + gas5.type = GET_INT_FROM(ui.ostc3GasTable->item(4, 3), 0); + gas5.depth = GET_INT_FROM(ui.ostc3GasTable->item(4, 4), 0); + + deviceDetails->gas1 = gas1; + deviceDetails->gas2 = gas2; + deviceDetails->gas3 = gas3; + deviceDetails->gas4 = gas4; + deviceDetails->gas5 = gas5; + + //set dil values + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + + dil1.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(0, 1), 21); + dil1.helium = GET_INT_FROM(ui.ostc3DilTable->item(0, 2), 0); + dil1.type = GET_INT_FROM(ui.ostc3DilTable->item(0, 3), 0); + dil1.depth = GET_INT_FROM(ui.ostc3DilTable->item(0, 4), 0); + + dil2.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(1, 1), 21); + dil2.helium = GET_INT_FROM(ui.ostc3DilTable->item(1, 2), 0); + dil2.type = GET_INT_FROM(ui.ostc3DilTable->item(1, 3), 0); + dil2.depth = GET_INT_FROM(ui.ostc3DilTable->item(1, 4), 0); + + dil3.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(2, 1), 21); + dil3.helium = GET_INT_FROM(ui.ostc3DilTable->item(2, 2), 0); + dil3.type = GET_INT_FROM(ui.ostc3DilTable->item(2, 3), 0); + dil3.depth = GET_INT_FROM(ui.ostc3DilTable->item(2, 4), 0); + + dil4.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(3, 1), 21); + dil4.helium = GET_INT_FROM(ui.ostc3DilTable->item(3, 2), 0); + dil4.type = GET_INT_FROM(ui.ostc3DilTable->item(3, 3), 0); + dil4.depth = GET_INT_FROM(ui.ostc3DilTable->item(3, 4), 0); + + dil5.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(4, 1), 21); + dil5.helium = GET_INT_FROM(ui.ostc3DilTable->item(4, 2), 0); + dil5.type = GET_INT_FROM(ui.ostc3DilTable->item(4, 3), 0); + dil5.depth = GET_INT_FROM(ui.ostc3DilTable->item(4, 4), 0); + + deviceDetails->dil1 = dil1; + deviceDetails->dil2 = dil2; + deviceDetails->dil3 = dil3; + deviceDetails->dil4 = dil4; + deviceDetails->dil5 = dil5; + + //set set point details + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + + sp1.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(0, 1), 70); + sp1.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(0, 2), 0); + + sp2.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(1, 1), 90); + sp2.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(1, 2), 20); + + sp3.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(2, 1), 100); + sp3.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(2, 2), 33); + + sp4.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(3, 1), 120); + sp4.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(3, 2), 50); + + sp5.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(4, 1), 140); + sp5.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(4, 2), 70); + + deviceDetails->sp1 = sp1; + deviceDetails->sp2 = sp2; + deviceDetails->sp3 = sp3; + deviceDetails->sp4 = sp4; + deviceDetails->sp5 = sp5; +} + +void ConfigureDiveComputerDialog::populateDeviceDetailsOSTC() +{ + deviceDetails->customText = ui.customTextLlineEdit_3->text(); + deviceDetails->saturation = ui.saturationSpinBox_3->value(); + deviceDetails->desaturation = ui.desaturationSpinBox_3->value(); + deviceDetails->lastDeco = ui.lastDecoSpinBox_3->value(); + deviceDetails->samplingRate = ui.samplingRateSpinBox_3->value(); + deviceDetails->salinity = ui.salinityDoubleSpinBox_3->value() * 100; + deviceDetails->dateFormat = ui.dateFormatComboBox_3->currentIndex(); + deviceDetails->syncTime = ui.dateTimeSyncCheckBox_3->isChecked(); + deviceDetails->safetyStop = ui.safetyStopCheckBox_3->isChecked(); + deviceDetails->gfHigh = ui.gfHighSpinBox_3->value(); + deviceDetails->gfLow = ui.gfLowSpinBox_3->value(); + deviceDetails->ppO2Min = ui.ppO2MinSpinBox_3->value(); + deviceDetails->ppO2Max = ui.ppO2MaxSpinBox_3->value(); + deviceDetails->futureTTS = ui.futureTTSSpinBox_3->value(); + deviceDetails->decoType = ui.decoTypeComboBox_3->currentIndex(); + deviceDetails->aGFSelectable = ui.aGFSelectableCheckBox_3->isChecked(); + deviceDetails->aGFHigh = ui.aGFHighSpinBox_3->value(); + deviceDetails->aGFLow = ui.aGFLowSpinBox_3->value(); + deviceDetails->bottomGasConsumption = ui.bottomGasConsumption_3->value(); + deviceDetails->decoGasConsumption = ui.decoGasConsumption_3->value(); + deviceDetails->graphicalSpeedIndicator = ui.graphicalSpeedIndicator_3->isChecked(); + + //set gas values + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + + gas1.oxygen = GET_INT_FROM(ui.ostcGasTable->item(0, 1), 21); + gas1.helium = GET_INT_FROM(ui.ostcGasTable->item(0, 2), 0); + gas1.type = GET_INT_FROM(ui.ostcGasTable->item(0, 3), 0); + gas1.depth = GET_INT_FROM(ui.ostcGasTable->item(0, 4), 0); + + gas2.oxygen = GET_INT_FROM(ui.ostcGasTable->item(1, 1), 21); + gas2.helium = GET_INT_FROM(ui.ostcGasTable->item(1, 2), 0); + gas2.type = GET_INT_FROM(ui.ostcGasTable->item(1, 3), 0); + gas2.depth = GET_INT_FROM(ui.ostcGasTable->item(1, 4), 0); + + gas3.oxygen = GET_INT_FROM(ui.ostcGasTable->item(2, 1), 21); + gas3.helium = GET_INT_FROM(ui.ostcGasTable->item(2, 2), 0); + gas3.type = GET_INT_FROM(ui.ostcGasTable->item(2, 3), 0); + gas3.depth = GET_INT_FROM(ui.ostcGasTable->item(2, 4), 0); + + gas4.oxygen = GET_INT_FROM(ui.ostcGasTable->item(3, 1), 21); + gas4.helium = GET_INT_FROM(ui.ostcGasTable->item(3, 2), 0); + gas4.type = GET_INT_FROM(ui.ostcGasTable->item(3, 3), 0); + gas4.depth = GET_INT_FROM(ui.ostcGasTable->item(3, 4), 0); + + gas5.oxygen = GET_INT_FROM(ui.ostcGasTable->item(4, 1), 21); + gas5.helium = GET_INT_FROM(ui.ostcGasTable->item(4, 2), 0); + gas5.type = GET_INT_FROM(ui.ostcGasTable->item(4, 3), 0); + gas5.depth = GET_INT_FROM(ui.ostcGasTable->item(4, 4), 0); + + deviceDetails->gas1 = gas1; + deviceDetails->gas2 = gas2; + deviceDetails->gas3 = gas3; + deviceDetails->gas4 = gas4; + deviceDetails->gas5 = gas5; + + //set dil values + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + + dil1.oxygen = GET_INT_FROM(ui.ostcDilTable->item(0, 1), 21); + dil1.helium = GET_INT_FROM(ui.ostcDilTable->item(0, 2), 0); + dil1.type = GET_INT_FROM(ui.ostcDilTable->item(0, 3), 0); + dil1.depth = GET_INT_FROM(ui.ostcDilTable->item(0, 4), 0); + + dil2.oxygen = GET_INT_FROM(ui.ostcDilTable->item(1, 1), 21); + dil2.helium = GET_INT_FROM(ui.ostcDilTable->item(1, 2), 0); + dil2.type = GET_INT_FROM(ui.ostcDilTable->item(1, 3), 0); + dil2.depth = GET_INT_FROM(ui.ostcDilTable->item(1, 4), 0); + + dil3.oxygen = GET_INT_FROM(ui.ostcDilTable->item(2, 1), 21); + dil3.helium = GET_INT_FROM(ui.ostcDilTable->item(2, 2), 0); + dil3.type = GET_INT_FROM(ui.ostcDilTable->item(2, 3), 0); + dil3.depth = GET_INT_FROM(ui.ostcDilTable->item(2, 4), 0); + + dil4.oxygen = GET_INT_FROM(ui.ostcDilTable->item(3, 1), 21); + dil4.helium = GET_INT_FROM(ui.ostcDilTable->item(3, 2), 0); + dil4.type = GET_INT_FROM(ui.ostcDilTable->item(3, 3), 0); + dil4.depth = GET_INT_FROM(ui.ostcDilTable->item(3, 4), 0); + + dil5.oxygen = GET_INT_FROM(ui.ostcDilTable->item(4, 1), 21); + dil5.helium = GET_INT_FROM(ui.ostcDilTable->item(4, 2), 0); + dil5.type = GET_INT_FROM(ui.ostcDilTable->item(4, 3), 0); + dil5.depth = GET_INT_FROM(ui.ostcDilTable->item(4, 4), 0); + + deviceDetails->dil1 = dil1; + deviceDetails->dil2 = dil2; + deviceDetails->dil3 = dil3; + deviceDetails->dil4 = dil4; + deviceDetails->dil5 = dil5; + + //set set point details + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + + sp1.sp = GET_INT_FROM(ui.ostcSetPointTable->item(0, 1), 70); + sp1.depth = GET_INT_FROM(ui.ostcSetPointTable->item(0, 2), 0); + + sp2.sp = GET_INT_FROM(ui.ostcSetPointTable->item(1, 1), 90); + sp2.depth = GET_INT_FROM(ui.ostcSetPointTable->item(1, 2), 20); + + sp3.sp = GET_INT_FROM(ui.ostcSetPointTable->item(2, 1), 100); + sp3.depth = GET_INT_FROM(ui.ostcSetPointTable->item(2, 2), 33); + + sp4.sp = GET_INT_FROM(ui.ostcSetPointTable->item(3, 1), 120); + sp4.depth = GET_INT_FROM(ui.ostcSetPointTable->item(3, 2), 50); + + sp5.sp = GET_INT_FROM(ui.ostcSetPointTable->item(4, 1), 140); + sp5.depth = GET_INT_FROM(ui.ostcSetPointTable->item(4, 2), 70); + + deviceDetails->sp1 = sp1; + deviceDetails->sp2 = sp2; + deviceDetails->sp3 = sp3; + deviceDetails->sp4 = sp4; + deviceDetails->sp5 = sp5; +} + +void ConfigureDiveComputerDialog::populateDeviceDetailsSuuntoVyper() +{ + deviceDetails->customText = ui.customTextLlineEdit_1->text(); + deviceDetails->samplingRate = ui.samplingRateComboBox_1->currentIndex() == 3 ? 60 : (ui.samplingRateComboBox_1->currentIndex() + 1) * 10; + deviceDetails->altitude = ui.altitudeRangeComboBox->currentIndex(); + deviceDetails->personalSafety = ui.personalSafetyComboBox->currentIndex(); + deviceDetails->timeFormat = ui.timeFormatComboBox->currentIndex(); + deviceDetails->units = ui.unitsComboBox_1->currentIndex(); + deviceDetails->diveMode = ui.diveModeComboBox_1->currentIndex(); + deviceDetails->lightEnabled = ui.lightCheckBox->isChecked(); + deviceDetails->light = ui.lightSpinBox->value(); + deviceDetails->alarmDepthEnabled = ui.alarmDepthCheckBox->isChecked(); + deviceDetails->alarmDepth = units_to_depth(ui.alarmDepthDoubleSpinBox->value()); + deviceDetails->alarmTimeEnabled = ui.alarmTimeCheckBox->isChecked(); + deviceDetails->alarmTime = ui.alarmTimeSpinBox->value(); +} + +void ConfigureDiveComputerDialog::readSettings() +{ + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + // Fw update is no longer a option, needs to be done on a untouched device + ui.updateFirmwareButton->setEnabled(false); + + config->readSettings(&device_data); +} + +void ConfigureDiveComputerDialog::resetSettings() +{ + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + + config->resetSettings(&device_data); +} + +void ConfigureDiveComputerDialog::configMessage(QString msg) +{ + ui.progressBar->setFormat(msg); +} + +void ConfigureDiveComputerDialog::configError(QString err) +{ + ui.progressBar->setFormat("Error: " + err); +} + +void ConfigureDiveComputerDialog::getDeviceData() +{ + device_data.devname = strdup(ui.device->currentText().toUtf8().data()); + device_data.vendor = strdup(selected_vendor.toUtf8().data()); + device_data.product = strdup(selected_product.toUtf8().data()); + + device_data.descriptor = descriptorLookup[selected_vendor + selected_product]; + device_data.deviceid = device_data.diveid = 0; + + set_default_dive_computer_device(device_data.devname); +} + +void ConfigureDiveComputerDialog::on_cancel_clicked() +{ + this->close(); +} + +void ConfigureDiveComputerDialog::on_saveSettingsPushButton_clicked() +{ + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + + populateDeviceDetails(); + config->saveDeviceDetails(deviceDetails, &device_data); +} + +void ConfigureDiveComputerDialog::deviceDetailsReceived(DeviceDetails *newDeviceDetails) +{ + deviceDetails = newDeviceDetails; + reloadValues(); +} + +void ConfigureDiveComputerDialog::reloadValues() +{ + // Enable the buttons to do operations on this data + ui.saveSettingsPushButton->setEnabled(true); + ui.backupButton->setEnabled(true); + + switch (ui.dcStackedWidget->currentIndex()) { + case 0: + reloadValuesOSTC3(); + break; + case 1: + reloadValuesSuuntoVyper(); + break; + case 2: + reloadValuesOSTC(); + break; + } +} + +void ConfigureDiveComputerDialog::reloadValuesOSTC3() +{ + ui.serialNoLineEdit->setText(deviceDetails->serialNo); + ui.firmwareVersionLineEdit->setText(deviceDetails->firmwareVersion); + ui.customTextLlineEdit->setText(deviceDetails->customText); + ui.modelLineEdit->setText(deviceDetails->model); + ui.diveModeComboBox->setCurrentIndex(deviceDetails->diveMode); + ui.saturationSpinBox->setValue(deviceDetails->saturation); + ui.desaturationSpinBox->setValue(deviceDetails->desaturation); + ui.lastDecoSpinBox->setValue(deviceDetails->lastDeco); + ui.brightnessComboBox->setCurrentIndex(deviceDetails->brightness); + ui.unitsComboBox->setCurrentIndex(deviceDetails->units); + ui.samplingRateComboBox->setCurrentIndex(deviceDetails->samplingRate); + ui.salinitySpinBox->setValue(deviceDetails->salinity); + ui.diveModeColour->setCurrentIndex(deviceDetails->diveModeColor); + ui.languageComboBox->setCurrentIndex(deviceDetails->language); + ui.dateFormatComboBox->setCurrentIndex(deviceDetails->dateFormat); + ui.compassGainComboBox->setCurrentIndex(deviceDetails->compassGain); + ui.safetyStopCheckBox->setChecked(deviceDetails->safetyStop); + ui.gfHighSpinBox->setValue(deviceDetails->gfHigh); + ui.gfLowSpinBox->setValue(deviceDetails->gfLow); + ui.pressureSensorOffsetSpinBox->setValue(deviceDetails->pressureSensorOffset); + ui.ppO2MinSpinBox->setValue(deviceDetails->ppO2Min); + ui.ppO2MaxSpinBox->setValue(deviceDetails->ppO2Max); + ui.futureTTSSpinBox->setValue(deviceDetails->futureTTS); + ui.ccrModeComboBox->setCurrentIndex(deviceDetails->ccrMode); + ui.decoTypeComboBox->setCurrentIndex(deviceDetails->decoType); + ui.aGFSelectableCheckBox->setChecked(deviceDetails->aGFSelectable); + ui.aGFHighSpinBox->setValue(deviceDetails->aGFHigh); + ui.aGFLowSpinBox->setValue(deviceDetails->aGFLow); + ui.calibrationGasSpinBox->setValue(deviceDetails->calibrationGas); + ui.flipScreenCheckBox->setChecked(deviceDetails->flipScreen); + ui.setPointFallbackCheckBox->setChecked(deviceDetails->setPointFallback); + ui.leftButtonSensitivity->setValue(deviceDetails->leftButtonSensitivity); + ui.rightButtonSensitivity->setValue(deviceDetails->rightButtonSensitivity); + ui.bottomGasConsumption->setValue(deviceDetails->bottomGasConsumption); + ui.decoGasConsumption->setValue(deviceDetails->decoGasConsumption); + ui.modWarning->setChecked(deviceDetails->modWarning); + ui.dynamicAscendRate->setChecked(deviceDetails->dynamicAscendRate); + ui.graphicalSpeedIndicator->setChecked(deviceDetails->graphicalSpeedIndicator); + ui.alwaysShowppO2->setChecked(deviceDetails->alwaysShowppO2); + + //load gas 1 values + ui.ostc3GasTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->gas1.oxygen))); + ui.ostc3GasTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->gas1.helium))); + ui.ostc3GasTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->gas1.type))); + ui.ostc3GasTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->gas1.depth))); + + //load gas 2 values + ui.ostc3GasTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->gas2.oxygen))); + ui.ostc3GasTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->gas2.helium))); + ui.ostc3GasTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->gas2.type))); + ui.ostc3GasTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->gas2.depth))); + + //load gas 3 values + ui.ostc3GasTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->gas3.oxygen))); + ui.ostc3GasTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->gas3.helium))); + ui.ostc3GasTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->gas3.type))); + ui.ostc3GasTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->gas3.depth))); + + //load gas 4 values + ui.ostc3GasTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->gas4.oxygen))); + ui.ostc3GasTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->gas4.helium))); + ui.ostc3GasTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->gas4.type))); + ui.ostc3GasTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->gas4.depth))); + + //load gas 5 values + ui.ostc3GasTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->gas5.oxygen))); + ui.ostc3GasTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->gas5.helium))); + ui.ostc3GasTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->gas5.type))); + ui.ostc3GasTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->gas5.depth))); + + //load dil 1 values + ui.ostc3DilTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->dil1.oxygen))); + ui.ostc3DilTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->dil1.helium))); + ui.ostc3DilTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->dil1.type))); + ui.ostc3DilTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->dil1.depth))); + + //load dil 2 values + ui.ostc3DilTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->dil2.oxygen))); + ui.ostc3DilTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->dil2.helium))); + ui.ostc3DilTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->dil2.type))); + ui.ostc3DilTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->dil2.depth))); + + //load dil 3 values + ui.ostc3DilTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->dil3.oxygen))); + ui.ostc3DilTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->dil3.helium))); + ui.ostc3DilTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->dil3.type))); + ui.ostc3DilTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->dil3.depth))); + + //load dil 4 values + ui.ostc3DilTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->dil4.oxygen))); + ui.ostc3DilTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->dil4.helium))); + ui.ostc3DilTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->dil4.type))); + ui.ostc3DilTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->dil4.depth))); + + //load dil 5 values + ui.ostc3DilTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->dil5.oxygen))); + ui.ostc3DilTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->dil5.helium))); + ui.ostc3DilTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->dil5.type))); + ui.ostc3DilTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->dil5.depth))); + + //load set point 1 values + ui.ostc3SetPointTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->sp1.sp))); + ui.ostc3SetPointTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->sp1.depth))); + + //load set point 2 values + ui.ostc3SetPointTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->sp2.sp))); + ui.ostc3SetPointTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->sp2.depth))); + + //load set point 3 values + ui.ostc3SetPointTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->sp3.sp))); + ui.ostc3SetPointTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->sp3.depth))); + + //load set point 4 values + ui.ostc3SetPointTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->sp4.sp))); + ui.ostc3SetPointTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->sp4.depth))); + + //load set point 5 values + ui.ostc3SetPointTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->sp5.sp))); + ui.ostc3SetPointTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->sp5.depth))); +} + +void ConfigureDiveComputerDialog::reloadValuesOSTC() +{ + /* +# Not in OSTC +setBrightness +setCalibrationGas +setCompassGain +setDiveMode - Bult into setDecoType +setDiveModeColor - Lots of different colors +setFlipScreen +setLanguage +setPressureSensorOffset +setUnits +setSetPointFallback +setCcrMode +# Not in OSTC3 +setNumberOfDives +*/ + ui.serialNoLineEdit_3->setText(deviceDetails->serialNo); + ui.firmwareVersionLineEdit_3->setText(deviceDetails->firmwareVersion); + ui.customTextLlineEdit_3->setText(deviceDetails->customText); + ui.saturationSpinBox_3->setValue(deviceDetails->saturation); + ui.desaturationSpinBox_3->setValue(deviceDetails->desaturation); + ui.lastDecoSpinBox_3->setValue(deviceDetails->lastDeco); + ui.samplingRateSpinBox_3->setValue(deviceDetails->samplingRate); + ui.salinityDoubleSpinBox_3->setValue((double)deviceDetails->salinity / 100.0); + ui.dateFormatComboBox_3->setCurrentIndex(deviceDetails->dateFormat); + ui.safetyStopCheckBox_3->setChecked(deviceDetails->safetyStop); + ui.gfHighSpinBox_3->setValue(deviceDetails->gfHigh); + ui.gfLowSpinBox_3->setValue(deviceDetails->gfLow); + ui.ppO2MinSpinBox_3->setValue(deviceDetails->ppO2Min); + ui.ppO2MaxSpinBox_3->setValue(deviceDetails->ppO2Max); + ui.futureTTSSpinBox_3->setValue(deviceDetails->futureTTS); + ui.decoTypeComboBox_3->setCurrentIndex(deviceDetails->decoType); + ui.aGFSelectableCheckBox_3->setChecked(deviceDetails->aGFSelectable); + ui.aGFHighSpinBox_3->setValue(deviceDetails->aGFHigh); + ui.aGFLowSpinBox_3->setValue(deviceDetails->aGFLow); + ui.numberOfDivesSpinBox_3->setValue(deviceDetails->numberOfDives); + ui.bottomGasConsumption_3->setValue(deviceDetails->bottomGasConsumption); + ui.decoGasConsumption_3->setValue(deviceDetails->decoGasConsumption); + ui.graphicalSpeedIndicator_3->setChecked(deviceDetails->graphicalSpeedIndicator); + + //load gas 1 values + ui.ostcGasTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->gas1.oxygen))); + ui.ostcGasTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->gas1.helium))); + ui.ostcGasTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->gas1.type))); + ui.ostcGasTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->gas1.depth))); + + //load gas 2 values + ui.ostcGasTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->gas2.oxygen))); + ui.ostcGasTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->gas2.helium))); + ui.ostcGasTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->gas2.type))); + ui.ostcGasTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->gas2.depth))); + + //load gas 3 values + ui.ostcGasTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->gas3.oxygen))); + ui.ostcGasTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->gas3.helium))); + ui.ostcGasTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->gas3.type))); + ui.ostcGasTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->gas3.depth))); + + //load gas 4 values + ui.ostcGasTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->gas4.oxygen))); + ui.ostcGasTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->gas4.helium))); + ui.ostcGasTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->gas4.type))); + ui.ostcGasTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->gas4.depth))); + + //load gas 5 values + ui.ostcGasTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->gas5.oxygen))); + ui.ostcGasTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->gas5.helium))); + ui.ostcGasTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->gas5.type))); + ui.ostcGasTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->gas5.depth))); + + //load dil 1 values + ui.ostcDilTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->dil1.oxygen))); + ui.ostcDilTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->dil1.helium))); + ui.ostcDilTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->dil1.type))); + ui.ostcDilTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->dil1.depth))); + + //load dil 2 values + ui.ostcDilTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->dil2.oxygen))); + ui.ostcDilTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->dil2.helium))); + ui.ostcDilTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->dil2.type))); + ui.ostcDilTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->dil2.depth))); + + //load dil 3 values + ui.ostcDilTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->dil3.oxygen))); + ui.ostcDilTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->dil3.helium))); + ui.ostcDilTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->dil3.type))); + ui.ostcDilTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->dil3.depth))); + + //load dil 4 values + ui.ostcDilTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->dil4.oxygen))); + ui.ostcDilTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->dil4.helium))); + ui.ostcDilTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->dil4.type))); + ui.ostcDilTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->dil4.depth))); + + //load dil 5 values + ui.ostcDilTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->dil5.oxygen))); + ui.ostcDilTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->dil5.helium))); + ui.ostcDilTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->dil5.type))); + ui.ostcDilTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->dil5.depth))); + + //load set point 1 values + ui.ostcSetPointTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->sp1.sp))); + ui.ostcSetPointTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->sp1.depth))); + + //load set point 2 values + ui.ostcSetPointTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->sp2.sp))); + ui.ostcSetPointTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->sp2.depth))); + + //load set point 3 values + ui.ostcSetPointTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->sp3.sp))); + ui.ostcSetPointTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->sp3.depth))); + + //load set point 4 values + ui.ostcSetPointTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->sp4.sp))); + ui.ostcSetPointTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->sp4.depth))); + + //load set point 5 values + ui.ostcSetPointTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->sp5.sp))); + ui.ostcSetPointTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->sp5.depth))); +} + +void ConfigureDiveComputerDialog::reloadValuesSuuntoVyper() +{ + const char *depth_unit; + ui.maxDepthDoubleSpinBox->setValue(get_depth_units(deviceDetails->maxDepth, NULL, &depth_unit)); + ui.maxDepthDoubleSpinBox->setSuffix(depth_unit); + ui.totalTimeSpinBox->setValue(deviceDetails->totalTime); + ui.numberOfDivesSpinBox->setValue(deviceDetails->numberOfDives); + ui.modelLineEdit_1->setText(deviceDetails->model); + ui.firmwareVersionLineEdit_1->setText(deviceDetails->firmwareVersion); + ui.serialNoLineEdit_1->setText(deviceDetails->serialNo); + ui.customTextLlineEdit_1->setText(deviceDetails->customText); + ui.samplingRateComboBox_1->setCurrentIndex(deviceDetails->samplingRate == 60 ? 3 : (deviceDetails->samplingRate / 10) - 1); + ui.altitudeRangeComboBox->setCurrentIndex(deviceDetails->altitude); + ui.personalSafetyComboBox->setCurrentIndex(deviceDetails->personalSafety); + ui.timeFormatComboBox->setCurrentIndex(deviceDetails->timeFormat); + ui.unitsComboBox_1->setCurrentIndex(deviceDetails->units); + ui.diveModeComboBox_1->setCurrentIndex(deviceDetails->diveMode); + ui.lightCheckBox->setChecked(deviceDetails->lightEnabled); + ui.lightSpinBox->setValue(deviceDetails->light); + ui.alarmDepthCheckBox->setChecked(deviceDetails->alarmDepthEnabled); + ui.alarmDepthDoubleSpinBox->setValue(get_depth_units(deviceDetails->alarmDepth, NULL, &depth_unit)); + ui.alarmDepthDoubleSpinBox->setSuffix(depth_unit); + ui.alarmTimeCheckBox->setChecked(deviceDetails->alarmTimeEnabled); + ui.alarmTimeSpinBox->setValue(deviceDetails->alarmTime); +} + +void ConfigureDiveComputerDialog::on_backupButton_clicked() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("Backup.xml"); + QString backupPath = QFileDialog::getSaveFileName(this, tr("Backup dive computer settings"), + filename, tr("Backup files (*.xml)")); + if (!backupPath.isEmpty()) { + populateDeviceDetails(); + if (!config->saveXMLBackup(backupPath, deviceDetails, &device_data)) { + QMessageBox::critical(this, tr("XML backup error"), + tr("An error occurred while saving the backup file.\n%1") + .arg(config->lastError)); + } else { + QMessageBox::information(this, tr("Backup succeeded"), + tr("Your settings have been saved to: %1") + .arg(backupPath)); + } + } +} + +void ConfigureDiveComputerDialog::on_restoreBackupButton_clicked() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("Backup.xml"); + QString restorePath = QFileDialog::getOpenFileName(this, tr("Restore dive computer settings"), + filename, tr("Backup files (*.xml)")); + if (!restorePath.isEmpty()) { + // Fw update is no longer a option, needs to be done on a untouched device + ui.updateFirmwareButton->setEnabled(false); + if (!config->restoreXMLBackup(restorePath, deviceDetails)) { + QMessageBox::critical(this, tr("XML restore error"), + tr("An error occurred while restoring the backup file.\n%1") + .arg(config->lastError)); + } else { + reloadValues(); + QMessageBox::information(this, tr("Restore succeeded"), + tr("Your settings have been restored successfully.")); + } + } +} + +void ConfigureDiveComputerDialog::on_updateFirmwareButton_clicked() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath(); + QString firmwarePath = QFileDialog::getOpenFileName(this, tr("Select firmware file"), + filename, tr("All files (*.*)")); + if (!firmwarePath.isEmpty()) { + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + + config->startFirmwareUpdate(firmwarePath, &device_data); + } +} + + +void ConfigureDiveComputerDialog::on_DiveComputerList_currentRowChanged(int currentRow) +{ + switch (currentRow) { + case 0: + selected_vendor = "Heinrichs Weikamp"; + selected_product = "OSTC 3"; + fw_upgrade_possible = true; + break; + case 1: + selected_vendor = "Suunto"; + selected_product = "Vyper"; + fw_upgrade_possible = false; + break; + case 2: + selected_vendor = "Heinrichs Weikamp"; + selected_product = "OSTC 2N"; + fw_upgrade_possible = true; + break; + default: + /* Not Supported */ + return; + } + + int dcType = DC_TYPE_SERIAL; + + + if (selected_vendor == QString("Uemis")) + dcType = DC_TYPE_UEMIS; + fill_device_list(dcType); +} + +void ConfigureDiveComputerDialog::checkLogFile(int state) +{ + ui.chooseLogFile->setEnabled(state == Qt::Checked); + device_data.libdc_log = (state == Qt::Checked); + if (state == Qt::Checked && logFile.isEmpty()) { + pickLogFile(); + } +} + +void ConfigureDiveComputerDialog::pickLogFile() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("subsurface.log"); + logFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer download logfile"), + filename, tr("Log files (*.log)")); + if (!logFile.isEmpty()) { + free(logfile_name); + logfile_name = strdup(logFile.toUtf8().data()); + } +} + +#ifdef BT_SUPPORT +void ConfigureDiveComputerDialog::selectRemoteBluetoothDevice() +{ + if (!btDeviceSelectionDialog) { + btDeviceSelectionDialog = new BtDeviceSelectionDialog(this); + connect(btDeviceSelectionDialog, SIGNAL(finished(int)), + this, SLOT(bluetoothSelectionDialogIsFinished(int))); + } + + btDeviceSelectionDialog->show(); +} + +void ConfigureDiveComputerDialog::bluetoothSelectionDialogIsFinished(int result) +{ + if (result == QDialog::Accepted) { + ui.device->setCurrentText(btDeviceSelectionDialog->getSelectedDeviceAddress()); + device_data.bluetooth_mode = true; + + ui.progressBar->setFormat("Connecting to device..."); + dc_open(); + } +} +#endif + +void ConfigureDiveComputerDialog::dc_open() +{ + getDeviceData(); + + QString err = config->dc_open(&device_data); + + if (err != "") + return ui.progressBar->setFormat(err); + + ui.retrieveDetails->setEnabled(true); + ui.resetButton->setEnabled(true); + ui.updateFirmwareButton->setEnabled(true); + ui.disconnectButton->setEnabled(true); + ui.restoreBackupButton->setEnabled(true); + ui.connectButton->setEnabled(false); + ui.bluetoothMode->setEnabled(false); + ui.DiveComputerList->setEnabled(false); + ui.logToFile->setEnabled(false); + if (fw_upgrade_possible) + ui.updateFirmwareButton->setEnabled(true); + ui.progressBar->setFormat("Connected to device"); +} + +void ConfigureDiveComputerDialog::dc_close() +{ + config->dc_close(&device_data); + + ui.retrieveDetails->setEnabled(false); + ui.resetButton->setEnabled(false); + ui.updateFirmwareButton->setEnabled(false); + ui.disconnectButton->setEnabled(false); + ui.connectButton->setEnabled(true); + ui.bluetoothMode->setEnabled(true); + ui.backupButton->setEnabled(false); + ui.saveSettingsPushButton->setEnabled(false); + ui.restoreBackupButton->setEnabled(false); + ui.DiveComputerList->setEnabled(true); + ui.logToFile->setEnabled(true); + ui.updateFirmwareButton->setEnabled(false); + ui.progressBar->setFormat("Disonnected from device"); + ui.progressBar->setValue(0); +} diff --git a/desktop-widgets/configuredivecomputerdialog.h b/desktop-widgets/configuredivecomputerdialog.h new file mode 100644 index 000000000..9ad30ac67 --- /dev/null +++ b/desktop-widgets/configuredivecomputerdialog.h @@ -0,0 +1,149 @@ +#ifndef CONFIGUREDIVECOMPUTERDIALOG_H +#define CONFIGUREDIVECOMPUTERDIALOG_H + +#include +#include +#include "ui_configuredivecomputerdialog.h" +#include "subsurface-core/libdivecomputer.h" +#include "configuredivecomputer.h" +#include +#include +#ifdef BT_SUPPORT +#include "btdeviceselectiondialog.h" +#endif + +class GasSpinBoxItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + enum column_type { + PERCENT, + DEPTH, + SETPOINT, + }; + + GasSpinBoxItemDelegate(QObject *parent = 0, column_type type = PERCENT); + ~GasSpinBoxItemDelegate(); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + +private: + column_type type; +}; + +class GasTypeComboBoxItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + enum computer_type { + OSTC3, + OSTC, + }; + + GasTypeComboBoxItemDelegate(QObject *parent = 0, computer_type type = OSTC3); + ~GasTypeComboBoxItemDelegate(); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + +private: + computer_type type; +}; + +class ConfigureDiveComputerDialog : public QDialog { + Q_OBJECT + +public: + explicit ConfigureDiveComputerDialog(QWidget *parent = 0); + ~ConfigureDiveComputerDialog(); + +protected: + void closeEvent(QCloseEvent *event); + +private +slots: + void checkLogFile(int state); + void pickLogFile(); + void readSettings(); + void resetSettings(); + void configMessage(QString msg); + void configError(QString err); + void on_cancel_clicked(); + void on_saveSettingsPushButton_clicked(); + void deviceDetailsReceived(DeviceDetails *newDeviceDetails); + void reloadValues(); + void on_backupButton_clicked(); + + void on_restoreBackupButton_clicked(); + + + void on_updateFirmwareButton_clicked(); + + void on_DiveComputerList_currentRowChanged(int currentRow); + + void dc_open(); + void dc_close(); + +#ifdef BT_SUPPORT + void bluetoothSelectionDialogIsFinished(int result); + void selectRemoteBluetoothDevice(); +#endif + +private: + Ui::ConfigureDiveComputerDialog ui; + + QString logFile; + + QStringList vendorList; + QHash productList; + + ConfigureDiveComputer *config; + device_data_t device_data; + void getDeviceData(); + + QHash descriptorLookup; + void fill_device_list(int dc_type); + void fill_computer_list(); + + DeviceDetails *deviceDetails; + void populateDeviceDetails(); + void populateDeviceDetailsOSTC3(); + void populateDeviceDetailsOSTC(); + void populateDeviceDetailsSuuntoVyper(); + void reloadValuesOSTC3(); + void reloadValuesOSTC(); + void reloadValuesSuuntoVyper(); + + QString selected_vendor; + QString selected_product; + bool fw_upgrade_possible; + +#ifdef BT_SUPPORT + BtDeviceSelectionDialog *btDeviceSelectionDialog; +#endif +}; + +class OstcFirmwareCheck : QObject { + Q_OBJECT +public: + explicit OstcFirmwareCheck(QString product); + void checkLatest(QWidget *parent, device_data_t *data); +public +slots: + void parseOstcFwVersion(QNetworkReply *reply); + void saveOstcFirmware(QNetworkReply *reply); + +private: + void upgradeFirmware(); + device_data_t devData; + QString latestFirmwareAvailable; + QString latestFirmwareHexFile; + QString storeFirmware; + QWidget *parent; + QNetworkAccessManager manager; +}; + +#endif // CONFIGUREDIVECOMPUTERDIALOG_H diff --git a/desktop-widgets/configuredivecomputerdialog.ui b/desktop-widgets/configuredivecomputerdialog.ui new file mode 100644 index 000000000..0986d71ca --- /dev/null +++ b/desktop-widgets/configuredivecomputerdialog.ui @@ -0,0 +1,2758 @@ + + + ConfigureDiveComputerDialog + + + + 0 + 0 + 842 + 614 + + + + Configure dive computer + + + + + + + + Device or mount point + + + device + + + + + + + + + + 0 + 0 + + + + true + + + + + + + Connect via Bluetooth + + + + + + + Connect + + + + + + + false + + + Disconnect + + + + + + + + + + + + + false + + + Retrieve available details + + + + + + + false + + + Read settings from backup file or from device before writing to the device + + + Save changes to device + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Read settings from backup file or from device before writing to a backup file + + + Backup + + + + + + + false + + + Restore backup + + + + + + + false + + + Update firmware + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save libdivecomputer logfile + + + + + + + ... + + + + + + + Cancel + + + + + + + + + + 0 + 2 + + + + Qt::Horizontal + + + + + 240 + 16777215 + + + + + 12 + + + + + 64 + 64 + + + + + OSTC 3,Sport,Cr,2 + + + + :/icons/ostc3.png:/icons/ostc3.png + + + + + Suunto Vyper family + + + + :/icons/suunto_vyper.png:/icons/suunto_vyper.png + + + + + OSTC, Mk.2/2N/2C + + + + :/icons/ostc2n.png:/icons/ostc2n.png + + + + + + 0 + + + + + + + 0 + + + + Basic settings + + + + + + + English + + + + + German + + + + + French + + + + + Italian + + + + + + + + + m/°C + + + + + ft/°F + + + + + + + + Serial No. + + + serialNoLineEdit + + + + + + + + 1 + 0 + + + + true + + + + + + + Firmware version + + + firmwareVersionLineEdit + + + + + + + true + + + + + + + Language + + + languageComboBox + + + + + + + + MMDDYY + + + + + DDMMYY + + + + + YYMMDD + + + + + + + + + Eco + + + + + Medium + + + + + High + + + + + + + + Date format + + + dateFormatComboBox + + + + + + + Brightness + + + brightnessComboBox + + + + + + + Units + + + unitsComboBox + + + + + + + Salinity (0-5%) + + + salinitySpinBox + + + + + + + % + + + 5 + + + + + + + + 1 + 0 + + + + + 230LSB/Gauss + + + + + 330LSB/Gauss + + + + + 390LSB/Gauss + + + + + 440LSB/Gauss + + + + + 660LSB/Gauss + + + + + 820LSB/Gauss + + + + + 1090LSB/Gauss + + + + + 1370LSB/Gauss + + + + + + + + Qt::Vertical + + + + 20 + 177 + + + + + + + + false + + + Reset device to default settings + + + + + + + Compass gain + + + compassGainComboBox + + + + + + + Custom text + + + customTextLlineEdit + + + + + + + + 1 + 0 + + + + 60 + + + + + + + Computer model + + + + + + + true + + + + + + + Dive mode + + + diveModeComboBox + + + + + + + + OC + + + + + CC + + + + + Gauge + + + + + Apnea + + + + + + + + Sampling rate + + + samplingRateComboBox + + + + + + + + 2s + + + + + 10s + + + + + + + + Dive mode color + + + diveModeColour + + + + + + + + Standard + + + + + Red + + + + + Green + + + + + Blue + + + + + + + + Sync dive computer time with PC + + + + + + + Show safety stop + + + + + + + + Advanced settings + + + + + + Left button sensitivity + + + + + + + Always show ppO2 + + + + + + + Alt GF can be selected underwater + + + + + + + Future TTS + + + + + + + Pressure sensor offset + + + + + + + GFLow + + + + + + + % + + + 10 + + + 100 + + + 30 + + + + + + + GFHigh + + + + + + + % + + + 60 + + + 110 + + + 85 + + + + + + + Desaturation + + + desaturationSpinBox + + + + + + + % + + + 60 + + + 100 + + + 90 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + m + + + 3 + + + 6 + + + + + + + Decotype + + + + + + + false + + + % + + + 60 + + + 100 + + + 60 + + + + + + + mbar + + + -20 + + + 20 + + + + + + + 1 + + + + ZH-L16 + + + + + ZH-L16+GF + + + + + + + + min + + + 9 + + + + + + + Last deco + + + lastDecoSpinBox + + + + + + + % + + + 100 + + + 140 + + + 110 + + + + + + + Alt GFLow + + + + + + + false + + + % + + + 70 + + + 120 + + + 85 + + + + + + + Alt GFHigh + + + + + + + Saturation + + + saturationSpinBox + + + + + + + Flip screen + + + + + + + Right button sensitivity + + + + + + + MOD warning + + + + + + + Graphical speed indicator + + + + + + + Dynamic ascent rate + + + + + + + Bottom gas consumption + + + + + + + Deco gas consumption + + + + + + + % + + + 20 + + + 100 + + + 40 + + + + + + + % + + + 20 + + + 100 + + + 40 + + + + + + + â„“/min + + + 5 + + + 50 + + + 20 + + + + + + + â„“/min + + + 5 + + + 50 + + + 20 + + + + + + + + Gas settings + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %Oâ‚‚ + + + + + %He + + + + + Type + + + + + Change depth + + + + + Gas 1 + + + + + Gas 2 + + + + + Gas 3 + + + + + Gas 4 + + + + + Gas 5 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %Oâ‚‚ + + + + + %He + + + + + Type + + + + + Change depth + + + + + Dil 1 + + + + + Dil 2 + + + + + Dil 3 + + + + + Dil 4 + + + + + Dil 5 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Setpoint + + + + + Change depth + + + + + SP 1 + + + + + SP 2 + + + + + SP 3 + + + + + SP 4 + + + + + SP 5 + + + + + + + + Oâ‚‚ in calibration gas + + + + + + + % + + + 21 + + + 100 + + + 21 + + + + + + + + Fixed setpoint + + + + + Sensor + + + + + + + + Setpoint fallback + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + cbar + + + 120 + + + 160 + + + 160 + + + + + + + cbar + + + 16 + + + 19 + + + 19 + + + + + + + pOâ‚‚ max + + + + + + + pOâ‚‚ min + + + + + + + + + + + + + + + 0 + + + + Basic settings + + + + + + + 1 + 0 + + + + true + + + 200.000000000000000 + + + + + + + Safety level + + + + + + + + A0 (0m - 300m) + + + + + A1 (300m - 1500m) + + + + + A2 (1500m - 3000m) + + + + + + + + Altitude range + + + + + + + Model + + + + + + + + 1 + 0 + + + + 30 + + + + + + + Number of dives + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + Serial No. + + + serialNoLineEdit_1 + + + + + + + + 1 + 0 + + + + true + + + + + + + Firmware version + + + firmwareVersionLineEdit_1 + + + + + + + true + + + + + + + Max depth + + + + + + + true + + + 5000 + + + + + + + Custom text + + + customTextLlineEdit_1 + + + + + + + + Air + + + + + Nitrox + + + + + Gauge + + + + + + + + + P0 (none) + + + + + P1 (medium) + + + + + P2 (high) + + + + + + + + Sample rate + + + + + + + + 10s + + + + + 20s + + + + + 30s + + + + + 60s + + + + + + + + Total dive time + + + + + + + Computer model + + + + + + + true + + + + + + + true + + + min + + + 0 + + + 5000000 + + + + + + + + 24h + + + + + 12h + + + + + + + + Time format + + + + + + + Units + + + + + + + + Imperial + + + + + Metric + + + + + + + + false + + + s + + + + + + + Light + + + + + + + false + + + 200.000000000000000 + + + + + + + Depth alarm + + + + + + + false + + + min + + + 999 + + + + + + + Time alarm + + + + + + + + + + + + + + + 0 + + + + Basic settings + + + + + + Salinity + + + salinitySpinBox + + + + + + + Serial No. + + + serialNoLineEdit + + + + + + + + 1 + 0 + + + + true + + + + + + + Firmware version + + + firmwareVersionLineEdit_3 + + + + + + + true + + + + + + + Custom text + + + customTextLlineEdit_3 + + + + + + + + 1 + 0 + + + + 23 + + + + + + + kg/â„“ + + + 1.000000000000000 + + + 1.040000000000000 + + + 0.010000000000000 + + + + + + + Qt::Vertical + + + + 20 + 177 + + + + + + + + Sync dive computer time with PC + + + + + + + Show safety stop + + + + + + + + MM/DD/YY + + + + + DD/MM/YY + + + + + YY/MM/DD + + + + + + + + Number of dives + + + + + + + true + + + + + + + + 0 + 0 + + + + 1 + + + 120 + + + 10 + + + + + + + Sampling rate + + + samplingRateComboBox + + + + + + + Date format + + + dateFormatComboBox + + + + + + + + Advanced settings + + + + + + Alt GF can be selected underwater + + + + + + + Desaturation + + + desaturationSpinBox + + + + + + + Future TTS + + + + + + + % + + + 60 + + + 100 + + + 90 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + m + + + 3 + + + 6 + + + + + + + Decotype + + + + + + + 4 + + + + ZH-L16 + + + + + Gauge + + + + + ZH-L16 CC + + + + + Apnoea + + + + + L16-GF OC + + + + + L16-GF CC + + + + + PSCR-GF + + + + + + + + min + + + 9 + + + + + + + false + + + % + + + 5 + + + 255 + + + 30 + + + + + + + Last deco + + + lastDecoSpinBox + + + + + + + % + + + 100 + + + 140 + + + 110 + + + + + + + Alt GFLow + + + + + + + false + + + % + + + 5 + + + 255 + + + 90 + + + + + + + Alt GFHigh + + + + + + + Saturation + + + saturationSpinBox + + + + + + + GFHigh + + + + + + + % + + + 10 + + + 100 + + + 30 + + + + + + + GFLow + + + + + + + % + + + 60 + + + 110 + + + 85 + + + + + + + Graphical speed indicator + + + + + + + â„“/min + + + 5 + + + 50 + + + 20 + + + + + + + â„“/min + + + 5 + + + 50 + + + 20 + + + + + + + Bottom gas consumption + + + + + + + Deco gas consumption + + + + + + + + Gas settings + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %Oâ‚‚ + + + + + %He + + + + + Type + + + + + Change depth + + + + + Gas 1 + + + + + Gas 2 + + + + + Gas 3 + + + + + Gas 4 + + + + + Gas 5 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %Oâ‚‚ + + + + + %He + + + + + Type + + + + + Change depth + + + + + Dil 1 + + + + + Dil 2 + + + + + Dil 3 + + + + + Dil 4 + + + + + Dil 5 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + Setpoint + + + + + Change depth + + + + + SP 1 + + + + + SP 2 + + + + + SP 3 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + cbar + + + 120 + + + 180 + + + 160 + + + + + + + cbar + + + 16 + + + 21 + + + 19 + + + + + + + pOâ‚‚ max + + + + + + + pOâ‚‚ min + + + + + + + + + + + + + + + + 0 + + + + + + + + + + device + retrieveDetails + saveSettingsPushButton + backupButton + restoreBackupButton + cancel + + + + + + + DiveComputerList + currentRowChanged(int) + dcStackedWidget + setCurrentIndex(int) + + + 20 + 20 + + + 20 + 20 + + + + + lightCheckBox + toggled(bool) + lightSpinBox + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + alarmDepthCheckBox + toggled(bool) + alarmDepthDoubleSpinBox + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + alarmTimeCheckBox + toggled(bool) + alarmTimeSpinBox + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + aGFSelectableCheckBox + toggled(bool) + aGFHighSpinBox + setEnabled(bool) + + + 340 + 229 + + + 686 + 265 + + + + + aGFSelectableCheckBox + toggled(bool) + aGFLowSpinBox + setEnabled(bool) + + + 340 + 229 + + + 686 + 229 + + + + + aGFSelectableCheckBox_3 + toggled(bool) + aGFHighSpinBox_3 + setEnabled(bool) + + + 340 + 229 + + + 686 + 265 + + + + + aGFSelectableCheckBox_3 + toggled(bool) + aGFLowSpinBox_3 + setEnabled(bool) + + + 340 + 229 + + + 686 + 229 + + + + + diff --git a/desktop-widgets/css/tableviews.css b/desktop-widgets/css/tableviews.css new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/divecomponentselection.ui b/desktop-widgets/divecomponentselection.ui new file mode 100644 index 000000000..8262a5c2a --- /dev/null +++ b/desktop-widgets/divecomponentselection.ui @@ -0,0 +1,190 @@ + + + DiveComponentSelectionDialog + + + Qt::WindowModal + + + + 0 + 0 + 401 + 317 + + + + + 0 + 0 + + + + Component selection + + + + :/subsurface-icon + + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 0 + + + + Which components would you like to copy + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Dive site + + + + + + + Suit + + + + + + + Visibility + + + + + + + Notes + + + + + + + Tags + + + + + + + Weights + + + + + + + Cylinders + + + + + + + Divemaster + + + + + + + Buddy + + + + + + + Rating + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + DiveComponentSelectionDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DiveComponentSelectionDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/divecomputermanagementdialog.cpp b/desktop-widgets/divecomputermanagementdialog.cpp new file mode 100644 index 000000000..fd9273ffb --- /dev/null +++ b/desktop-widgets/divecomputermanagementdialog.cpp @@ -0,0 +1,69 @@ +#include "divecomputermanagementdialog.h" +#include "mainwindow.h" +#include "helpers.h" +#include "divecomputermodel.h" +#include +#include + +DiveComputerManagementDialog::DiveComputerManagementDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), + model(0) +{ + ui.setupUi(this); + init(); + connect(ui.tableView, SIGNAL(clicked(QModelIndex)), this, SLOT(tryRemove(QModelIndex))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void DiveComputerManagementDialog::init() +{ + delete model; + model = new DiveComputerModel(dcList.dcMap); + ui.tableView->setModel(model); +} + +DiveComputerManagementDialog *DiveComputerManagementDialog::instance() +{ + static DiveComputerManagementDialog *self = new DiveComputerManagementDialog(MainWindow::instance()); + self->setAttribute(Qt::WA_QuitOnClose, false); + return self; +} + +void DiveComputerManagementDialog::update() +{ + model->update(); + ui.tableView->resizeColumnsToContents(); + ui.tableView->setColumnWidth(DiveComputerModel::REMOVE, 22); + layout()->activate(); +} + +void DiveComputerManagementDialog::tryRemove(const QModelIndex &index) +{ + if (index.column() != DiveComputerModel::REMOVE) + return; + + QMessageBox::StandardButton response = QMessageBox::question( + this, TITLE_OR_TEXT( + tr("Remove the selected dive computer?"), + tr("Are you sure that you want to \n remove the selected dive computer?")), + QMessageBox::Ok | QMessageBox::Cancel); + + if (response == QMessageBox::Ok) + model->remove(index); +} + +void DiveComputerManagementDialog::accept() +{ + model->keepWorkingList(); + hide(); + close(); +} + +void DiveComputerManagementDialog::reject() +{ + model->dropWorkingList(); + hide(); + close(); +} diff --git a/desktop-widgets/divecomputermanagementdialog.h b/desktop-widgets/divecomputermanagementdialog.h new file mode 100644 index 000000000..d065a0208 --- /dev/null +++ b/desktop-widgets/divecomputermanagementdialog.h @@ -0,0 +1,29 @@ +#ifndef DIVECOMPUTERMANAGEMENTDIALOG_H +#define DIVECOMPUTERMANAGEMENTDIALOG_H +#include +#include "ui_divecomputermanagementdialog.h" + +class QModelIndex; +class DiveComputerModel; + +class DiveComputerManagementDialog : public QDialog { + Q_OBJECT + +public: + static DiveComputerManagementDialog *instance(); + void update(); + void init(); + +public +slots: + void tryRemove(const QModelIndex &index); + void accept(); + void reject(); + +private: + explicit DiveComputerManagementDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + Ui::DiveComputerManagementDialog ui; + DiveComputerModel *model; +}; + +#endif // DIVECOMPUTERMANAGEMENTDIALOG_H diff --git a/desktop-widgets/divecomputermanagementdialog.ui b/desktop-widgets/divecomputermanagementdialog.ui new file mode 100644 index 000000000..0e5db2c41 --- /dev/null +++ b/desktop-widgets/divecomputermanagementdialog.ui @@ -0,0 +1,81 @@ + + + DiveComputerManagementDialog + + + Qt::WindowModal + + + + 0 + 0 + 560 + 300 + + + + Edit dive computer nicknames + + + + :/subsurface-icon + + + + + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + DiveComputerManagementDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DiveComputerManagementDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/divelistview.cpp b/desktop-widgets/divelistview.cpp new file mode 100644 index 000000000..d2386ecf1 --- /dev/null +++ b/desktop-widgets/divelistview.cpp @@ -0,0 +1,1035 @@ +/* + * divelistview.cpp + * + * classes for the divelist of Subsurface + * + */ +#include "filtermodels.h" +#include "modeldelegates.h" +#include "mainwindow.h" +#include "divepicturewidget.h" +#include "display.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "qthelper.h" +#include "undocommands.h" +#include "divelistview.h" +#include "divepicturemodel.h" +#include "metrics.h" +#include "helpers.h" + +// # Date Rtg Dpth Dur Tmp Wght Suit Cyl Gas SAC OTU CNS Loc +static int defaultWidth[] = { 70, 140, 90, 50, 50, 50, 50, 70, 50, 50, 70, 50, 50, 500}; + +DiveListView::DiveListView(QWidget *parent) : QTreeView(parent), mouseClickSelection(false), sortColumn(0), + currentOrder(Qt::DescendingOrder), dontEmitDiveChangedSignal(false), selectionSaved(false) +{ + setItemDelegate(new DiveListDelegate(this)); + setUniformRowHeights(true); + setItemDelegateForColumn(DiveTripModel::RATING, new StarWidgetsDelegate(this)); + MultiFilterSortModel *model = MultiFilterSortModel::instance(); + model->setSortRole(DiveTripModel::SORT_ROLE); + model->setFilterKeyColumn(-1); // filter all columns + model->setFilterCaseSensitivity(Qt::CaseInsensitive); + setModel(model); + connect(model, SIGNAL(layoutChanged()), this, SLOT(fixMessyQtModelBehaviour())); + + setSortingEnabled(false); + setContextMenuPolicy(Qt::DefaultContextMenu); + setSelectionMode(ExtendedSelection); + header()->setContextMenuPolicy(Qt::ActionsContextMenu); + + const QFontMetrics metrics(defaultModelFont()); + int em = metrics.width('m'); + int zw = metrics.width('0'); + + // Fixes for the layout needed for mac +#ifdef Q_OS_MAC + int ht = metrics.height(); + header()->setMinimumHeight(ht + 4); +#endif + + // TODO FIXME we need this to get the header names + // can we find a smarter way? + DiveTripModel *tripModel = new DiveTripModel(this); + + // set the default width as a minimum between the hard-coded defaults, + // the header text width and the (assumed) content width, calculated + // based on type + for (int col = DiveTripModel::NR; col < DiveTripModel::COLUMNS; ++col) { + QString header_txt = tripModel->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString(); + int width = metrics.width(header_txt); + int sw = 0; + switch (col) { + case DiveTripModel::NR: + case DiveTripModel::DURATION: + sw = 8*zw; + break; + case DiveTripModel::DATE: + sw = 14*em; + break; + case DiveTripModel::RATING: + sw = static_cast(itemDelegateForColumn(col))->starSize().width(); + break; + case DiveTripModel::SUIT: + case DiveTripModel::SAC: + sw = 7*em; + break; + case DiveTripModel::LOCATION: + sw = 50*em; + break; + default: + sw = 5*em; + } + if (sw > width) + width = sw; + width += zw; // small padding + if (width > defaultWidth[col]) + defaultWidth[col] = width; + } + delete tripModel; + + + header()->setStretchLastSection(true); + + installEventFilter(this); +} + +DiveListView::~DiveListView() +{ + QSettings settings; + settings.beginGroup("ListWidget"); + // don't set a width for the last column - location is supposed to be "the rest" + for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS - 1; i++) { + if (isColumnHidden(i)) + continue; + // we used to hardcode them all to 100 - so that might still be in the settings + if (columnWidth(i) == 100 || columnWidth(i) == defaultWidth[i]) + settings.remove(QString("colwidth%1").arg(i)); + else + settings.setValue(QString("colwidth%1").arg(i), columnWidth(i)); + } + settings.remove(QString("colwidth%1").arg(DiveTripModel::COLUMNS - 1)); + settings.endGroup(); +} + +void DiveListView::setupUi() +{ + QSettings settings; + static bool firstRun = true; + if (firstRun) + backupExpandedRows(); + settings.beginGroup("ListWidget"); + /* if no width are set, use the calculated width for each column; + * for that to work we need to temporarily expand all rows */ + expandAll(); + for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS; i++) { + if (isColumnHidden(i)) + continue; + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + setColumnWidth(i, width.toInt()); + else + setColumnWidth(i, defaultWidth[i]); + } + settings.endGroup(); + if (firstRun) + restoreExpandedRows(); + else + collapseAll(); + firstRun = false; + setColumnWidth(lastVisibleColumn(), 10); +} + +int DiveListView::lastVisibleColumn() +{ + int lastColumn = -1; + for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS; i++) { + if (isColumnHidden(i)) + continue; + lastColumn = i; + } + return lastColumn; +} + +void DiveListView::backupExpandedRows() +{ + expandedRows.clear(); + for (int i = 0; i < model()->rowCount(); i++) + if (isExpanded(model()->index(i, 0))) + expandedRows.push_back(i); +} + +void DiveListView::restoreExpandedRows() +{ + setAnimated(false); + Q_FOREACH (const int &i, expandedRows) + setExpanded(model()->index(i, 0), true); + setAnimated(true); +} +void DiveListView::fixMessyQtModelBehaviour() +{ + QAbstractItemModel *m = model(); + for (int i = 0; i < model()->rowCount(); i++) + if (m->rowCount(m->index(i, 0)) != 0) + setFirstColumnSpanned(i, QModelIndex(), true); +} + +// this only remembers dives that were selected, not trips +void DiveListView::rememberSelection() +{ + selectedDives.clear(); + QItemSelection selection = selectionModel()->selection(); + Q_FOREACH (const QModelIndex &index, selection.indexes()) { + if (index.column() != 0) // We only care about the dives, so, let's stick to rows and discard columns. + continue; + struct dive *d = (struct dive *)index.data(DiveTripModel::DIVE_ROLE).value(); + if (d) { + selectedDives.insert(d->divetrip, get_divenr(d)); + } else { + struct dive_trip *t = (struct dive_trip *)index.data(DiveTripModel::TRIP_ROLE).value(); + if (t) + selectedDives.insert(t, -1); + } + } + selectionSaved = true; +} + +void DiveListView::restoreSelection() +{ + if (!selectionSaved) + return; + + selectionSaved = false; + dontEmitDiveChangedSignal = true; + unselectDives(); + dontEmitDiveChangedSignal = false; + Q_FOREACH (dive_trip_t *trip, selectedDives.keys()) { + QList divesOnTrip = getDivesInTrip(trip); + QList selectedDivesOnTrip = selectedDives.values(trip); + + // Only select trip if all of its dives were selected + if(selectedDivesOnTrip.contains(-1)) { + selectTrip(trip); + selectedDivesOnTrip.removeAll(-1); + } + selectDives(selectedDivesOnTrip); + } +} + +void DiveListView::selectTrip(dive_trip_t *trip) +{ + if (!trip) + return; + + QSortFilterProxyModel *m = qobject_cast(model()); + QModelIndexList match = m->match(m->index(0, 0), DiveTripModel::TRIP_ROLE, QVariant::fromValue(trip), 2, Qt::MatchRecursive); + QItemSelectionModel::SelectionFlags flags; + if (!match.count()) + return; + QModelIndex idx = match.first(); + flags = QItemSelectionModel::Select; + flags |= QItemSelectionModel::Rows; + selectionModel()->select(idx, flags); + expand(idx); +} + +// this is an odd one - when filtering the dive list the selection status of the trips +// is kept - but all other selections are lost. That's gets us into rather inconsistent state +// we call this function which clears the selection state of the trips as well, but does so +// without updating our internal "->selected" state. So once we called this function we can +// go back and select those dives that are still visible under the filter and everything +// works as expected +void DiveListView::clearTripSelection() +{ + // we want to make sure no trips are selected + disconnect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); + + Q_FOREACH (const QModelIndex &index, selectionModel()->selectedRows()) { + dive_trip_t *trip = static_cast(index.data(DiveTripModel::TRIP_ROLE).value()); + if (!trip) + continue; + selectionModel()->select(index, QItemSelectionModel::Deselect); + } + + connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); +} + +void DiveListView::unselectDives() +{ + // make sure we don't try to redraw the dives during the selection change + selected_dive = -1; + amount_selected = 0; + // clear the Qt selection + selectionModel()->clearSelection(); + // clearSelection should emit selectionChanged() but sometimes that + // appears not to happen + // since we are unselecting all dives there is no need to use deselect_dive() - that + // would only cause pointless churn + int i; + struct dive *dive; + for_each_dive (i, dive) { + dive->selected = false; + } +} + +QList DiveListView::selectedTrips() +{ + QList ret; + Q_FOREACH (const QModelIndex &index, selectionModel()->selectedRows()) { + dive_trip_t *trip = static_cast(index.data(DiveTripModel::TRIP_ROLE).value()); + if (!trip) + continue; + ret.push_back(trip); + } + return ret; +} + +void DiveListView::selectDive(int i, bool scrollto, bool toggle) +{ + if (i == -1) + return; + QSortFilterProxyModel *m = qobject_cast(model()); + QModelIndexList match = m->match(m->index(0, 0), DiveTripModel::DIVE_IDX, i, 2, Qt::MatchRecursive); + QItemSelectionModel::SelectionFlags flags; + if (match.isEmpty()) + return; + QModelIndex idx = match.first(); + flags = toggle ? QItemSelectionModel::Toggle : QItemSelectionModel::Select; + flags |= QItemSelectionModel::Rows; + selectionModel()->setCurrentIndex(idx, flags); + if (idx.parent().isValid()) { + setAnimated(false); + expand(idx.parent()); + if (scrollto) + scrollTo(idx.parent()); + setAnimated(true); + } + if (scrollto) + scrollTo(idx, PositionAtCenter); +} + +void DiveListView::selectDives(const QList &newDiveSelection) +{ + int firstInList, newSelection; + struct dive *d; + + if (!newDiveSelection.count()) + return; + + dontEmitDiveChangedSignal = true; + // select the dives, highest index first - this way the oldest of the dives + // becomes the selected_dive that we scroll to + QList sortedSelection = newDiveSelection; + qSort(sortedSelection.begin(), sortedSelection.end()); + newSelection = firstInList = sortedSelection.first(); + + while (!sortedSelection.isEmpty()) + selectDive(sortedSelection.takeLast()); + + while (selected_dive == -1) { + // that can happen if we restored a selection after edit + // and the only selected dive is no longer visible because of a filter + newSelection--; + if (newSelection < 0) + newSelection = dive_table.nr - 1; + if (newSelection == firstInList) + break; + if ((d = get_dive(newSelection)) != NULL && !d->hidden_by_filter) + selectDive(newSelection); + } + QSortFilterProxyModel *m = qobject_cast(model()); + QModelIndexList idxList = m->match(m->index(0, 0), DiveTripModel::DIVE_IDX, selected_dive, 2, Qt::MatchRecursive); + if (!idxList.isEmpty()) { + QModelIndex idx = idxList.first(); + if (idx.parent().isValid()) + scrollTo(idx.parent()); + scrollTo(idx); + } + // now that everything is up to date, update the widgets + Q_EMIT currentDiveChanged(selected_dive); + dontEmitDiveChangedSignal = false; + return; +} + +bool DiveListView::eventFilter(QObject *, QEvent *event) +{ + if (event->type() != QEvent::KeyPress) + return false; + QKeyEvent *keyEv = static_cast(event); + if (keyEv->key() == Qt::Key_Delete) { + contextMenuIndex = currentIndex(); + deleteDive(); + } + if (keyEv->key() != Qt::Key_Escape) + return false; + return true; +} + +// NOTE! This loses trip selection, because while we remember the +// dives, we don't remember the trips (see the "currentSelectedDives" +// list). I haven't figured out how to look up the trip from the +// index. TRIP_ROLE vs DIVE_ROLE? +void DiveListView::headerClicked(int i) +{ + DiveTripModel::Layout newLayout = i == (int)DiveTripModel::NR ? DiveTripModel::TREE : DiveTripModel::LIST; + rememberSelection(); + unselectDives(); + /* No layout change? Just re-sort, and scroll to first selection, making sure all selections are expanded */ + if (currentLayout == newLayout) { + currentOrder = (currentOrder == Qt::DescendingOrder) ? Qt::AscendingOrder : Qt::DescendingOrder; + sortByColumn(i, currentOrder); + } else { + // clear the model, repopulate with new indexes. + if (currentLayout == DiveTripModel::TREE) { + backupExpandedRows(); + } + reload(newLayout, false); + currentOrder = Qt::DescendingOrder; + sortByColumn(i, currentOrder); + if (newLayout == DiveTripModel::TREE) { + restoreExpandedRows(); + } + } + restoreSelection(); + // remember the new sort column + sortColumn = i; +} + +void DiveListView::reload(DiveTripModel::Layout layout, bool forceSort) +{ + // we want to run setupUi() once we actually are displaying something + // in the widget + static bool first = true; + if (first && dive_table.nr > 0) { + setupUi(); + first = false; + } + if (layout == DiveTripModel::CURRENT) + layout = currentLayout; + else + currentLayout = layout; + + header()->setSectionsClickable(true); + connect(header(), SIGNAL(sectionPressed(int)), this, SLOT(headerClicked(int)), Qt::UniqueConnection); + + QSortFilterProxyModel *m = qobject_cast(model()); + QAbstractItemModel *oldModel = m->sourceModel(); + if (oldModel) { + oldModel->deleteLater(); + } + DiveTripModel *tripModel = new DiveTripModel(this); + tripModel->setLayout(layout); + + m->setSourceModel(tripModel); + + if (!forceSort) + return; + + sortByColumn(sortColumn, currentOrder); + if (amount_selected && current_dive != NULL) { + selectDive(selected_dive, true); + } else { + QModelIndex firstDiveOrTrip = m->index(0, 0); + if (firstDiveOrTrip.isValid()) { + if (m->index(0, 0, firstDiveOrTrip).isValid()) + setCurrentIndex(m->index(0, 0, firstDiveOrTrip)); + else + setCurrentIndex(firstDiveOrTrip); + } + } + if (selectedIndexes().count()) { + QModelIndex curr = selectedIndexes().first(); + curr = curr.parent().isValid() ? curr.parent() : curr; + if (!isExpanded(curr)) { + setAnimated(false); + expand(curr); + scrollTo(curr); + setAnimated(true); + } + } + if (currentLayout == DiveTripModel::TREE) { + fixMessyQtModelBehaviour(); + } +} + +void DiveListView::reloadHeaderActions() +{ + // Populate the context menu of the headers that will show + // the menu to show / hide columns. + if (!header()->actions().size()) { + QSettings s; + s.beginGroup("DiveListColumnState"); + for (int i = 0; i < model()->columnCount(); i++) { + QString title = QString("%1").arg(model()->headerData(i, Qt::Horizontal).toString()); + QString settingName = QString("showColumn%1").arg(i); + QAction *a = new QAction(title, header()); + bool showHeaderFirstRun = !(i == DiveTripModel::MAXCNS || + i == DiveTripModel::GAS || + i == DiveTripModel::OTU || + i == DiveTripModel::TEMPERATURE || + i == DiveTripModel::TOTALWEIGHT || + i == DiveTripModel::SUIT || + i == DiveTripModel::CYLINDER || + i == DiveTripModel::SAC); + bool shown = s.value(settingName, showHeaderFirstRun).toBool(); + a->setCheckable(true); + a->setChecked(shown); + a->setProperty("index", i); + a->setProperty("settingName", settingName); + connect(a, SIGNAL(triggered(bool)), this, SLOT(toggleColumnVisibilityByIndex())); + header()->addAction(a); + setColumnHidden(i, !shown); + } + s.endGroup(); + } else { + for (int i = 0; i < model()->columnCount(); i++) { + QString title = QString("%1").arg(model()->headerData(i, Qt::Horizontal).toString()); + header()->actions()[i]->setText(title); + } + } +} + +void DiveListView::toggleColumnVisibilityByIndex() +{ + QAction *action = qobject_cast(sender()); + if (!action) + return; + + QSettings s; + s.beginGroup("DiveListColumnState"); + s.setValue(action->property("settingName").toString(), action->isChecked()); + s.endGroup(); + s.sync(); + setColumnHidden(action->property("index").toInt(), !action->isChecked()); + setColumnWidth(lastVisibleColumn(), 10); +} + +void DiveListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!isVisible()) + return; + if (!current.isValid()) + return; + scrollTo(current); +} + +void DiveListView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + QItemSelection newSelected = selected.size() ? selected : selectionModel()->selection(); + QItemSelection newDeselected = deselected; + + disconnect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); + + Q_FOREACH (const QModelIndex &index, newDeselected.indexes()) { + if (index.column() != 0) + continue; + const QAbstractItemModel *model = index.model(); + struct dive *dive = (struct dive *)model->data(index, DiveTripModel::DIVE_ROLE).value(); + if (!dive) // it's a trip! + deselect_dives_in_trip((dive_trip_t *)model->data(index, DiveTripModel::TRIP_ROLE).value()); + else + deselect_dive(get_divenr(dive)); + } + Q_FOREACH (const QModelIndex &index, newSelected.indexes()) { + if (index.column() != 0) + continue; + + const QAbstractItemModel *model = index.model(); + struct dive *dive = (struct dive *)model->data(index, DiveTripModel::DIVE_ROLE).value(); + if (!dive) { // it's a trip! + if (model->rowCount(index)) { + QItemSelection selection; + select_dives_in_trip((dive_trip_t *)model->data(index, DiveTripModel::TRIP_ROLE).value()); + selection.select(index.child(0, 0), index.child(model->rowCount(index) - 1, 0)); + selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); + selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::NoUpdate); + if (!isExpanded(index)) + expand(index); + } + } else { + select_dive(get_divenr(dive)); + } + } + QTreeView::selectionChanged(selectionModel()->selection(), newDeselected); + connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); + if (!dontEmitDiveChangedSignal) + Q_EMIT currentDiveChanged(selected_dive); +} + +enum asked_user {NOTYET, MERGE, DONTMERGE}; + +static bool can_merge(const struct dive *a, const struct dive *b, enum asked_user *have_asked) +{ + if (!a || !b) + return false; + if (a->when > b->when) + return false; + /* Don't merge dives if there's more than half an hour between them */ + if (dive_endtime(a) + 30 * 60 < b->when) { + if (*have_asked == NOTYET) { + if (QMessageBox::warning(MainWindow::instance(), + MainWindow::instance()->tr("Warning"), + MainWindow::instance()->tr("Trying to merge dives with %1min interval in between").arg( + (b->when - dive_endtime(a)) / 60), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { + *have_asked = DONTMERGE; + return false; + } else { + *have_asked = MERGE; + return true; + } + } else { + return *have_asked == MERGE ? true : false; + } + } + return true; +} + +void DiveListView::mergeDives() +{ + int i; + struct dive *dive, *maindive = NULL; + enum asked_user have_asked = NOTYET; + + for_each_dive (i, dive) { + if (dive->selected) { + if (!can_merge(maindive, dive, &have_asked)) { + maindive = dive; + } else { + maindive = merge_two_dives(maindive, dive); + i--; // otherwise we skip a dive in the freshly changed list + } + } + } + MainWindow::instance()->refreshProfile(); + MainWindow::instance()->refreshDisplay(); +} + +void DiveListView::splitDives() +{ + int i; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive->selected) + split_dive(dive); + } + MainWindow::instance()->refreshProfile(); + MainWindow::instance()->refreshDisplay(); +} + +void DiveListView::renumberDives() +{ + RenumberDialog::instance()->renumberOnlySelected(); + RenumberDialog::instance()->show(); +} + +void DiveListView::merge_trip(const QModelIndex &a, int offset) +{ + int i = a.row() + offset; + QModelIndex b = a.sibling(i, 0); + + dive_trip_t *trip_a = (dive_trip_t *)a.data(DiveTripModel::TRIP_ROLE).value(); + dive_trip_t *trip_b = (dive_trip_t *)b.data(DiveTripModel::TRIP_ROLE).value(); + if (trip_a == trip_b || !trip_a || !trip_b) + return; + combine_trips(trip_a, trip_b); + rememberSelection(); + reload(currentLayout, false); + fixMessyQtModelBehaviour(); + restoreSelection(); + mark_divelist_changed(true); + //TODO: emit a signal to signalize that the divelist changed? +} + +void DiveListView::mergeTripAbove() +{ + merge_trip(contextMenuIndex, -1); +} + +void DiveListView::mergeTripBelow() +{ + merge_trip(contextMenuIndex, +1); +} + +void DiveListView::removeFromTrip() +{ + //TODO: move this to C-code. + int i; + struct dive *d; + QMap divesToRemove; + for_each_dive (i, d) { + if (d->selected) + divesToRemove.insert(d, d->divetrip); + } + UndoRemoveDivesFromTrip *undoCommand = new UndoRemoveDivesFromTrip(divesToRemove); + MainWindow::instance()->undoStack->push(undoCommand); + + rememberSelection(); + reload(currentLayout, false); + fixMessyQtModelBehaviour(); + restoreSelection(); + mark_divelist_changed(true); +} + +void DiveListView::newTripAbove() +{ + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); + if (!d) // shouldn't happen as we only are setting up this action if this is a dive + return; + //TODO: port to c-code. + dive_trip_t *trip; + int idx; + rememberSelection(); + trip = create_and_hookup_trip_from_dive(d); + for_each_dive (idx, d) { + if (d->selected) + add_dive_to_trip(d, trip); + } + trip->expanded = 1; + reload(currentLayout, false); + fixMessyQtModelBehaviour(); + mark_divelist_changed(true); + restoreSelection(); +} + +void DiveListView::addToTripBelow() +{ + addToTrip(1); +} + +void DiveListView::addToTripAbove() +{ + addToTrip(-1); +} + +void DiveListView::addToTrip(int delta) +{ + // if there is a trip above / below, then it's a sibling at the same + // level as this dive. So let's take a look + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); + QModelIndex t = contextMenuIndex.sibling(contextMenuIndex.row() + delta, 0); + dive_trip_t *trip = (dive_trip_t *)t.data(DiveTripModel::TRIP_ROLE).value(); + + if (!trip || !d) + // no dive, no trip? get me out of here + return; + + rememberSelection(); + + add_dive_to_trip(d, trip); + if (d->selected) { // there are possibly other selected dives that we should add + int idx; + for_each_dive (idx, d) { + if (d->selected) + add_dive_to_trip(d, trip); + } + } + trip->expanded = 1; + mark_divelist_changed(true); + + reload(currentLayout, false); + restoreSelection(); + fixMessyQtModelBehaviour(); +} + +void DiveListView::markDiveInvalid() +{ + int i; + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); + if (!d) + return; + for_each_dive (i, d) { + if (!d->selected) + continue; + //TODO: this should be done in the future + // now mark the dive invalid... how do we do THAT? + // d->invalid = true; + } + if (amount_selected == 0) { + MainWindow::instance()->cleanUpEmpty(); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); + if (prefs.display_invalid_dives == false) { + clearSelection(); + // select top dive that isn't marked invalid + rememberSelection(); + } + fixMessyQtModelBehaviour(); +} + +void DiveListView::deleteDive() +{ + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); + if (!d) + return; + + int i; + int lastDiveNr = -1; + QList deletedDives; //a list of all deleted dives to be stored in the undo command + for_each_dive (i, d) { + if (!d->selected) + continue; + deletedDives.append(d); + lastDiveNr = i; + } + // the actual dive deletion is happening in the redo command that is implicitly triggered + UndoDeleteDive *undoEntry = new UndoDeleteDive(deletedDives); + MainWindow::instance()->undoStack->push(undoEntry); + if (amount_selected == 0) { + MainWindow::instance()->cleanUpEmpty(); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); + if (lastDiveNr != -1) { + clearSelection(); + selectDive(lastDiveNr); + rememberSelection(); + } + fixMessyQtModelBehaviour(); +} + +void DiveListView::testSlot() +{ + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); + if (d) { + qDebug("testSlot called on dive #%d", d->number); + } else { + QModelIndex child = contextMenuIndex.child(0, 0); + d = (struct dive *)child.data(DiveTripModel::DIVE_ROLE).value(); + if (d) + qDebug("testSlot called on trip including dive #%d", d->number); + else + qDebug("testSlot called on trip with no dive"); + } +} + +void DiveListView::contextMenuEvent(QContextMenuEvent *event) +{ + QAction *collapseAction = NULL; + // let's remember where we are + contextMenuIndex = indexAt(event->pos()); + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); + dive_trip_t *trip = (dive_trip_t *)contextMenuIndex.data(DiveTripModel::TRIP_ROLE).value(); + QMenu popup(this); + if (currentLayout == DiveTripModel::TREE) { + // verify if there is a node that`s not expanded. + bool needs_expand = false; + bool needs_collapse = false; + uint expanded_nodes = 0; + for(int i = 0, end = model()->rowCount(); i < end; i++) { + QModelIndex idx = model()->index(i, 0); + if (idx.data(DiveTripModel::DIVE_ROLE).value()) + continue; + + if (!isExpanded(idx)) { + needs_expand = true; + } else { + needs_collapse = true; + expanded_nodes ++; + } + } + if (needs_expand) + popup.addAction(tr("Expand all"), this, SLOT(expandAll())); + if (needs_collapse) + popup.addAction(tr("Collapse all"), this, SLOT(collapseAll())); + + // verify if there`s a need for collapse others + if (expanded_nodes > 1) + collapseAction = popup.addAction(tr("Collapse others"), this, SLOT(collapseAll())); + + + if (d) { + popup.addAction(tr("Remove dive(s) from trip"), this, SLOT(removeFromTrip())); + popup.addAction(tr("Create new trip above"), this, SLOT(newTripAbove())); + if (!d->divetrip) { + struct dive *top = d; + struct dive *bottom = d; + if (d->selected) { + if (currentOrder == Qt::AscendingOrder) { + top = first_selected_dive(); + bottom = last_selected_dive(); + } else { + top = last_selected_dive(); + bottom = first_selected_dive(); + } + } + if (is_trip_before_after(top, (currentOrder == Qt::AscendingOrder))) + popup.addAction(tr("Add dive(s) to trip immediately above"), this, SLOT(addToTripAbove())); + if (is_trip_before_after(bottom, (currentOrder == Qt::DescendingOrder))) + popup.addAction(tr("Add dive(s) to trip immediately below"), this, SLOT(addToTripBelow())); + } + } + if (trip) { + popup.addAction(tr("Merge trip with trip above"), this, SLOT(mergeTripAbove())); + popup.addAction(tr("Merge trip with trip below"), this, SLOT(mergeTripBelow())); + } + } + if (d) { + popup.addAction(tr("Delete dive(s)"), this, SLOT(deleteDive())); +#if 0 + popup.addAction(tr("Mark dive(s) invalid", this, SLOT(markDiveInvalid()))); +#endif + } + if (amount_selected > 1 && consecutive_selected()) + popup.addAction(tr("Merge selected dives"), this, SLOT(mergeDives())); + if (amount_selected >= 1) { + popup.addAction(tr("Renumber dive(s)"), this, SLOT(renumberDives())); + popup.addAction(tr("Shift dive times"), this, SLOT(shiftTimes())); + popup.addAction(tr("Split selected dives"), this, SLOT(splitDives())); + popup.addAction(tr("Load image(s) from file(s)"), this, SLOT(loadImages())); + popup.addAction(tr("Load image(s) from web"), this, SLOT(loadWebImages())); + } + + // "collapse all" really closes all trips, + // "collapse" keeps the trip with the selected dive open + QAction *actionTaken = popup.exec(event->globalPos()); + if (actionTaken == collapseAction && collapseAction) { + this->setAnimated(false); + selectDive(selected_dive, true); + scrollTo(selectedIndexes().first()); + this->setAnimated(true); + } + event->accept(); +} + + +void DiveListView::shiftTimes() +{ + ShiftTimesDialog::instance()->show(); +} + +void DiveListView::loadImages() +{ + QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open image files"), lastUsedImageDir(), tr("Image files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); + if (fileNames.isEmpty()) + return; + updateLastUsedImageDir(QFileInfo(fileNames[0]).dir().path()); + matchImagesToDives(fileNames); +} + +void DiveListView::matchImagesToDives(QStringList fileNames) +{ + ShiftImageTimesDialog shiftDialog(this, fileNames); + shiftDialog.setOffset(lastImageTimeOffset()); + if (!shiftDialog.exec()) + return; + updateLastImageTimeOffset(shiftDialog.amount()); + + Q_FOREACH (const QString &fileName, fileNames) { + int j = 0; + struct dive *dive; + for_each_dive (j, dive) { + if (!dive->selected) + continue; + dive_create_picture(dive, copy_string(fileName.toUtf8().data()), shiftDialog.amount(), shiftDialog.matchAll()); + } + } + + mark_divelist_changed(true); + copy_dive(current_dive, &displayed_dive); + DivePictureModel::instance()->updateDivePictures(); +} + +void DiveListView::loadWebImages() +{ + URLDialog urlDialog(this); + if (!urlDialog.exec()) + return; + loadImageFromURL(QUrl::fromUserInput(urlDialog.url())); + +} + +void DiveListView::loadImageFromURL(QUrl url) +{ + if (url.isValid()) { + QEventLoop loop; + QNetworkRequest request(url); + QNetworkReply *reply = manager.get(request); + while (reply->isRunning()) { + loop.processEvents(); + sleep(1); + } + QByteArray imageData = reply->readAll(); + + QImage image = QImage(); + image.loadFromData(imageData); + if (image.isNull()) + // If this is not an image, maybe it's an html file and Miika can provide some xslr magic to extract images. + // In this case we would call the function recursively on the list of image source urls; + return; + + // Since we already downloaded the image we can cache it as well. + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(imageData); + QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); + QDir dir(path); + if (!dir.exists()) + dir.mkpath(path); + QFile imageFile(path.append("/").append(hash.result().toHex())); + if (imageFile.open(QIODevice::WriteOnly)) { + QDataStream stream(&imageFile); + stream.writeRawData(imageData.data(), imageData.length()); + imageFile.waitForBytesWritten(-1); + imageFile.close(); + add_hash(imageFile.fileName(), hash.result()); + struct picture picture; + picture.hash = NULL; + picture.filename = strdup(url.toString().toUtf8().data()); + learnHash(&picture, hash.result()); + matchImagesToDives(QStringList(url.toString())); + } + } + + +} + + +QString DiveListView::lastUsedImageDir() +{ + QSettings settings; + QString lastImageDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastImageDir")) + if (QDir::setCurrent(settings.value("LastImageDir").toString())) + lastImageDir = settings.value("LastIamgeDir").toString(); + return lastImageDir; +} + +void DiveListView::updateLastUsedImageDir(const QString &dir) +{ + QSettings s; + s.beginGroup("FileDialog"); + s.setValue("LastImageDir", dir); +} + +int DiveListView::lastImageTimeOffset() +{ + QSettings settings; + int offset = 0; + + settings.beginGroup("MainWindow"); + if (settings.contains("LastImageTimeOffset")) + offset = settings.value("LastImageTimeOffset").toInt(); + return offset; +} + +void DiveListView::updateLastImageTimeOffset(const int offset) +{ + QSettings s; + s.beginGroup("MainWindow"); + s.setValue("LastImageTimeOffset", offset); +} diff --git a/desktop-widgets/divelistview.h b/desktop-widgets/divelistview.h new file mode 100644 index 000000000..aaec37af5 --- /dev/null +++ b/desktop-widgets/divelistview.h @@ -0,0 +1,89 @@ +/* + * divelistview.h + * + * header file for the dive list of Subsurface + * + */ +#ifndef DIVELISTVIEW_H +#define DIVELISTVIEW_H + +/*! A view subclass for use with dives + Note: calling this a list view might be misleading? +*/ + +#include +#include +#include +#include "divetripmodel.h" + +class DiveListView : public QTreeView { + Q_OBJECT +public: + DiveListView(QWidget *parent = 0); + ~DiveListView(); + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void reload(DiveTripModel::Layout layout, bool forceSort = true); + bool eventFilter(QObject *, QEvent *); + void unselectDives(); + void clearTripSelection(); + void selectDive(int dive_table_idx, bool scrollto = false, bool toggle = false); + void selectDives(const QList &newDiveSelection); + void rememberSelection(); + void restoreSelection(); + void contextMenuEvent(QContextMenuEvent *event); + QList selectedTrips(); +public +slots: + void toggleColumnVisibilityByIndex(); + void reloadHeaderActions(); + void headerClicked(int); + void removeFromTrip(); + void deleteDive(); + void markDiveInvalid(); + void testSlot(); + void fixMessyQtModelBehaviour(); + void mergeTripAbove(); + void mergeTripBelow(); + void newTripAbove(); + void addToTripAbove(); + void addToTripBelow(); + void mergeDives(); + void splitDives(); + void renumberDives(); + void shiftTimes(); + void loadImages(); + void loadWebImages(); + static QString lastUsedImageDir(); + +signals: + void currentDiveChanged(int divenr); + +private: + bool mouseClickSelection; + QList expandedRows; + int sortColumn; + Qt::SortOrder currentOrder; + DiveTripModel::Layout currentLayout; + QModelIndex contextMenuIndex; + bool dontEmitDiveChangedSignal; + bool selectionSaved; + + /* if dive_trip_t is null, there's no problem. */ + QMultiHash selectedDives; + void merge_trip(const QModelIndex &a, const int offset); + void setupUi(); + void backupExpandedRows(); + void restoreExpandedRows(); + int lastVisibleColumn(); + void selectTrip(dive_trip_t *trip); + void updateLastUsedImageDir(const QString &s); + void updateLastImageTimeOffset(int offset); + int lastImageTimeOffset(); + void addToTrip(int delta); + void matchImagesToDives(QStringList fileNames); + void loadImageFromURL(QUrl url); + QNetworkAccessManager manager; +}; + +#endif // DIVELISTVIEW_H diff --git a/desktop-widgets/divelogexportdialog.cpp b/desktop-widgets/divelogexportdialog.cpp new file mode 100644 index 000000000..7a406b982 --- /dev/null +++ b/desktop-widgets/divelogexportdialog.cpp @@ -0,0 +1,240 @@ +#include +#include +#include +#include + +#include "divelogexportdialog.h" +#include "divelogexportlogic.h" +#include "diveshareexportdialog.h" +#include "ui_divelogexportdialog.h" +#include "subsurfacewebservices.h" +#include "worldmap-save.h" +#include "save-html.h" +#include "mainwindow.h" + +#define GET_UNIT(name, field, f, t) \ + v = settings.value(QString(name)); \ + if (v.isValid()) \ + field = (v.toInt() == 0) ? (t) : (f); \ + else \ + field = default_prefs.units.field + +DiveLogExportDialog::DiveLogExportDialog(QWidget *parent) : QDialog(parent), + ui(new Ui::DiveLogExportDialog) +{ + ui->setupUi(this); + showExplanation(); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), MainWindow::instance(), SLOT(close())); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + + /* the names are not the actual values exported to the json files,The font-family property should hold several + font names as a "fallback" system, to ensure maximum compatibility between browsers/operating systems */ + ui->fontSelection->addItem("Arial", "Arial, Helvetica, sans-serif"); + ui->fontSelection->addItem("Impact", "Impact, Charcoal, sans-serif"); + ui->fontSelection->addItem("Georgia", "Georgia, serif"); + ui->fontSelection->addItem("Courier", "Courier, monospace"); + ui->fontSelection->addItem("Verdana", "Verdana, Geneva, sans-serif"); + + QSettings settings; + settings.beginGroup("HTML"); + if (settings.contains("fontSelection")) { + ui->fontSelection->setCurrentIndex(settings.value("fontSelection").toInt()); + } + if (settings.contains("fontSizeSelection")) { + ui->fontSizeSelection->setCurrentIndex(settings.value("fontSizeSelection").toInt()); + } + if (settings.contains("themeSelection")) { + ui->themeSelection->setCurrentIndex(settings.value("themeSelection").toInt()); + } + if (settings.contains("subsurfaceNumbers")) { + ui->exportSubsurfaceNumber->setChecked(settings.value("subsurfaceNumbers").toBool()); + } + if (settings.contains("yearlyStatistics")) { + ui->exportStatistics->setChecked(settings.value("yearlyStatistics").toBool()); + } + if (settings.contains("listOnly")) { + ui->exportListOnly->setChecked(settings.value("listOnly").toBool()); + } + if (settings.contains("exportPhotos")) { + ui->exportPhotos->setChecked(settings.value("exportPhotos").toBool()); + } + settings.endGroup(); +} + +DiveLogExportDialog::~DiveLogExportDialog() +{ + delete ui; +} + +void DiveLogExportDialog::showExplanation() +{ + if (ui->exportUDDF->isChecked()) { + ui->description->setText(tr("Generic format that is used for data exchange between a variety of diving related programs.")); + } else if (ui->exportCSV->isChecked()) { + ui->description->setText(tr("Comma separated values describing the dive profile.")); + } else if (ui->exportCSVDetails->isChecked()) { + ui->description->setText(tr("Comma separated values of the dive information. This includes most of the dive details but no profile information.")); + } else if (ui->exportDivelogs->isChecked()) { + ui->description->setText(tr("Send the dive data to divelogs.de website.")); + } else if (ui->exportDiveshare->isChecked()) { + ui->description->setText(tr("Send the dive data to dive-share.appspot.com website")); + } else if (ui->exportWorldMap->isChecked()) { + ui->description->setText(tr("HTML export of the dive locations, visualized on a world map.")); + } else if (ui->exportSubsurfaceXML->isChecked()) { + ui->description->setText(tr("Subsurface native XML format.")); + } else if (ui->exportImageDepths->isChecked()) { + ui->description->setText(tr("Write depths of images to file.")); + } +} + +void DiveLogExportDialog::exportHtmlInit(const QString &filename) +{ + struct htmlExportSetting hes; + hes.themeFile = (ui->themeSelection->currentText() == tr("Light")) ? "light.css" : "sand.css"; + hes.exportPhotos = ui->exportPhotos->isChecked(); + hes.selectedOnly = ui->exportSelectedDives->isChecked(); + hes.listOnly = ui->exportListOnly->isChecked(); + hes.fontFamily = ui->fontSelection->itemData(ui->fontSelection->currentIndex()).toString(); + hes.fontSize = ui->fontSizeSelection->currentText(); + hes.themeSelection = ui->themeSelection->currentIndex(); + hes.subsurfaceNumbers = ui->exportSubsurfaceNumber->isChecked(); + hes.yearlyStatistics = ui->exportStatistics->isChecked(); + + exportHtmlInitLogic(filename, hes); +} + +void DiveLogExportDialog::exportHTMLsettings(const QString &filename) +{ + QSettings settings; + settings.beginGroup("HTML"); + settings.setValue("fontSelection", ui->fontSelection->currentIndex()); + settings.setValue("fontSizeSelection", ui->fontSizeSelection->currentIndex()); + settings.setValue("themeSelection", ui->themeSelection->currentIndex()); + settings.setValue("subsurfaceNumbers", ui->exportSubsurfaceNumber->isChecked()); + settings.setValue("yearlyStatistics", ui->exportStatistics->isChecked()); + settings.setValue("listOnly", ui->exportListOnly->isChecked()); + settings.setValue("exportPhotos", ui->exportPhotos->isChecked()); + settings.endGroup(); + +} + + +void DiveLogExportDialog::on_exportGroup_buttonClicked(QAbstractButton *button) +{ + showExplanation(); +} + +void DiveLogExportDialog::on_buttonBox_accepted() +{ + QString filename; + QString stylesheet; + QSettings settings; + QString lastDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastDir")) { + if (QDir::setCurrent(settings.value("LastDir").toString())) { + lastDir = settings.value("LastDir").toString(); + } + } + settings.endGroup(); + + switch (ui->tabWidget->currentIndex()) { + case 0: + if (ui->exportUDDF->isChecked()) { + stylesheet = "uddf-export.xslt"; + filename = QFileDialog::getSaveFileName(this, tr("Export UDDF file as"), lastDir, + tr("UDDF files (*.uddf *.UDDF)")); + } else if (ui->exportCSV->isChecked()) { + stylesheet = "xml2csv.xslt"; + filename = QFileDialog::getSaveFileName(this, tr("Export CSV file as"), lastDir, + tr("CSV files (*.csv *.CSV)")); + } else if (ui->exportCSVDetails->isChecked()) { + stylesheet = "xml2manualcsv.xslt"; + filename = QFileDialog::getSaveFileName(this, tr("Export CSV file as"), lastDir, + tr("CSV files (*.csv *.CSV)")); + } else if (ui->exportDivelogs->isChecked()) { + DivelogsDeWebServices::instance()->prepareDivesForUpload(ui->exportSelected->isChecked()); + } else if (ui->exportDiveshare->isChecked()) { + DiveShareExportDialog::instance()->prepareDivesForUpload(ui->exportSelected->isChecked()); + } else if (ui->exportWorldMap->isChecked()) { + filename = QFileDialog::getSaveFileName(this, tr("Export world map"), lastDir, + tr("HTML files (*.html)")); + if (!filename.isNull() && !filename.isEmpty()) + export_worldmap_HTML(filename.toUtf8().data(), ui->exportSelected->isChecked()); + } else if (ui->exportSubsurfaceXML->isChecked()) { + filename = QFileDialog::getSaveFileName(this, tr("Export Subsurface XML"), lastDir, + tr("XML files (*.xml *.ssrf)")); + if (!filename.isNull() && !filename.isEmpty()) { + if (!filename.contains('.')) + filename.append(".ssrf"); + QByteArray bt = QFile::encodeName(filename); + save_dives_logic(bt.data(), ui->exportSelected->isChecked()); + } + } else if (ui->exportImageDepths->isChecked()) { + filename = QFileDialog::getSaveFileName(this, tr("Save image depths"), lastDir); + if (!filename.isNull() && !filename.isEmpty()) + export_depths(filename.toUtf8().data(), ui->exportSelected->isChecked()); + } + break; + case 1: + filename = QFileDialog::getSaveFileName(this, tr("Export HTML files as"), lastDir, + tr("HTML files (*.html)")); + if (!filename.isNull() && !filename.isEmpty()) + exportHtmlInit(filename); + break; + } + + if (!filename.isNull() && !filename.isEmpty()) { + // remember the last export path + QFileInfo fileInfo(filename); + settings.beginGroup("FileDialog"); + settings.setValue("LastDir", fileInfo.dir().path()); + settings.endGroup(); + // the non XSLT exports are called directly above, the XSLT based ons are called here + if (!stylesheet.isEmpty()) { + future = QtConcurrent::run(export_dives_xslt, filename.toUtf8(), ui->exportSelected->isChecked(), ui->CSVUnits_2->currentIndex(), stylesheet.toUtf8()); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Please wait, exporting..."), KMessageWidget::Information); + MainWindow::instance()->getNotificationWidget()->setFuture(future); + } + } +} + +void DiveLogExportDialog::export_depths(const char *filename, const bool selected_only) +{ + FILE *f; + struct dive *dive; + depth_t depth; + int i; + const char *unit = NULL; + + struct membuffer buf = { 0 }; + + for_each_dive (i, dive) { + if (selected_only && !dive->selected) + continue; + + FOR_EACH_PICTURE (dive) { + int n = dive->dc.samples; + struct sample *s = dive->dc.sample; + depth.mm = 0; + while (--n >= 0 && (int32_t)s->time.seconds <= picture->offset.seconds) { + depth.mm = s->depth.mm; + s++; + } + put_format(&buf, "%s\t%.1f", picture->filename, get_depth_units(depth.mm, NULL, &unit)); + put_format(&buf, "%s\n", unit); + } + } + + f = subsurface_fopen(filename, "w+"); + if (!f) { + report_error(tr("Can't open file %s").toUtf8().data(), filename); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} diff --git a/desktop-widgets/divelogexportdialog.h b/desktop-widgets/divelogexportdialog.h new file mode 100644 index 000000000..a5b5cc770 --- /dev/null +++ b/desktop-widgets/divelogexportdialog.h @@ -0,0 +1,39 @@ +#ifndef DIVELOGEXPORTDIALOG_H +#define DIVELOGEXPORTDIALOG_H + +#include +#include +#include +#include "helpers.h" +#include "statistics.h" + +class QAbstractButton; + +namespace Ui { + class DiveLogExportDialog; +} + +void exportHTMLstatisticsTotal(QTextStream &out, stats_t *total_stats); + +class DiveLogExportDialog : public QDialog { + Q_OBJECT + +public: + explicit DiveLogExportDialog(QWidget *parent = 0); + ~DiveLogExportDialog(); + +private +slots: + void on_buttonBox_accepted(); + void on_exportGroup_buttonClicked(QAbstractButton *); + +private: + QFuture future; + Ui::DiveLogExportDialog *ui; + void showExplanation(); + void exportHtmlInit(const QString &filename); + void exportHTMLsettings(const QString &filename); + void export_depths(const char *filename, const bool selected_only); +}; + +#endif // DIVELOGEXPORTDIALOG_H diff --git a/desktop-widgets/divelogexportdialog.ui b/desktop-widgets/divelogexportdialog.ui new file mode 100644 index 000000000..02c8cf38b --- /dev/null +++ b/desktop-widgets/divelogexportdialog.ui @@ -0,0 +1,606 @@ + + + DiveLogExportDialog + + + + 0 + 0 + 507 + 548 + + + + Export dive log files + + + + :/subsurface-icon + + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + 0 + + + true + + + + General export + + + + 5 + + + 5 + + + 5 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Export format + + + + + + + 171 + 16777215 + + + + Subsurface &XML + + + true + + + exportGroup + + + + + + + + 110 + 16777215 + + + + UDDF + + + false + + + exportGroup + + + + + + + di&velogs.de + + + exportGroup + + + + + + + DiveShare + + + exportGroup + + + + + + + CSV dive profile + + + exportGroup + + + + + + + CSV dive details + + + exportGroup + + + + + + + Worldmap + + + exportGroup + + + + + + + I&mage depths + + + exportGroup + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 16777215 + + + + Selection + + + + + + true + + + Selected dives + + + true + + + + + + + All dives + + + + + + + + + + false + + + CSV units + + + + + 30 + 30 + 102 + 27 + + + + + Metric + + + + + Imperial + + + + + + + + + + + + + HTML + + + + 0 + + + 5 + + + 0 + + + 0 + + + + + General settings + + + + + + Subsurface numbers + + + true + + + + + + + + 117 + 0 + + + + Selected dives + + + true + + + buttonGroup + + + + + + + Export yearly statistics + + + true + + + + + + + + 117 + 0 + + + + All di&ves + + + buttonGroup + + + + + + + Export list only + + + + + + + Export photos + + + true + + + + + + + + + + true + + + Style options + + + true + + + false + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Font + + + + + + + + + + Font size + + + + + + + 3 + + + + 8 + + + + + 10 + + + + + 12 + + + + + 14 + + + + + 16 + + + + + 18 + + + + + 20 + + + + + + + + Theme + + + + + + + 0 + + + + Light + + + + + Sand + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + DiveLogExportDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DiveLogExportDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + exportCSV + toggled(bool) + groupBox + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + exportCSVDetails + toggled(bool) + groupBox + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + + + + + diff --git a/desktop-widgets/divelogimportdialog.cpp b/desktop-widgets/divelogimportdialog.cpp new file mode 100644 index 000000000..025d181d1 --- /dev/null +++ b/desktop-widgets/divelogimportdialog.cpp @@ -0,0 +1,861 @@ +#include "divelogimportdialog.h" +#include "mainwindow.h" +#include "color.h" +#include "ui_divelogimportdialog.h" +#include +#include +#include + +static QString subsurface_mimedata = "subsurface/csvcolumns"; +static QString subsurface_index = "subsurface/csvindex"; + +const DiveLogImportDialog::CSVAppConfig DiveLogImportDialog::CSVApps[CSVAPPS] = { + // time, depth, temperature, po2, sensor1, sensor2, sensor3, cns, ndl, tts, stopdepth, pressure, setpoint + // indices are 0 based, -1 means the column doesn't exist + { "Manual import", }, + { "APD Log Viewer - DC1", 0, 1, 15, 6, 3, 4, 5, 17, -1, -1, 18, -1, 2, "Tab" }, + { "APD Log Viewer - DC2", 0, 1, 15, 6, 7, 8, 9, 17, -1, -1, 18, -1, 2, "Tab" }, + { "XP5", 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "Tab" }, + { "SensusCSV", 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "," }, + { "Seabear CSV", 0, 1, 5, -1, -1, -1, -1, -1, 2, 3, 4, 6, -1, ";" }, + { "SubsurfaceCSV", -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "Tab" }, + { NULL, } +}; + +static enum { + MANUAL, + APD, + APD2, + XP5, + SENSUS, + SEABEAR, + SUBSURFACE +} known; + +ColumnNameProvider::ColumnNameProvider(QObject *parent) : QAbstractListModel(parent) +{ + columnNames << tr("Dive #") << tr("Date") << tr("Time") << tr("Duration") << tr("Location") << tr("GPS") << tr("Weight") << tr("Cyl. size") << tr("Start pressure") << + tr("End pressure") << tr("Max. depth") << tr("Avg. depth") << tr("Divemaster") << tr("Buddy") << tr("Suit") << tr("Notes") << tr("Tags") << tr("Air temp.") << tr("Water temp.") << + tr("Oâ‚‚") << tr("He") << tr("Sample time") << tr("Sample depth") << tr("Sample temperature") << tr("Sample pOâ‚‚") << tr("Sample CNS") << tr("Sample NDL") << + tr("Sample TTS") << tr("Sample stopdepth") << tr("Sample pressure") << + tr("Sample sensor1 pOâ‚‚") << tr("Sample sensor2 pOâ‚‚") << tr("Sample sensor3 pOâ‚‚") << + tr("Sample setpoint"); +} + +bool ColumnNameProvider::insertRows(int row, int count, const QModelIndex &parent) +{ + beginInsertRows(QModelIndex(), row, row); + columnNames.append(QString()); + endInsertRows(); + return true; +} + +bool ColumnNameProvider::removeRows(int row, int count, const QModelIndex &parent) +{ + beginRemoveRows(QModelIndex(), row, row); + columnNames.removeAt(row); + endRemoveRows(); + return true; +} + +bool ColumnNameProvider::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == Qt::EditRole) { + columnNames[index.row()] = value.toString(); + } + dataChanged(index, index); + return true; +} + +QVariant ColumnNameProvider::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + if (role != Qt::DisplayRole) + return QVariant(); + + return QVariant(columnNames[index.row()]); +} + +int ColumnNameProvider::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return columnNames.count(); +} + +int ColumnNameProvider::mymatch(QString value) const +{ + QString searchString = value.toLower(); + searchString.replace("\"", "").replace(" ", "").replace(".", "").replace("\n",""); + for (int i = 0; i < columnNames.count(); i++) { + QString name = columnNames.at(i).toLower(); + name.replace("\"", "").replace(" ", "").replace(".", "").replace("\n",""); + if (searchString == name.toLower()) + return i; + } + return -1; +} + + + +ColumnNameView::ColumnNameView(QWidget *parent) +{ + setAcceptDrops(true); + setDragEnabled(true); +} + +void ColumnNameView::mousePressEvent(QMouseEvent *press) +{ + QModelIndex atClick = indexAt(press->pos()); + if (!atClick.isValid()) + return; + + QRect indexRect = visualRect(atClick); + QPixmap pix(indexRect.width(), indexRect.height()); + pix.fill(QColor(0,0,0,0)); + render(&pix, QPoint(0, 0),QRegion(indexRect)); + + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + mimeData->setData(subsurface_mimedata, atClick.data().toByteArray()); + model()->removeRow(atClick.row()); + drag->setPixmap(pix); + drag->setMimeData(mimeData); + if (drag->exec() == Qt::IgnoreAction){ + model()->insertRow(model()->rowCount()); + QModelIndex idx = model()->index(model()->rowCount()-1, 0); + model()->setData(idx, mimeData->data(subsurface_mimedata)); + } +} + +void ColumnNameView::dragLeaveEvent(QDragLeaveEvent *leave) +{ + Q_UNUSED(leave); +} + +void ColumnNameView::dragEnterEvent(QDragEnterEvent *event) +{ + event->acceptProposedAction(); +} + +void ColumnNameView::dragMoveEvent(QDragMoveEvent *event) +{ + QModelIndex curr = indexAt(event->pos()); + if (!curr.isValid() || curr.row() != 0) + return; + event->acceptProposedAction(); +} + +void ColumnNameView::dropEvent(QDropEvent *event) +{ + const QMimeData *mimeData = event->mimeData(); + if (mimeData->data(subsurface_mimedata).count()) { + if (event->source() != this) { + event->acceptProposedAction(); + QVariant value = QString(mimeData->data(subsurface_mimedata)); + model()->insertRow(model()->rowCount()); + model()->setData(model()->index(model()->rowCount()-1, 0), value); + } + } +} + +ColumnDropCSVView::ColumnDropCSVView(QWidget *parent) +{ + setAcceptDrops(true); +} + +void ColumnDropCSVView::dragLeaveEvent(QDragLeaveEvent *leave) +{ + Q_UNUSED(leave); +} + +void ColumnDropCSVView::dragEnterEvent(QDragEnterEvent *event) +{ + event->acceptProposedAction(); +} + +void ColumnDropCSVView::dragMoveEvent(QDragMoveEvent *event) +{ + QModelIndex curr = indexAt(event->pos()); + if (!curr.isValid() || curr.row() != 0) + return; + event->acceptProposedAction(); +} + +void ColumnDropCSVView::dropEvent(QDropEvent *event) +{ + QModelIndex curr = indexAt(event->pos()); + if (!curr.isValid() || curr.row() != 0) + return; + + const QMimeData *mimeData = event->mimeData(); + if (!mimeData->data(subsurface_mimedata).count()) + return; + + if (event->source() == this ) { + int value_old = mimeData->data(subsurface_index).toInt(); + int value_new = curr.column(); + ColumnNameResult *m = qobject_cast(model()); + m->swapValues(value_old, value_new); + event->acceptProposedAction(); + return; + } + + if (curr.data().toString().isEmpty()) { + QVariant value = QString(mimeData->data(subsurface_mimedata)); + model()->setData(curr, value); + event->acceptProposedAction(); + } +} + +ColumnNameResult::ColumnNameResult(QObject *parent) : QAbstractTableModel(parent) +{ + +} + +void ColumnNameResult::swapValues(int firstIndex, int secondIndex) { + QString one = columnNames[firstIndex]; + QString two = columnNames[secondIndex]; + setData(index(0, firstIndex), QVariant(two), Qt::EditRole); + setData(index(0, secondIndex), QVariant(one), Qt::EditRole); +} + +bool ColumnNameResult::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() != 0) { + return false; + } + if (role == Qt::EditRole) { + columnNames[index.column()] = value.toString(); + dataChanged(index, index); + } + return true; +} + +QVariant ColumnNameResult::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + if (role == Qt::BackgroundColorRole) + if (index.row() == 0) + return QVariant(AIR_BLUE_TRANS); + + if (role != Qt::DisplayRole) + return QVariant(); + + if (index.row() == 0) { + return (columnNames[index.column()]); + } + // make sure the element exists before returning it - this might get called before the + // model is correctly set up again (e.g., when changing separators) + if (columnValues.count() > index.row() - 1 && columnValues[index.row() - 1].count() > index.column()) + return QVariant(columnValues[index.row() - 1][index.column()]); + else + return QVariant(); +} + +int ColumnNameResult::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return columnValues.count() + 1; // +1 == the header. +} + +int ColumnNameResult::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return columnNames.count(); +} + +QStringList ColumnNameResult::result() const +{ + return columnNames; +} + +void ColumnNameResult::setColumnValues(QList columns) +{ + if (rowCount() != 1) { + beginRemoveRows(QModelIndex(), 1, rowCount()-1); + columnValues.clear(); + endRemoveRows(); + } + if (columnCount() != 0) { + beginRemoveColumns(QModelIndex(), 0, columnCount()-1); + columnNames.clear(); + endRemoveColumns(); + } + + QStringList first = columns.first(); + beginInsertColumns(QModelIndex(), 0, first.count()-1); + for(int i = 0; i < first.count(); i++) + columnNames.append(QString()); + + endInsertColumns(); + + beginInsertRows(QModelIndex(), 0, columns.count()-1); + columnValues = columns; + endInsertRows(); +} + +void ColumnDropCSVView::mousePressEvent(QMouseEvent *press) +{ + QModelIndex atClick = indexAt(press->pos()); + if (!atClick.isValid() || atClick.row()) + return; + + QRect indexRect = visualRect(atClick); + QPixmap pix(indexRect.width(), indexRect.height()); + pix.fill(QColor(0,0,0,0)); + render(&pix, QPoint(0, 0),QRegion(indexRect)); + + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + mimeData->setData(subsurface_mimedata, atClick.data().toByteArray()); + mimeData->setData(subsurface_index, QString::number(atClick.column()).toLocal8Bit()); + drag->setPixmap(pix); + drag->setMimeData(mimeData); + if (drag->exec() != Qt::IgnoreAction){ + QObject *target = drag->target(); + if (target->objectName() == "qt_scrollarea_viewport") + target = target->parent(); + if (target != drag->source()) + model()->setData(atClick, QString()); + } +} + +DiveLogImportDialog::DiveLogImportDialog(QStringList fn, QWidget *parent) : QDialog(parent), + selector(true), + ui(new Ui::DiveLogImportDialog) +{ + ui->setupUi(this); + fileNames = fn; + column = 0; + delta = "0"; + hw = ""; + + /* Add indexes of XSLTs requiring special handling to the list */ + specialCSV << SENSUS; + specialCSV << SUBSURFACE; + + for (int i = 0; !CSVApps[i].name.isNull(); ++i) + ui->knownImports->addItem(CSVApps[i].name); + + ui->CSVSeparator->addItems( QStringList() << tr("Tab") << "," << ";"); + + loadFileContents(-1, INITIAL); + + /* manually import CSV file */ + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + + connect(ui->CSVSeparator, SIGNAL(currentIndexChanged(int)), this, SLOT(loadFileContentsSeperatorSelected(int))); + connect(ui->knownImports, SIGNAL(currentIndexChanged(int)), this, SLOT(loadFileContentsKnownTypesSelected(int))); +} + +DiveLogImportDialog::~DiveLogImportDialog() +{ + delete ui; +} + +void DiveLogImportDialog::loadFileContentsSeperatorSelected(int value) +{ + loadFileContents(value, SEPARATOR); +} + +void DiveLogImportDialog::loadFileContentsKnownTypesSelected(int value) +{ + loadFileContents(value, KNOWNTYPES); +} + +void DiveLogImportDialog::loadFileContents(int value, whatChanged triggeredBy) +{ + QFile f(fileNames.first()); + QList fileColumns; + QStringList currColumns; + QStringList headers; + bool matchedSome = false; + bool seabear = false; + bool xp5 = false; + bool apd = false; + + // reset everything + ColumnNameProvider *provider = new ColumnNameProvider(this); + ui->avaliableColumns->setModel(provider); + ui->avaliableColumns->setItemDelegate(new TagDragDelegate(ui->avaliableColumns)); + resultModel = new ColumnNameResult(this); + ui->tableView->setModel(resultModel); + + f.open(QFile::ReadOnly); + QString firstLine = f.readLine(); + if (firstLine.contains("SEABEAR")) { + seabear = true; + + /* + * Parse header - currently only interested in sample + * interval and hardware version. If we have old format + * the interval value is missing from the header. + */ + + while ((firstLine = f.readLine().trimmed()).length() > 0 && !f.atEnd()) { + if (firstLine.contains("//Hardware Version: ")) { + hw = firstLine.replace(QString::fromLatin1("//Hardware Version: "), QString::fromLatin1("\"Seabear ")).trimmed().append("\""); + break; + } + } + + /* + * Note that we scan over the "Log interval" on purpose + */ + + while ((firstLine = f.readLine().trimmed()).length() > 0 && !f.atEnd()) { + if (firstLine.contains("//Log interval: ")) + delta = firstLine.remove(QString::fromLatin1("//Log interval: ")).trimmed().remove(QString::fromLatin1(" s")); + } + + /* + * Parse CSV fields + */ + + firstLine = f.readLine().trimmed(); + + currColumns = firstLine.split(';'); + Q_FOREACH (QString columnText, currColumns) { + if (columnText == "Time") { + headers.append("Sample time"); + } else if (columnText == "Depth") { + headers.append("Sample depth"); + } else if (columnText == "Temperature") { + headers.append("Sample temperature"); + } else if (columnText == "NDT") { + headers.append("Sample NDL"); + } else if (columnText == "TTS") { + headers.append("Sample TTS"); + } else if (columnText == "pO2_1") { + headers.append("Sample sensor1 pOâ‚‚"); + } else if (columnText == "pO2_2") { + headers.append("Sample sensor2 pOâ‚‚"); + } else if (columnText == "pO2_3") { + headers.append("Sample sensor3 pOâ‚‚"); + } else if (columnText == "Ceiling") { + headers.append("Sample ceiling"); + } else if (columnText == "Tank pressure") { + headers.append("Sample pressure"); + } else { + // We do not know about this value + qDebug() << "Seabear import found an un-handled field: " << columnText; + headers.append(""); + } + } + + firstLine = headers.join(";"); + blockSignals(true); + ui->knownImports->setCurrentText("Seabear CSV"); + blockSignals(false); + } else if (firstLine.contains("Tauchgangs-Nr.:")) { + xp5 = true; + //"Abgelaufene Tauchzeit (Std:Min.)\tTiefe\tStickstoff Balkenanzeige\tSauerstoff Balkenanzeige\tAufstiegsgeschwindigkeit\tRestluftzeit\tRestliche Tauchzeit\tDekompressionszeit (Std:Min)\tDekostopp-Tiefe\tTemperatur\tPO2\tPressluftflasche\tLesen des Druckes\tStatus der Verbindung\tTauchstatus"; + firstLine = "Sample time\tSample depth\t\t\t\t\t\t\t\tSample temperature\t"; + blockSignals(true); + ui->knownImports->setCurrentText("XP5"); + blockSignals(false); + } + + // Special handling for APD Log Viewer + if ((triggeredBy == KNOWNTYPES && (value == APD || value == APD2)) || (triggeredBy == INITIAL && fileNames.first().endsWith(".apd", Qt::CaseInsensitive))) { + apd=true; + firstLine = "Sample time\tSample depth\tSample setpoint\tSample sensor1 pOâ‚‚\tSample sensor2 pOâ‚‚\tSample sensor3 pOâ‚‚\tSample pOâ‚‚\t\t\t\t\t\t\t\t\tSample temperature\t\tSample CNS\tSample stopdepth"; + blockSignals(true); + ui->CSVSeparator->setCurrentText(tr("Tab")); + if (triggeredBy == INITIAL && fileNames.first().contains(".apd", Qt::CaseInsensitive)) + ui->knownImports->setCurrentText("APD Log Viewer - DC1"); + blockSignals(false); + } + + QString separator = ui->CSVSeparator->currentText() == tr("Tab") ? "\t" : ui->CSVSeparator->currentText(); + currColumns = firstLine.split(separator); + if (triggeredBy == INITIAL) { + // guess the separator + int tabs = firstLine.count('\t'); + int commas = firstLine.count(','); + int semis = firstLine.count(';'); + if (tabs > commas && tabs > semis) + separator = "\t"; + else if (commas > tabs && commas > semis) + separator = ","; + else if (semis > tabs && semis > commas) + separator = ";"; + if (ui->CSVSeparator->currentText() != separator) { + blockSignals(true); + ui->CSVSeparator->setCurrentText(separator); + blockSignals(false); + currColumns = firstLine.split(separator); + } + } + if (triggeredBy == INITIAL || (triggeredBy == KNOWNTYPES && value == MANUAL) || triggeredBy == SEPARATOR) { + // now try and guess the columns + Q_FOREACH (QString columnText, currColumns) { + /* + * We have to skip the conversion of 2 to â‚‚ for APD Log + * viewer as that would mess up the sensor numbering. We + * also know that the column headers do not need this + * conversion. + */ + if (apd == false) { + columnText.replace("\"", ""); + columnText.replace("number", "#", Qt::CaseInsensitive); + columnText.replace("2", "â‚‚", Qt::CaseInsensitive); + columnText.replace("cylinder", "cyl.", Qt::CaseInsensitive); + } + int idx = provider->mymatch(columnText.trimmed()); + if (idx >= 0) { + QString foundHeading = provider->data(provider->index(idx, 0), Qt::DisplayRole).toString(); + provider->removeRow(idx); + headers.append(foundHeading); + matchedSome = true; + } else { + headers.append(""); + } + } + if (matchedSome) { + ui->dragInstructions->setText(tr("Some column headers were pre-populated; please drag and drop the headers so they match the column they are in.")); + if (triggeredBy != KNOWNTYPES && !seabear && !xp5 && !apd) { + blockSignals(true); + ui->knownImports->setCurrentIndex(0); // <- that's "Manual import" + blockSignals(false); + } + } + } + if (triggeredBy == KNOWNTYPES && value != MANUAL) { + // an actual known type + if (value == SUBSURFACE) { + /* + * Subsurface CSV file needs separator detection + * as we used to default to comma but switched + * to tab. + */ + int tabs = firstLine.count('\t'); + int commas = firstLine.count(','); + if (tabs > commas) + separator = "Tab"; + else + separator = ","; + } else { + separator = CSVApps[value].separator; + } + + if (ui->CSVSeparator->currentText() != separator || separator == "Tab") { + ui->CSVSeparator->blockSignals(true); + ui->CSVSeparator->setCurrentText(separator); + ui->CSVSeparator->blockSignals(false); + if (separator == "Tab") + separator = "\t"; + currColumns = firstLine.split(separator); + } + // now set up time, depth, temperature, po2, cns, ndl, tts, stopdepth, pressure, setpoint + for (int i = 0; i < currColumns.count(); i++) + headers.append(""); + if (CSVApps[value].time > -1 && CSVApps[value].time < currColumns.count()) + headers.replace(CSVApps[value].time, tr("Sample time")); + if (CSVApps[value].depth > -1 && CSVApps[value].depth < currColumns.count()) + headers.replace(CSVApps[value].depth, tr("Sample depth")); + if (CSVApps[value].temperature > -1 && CSVApps[value].temperature < currColumns.count()) + headers.replace(CSVApps[value].temperature, tr("Sample temperature")); + if (CSVApps[value].po2 > -1 && CSVApps[value].po2 < currColumns.count()) + headers.replace(CSVApps[value].po2, tr("Sample pOâ‚‚")); + if (CSVApps[value].sensor1 > -1 && CSVApps[value].sensor1 < currColumns.count()) + headers.replace(CSVApps[value].sensor1, tr("Sample sensor1 pOâ‚‚")); + if (CSVApps[value].sensor2 > -1 && CSVApps[value].sensor2 < currColumns.count()) + headers.replace(CSVApps[value].sensor2, tr("Sample sensor2 pOâ‚‚")); + if (CSVApps[value].sensor3 > -1 && CSVApps[value].sensor3 < currColumns.count()) + headers.replace(CSVApps[value].sensor3, tr("Sample sensor3 pOâ‚‚")); + if (CSVApps[value].cns > -1 && CSVApps[value].cns < currColumns.count()) + headers.replace(CSVApps[value].cns, tr("Sample CNS")); + if (CSVApps[value].ndl > -1 && CSVApps[value].ndl < currColumns.count()) + headers.replace(CSVApps[value].ndl, tr("Sample NDL")); + if (CSVApps[value].tts > -1 && CSVApps[value].tts < currColumns.count()) + headers.replace(CSVApps[value].tts, tr("Sample TTS")); + if (CSVApps[value].stopdepth > -1 && CSVApps[value].stopdepth < currColumns.count()) + headers.replace(CSVApps[value].stopdepth, tr("Sample stopdepth")); + if (CSVApps[value].pressure > -1 && CSVApps[value].pressure < currColumns.count()) + headers.replace(CSVApps[value].pressure, tr("Sample pressure")); + if (CSVApps[value].setpoint > -1 && CSVApps[value].setpoint < currColumns.count()) + headers.replace(CSVApps[value].setpoint, tr("Sample setpoint")); + + /* Show the Subsurface CSV column headers */ + if (value == SUBSURFACE && currColumns.count() >= 23) { + headers.replace(0, tr("Dive #")); + headers.replace(1, tr("Date")); + headers.replace(2, tr("Time")); + headers.replace(3, tr("Duration")); + headers.replace(4, tr("Max. depth")); + headers.replace(5, tr("Avg. depth")); + headers.replace(6, tr("Air temp.")); + headers.replace(7, tr("Water temp.")); + headers.replace(8, tr("Cyl. size")); + headers.replace(9, tr("Start pressure")); + headers.replace(10, tr("End pressure")); + headers.replace(11, tr("Oâ‚‚")); + headers.replace(12, tr("He")); + headers.replace(13, tr("Location")); + headers.replace(14, tr("GPS")); + headers.replace(15, tr("Divemaster")); + headers.replace(16, tr("Buddy")); + headers.replace(17, tr("Suit")); + headers.replace(18, tr("Rating")); + headers.replace(19, tr("Visibility")); + headers.replace(20, tr("Notes")); + headers.replace(21, tr("Weight")); + headers.replace(22, tr("Tags")); + + blockSignals(true); + ui->CSVSeparator->setCurrentText(separator); + ui->DateFormat->setCurrentText("yyyy-mm-dd"); + ui->DurationFormat->setCurrentText("Minutes:seconds"); + blockSignals(false); + } + } + + f.reset(); + int rows = 0; + + /* Skipping the header of Seabear and XP5 CSV files. */ + if (seabear || xp5) { + /* + * First set of data on Seabear CSV file is metadata + * that is separated by an empty line (windows line + * termination might be encountered. + */ + while (strlen(f.readLine()) > 3 && !f.atEnd()); + /* + * Next we have description of the fields and two dummy + * lines. Separated again with an empty line from the + * actual data. + */ + while (strlen(f.readLine()) > 3 && !f.atEnd()); + } + + while (rows < 10 && !f.atEnd()) { + QString currLine = f.readLine().trimmed(); + currColumns = currLine.split(separator); + fileColumns.append(currColumns); + rows += 1; + } + resultModel->setColumnValues(fileColumns); + for (int i = 0; i < headers.count(); i++) + if (!headers.at(i).isEmpty()) + resultModel->setData(resultModel->index(0, i),headers.at(i),Qt::EditRole); +} + +char *intdup(int index) +{ + char tmpbuf[21]; + + snprintf(tmpbuf, sizeof(tmpbuf) - 2, "%d", index); + tmpbuf[20] = 0; + return strdup(tmpbuf); +} + +int DiveLogImportDialog::setup_csv_params(QStringList r, char **params, int pnr) +{ + params[pnr++] = strdup("timeField"); + params[pnr++] = intdup(r.indexOf(tr("Sample time"))); + params[pnr++] = strdup("depthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample depth"))); + params[pnr++] = strdup("tempField"); + params[pnr++] = intdup(r.indexOf(tr("Sample temperature"))); + params[pnr++] = strdup("po2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample pOâ‚‚"))); + params[pnr++] = strdup("o2sensor1Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor1 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor2 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor3Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor3 pOâ‚‚"))); + params[pnr++] = strdup("cnsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample CNS"))); + params[pnr++] = strdup("ndlField"); + params[pnr++] = intdup(r.indexOf(tr("Sample NDL"))); + params[pnr++] = strdup("ttsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample TTS"))); + params[pnr++] = strdup("stopdepthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample stopdepth"))); + params[pnr++] = strdup("pressureField"); + params[pnr++] = intdup(r.indexOf(tr("Sample pressure"))); + params[pnr++] = strdup("setpointFiend"); + params[pnr++] = intdup(r.indexOf(tr("Sample setpoint"))); + params[pnr++] = strdup("separatorIndex"); + params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); + params[pnr++] = strdup("units"); + params[pnr++] = intdup(ui->CSVUnits->currentIndex()); + if (hw.length()) { + params[pnr++] = strdup("hw"); + params[pnr++] = strdup(hw.toUtf8().data()); + } else if (ui->knownImports->currentText().length() > 0) { + params[pnr++] = strdup("hw"); + params[pnr++] = strdup(ui->knownImports->currentText().prepend("\"").append("\"").toUtf8().data()); + } + params[pnr++] = NULL; + + return pnr; +} + +void DiveLogImportDialog::on_buttonBox_accepted() +{ + QStringList r = resultModel->result(); + if (ui->knownImports->currentText() != "Manual import") { + for (int i = 0; i < fileNames.size(); ++i) { + if (ui->knownImports->currentText() == "Seabear CSV") { + char *params[40]; + int pnr = 0; + + params[pnr++] = strdup("timeField"); + params[pnr++] = intdup(r.indexOf(tr("Sample time"))); + params[pnr++] = strdup("depthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample depth"))); + params[pnr++] = strdup("tempField"); + params[pnr++] = intdup(r.indexOf(tr("Sample temperature"))); + params[pnr++] = strdup("po2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample pOâ‚‚"))); + params[pnr++] = strdup("o2sensor1Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor1 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor2 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor3Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor3 pOâ‚‚"))); + params[pnr++] = strdup("cnsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample CNS"))); + params[pnr++] = strdup("ndlField"); + params[pnr++] = intdup(r.indexOf(tr("Sample NDL"))); + params[pnr++] = strdup("ttsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample TTS"))); + params[pnr++] = strdup("stopdepthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample stopdepth"))); + params[pnr++] = strdup("pressureField"); + params[pnr++] = intdup(r.indexOf(tr("Sample pressure"))); + params[pnr++] = strdup("setpointFiend"); + params[pnr++] = intdup(-1); + params[pnr++] = strdup("separatorIndex"); + params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); + params[pnr++] = strdup("units"); + params[pnr++] = intdup(ui->CSVUnits->currentIndex()); + params[pnr++] = strdup("delta"); + params[pnr++] = strdup(delta.toUtf8().data()); + if (hw.length()) { + params[pnr++] = strdup("hw"); + params[pnr++] = strdup(hw.toUtf8().data()); + } + params[pnr++] = NULL; + + if (parse_seabear_csv_file(fileNames[i].toUtf8().data(), + params, pnr - 1, "csv") < 0) { + return; + } + // Seabear CSV stores NDL and TTS in Minutes, not seconds + struct dive *dive = dive_table.dives[dive_table.nr - 1]; + for(int s_nr = 0 ; s_nr <= dive->dc.samples ; s_nr++) { + struct sample *sample = dive->dc.sample + s_nr; + sample->ndl.seconds *= 60; + sample->tts.seconds *= 60; + } + } else { + char *params[37]; + int pnr = 0; + + pnr = setup_csv_params(r, params, pnr); + parse_csv_file(fileNames[i].toUtf8().data(), params, pnr - 1, + specialCSV.contains(ui->knownImports->currentIndex()) ? CSVApps[ui->knownImports->currentIndex()].name.toUtf8().data() : "csv"); + } + } + } else { + for (int i = 0; i < fileNames.size(); ++i) { + if (r.indexOf(tr("Sample time")) < 0) { + char *params[55]; + int pnr = 0; + params[pnr++] = strdup("numberField"); + params[pnr++] = intdup(r.indexOf(tr("Dive #"))); + params[pnr++] = strdup("dateField"); + params[pnr++] = intdup(r.indexOf(tr("Date"))); + params[pnr++] = strdup("timeField"); + params[pnr++] = intdup(r.indexOf(tr("Time"))); + params[pnr++] = strdup("durationField"); + params[pnr++] = intdup(r.indexOf(tr("Duration"))); + params[pnr++] = strdup("locationField"); + params[pnr++] = intdup(r.indexOf(tr("Location"))); + params[pnr++] = strdup("gpsField"); + params[pnr++] = intdup(r.indexOf(tr("GPS"))); + params[pnr++] = strdup("maxDepthField"); + params[pnr++] = intdup(r.indexOf(tr("Max. depth"))); + params[pnr++] = strdup("meanDepthField"); + params[pnr++] = intdup(r.indexOf(tr("Avg. depth"))); + params[pnr++] = strdup("divemasterField"); + params[pnr++] = intdup(r.indexOf(tr("Divemaster"))); + params[pnr++] = strdup("buddyField"); + params[pnr++] = intdup(r.indexOf(tr("Buddy"))); + params[pnr++] = strdup("suitField"); + params[pnr++] = intdup(r.indexOf(tr("Suit"))); + params[pnr++] = strdup("notesField"); + params[pnr++] = intdup(r.indexOf(tr("Notes"))); + params[pnr++] = strdup("weightField"); + params[pnr++] = intdup(r.indexOf(tr("Weight"))); + params[pnr++] = strdup("tagsField"); + params[pnr++] = intdup(r.indexOf(tr("Tags"))); + params[pnr++] = strdup("separatorIndex"); + params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); + params[pnr++] = strdup("units"); + params[pnr++] = intdup(ui->CSVUnits->currentIndex()); + params[pnr++] = strdup("datefmt"); + params[pnr++] = intdup(ui->DateFormat->currentIndex()); + params[pnr++] = strdup("durationfmt"); + params[pnr++] = intdup(ui->DurationFormat->currentIndex()); + params[pnr++] = strdup("cylindersizeField"); + params[pnr++] = intdup(r.indexOf(tr("Cyl. size"))); + params[pnr++] = strdup("startpressureField"); + params[pnr++] = intdup(r.indexOf(tr("Start pressure"))); + params[pnr++] = strdup("endpressureField"); + params[pnr++] = intdup(r.indexOf(tr("End pressure"))); + params[pnr++] = strdup("o2Field"); + params[pnr++] = intdup(r.indexOf(tr("Oâ‚‚"))); + params[pnr++] = strdup("heField"); + params[pnr++] = intdup(r.indexOf(tr("He"))); + params[pnr++] = strdup("airtempField"); + params[pnr++] = intdup(r.indexOf(tr("Air temp."))); + params[pnr++] = strdup("watertempField"); + params[pnr++] = intdup(r.indexOf(tr("Water temp."))); + params[pnr++] = NULL; + + parse_manual_file(fileNames[i].toUtf8().data(), params, pnr - 1); + } else { + char *params[37]; + int pnr = 0; + + pnr = setup_csv_params(r, params, pnr); + parse_csv_file(fileNames[i].toUtf8().data(), params, pnr - 1, + specialCSV.contains(ui->knownImports->currentIndex()) ? CSVApps[ui->knownImports->currentIndex()].name.toUtf8().data() : "csv"); + } + } + } + + process_dives(true, false); + MainWindow::instance()->refreshDisplay(); +} + +TagDragDelegate::TagDragDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +QSize TagDragDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + QSize originalSize = QStyledItemDelegate::sizeHint(option, index); + return originalSize + QSize(5,5); +} + +void TagDragDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + painter->save(); + painter->setRenderHints(QPainter::Antialiasing); + painter->setBrush(QBrush(AIR_BLUE_TRANS)); + painter->drawRoundedRect(option.rect.adjusted(2,2,-2,-2), 5, 5); + painter->restore(); + QStyledItemDelegate::paint(painter, option, index); +} diff --git a/desktop-widgets/divelogimportdialog.h b/desktop-widgets/divelogimportdialog.h new file mode 100644 index 000000000..2d12c7cac --- /dev/null +++ b/desktop-widgets/divelogimportdialog.h @@ -0,0 +1,131 @@ +#ifndef DIVELOGIMPORTDIALOG_H +#define DIVELOGIMPORTDIALOG_H + +#include +#include +#include +#include +#include +#include +#include + +#include "subsurface-core/dive.h" +#include "subsurface-core/divelist.h" + +namespace Ui { + class DiveLogImportDialog; +} + +class ColumnNameProvider : public QAbstractListModel { + Q_OBJECT +public: + ColumnNameProvider(QObject *parent); + bool insertRows(int row, int count, const QModelIndex &parent); + bool removeRows(int row, int count, const QModelIndex &parent); + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant data(const QModelIndex &index, int role) const; + int rowCount(const QModelIndex &parent) const; + int mymatch(QString value) const; +private: + QStringList columnNames; +}; + +class ColumnNameResult : public QAbstractTableModel { + Q_OBJECT +public: + ColumnNameResult(QObject *parent); + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant data(const QModelIndex &index, int role) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + void setColumnValues(QList columns); + QStringList result() const; + void swapValues(int firstIndex, int secondIndex); +private: + QList columnValues; + QStringList columnNames; +}; + +class ColumnNameView : public QListView { + Q_OBJECT +public: + ColumnNameView(QWidget *parent); +protected: + void mousePressEvent(QMouseEvent *press); + void dragLeaveEvent(QDragLeaveEvent *leave); + void dragEnterEvent(QDragEnterEvent *event); + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); +private: +}; + +class ColumnDropCSVView : public QTableView { + Q_OBJECT +public: + ColumnDropCSVView(QWidget *parent); +protected: + void mousePressEvent(QMouseEvent *press); + void dragLeaveEvent(QDragLeaveEvent *leave); + void dragEnterEvent(QDragEnterEvent *event); + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); +private: + QStringList columns; +}; + +class DiveLogImportDialog : public QDialog { + Q_OBJECT + +public: + explicit DiveLogImportDialog(QStringList fn, QWidget *parent = 0); + ~DiveLogImportDialog(); + enum whatChanged { INITIAL, SEPARATOR, KNOWNTYPES }; +private +slots: + void on_buttonBox_accepted(); + void loadFileContentsSeperatorSelected(int value); + void loadFileContentsKnownTypesSelected(int value); + void loadFileContents(int value, enum whatChanged triggeredBy); + int setup_csv_params(QStringList r, char **params, int pnr); + +private: + bool selector; + QStringList fileNames; + Ui::DiveLogImportDialog *ui; + QList specialCSV; + int column; + ColumnNameResult *resultModel; + QString delta; + QString hw; + + struct CSVAppConfig { + QString name; + int time; + int depth; + int temperature; + int po2; + int sensor1; + int sensor2; + int sensor3; + int cns; + int ndl; + int tts; + int stopdepth; + int pressure; + int setpoint; + QString separator; + }; + +#define CSVAPPS 8 + static const CSVAppConfig CSVApps[CSVAPPS]; +}; + +class TagDragDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + TagDragDelegate(QObject *parent); + QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const; + void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const; +}; + +#endif // DIVELOGIMPORTDIALOG_H diff --git a/desktop-widgets/divelogimportdialog.ui b/desktop-widgets/divelogimportdialog.ui new file mode 100644 index 000000000..6d154b7c6 --- /dev/null +++ b/desktop-widgets/divelogimportdialog.ui @@ -0,0 +1,249 @@ + + + DiveLogImportDialog + + + + 0 + 0 + 614 + 434 + + + + Import dive log file + + + + :/subsurface-icon + + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 2 + + + + + -1 + + + + + + + + + + + dd.mm.yyyy + + + + + mm/dd/yyyy + + + + + yyyy-mm-dd + + + + + + + + + Seconds + + + + + Minutes + + + + + Minutes:seconds + + + + + + + + + Metric + + + + + Imperial + + + + + + + + + + + 16777215 + 100 + + + + QListView::IconMode + + + + + + + Drag the tags above to each corresponding column below + + + + + + + + 16777215 + 16777215 + + + + false + + + false + + + false + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + + ColumnNameView + QListView +
divelogimportdialog.h
+
+ + ColumnDropCSVView + QTableView +
divelogimportdialog.h
+
+
+ + buttonBox + + + + + buttonBox + accepted() + DiveLogImportDialog + accept() + + + 334 + 467 + + + 215 + 164 + + + + + buttonBox + rejected() + DiveLogImportDialog + reject() + + + 334 + 467 + + + 215 + 164 + + + + +
diff --git a/desktop-widgets/divepicturewidget.cpp b/desktop-widgets/divepicturewidget.cpp new file mode 100644 index 000000000..bed3d3bd1 --- /dev/null +++ b/desktop-widgets/divepicturewidget.cpp @@ -0,0 +1,100 @@ +#include "divepicturewidget.h" +#include "divepicturemodel.h" +#include "metrics.h" +#include "dive.h" +#include "divelist.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void loadPicture(struct picture *picture) +{ + ImageDownloader download(picture); + download.load(); +} + +SHashedImage::SHashedImage(struct picture *picture) : QImage() +{ + QUrl url = QUrl::fromUserInput(QString(picture->filename)); + if(url.isLocalFile()) + load(url.toLocalFile()); + if (isNull()) { + // Hash lookup. + load(fileFromHash(picture->hash)); + if (!isNull()) { + QtConcurrent::run(updateHash, picture); + } else { + QtConcurrent::run(loadPicture, picture); + } + } else { + QByteArray hash = hashFile(url.toLocalFile()); + free(picture->hash); + picture->hash = strdup(hash.toHex().data()); + } +} + +ImageDownloader::ImageDownloader(struct picture *pic) +{ + picture = pic; +} + +void ImageDownloader::load(){ + QUrl url = QUrl::fromUserInput(QString(picture->filename)); + if (url.isValid()) { + QEventLoop loop; + QNetworkRequest request(url); + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveImage(QNetworkReply *))); + QNetworkReply *reply = manager.get(request); + while (reply->isRunning()) { + loop.processEvents(); + sleep(1); + } + } + +} + +void ImageDownloader::saveImage(QNetworkReply *reply) +{ + QByteArray imageData = reply->readAll(); + QImage image = QImage(); + image.loadFromData(imageData); + if (image.isNull()) + return; + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(imageData); + QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); + QDir dir(path); + if (!dir.exists()) + dir.mkpath(path); + QFile imageFile(path.append("/").append(hash.result().toHex())); + if (imageFile.open(QIODevice::WriteOnly)) { + QDataStream stream(&imageFile); + stream.writeRawData(imageData.data(), imageData.length()); + imageFile.waitForBytesWritten(-1); + imageFile.close(); + add_hash(imageFile.fileName(), hash.result()); + learnHash(picture, hash.result()); + DivePictureModel::instance()->updateDivePictures(); + } + reply->manager()->deleteLater(); + reply->deleteLater(); +} + +DivePictureWidget::DivePictureWidget(QWidget *parent) : QListView(parent) +{ + connect(this, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(doubleClicked(const QModelIndex &))); +} + +void DivePictureWidget::doubleClicked(const QModelIndex &index) +{ + QString filePath = model()->data(index, Qt::DisplayPropertyRole).toString(); + emit photoDoubleClicked(localFilePath(filePath)); +} diff --git a/desktop-widgets/divepicturewidget.h b/desktop-widgets/divepicturewidget.h new file mode 100644 index 000000000..54f5bb826 --- /dev/null +++ b/desktop-widgets/divepicturewidget.h @@ -0,0 +1,36 @@ +#ifndef DIVEPICTUREWIDGET_H +#define DIVEPICTUREWIDGET_H + +#include +#include +#include +#include +#include + +class ImageDownloader : public QObject { + Q_OBJECT; +public: + ImageDownloader(struct picture *picture); + void load(); +private: + struct picture *picture; + QNetworkAccessManager manager; +private slots: + void saveImage(QNetworkReply *reply); +}; + +class DivePictureWidget : public QListView { + Q_OBJECT +public: + DivePictureWidget(QWidget *parent); +signals: + void photoDoubleClicked(const QString filePath); +private +slots: + void doubleClicked(const QModelIndex &index); +}; + +class DivePictureThumbnailThread : public QThread { +}; + +#endif diff --git a/desktop-widgets/diveplanner.cpp b/desktop-widgets/diveplanner.cpp new file mode 100644 index 000000000..b4413d11a --- /dev/null +++ b/desktop-widgets/diveplanner.cpp @@ -0,0 +1,513 @@ +#include "diveplanner.h" +#include "modeldelegates.h" +#include "mainwindow.h" +#include "planner.h" +#include "helpers.h" +#include "cylindermodel.h" +#include "models.h" +#include "profile/profilewidget2.h" +#include "diveplannermodel.h" + +#include +#include +#include +#include + +#define TIME_INITIAL_MAX 30 + +#define MAX_DEPTH M_OR_FT(150, 450) +#define MIN_DEPTH M_OR_FT(20, 60) + +#define UNIT_FACTOR ((prefs.units.length == units::METERS) ? 1000.0 / 60.0 : feet_to_mm(1.0) / 60.0) + +static DivePlannerPointsModel* plannerModel = DivePlannerPointsModel::instance(); + +DiveHandler::DiveHandler() : QGraphicsEllipseItem() +{ + setRect(-5, -5, 10, 10); + setFlags(ItemIgnoresTransformations | ItemIsSelectable | ItemIsMovable | ItemSendsGeometryChanges); + setBrush(Qt::white); + setZValue(2); + t.start(); +} + +int DiveHandler::parentIndex() +{ + ProfileWidget2 *view = qobject_cast(scene()->views().first()); + return view->handles.indexOf(this); +} + +void DiveHandler::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + QMenu m; + // Don't have a gas selection for the last point + QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS); + if (index.sibling(index.row() + 1, index.column()).isValid()) { + GasSelectionModel *model = GasSelectionModel::instance(); + model->repopulate(); + int rowCount = model->rowCount(); + for (int i = 0; i < rowCount; i++) { + QAction *action = new QAction(&m); + action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString()); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); + m.addAction(action); + } + } + // don't allow removing the last point + if (plannerModel->rowCount() > 1) { + m.addSeparator(); + m.addAction(QObject::tr("Remove this point"), this, SLOT(selfRemove())); + m.exec(event->screenPos()); + } +} + +void DiveHandler::selfRemove() +{ + setSelected(true); + ProfileWidget2 *view = qobject_cast(scene()->views().first()); + view->keyDeleteAction(); +} + +void DiveHandler::changeGas() +{ + QAction *action = qobject_cast(sender()); + QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS); + plannerModel->gaschange(index.sibling(index.row() + 1, index.column()), action->text()); +} + +void DiveHandler::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (t.elapsed() < 40) + return; + t.start(); + + ProfileWidget2 *view = qobject_cast(scene()->views().first()); + if(view->isPointOutOfBoundaries(event->scenePos())) + return; + + QGraphicsEllipseItem::mouseMoveEvent(event); + emit moved(); +} + +void DiveHandler::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mousePressEvent(event); + emit clicked(); +} + +void DiveHandler::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mouseReleaseEvent(event); + emit released(); +} + +DivePlannerWidget::DivePlannerWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) +{ + ui.setupUi(this); + ui.dateEdit->setDisplayFormat(getDateFormat()); + ui.tableWidget->setTitle(tr("Dive planner points")); + ui.tableWidget->setModel(plannerModel); + plannerModel->setRecalc(true); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::GAS, new AirTypesDelegate(this)); + ui.cylinderTableWidget->setTitle(tr("Available gases")); + ui.cylinderTableWidget->setModel(CylindersModel::instance()); + QTableView *view = ui.cylinderTableWidget->view(); + view->setColumnHidden(CylindersModel::START, true); + view->setColumnHidden(CylindersModel::END, true); + view->setColumnHidden(CylindersModel::DEPTH, false); + view->setItemDelegateForColumn(CylindersModel::TYPE, new TankInfoDelegate(this)); + connect(ui.cylinderTableWidget, SIGNAL(addButtonClicked()), plannerModel, SLOT(addCylinder_clicked())); + connect(ui.tableWidget, SIGNAL(addButtonClicked()), plannerModel, SLOT(addStop())); + + connect(CylindersModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), + GasSelectionModel::instance(), SLOT(repopulate())); + connect(CylindersModel::instance(), SIGNAL(rowsInserted(QModelIndex, int, int)), + GasSelectionModel::instance(), SLOT(repopulate())); + connect(CylindersModel::instance(), SIGNAL(rowsRemoved(QModelIndex, int, int)), + GasSelectionModel::instance(), SLOT(repopulate())); + connect(CylindersModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), + plannerModel, SIGNAL(cylinderModelEdited())); + connect(CylindersModel::instance(), SIGNAL(rowsInserted(QModelIndex, int, int)), + plannerModel, SIGNAL(cylinderModelEdited())); + connect(CylindersModel::instance(), SIGNAL(rowsRemoved(QModelIndex, int, int)), + plannerModel, SIGNAL(cylinderModelEdited())); + connect(plannerModel, SIGNAL(calculatedPlanNotes()), MainWindow::instance(), SLOT(setPlanNotes())); + + + ui.tableWidget->setBtnToolTip(tr("Add dive data point")); + connect(ui.startTime, SIGNAL(timeChanged(QTime)), plannerModel, SLOT(setStartTime(QTime))); + connect(ui.dateEdit, SIGNAL(dateChanged(QDate)), plannerModel, SLOT(setStartDate(QDate))); + connect(ui.ATMPressure, SIGNAL(valueChanged(int)), this, SLOT(atmPressureChanged(int))); + connect(ui.atmHeight, SIGNAL(valueChanged(int)), this, SLOT(heightChanged(int))); + connect(ui.salinity, SIGNAL(valueChanged(double)), this, SLOT(salinityChanged(double))); + connect(plannerModel, SIGNAL(startTimeChanged(QDateTime)), this, SLOT(setupStartTime(QDateTime))); + + // Creating (and canceling) the plan + replanButton = ui.buttonBox->addButton(tr("Save new"), QDialogButtonBox::ActionRole); + connect(replanButton, SIGNAL(clicked()), plannerModel, SLOT(saveDuplicatePlan())); + connect(ui.buttonBox, SIGNAL(accepted()), plannerModel, SLOT(savePlan())); + connect(ui.buttonBox, SIGNAL(rejected()), plannerModel, SLOT(cancelPlan())); + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::Key_Escape), this); + connect(closeKey, SIGNAL(activated()), plannerModel, SLOT(cancelPlan())); + + // This makes shure the spinbox gets a setMinimum(0) on it so we can't have negative time or depth. + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::DEPTH, new SpinBoxDelegate(0, INT_MAX, 1, this)); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::RUNTIME, new SpinBoxDelegate(0, INT_MAX, 1, this)); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::DURATION, new SpinBoxDelegate(0, INT_MAX, 1, this)); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::CCSETPOINT, new DoubleSpinBoxDelegate(0, 2, 0.1, this)); + + /* set defaults. */ + ui.ATMPressure->setValue(1013); + ui.atmHeight->setValue(0); + + setMinimumWidth(0); + setMinimumHeight(0); +} + +void DivePlannerWidget::setReplanButton(bool replan) +{ + replanButton->setVisible(replan); +} + +void DivePlannerWidget::setupStartTime(QDateTime startTime) +{ + ui.startTime->setTime(startTime.time()); + ui.dateEdit->setDate(startTime.date()); +} + +void DivePlannerWidget::settingsChanged() +{ + // Adopt units + if (get_units()->length == units::FEET) { + ui.atmHeight->setSuffix("ft"); + } else { + ui.atmHeight->setSuffix(("m")); + } + ui.atmHeight->blockSignals(true); + ui.atmHeight->setValue((int) get_depth_units((int) (log(1013.0 / plannerModel->getSurfacePressure()) * 7800000), NULL,NULL)); + ui.atmHeight->blockSignals(false); +} + +void DivePlannerWidget::atmPressureChanged(const int pressure) +{ + plannerModel->setSurfacePressure(pressure); + ui.atmHeight->blockSignals(true); + ui.atmHeight->setValue((int) get_depth_units((int) (log(1013.0 / pressure) * 7800000), NULL,NULL)); + ui.atmHeight->blockSignals(false); +} + +void DivePlannerWidget::heightChanged(const int height) +{ + int pressure = (int) (1013.0 * exp(- (double) units_to_depth((double) height) / 7800000.0)); + ui.ATMPressure->blockSignals(true); + ui.ATMPressure->setValue(pressure); + ui.ATMPressure->blockSignals(false); + plannerModel->setSurfacePressure(pressure); +} + +void DivePlannerWidget::salinityChanged(const double salinity) +{ + /* Salinity is expressed in weight in grams per 10l */ + plannerModel->setSalinity(10000 * salinity); +} + +void PlannerSettingsWidget::bottomSacChanged(const double bottomSac) +{ + plannerModel->setBottomSac(bottomSac); +} + +void PlannerSettingsWidget::decoSacChanged(const double decosac) +{ + plannerModel->setDecoSac(decosac); +} + +void PlannerSettingsWidget::disableDecoElements(int mode) +{ + if (mode == RECREATIONAL) { + ui.gflow->setDisabled(false); + ui.gfhigh->setDisabled(false); + ui.lastStop->setDisabled(true); + ui.backgasBreaks->setDisabled(true); + ui.bottompo2->setDisabled(true); + ui.decopo2->setDisabled(true); + ui.reserve_gas->setDisabled(false); + ui.conservatism_lvl->setDisabled(true); + ui.switch_at_req_stop->setDisabled(true); + ui.min_switch_duration->setDisabled(true); + } + else if (mode == VPMB) { + ui.gflow->setDisabled(true); + ui.gfhigh->setDisabled(true); + ui.lastStop->setDisabled(false); + ui.backgasBreaks->setDisabled(false); + ui.bottompo2->setDisabled(false); + ui.decopo2->setDisabled(false); + ui.reserve_gas->setDisabled(true); + ui.conservatism_lvl->setDisabled(false); + ui.switch_at_req_stop->setDisabled(false); + ui.min_switch_duration->setDisabled(false); + } + else if (mode == BUEHLMANN) { + ui.gflow->setDisabled(false); + ui.gfhigh->setDisabled(false); + ui.lastStop->setDisabled(false); + ui.backgasBreaks->setDisabled(false); + ui.bottompo2->setDisabled(false); + ui.decopo2->setDisabled(false); + ui.reserve_gas->setDisabled(true); + ui.conservatism_lvl->setDisabled(true); + ui.switch_at_req_stop->setDisabled(false); + ui.min_switch_duration->setDisabled(false); + } +} + +void DivePlannerWidget::printDecoPlan() +{ + MainWindow::instance()->printPlan(); +} + +PlannerSettingsWidget::PlannerSettingsWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) +{ + ui.setupUi(this); + + QSettings s; + QStringList rebreather_modes; + s.beginGroup("Planner"); + prefs.last_stop = s.value("last_stop", prefs.last_stop).toBool(); + prefs.verbatim_plan = s.value("verbatim_plan", prefs.verbatim_plan).toBool(); + prefs.display_duration = s.value("display_duration", prefs.display_duration).toBool(); + prefs.display_runtime = s.value("display_runtime", prefs.display_runtime).toBool(); + prefs.display_transitions = s.value("display_transitions", prefs.display_transitions).toBool(); + prefs.deco_mode = deco_mode(s.value("deco_mode", prefs.deco_mode).toInt()); + prefs.safetystop = s.value("safetystop", prefs.safetystop).toBool(); + prefs.reserve_gas = s.value("reserve_gas", prefs.reserve_gas).toInt(); + prefs.ascrate75 = s.value("ascrate75", prefs.ascrate75).toInt(); + prefs.ascrate50 = s.value("ascrate50", prefs.ascrate50).toInt(); + prefs.ascratestops = s.value("ascratestops", prefs.ascratestops).toInt(); + prefs.ascratelast6m = s.value("ascratelast6m", prefs.ascratelast6m).toInt(); + prefs.descrate = s.value("descrate", prefs.descrate).toInt(); + prefs.bottompo2 = s.value("bottompo2", prefs.bottompo2).toInt(); + prefs.decopo2 = s.value("decopo2", prefs.decopo2).toInt(); + prefs.doo2breaks = s.value("doo2breaks", prefs.doo2breaks).toBool(); + prefs.switch_at_req_stop = s.value("switch_at_req_stop", prefs.switch_at_req_stop).toBool(); + prefs.min_switch_duration = s.value("min_switch_duration", prefs.min_switch_duration).toInt(); + prefs.drop_stone_mode = s.value("drop_stone_mode", prefs.drop_stone_mode).toBool(); + prefs.bottomsac = s.value("bottomsac", prefs.bottomsac).toInt(); + prefs.decosac = s.value("decosac", prefs.decosac).toInt(); + prefs.conservatism_level = s.value("conservatism", prefs.conservatism_level).toInt(); + plannerModel->getDiveplan().bottomsac = prefs.bottomsac; + plannerModel->getDiveplan().decosac = prefs.decosac; + s.endGroup(); + + updateUnitsUI(); + ui.lastStop->setChecked(prefs.last_stop); + ui.verbatim_plan->setChecked(prefs.verbatim_plan); + ui.display_duration->setChecked(prefs.display_duration); + ui.display_runtime->setChecked(prefs.display_runtime); + ui.display_transitions->setChecked(prefs.display_transitions); + ui.safetystop->setChecked(prefs.safetystop); + ui.reserve_gas->setValue(prefs.reserve_gas / 1000); + ui.bottompo2->setValue(prefs.bottompo2 / 1000.0); + ui.decopo2->setValue(prefs.decopo2 / 1000.0); + ui.backgasBreaks->setChecked(prefs.doo2breaks); + ui.drop_stone_mode->setChecked(prefs.drop_stone_mode); + ui.switch_at_req_stop->setChecked(prefs.switch_at_req_stop); + ui.min_switch_duration->setValue(prefs.min_switch_duration / 60); + ui.recreational_deco->setChecked(prefs.deco_mode == RECREATIONAL); + ui.buehlmann_deco->setChecked(prefs.deco_mode == BUEHLMANN); + ui.vpmb_deco->setChecked(prefs.deco_mode == VPMB); + ui.conservatism_lvl->setValue(prefs.conservatism_level); + disableDecoElements((int) prefs.deco_mode); + + // should be the same order as in dive_comp_type! + rebreather_modes << tr("Open circuit") << tr("CCR") << tr("pSCR"); + ui.rebreathermode->insertItems(0, rebreather_modes); + + modeMapper = new QSignalMapper(this); + connect(modeMapper, SIGNAL(mapped(int)) , plannerModel, SLOT(setDecoMode(int))); + modeMapper->setMapping(ui.recreational_deco, int(RECREATIONAL)); + modeMapper->setMapping(ui.buehlmann_deco, int(BUEHLMANN)); + modeMapper->setMapping(ui.vpmb_deco, int(VPMB)); + + connect(ui.recreational_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); + connect(ui.buehlmann_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); + connect(ui.vpmb_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); + + connect(ui.lastStop, SIGNAL(toggled(bool)), plannerModel, SLOT(setLastStop6m(bool))); + connect(ui.verbatim_plan, SIGNAL(toggled(bool)), plannerModel, SLOT(setVerbatim(bool))); + connect(ui.display_duration, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayDuration(bool))); + connect(ui.display_runtime, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayRuntime(bool))); + connect(ui.display_transitions, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayTransitions(bool))); + connect(ui.safetystop, SIGNAL(toggled(bool)), plannerModel, SLOT(setSafetyStop(bool))); + connect(ui.reserve_gas, SIGNAL(valueChanged(int)), plannerModel, SLOT(setReserveGas(int))); + connect(ui.ascRate75, SIGNAL(valueChanged(int)), this, SLOT(setAscRate75(int))); + connect(ui.ascRate75, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.ascRate50, SIGNAL(valueChanged(int)), this, SLOT(setAscRate50(int))); + connect(ui.ascRate50, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.ascRateStops, SIGNAL(valueChanged(int)), this, SLOT(setAscRateStops(int))); + connect(ui.ascRateStops, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.ascRateLast6m, SIGNAL(valueChanged(int)), this, SLOT(setAscRateLast6m(int))); + connect(ui.ascRateLast6m, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.descRate, SIGNAL(valueChanged(int)), this, SLOT(setDescRate(int))); + connect(ui.descRate, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.bottompo2, SIGNAL(valueChanged(double)), this, SLOT(setBottomPo2(double))); + connect(ui.decopo2, SIGNAL(valueChanged(double)), this, SLOT(setDecoPo2(double))); + connect(ui.drop_stone_mode, SIGNAL(toggled(bool)), plannerModel, SLOT(setDropStoneMode(bool))); + connect(ui.bottomSAC, SIGNAL(valueChanged(double)), this, SLOT(bottomSacChanged(double))); + connect(ui.decoStopSAC, SIGNAL(valueChanged(double)), this, SLOT(decoSacChanged(double))); + connect(ui.gfhigh, SIGNAL(valueChanged(int)), plannerModel, SLOT(setGFHigh(int))); + connect(ui.gflow, SIGNAL(valueChanged(int)), plannerModel, SLOT(setGFLow(int))); + connect(ui.gfhigh, SIGNAL(editingFinished()), plannerModel, SLOT(triggerGFHigh())); + connect(ui.gflow, SIGNAL(editingFinished()), plannerModel, SLOT(triggerGFLow())); + connect(ui.conservatism_lvl, SIGNAL(valueChanged(int)), plannerModel, SLOT(setConservatism(int))); + connect(ui.backgasBreaks, SIGNAL(toggled(bool)), this, SLOT(setBackgasBreaks(bool))); + connect(ui.switch_at_req_stop, SIGNAL(toggled(bool)), plannerModel, SLOT(setSwitchAtReqStop(bool))); + connect(ui.min_switch_duration, SIGNAL(valueChanged(int)), plannerModel, SLOT(setMinSwitchDuration(int))); + connect(ui.rebreathermode, SIGNAL(currentIndexChanged(int)), plannerModel, SLOT(setRebreatherMode(int))); + connect(modeMapper, SIGNAL(mapped(int)), this, SLOT(disableDecoElements(int))); + + settingsChanged(); + ui.gflow->setValue(prefs.gflow); + ui.gfhigh->setValue(prefs.gfhigh); + + setMinimumWidth(0); + setMinimumHeight(0); +} + +void PlannerSettingsWidget::updateUnitsUI() +{ + ui.ascRate75->setValue(rint(prefs.ascrate75 / UNIT_FACTOR)); + ui.ascRate50->setValue(rint(prefs.ascrate50 / UNIT_FACTOR)); + ui.ascRateStops->setValue(rint(prefs.ascratestops / UNIT_FACTOR)); + ui.ascRateLast6m->setValue(rint(prefs.ascratelast6m / UNIT_FACTOR)); + ui.descRate->setValue(rint(prefs.descrate / UNIT_FACTOR)); +} + +PlannerSettingsWidget::~PlannerSettingsWidget() +{ + QSettings s; + s.beginGroup("Planner"); + s.setValue("last_stop", prefs.last_stop); + s.setValue("verbatim_plan", prefs.verbatim_plan); + s.setValue("display_duration", prefs.display_duration); + s.setValue("display_runtime", prefs.display_runtime); + s.setValue("display_transitions", prefs.display_transitions); + s.setValue("safetystop", prefs.safetystop); + s.setValue("reserve_gas", prefs.reserve_gas); + s.setValue("ascrate75", prefs.ascrate75); + s.setValue("ascrate50", prefs.ascrate50); + s.setValue("ascratestops", prefs.ascratestops); + s.setValue("ascratelast6m", prefs.ascratelast6m); + s.setValue("descrate", prefs.descrate); + s.setValue("bottompo2", prefs.bottompo2); + s.setValue("decopo2", prefs.decopo2); + s.setValue("doo2breaks", prefs.doo2breaks); + s.setValue("drop_stone_mode", prefs.drop_stone_mode); + s.setValue("switch_at_req_stop", prefs.switch_at_req_stop); + s.setValue("min_switch_duration", prefs.min_switch_duration); + s.setValue("bottomsac", prefs.bottomsac); + s.setValue("decosac", prefs.decosac); + s.setValue("deco_mode", int(prefs.deco_mode)); + s.setValue("conservatism", prefs.conservatism_level); + s.endGroup(); +} + +void PlannerSettingsWidget::settingsChanged() +{ + QString vs; + // don't recurse into setting the value from the ui when setting the ui from the value + ui.bottomSAC->blockSignals(true); + ui.decoStopSAC->blockSignals(true); + if (get_units()->length == units::FEET) { + vs.append(tr("ft/min")); + ui.lastStop->setText(tr("Last stop at 20ft")); + ui.asc50to6->setText(tr("50% avg. depth to 20ft")); + ui.asc6toSurf->setText(tr("20ft to surface")); + } else { + vs.append(tr("m/min")); + ui.lastStop->setText(tr("Last stop at 6m")); + ui.asc50to6->setText(tr("50% avg. depth to 6m")); + ui.asc6toSurf->setText(tr("6m to surface")); + } + if(get_units()->volume == units::CUFT) { + ui.bottomSAC->setSuffix(tr("cuft/min")); + ui.decoStopSAC->setSuffix(tr("cuft/min")); + ui.bottomSAC->setDecimals(2); + ui.bottomSAC->setSingleStep(0.1); + ui.decoStopSAC->setDecimals(2); + ui.decoStopSAC->setSingleStep(0.1); + ui.bottomSAC->setValue(ml_to_cuft(prefs.bottomsac)); + ui.decoStopSAC->setValue(ml_to_cuft(prefs.decosac)); + } else { + ui.bottomSAC->setSuffix(tr("â„“/min")); + ui.decoStopSAC->setSuffix(tr("â„“/min")); + ui.bottomSAC->setDecimals(0); + ui.bottomSAC->setSingleStep(1); + ui.decoStopSAC->setDecimals(0); + ui.decoStopSAC->setSingleStep(1); + ui.bottomSAC->setValue((double) prefs.bottomsac / 1000.0); + ui.decoStopSAC->setValue((double) prefs.decosac / 1000.0); + } + ui.bottomSAC->blockSignals(false); + ui.decoStopSAC->blockSignals(false); + updateUnitsUI(); + ui.ascRate75->setSuffix(vs); + ui.ascRate50->setSuffix(vs); + ui.ascRateStops->setSuffix(vs); + ui.ascRateLast6m->setSuffix(vs); + ui.descRate->setSuffix(vs); +} + +void PlannerSettingsWidget::atmPressureChanged(const QString &pressure) +{ +} + +void PlannerSettingsWidget::printDecoPlan() +{ +} + +void PlannerSettingsWidget::setAscRate75(int rate) +{ + prefs.ascrate75 = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setAscRate50(int rate) +{ + prefs.ascrate50 = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setAscRateStops(int rate) +{ + prefs.ascratestops = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setAscRateLast6m(int rate) +{ + prefs.ascratelast6m = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setDescRate(int rate) +{ + prefs.descrate = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setBottomPo2(double po2) +{ + prefs.bottompo2 = (int) (po2 * 1000.0); +} + +void PlannerSettingsWidget::setDecoPo2(double po2) +{ + prefs.decopo2 = (int) (po2 * 1000.0); +} + +void PlannerSettingsWidget::setBackgasBreaks(bool dobreaks) +{ + prefs.doo2breaks = dobreaks; + plannerModel->emitDataChanged(); +} + +PlannerDetails::PlannerDetails(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); +} diff --git a/desktop-widgets/diveplanner.h b/desktop-widgets/diveplanner.h new file mode 100644 index 000000000..b2e03a97b --- /dev/null +++ b/desktop-widgets/diveplanner.h @@ -0,0 +1,104 @@ +#ifndef DIVEPLANNER_H +#define DIVEPLANNER_H + +#include +#include +#include +#include +#include + +#include "dive.h" + +class QListView; +class QModelIndex; +class DivePlannerPointsModel; + +class DiveHandler : public QObject, public QGraphicsEllipseItem { + Q_OBJECT +public: + DiveHandler(); + +protected: + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event); + void mouseMoveEvent(QGraphicsSceneMouseEvent *event); + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); +signals: + void moved(); + void clicked(); + void released(); +private: + int parentIndex(); +public +slots: + void selfRemove(); + void changeGas(); +private: + QTime t; +}; + +#include "ui_diveplanner.h" + +class DivePlannerWidget : public QWidget { + Q_OBJECT +public: + explicit DivePlannerWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + void setReplanButton(bool replan); +public +slots: + void setupStartTime(QDateTime startTime); + void settingsChanged(); + void atmPressureChanged(const int pressure); + void heightChanged(const int height); + void salinityChanged(const double salinity); + void printDecoPlan(); + +private: + Ui::DivePlanner ui; + QAbstractButton *replanButton; +}; + +#include "ui_plannerSettings.h" + +class PlannerSettingsWidget : public QWidget { + Q_OBJECT +public: + explicit PlannerSettingsWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + virtual ~PlannerSettingsWidget(); +public +slots: + void settingsChanged(); + void atmPressureChanged(const QString &pressure); + void bottomSacChanged(const double bottomSac); + void decoSacChanged(const double decosac); + void printDecoPlan(); + void setAscRate75(int rate); + void setAscRate50(int rate); + void setAscRateStops(int rate); + void setAscRateLast6m(int rate); + void setDescRate(int rate); + void setBottomPo2(double po2); + void setDecoPo2(double po2); + void setBackgasBreaks(bool dobreaks); + void disableDecoElements(int mode); + +private: + Ui::plannerSettingsWidget ui; + void updateUnitsUI(); + QSignalMapper *modeMapper; +}; + +#include "ui_plannerDetails.h" + +class PlannerDetails : public QWidget { + Q_OBJECT +public: + explicit PlannerDetails(QWidget *parent = 0); + QPushButton *printPlan() const { return ui.printPlan; } + QTextEdit *divePlanOutput() const { return ui.divePlanOutput; } + +private: + Ui::plannerDetails ui; +}; + +#endif // DIVEPLANNER_H diff --git a/desktop-widgets/diveplanner.ui b/desktop-widgets/diveplanner.ui new file mode 100644 index 000000000..adb44fad9 --- /dev/null +++ b/desktop-widgets/diveplanner.ui @@ -0,0 +1,257 @@ + + + DivePlanner + + + true + + + + 0 + 0 + 515 + 591 + + + + + 5 + + + 5 + + + 5 + + + 5 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 505 + 581 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + + + + + 0 + 0 + + + + Planned dive time + + + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + Altitude + + + + + + + ATM pressure + + + + + + + Salinity + + + + + + + + 0 + 0 + + + + mbar + + + 689 + + + 1100 + + + + + + + + 0 + 0 + + + + m + + + -100 + + + 3000 + + + 10 + + + + + + + + 0 + 0 + + + + kg/â„“ + + + 1.000000000000000 + + + 1.050000000000000 + + + 0.010000000000000 + + + 1.030000000000000 + + + + + + + + + + + + TableView + QWidget +
tableview.h
+ 1 +
+
+ + startTime + buttonBox + scrollArea + + + +
diff --git a/desktop-widgets/diveshareexportdialog.cpp b/desktop-widgets/diveshareexportdialog.cpp new file mode 100644 index 000000000..9fe6eefd6 --- /dev/null +++ b/desktop-widgets/diveshareexportdialog.cpp @@ -0,0 +1,141 @@ +#include "diveshareexportdialog.h" +#include "ui_diveshareexportdialog.h" +#include "mainwindow.h" +#include "save-html.h" +#include "subsurfacewebservices.h" +#include "helpers.h" + +#include +#include + +DiveShareExportDialog::DiveShareExportDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::DiveShareExportDialog), + reply(NULL), + exportSelected(false) +{ + ui->setupUi(this); +} + +DiveShareExportDialog::~DiveShareExportDialog() +{ + delete ui; +} + +void DiveShareExportDialog::UIDFromBrowser() +{ + QDesktopServices::openUrl(QUrl(DIVESHARE_BASE_URI "/secret")); +} + +DiveShareExportDialog *DiveShareExportDialog::instance() +{ + static DiveShareExportDialog *self = new DiveShareExportDialog(MainWindow::instance()); + self->setAttribute(Qt::WA_QuitOnClose, false); + self->ui->txtResult->setHtml(""); + self->ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); + return self; +} + +void DiveShareExportDialog::prepareDivesForUpload(bool selected) +{ + exportSelected = selected; + ui->frameConfigure->setVisible(true); + ui->frameResults->setVisible(false); + + QSettings settings; + if (settings.contains("diveshareExport/uid")) + ui->txtUID->setText(settings.value("diveshareExport/uid").toString()); + + if (settings.contains("diveshareExport/private")) + ui->chkPrivate->setChecked(settings.value("diveshareExport/private").toBool()); + + show(); +} + +static QByteArray generate_html_list(const QByteArray &data) +{ + QList dives = data.split('\n'); + QByteArray html; + html.append(""); + for (int i = 0; i < dives.length(); i++ ) { + html.append(""); + QList dive_details = dives[i].split(','); + if (dive_details.length() < 3) + continue; + + QByteArray dive_id = dive_details[0]; + QByteArray dive_delete = dive_details[1]; + + html.append(""); + html.append("" ); + + html.append(""); + } + + html.append("
"); + html.append(""); + + //Title gets separated too, this puts it back together + const char *sep = ""; + for (int t = 2; t < dive_details.length(); t++) { + html.append(sep); + html.append(dive_details[t]); + sep = ","; + } + + html.append(""); + html.append(""); + html.append("Delete dive"); + html.append("
"); + return html; +} + +void DiveShareExportDialog::finishedSlot() +{ + ui->progressBar->setVisible(false); + if (reply->error() != 0) { + ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); + ui->txtResult->setText(reply->errorString()); + } else { + ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok); + ui->txtResult->setHtml(generate_html_list(reply->readAll())); + } + + reply->deleteLater(); +} + +void DiveShareExportDialog::doUpload() +{ + //Store current settings + QSettings settings; + settings.setValue("diveshareExport/uid", ui->txtUID->text()); + settings.setValue("diveshareExport/private", ui->chkPrivate->isChecked()); + + //Change UI into results mode + ui->frameConfigure->setVisible(false); + ui->frameResults->setVisible(true); + ui->progressBar->setVisible(true); + ui->progressBar->setRange(0, 0); + + //generate json + struct membuffer buf = { 0 }; + export_list(&buf, NULL, exportSelected, false); + QByteArray json_data(buf.buffer, buf.len); + free_buffer(&buf); + + //Request to server + QNetworkRequest request; + + if (ui->chkPrivate->isChecked()) + request.setUrl(QUrl(DIVESHARE_BASE_URI "/upload?private=true")); + else + request.setUrl(QUrl(DIVESHARE_BASE_URI "/upload")); + + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + if (ui->txtUID->text().length() != 0) + request.setRawHeader("X-UID", ui->txtUID->text().toUtf8()); + + reply = WebServices::manager()->put(request, json_data); + + QObject::connect(reply, SIGNAL(finished()), this, SLOT(finishedSlot())); +} diff --git a/desktop-widgets/diveshareexportdialog.h b/desktop-widgets/diveshareexportdialog.h new file mode 100644 index 000000000..85dadf5f1 --- /dev/null +++ b/desktop-widgets/diveshareexportdialog.h @@ -0,0 +1,34 @@ +#ifndef DIVESHAREEXPORTDIALOG_H +#define DIVESHAREEXPORTDIALOG_H + +#include +#include +#include + +#define DIVESHARE_WEBSITE "dive-share.appspot.com" +#define DIVESHARE_BASE_URI "http://" DIVESHARE_WEBSITE + +namespace Ui { +class DiveShareExportDialog; +} + +class DiveShareExportDialog : public QDialog +{ + Q_OBJECT +public: + explicit DiveShareExportDialog(QWidget *parent = 0); + ~DiveShareExportDialog(); + static DiveShareExportDialog *instance(); + void prepareDivesForUpload(bool); +private: + Ui::DiveShareExportDialog *ui; + bool exportSelected; + QNetworkReply *reply; +private +slots: + void UIDFromBrowser(); + void doUpload(); + void finishedSlot(); +}; + +#endif // DIVESHAREEXPORTDIALOG_H diff --git a/desktop-widgets/diveshareexportdialog.ui b/desktop-widgets/diveshareexportdialog.ui new file mode 100644 index 000000000..2235740c8 --- /dev/null +++ b/desktop-widgets/diveshareexportdialog.ui @@ -0,0 +1,291 @@ + + + DiveShareExportDialog + + + + 0 + 0 + 593 + 420 + + + + Dialog + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 2 + + + + + User ID + + + + + + + + + + + + + + ⌫ + + + + + + + Get user ID + + + + + + + + + <html><head/><body><p><span style=" font-size:20pt; font-weight:600; color:#ff8000;">âš </span> Not using a UserID means that you will need to manually keep bookmarks to your dives, to find them again.</p></body></html> + + + Qt::AutoText + + + true + + + + + + + Private dives will not appear in "related dives" lists, and will only be accessible if their URL is known. + + + Keep dives private + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Upload dive data + + + false + + + true + + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Oxygen-Sans'; font-size:7pt; font-weight:600; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + true + + + + + + + 24 + + + + + + + + + + Qt::Vertical + + + + 20 + 68 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + + buttonBox + rejected() + DiveShareExportDialog + reject() + + + 97 + 299 + + + 286 + 252 + + + + + getUIDbutton + clicked() + DiveShareExportDialog + UIDFromBrowser() + + + 223 + 29 + + + 159 + 215 + + + + + doUploadButton + clicked() + DiveShareExportDialog + doUpload() + + + 223 + 120 + + + 159 + 215 + + + + + buttonBox + accepted() + DiveShareExportDialog + accept() + + + 159 + 288 + + + 159 + 154 + + + + + + getUID() + doUpload() + + diff --git a/desktop-widgets/downloadfromdivecomputer.cpp b/desktop-widgets/downloadfromdivecomputer.cpp new file mode 100644 index 000000000..4c8fa6b4a --- /dev/null +++ b/desktop-widgets/downloadfromdivecomputer.cpp @@ -0,0 +1,727 @@ +#include "downloadfromdivecomputer.h" +#include "helpers.h" +#include "mainwindow.h" +#include "divelistview.h" +#include "display.h" +#include "uemis.h" +#include "models.h" + +#include +#include +#include +#include + +struct product { + const char *product; + dc_descriptor_t *descriptor; + struct product *next; +}; + +struct vendor { + const char *vendor; + struct product *productlist; + struct vendor *next; +}; + +struct mydescriptor { + const char *vendor; + const char *product; + dc_family_t type; + unsigned int model; +}; + +namespace DownloadFromDcGlobal { + const char *err_string; +}; + +struct dive_table downloadTable; + +DownloadFromDCWidget::DownloadFromDCWidget(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), + thread(0), + downloading(false), + previousLast(0), + vendorModel(0), + productModel(0), + timer(new QTimer(this)), + dumpWarningShown(false), + ostcFirmwareCheck(0), + currentState(INITIAL) +{ + clear_table(&downloadTable); + ui.setupUi(this); + ui.progressBar->hide(); + ui.progressBar->setMinimum(0); + ui.progressBar->setMaximum(100); + diveImportedModel = new DiveImportedModel(this); + ui.downloadedView->setModel(diveImportedModel); + ui.downloadedView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui.downloadedView->setSelectionMode(QAbstractItemView::SingleSelection); + int startingWidth = defaultModelFont().pointSize(); + ui.downloadedView->setColumnWidth(0, startingWidth * 20); + ui.downloadedView->setColumnWidth(1, startingWidth * 10); + ui.downloadedView->setColumnWidth(2, startingWidth * 10); + connect(ui.downloadedView, SIGNAL(clicked(QModelIndex)), diveImportedModel, SLOT(changeSelected(QModelIndex))); + + progress_bar_text = ""; + + fill_computer_list(); + + ui.chooseDumpFile->setEnabled(ui.dumpToFile->isChecked()); + connect(ui.chooseDumpFile, SIGNAL(clicked()), this, SLOT(pickDumpFile())); + connect(ui.dumpToFile, SIGNAL(stateChanged(int)), this, SLOT(checkDumpFile(int))); + ui.chooseLogFile->setEnabled(ui.logToFile->isChecked()); + connect(ui.chooseLogFile, SIGNAL(clicked()), this, SLOT(pickLogFile())); + connect(ui.logToFile, SIGNAL(stateChanged(int)), this, SLOT(checkLogFile(int))); + ui.selectAllButton->setEnabled(false); + ui.unselectAllButton->setEnabled(false); + connect(ui.selectAllButton, SIGNAL(clicked()), diveImportedModel, SLOT(selectAll())); + connect(ui.unselectAllButton, SIGNAL(clicked()), diveImportedModel, SLOT(selectNone())); + vendorModel = new QStringListModel(vendorList); + ui.vendor->setModel(vendorModel); + if (default_dive_computer_vendor) { + ui.vendor->setCurrentIndex(ui.vendor->findText(default_dive_computer_vendor)); + productModel = new QStringListModel(productList[default_dive_computer_vendor]); + ui.product->setModel(productModel); + if (default_dive_computer_product) + ui.product->setCurrentIndex(ui.product->findText(default_dive_computer_product)); + } + if (default_dive_computer_device) + ui.device->setEditText(default_dive_computer_device); + + timer->setInterval(200); + connect(timer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); + updateState(INITIAL); + memset(&data, 0, sizeof(data)); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + ui.ok->setEnabled(false); + ui.downloadCancelRetryButton->setEnabled(true); + ui.downloadCancelRetryButton->setText(tr("Download")); + +#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) + ui.bluetoothMode->setText(tr("Choose Bluetooth download mode")); + ui.bluetoothMode->setChecked(default_dive_computer_download_mode == DC_TRANSPORT_BLUETOOTH); + btDeviceSelectionDialog = 0; + ui.chooseBluetoothDevice->setEnabled(ui.bluetoothMode->isChecked()); + connect(ui.bluetoothMode, SIGNAL(stateChanged(int)), this, SLOT(enableBluetoothMode(int))); + connect(ui.chooseBluetoothDevice, SIGNAL(clicked()), this, SLOT(selectRemoteBluetoothDevice())); +#else + ui.bluetoothMode->hide(); + ui.chooseBluetoothDevice->hide(); +#endif +} + +void DownloadFromDCWidget::updateProgressBar() +{ + if (*progress_bar_text != '\0') { + ui.progressBar->setFormat(progress_bar_text); + } else { + ui.progressBar->setFormat("%p%"); + } + ui.progressBar->setValue(progress_bar_fraction * 100); +} + +void DownloadFromDCWidget::updateState(states state) +{ + if (state == currentState) + return; + + if (state == INITIAL) { + fill_device_list(DC_TYPE_OTHER); + ui.progressBar->hide(); + markChildrenAsEnabled(); + timer->stop(); + } + + // tries to cancel an on going download + else if (currentState == DOWNLOADING && state == CANCELLING) { + import_thread_cancelled = true; + ui.downloadCancelRetryButton->setEnabled(false); + } + + // user pressed cancel but the application isn't doing anything. + // means close the window + else if ((currentState == INITIAL || currentState == DONE || currentState == ERROR) && state == CANCELLING) { + timer->stop(); + reject(); + } + + // the cancelation process is finished + else if (currentState == CANCELLING && state == DONE) { + timer->stop(); + ui.progressBar->setValue(0); + ui.progressBar->hide(); + markChildrenAsEnabled(); + } + + // DOWNLOAD is finally done, but we don't know if there was an error as libdivecomputer doesn't pass + // that information on to us. + // If we find an error, offer to retry, otherwise continue the interaction to pick the dives the user wants + else if (currentState == DOWNLOADING && state == DONE) { + timer->stop(); + if (QString(progress_bar_text).contains("error", Qt::CaseInsensitive)) { + updateProgressBar(); + markChildrenAsEnabled(); + progress_bar_text = ""; + } else { + ui.progressBar->setValue(100); + markChildrenAsEnabled(); + } + } + + // DOWNLOAD is started. + else if (state == DOWNLOADING) { + timer->start(); + ui.progressBar->setValue(0); + updateProgressBar(); + ui.progressBar->show(); + markChildrenAsDisabled(); + } + + // got an error + else if (state == ERROR) { + QMessageBox::critical(this, TITLE_OR_TEXT(tr("Error"), this->thread->error), QMessageBox::Ok); + + markChildrenAsEnabled(); + ui.progressBar->hide(); + } + + // properly updating the widget state + currentState = state; +} + +void DownloadFromDCWidget::on_vendor_currentIndexChanged(const QString &vendor) +{ + int dcType = DC_TYPE_SERIAL; + QAbstractItemModel *currentModel = ui.product->model(); + if (!currentModel) + return; + + productModel = new QStringListModel(productList[vendor]); + ui.product->setModel(productModel); + + if (vendor == QString("Uemis")) + dcType = DC_TYPE_UEMIS; + fill_device_list(dcType); + + // Memleak - but deleting gives me a crash. + //currentModel->deleteLater(); +} + +void DownloadFromDCWidget::on_product_currentIndexChanged(const QString &product) +{ + // Set up the DC descriptor + dc_descriptor_t *descriptor = NULL; + descriptor = descriptorLookup[ui.vendor->currentText() + product]; + + // call dc_descriptor_get_transport to see if the dc_transport_t is DC_TRANSPORT_SERIAL + if (dc_descriptor_get_transport(descriptor) == DC_TRANSPORT_SERIAL) { + // if the dc_transport_t is DC_TRANSPORT_SERIAL, then enable the device node box. + ui.device->setEnabled(true); + } else { + // otherwise disable the device node box + ui.device->setEnabled(false); + } +} + +void DownloadFromDCWidget::fill_computer_list() +{ + dc_iterator_t *iterator = NULL; + dc_descriptor_t *descriptor = NULL; + struct mydescriptor *mydescriptor; + + QStringList computer; + dc_descriptor_iterator(&iterator); + while (dc_iterator_next(iterator, &descriptor) == DC_STATUS_SUCCESS) { + const char *vendor = dc_descriptor_get_vendor(descriptor); + const char *product = dc_descriptor_get_product(descriptor); + + if (!vendorList.contains(vendor)) + vendorList.append(vendor); + + if (!productList[vendor].contains(product)) + productList[vendor].push_back(product); + + descriptorLookup[QString(vendor) + QString(product)] = descriptor; + } + dc_iterator_free(iterator); + Q_FOREACH (QString vendor, vendorList) + qSort(productList[vendor]); + + /* and add the Uemis Zurich which we are handling internally + THIS IS A HACK as we magically have a data structure here that + happens to match a data structure that is internal to libdivecomputer; + this WILL BREAK if libdivecomputer changes the dc_descriptor struct... + eventually the UEMIS code needs to move into libdivecomputer, I guess */ + + mydescriptor = (struct mydescriptor *)malloc(sizeof(struct mydescriptor)); + mydescriptor->vendor = "Uemis"; + mydescriptor->product = "Zurich"; + mydescriptor->type = DC_FAMILY_NULL; + mydescriptor->model = 0; + + if (!vendorList.contains("Uemis")) + vendorList.append("Uemis"); + + if (!productList["Uemis"].contains("Zurich")) + productList["Uemis"].push_back("Zurich"); + + descriptorLookup["UemisZurich"] = (dc_descriptor_t *)mydescriptor; + + qSort(vendorList); +} + +void DownloadFromDCWidget::on_search_clicked() +{ + if (ui.vendor->currentText() == "Uemis") { + QString dirName = QFileDialog::getExistingDirectory(this, + tr("Find Uemis dive computer"), + QDir::homePath(), + QFileDialog::ShowDirsOnly); + if (ui.device->findText(dirName) == -1) + ui.device->addItem(dirName); + ui.device->setEditText(dirName); + } +} + +void DownloadFromDCWidget::on_downloadCancelRetryButton_clicked() +{ + if (currentState == DOWNLOADING) { + updateState(CANCELLING); + return; + } + if (currentState == DONE) { + // this means we are retrying - so we better clean out the partial + // list of downloaded dives from the last attempt + diveImportedModel->clearTable(); + clear_table(&downloadTable); + } + updateState(DOWNLOADING); + + // you cannot cancel the dialog, just the download + ui.cancel->setEnabled(false); + ui.downloadCancelRetryButton->setText("Cancel download"); + + // I don't really think that create/destroy the thread + // is really necessary. + if (thread) { + thread->deleteLater(); + } + + data.vendor = strdup(ui.vendor->currentText().toUtf8().data()); + data.product = strdup(ui.product->currentText().toUtf8().data()); +#if defined(BT_SUPPORT) + data.bluetooth_mode = ui.bluetoothMode->isChecked(); + if (data.bluetooth_mode && btDeviceSelectionDialog != NULL) { + // Get the selected device address + data.devname = strdup(btDeviceSelectionDialog->getSelectedDeviceAddress().toUtf8().data()); + } else + // this breaks an "else if" across lines... not happy... +#endif + if (same_string(data.vendor, "Uemis")) { + char *colon; + char *devname = strdup(ui.device->currentText().toUtf8().data()); + + if ((colon = strstr(devname, ":\\ (UEMISSDA)")) != NULL) { + *(colon + 2) = '\0'; + fprintf(stderr, "shortened devname to \"%s\"", data.devname); + } + data.devname = devname; + } else { + data.devname = strdup(ui.device->currentText().toUtf8().data()); + } + data.descriptor = descriptorLookup[ui.vendor->currentText() + ui.product->currentText()]; + data.force_download = ui.forceDownload->isChecked(); + data.create_new_trip = ui.createNewTrip->isChecked(); + data.trip = NULL; + data.deviceid = data.diveid = 0; + set_default_dive_computer(data.vendor, data.product); + set_default_dive_computer_device(data.devname); +#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) + set_default_dive_computer_download_mode(ui.bluetoothMode->isChecked() ? DC_TRANSPORT_BLUETOOTH : DC_TRANSPORT_SERIAL); +#endif + thread = new DownloadThread(this, &data); + + connect(thread, SIGNAL(finished()), + this, SLOT(onDownloadThreadFinished()), Qt::QueuedConnection); + + MainWindow *w = MainWindow::instance(); + connect(thread, SIGNAL(finished()), w, SLOT(refreshDisplay())); + + // before we start, remember where the dive_table ended + previousLast = dive_table.nr; + + thread->start(); + + QString product(ui.product->currentText()); + if (product == "OSTC 3" || product == "OSTC Sport") + ostcFirmwareCheck = new OstcFirmwareCheck(product); +} + +bool DownloadFromDCWidget::preferDownloaded() +{ + return ui.preferDownloaded->isChecked(); +} + +void DownloadFromDCWidget::checkLogFile(int state) +{ + ui.chooseLogFile->setEnabled(state == Qt::Checked); + data.libdc_log = (state == Qt::Checked); + if (state == Qt::Checked && logFile.isEmpty()) { + pickLogFile(); + } +} + +void DownloadFromDCWidget::pickLogFile() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("subsurface.log"); + logFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer download logfile"), + filename, tr("Log files (*.log)")); + if (!logFile.isEmpty()) { + free(logfile_name); + logfile_name = strdup(logFile.toUtf8().data()); + } +} + +void DownloadFromDCWidget::checkDumpFile(int state) +{ + ui.chooseDumpFile->setEnabled(state == Qt::Checked); + data.libdc_dump = (state == Qt::Checked); + if (state == Qt::Checked) { + if (dumpFile.isEmpty()) + pickDumpFile(); + if (!dumpWarningShown) { + QMessageBox::warning(this, tr("Warning"), + tr("Saving the libdivecomputer dump will NOT download dives to the dive list.")); + dumpWarningShown = true; + } + } +} + +void DownloadFromDCWidget::pickDumpFile() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("subsurface.bin"); + dumpFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer binary dump file"), + filename, tr("Dump files (*.bin)")); + if (!dumpFile.isEmpty()) { + free(dumpfile_name); + dumpfile_name = strdup(dumpFile.toUtf8().data()); + } +} + +void DownloadFromDCWidget::reject() +{ + // we don't want the download window being able to close + // while we're still downloading. + if (currentState != DOWNLOADING && currentState != CANCELLING) + QDialog::reject(); +} + +void DownloadFromDCWidget::onDownloadThreadFinished() +{ + if (currentState == DOWNLOADING) { + if (thread->error.isEmpty()) + updateState(DONE); + else + updateState(ERROR); + } else if (currentState == CANCELLING) { + updateState(DONE); + } + ui.downloadCancelRetryButton->setText(tr("Retry")); + ui.downloadCancelRetryButton->setEnabled(true); + // regardless, if we got dives, we should show them to the user + if (downloadTable.nr) { + diveImportedModel->setImportedDivesIndexes(0, downloadTable.nr - 1); + } + +} + +void DownloadFromDCWidget::on_cancel_clicked() +{ + if (currentState == DOWNLOADING || currentState == CANCELLING) + return; + + // now discard all the dives + clear_table(&downloadTable); + done(-1); +} + +void DownloadFromDCWidget::on_ok_clicked() +{ + struct dive *dive; + + if (currentState != DONE && currentState != ERROR) + return; + + // record all the dives in the 'real' dive_table + for (int i = 0; i < downloadTable.nr; i++) { + if (diveImportedModel->data(diveImportedModel->index(i, 0),Qt::CheckStateRole) == Qt::Checked) + record_dive(downloadTable.dives[i]); + downloadTable.dives[i] = NULL; + } + downloadTable.nr = 0; + + int uniqId, idx; + // remember the last downloaded dive (on most dive computers this will be the chronologically + // first new dive) and select it again after processing all the dives + MainWindow::instance()->dive_list()->unselectDives(); + + dive = get_dive(dive_table.nr - 1); + if (dive != NULL) { + uniqId = get_dive(dive_table.nr - 1)->id; + process_dives(true, preferDownloaded()); + // after process_dives does any merging or resorting needed, we need + // to recreate the model for the dive list so we can select the newest dive + MainWindow::instance()->recreateDiveList(); + idx = get_idx_by_uniq_id(uniqId); + // this shouldn't be necessary - but there are reports that somehow existing dives stay selected + // (but not visible as selected) + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDive(idx, true); + } + + if (ostcFirmwareCheck && currentState == DONE) + ostcFirmwareCheck->checkLatest(this, &data); + accept(); +} + +void DownloadFromDCWidget::markChildrenAsDisabled() +{ + ui.device->setEnabled(false); + ui.vendor->setEnabled(false); + ui.product->setEnabled(false); + ui.forceDownload->setEnabled(false); + ui.createNewTrip->setEnabled(false); + ui.preferDownloaded->setEnabled(false); + ui.ok->setEnabled(false); + ui.search->setEnabled(false); + ui.logToFile->setEnabled(false); + ui.dumpToFile->setEnabled(false); + ui.chooseLogFile->setEnabled(false); + ui.chooseDumpFile->setEnabled(false); + ui.selectAllButton->setEnabled(false); + ui.unselectAllButton->setEnabled(false); + ui.bluetoothMode->setEnabled(false); + ui.chooseBluetoothDevice->setEnabled(false); +} + +void DownloadFromDCWidget::markChildrenAsEnabled() +{ + ui.device->setEnabled(true); + ui.vendor->setEnabled(true); + ui.product->setEnabled(true); + ui.forceDownload->setEnabled(true); + ui.createNewTrip->setEnabled(true); + ui.preferDownloaded->setEnabled(true); + ui.ok->setEnabled(true); + ui.cancel->setEnabled(true); + ui.search->setEnabled(true); + ui.logToFile->setEnabled(true); + ui.dumpToFile->setEnabled(true); + ui.chooseLogFile->setEnabled(true); + ui.chooseDumpFile->setEnabled(true); + ui.selectAllButton->setEnabled(true); + ui.unselectAllButton->setEnabled(true); +#if defined(BT_SUPPORT) + ui.bluetoothMode->setEnabled(true); + ui.chooseBluetoothDevice->setEnabled(true); +#endif +} + +#if defined(BT_SUPPORT) +void DownloadFromDCWidget::selectRemoteBluetoothDevice() +{ + if (!btDeviceSelectionDialog) { + btDeviceSelectionDialog = new BtDeviceSelectionDialog(this); + connect(btDeviceSelectionDialog, SIGNAL(finished(int)), + this, SLOT(bluetoothSelectionDialogIsFinished(int))); + } + + btDeviceSelectionDialog->show(); +} + +void DownloadFromDCWidget::bluetoothSelectionDialogIsFinished(int result) +{ + if (result == QDialog::Accepted) { + /* Make the selected Bluetooth device default */ + QString selectedDeviceName = btDeviceSelectionDialog->getSelectedDeviceName(); + + if (selectedDeviceName == NULL || selectedDeviceName.isEmpty()) { + ui.device->setCurrentText(btDeviceSelectionDialog->getSelectedDeviceAddress()); + } else { + ui.device->setCurrentText(selectedDeviceName); + } + } else if (result == QDialog::Rejected){ + /* Disable Bluetooth download mode */ + ui.bluetoothMode->setChecked(false); + } +} + +void DownloadFromDCWidget::enableBluetoothMode(int state) +{ + ui.chooseBluetoothDevice->setEnabled(state == Qt::Checked); + + if (state == Qt::Checked) + selectRemoteBluetoothDevice(); + else + ui.device->setCurrentIndex(-1); +} +#endif + +static void fillDeviceList(const char *name, void *data) +{ + QComboBox *comboBox = (QComboBox *)data; + comboBox->addItem(name); +} + +void DownloadFromDCWidget::fill_device_list(int dc_type) +{ + int deviceIndex; + ui.device->clear(); + deviceIndex = enumerate_devices(fillDeviceList, ui.device, dc_type); + if (deviceIndex >= 0) + ui.device->setCurrentIndex(deviceIndex); +} + +DownloadThread::DownloadThread(QObject *parent, device_data_t *data) : QThread(parent), + data(data) +{ +} + +static QString str_error(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + const QString str = QString().vsprintf(fmt, args); + va_end(args); + + return str; +} + +void DownloadThread::run() +{ + const char *errorText; + import_thread_cancelled = false; + data->download_table = &downloadTable; + if (!strcmp(data->vendor, "Uemis")) + errorText = do_uemis_import(data); + else + errorText = do_libdivecomputer_import(data); + if (errorText) + error = str_error(errorText, data->devname, data->vendor, data->product); +} + +DiveImportedModel::DiveImportedModel(QObject *o) : QAbstractTableModel(o), + firstIndex(0), + lastIndex(-1), + checkStates(0) +{ +} + +int DiveImportedModel::columnCount(const QModelIndex &model) const +{ + return 3; +} + +int DiveImportedModel::rowCount(const QModelIndex &model) const +{ + return lastIndex - firstIndex + 1; +} + +QVariant DiveImportedModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) + return QVariant(); + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return QVariant(tr("Date/time")); + case 1: + return QVariant(tr("Duration")); + case 2: + return QVariant(tr("Depth")); + } + } + return QVariant(); +} + +QVariant DiveImportedModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() + firstIndex > lastIndex) + return QVariant(); + + struct dive *d = get_dive_from_table(index.row() + firstIndex, &downloadTable); + if (!d) + return QVariant(); + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: + return QVariant(get_short_dive_date_string(d->when)); + case 1: + return QVariant(get_dive_duration_string(d->duration.seconds, tr("h:"), tr("min"))); + case 2: + return QVariant(get_depth_string(d->maxdepth.mm, true, false)); + } + } + if (role == Qt::CheckStateRole) { + if (index.column() == 0) + return checkStates[index.row()] ? Qt::Checked : Qt::Unchecked; + } + return QVariant(); +} + +void DiveImportedModel::changeSelected(QModelIndex clickedIndex) +{ + checkStates[clickedIndex.row()] = !checkStates[clickedIndex.row()]; + dataChanged(index(clickedIndex.row(), 0), index(clickedIndex.row(), 0), QVector() << Qt::CheckStateRole); +} + +void DiveImportedModel::selectAll() +{ + memset(checkStates, true, lastIndex - firstIndex + 1); + dataChanged(index(0, 0), index(lastIndex - firstIndex, 0), QVector() << Qt::CheckStateRole); +} + +void DiveImportedModel::selectNone() +{ + memset(checkStates, false, lastIndex - firstIndex + 1); + dataChanged(index(0, 0), index(lastIndex - firstIndex,0 ), QVector() << Qt::CheckStateRole); +} + +Qt::ItemFlags DiveImportedModel::flags(const QModelIndex &index) const +{ + if (index.column() != 0) + return QAbstractTableModel::flags(index); + return QAbstractTableModel::flags(index) | Qt::ItemIsUserCheckable; +} + +void DiveImportedModel::clearTable() +{ + beginRemoveRows(QModelIndex(), 0, lastIndex - firstIndex); + lastIndex = -1; + firstIndex = 0; + endRemoveRows(); +} + +void DiveImportedModel::setImportedDivesIndexes(int first, int last) +{ + Q_ASSERT(last >= first); + beginRemoveRows(QModelIndex(), 0, lastIndex - firstIndex); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, last - first); + lastIndex = last; + firstIndex = first; + delete[] checkStates; + checkStates = new bool[last - first + 1]; + memset(checkStates, true, last - first + 1); + endInsertRows(); +} diff --git a/desktop-widgets/downloadfromdivecomputer.h b/desktop-widgets/downloadfromdivecomputer.h new file mode 100644 index 000000000..7acd49e95 --- /dev/null +++ b/desktop-widgets/downloadfromdivecomputer.h @@ -0,0 +1,125 @@ +#ifndef DOWNLOADFROMDIVECOMPUTER_H +#define DOWNLOADFROMDIVECOMPUTER_H + +#include +#include +#include +#include +#include + +#include "libdivecomputer.h" +#include "configuredivecomputerdialog.h" +#include "ui_downloadfromdivecomputer.h" + +#if defined(BT_SUPPORT) +#include "btdeviceselectiondialog.h" +#endif + +class QStringListModel; + +class DownloadThread : public QThread { + Q_OBJECT +public: + DownloadThread(QObject *parent, device_data_t *data); + virtual void run(); + + QString error; + +private: + device_data_t *data; +}; + +class DiveImportedModel : public QAbstractTableModel +{ + Q_OBJECT +public: + DiveImportedModel(QObject *o); + int columnCount(const QModelIndex& index = QModelIndex()) const; + int rowCount(const QModelIndex& index = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + void setImportedDivesIndexes(int first, int last); + Qt::ItemFlags flags(const QModelIndex &index) const; + void clearTable(); + +public +slots: + void changeSelected(QModelIndex clickedIndex); + void selectAll(); + void selectNone(); + +private: + int firstIndex; + int lastIndex; + bool *checkStates; +}; + +class DownloadFromDCWidget : public QDialog { + Q_OBJECT +public: + explicit DownloadFromDCWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + void reject(); + + enum states { + INITIAL, + DOWNLOADING, + CANCELLING, + ERROR, + DONE, + }; + +public +slots: + void on_downloadCancelRetryButton_clicked(); + void on_ok_clicked(); + void on_cancel_clicked(); + void on_search_clicked(); + void on_vendor_currentIndexChanged(const QString &vendor); + void on_product_currentIndexChanged(const QString &product); + + void onDownloadThreadFinished(); + void updateProgressBar(); + void checkLogFile(int state); + void checkDumpFile(int state); + void pickDumpFile(); + void pickLogFile(); +#if defined(BT_SUPPORT) + void enableBluetoothMode(int state); + void selectRemoteBluetoothDevice(); + void bluetoothSelectionDialogIsFinished(int result); +#endif +private: + void markChildrenAsDisabled(); + void markChildrenAsEnabled(); + + Ui::DownloadFromDiveComputer ui; + DownloadThread *thread; + bool downloading; + + QStringList vendorList; + QHash productList; + QMap descriptorLookup; + device_data_t data; + int previousLast; + + QStringListModel *vendorModel; + QStringListModel *productModel; + void fill_computer_list(); + void fill_device_list(int dc_type); + QString logFile; + QString dumpFile; + QTimer *timer; + bool dumpWarningShown; + OstcFirmwareCheck *ostcFirmwareCheck; + DiveImportedModel *diveImportedModel; +#if defined(BT_SUPPORT) + BtDeviceSelectionDialog *btDeviceSelectionDialog; +#endif + +public: + bool preferDownloaded(); + void updateState(states state); + states currentState; +}; + +#endif // DOWNLOADFROMDIVECOMPUTER_H diff --git a/desktop-widgets/downloadfromdivecomputer.ui b/desktop-widgets/downloadfromdivecomputer.ui new file mode 100644 index 000000000..b1f152034 --- /dev/null +++ b/desktop-widgets/downloadfromdivecomputer.ui @@ -0,0 +1,301 @@ + + + DownloadFromDiveComputer + + + + 0 + 0 + 747 + 535 + + + + Download from dive computer + + + + :/subsurface-icon + + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + 0 + + + 5 + + + + + Device or mount point + + + + + + + true + + + + + + + ... + + + + + + + Force download of all dives + + + + + + + Always prefer downloaded dives + + + + + + + Download into new trip + + + + + + + Save libdivecomputer logfile + + + + + + + ... + + + + + + + Save libdivecomputer dumpfile + + + + + + + ... + + + + + + + Choose Bluetooth download mode + + + + + + + Select a remote Bluetooth device. + + + ... + + + + + + + Vendor + + + + + + + + + + Dive computer + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Download + + + + + + + + + 0 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + + + + + 5 + + + + + + 1 + 0 + + + + Downloaded dives + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Select all + + + + + + + Unselect all + + + + + + + + + + 1 + 0 + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + diff --git a/desktop-widgets/filterwidget.ui b/desktop-widgets/filterwidget.ui new file mode 100644 index 000000000..1450d81b2 --- /dev/null +++ b/desktop-widgets/filterwidget.ui @@ -0,0 +1,140 @@ + + + FilterWidget2 + + + + 0 + 0 + 594 + 362 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Reset filters + + + + :/filter-reset:/filter-reset + + + true + + + + + + + Show/hide filters + + + + :/filter-hide:/filter-hide + + + true + + + + + + + Close and reset filters + + + + :/filter-close:/filter-close + + + true + + + + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 594 + 337 + + + + + + + + + + + + diff --git a/desktop-widgets/globe.cpp b/desktop-widgets/globe.cpp new file mode 100644 index 000000000..135f195a1 --- /dev/null +++ b/desktop-widgets/globe.cpp @@ -0,0 +1,431 @@ +#include "globe.h" +#ifndef NO_MARBLE +#include "mainwindow.h" +#include "helpers.h" +#include "divelistview.h" +#include "maintab.h" +#include "display.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef MARBLE_SUBSURFACE_BRANCH +#include +#endif + +GlobeGPS *GlobeGPS::instance() +{ + static GlobeGPS *self = new GlobeGPS(); + return self; +} + +GlobeGPS::GlobeGPS(QWidget *parent) : MarbleWidget(parent), + loadedDives(0), + messageWidget(new KMessageWidget(this)), + fixZoomTimer(new QTimer(this)), + needResetZoom(false), + editingDiveLocation(false), + doubleClick(false) +{ +#ifdef MARBLE_SUBSURFACE_BRANCH + // we need to make sure this gets called after the command line arguments have + // been processed but before we initialize the rest of Marble + Marble::MarbleDebug::setEnabled(verbose); +#endif + currentZoomLevel = -1; + // check if Google Sat Maps are installed + // if not, check if they are in a known location + MapThemeManager mtm; + QStringList list = mtm.mapThemeIds(); + QString subsurfaceDataPath; + QDir marble; + if (!list.contains("earth/googlesat/googlesat.dgml")) { + subsurfaceDataPath = getSubsurfaceDataPath("marbledata"); + if (subsurfaceDataPath.size()) { + MarbleDirs::setMarbleDataPath(subsurfaceDataPath); + } else { + subsurfaceDataPath = getSubsurfaceDataPath("data"); + if (subsurfaceDataPath.size()) + MarbleDirs::setMarbleDataPath(subsurfaceDataPath); + } + } + messageWidget->setCloseButtonVisible(false); + messageWidget->setHidden(true); + + setMapThemeId("earth/googlesat/googlesat.dgml"); + //setMapThemeId("earth/openstreetmap/openstreetmap.dgml"); + setProjection(Marble::Spherical); + + setAnimationsEnabled(true); + Q_FOREACH (AbstractFloatItem *i, floatItems()) { + i->setVisible(false); + } + + setShowClouds(false); + setShowBorders(false); + setShowPlaces(true); + setShowCrosshairs(false); + setShowGrid(false); + setShowOverviewMap(false); + setShowScaleBar(true); + setShowCompass(false); + connect(this, SIGNAL(mouseClickGeoPosition(qreal, qreal, GeoDataCoordinates::Unit)), + this, SLOT(mouseClicked(qreal, qreal, GeoDataCoordinates::Unit))); + + setMinimumHeight(0); + setMinimumWidth(0); + connect(fixZoomTimer, SIGNAL(timeout()), this, SLOT(fixZoom())); + fixZoomTimer->setSingleShot(true); + installEventFilter(this); +} + +bool GlobeGPS::eventFilter(QObject *obj, QEvent *ev) +{ + // sometimes Marble seems not to notice double clicks and consequently not call + // the right callback - so let's remember here if the last 'click' is a 'double' or not + enum QEvent::Type type = ev->type(); + if (type == QEvent::MouseButtonDblClick) + doubleClick = true; + else if (type == QEvent::MouseButtonPress) + doubleClick = false; + + // This disables Zooming when a double click occours on the scene. + if (type == QEvent::MouseButtonDblClick && !editingDiveLocation) + return true; + // This disables the Marble's Context Menu + // we need to move this to our 'contextMenuEvent' + // if we plan to do a different one in the future. + if (type == QEvent::ContextMenu) { + contextMenuEvent(static_cast(ev)); + return true; + } + if (type == QEvent::MouseButtonPress) { + QMouseEvent *e = static_cast(ev); + if (e->button() == Qt::RightButton) + return true; + } + return QObject::eventFilter(obj, ev); +} + +void GlobeGPS::contextMenuEvent(QContextMenuEvent *ev) +{ + QMenu m; + QAction *a = m.addAction(tr("Edit selected dive locations"), this, SLOT(prepareForGetDiveCoordinates())); + a->setData(QVariant::fromValue(&m)); + a->setEnabled(current_dive); + m.exec(ev->globalPos()); +} + +void GlobeGPS::mouseClicked(qreal lon, qreal lat, GeoDataCoordinates::Unit unit) +{ + if (doubleClick) { + // strangely sometimes we don't get the changeDiveGeoPosition callback + // and end up here instead + changeDiveGeoPosition(lon, lat, unit); + return; + } + // don't mess with the selection while the user is editing a dive + if (MainWindow::instance()->information()->isEditing() || messageWidget->isVisible()) + return; + + GeoDataCoordinates here(lon, lat, unit); + long lon_udeg = rint(1000000 * here.longitude(GeoDataCoordinates::Degree)); + long lat_udeg = rint(1000000 * here.latitude(GeoDataCoordinates::Degree)); + + // distance() is in km above the map. + // We're going to use that to decide how + // approximate the dives have to be. + // + // Totally arbitrarily I say that 1km + // distance means that we can resolve + // to about 100m. Which in turn is about + // 1000 udeg. + // + // Trigonometry is hard, but sin x == x + // for small x, so let's just do this as + // a linear thing. + long resolve = rint(distance() * 1000); + + int idx; + struct dive *dive; + bool clear = !(QApplication::keyboardModifiers() & Qt::ControlModifier); + QList selectedDiveIds; + for_each_dive (idx, dive) { + long lat_diff, lon_diff; + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!dive_site_has_gps_location(ds)) + continue; + lat_diff = labs(ds->latitude.udeg - lat_udeg); + lon_diff = labs(ds->longitude.udeg - lon_udeg); + if (lat_diff > 180000000) + lat_diff = 360000000 - lat_diff; + if (lon_diff > 180000000) + lon_diff = 180000000 - lon_diff; + if (lat_diff > resolve || lon_diff > resolve) + continue; + + selectedDiveIds.push_back(idx); + } + if (selectedDiveIds.empty()) + return; + if (clear) + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDives(selectedDiveIds); +} + +void GlobeGPS::repopulateLabels() +{ + static GeoDataStyle otherSite, currentSite; + static GeoDataIconStyle darkFlag(QImage(":flagDark")), lightFlag(QImage(":flagLight")); + struct dive_site *ds; + int idx; + QMap locationMap; + if (loadedDives) { + model()->treeModel()->removeDocument(loadedDives); + delete loadedDives; + } + loadedDives = new GeoDataDocument; + otherSite.setIconStyle(darkFlag); + currentSite.setIconStyle(lightFlag); + + if (displayed_dive_site.uuid && dive_site_has_gps_location(&displayed_dive_site)) { + GeoDataPlacemark *place = new GeoDataPlacemark(displayed_dive_site.name); + place->setStyle(¤tSite); + place->setCoordinate(displayed_dive_site.longitude.udeg / 1000000.0, + displayed_dive_site.latitude.udeg / 1000000.0, 0, GeoDataCoordinates::Degree); + locationMap[QString(displayed_dive_site.name)] = place; + loadedDives->append(place); + } + for_each_dive_site(idx, ds) { + if (ds->uuid == displayed_dive_site.uuid) + continue; + if (dive_site_has_gps_location(ds)) { + GeoDataPlacemark *place = new GeoDataPlacemark(ds->name); + place->setStyle(&otherSite); + place->setCoordinate(ds->longitude.udeg / 1000000.0, ds->latitude.udeg / 1000000.0, 0, GeoDataCoordinates::Degree); + + // don't add dive locations twice, unless they are at least 50m apart + if (locationMap[QString(ds->name)]) { + GeoDataCoordinates existingLocation = locationMap[QString(ds->name)]->coordinate(); + GeoDataLineString segment = GeoDataLineString(); + segment.append(existingLocation); + GeoDataCoordinates newLocation = place->coordinate(); + segment.append(newLocation); + double dist = segment.length(6371); + // the dist is scaled to the radius given - so with 6371km as radius + // 50m turns into 0.05 as threashold + if (dist < 0.05) + continue; + } + locationMap[QString(ds->name)] = place; + loadedDives->append(place); + } + } + model()->treeModel()->addDocument(loadedDives); + + struct dive_site *center = displayed_dive_site.uuid != 0 ? + &displayed_dive_site : current_dive ? + get_dive_site_by_uuid(current_dive->dive_site_uuid) : NULL; + if(dive_site_has_gps_location(&displayed_dive_site) && center) + centerOn(displayed_dive_site.longitude.udeg / 1000000.0, displayed_dive_site.latitude.udeg / 1000000.0, true); +} + +void GlobeGPS::reload() +{ + editingDiveLocation = false; + messageWidget->hide(); + repopulateLabels(); +} + +void GlobeGPS::centerOnDiveSite(struct dive_site *ds) +{ + if (!dive_site_has_gps_location(ds)) { + // this is not intuitive and at times causes trouble - let's comment it out for now + // zoomOutForNoGPS(); + return; + } + qreal longitude = ds->longitude.udeg / 1000000.0; + qreal latitude = ds->latitude.udeg / 1000000.0; + + if(IS_FP_SAME(longitude, centerLongitude()) && IS_FP_SAME(latitude,centerLatitude())) { + return; + } + + // if no zoom is set up, set the zoom as seen from 3km above + // if we come back from a dive without GPS data, reset to the last zoom value + // otherwise check to make sure we aren't still running an animation and then remember + // the current zoom level + if (currentZoomLevel == -1) { + currentZoomLevel = zoomFromDistance(3.0); + centerOn(longitude, latitude); + fixZoom(true); + return; + } + if (!fixZoomTimer->isActive()) { + if (needResetZoom) { + needResetZoom = false; + fixZoom(); + } else if (zoom() >= 1200) { + currentZoomLevel = zoom(); + } + } + // From the marble source code, the maximum time of + // 'spin and fit' is 2000 miliseconds so wait a bit them zoom again. + fixZoomTimer->stop(); + if (zoom() < 1200 && IS_FP_SAME(centerLatitude(), latitude) && IS_FP_SAME(centerLongitude(), longitude)) { + // create a tiny movement + centerOn(longitude + 0.00001, latitude + 0.00001); + fixZoomTimer->start(300); + } else { + fixZoomTimer->start(2100); + } + centerOn(longitude, latitude, true); +} + +void GlobeGPS::fixZoom(bool now) +{ + setZoom(currentZoomLevel, now ? Marble::Instant : Marble::Linear); +} + +void GlobeGPS::zoomOutForNoGPS() +{ + // this is called if the dive has no GPS location. + // zoom out quite a bit to show the globe and remember that the next time + // we show a dive with GPS location we need to zoom in again + if (!needResetZoom) { + needResetZoom = true; + if (!fixZoomTimer->isActive() && zoom() >= 1500) { + currentZoomLevel = zoom(); + } + } + if (fixZoomTimer->isActive()) + fixZoomTimer->stop(); + // 1000 is supposed to make sure you see the whole globe + setZoom(1000, Marble::Linear); +} + +void GlobeGPS::endGetDiveCoordinates() +{ + messageWidget->animatedHide(); + editingDiveLocation = false; +} + +void GlobeGPS::prepareForGetDiveCoordinates() +{ + messageWidget->setMessageType(KMessageWidget::Warning); + messageWidget->setText(QObject::tr("Move the map and double-click to set the dive location")); + messageWidget->setWordWrap(true); + messageWidget->setCloseButtonVisible(false); + messageWidget->animatedShow(); + editingDiveLocation = true; + // this is not intuitive and at times causes trouble - let's comment it out for now + // if (!dive_has_gps_location(current_dive)) + // zoomOutForNoGPS(); +} + +void GlobeGPS::changeDiveGeoPosition(qreal lon, qreal lat, GeoDataCoordinates::Unit unit) +{ + if (!editingDiveLocation) + return; + + // convert to degrees if in radian. + if (unit == GeoDataCoordinates::Radian) { + lon = lon * 180 / M_PI; + lat = lat * 180 / M_PI; + } + centerOn(lon, lat, true); + + // change the location of the displayed_dive and put the UI in edit mode + displayed_dive_site.latitude.udeg = lrint(lat * 1000000.0); + displayed_dive_site.longitude.udeg = lrint(lon * 1000000.0); + emit coordinatesChanged(); + repopulateLabels(); +} + +void GlobeGPS::mousePressEvent(QMouseEvent *event) +{ + if (event->type() != QEvent::MouseButtonDblClick) + return; + + qreal lat, lon; + bool clickOnGlobe = geoCoordinates(event->pos().x(), event->pos().y(), lon, lat, GeoDataCoordinates::Degree); + + // there could be two scenarios that got us here; let's check if we are editing a dive + if (MainWindow::instance()->information()->isEditing() && clickOnGlobe) { + // + // FIXME + // TODO + // + // this needs to do this on the dive site screen + // MainWindow::instance()->information()->updateCoordinatesText(lat, lon); + repopulateLabels(); + } else if (clickOnGlobe) { + changeDiveGeoPosition(lon, lat, GeoDataCoordinates::Degree); + } +} + +void GlobeGPS::resizeEvent(QResizeEvent *event) +{ + int size = event->size().width(); + MarbleWidget::resizeEvent(event); + if (size > 600) + messageWidget->setGeometry((size - 600) / 2, 5, 600, 0); + else + messageWidget->setGeometry(5, 5, size - 10, 0); + messageWidget->setMaximumHeight(500); +} + +void GlobeGPS::centerOnIndex(const QModelIndex& idx) +{ + struct dive_site *ds = get_dive_site_by_uuid(idx.model()->index(idx.row(), 0).data().toInt()); + if (!ds || !dive_site_has_gps_location(ds)) + centerOnDiveSite(&displayed_dive_site); + else + centerOnDiveSite(ds); +} +#else + +GlobeGPS *GlobeGPS::instance() +{ + static GlobeGPS *self = new GlobeGPS(); + return self; +} + +GlobeGPS::GlobeGPS(QWidget *parent) +{ + setText("MARBLE DISABLED AT BUILD TIME"); +} +void GlobeGPS::repopulateLabels() +{ +} +void GlobeGPS::centerOnCurrentDive() +{ +} +bool GlobeGPS::eventFilter(QObject *obj, QEvent *ev) +{ + return QObject::eventFilter(obj, ev); +} +void GlobeGPS::prepareForGetDiveCoordinates() +{ +} +void GlobeGPS::endGetDiveCoordinates() +{ +} +void GlobeGPS::reload() +{ +} +void GlobeGPS::centerOnIndex(const QModelIndex& idx) +{ +} +#endif diff --git a/desktop-widgets/globe.h b/desktop-widgets/globe.h new file mode 100644 index 000000000..8cc1265e4 --- /dev/null +++ b/desktop-widgets/globe.h @@ -0,0 +1,84 @@ +#ifndef GLOBE_H +#define GLOBE_H + +#include + +#ifndef NO_MARBLE +#include +#include + +#include + +namespace Marble{ + class GeoDataDocument; +} + +class KMessageWidget; +using namespace Marble; +struct dive; + +class GlobeGPS : public MarbleWidget { + Q_OBJECT +public: + using MarbleWidget::centerOn; + static GlobeGPS *instance(); + void reload(); + bool eventFilter(QObject *, QEvent *); + +protected: + /* reimp */ void resizeEvent(QResizeEvent *event); + /* reimp */ void mousePressEvent(QMouseEvent *event); + /* reimp */ void contextMenuEvent(QContextMenuEvent *); + +private: + GeoDataDocument *loadedDives; + KMessageWidget *messageWidget; + QTimer *fixZoomTimer; + int currentZoomLevel; + bool needResetZoom; + bool editingDiveLocation; + bool doubleClick; + GlobeGPS(QWidget *parent = 0); + +signals: + void coordinatesChanged(); + +public +slots: + void repopulateLabels(); + void changeDiveGeoPosition(qreal lon, qreal lat, GeoDataCoordinates::Unit); + void mouseClicked(qreal lon, qreal lat, GeoDataCoordinates::Unit); + void fixZoom(bool now = false); + void zoomOutForNoGPS(); + void prepareForGetDiveCoordinates(); + void endGetDiveCoordinates(); + void centerOnDiveSite(struct dive_site *ds); + void centerOnIndex(const QModelIndex& idx); +}; + +#else // NO_MARBLE +/* Dummy widget for when we don't have MarbleWidget */ +#include + +class GlobeGPS : public QLabel { + Q_OBJECT +public: + GlobeGPS(QWidget *parent = 0); + static GlobeGPS *instance(); + void reload(); + void repopulateLabels(); + void centerOnDiveSite(uint32_t uuid); + void centerOnIndex(const QModelIndex& idx); + void centerOnCurrentDive(); + bool eventFilter(QObject *, QEvent *); +public +slots: + void prepareForGetDiveCoordinates(); + void endGetDiveCoordinates(); +}; + +#endif // NO_MARBLE + +extern "C" double getDistance(int lat1, int lon1, int lat2, int lon2); + +#endif // GLOBE_H diff --git a/desktop-widgets/groupedlineedit.cpp b/desktop-widgets/groupedlineedit.cpp new file mode 100644 index 000000000..9ce5e175c --- /dev/null +++ b/desktop-widgets/groupedlineedit.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2013 Maximilian Güntner + * + * This file is subject to the terms and conditions of version 2 of + * the GNU General Public License. See the file gpl-2.0.txt in the main + * directory of this archive for more details. + * + * Original License: + * + * This file is part of the Nepomuk widgets collection + * Copyright (c) 2013 Denis Steckelmacher + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2.1 as published by the Free Software Foundation, + * or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#include "groupedlineedit.h" + +#include +#include +#include +#include +#include +#include +#include + +struct GroupedLineEdit::Private { + struct Block { + int start; + int end; + QString text; + }; + QVector blocks; + QVector colors; +}; + +GroupedLineEdit::GroupedLineEdit(QWidget *parent) : QPlainTextEdit(parent), + d(new Private) +{ + setWordWrapMode(QTextOption::NoWrap); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + document()->setMaximumBlockCount(1); +} + +GroupedLineEdit::~GroupedLineEdit() +{ + delete d; +} + +QString GroupedLineEdit::text() const +{ + // Remove the block crosses from the text + return toPlainText(); +} + +int GroupedLineEdit::cursorPosition() const +{ + return textCursor().positionInBlock(); +} + +void GroupedLineEdit::addBlock(int start, int end) +{ + Private::Block block; + block.start = start; + block.end = end; + block.text = text().mid(start, end - start + 1).remove(',').trimmed(); + if (block.text.isEmpty()) + return; + d->blocks.append(block); + viewport()->update(); +} + +void GroupedLineEdit::addColor(QColor color) +{ + d->colors.append(color); +} + +void GroupedLineEdit::removeAllColors() +{ + d->colors.clear(); +} + +QStringList GroupedLineEdit::getBlockStringList() +{ + QStringList retList; + foreach (const Private::Block &block, d->blocks) + retList.append(block.text); + return retList; +} + +void GroupedLineEdit::setCursorPosition(int position) +{ + QTextCursor c = textCursor(); + c.setPosition(position, QTextCursor::MoveAnchor); + setTextCursor(c); +} + +void GroupedLineEdit::setText(const QString &text) +{ + setPlainText(text); +} + +void GroupedLineEdit::clear() +{ + QPlainTextEdit::clear(); + removeAllBlocks(); +} + +void GroupedLineEdit::selectAll() +{ + QTextCursor c = textCursor(); + c.select(QTextCursor::LineUnderCursor); + setTextCursor(c); +} + +void GroupedLineEdit::removeAllBlocks() +{ + d->blocks.clear(); + viewport()->update(); +} + +QSize GroupedLineEdit::sizeHint() const +{ + QSize rs( + 40, + document()->findBlock(0).layout()->lineAt(0).height() + + document()->documentMargin() * 2 + + frameWidth() * 2); + return rs; +} + +QSize GroupedLineEdit::minimumSizeHint() const +{ + return sizeHint(); +} + +void GroupedLineEdit::keyPressEvent(QKeyEvent *e) +{ + switch (e->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + emit editingFinished(); + return; + } + QPlainTextEdit::keyPressEvent(e); +} + +void GroupedLineEdit::paintEvent(QPaintEvent *e) +{ + QTextLine line = document()->findBlock(0).layout()->lineAt(0); + QPainter painter(viewport()); + + painter.setRenderHint(QPainter::Antialiasing, true); + painter.fillRect(0, 0, viewport()->width(), viewport()->height(), palette().base()); + + QVectorIterator i(d->colors); + i.toFront(); + foreach (const Private::Block &block, d->blocks) { + qreal start_x = line.cursorToX(block.start, QTextLine::Leading); + qreal end_x = line.cursorToX(block.end-1, QTextLine::Trailing); + + QPainterPath path; + QRectF rectangle( + start_x - 1.0 - double(horizontalScrollBar()->value()), + 1.0, + end_x - start_x + 2.0, + double(viewport()->height() - 2)); + if (!i.hasNext()) + i.toFront(); + path.addRoundedRect(rectangle, 5.0, 5.0); + painter.setPen(i.peekNext()); + if (palette().color(QPalette::Text).lightnessF() <= 0.3) + painter.setBrush(i.next().lighter()); + else if (palette().color(QPalette::Text).lightnessF() <= 0.6) + painter.setBrush(i.next()); + else + painter.setBrush(i.next().darker()); + painter.drawPath(path); + } + QPlainTextEdit::paintEvent(e); +} diff --git a/desktop-widgets/groupedlineedit.h b/desktop-widgets/groupedlineedit.h new file mode 100644 index 000000000..c9cd1a0e0 --- /dev/null +++ b/desktop-widgets/groupedlineedit.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2013 Maximilian Güntner + * + * This file is subject to the terms and conditions of version 2 of + * the GNU General Public License. See the file gpl-2.0.txt in the main + * directory of this archive for more details. + * + * Original License: + * + * This file is part of the Nepomuk widgets collection + * Copyright (c) 2013 Denis Steckelmacher + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2.1 as published by the Free Software Foundation, + * or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GROUPEDLINEEDIT_H +#define GROUPEDLINEEDIT_H + +#include +#include + +class GroupedLineEdit : public QPlainTextEdit { + Q_OBJECT + +public: + explicit GroupedLineEdit(QWidget *parent = 0); + virtual ~GroupedLineEdit(); + + QString text() const; + + int cursorPosition() const; + void setCursorPosition(int position); + void setText(const QString &text); + void clear(); + void selectAll(); + + void removeAllBlocks(); + void addBlock(int start, int end); + QStringList getBlockStringList(); + + void addColor(QColor color); + void removeAllColors(); + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + +signals: + void editingFinished(); + +protected: + virtual void paintEvent(QPaintEvent *e); + virtual void keyPressEvent(QKeyEvent *e); + +private: + struct Private; + Private *d; +}; +#endif // GROUPEDLINEEDIT_H diff --git a/desktop-widgets/kmessagewidget.cpp b/desktop-widgets/kmessagewidget.cpp new file mode 100644 index 000000000..2e506af2d --- /dev/null +++ b/desktop-widgets/kmessagewidget.cpp @@ -0,0 +1,480 @@ +/* This file is part of the KDE libraries + * + * Copyright (c) 2011 Aurélien Gâteau + * Copyright (c) 2014 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ +#include "kmessagewidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//--------------------------------------------------------------------- +// KMessageWidgetPrivate +//--------------------------------------------------------------------- +class KMessageWidgetPrivate +{ +public: + void init(KMessageWidget *); + + KMessageWidget *q; + QFrame *content; + QLabel *iconLabel; + QLabel *textLabel; + QToolButton *closeButton; + QTimeLine *timeLine; + QIcon icon; + + KMessageWidget::MessageType messageType; + bool wordWrap; + QList buttons; + QPixmap contentSnapShot; + + void createLayout(); + void updateSnapShot(); + void updateLayout(); + void slotTimeLineChanged(qreal); + void slotTimeLineFinished(); + + int bestContentHeight() const; +}; + +void KMessageWidgetPrivate::init(KMessageWidget *q_ptr) +{ + q = q_ptr; + + q->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + + timeLine = new QTimeLine(500, q); + QObject::connect(timeLine, SIGNAL(valueChanged(qreal)), q, SLOT(slotTimeLineChanged(qreal))); + QObject::connect(timeLine, SIGNAL(finished()), q, SLOT(slotTimeLineFinished())); + + content = new QFrame(q); + content->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + wordWrap = false; + + iconLabel = new QLabel(content); + iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + iconLabel->hide(); + + textLabel = new QLabel(content); + textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + QObject::connect(textLabel, SIGNAL(linkActivated(QString)), q, SIGNAL(linkActivated(QString))); + QObject::connect(textLabel, SIGNAL(linkHovered(QString)), q, SIGNAL(linkHovered(QString))); + + QAction *closeAction = new QAction(q); + closeAction->setText(KMessageWidget::tr("&Close")); + closeAction->setToolTip(KMessageWidget::tr("Close message")); + closeAction->setIcon(q->style()->standardIcon(QStyle::SP_DialogCloseButton)); + + QObject::connect(closeAction, SIGNAL(triggered(bool)), q, SLOT(animatedHide())); + + closeButton = new QToolButton(content); + closeButton->setAutoRaise(true); + closeButton->setDefaultAction(closeAction); + + q->setMessageType(KMessageWidget::Information); +} + +void KMessageWidgetPrivate::createLayout() +{ + delete content->layout(); + + content->resize(q->size()); + + qDeleteAll(buttons); + buttons.clear(); + + Q_FOREACH (QAction *action, q->actions()) { + QToolButton *button = new QToolButton(content); + button->setDefaultAction(action); + button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + buttons.append(button); + } + + // AutoRaise reduces visual clutter, but we don't want to turn it on if + // there are other buttons, otherwise the close button will look different + // from the others. + closeButton->setAutoRaise(buttons.isEmpty()); + + if (wordWrap) { + QGridLayout *layout = new QGridLayout(content); + // Set alignment to make sure icon does not move down if text wraps + layout->addWidget(iconLabel, 0, 0, 1, 1, Qt::AlignHCenter | Qt::AlignTop); + layout->addWidget(textLabel, 0, 1); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + Q_FOREACH (QToolButton *button, buttons) { + // For some reason, calling show() is necessary if wordwrap is true, + // otherwise the buttons do not show up. It is not needed if + // wordwrap is false. + button->show(); + buttonLayout->addWidget(button); + } + buttonLayout->addWidget(closeButton); + layout->addItem(buttonLayout, 1, 0, 1, 2); + } else { + QHBoxLayout *layout = new QHBoxLayout(content); + layout->addWidget(iconLabel); + layout->addWidget(textLabel); + + Q_FOREACH (QToolButton *button, buttons) { + layout->addWidget(button); + } + + layout->addWidget(closeButton); + }; + + if (q->isVisible()) { + q->setFixedHeight(content->sizeHint().height()); + } + q->updateGeometry(); +} + +void KMessageWidgetPrivate::updateLayout() +{ + if (content->layout()) { + createLayout(); + } +} + +void KMessageWidgetPrivate::updateSnapShot() +{ + // Attention: updateSnapShot calls QWidget::render(), which causes the whole + // window layouts to be activated. Calling this method from resizeEvent() + // can lead to infinite recursion, see: + // https://bugs.kde.org/show_bug.cgi?id=311336 + contentSnapShot = QPixmap(content->size() * q->devicePixelRatio()); + contentSnapShot.setDevicePixelRatio(q->devicePixelRatio()); + contentSnapShot.fill(Qt::transparent); + content->render(&contentSnapShot, QPoint(), QRegion(), QWidget::DrawChildren); +} + +void KMessageWidgetPrivate::slotTimeLineChanged(qreal value) +{ + q->setFixedHeight(qMin(value * 2, qreal(1.0)) * content->height()); + q->update(); +} + +void KMessageWidgetPrivate::slotTimeLineFinished() +{ + if (timeLine->direction() == QTimeLine::Forward) { + // Show + // We set the whole geometry here, because it may be wrong if a + // KMessageWidget is shown right when the toplevel window is created. + content->setGeometry(0, 0, q->width(), bestContentHeight()); + + // notify about finished animation + emit q->showAnimationFinished(); + } else { + // hide and notify about finished animation + q->hide(); + emit q->hideAnimationFinished(); + } +} + +int KMessageWidgetPrivate::bestContentHeight() const +{ + int height = content->heightForWidth(q->width()); + if (height == -1) { + height = content->sizeHint().height(); + } + return height; +} + +//--------------------------------------------------------------------- +// KMessageWidget +//--------------------------------------------------------------------- +KMessageWidget::KMessageWidget(QWidget *parent) + : QFrame(parent) + , d(new KMessageWidgetPrivate) +{ + d->init(this); +} + +KMessageWidget::KMessageWidget(const QString &text, QWidget *parent) + : QFrame(parent) + , d(new KMessageWidgetPrivate) +{ + d->init(this); + setText(text); +} + +KMessageWidget::~KMessageWidget() +{ + delete d; +} + +QString KMessageWidget::text() const +{ + return d->textLabel->text(); +} + +void KMessageWidget::setText(const QString &text) +{ + d->textLabel->setText(text); + updateGeometry(); +} + +KMessageWidget::MessageType KMessageWidget::messageType() const +{ + return d->messageType; +} + +static QColor darkShade(QColor c) +{ + qreal contrast = 0.7; // taken from kcolorscheme for the dark shade + + qreal darkAmount; + if (c.lightnessF() < 0.006) { /* too dark */ + darkAmount = 0.02 + 0.40 * contrast; + } else if (c.lightnessF() > 0.93) { /* too bright */ + darkAmount = -0.06 - 0.60 * contrast; + } else { + darkAmount = (-c.lightnessF()) * (0.55 + contrast * 0.35); + } + + qreal v = c.lightnessF() + darkAmount; + v = v > 0.0 ? (v < 1.0 ? v : 1.0) : 0.0; + c.setHsvF(c.hslHueF(), c.hslSaturationF(), v); + return c; +} + +void KMessageWidget::setMessageType(KMessageWidget::MessageType type) +{ + d->messageType = type; + QColor bg0, bg1, bg2, border, fg; + switch (type) { + case Positive: + bg1.setRgb(0, 110, 40); // values taken from kcolorscheme.cpp (Positive) + break; + case Information: + bg1 = palette().highlight().color(); + break; + case Warning: + bg1.setRgb(176, 128, 0); // values taken from kcolorscheme.cpp (Neutral) + break; + case Error: + bg1.setRgb(191, 3, 3); // values taken from kcolorscheme.cpp (Negative) + break; + } + + // Colors + fg = palette().highlightedText().color(); + bg0 = bg1.lighter(110); + bg2 = bg1.darker(110); + border = darkShade(bg1); + + d->content->setStyleSheet( + QString(QLatin1String(".QFrame {" + "background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," + " stop: 0 %1," + " stop: 0.1 %2," + " stop: 1.0 %3);" + "border-radius: 5px;" + "border: 1px solid %4;" + "margin: %5px;" + "}" + ".QLabel { color: %6; }" + )) + .arg(bg0.name()) + .arg(bg1.name()) + .arg(bg2.name()) + .arg(border.name()) + // DefaultFrameWidth returns the size of the external margin + border width. We know our border is 1px, so we subtract this from the frame normal QStyle FrameWidth to get our margin + .arg(style()->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, this) - 1) + .arg(fg.name()) + ); +} + +QSize KMessageWidget::sizeHint() const +{ + ensurePolished(); + return d->content->sizeHint(); +} + +QSize KMessageWidget::minimumSizeHint() const +{ + ensurePolished(); + return d->content->minimumSizeHint(); +} + +bool KMessageWidget::event(QEvent *event) +{ + if (event->type() == QEvent::Polish && !d->content->layout()) { + d->createLayout(); + } + return QFrame::event(event); +} + +void KMessageWidget::resizeEvent(QResizeEvent *event) +{ + QFrame::resizeEvent(event); + + if (d->timeLine->state() == QTimeLine::NotRunning) { + d->content->resize(width(), d->bestContentHeight()); + } +} + +int KMessageWidget::heightForWidth(int width) const +{ + ensurePolished(); + return d->content->heightForWidth(width); +} + +void KMessageWidget::paintEvent(QPaintEvent *event) +{ + QFrame::paintEvent(event); + if (d->timeLine->state() == QTimeLine::Running) { + QPainter painter(this); + painter.setOpacity(d->timeLine->currentValue() * d->timeLine->currentValue()); + painter.drawPixmap(0, 0, d->contentSnapShot); + } +} + +bool KMessageWidget::wordWrap() const +{ + return d->wordWrap; +} + +void KMessageWidget::setWordWrap(bool wordWrap) +{ + d->wordWrap = wordWrap; + d->textLabel->setWordWrap(wordWrap); + QSizePolicy policy = sizePolicy(); + policy.setHeightForWidth(wordWrap); + setSizePolicy(policy); + d->updateLayout(); + // Without this, when user does wordWrap -> !wordWrap -> wordWrap, a minimum + // height is set, causing the widget to be too high. + // Mostly visible in test programs. + if (wordWrap) { + setMinimumHeight(0); + } +} + +bool KMessageWidget::isCloseButtonVisible() const +{ + return d->closeButton->isVisible(); +} + +void KMessageWidget::setCloseButtonVisible(bool show) +{ + d->closeButton->setVisible(show); + updateGeometry(); +} + +void KMessageWidget::addAction(QAction *action) +{ + QFrame::addAction(action); + d->updateLayout(); +} + +void KMessageWidget::removeAction(QAction *action) +{ + QFrame::removeAction(action); + d->updateLayout(); +} + +void KMessageWidget::animatedShow() +{ + if (!style()->styleHint(QStyle::SH_Widget_Animate, 0, this)) { + show(); + emit showAnimationFinished(); + return; + } + + if (isVisible()) { + return; + } + + QFrame::show(); + setFixedHeight(0); + int wantedHeight = d->bestContentHeight(); + d->content->setGeometry(0, -wantedHeight, width(), wantedHeight); + + d->updateSnapShot(); + + d->timeLine->setDirection(QTimeLine::Forward); + if (d->timeLine->state() == QTimeLine::NotRunning) { + d->timeLine->start(); + } +} + +void KMessageWidget::animatedHide() +{ + if (!style()->styleHint(QStyle::SH_Widget_Animate, 0, this)) { + hide(); + emit hideAnimationFinished(); + return; + } + + if (!isVisible()) { + hide(); + return; + } + + d->content->move(0, -d->content->height()); + d->updateSnapShot(); + + d->timeLine->setDirection(QTimeLine::Backward); + if (d->timeLine->state() == QTimeLine::NotRunning) { + d->timeLine->start(); + } +} + +bool KMessageWidget::isHideAnimationRunning() const +{ + return (d->timeLine->direction() == QTimeLine::Backward) + && (d->timeLine->state() == QTimeLine::Running); +} + +bool KMessageWidget::isShowAnimationRunning() const +{ + return (d->timeLine->direction() == QTimeLine::Forward) + && (d->timeLine->state() == QTimeLine::Running); +} + +QIcon KMessageWidget::icon() const +{ + return d->icon; +} + +void KMessageWidget::setIcon(const QIcon &icon) +{ + d->icon = icon; + if (d->icon.isNull()) { + d->iconLabel->hide(); + } else { + const int size = style()->pixelMetric(QStyle::PM_ToolBarIconSize); + d->iconLabel->setPixmap(d->icon.pixmap(size)); + d->iconLabel->show(); + } +} + +#include "moc_kmessagewidget.cpp" diff --git a/desktop-widgets/kmessagewidget.h b/desktop-widgets/kmessagewidget.h new file mode 100644 index 000000000..885d2a78f --- /dev/null +++ b/desktop-widgets/kmessagewidget.h @@ -0,0 +1,342 @@ +/* This file is part of the KDE libraries + * + * Copyright (c) 2011 Aurélien Gâteau + * Copyright (c) 2014 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ +#ifndef KMESSAGEWIDGET_H +#define KMESSAGEWIDGET_H + +#include + +class KMessageWidgetPrivate; + +/** + * @short A widget to provide feedback or propose opportunistic interactions. + * + * KMessageWidget can be used to provide inline positive or negative + * feedback, or to implement opportunistic interactions. + * + * As a feedback widget, KMessageWidget provides a less intrusive alternative + * to "OK Only" message boxes. If you want to avoid a modal KMessageBox, + * consider using KMessageWidget instead. + * + * Examples of KMessageWidget look as follows, all of them having an icon set + * with setIcon(), and the first three show a close button: + * + * \image html kmessagewidget.png "KMessageWidget with different message types" + * + * Negative feedback + * + * The KMessageWidget can be used as a secondary indicator of failure: the + * first indicator is usually the fact the action the user expected to happen + * did not happen. + * + * Example: User fills a form, clicks "Submit". + * + * @li Expected feedback: form closes + * @li First indicator of failure: form stays there + * @li Second indicator of failure: a KMessageWidget appears on top of the + * form, explaining the error condition + * + * When used to provide negative feedback, KMessageWidget should be placed + * close to its context. In the case of a form, it should appear on top of the + * form entries. + * + * KMessageWidget should get inserted in the existing layout. Space should not + * be reserved for it, otherwise it becomes "dead space", ignored by the user. + * KMessageWidget should also not appear as an overlay to prevent blocking + * access to elements the user needs to interact with to fix the failure. + * + * Positive feedback + * + * KMessageWidget can be used for positive feedback but it shouldn't be + * overused. It is often enough to provide feedback by simply showing the + * results of an action. + * + * Examples of acceptable uses: + * + * @li Confirm success of "critical" transactions + * @li Indicate completion of background tasks + * + * Example of unadapted uses: + * + * @li Indicate successful saving of a file + * @li Indicate a file has been successfully removed + * + * Opportunistic interaction + * + * Opportunistic interaction is the situation where the application suggests to + * the user an action he could be interested in perform, either based on an + * action the user just triggered or an event which the application noticed. + * + * Example of acceptable uses: + * + * @li A browser can propose remembering a recently entered password + * @li A music collection can propose ripping a CD which just got inserted + * @li A chat application may notify the user a "special friend" just connected + * + * @author Aurélien Gâteau + * @since 4.7 + */ +class KMessageWidget : public QFrame +{ + Q_OBJECT + Q_ENUMS(MessageType) + + Q_PROPERTY(QString text READ text WRITE setText) + Q_PROPERTY(bool wordWrap READ wordWrap WRITE setWordWrap) + Q_PROPERTY(bool closeButtonVisible READ isCloseButtonVisible WRITE setCloseButtonVisible) + Q_PROPERTY(MessageType messageType READ messageType WRITE setMessageType) + Q_PROPERTY(QIcon icon READ icon WRITE setIcon) +public: + + /** + * Available message types. + * The background colors are chosen depending on the message type. + */ + enum MessageType { + Positive, + Information, + Warning, + Error + }; + + /** + * Constructs a KMessageWidget with the specified @p parent. + */ + explicit KMessageWidget(QWidget *parent = 0); + + /** + * Constructs a KMessageWidget with the specified @p parent and + * contents @p text. + */ + explicit KMessageWidget(const QString &text, QWidget *parent = 0); + + /** + * Destructor. + */ + ~KMessageWidget(); + + /** + * Get the text of this message widget. + * @see setText() + */ + QString text() const; + + /** + * Check whether word wrap is enabled. + * + * If word wrap is enabled, the message widget wraps the displayed text + * as required to the available width of the widget. This is useful to + * avoid breaking widget layouts. + * + * @see setWordWrap() + */ + bool wordWrap() const; + + /** + * Check whether the close button is visible. + * + * @see setCloseButtonVisible() + */ + bool isCloseButtonVisible() const; + + /** + * Get the type of this message. + * By default, the type is set to KMessageWidget::Information. + * + * @see KMessageWidget::MessageType, setMessageType() + */ + MessageType messageType() const; + + /** + * Add @p action to the message widget. + * For each action a button is added to the message widget in the + * order the actions were added. + * + * @param action the action to add + * @see removeAction(), QWidget::actions() + */ + void addAction(QAction *action); + + /** + * Remove @p action from the message widget. + * + * @param action the action to remove + * @see KMessageWidget::MessageType, addAction(), setMessageType() + */ + void removeAction(QAction *action); + + /** + * Returns the preferred size of the message widget. + */ + QSize sizeHint() const Q_DECL_OVERRIDE; + + /** + * Returns the minimum size of the message widget. + */ + QSize minimumSizeHint() const Q_DECL_OVERRIDE; + + /** + * Returns the required height for @p width. + * @param width the width in pixels + */ + int heightForWidth(int width) const Q_DECL_OVERRIDE; + + /** + * The icon shown on the left of the text. By default, no icon is shown. + * @since 4.11 + */ + QIcon icon() const; + + /** + * Check whether the hide animation started by calling animatedHide() + * is still running. If animations are disabled, this function always + * returns @e false. + * + * @see animatedHide(), hideAnimationFinished() + * @since 5.0 + */ + bool isHideAnimationRunning() const; + + /** + * Check whether the show animation started by calling animatedShow() + * is still running. If animations are disabled, this function always + * returns @e false. + * + * @see animatedShow(), showAnimationFinished() + * @since 5.0 + */ + bool isShowAnimationRunning() const; + +public Q_SLOTS: + /** + * Set the text of the message widget to @p text. + * If the message widget is already visible, the text changes on the fly. + * + * @param text the text to display, rich text is allowed + * @see text() + */ + void setText(const QString &text); + + /** + * Set word wrap to @p wordWrap. If word wrap is enabled, the text() + * of the message widget is wrapped to fit the available width. + * If word wrap is disabled, the message widget's minimum size is + * such that the entire text fits. + * + * @param wordWrap disable/enable word wrap + * @see wordWrap() + */ + void setWordWrap(bool wordWrap); + + /** + * Set the visibility of the close button. If @p visible is @e true, + * a close button is shown that calls animatedHide() if clicked. + * + * @see closeButtonVisible(), animatedHide() + */ + void setCloseButtonVisible(bool visible); + + /** + * Set the message type to @p type. + * By default, the message type is set to KMessageWidget::Information. + * + * @see messageType(), KMessageWidget::MessageType + */ + void setMessageType(KMessageWidget::MessageType type); + + /** + * Show the widget using an animation. + */ + void animatedShow(); + + /** + * Hide the widget using an animation. + */ + void animatedHide(); + + /** + * Define an icon to be shown on the left of the text + * @since 4.11 + */ + void setIcon(const QIcon &icon); + +Q_SIGNALS: + /** + * This signal is emitted when the user clicks a link in the text label. + * The URL referred to by the href anchor is passed in contents. + * @param contents text of the href anchor + * @see QLabel::linkActivated() + * @since 4.10 + */ + void linkActivated(const QString &contents); + + /** + * This signal is emitted when the user hovers over a link in the text label. + * The URL referred to by the href anchor is passed in contents. + * @param contents text of the href anchor + * @see QLabel::linkHovered() + * @since 4.11 + */ + void linkHovered(const QString &contents); + + /** + * This signal is emitted when the hide animation is finished, started by + * calling animatedHide(). If animations are disabled, this signal is + * emitted immediately after the message widget got hidden. + * + * @note This signal is @e not emitted if the widget was hidden by + * calling hide(), so this signal is only useful in conjunction + * with animatedHide(). + * + * @see animatedHide() + * @since 5.0 + */ + void hideAnimationFinished(); + + /** + * This signal is emitted when the show animation is finished, started by + * calling animatedShow(). If animations are disabled, this signal is + * emitted immediately after the message widget got shown. + * + * @note This signal is @e not emitted if the widget was shown by + * calling show(), so this signal is only useful in conjunction + * with animatedShow(). + * + * @see animatedShow() + * @since 5.0 + */ + void showAnimationFinished(); + +protected: + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + + bool event(QEvent *event) Q_DECL_OVERRIDE; + + void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; + +private: + KMessageWidgetPrivate *const d; + friend class KMessageWidgetPrivate; + + Q_PRIVATE_SLOT(d, void slotTimeLineChanged(qreal)) + Q_PRIVATE_SLOT(d, void slotTimeLineFinished()) +}; + +#endif /* KMESSAGEWIDGET_H */ diff --git a/desktop-widgets/listfilter.ui b/desktop-widgets/listfilter.ui new file mode 100644 index 000000000..48d813d21 --- /dev/null +++ b/desktop-widgets/listfilter.ui @@ -0,0 +1,63 @@ + + + FilterWidget + + + + 0 + 0 + 400 + 166 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 5 + + + + + Text label + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Filter this list + + + + + + + + + + + + + diff --git a/desktop-widgets/locationInformation.ui b/desktop-widgets/locationInformation.ui new file mode 100644 index 000000000..58d065648 --- /dev/null +++ b/desktop-widgets/locationInformation.ui @@ -0,0 +1,156 @@ + + + LocationInformation + + + + 0 + 0 + 556 + 707 + + + + GroupBox + + + + + + + 6 + + + 4 + + + + + Name + + + + + + + Description + + + + + + + Notes + + + + + + + + + + Coordinates + + + + + + + + + + + + + + + + Reverse geo lookup + + + ... + + + + :/satellite:/satellite + + + + + + + + 0 + 0 + + + + + + + + Dive sites on same coordinates + + + + + + + 0 + 0 + + + + QAbstractItemView::MultiSelection + + + 0 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Tags + + + + + + + + + + + + + + + KMessageWidget + QFrame +
kmessagewidget.h
+ 1 +
+
+ + + + +
diff --git a/desktop-widgets/locationinformation.cpp b/desktop-widgets/locationinformation.cpp new file mode 100644 index 000000000..aee0b7328 --- /dev/null +++ b/desktop-widgets/locationinformation.cpp @@ -0,0 +1,618 @@ +#include "locationinformation.h" +#include "dive.h" +#include "mainwindow.h" +#include "divelistview.h" +#include "qthelper.h" +#include "globe.h" +#include "filtermodels.h" +#include "divelocationmodel.h" +#include "divesitehelpers.h" +#include "modeldelegates.h" + +#include +#include +#include +#include +#include +#include +#include + +LocationInformationWidget::LocationInformationWidget(QWidget *parent) : QGroupBox(parent), modified(false) +{ + ui.setupUi(this); + ui.diveSiteMessage->setCloseButtonVisible(false); + + acceptAction = new QAction(tr("Apply changes"), this); + connect(acceptAction, SIGNAL(triggered(bool)), this, SLOT(acceptChanges())); + + rejectAction = new QAction(tr("Discard changes"), this); + connect(rejectAction, SIGNAL(triggered(bool)), this, SLOT(rejectChanges())); + + ui.diveSiteMessage->setText(tr("Dive site management")); + ui.diveSiteMessage->addAction(acceptAction); + ui.diveSiteMessage->addAction(rejectAction); + + connect(this, SIGNAL(startFilterDiveSite(uint32_t)), MultiFilterSortModel::instance(), SLOT(startFilterDiveSite(uint32_t))); + connect(this, SIGNAL(stopFilterDiveSite()), MultiFilterSortModel::instance(), SLOT(stopFilterDiveSite())); + connect(ui.geoCodeButton, SIGNAL(clicked()), this, SLOT(reverseGeocode())); + + SsrfSortFilterProxyModel *filter_model = new SsrfSortFilterProxyModel(this); + filter_model->setSourceModel(LocationInformationModel::instance()); + filter_model->setFilterRow(filter_same_gps_cb); + ui.diveSiteListView->setModel(filter_model); + ui.diveSiteListView->setModelColumn(LocationInformationModel::NAME); + ui.diveSiteListView->installEventFilter(this); +#ifndef NO_MARBLE + // Globe Management Code. + connect(this, &LocationInformationWidget::requestCoordinates, + GlobeGPS::instance(), &GlobeGPS::prepareForGetDiveCoordinates); + connect(this, &LocationInformationWidget::endRequestCoordinates, + GlobeGPS::instance(), &GlobeGPS::endGetDiveCoordinates); + connect(GlobeGPS::instance(), &GlobeGPS::coordinatesChanged, + this, &LocationInformationWidget::updateGpsCoordinates); + connect(this, &LocationInformationWidget::endEditDiveSite, + GlobeGPS::instance(), &GlobeGPS::repopulateLabels); +#endif +} + +bool LocationInformationWidget::eventFilter(QObject *, QEvent *ev) +{ + if (ev->type() == QEvent::ContextMenu) { + QContextMenuEvent *ctx = (QContextMenuEvent *)ev; + QMenu contextMenu; + contextMenu.addAction(tr("Merge into current site"), this, SLOT(mergeSelectedDiveSites())); + contextMenu.exec(ctx->globalPos()); + return true; + } + return false; +} + +void LocationInformationWidget::mergeSelectedDiveSites() +{ + if (QMessageBox::warning(MainWindow::instance(), tr("Merging dive sites"), + tr("You are about to merge dive sites, you can't undo that action \n Are you sure you want to continue?"), + QMessageBox::Ok, QMessageBox::Cancel) != QMessageBox::Ok) + return; + + QModelIndexList selection = ui.diveSiteListView->selectionModel()->selectedIndexes(); + uint32_t *selected_dive_sites = (uint32_t *)malloc(sizeof(uint32_t) * selection.count()); + int i = 0; + Q_FOREACH (const QModelIndex &idx, selection) { + selected_dive_sites[i] = (uint32_t)idx.data(LocationInformationModel::UUID_ROLE).toInt(); + i++; + } + merge_dive_sites(displayed_dive_site.uuid, selected_dive_sites, i); + LocationInformationModel::instance()->update(); + QSortFilterProxyModel *m = (QSortFilterProxyModel *)ui.diveSiteListView->model(); + m->invalidate(); + free(selected_dive_sites); +} + +void LocationInformationWidget::updateLabels() +{ + if (displayed_dive_site.name) + ui.diveSiteName->setText(displayed_dive_site.name); + else + ui.diveSiteName->clear(); + if (displayed_dive_site.description) + ui.diveSiteDescription->setText(displayed_dive_site.description); + else + ui.diveSiteDescription->clear(); + if (displayed_dive_site.notes) + ui.diveSiteNotes->setPlainText(displayed_dive_site.notes); + else + ui.diveSiteNotes->clear(); + if (displayed_dive_site.latitude.udeg || displayed_dive_site.longitude.udeg) { + const char *coords = printGPSCoords(displayed_dive_site.latitude.udeg, displayed_dive_site.longitude.udeg); + ui.diveSiteCoordinates->setText(coords); + free((void *)coords); + } else { + ui.diveSiteCoordinates->clear(); + } + + ui.locationTags->setText(constructLocationTags(displayed_dive_site.uuid)); + + emit startFilterDiveSite(displayed_dive_site.uuid); + emit startEditDiveSite(displayed_dive_site.uuid); +} + +void LocationInformationWidget::updateGpsCoordinates() +{ + QString oldText = ui.diveSiteCoordinates->text(); + const char *coords = printGPSCoords(displayed_dive_site.latitude.udeg, displayed_dive_site.longitude.udeg); + ui.diveSiteCoordinates->setText(coords); + free((void *)coords); + if (oldText != ui.diveSiteCoordinates->text()) + markChangedWidget(ui.diveSiteCoordinates); +} + +void LocationInformationWidget::acceptChanges() +{ + char *uiString; + struct dive_site *currentDs; + uiString = ui.diveSiteName->text().toUtf8().data(); + + if (get_dive_site_by_uuid(displayed_dive_site.uuid) != NULL) + currentDs = get_dive_site_by_uuid(displayed_dive_site.uuid); + else + currentDs = get_dive_site_by_uuid(create_dive_site_from_current_dive(uiString)); + + currentDs->latitude = displayed_dive_site.latitude; + currentDs->longitude = displayed_dive_site.longitude; + if (!same_string(uiString, currentDs->name)) { + free(currentDs->name); + currentDs->name = copy_string(uiString); + } + uiString = ui.diveSiteDescription->text().toUtf8().data(); + if (!same_string(uiString, currentDs->description)) { + free(currentDs->description); + currentDs->description = copy_string(uiString); + } + uiString = ui.diveSiteNotes->document()->toPlainText().toUtf8().data(); + if (!same_string(uiString, currentDs->notes)) { + free(currentDs->notes); + currentDs->notes = copy_string(uiString); + } + if (!ui.diveSiteCoordinates->text().isEmpty()) { + double lat, lon; + parseGpsText(ui.diveSiteCoordinates->text(), &lat, &lon); + currentDs->latitude.udeg = lat * 1000000.0; + currentDs->longitude.udeg = lon * 1000000.0; + } + if (dive_site_is_empty(currentDs)) { + LocationInformationModel::instance()->removeRow(get_divesite_idx(currentDs)); + displayed_dive.dive_site_uuid = 0; + } + copy_dive_site(currentDs, &displayed_dive_site); + mark_divelist_changed(true); + resetState(); + emit endRequestCoordinates(); + emit endEditDiveSite(); + emit stopFilterDiveSite(); + emit coordinatesChanged(); +} + +void LocationInformationWidget::rejectChanges() +{ + resetState(); + emit endRequestCoordinates(); + emit stopFilterDiveSite(); + emit endEditDiveSite(); + emit coordinatesChanged(); +} + +void LocationInformationWidget::showEvent(QShowEvent *ev) +{ + if (displayed_dive_site.uuid) { + updateLabels(); + ui.geoCodeButton->setEnabled(dive_site_has_gps_location(&displayed_dive_site)); + QSortFilterProxyModel *m = qobject_cast(ui.diveSiteListView->model()); + emit startFilterDiveSite(displayed_dive_site.uuid); + if (m) + m->invalidate(); + } + emit requestCoordinates(); + + QGroupBox::showEvent(ev); +} + +void LocationInformationWidget::markChangedWidget(QWidget *w) +{ + QPalette p; + qreal h, s, l, a; + if (!modified) + enableEdition(); + qApp->palette().color(QPalette::Text).getHslF(&h, &s, &l, &a); + p.setBrush(QPalette::Base, (l <= 0.3) ? QColor(Qt::yellow).lighter() : (l <= 0.6) ? QColor(Qt::yellow).light() : /* else */ QColor(Qt::yellow).darker(300)); + w->setPalette(p); + modified = true; +} + +void LocationInformationWidget::resetState() +{ + modified = false; + resetPallete(); + MainWindow::instance()->dive_list()->setEnabled(true); + MainWindow::instance()->setEnabledToolbar(true); + ui.diveSiteMessage->setText(tr("Dive site management")); +} + +void LocationInformationWidget::enableEdition() +{ + MainWindow::instance()->dive_list()->setEnabled(false); + MainWindow::instance()->setEnabledToolbar(false); + ui.diveSiteMessage->setText(tr("You are editing a dive site")); +} + +void LocationInformationWidget::on_diveSiteCoordinates_textChanged(const QString &text) +{ + uint lat = displayed_dive_site.latitude.udeg; + uint lon = displayed_dive_site.longitude.udeg; + const char *coords = printGPSCoords(lat, lon); + if (!same_string(qPrintable(text), coords)) { + double latitude, longitude; + if (parseGpsText(text, &latitude, &longitude)) { + displayed_dive_site.latitude.udeg = latitude * 1000000; + displayed_dive_site.longitude.udeg = longitude * 1000000; + markChangedWidget(ui.diveSiteCoordinates); + emit coordinatesChanged(); + ui.geoCodeButton->setEnabled(latitude != 0 && longitude != 0); + } else { + ui.geoCodeButton->setEnabled(false); + } + } + free((void *)coords); +} + +void LocationInformationWidget::on_diveSiteDescription_textChanged(const QString &text) +{ + if (!same_string(qPrintable(text), displayed_dive_site.description)) + markChangedWidget(ui.diveSiteDescription); +} + +void LocationInformationWidget::on_diveSiteName_textChanged(const QString &text) +{ + if (!same_string(qPrintable(text), displayed_dive_site.name)) + markChangedWidget(ui.diveSiteName); +} + +void LocationInformationWidget::on_diveSiteNotes_textChanged() +{ + if (!same_string(qPrintable(ui.diveSiteNotes->toPlainText()), displayed_dive_site.notes)) + markChangedWidget(ui.diveSiteNotes); +} + +void LocationInformationWidget::resetPallete() +{ + QPalette p; + ui.diveSiteCoordinates->setPalette(p); + ui.diveSiteDescription->setPalette(p); + ui.diveSiteName->setPalette(p); + ui.diveSiteNotes->setPalette(p); +} + +void LocationInformationWidget::reverseGeocode() +{ + ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); + geoLookup->lookup(&displayed_dive_site); + updateLabels(); +} + +DiveLocationFilterProxyModel::DiveLocationFilterProxyModel(QObject *parent) +{ +} + +DiveLocationLineEdit *location_line_edit = 0; + +bool DiveLocationFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (source_row == 0) + return true; + + QString sourceString = sourceModel()->index(source_row, DiveLocationModel::NAME).data(Qt::DisplayRole).toString(); + return sourceString.toLower().startsWith(location_line_edit->text().toLower()); +} + +bool DiveLocationFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + return source_left.data().toString() <= source_right.data().toString(); +} + + +DiveLocationModel::DiveLocationModel(QObject *o) +{ + resetModel(); +} + +void DiveLocationModel::resetModel() +{ + beginResetModel(); + endResetModel(); +} + +QVariant DiveLocationModel::data(const QModelIndex &index, int role) const +{ + static const QIcon plusIcon(":plus"); + static const QIcon geoCode(":geocode"); + + if (index.row() <= 1) { // two special cases. + if (index.column() == UUID) { + return RECENTLY_ADDED_DIVESITE; + } + switch (role) { + case Qt::DisplayRole: + return new_ds_value[index.row()]; + case Qt::ToolTipRole: + return displayed_dive_site.uuid ? + tr("Create a new dive site, copying relevant information from the current dive.") : + tr("Create a new dive site with this name"); + case Qt::DecorationRole: + return plusIcon; + } + } + + // The dive sites are -2 because of the first two items. + struct dive_site *ds = get_dive_site(index.row() - 2); + switch (role) { + case Qt::EditRole: + case Qt::DisplayRole: + switch (index.column()) { + case UUID: + return ds->uuid; + case NAME: + return ds->name; + case LATITUDE: + return ds->latitude.udeg; + case LONGITUDE: + return ds->longitude.udeg; + case DESCRIPTION: + return ds->description; + case NOTES: + return ds->name; + } + break; + case Qt::DecorationRole: { + if (dive_site_has_gps_location(ds)) + return geoCode; + } + } + return QVariant(); +} + +int DiveLocationModel::columnCount(const QModelIndex &parent) const +{ + return COLUMNS; +} + +int DiveLocationModel::rowCount(const QModelIndex &parent) const +{ + return dive_site_table.nr + 2; +} + +bool DiveLocationModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) + return false; + if (index.row() > 1) + return false; + + new_ds_value[index.row()] = value.toString(); + + dataChanged(index, index); + return true; +} + +DiveLocationLineEdit::DiveLocationLineEdit(QWidget *parent) : QLineEdit(parent), + proxy(new DiveLocationFilterProxyModel()), + model(new DiveLocationModel()), + view(new DiveLocationListView()), + currType(NO_DIVE_SITE) +{ + currUuid = 0; + location_line_edit = this; + + proxy->setSourceModel(model); + proxy->setFilterKeyColumn(DiveLocationModel::NAME); + + view->setModel(proxy); + view->setModelColumn(DiveLocationModel::NAME); + view->setItemDelegate(new LocationFilterDelegate()); + view->setEditTriggers(QAbstractItemView::NoEditTriggers); + view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setSelectionMode(QAbstractItemView::SingleSelection); + view->setParent(0, Qt::Popup); + view->installEventFilter(this); + view->setFocusPolicy(Qt::NoFocus); + view->setFocusProxy(this); + view->setMouseTracking(true); + + connect(this, &QLineEdit::textEdited, this, &DiveLocationLineEdit::setTemporaryDiveSiteName); + connect(view, &QAbstractItemView::activated, this, &DiveLocationLineEdit::itemActivated); + connect(view, &QAbstractItemView::entered, this, &DiveLocationLineEdit::entered); + connect(view, &DiveLocationListView::currentIndexChanged, this, &DiveLocationLineEdit::currentChanged); +} + +bool DiveLocationLineEdit::eventFilter(QObject *o, QEvent *e) +{ + if (e->type() == QEvent::KeyPress) { + QKeyEvent *keyEv = (QKeyEvent *)e; + + if (keyEv->key() == Qt::Key_Escape) { + view->hide(); + return true; + } + + if (keyEv->key() == Qt::Key_Return || + keyEv->key() == Qt::Key_Enter) { +#if __APPLE__ + // for some reason it seems like on a Mac hitting return/enter + // doesn't call 'activated' for that index. so let's do it manually + if (view->currentIndex().isValid()) + itemActivated(view->currentIndex()); +#endif + view->hide(); + return false; + } + + if (keyEv->key() == Qt::Key_Tab) { + itemActivated(view->currentIndex()); + view->hide(); + return false; + } + event(e); + } else if (e->type() == QEvent::MouseButtonPress) { + if (!view->underMouse()) { + view->hide(); + return true; + } + } + + return false; +} + +void DiveLocationLineEdit::focusOutEvent(QFocusEvent *ev) +{ + if (!view->isVisible()) { + QLineEdit::focusOutEvent(ev); + } +} + +void DiveLocationLineEdit::itemActivated(const QModelIndex &index) +{ + QModelIndex idx = index; + if (index.column() == DiveLocationModel::UUID) + idx = index.model()->index(index.row(), DiveLocationModel::NAME); + + QModelIndex uuidIndex = index.model()->index(index.row(), DiveLocationModel::UUID); + uint32_t uuid = uuidIndex.data().toInt(); + currType = uuid == 1 ? NEW_DIVE_SITE : EXISTING_DIVE_SITE; + currUuid = uuid; + setText(idx.data().toString()); + if (currUuid == NEW_DIVE_SITE) + qDebug() << "Setting a New dive site"; + else + qDebug() << "Setting a Existing dive site"; + if (view->isVisible()) + view->hide(); + emit diveSiteSelected(currUuid); +} + +void DiveLocationLineEdit::refreshDiveSiteCache() +{ + model->resetModel(); +} + +static struct dive_site *get_dive_site_name_start_which_str(const QString &str) +{ + struct dive_site *ds; + int i; + for_each_dive_site (i, ds) { + QString dsName(ds->name); + if (dsName.toLower().startsWith(str.toLower())) { + return ds; + } + } + return NULL; +} + +void DiveLocationLineEdit::setTemporaryDiveSiteName(const QString &s) +{ + QModelIndex i0 = model->index(0, DiveLocationModel::NAME); + QModelIndex i1 = model->index(1, DiveLocationModel::NAME); + model->setData(i0, text()); + + QString i1_name = INVALID_DIVE_SITE_NAME; + if (struct dive_site *ds = get_dive_site_name_start_which_str(text())) { + const QString orig_name = QString(ds->name).toLower(); + const QString new_name = text().toLower(); + if (new_name != orig_name) + i1_name = QString(ds->name); + } + + model->setData(i1, i1_name); + proxy->invalidate(); + fixPopupPosition(); + if (!view->isVisible()) + view->show(); +} + +void DiveLocationLineEdit::keyPressEvent(QKeyEvent *ev) +{ + QLineEdit::keyPressEvent(ev); + if (ev->key() != Qt::Key_Left && + ev->key() != Qt::Key_Right && + ev->key() != Qt::Key_Escape && + ev->key() != Qt::Key_Return) { + + if (ev->key() != Qt::Key_Up && ev->key() != Qt::Key_Down) { + currType = NEW_DIVE_SITE; + currUuid = RECENTLY_ADDED_DIVESITE; + } else { + showPopup(); + } + } else if (ev->key() == Qt::Key_Escape) { + view->hide(); + } +} + +void DiveLocationLineEdit::fixPopupPosition() +{ + const QRect screen = QApplication::desktop()->availableGeometry(this); + const int maxVisibleItems = 5; + Qt::LayoutDirection dir = layoutDirection(); + QPoint pos; + int rh, w; + int h = (view->sizeHintForRow(0) * qMin(maxVisibleItems, view->model()->rowCount()) + 3) + 3; + QScrollBar *hsb = view->horizontalScrollBar(); + if (hsb && hsb->isVisible()) + h += view->horizontalScrollBar()->sizeHint().height(); + + rh = height(); + pos = mapToGlobal(QPoint(0, height() - 2)); + w = width(); + + if (w > screen.width()) + w = screen.width(); + if ((pos.x() + w) > (screen.x() + screen.width())) + pos.setX(screen.x() + screen.width() - w); + if (pos.x() < screen.x()) + pos.setX(screen.x()); + + int top = pos.y() - rh - screen.top() + 2; + int bottom = screen.bottom() - pos.y(); + h = qMax(h, view->minimumHeight()); + if (h > bottom) { + h = qMin(qMax(top, bottom), h); + if (top > bottom) + pos.setY(pos.y() - h - rh + 2); + } + + view->setGeometry(pos.x(), pos.y(), w, h); + if (!view->currentIndex().isValid() && view->model()->rowCount()) { + view->setCurrentIndex(view->model()->index(0, 0)); + } +} + +void DiveLocationLineEdit::setCurrentDiveSiteUuid(uint32_t uuid) +{ + currUuid = uuid; + if (uuid == 0) { + currType = NO_DIVE_SITE; + } + struct dive_site *ds = get_dive_site_by_uuid(uuid); + if (!ds) + clear(); + else + setText(ds->name); +} + +void DiveLocationLineEdit::showPopup() +{ + fixPopupPosition(); + if (!view->isVisible()) { + setTemporaryDiveSiteName(text()); + proxy->invalidate(); + view->show(); + } +} + +DiveLocationLineEdit::DiveSiteType DiveLocationLineEdit::currDiveSiteType() const +{ + return currType; +} + +uint32_t DiveLocationLineEdit::currDiveSiteUuid() const +{ + return currUuid; +} + +DiveLocationListView::DiveLocationListView(QWidget *parent) +{ +} + +void DiveLocationListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + QListView::currentChanged(current, previous); + emit currentIndexChanged(current); +} diff --git a/desktop-widgets/locationinformation.h b/desktop-widgets/locationinformation.h new file mode 100644 index 000000000..243df939b --- /dev/null +++ b/desktop-widgets/locationinformation.h @@ -0,0 +1,114 @@ +#ifndef LOCATIONINFORMATION_H +#define LOCATIONINFORMATION_H + +#include "ui_locationInformation.h" +#include +#include +#include + +class LocationInformationWidget : public QGroupBox { +Q_OBJECT +public: + LocationInformationWidget(QWidget *parent = 0); + virtual bool eventFilter(QObject*, QEvent*); + +protected: + void showEvent(QShowEvent *); + +public slots: + void acceptChanges(); + void rejectChanges(); + void updateGpsCoordinates(); + void markChangedWidget(QWidget *w); + void enableEdition(); + void resetState(); + void resetPallete(); + void on_diveSiteCoordinates_textChanged(const QString& text); + void on_diveSiteDescription_textChanged(const QString& text); + void on_diveSiteName_textChanged(const QString& text); + void on_diveSiteNotes_textChanged(); + void reverseGeocode(); + void mergeSelectedDiveSites(); +private slots: + void updateLabels(); +signals: + void startEditDiveSite(uint32_t uuid); + void endEditDiveSite(); + void coordinatesChanged(); + void startFilterDiveSite(uint32_t uuid); + void stopFilterDiveSite(); + void requestCoordinates(); + void endRequestCoordinates(); + +private: + Ui::LocationInformation ui; + bool modified; + QAction *acceptAction, *rejectAction; +}; + +class DiveLocationFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + DiveLocationFilterProxyModel(QObject *parent = 0); + virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + virtual bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const; +}; + +class DiveLocationModel : public QAbstractTableModel { + Q_OBJECT +public: + enum columns{UUID, NAME, LATITUDE, LONGITUDE, DESCRIPTION, NOTES, COLUMNS}; + DiveLocationModel(QObject *o = 0); + void resetModel(); + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); +private: + QString new_ds_value[2]; +}; + +class DiveLocationListView : public QListView { + Q_OBJECT +public: + DiveLocationListView(QWidget *parent = 0); +protected: + virtual void currentChanged(const QModelIndex& current, const QModelIndex& previous); +signals: + void currentIndexChanged(const QModelIndex& current); +}; + +class DiveLocationLineEdit : public QLineEdit { + Q_OBJECT +public: + enum DiveSiteType { NO_DIVE_SITE, NEW_DIVE_SITE, EXISTING_DIVE_SITE }; + DiveLocationLineEdit(QWidget *parent =0 ); + void refreshDiveSiteCache(); + void setTemporaryDiveSiteName(const QString& s); + bool eventFilter(QObject*, QEvent*); + void itemActivated(const QModelIndex& index); + DiveSiteType currDiveSiteType() const; + uint32_t currDiveSiteUuid() const; + void fixPopupPosition(); + void setCurrentDiveSiteUuid(uint32_t uuid); + +signals: + void diveSiteSelected(uint32_t uuid); + void entered(const QModelIndex& index); + void currentChanged(const QModelIndex& index); + +protected: + void keyPressEvent(QKeyEvent *ev); + void focusOutEvent(QFocusEvent *ev); + void showPopup(); + +private: + using QLineEdit::setText; + DiveLocationFilterProxyModel *proxy; + DiveLocationModel *model; + DiveLocationListView *view; + DiveSiteType currType; + uint32_t currUuid; +}; + +#endif diff --git a/desktop-widgets/maintab.cpp b/desktop-widgets/maintab.cpp new file mode 100644 index 000000000..0afb7b4c0 --- /dev/null +++ b/desktop-widgets/maintab.cpp @@ -0,0 +1,1612 @@ +/* + * maintab.cpp + * + * classes for the "notebook" area of the main window of Subsurface + * + */ +#include "maintab.h" +#include "mainwindow.h" +#include "globe.h" +#include "helpers.h" +#include "statistics.h" +#include "modeldelegates.h" +#include "diveplannermodel.h" +#include "divelistview.h" +#include "display.h" +#include "profile/profilewidget2.h" +#include "diveplanner.h" +#include "divesitehelpers.h" +#include "cylindermodel.h" +#include "weightmodel.h" +#include "divepicturemodel.h" +#include "divecomputerextradatamodel.h" +#include "divelocationmodel.h" +#include "divesite.h" +#include "locationinformation.h" +#include "divesite.h" + +#include +#include +#include +#include +#include +#include +#include + +MainTab::MainTab(QWidget *parent) : QTabWidget(parent), + weightModel(new WeightModel(this)), + cylindersModel(CylindersModel::instance()), + extraDataModel(new ExtraDataModel(this)), + editMode(NONE), + divePictureModel(DivePictureModel::instance()), + copyPaste(false), + currentTrip(0) +{ + ui.setupUi(this); + ui.dateEdit->setDisplayFormat(getDateFormat()); + + memset(&displayed_dive, 0, sizeof(displayed_dive)); + memset(&displayedTrip, 0, sizeof(displayedTrip)); + + ui.cylinders->setModel(cylindersModel); + ui.weights->setModel(weightModel); + ui.photosView->setModel(divePictureModel); + connect(ui.photosView, SIGNAL(photoDoubleClicked(QString)), this, SLOT(photoDoubleClicked(QString))); + ui.extraData->setModel(extraDataModel); + closeMessage(); + + connect(ui.editDiveSiteButton, SIGNAL(clicked()), MainWindow::instance(), SIGNAL(startDiveSiteEdit())); +#ifndef NO_MARBLE + connect(ui.location, &DiveLocationLineEdit::entered, GlobeGPS::instance(), &GlobeGPS::centerOnIndex); + connect(ui.location, &DiveLocationLineEdit::currentChanged, GlobeGPS::instance(), &GlobeGPS::centerOnIndex); +#endif + + QAction *action = new QAction(tr("Apply changes"), this); + connect(action, SIGNAL(triggered(bool)), this, SLOT(acceptChanges())); + addMessageAction(action); + + action = new QAction(tr("Discard changes"), this); + connect(action, SIGNAL(triggered(bool)), this, SLOT(rejectChanges())); + addMessageAction(action); + + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::Key_Escape), this); + connect(closeKey, SIGNAL(activated()), this, SLOT(escDetected())); + + if (qApp->style()->objectName() == "oxygen") + setDocumentMode(true); + else + setDocumentMode(false); + + // we start out with the fields read-only; once things are + // filled from a dive, they are made writeable + setEnabled(false); + + Q_FOREACH (QObject *obj, ui.statisticsTab->children()) { + QLabel *label = qobject_cast(obj); + if (label) + label->setAlignment(Qt::AlignHCenter); + } + ui.cylinders->setTitle(tr("Cylinders")); + ui.cylinders->setBtnToolTip(tr("Add cylinder")); + connect(ui.cylinders, SIGNAL(addButtonClicked()), this, SLOT(addCylinder_clicked())); + + ui.weights->setTitle(tr("Weights")); + ui.weights->setBtnToolTip(tr("Add weight system")); + connect(ui.weights, SIGNAL(addButtonClicked()), this, SLOT(addWeight_clicked())); + + // This needs to be the same order as enum dive_comp_type in dive.h! + ui.DiveType->insertItems(0, QStringList() << tr("OC") << tr("CCR") << tr("pSCR") << tr("Freedive")); + connect(ui.DiveType, SIGNAL(currentIndexChanged(int)), this, SLOT(divetype_Changed(int))); + + connect(ui.cylinders->view(), SIGNAL(clicked(QModelIndex)), this, SLOT(editCylinderWidget(QModelIndex))); + connect(ui.weights->view(), SIGNAL(clicked(QModelIndex)), this, SLOT(editWeightWidget(QModelIndex))); + + ui.cylinders->view()->setItemDelegateForColumn(CylindersModel::TYPE, new TankInfoDelegate(this)); + ui.cylinders->view()->setItemDelegateForColumn(CylindersModel::USE, new TankUseDelegate(this)); + ui.weights->view()->setItemDelegateForColumn(WeightModel::TYPE, new WSInfoDelegate(this)); + ui.cylinders->view()->setColumnHidden(CylindersModel::DEPTH, true); + completers.buddy = new QCompleter(&buddyModel, ui.buddy); + completers.divemaster = new QCompleter(&diveMasterModel, ui.divemaster); + completers.suit = new QCompleter(&suitModel, ui.suit); + completers.tags = new QCompleter(&tagModel, ui.tagWidget); + completers.buddy->setCaseSensitivity(Qt::CaseInsensitive); + completers.divemaster->setCaseSensitivity(Qt::CaseInsensitive); + completers.suit->setCaseSensitivity(Qt::CaseInsensitive); + completers.tags->setCaseSensitivity(Qt::CaseInsensitive); + ui.buddy->setCompleter(completers.buddy); + ui.divemaster->setCompleter(completers.divemaster); + ui.suit->setCompleter(completers.suit); + ui.tagWidget->setCompleter(completers.tags); + ui.diveNotesMessage->hide(); + ui.diveEquipmentMessage->hide(); + ui.diveInfoMessage->hide(); + ui.diveStatisticsMessage->hide(); + setMinimumHeight(0); + setMinimumWidth(0); + + // Current display of things on Gnome3 looks like shit, so + // let`s fix that. + if (isGnome3Session()) { + QPalette p; + p.setColor(QPalette::Window, QColor(Qt::white)); + ui.scrollArea->viewport()->setPalette(p); + ui.scrollArea_2->viewport()->setPalette(p); + ui.scrollArea_3->viewport()->setPalette(p); + ui.scrollArea_4->viewport()->setPalette(p); + + // GroupBoxes in Gnome3 looks like I'v drawn them... + static const QString gnomeCss( + "QGroupBox {" + " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," + " stop: 0 #E0E0E0, stop: 1 #FFFFFF);" + " border: 2px solid gray;" + " border-radius: 5px;" + " margin-top: 1ex;" + "}" + "QGroupBox::title {" + " subcontrol-origin: margin;" + " subcontrol-position: top center;" + " padding: 0 3px;" + " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," + " stop: 0 #E0E0E0, stop: 1 #FFFFFF);" + "}"); + Q_FOREACH (QGroupBox *box, findChildren()) { + box->setStyleSheet(gnomeCss); + } + } + // QLineEdit and QLabels should have minimal margin on the left and right but not waste vertical space + QMargins margins(3, 2, 1, 0); + Q_FOREACH (QLabel *label, findChildren()) { + label->setContentsMargins(margins); + } + ui.cylinders->view()->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu); + ui.weights->view()->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu); + + QSettings s; + s.beginGroup("cylinders_dialog"); + for (int i = 0; i < CylindersModel::COLUMNS; i++) { + if ((i == CylindersModel::REMOVE) || (i == CylindersModel::TYPE)) + continue; + bool checked = s.value(QString("column%1_hidden").arg(i)).toBool(); + action = new QAction(cylindersModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(), ui.cylinders->view()); + action->setCheckable(true); + action->setData(i); + action->setChecked(!checked); + connect(action, SIGNAL(triggered(bool)), this, SLOT(toggleTriggeredColumn())); + ui.cylinders->view()->setColumnHidden(i, checked); + ui.cylinders->view()->horizontalHeader()->addAction(action); + } + + QAction *deletePhoto = new QAction(this); + deletePhoto->setShortcut(Qt::Key_Delete); + deletePhoto->setShortcutContext(Qt::WidgetShortcut); + ui.photosView->addAction(deletePhoto); + ui.photosView->setSelectionMode(QAbstractItemView::SingleSelection); + connect(deletePhoto, SIGNAL(triggered(bool)), this, SLOT(removeSelectedPhotos())); + + ui.waitingSpinner->setRoundness(70.0); + ui.waitingSpinner->setMinimumTrailOpacity(15.0); + ui.waitingSpinner->setTrailFadePercentage(70.0); + ui.waitingSpinner->setNumberOfLines(8); + ui.waitingSpinner->setLineLength(5); + ui.waitingSpinner->setLineWidth(3); + ui.waitingSpinner->setInnerRadius(5); + ui.waitingSpinner->setRevolutionsPerSecond(1); + + connect(ReverseGeoLookupThread::instance(), SIGNAL(finished()), + LocationInformationModel::instance(), SLOT(update())); + + connect(ReverseGeoLookupThread::instance(), &QThread::finished, + this, &MainTab::setCurrentLocationIndex); + + connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished, + ui.location, &DiveLocationLineEdit::fixPopupPosition); + + acceptingEdit = false; + + ui.diveTripLocation->hide(); +} + +MainTab::~MainTab() +{ + QSettings s; + s.beginGroup("cylinders_dialog"); + for (int i = 0; i < CylindersModel::COLUMNS; i++) { + if ((i == CylindersModel::REMOVE) || (i == CylindersModel::TYPE)) + continue; + s.setValue(QString("column%1_hidden").arg(i), ui.cylinders->view()->isColumnHidden(i)); + } +} + +void MainTab::setCurrentLocationIndex() +{ + if (current_dive) { + struct dive_site *ds = get_dive_site_by_uuid(current_dive->dive_site_uuid); + if (ds) + ui.location->setCurrentDiveSiteUuid(ds->uuid); + else + ui.location->clear(); + } +} + +void MainTab::enableGeoLookupEdition() +{ + ui.waitingSpinner->stop(); +} + +void MainTab::disableGeoLookupEdition() +{ + ui.waitingSpinner->start(); +} + +void MainTab::toggleTriggeredColumn() +{ + QAction *action = qobject_cast(sender()); + int col = action->data().toInt(); + QTableView *view = ui.cylinders->view(); + + if (action->isChecked()) { + view->showColumn(col); + if (view->columnWidth(col) <= 15) + view->setColumnWidth(col, 80); + } else + view->hideColumn(col); +} + +void MainTab::addDiveStarted() +{ + enableEdition(ADD); +} + +void MainTab::addMessageAction(QAction *action) +{ + ui.diveEquipmentMessage->addAction(action); + ui.diveNotesMessage->addAction(action); + ui.diveInfoMessage->addAction(action); + ui.diveStatisticsMessage->addAction(action); +} + +void MainTab::hideMessage() +{ + ui.diveNotesMessage->animatedHide(); + ui.diveEquipmentMessage->animatedHide(); + ui.diveInfoMessage->animatedHide(); + ui.diveStatisticsMessage->animatedHide(); + updateTextLabels(false); +} + +void MainTab::closeMessage() +{ + hideMessage(); + ui.diveNotesMessage->setCloseButtonVisible(false); + ui.diveEquipmentMessage->setCloseButtonVisible(false); + ui.diveInfoMessage->setCloseButtonVisible(false); + ui.diveStatisticsMessage->setCloseButtonVisible(false); +} + +void MainTab::displayMessage(QString str) +{ + ui.diveNotesMessage->setCloseButtonVisible(false); + ui.diveEquipmentMessage->setCloseButtonVisible(false); + ui.diveInfoMessage->setCloseButtonVisible(false); + ui.diveStatisticsMessage->setCloseButtonVisible(false); + ui.diveNotesMessage->setText(str); + ui.diveNotesMessage->animatedShow(); + ui.diveEquipmentMessage->setText(str); + ui.diveEquipmentMessage->animatedShow(); + ui.diveInfoMessage->setText(str); + ui.diveInfoMessage->animatedShow(); + ui.diveStatisticsMessage->setText(str); + ui.diveStatisticsMessage->animatedShow(); + updateTextLabels(); +} + +void MainTab::updateTextLabels(bool showUnits) +{ + if (showUnits) { + ui.airTempLabel->setText(tr("Air temp. [%1]").arg(get_temp_unit())); + ui.waterTempLabel->setText(tr("Water temp. [%1]").arg(get_temp_unit())); + } else { + ui.airTempLabel->setText(tr("Air temp.")); + ui.waterTempLabel->setText(tr("Water temp.")); + } +} + +void MainTab::enableEdition(EditMode newEditMode) +{ + const bool isTripEdit = MainWindow::instance() && + MainWindow::instance()->dive_list()->selectedTrips().count() == 1; + + if (((newEditMode == DIVE || newEditMode == NONE) && current_dive == NULL) || editMode != NONE) + return; + modified = false; + copyPaste = false; + if ((newEditMode == DIVE || newEditMode == NONE) && + !isTripEdit && + current_dive->dc.model && + strcmp(current_dive->dc.model, "manually added dive") == 0) { + // editCurrentDive will call enableEdition with newEditMode == MANUALLY_ADDED_DIVE + // so exit this function here after editCurrentDive() returns + + + + // FIXME : can we get rid of this recursive crap? + + + + MainWindow::instance()->editCurrentDive(); + return; + } + + ui.editDiveSiteButton->setEnabled(false); + MainWindow::instance()->dive_list()->setEnabled(false); + MainWindow::instance()->setEnabledToolbar(false); + + if (isTripEdit) { + // we are editing trip location and notes + displayMessage(tr("This trip is being edited.")); + currentTrip = current_dive->divetrip; + ui.dateEdit->setEnabled(false); + editMode = TRIP; + } else { + ui.dateEdit->setEnabled(true); + if (amount_selected > 1) { + displayMessage(tr("Multiple dives are being edited.")); + } else { + displayMessage(tr("This dive is being edited.")); + } + editMode = newEditMode != NONE ? newEditMode : DIVE; + } +} + +void MainTab::clearEquipment() +{ + cylindersModel->clear(); + weightModel->clear(); +} + +void MainTab::nextInputField(QKeyEvent *event) +{ + keyPressEvent(event); +} + +void MainTab::clearInfo() +{ + ui.sacText->clear(); + ui.otuText->clear(); + ui.maxcnsText->clear(); + ui.oxygenHeliumText->clear(); + ui.gasUsedText->clear(); + ui.dateText->clear(); + ui.diveTimeText->clear(); + ui.surfaceIntervalText->clear(); + ui.maximumDepthText->clear(); + ui.averageDepthText->clear(); + ui.waterTemperatureText->clear(); + ui.airTemperatureText->clear(); + ui.airPressureText->clear(); + ui.salinityText->clear(); + ui.tagWidget->clear(); +} + +void MainTab::clearStats() +{ + ui.depthLimits->clear(); + ui.sacLimits->clear(); + ui.divesAllText->clear(); + ui.tempLimits->clear(); + ui.totalTimeAllText->clear(); + ui.timeLimits->clear(); +} + +#define UPDATE_TEXT(d, field) \ + if (clear || !d.field) \ + ui.field->setText(QString()); \ + else \ + ui.field->setText(d.field) + +#define UPDATE_TEMP(d, field) \ + if (clear || d.field.mkelvin == 0) \ + ui.field->setText(""); \ + else \ + ui.field->setText(get_temperature_string(d.field, true)) + +bool MainTab::isEditing() +{ + return editMode != NONE; +} + +void MainTab::showLocation() +{ + if (get_dive_site_by_uuid(displayed_dive.dive_site_uuid)) + ui.location->setCurrentDiveSiteUuid(displayed_dive.dive_site_uuid); + else + ui.location->clear(); +} + +// Seems wrong, since we can also call updateDiveInfo(), but since the updateDiveInfo +// has a parameter on it's definition it didn't worked on the signal slot connection. +void MainTab::refreshDiveInfo() +{ + updateDiveInfo(); +} + +void MainTab::updateDiveInfo(bool clear) +{ + ui.location->refreshDiveSiteCache(); + EditMode rememberEM = editMode; + // don't execute this while adding / planning a dive + if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE || MainWindow::instance()->graphics()->isPlanner()) + return; + if (!isEnabled() && !clear ) + setEnabled(true); + if (isEnabled() && clear) + setEnabled(false); + editMode = IGNORE; // don't trigger on changes to the widgets + + // This method updates ALL tabs whenever a new dive or trip is + // selected. + // If exactly one trip has been selected, we show the location / notes + // for the trip in the Info tab, otherwise we show the info of the + // selected_dive + temperature_t temp; + struct dive *prevd; + char buf[1024]; + + process_selected_dives(); + process_all_dives(&displayed_dive, &prevd); + + divePictureModel->updateDivePictures(); + + ui.notes->setText(QString()); + if (!clear) { + QString tmp(displayed_dive.notes); + if (tmp.indexOf("setHtml(tmp); + else + ui.notes->setPlainText(tmp); + } + UPDATE_TEXT(displayed_dive, notes); + UPDATE_TEXT(displayed_dive, suit); + UPDATE_TEXT(displayed_dive, divemaster); + UPDATE_TEXT(displayed_dive, buddy); + UPDATE_TEMP(displayed_dive, airtemp); + UPDATE_TEMP(displayed_dive, watertemp); + ui.DiveType->setCurrentIndex(get_dive_dc(&displayed_dive, dc_number)->divemode); + + if (!clear) { + struct dive_site *ds = NULL; + // if we are showing a dive and editing it, let's refer to the displayed_dive_site as that + // already may contain changes, otherwise start with the dive site referred to by the displayed + // dive + if (rememberEM == DIVE) { + ds = &displayed_dive_site; + } else { + ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); + if (ds) + copy_dive_site(ds, &displayed_dive_site); + } + + if (ds) { + ui.location->setCurrentDiveSiteUuid(ds->uuid); + ui.locationTags->setText(constructLocationTags(ds->uuid)); + } else { + ui.location->clear(); + clear_dive_site(&displayed_dive_site); + } + + // Subsurface always uses "local time" as in "whatever was the local time at the location" + // so all time stamps have no time zone information and are in UTC + QDateTime localTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + localTime.setTimeSpec(Qt::UTC); + ui.dateEdit->setDate(localTime.date()); + ui.timeEdit->setTime(localTime.time()); + if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { + setTabText(0, tr("Trip notes")); + currentTrip = *MainWindow::instance()->dive_list()->selectedTrips().begin(); + // only use trip relevant fields + ui.divemaster->setVisible(false); + ui.DivemasterLabel->setVisible(false); + ui.buddy->setVisible(false); + ui.BuddyLabel->setVisible(false); + ui.suit->setVisible(false); + ui.SuitLabel->setVisible(false); + ui.rating->setVisible(false); + ui.RatingLabel->setVisible(false); + ui.visibility->setVisible(false); + ui.visibilityLabel->setVisible(false); + ui.tagWidget->setVisible(false); + ui.TagLabel->setVisible(false); + ui.airTempLabel->setVisible(false); + ui.airtemp->setVisible(false); + ui.DiveType->setVisible(false); + ui.TypeLabel->setVisible(false); + ui.waterTempLabel->setVisible(false); + ui.watertemp->setVisible(false); + ui.diveTripLocation->show(); + ui.location->hide(); + ui.editDiveSiteButton->hide(); + // rename the remaining fields and fill data from selected trip + ui.LocationLabel->setText(tr("Trip location")); + ui.diveTripLocation->setText(currentTrip->location); + ui.locationTags->clear(); + //TODO: Fix this. + //ui.location->setText(currentTrip->location); + ui.NotesLabel->setText(tr("Trip notes")); + ui.notes->setText(currentTrip->notes); + clearEquipment(); + ui.equipmentTab->setEnabled(false); + } else { + setTabText(0, tr("Notes")); + currentTrip = NULL; + // make all the fields visible writeable + ui.diveTripLocation->hide(); + ui.location->show(); + ui.editDiveSiteButton->show(); + ui.divemaster->setVisible(true); + ui.buddy->setVisible(true); + ui.suit->setVisible(true); + ui.SuitLabel->setVisible(true); + ui.rating->setVisible(true); + ui.RatingLabel->setVisible(true); + ui.visibility->setVisible(true); + ui.visibilityLabel->setVisible(true); + ui.BuddyLabel->setVisible(true); + ui.DivemasterLabel->setVisible(true); + ui.TagLabel->setVisible(true); + ui.tagWidget->setVisible(true); + ui.airTempLabel->setVisible(true); + ui.airtemp->setVisible(true); + ui.TypeLabel->setVisible(true); + ui.DiveType->setVisible(true); + ui.waterTempLabel->setVisible(true); + ui.watertemp->setVisible(true); + /* and fill them from the dive */ + ui.rating->setCurrentStars(displayed_dive.rating); + ui.visibility->setCurrentStars(displayed_dive.visibility); + // reset labels in case we last displayed trip notes + ui.LocationLabel->setText(tr("Location")); + ui.NotesLabel->setText(tr("Notes")); + ui.equipmentTab->setEnabled(true); + cylindersModel->updateDive(); + weightModel->updateDive(); + extraDataModel->updateDive(); + taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); + ui.tagWidget->setText(QString(buf)); + } + ui.maximumDepthText->setText(get_depth_string(displayed_dive.maxdepth, true)); + ui.averageDepthText->setText(get_depth_string(displayed_dive.meandepth, true)); + ui.maxcnsText->setText(QString("%1\%").arg(displayed_dive.maxcns)); + ui.otuText->setText(QString("%1").arg(displayed_dive.otu)); + ui.waterTemperatureText->setText(get_temperature_string(displayed_dive.watertemp, true)); + ui.airTemperatureText->setText(get_temperature_string(displayed_dive.airtemp, true)); + ui.DiveType->setCurrentIndex(get_dive_dc(&displayed_dive, dc_number)->divemode); + + volume_t gases[MAX_CYLINDERS] = {}; + get_gas_used(&displayed_dive, gases); + QString volumes; + int mean[MAX_CYLINDERS], duration[MAX_CYLINDERS]; + per_cylinder_mean_depth(&displayed_dive, select_dc(&displayed_dive), mean, duration); + volume_t sac; + QString gaslist, SACs, separator; + + gaslist = ""; SACs = ""; volumes = ""; separator = ""; + for (int i = 0; i < MAX_CYLINDERS; i++) { + if (!is_cylinder_used(&displayed_dive, i)) + continue; + gaslist.append(separator); volumes.append(separator); SACs.append(separator); + separator = "\n"; + + gaslist.append(gasname(&displayed_dive.cylinder[i].gasmix)); + if (!gases[i].mliter) + continue; + volumes.append(get_volume_string(gases[i], true)); + if (duration[i]) { + sac.mliter = gases[i].mliter / (depth_to_atm(mean[i], &displayed_dive) * duration[i] / 60); + SACs.append(get_volume_string(sac, true).append(tr("/min"))); + } + } + ui.gasUsedText->setText(volumes); + ui.oxygenHeliumText->setText(gaslist); + ui.dateText->setText(get_short_dive_date_string(displayed_dive.when)); + if (displayed_dive.dc.divemode != FREEDIVE) + ui.diveTimeText->setText(get_time_string_s(displayed_dive.duration.seconds + 30, 0, false)); + else + ui.diveTimeText->setText(get_time_string_s(displayed_dive.duration.seconds, 0, true)); + if (prevd) + ui.surfaceIntervalText->setText(get_time_string_s(displayed_dive.when - (prevd->when + prevd->duration.seconds), 4, + (displayed_dive.dc.divemode == FREEDIVE))); + else + ui.surfaceIntervalText->clear(); + if (mean[0]) + ui.sacText->setText(SACs); + else + ui.sacText->clear(); + if (displayed_dive.surface_pressure.mbar) + /* this is ALWAYS displayed in mbar */ + ui.airPressureText->setText(QString("%1mbar").arg(displayed_dive.surface_pressure.mbar)); + else + ui.airPressureText->clear(); + if (displayed_dive.salinity) + ui.salinityText->setText(QString("%1g/l").arg(displayed_dive.salinity / 10.0)); + else + ui.salinityText->clear(); + ui.depthLimits->setMaximum(get_depth_string(stats_selection.max_depth, true)); + ui.depthLimits->setMinimum(get_depth_string(stats_selection.min_depth, true)); + // the overall average depth is really confusing when listed between the + // deepest and shallowest dive - let's just not set it + // ui.depthLimits->setAverage(get_depth_string(stats_selection.avg_depth, true)); + ui.depthLimits->overrideMaxToolTipText(tr("Deepest dive")); + ui.depthLimits->overrideMinToolTipText(tr("Shallowest dive")); + if (amount_selected > 1 && stats_selection.max_sac.mliter) + ui.sacLimits->setMaximum(get_volume_string(stats_selection.max_sac, true).append(tr("/min"))); + else + ui.sacLimits->setMaximum(""); + if (amount_selected > 1 && stats_selection.min_sac.mliter) + ui.sacLimits->setMinimum(get_volume_string(stats_selection.min_sac, true).append(tr("/min"))); + else + ui.sacLimits->setMinimum(""); + if (stats_selection.avg_sac.mliter) + ui.sacLimits->setAverage(get_volume_string(stats_selection.avg_sac, true).append(tr("/min"))); + else + ui.sacLimits->setAverage(""); + ui.sacLimits->overrideMaxToolTipText(tr("Highest total SAC of a dive")); + ui.sacLimits->overrideMinToolTipText(tr("Lowest total SAC of a dive")); + ui.sacLimits->overrideAvgToolTipText(tr("Average total SAC of all selected dives")); + ui.divesAllText->setText(QString::number(stats_selection.selection_size)); + temp.mkelvin = stats_selection.max_temp; + ui.tempLimits->setMaximum(get_temperature_string(temp, true)); + temp.mkelvin = stats_selection.min_temp; + ui.tempLimits->setMinimum(get_temperature_string(temp, true)); + if (stats_selection.combined_temp && stats_selection.combined_count) { + const char *unit; + get_temp_units(0, &unit); + ui.tempLimits->setAverage(QString("%1%2").arg(stats_selection.combined_temp / stats_selection.combined_count, 0, 'f', 1).arg(unit)); + } + ui.tempLimits->overrideMaxToolTipText(tr("Highest temperature")); + ui.tempLimits->overrideMinToolTipText(tr("Lowest temperature")); + ui.tempLimits->overrideAvgToolTipText(tr("Average temperature of all selected dives")); + ui.totalTimeAllText->setText(get_time_string_s(stats_selection.total_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); + int seconds = stats_selection.total_time.seconds; + if (stats_selection.selection_size) + seconds /= stats_selection.selection_size; + ui.timeLimits->setAverage(get_time_string_s(seconds, 0,(displayed_dive.dc.divemode == FREEDIVE))); + if (amount_selected > 1) { + ui.timeLimits->setMaximum(get_time_string_s(stats_selection.longest_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); + ui.timeLimits->setMinimum(get_time_string_s(stats_selection.shortest_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); + } + ui.timeLimits->overrideMaxToolTipText(tr("Longest dive")); + ui.timeLimits->overrideMinToolTipText(tr("Shortest dive")); + ui.timeLimits->overrideAvgToolTipText(tr("Average length of all selected dives")); + // now let's get some gas use statistics + QVector > gasUsed; + QString gasUsedString; + volume_t vol; + selectedDivesGasUsed(gasUsed); + for (int j = 0; j < 20; j++) { + if (gasUsed.isEmpty()) + break; + QPair gasPair = gasUsed.last(); + gasUsed.pop_back(); + vol.mliter = gasPair.second; + gasUsedString.append(gasPair.first).append(": ").append(get_volume_string(vol, true)).append("\n"); + } + if (!gasUsed.isEmpty()) + gasUsedString.append("..."); + volume_t o2_tot = {}, he_tot = {}; + selected_dives_gas_parts(&o2_tot, &he_tot); + + /* No need to show the gas mixing information if diving + * with pure air, and only display the he / O2 part when + * it is used. + */ + if (he_tot.mliter || o2_tot.mliter) { + gasUsedString.append(tr("These gases could be\nmixed from Air and using:\n")); + if (he_tot.mliter) + gasUsedString.append(QString("He: %1").arg(get_volume_string(he_tot, true))); + if (he_tot.mliter && o2_tot.mliter) + gasUsedString.append(tr(" and ")); + if (o2_tot.mliter) + gasUsedString.append(QString("O2: %2\n").arg(get_volume_string(o2_tot, true))); + } + ui.gasConsumption->setText(gasUsedString); + if(ui.locationTags->text().isEmpty()) + ui.locationTags->hide(); + else + ui.locationTags->show(); + /* unset the special value text for date and time, just in case someone dove at midnight */ + ui.dateEdit->setSpecialValueText(QString("")); + ui.timeEdit->setSpecialValueText(QString("")); + + } else { + /* clear the fields */ + clearInfo(); + clearStats(); + clearEquipment(); + ui.rating->setCurrentStars(0); + ui.visibility->setCurrentStars(0); + ui.location->clear(); + /* set date and time to minimums which triggers showing the special value text */ + ui.dateEdit->setSpecialValueText(QString("-")); + ui.dateEdit->setMinimumDate(QDate(1, 1, 1)); + ui.dateEdit->setDate(QDate(1, 1, 1)); + ui.timeEdit->setSpecialValueText(QString("-")); + ui.timeEdit->setMinimumTime(QTime(0, 0, 0, 0)); + ui.timeEdit->setTime(QTime(0, 0, 0, 0)); + } + editMode = rememberEM; + ui.cylinders->view()->hideColumn(CylindersModel::DEPTH); + if (get_dive_dc(&displayed_dive, dc_number)->divemode == CCR) + ui.cylinders->view()->showColumn(CylindersModel::USE); + else + ui.cylinders->view()->hideColumn(CylindersModel::USE); + + if (verbose) + qDebug() << "Set the current dive site:" << displayed_dive.dive_site_uuid; + emit diveSiteChanged(get_dive_site_by_uuid(displayed_dive.dive_site_uuid)); +} + +void MainTab::addCylinder_clicked() +{ + if (editMode == NONE) + enableEdition(); + cylindersModel->add(); +} + +void MainTab::addWeight_clicked() +{ + if (editMode == NONE) + enableEdition(); + weightModel->add(); +} + +void MainTab::reload() +{ + suitModel.updateModel(); + buddyModel.updateModel(); + diveMasterModel.updateModel(); + tagModel.updateModel(); + LocationInformationModel::instance()->update(); +} + +// tricky little macro to edit all the selected dives +// loop over all dives, for each selected dive do WHAT, but do it +// last for the current dive; this is required in case the invocation +// wants to compare things to the original value in current_dive like it should +#define MODIFY_SELECTED_DIVES(WHAT) \ + do { \ + struct dive *mydive = NULL; \ + int _i; \ + for_each_dive (_i, mydive) { \ + if (!mydive->selected || mydive == cd) \ + continue; \ + \ + WHAT; \ + } \ + mydive = cd; \ + WHAT; \ + mark_divelist_changed(true); \ + } while (0) + +#define EDIT_TEXT(what) \ + if (same_string(mydive->what, cd->what) || copyPaste) { \ + free(mydive->what); \ + mydive->what = copy_string(displayed_dive.what); \ + } + +MainTab::EditMode MainTab::getEditMode() const +{ + return editMode; +} + +#define EDIT_VALUE(what) \ + if (mydive->what == cd->what || copyPaste) { \ + mydive->what = displayed_dive.what; \ + } + +void MainTab::refreshDisplayedDiveSite() +{ + if (displayed_dive_site.uuid) { + copy_dive_site(get_dive_site_by_uuid(displayed_dive_site.uuid), &displayed_dive_site); + ui.location->setCurrentDiveSiteUuid(displayed_dive_site.uuid); + } +} + +// when this is called we already have updated the current_dive and know that it exists +// there is no point in calling this function if there is no current dive +uint32_t MainTab::updateDiveSite(uint32_t pickedUuid, int divenr) +{ + struct dive *cd = get_dive(divenr); + if (!cd) + return 0; + + if (ui.location->text().isEmpty()) + return 0; + + if (pickedUuid == 0) + return 0; + + const uint32_t origUuid = cd->dive_site_uuid; + struct dive_site *origDs = get_dive_site_by_uuid(origUuid); + struct dive_site *newDs = NULL; + bool createdNewDive = false; + + if (pickedUuid == origUuid) + return origUuid; + + if (pickedUuid == RECENTLY_ADDED_DIVESITE) { + pickedUuid = create_dive_site(ui.location->text().isEmpty() ? qPrintable(tr("New dive site")) : qPrintable(ui.location->text()), displayed_dive.when); + createdNewDive = true; + } + + newDs = get_dive_site_by_uuid(pickedUuid); + + // Copy everything from the displayed_dive_site, so we have the latitude, longitude, notes, etc. + // The user *might* be using wrongly the 'choose dive site' just to edit the name of it, sigh. + if (origDs) { + if(createdNewDive) { + copy_dive_site(origDs, newDs); + free(newDs->name); + newDs->name = copy_string(qPrintable(ui.location->text().constData())); + newDs->uuid = pickedUuid; + qDebug() << "Creating and copying dive site"; + } else if (newDs->latitude.udeg == 0 && newDs->longitude.udeg == 0) { + newDs->latitude.udeg = origDs->latitude.udeg; + newDs->longitude.udeg = origDs->longitude.udeg; + qDebug() << "Copying GPS information"; + } + } + + if (origDs && pickedUuid != origDs->uuid && same_string(origDs->notes, "SubsurfaceWebservice")) { + if (!is_dive_site_used(origDs->uuid, false)) { + if (verbose) + qDebug() << "delete the autogenerated dive site" << origDs->name; + delete_dive_site(origDs->uuid); + } + } + + cd->dive_site_uuid = pickedUuid; + qDebug() << "Setting the dive site id on the dive:" << pickedUuid; + return pickedUuid; +} + +void MainTab::acceptChanges() +{ + int i, addedId = -1; + struct dive *d; + bool do_replot = false; + + if(ui.location->hasFocus()) { + this->setFocus(); + } + + acceptingEdit = true; + tabBar()->setTabIcon(0, QIcon()); // Notes + tabBar()->setTabIcon(1, QIcon()); // Equipment + ui.dateEdit->setEnabled(true); + hideMessage(); + ui.equipmentTab->setEnabled(true); + if (editMode == ADD) { + // We need to add the dive we just created to the dive list and select it. + // Easy, right? + struct dive *added_dive = clone_dive(&displayed_dive); + record_dive(added_dive); + addedId = added_dive->id; + // make sure that the dive site is handled as well + updateDiveSite(ui.location->currDiveSiteUuid(), get_idx_by_uniq_id(added_dive->id)); + + // unselect everything as far as the UI is concerned and select the new + // dive - we'll have to undo/redo this later after we resort the dive_table + // but we need the dive selected for the middle part of this function - this + // way we can reuse the code used for editing dives + MainWindow::instance()->dive_list()->unselectDives(); + selected_dive = get_divenr(added_dive); + amount_selected = 1; + } else if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { + /* now figure out if things have changed */ + if (displayedTrip.notes && !same_string(displayedTrip.notes, currentTrip->notes)) { + currentTrip->notes = copy_string(displayedTrip.notes); + mark_divelist_changed(true); + } + if (displayedTrip.location && !same_string(displayedTrip.location, currentTrip->location)) { + currentTrip->location = copy_string(displayedTrip.location); + mark_divelist_changed(true); + } + currentTrip = NULL; + ui.dateEdit->setEnabled(true); + } else { + if (editMode == MANUALLY_ADDED_DIVE) { + // preserve any changes to the profile + free(current_dive->dc.sample); + copy_samples(&displayed_dive.dc, ¤t_dive->dc); + addedId = displayed_dive.id; + } + struct dive *cd = current_dive; + struct divecomputer *displayed_dc = get_dive_dc(&displayed_dive, dc_number); + // now check if something has changed and if yes, edit the selected dives that + // were identical with the master dive shown (and mark the divelist as changed) + if (!same_string(displayed_dive.suit, cd->suit)) + MODIFY_SELECTED_DIVES(EDIT_TEXT(suit)); + if (!same_string(displayed_dive.notes, cd->notes)) + MODIFY_SELECTED_DIVES(EDIT_TEXT(notes)); + if (displayed_dive.rating != cd->rating) + MODIFY_SELECTED_DIVES(EDIT_VALUE(rating)); + if (displayed_dive.visibility != cd->visibility) + MODIFY_SELECTED_DIVES(EDIT_VALUE(visibility)); + if (displayed_dive.airtemp.mkelvin != cd->airtemp.mkelvin) + MODIFY_SELECTED_DIVES(EDIT_VALUE(airtemp.mkelvin)); + if (displayed_dc->divemode != current_dc->divemode) { + MODIFY_SELECTED_DIVES( + if (get_dive_dc(mydive, dc_number)->divemode == current_dc->divemode || copyPaste) { + get_dive_dc(mydive, dc_number)->divemode = displayed_dc->divemode; + } + ); + MODIFY_SELECTED_DIVES(update_setpoint_events(get_dive_dc(mydive, dc_number))); + do_replot = true; + } + if (displayed_dive.watertemp.mkelvin != cd->watertemp.mkelvin) + MODIFY_SELECTED_DIVES(EDIT_VALUE(watertemp.mkelvin)); + if (displayed_dive.when != cd->when) { + time_t offset = cd->when - displayed_dive.when; + MODIFY_SELECTED_DIVES(mydive->when -= offset;); + } + + if (displayed_dive.dive_site_uuid != cd->dive_site_uuid) + MODIFY_SELECTED_DIVES(EDIT_VALUE(dive_site_uuid)); + + // three text fields are somewhat special and are represented as tags + // in the UI - they need somewhat smarter handling + saveTaggedStrings(); + saveTags(); + + if (editMode != ADD && cylindersModel->changed) { + mark_divelist_changed(true); + MODIFY_SELECTED_DIVES( + for (int i = 0; i < MAX_CYLINDERS; i++) { + if (mydive != cd) { + if (same_string(mydive->cylinder[i].type.description, cd->cylinder[i].type.description) || copyPaste) { + // if we started out with the same cylinder description (for multi-edit) or if we do copt & paste + // make sure that we have the same cylinder type and copy the gasmix, but DON'T copy the start + // and end pressures (those are per dive after all) + if (!same_string(mydive->cylinder[i].type.description, displayed_dive.cylinder[i].type.description)) { + free((void*)mydive->cylinder[i].type.description); + mydive->cylinder[i].type.description = copy_string(displayed_dive.cylinder[i].type.description); + } + mydive->cylinder[i].type.size = displayed_dive.cylinder[i].type.size; + mydive->cylinder[i].type.workingpressure = displayed_dive.cylinder[i].type.workingpressure; + mydive->cylinder[i].gasmix = displayed_dive.cylinder[i].gasmix; + mydive->cylinder[i].cylinder_use = displayed_dive.cylinder[i].cylinder_use; + mydive->cylinder[i].depth = displayed_dive.cylinder[i].depth; + } + } + } + ); + for (int i = 0; i < MAX_CYLINDERS; i++) { + // copy the cylinder but make sure we have our own copy of the strings + free((void*)cd->cylinder[i].type.description); + cd->cylinder[i] = displayed_dive.cylinder[i]; + cd->cylinder[i].type.description = copy_string(displayed_dive.cylinder[i].type.description); + } + /* if cylinders changed we may have changed gas change events + * - so far this is ONLY supported for a single selected dive */ + struct divecomputer *tdc = ¤t_dive->dc; + struct divecomputer *sdc = &displayed_dive.dc; + while(tdc && sdc) { + free_events(tdc->events); + copy_events(sdc, tdc); + tdc = tdc->next; + sdc = sdc->next; + } + do_replot = true; + } + + if (weightModel->changed) { + mark_divelist_changed(true); + MODIFY_SELECTED_DIVES( + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + if (mydive != cd && (copyPaste || same_string(mydive->weightsystem[i].description, cd->weightsystem[i].description))) { + mydive->weightsystem[i] = displayed_dive.weightsystem[i]; + mydive->weightsystem[i].description = copy_string(displayed_dive.weightsystem[i].description); + } + } + ); + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + cd->weightsystem[i] = displayed_dive.weightsystem[i]; + cd->weightsystem[i].description = copy_string(displayed_dive.weightsystem[i].description); + } + } + + // update the dive site for the selected dives that had the same dive site as the current dive + uint32_t oldUuid = cd->dive_site_uuid; + uint32_t newUuid = 0; + MODIFY_SELECTED_DIVES( + if (mydive->dive_site_uuid == current_dive->dive_site_uuid) { + newUuid = updateDiveSite(newUuid == 0 ? ui.location->currDiveSiteUuid() : newUuid, get_idx_by_uniq_id(mydive->id)); + } + ); + if (!is_dive_site_used(oldUuid, false)) { + if (verbose) { + struct dive_site *ds = get_dive_site_by_uuid(oldUuid); + qDebug() << "delete now unused dive site" << ((ds && ds->name) ? ds->name : "without name"); + } + delete_dive_site(oldUuid); + GlobeGPS::instance()->reload(); + } + // the code above can change the correct uuid for the displayed dive site - and the + // code below triggers an update of the display without re-initializing displayed_dive + // so let's make sure here that our data is consistent now that we have handled the + // dive sites + displayed_dive.dive_site_uuid = current_dive->dive_site_uuid; + struct dive_site *ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); + if (ds) + copy_dive_site(ds, &displayed_dive_site); + + // each dive that was selected might have had the temperatures in its active divecomputer changed + // so re-populate the temperatures - easiest way to do this is by calling fixup_dive + for_each_dive (i, d) { + if (d->selected) + fixup_dive(d); + } + } + if (editMode != TRIP && current_dive->divetrip) { + current_dive->divetrip->when = current_dive->when; + find_new_trip_start_time(current_dive->divetrip); + } + if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { + // we just added or edited the dive, let fixup_dive() make + // sure we get the max depth right + current_dive->maxdepth.mm = current_dc->maxdepth.mm = 0; + fixup_dive(current_dive); + set_dive_nr_for_current_dive(); + MainWindow::instance()->showProfile(); + mark_divelist_changed(true); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); + } + int scrolledBy = MainWindow::instance()->dive_list()->verticalScrollBar()->sliderPosition(); + resetPallete(); + if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { + // since a newly added dive could be in the middle of the dive_table we need + // to resort the dive list and make sure the newly added dive gets selected again + sort_table(&dive_table); + MainWindow::instance()->dive_list()->reload(DiveTripModel::CURRENT, true); + int newDiveNr = get_divenr(get_dive_by_uniq_id(addedId)); + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDive(newDiveNr, true); + editMode = NONE; + MainWindow::instance()->refreshDisplay(); + MainWindow::instance()->graphics()->replot(); + emit addDiveFinished(); + } else { + editMode = NONE; + if (do_replot) + MainWindow::instance()->graphics()->replot(); + MainWindow::instance()->dive_list()->rememberSelection(); + sort_table(&dive_table); + MainWindow::instance()->refreshDisplay(); + MainWindow::instance()->dive_list()->restoreSelection(); + } + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); + MainWindow::instance()->dive_list()->verticalScrollBar()->setSliderPosition(scrolledBy); + MainWindow::instance()->dive_list()->setFocus(); + cylindersModel->changed = false; + weightModel->changed = false; + MainWindow::instance()->setEnabledToolbar(true); + acceptingEdit = false; + ui.editDiveSiteButton->setEnabled(true); +} + +void MainTab::resetPallete() +{ + QPalette p; + ui.buddy->setPalette(p); + ui.notes->setPalette(p); + ui.location->setPalette(p); + ui.divemaster->setPalette(p); + ui.suit->setPalette(p); + ui.airtemp->setPalette(p); + ui.DiveType->setPalette(p); + ui.watertemp->setPalette(p); + ui.dateEdit->setPalette(p); + ui.timeEdit->setPalette(p); + ui.tagWidget->setPalette(p); + ui.diveTripLocation->setPalette(p); +} + +#define EDIT_TEXT2(what, text) \ + textByteArray = text.toUtf8(); \ + free(what); \ + what = strdup(textByteArray.data()); + +#define FREE_IF_DIFFERENT(what) \ + if (displayed_dive.what != cd->what) \ + free(displayed_dive.what) + +void MainTab::rejectChanges() +{ + EditMode lastMode = editMode; + + if (lastMode != NONE && current_dive && + (modified || + memcmp(¤t_dive->cylinder[0], &displayed_dive.cylinder[0], sizeof(cylinder_t) * MAX_CYLINDERS) || + memcmp(¤t_dive->cylinder[0], &displayed_dive.weightsystem[0], sizeof(weightsystem_t) * MAX_WEIGHTSYSTEMS))) { + if (QMessageBox::warning(MainWindow::instance(), TITLE_OR_TEXT(tr("Discard the changes?"), + tr("You are about to discard your changes.")), + QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Discard) != QMessageBox::Discard) { + return; + } + } + ui.dateEdit->setEnabled(true); + editMode = NONE; + tabBar()->setTabIcon(0, QIcon()); // Notes + tabBar()->setTabIcon(1, QIcon()); // Equipment + hideMessage(); + resetPallete(); + // no harm done to call cancelPlan even if we were not in ADD or PLAN mode... + DivePlannerPointsModel::instance()->cancelPlan(); + if(lastMode == ADD) + MainWindow::instance()->dive_list()->restoreSelection(); + + // now make sure that the correct dive is displayed + if (selected_dive >= 0) + copy_dive(current_dive, &displayed_dive); + else + clear_dive(&displayed_dive); + updateDiveInfo(selected_dive < 0); + DivePictureModel::instance()->updateDivePictures(); + // the user could have edited the location and then canceled the edit + // let's get the correct location back in view +#ifndef NO_MARBLE + GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(displayed_dive.dive_site_uuid)); +#endif + // show the profile and dive info + MainWindow::instance()->graphics()->replot(); + MainWindow::instance()->setEnabledToolbar(true); + cylindersModel->changed = false; + weightModel->changed = false; + cylindersModel->updateDive(); + weightModel->updateDive(); + extraDataModel->updateDive(); + ui.editDiveSiteButton->setEnabled(true); +} +#undef EDIT_TEXT2 + +void MainTab::markChangedWidget(QWidget *w) +{ + QPalette p; + qreal h, s, l, a; + enableEdition(); + qApp->palette().color(QPalette::Text).getHslF(&h, &s, &l, &a); + p.setBrush(QPalette::Base, (l <= 0.3) ? QColor(Qt::yellow).lighter() : (l <= 0.6) ? QColor(Qt::yellow).light() : /* else */ QColor(Qt::yellow).darker(300)); + w->setPalette(p); + modified = true; +} + +void MainTab::on_buddy_textChanged() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + + if (same_string(displayed_dive.buddy, ui.buddy->toPlainText().toUtf8().data())) + return; + + QStringList text_list = ui.buddy->toPlainText().split(",", QString::SkipEmptyParts); + for (int i = 0; i < text_list.size(); i++) + text_list[i] = text_list[i].trimmed(); + QString text = text_list.join(", "); + free(displayed_dive.buddy); + displayed_dive.buddy = strdup(text.toUtf8().data()); + markChangedWidget(ui.buddy); +} + +void MainTab::on_divemaster_textChanged() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + + if (same_string(displayed_dive.divemaster, ui.divemaster->toPlainText().toUtf8().data())) + return; + + QStringList text_list = ui.divemaster->toPlainText().split(",", QString::SkipEmptyParts); + for (int i = 0; i < text_list.size(); i++) + text_list[i] = text_list[i].trimmed(); + QString text = text_list.join(", "); + free(displayed_dive.divemaster); + displayed_dive.divemaster = strdup(text.toUtf8().data()); + markChangedWidget(ui.divemaster); +} + +void MainTab::on_airtemp_textChanged(const QString &text) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + displayed_dive.airtemp.mkelvin = parseTemperatureToMkelvin(text); + markChangedWidget(ui.airtemp); + validate_temp_field(ui.airtemp, text); +} + +void MainTab::divetype_Changed(int index) +{ + if (editMode == IGNORE) + return; + struct divecomputer *displayed_dc = get_dive_dc(&displayed_dive, dc_number); + displayed_dc->divemode = (enum dive_comp_type) index; + update_setpoint_events(displayed_dc); + markChangedWidget(ui.DiveType); + MainWindow::instance()->graphics()->recalcCeiling(); +} + +void MainTab::on_watertemp_textChanged(const QString &text) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + displayed_dive.watertemp.mkelvin = parseTemperatureToMkelvin(text); + markChangedWidget(ui.watertemp); + validate_temp_field(ui.watertemp, text); +} + +void MainTab::validate_temp_field(QLineEdit *tempField, const QString &text) +{ + static bool missing_unit = false; + static bool missing_precision = false; + if (!text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.][0-9]+){0,1}(°[CF]){0,1}$")) && + !text.isEmpty() && + !text.contains(QRegExp("^[-+]$"))) { + if (text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.][0-9]+){0,1}(°)$")) && !missing_unit) { + if (!missing_unit) { + missing_unit = true; + return; + } + } + if (text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.]){0,1}(°[CF]){0,1}$")) && !missing_precision) { + if (!missing_precision) { + missing_precision = true; + return; + } + } + QPalette p; + p.setBrush(QPalette::Base, QColor(Qt::red).lighter()); + tempField->setPalette(p); + } else { + missing_unit = false; + missing_precision = false; + } +} + +void MainTab::on_dateEdit_dateChanged(const QDate &date) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + markChangedWidget(ui.dateEdit); + QDateTime dateTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + dateTime.setTimeSpec(Qt::UTC); + dateTime.setDate(date); + DivePlannerPointsModel::instance()->getDiveplan().when = displayed_dive.when = dateTime.toTime_t(); + emit dateTimeChanged(); +} + +void MainTab::on_timeEdit_timeChanged(const QTime &time) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + markChangedWidget(ui.timeEdit); + QDateTime dateTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + dateTime.setTimeSpec(Qt::UTC); + dateTime.setTime(time); + DivePlannerPointsModel::instance()->getDiveplan().when = displayed_dive.when = dateTime.toTime_t(); + emit dateTimeChanged(); +} + +// changing the tags on multiple dives is semantically strange - what's the right thing to do? +// here's what I think... add the tags that were added to the displayed dive and remove the tags +// that were removed from it +void MainTab::saveTags() +{ + struct dive *cd = current_dive; + struct tag_entry *added_list = NULL; + struct tag_entry *removed_list = NULL; + struct tag_entry *tl; + + taglist_free(displayed_dive.tag_list); + displayed_dive.tag_list = NULL; + Q_FOREACH (const QString& tag, ui.tagWidget->getBlockStringList()) + taglist_add_tag(&displayed_dive.tag_list, tag.toUtf8().data()); + taglist_cleanup(&displayed_dive.tag_list); + + // figure out which tags were added and which tags were removed + added_list = taglist_added(cd->tag_list, displayed_dive.tag_list); + removed_list = taglist_added(displayed_dive.tag_list, cd->tag_list); + // dump_taglist("added tags:", added_list); + // dump_taglist("removed tags:", removed_list); + + // we need to check if the tags were changed before just overwriting them + if (added_list == NULL && removed_list == NULL) + return; + + MODIFY_SELECTED_DIVES( + // create a new tag list and all the existing tags that were not + // removed and then all the added tags + struct tag_entry *new_tag_list; + new_tag_list = NULL; + tl = mydive->tag_list; + while (tl) { + if (!taglist_contains(removed_list, tl->tag->name)) + taglist_add_tag(&new_tag_list, tl->tag->name); + tl = tl->next; + } + tl = added_list; + while (tl) { + taglist_add_tag(&new_tag_list, tl->tag->name); + tl = tl->next; + } + taglist_free(mydive->tag_list); + mydive->tag_list = new_tag_list; + ); + taglist_free(added_list); + taglist_free(removed_list); +} + +// buddy and divemaster are represented in the UI just like the tags, but the internal +// representation is just a string (with commas as delimiters). So we need to do the same +// thing we did for tags, just differently +void MainTab::saveTaggedStrings() +{ + QStringList addedList, removedList; + struct dive *cd = current_dive; + + diffTaggedStrings(cd->buddy, displayed_dive.buddy, addedList, removedList); + MODIFY_SELECTED_DIVES( + QStringList oldList = QString(mydive->buddy).split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); + QString newString; + QString comma; + Q_FOREACH (const QString tag, oldList) { + if (!removedList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + Q_FOREACH (const QString tag, addedList) { + if (!oldList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + free(mydive->buddy); + mydive->buddy = copy_string(qPrintable(newString)); + ); + addedList.clear(); + removedList.clear(); + diffTaggedStrings(cd->divemaster, displayed_dive.divemaster, addedList, removedList); + MODIFY_SELECTED_DIVES( + QStringList oldList = QString(mydive->divemaster).split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); + QString newString; + QString comma; + Q_FOREACH (const QString tag, oldList) { + if (!removedList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + Q_FOREACH (const QString tag, addedList) { + if (!oldList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + free(mydive->divemaster); + mydive->divemaster = copy_string(qPrintable(newString)); + ); +} + +void MainTab::diffTaggedStrings(QString currentString, QString displayedString, QStringList &addedList, QStringList &removedList) +{ + QStringList displayedList, currentList; + currentList = currentString.split(',', QString::SkipEmptyParts); + displayedList = displayedString.split(',', QString::SkipEmptyParts); + Q_FOREACH ( const QString tag, currentList) { + if (!displayedList.contains(tag, Qt::CaseInsensitive)) + removedList << tag.trimmed(); + } + Q_FOREACH (const QString tag, displayedList) { + if (!currentList.contains(tag, Qt::CaseInsensitive)) + addedList << tag.trimmed(); + } +} + +void MainTab::on_tagWidget_textChanged() +{ + char buf[1024]; + + if (editMode == IGNORE || acceptingEdit == true) + return; + + taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); + if (same_string(buf, ui.tagWidget->toPlainText().toUtf8().data())) + return; + + markChangedWidget(ui.tagWidget); +} + +void MainTab::on_location_textChanged() +{ + if (editMode == IGNORE) + return; + + // we don't want to act on the edit until editing is finished, + // but we want to mark the field so it's obvious it is being edited + QString currentLocation; + struct dive_site *ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); + if (ds) + currentLocation = ds->name; + if (ui.location->text() != currentLocation) + markChangedWidget(ui.location); +} + +void MainTab::on_location_diveSiteSelected() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + + if (ui.location->text().isEmpty()) { + displayed_dive.dive_site_uuid = 0; + markChangedWidget(ui.location); + emit diveSiteChanged(0); + return; + } else { + if (ui.location->currDiveSiteUuid() != displayed_dive.dive_site_uuid) { + markChangedWidget(ui.location); + } else { + QPalette p; + ui.location->setPalette(p); + } + } +} + +void MainTab::on_diveTripLocation_textEdited(const QString& text) +{ + if (currentTrip) { + free(displayedTrip.location); + displayedTrip.location = strdup(qPrintable(text)); + markChangedWidget(ui.diveTripLocation); + } +} + +void MainTab::on_suit_textChanged(const QString &text) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + free(displayed_dive.suit); + displayed_dive.suit = strdup(text.toUtf8().data()); + markChangedWidget(ui.suit); +} + +void MainTab::on_notes_textChanged() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + if (currentTrip) { + if (same_string(displayedTrip.notes, ui.notes->toPlainText().toUtf8().data())) + return; + free(displayedTrip.notes); + displayedTrip.notes = strdup(ui.notes->toPlainText().toUtf8().data()); + } else { + if (same_string(displayed_dive.notes, ui.notes->toPlainText().toUtf8().data())) + return; + free(displayed_dive.notes); + if (ui.notes->toHtml().indexOf("toHtml().toUtf8().data()); + else + displayed_dive.notes = strdup(ui.notes->toPlainText().toUtf8().data()); + } + markChangedWidget(ui.notes); +} + +void MainTab::on_rating_valueChanged(int value) +{ + if (acceptingEdit == true) + return; + if (displayed_dive.rating != value) { + displayed_dive.rating = value; + modified = true; + enableEdition(); + } +} + +void MainTab::on_visibility_valueChanged(int value) +{ + if (acceptingEdit == true) + return; + if (displayed_dive.visibility != value) { + displayed_dive.visibility = value; + modified = true; + enableEdition(); + } +} + +#undef MODIFY_SELECTED_DIVES +#undef EDIT_TEXT +#undef EDIT_VALUE + +void MainTab::editCylinderWidget(const QModelIndex &index) +{ + // we need a local copy or bad things happen when enableEdition() is called + QModelIndex editIndex = index; + if (cylindersModel->changed && editMode == NONE) { + enableEdition(); + return; + } + if (editIndex.isValid() && editIndex.column() != CylindersModel::REMOVE) { + if (editMode == NONE) + enableEdition(); + ui.cylinders->edit(editIndex); + } +} + +void MainTab::editWeightWidget(const QModelIndex &index) +{ + if (editMode == NONE) + enableEdition(); + + if (index.isValid() && index.column() != WeightModel::REMOVE) + ui.weights->edit(index); +} + +void MainTab::escDetected() +{ + if (editMode != NONE) + rejectChanges(); +} + +void MainTab::photoDoubleClicked(const QString filePath) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); +} + +void MainTab::removeSelectedPhotos() +{ + if (!ui.photosView->selectionModel()->hasSelection()) + return; + + QModelIndex photoIndex = ui.photosView->selectionModel()->selectedIndexes().first(); + QString fileUrl = photoIndex.data(Qt::DisplayPropertyRole).toString(); + DivePictureModel::instance()->removePicture(fileUrl); +} + +#define SHOW_SELECTIVE(_component) \ + if (what._component) \ + ui._component->setText(displayed_dive._component); + +void MainTab::showAndTriggerEditSelective(struct dive_components what) +{ + // take the data in our copyPasteDive and apply it to selected dives + enableEdition(); + copyPaste = true; + SHOW_SELECTIVE(buddy); + SHOW_SELECTIVE(divemaster); + SHOW_SELECTIVE(suit); + if (what.notes) { + QString tmp(displayed_dive.notes); + if (tmp.contains("setHtml(tmp); + else + ui.notes->setPlainText(tmp); + } + if (what.rating) + ui.rating->setCurrentStars(displayed_dive.rating); + if (what.visibility) + ui.visibility->setCurrentStars(displayed_dive.visibility); + if (what.divesite) + ui.location->setCurrentDiveSiteUuid(displayed_dive.dive_site_uuid); + if (what.tags) { + char buf[1024]; + taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); + ui.tagWidget->setText(QString(buf)); + } + if (what.cylinders) { + cylindersModel->updateDive(); + cylindersModel->changed = true; + } + if (what.weights) { + weightModel->updateDive(); + weightModel->changed = true; + } +} diff --git a/desktop-widgets/maintab.h b/desktop-widgets/maintab.h new file mode 100644 index 000000000..20b4da690 --- /dev/null +++ b/desktop-widgets/maintab.h @@ -0,0 +1,129 @@ +/* + * maintab.h + * + * header file for the main tab of Subsurface + * + */ +#ifndef MAINTAB_H +#define MAINTAB_H + +#include +#include +#include +#include + +#include "ui_maintab.h" +#include "completionmodels.h" +#include "divelocationmodel.h" +#include "dive.h" + +class WeightModel; +class CylindersModel; +class ExtraDataModel; +class DivePictureModel; +class QCompleter; + +struct Completers { + QCompleter *divemaster; + QCompleter *buddy; + QCompleter *suit; + QCompleter *tags; +}; + +class MainTab : public QTabWidget { + Q_OBJECT +public: + enum EditMode { + NONE, + DIVE, + TRIP, + ADD, + MANUALLY_ADDED_DIVE, + IGNORE + }; + + MainTab(QWidget *parent = 0); + ~MainTab(); + void clearStats(); + void clearInfo(); + void clearEquipment(); + void reload(); + void initialUiSetup(); + bool isEditing(); + void updateCoordinatesText(qreal lat, qreal lon); + void refreshDisplayedDiveSite(); + void nextInputField(QKeyEvent *event); + void showAndTriggerEditSelective(struct dive_components what); + +signals: + void addDiveFinished(); + void dateTimeChanged(); + void diveSiteChanged(struct dive_site * ds); +public +slots: + void addCylinder_clicked(); + void addWeight_clicked(); + void refreshDiveInfo(); + void updateDiveInfo(bool clear = false); + void acceptChanges(); + void rejectChanges(); + void on_location_diveSiteSelected(); + void on_location_textChanged(); + void on_divemaster_textChanged(); + void on_buddy_textChanged(); + void on_suit_textChanged(const QString &text); + void on_diveTripLocation_textEdited(const QString& text); + void on_notes_textChanged(); + void on_airtemp_textChanged(const QString &text); + void divetype_Changed(int); + void on_watertemp_textChanged(const QString &text); + void validate_temp_field(QLineEdit *tempField, const QString &text); + void on_dateEdit_dateChanged(const QDate &date); + void on_timeEdit_timeChanged(const QTime & time); + void on_rating_valueChanged(int value); + void on_visibility_valueChanged(int value); + void on_tagWidget_textChanged(); + void editCylinderWidget(const QModelIndex &index); + void editWeightWidget(const QModelIndex &index); + void addDiveStarted(); + void addMessageAction(QAction *action); + void hideMessage(); + void closeMessage(); + void displayMessage(QString str); + void enableEdition(EditMode newEditMode = NONE); + void toggleTriggeredColumn(); + void updateTextLabels(bool showUnits = true); + void escDetected(void); + void photoDoubleClicked(const QString filePath); + void removeSelectedPhotos(); + void showLocation(); + void enableGeoLookupEdition(); + void disableGeoLookupEdition(); + void setCurrentLocationIndex(); + EditMode getEditMode() const; +private: + Ui::MainTab ui; + WeightModel *weightModel; + CylindersModel *cylindersModel; + ExtraDataModel *extraDataModel; + EditMode editMode; + BuddyCompletionModel buddyModel; + DiveMasterCompletionModel diveMasterModel; + SuitCompletionModel suitModel; + TagCompletionModel tagModel; + DivePictureModel *divePictureModel; + Completers completers; + bool modified; + bool copyPaste; + void resetPallete(); + void saveTags(); + void saveTaggedStrings(); + void diffTaggedStrings(QString currentString, QString displayedString, QStringList &addedList, QStringList &removedList); + void markChangedWidget(QWidget *w); + dive_trip_t *currentTrip; + dive_trip_t displayedTrip; + bool acceptingEdit; + uint32_t updateDiveSite(uint32_t pickedUuid, int divenr); +}; + +#endif // MAINTAB_H diff --git a/desktop-widgets/maintab.ui b/desktop-widgets/maintab.ui new file mode 100644 index 000000000..7bc516b1a --- /dev/null +++ b/desktop-widgets/maintab.ui @@ -0,0 +1,1267 @@ + + + MainTab + + + + 0 + 0 + 463 + 815 + + + + 0 + + + + Notes + + + General notes about the current selection + + + + 5 + + + 5 + + + 5 + + + 5 + + + 0 + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 445 + 726 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 5 + + + 5 + + + 8 + + + 0 + + + + + Date + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + Time + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + Air temp. + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + Water temp. + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + true + + + Qt::UTC + + + + + + + + 0 + 0 + + + + Qt::UTC + + + + + + + false + + + + + + + false + + + + + + + + + 0 + + + 5 + + + 5 + + + + + + + Location + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + + + Qt::RichText + + + + + + + + + 2 + + + + + + + + Edit dive site + + + ... + + + + :/geocode:/geocode + + + + + + + + + + + + + + + + + 5 + + + 5 + + + 5 + + + 0 + + + + + Divemaster + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + Buddy + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + false + + + + + + + false + + + + + + + + + 5 + + + 5 + + + 5 + + + 0 + + + + + + 0 + 0 + + + + Rating + + + + + + + + 0 + 0 + + + + Visibility + + + + + + + Suit + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + + 0 + 0 + + + + false + + + + + + + + + 5 + + + 0 + + + + + + + + Tags + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + Dive mode + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QPlainTextEdit::NoWrap + + + + + + + + + 0 + + + + + Notes + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + 0 + + + + + false + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + + Equipment + + + Used equipment in the current selection + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 445 + 754 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + + + + + + + + + + + + + Info + + + Dive information + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 287 + 320 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + + + Date + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Interval + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Gases used + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Gas consumed + + + + + + + + + Qt::AlignCenter + + + + + + + + + + SAC + + + + + + + + + Qt::AlignCenter + + + + + + + + + + CNS + + + + + + + + + Qt::AlignCenter + + + + + + + + + + OTU + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Max. depth + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Avg. depth + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Air pressure + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Air temp. + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Water temp. + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Dive time + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Salinity + + + + + + Qt::AlignCenter + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + + + + + + + + Stats + + + Simple statistics about the selection + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 297 + 187 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Depth + + + + + + + + + + + + Duration + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Temperature + + + + + + + + + + + + Total time + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Dives + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + SAC + + + + + + + + + + + + Gas consumption + + + + + + + + + Qt::AlignCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + Photos + + + All photos from the current selection + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + QListView::IconMode + + + + + + + + Extra data + + + Adittional data from the dive computer + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + + + KMessageWidget + QFrame +
kmessagewidget.h
+ 1 +
+ + StarWidget + QWidget +
starwidget.h
+ 1 +
+ + MinMaxAvgWidget + QWidget +
simplewidgets.h
+ 1 +
+ + TableView + QWidget +
tableview.h
+ 1 +
+ + TagWidget + QPlainTextEdit +
tagwidget.h
+
+ + DivePictureWidget + QListView +
divepicturewidget.h
+
+ + QtWaitingSpinner + QWidget +
qtwaitingspinner.h
+ 1 +
+ + DiveLocationLineEdit + QLineEdit +
locationinformation.h
+
+
+ + dateEdit + timeEdit + airtemp + watertemp + divemaster + buddy + rating + visibility + suit + notes + + + + + +
diff --git a/desktop-widgets/mainwindow.cpp b/desktop-widgets/mainwindow.cpp new file mode 100644 index 000000000..e1e0d81a2 --- /dev/null +++ b/desktop-widgets/mainwindow.cpp @@ -0,0 +1,1923 @@ +/* + * mainwindow.cpp + * + * classes for the main UI window in Subsurface + */ +#include "mainwindow.h" + +#include +#include +#include +#include +#include +#include +#include "version.h" +#include "divelistview.h" +#include "downloadfromdivecomputer.h" +#include "preferences.h" +#include "subsurfacewebservices.h" +#include "divecomputermanagementdialog.h" +#include "about.h" +#include "updatemanager.h" +#include "planner.h" +#include "filtermodels.h" +#include "profile/profilewidget2.h" +#include "globe.h" +#include "divecomputer.h" +#include "maintab.h" +#include "diveplanner.h" +#ifndef NO_PRINTING +#include +#include "printdialog.h" +#endif +#include "tankinfomodel.h" +#include "weigthsysteminfomodel.h" +#include "yearlystatisticsmodel.h" +#include "diveplannermodel.h" +#include "divelogimportdialog.h" +#include "divelogexportdialog.h" +#include "usersurvey.h" +#include "divesitehelpers.h" +#include "windowtitleupdate.h" +#include "locationinformation.h" + +#ifndef NO_USERMANUAL +#include "usermanual.h" +#endif +#include "divepicturemodel.h" +#include "git-access.h" +#include +#include +#include +#include +#include "subsurface-core/color.h" + +#if defined(FBSUPPORT) +#include "socialnetworks.h" +#endif + +QProgressDialog *progressDialog = NULL; +bool progressDialogCanceled = false; + +extern "C" int updateProgress(int percent) +{ + if (progressDialog) + progressDialog->setValue(percent); + return progressDialogCanceled; +} + +MainWindow *MainWindow::m_Instance = NULL; + +MainWindow::MainWindow() : QMainWindow(), + actionNextDive(0), + actionPreviousDive(0), + helpView(0), + state(VIEWALL), + survey(0) +{ + Q_ASSERT_X(m_Instance == NULL, "MainWindow", "MainWindow recreated!"); + m_Instance = this; + ui.setupUi(this); + read_hashes(); + // Define the States of the Application Here, Currently the states are situations where the different + // widgets will change on the mainwindow. + + // for the "default" mode + MainTab *mainTab = new MainTab(); + DiveListView *diveListView = new DiveListView(); + ProfileWidget2 *profileWidget = new ProfileWidget2(); + +#ifndef NO_MARBLE + GlobeGPS *globeGps = GlobeGPS::instance(); +#else + QWidget *globeGps = NULL; +#endif + + PlannerSettingsWidget *plannerSettings = new PlannerSettingsWidget(); + DivePlannerWidget *plannerWidget = new DivePlannerWidget(); + PlannerDetails *plannerDetails = new PlannerDetails(); + + // what is a sane order for those icons? we should have the ones the user is + // most likely to want towards the top so they are always visible + // and the ones that someone likely sets and then never touches again towards the bottom + profileToolbarActions << ui.profCalcCeiling << ui.profCalcAllTissues << // start with various ceilings + ui.profIncrement3m << ui.profDcCeiling << + ui.profPhe << ui.profPn2 << ui.profPO2 << // partial pressure graphs + ui.profRuler << ui.profScaled << // measuring and scaling + ui.profTogglePicture << ui.profTankbar << + ui.profMod << ui.profNdl_tts << // various values that a user is either interested in or not + ui.profEad << ui.profSAC << + ui.profHR << // very few dive computers support this + ui.profTissues; // maybe less frequently used + + QToolBar *toolBar = new QToolBar(); + Q_FOREACH (QAction *a, profileToolbarActions) + toolBar->addAction(a); + toolBar->setOrientation(Qt::Vertical); + toolBar->setIconSize(QSize(24,24)); + QWidget *profileContainer = new QWidget(); + QHBoxLayout *profLayout = new QHBoxLayout(); + profLayout->setSpacing(0); + profLayout->setMargin(0); + profLayout->setContentsMargins(0,0,0,0); + profLayout->addWidget(toolBar); + profLayout->addWidget(profileWidget); + profileContainer->setLayout(profLayout); + + LocationInformationWidget * diveSiteEdit = new LocationInformationWidget(); + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + this, &MainWindow::setDefaultState); + + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + mainTab, &MainTab::refreshDiveInfo); + + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + mainTab, &MainTab::refreshDisplayedDiveSite); + + QWidget *diveSitePictures = new QWidget(); // Placeholder + + std::pair enabled = std::make_pair("enabled", QVariant(true)); + std::pair disabled = std::make_pair("enabled", QVariant(false)); + PropertyList enabledList; + PropertyList disabledList; + enabledList.push_back(enabled); + disabledList.push_back(disabled); + + registerApplicationState("Default", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("AddDive", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("EditDive", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("PlanDive", plannerWidget, profileContainer, plannerSettings, plannerDetails ); + registerApplicationState("EditPlannedDive", plannerWidget, profileContainer, diveListView, globeGps ); + registerApplicationState("EditDiveSite", diveSiteEdit, profileContainer, diveListView, globeGps); + + setStateProperties("Default", enabledList, enabledList, enabledList,enabledList); + setStateProperties("AddDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("PlanDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditPlannedDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditDiveSite", enabledList, disabledList, disabledList, enabledList); + + setApplicationState("Default"); + + ui.multiFilter->hide(); + + setWindowIcon(QIcon(":subsurface-icon")); + if (!QIcon::hasThemeIcon("window-close")) { + QIcon::setThemeName("subsurface"); + } + connect(dive_list(), SIGNAL(currentDiveChanged(int)), this, SLOT(current_dive_changed(int))); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(readSettings())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(update())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(reloadHeaderActions())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), information(), SLOT(updateDiveInfo())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerWidget(), SLOT(settingsChanged())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerSettingsWidget(), SLOT(settingsChanged())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), TankInfoModel::instance(), SLOT(update())); + connect(ui.actionRecent1, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent2, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent3, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent4, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(information(), SIGNAL(addDiveFinished()), graphics(), SLOT(setProfileState())); + connect(DivePlannerPointsModel::instance(), SIGNAL(planCreated()), this, SLOT(planCreated())); + connect(DivePlannerPointsModel::instance(), SIGNAL(planCanceled()), this, SLOT(planCanceled())); + connect(plannerDetails->printPlan(), SIGNAL(pressed()), divePlannerWidget(), SLOT(printDecoPlan())); + connect(this, SIGNAL(startDiveSiteEdit()), this, SLOT(on_actionDiveSiteEdit_triggered())); + +#ifndef NO_MARBLE + connect(information(), SIGNAL(diveSiteChanged(struct dive_site *)), globeGps, SLOT(centerOnDiveSite(struct dive_site *))); +#endif + wtu = new WindowTitleUpdate(); + connect(WindowTitleUpdate::instance(), SIGNAL(updateTitle()), this, SLOT(setAutomaticTitle())); +#ifdef NO_PRINTING + plannerDetails->printPlan()->hide(); + ui.menuFile->removeAction(ui.actionPrint); +#endif +#ifndef USE_LIBGIT23_API + ui.menuFile->removeAction(ui.actionCloudstorageopen); + ui.menuFile->removeAction(ui.actionCloudstoragesave); + qDebug() << "disabled / made invisible the cloud storage stuff"; +#else + enableDisableCloudActions(); +#endif + + ui.mainErrorMessage->hide(); + graphics()->setEmptyState(); + initialUiSetup(); + readSettings(); + diveListView->reload(DiveTripModel::TREE); + diveListView->reloadHeaderActions(); + diveListView->setFocus(); + GlobeGPS::instance()->reload(); + diveListView->expand(dive_list()->model()->index(0, 0)); + diveListView->scrollTo(dive_list()->model()->index(0, 0), QAbstractItemView::PositionAtCenter); + divePlannerWidget()->settingsChanged(); + divePlannerSettingsWidget()->settingsChanged(); +#ifdef NO_MARBLE + ui.menuView->removeAction(ui.actionViewGlobe); +#endif +#ifdef NO_USERMANUAL + ui.menuHelp->removeAction(ui.actionUserManual); +#endif + memset(©PasteDive, 0, sizeof(copyPasteDive)); + memset(&what, 0, sizeof(what)); + + updateManager = new UpdateManager(this); + undoStack = new QUndoStack(this); + QAction *undoAction = undoStack->createUndoAction(this, tr("&Undo")); + QAction *redoAction = undoStack->createRedoAction(this, tr("&Redo")); + undoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Z)); + redoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z)); + QListundoRedoActions; + undoRedoActions.append(undoAction); + undoRedoActions.append(redoAction); + ui.menu_Edit->addActions(undoRedoActions); + + ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); + connect(geoLookup, SIGNAL(started()),information(), SLOT(disableGeoLookupEdition())); + connect(geoLookup, SIGNAL(finished()), information(), SLOT(enableGeoLookupEdition())); +#ifndef NO_PRINTING + // copy the bundled print templates to the user path; no overwriting occurs! + copyPath(getPrintingTemplatePathBundle(), getPrintingTemplatePathUser()); + find_all_templates(); +#endif + +#if defined(FBSUPPORT) + FacebookManager *fb = FacebookManager::instance(); + connect(fb, SIGNAL(justLoggedIn(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); + connect(fb, SIGNAL(justLoggedOut(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); + connect(ui.actionFacebook, SIGNAL(triggered(bool)), fb, SLOT(sendDive())); + ui.actionFacebook->setEnabled(fb->loggedIn()); +#else + ui.actionFacebook->setEnabled(false); +#endif + + + ui.menubar->show(); + set_git_update_cb(&updateProgress); +} + +MainWindow::~MainWindow() +{ + write_hashes(); + m_Instance = NULL; +} + +void MainWindow::setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl, const PropertyList& br) +{ + stateProperties[state] = PropertiesForQuadrant(tl, tr, bl, br); +} + +void MainWindow::on_actionDiveSiteEdit_triggered() { + setApplicationState("EditDiveSite"); +} + +void MainWindow::enableDisableCloudActions() +{ +#ifdef USE_LIBGIT23_API + ui.actionCloudstorageopen->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.actionCloudstoragesave->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); +#endif +} + +PlannerDetails *MainWindow::plannerDetails() const { + return qobject_cast(applicationState["PlanDive"].bottomRight); +} + +PlannerSettingsWidget *MainWindow::divePlannerSettingsWidget() { + return qobject_cast(applicationState["PlanDive"].bottomLeft); +} + +void MainWindow::setDefaultState() { + setApplicationState("Default"); + if (information()->getEditMode() != MainTab::NONE) { + ui.bottomLeft->currentWidget()->setEnabled(false); + } +} + +void MainWindow::setLoadedWithFiles(bool f) +{ + filesAsArguments = f; +} + +bool MainWindow::filesFromCommandLine() const +{ + return filesAsArguments; +} + +MainWindow *MainWindow::instance() +{ + return m_Instance; +} + +// this gets called after we download dives from a divecomputer +void MainWindow::refreshDisplay(bool doRecreateDiveList) +{ + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + information()->reload(); + TankInfoModel::instance()->update(); + GlobeGPS::instance()->reload(); + if (doRecreateDiveList) + recreateDiveList(); + + setApplicationState("Default"); + dive_list()->setEnabled(true); + dive_list()->setFocus(); + WSInfoModel::instance()->updateInfo(); + if (amount_selected == 0) + cleanUpEmpty(); +} + +void MainWindow::recreateDiveList() +{ + dive_list()->reload(DiveTripModel::CURRENT); + TagFilterModel::instance()->repopulate(); + BuddyFilterModel::instance()->repopulate(); + LocationFilterModel::instance()->repopulate(); + SuitsFilterModel::instance()->repopulate(); +} + +void MainWindow::configureToolbar() { + if (selected_dive>0) { + if (current_dive->dc.divemode == FREEDIVE) { + ui.profCalcCeiling->setDisabled(true); + ui.profCalcAllTissues ->setDisabled(true); + ui.profIncrement3m->setDisabled(true); + ui.profDcCeiling->setDisabled(true); + ui.profPhe->setDisabled(true); + ui.profPn2->setDisabled(true); //TODO is the same as scuba? + ui.profPO2->setDisabled(true); //TODO is the same as scuba? + ui.profRuler->setDisabled(false); + ui.profScaled->setDisabled(false); // measuring and scaling + ui.profTogglePicture->setDisabled(false); + ui.profTankbar->setDisabled(true); + ui.profMod->setDisabled(true); + ui.profNdl_tts->setDisabled(true); + ui.profEad->setDisabled(true); + ui.profSAC->setDisabled(true); + ui.profHR->setDisabled(false); + ui.profTissues->setDisabled(true); + } else { + ui.profCalcCeiling->setDisabled(false); + ui.profCalcAllTissues ->setDisabled(false); + ui.profIncrement3m->setDisabled(false); + ui.profDcCeiling->setDisabled(false); + ui.profPhe->setDisabled(false); + ui.profPn2->setDisabled(false); + ui.profPO2->setDisabled(false); // partial pressure graphs + ui.profRuler->setDisabled(false); + ui.profScaled->setDisabled(false); // measuring and scaling + ui.profTogglePicture->setDisabled(false); + ui.profTankbar->setDisabled(false); + ui.profMod->setDisabled(false); + ui.profNdl_tts->setDisabled(false); // various values that a user is either interested in or not + ui.profEad->setDisabled(false); + ui.profSAC->setDisabled(false); + ui.profHR->setDisabled(false); // very few dive computers support this + ui.profTissues->setDisabled(false);; // maybe less frequently used + } + } +} + +void MainWindow::current_dive_changed(int divenr) +{ + if (divenr >= 0) { + select_dive(divenr); + } + graphics()->plotDive(); + information()->updateDiveInfo(); + configureToolbar(); + GlobeGPS::instance()->reload(); +} + +void MainWindow::on_actionNew_triggered() +{ + on_actionClose_triggered(); +} + +void MainWindow::on_actionOpen_triggered() +{ + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + // yes, this look wrong to use getSaveFileName() for the open dialog, but we need to be able + // to enter file names that don't exist in order to use our git syntax /path/to/dir[branch] + // with is a potentially valid input, but of course won't exist. So getOpenFileName() wouldn't work + QFileDialog dialog(this, tr("Open file"), lastUsedDir(), filter()); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::Accept, tr("Open")); + dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + QStringList filenames; + if (dialog.exec()) + filenames = dialog.selectedFiles(); + if (filenames.isEmpty()) + return; + updateLastUsedDir(QFileInfo(filenames.first()).dir().path()); + closeCurrentFile(); + // some file dialogs decide to add the default extension to a filename without extension + // so we would get dir[branch].ssrf when trying to select dir[branch]. + // let's detect that and remove the incorrect extension + QStringList cleanFilenames; + QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); + + Q_FOREACH (QString filename, filenames) { + if (reg.match(filename).hasMatch()) + filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); + cleanFilenames << filename; + } + loadFiles(cleanFilenames); +} + +void MainWindow::on_actionSave_triggered() +{ + file_save(); +} + +void MainWindow::on_actionSaveAs_triggered() +{ + file_save_as(); +} + +void MainWindow::on_actionCloudstorageopen_triggered() +{ + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + QString filename; + if (getCloudURL(filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + qDebug() << filename; + + closeCurrentFile(); + + int error; + + showProgressBar(); + QByteArray fileNamePtr = QFile::encodeName(filename); + error = parse_file(fileNamePtr.data()); + if (!error) { + set_filename(fileNamePtr.data(), true); + setTitle(MWTF_FILENAME); + } + getNotificationWidget()->hideNotification(); + process_dives(false, false); + hideProgressBar(); + refreshDisplay(); + ui.actionAutoGroup->setChecked(autogroup); +} + +void MainWindow::on_actionCloudstoragesave_triggered() +{ + QString filename; + if (getCloudURL(filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + qDebug() << filename; + if (information()->isEditing()) + information()->acceptChanges(); + + showProgressBar(); + + if (save_dives(filename.toUtf8().data())) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + + hideProgressBar(); + + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + set_filename(filename.toUtf8().data(), true); + setTitle(MWTF_FILENAME); + mark_divelist_changed(false); +} + +void learnImageDirs(QStringList dirnames) +{ + QList > futures; + foreach (QString dir, dirnames) { + futures << QtConcurrent::run(learnImages, QDir(dir), 10, false); + } + DivePictureModel::instance()->updateDivePicturesWhenDone(futures); +} + +void MainWindow::on_actionHash_images_triggered() +{ + QFuture future; + QFileDialog dialog(this, tr("Traverse image directories"), lastUsedDir(), filter()); + dialog.setFileMode(QFileDialog::Directory); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::Accept, tr("Scan")); + dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); + QStringList dirnames; + if (dialog.exec()) + dirnames = dialog.selectedFiles(); + if (dirnames.isEmpty()) + return; + future = QtConcurrent::run(learnImageDirs,dirnames); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Scanning images...(this can take a while)"), KMessageWidget::Information); + MainWindow::instance()->getNotificationWidget()->setFuture(future); + +} + +ProfileWidget2 *MainWindow::graphics() const +{ + return qobject_cast(applicationState["Default"].topRight->layout()->itemAt(1)->widget()); +} + +void MainWindow::cleanUpEmpty() +{ + information()->clearStats(); + information()->clearInfo(); + information()->clearEquipment(); + information()->updateDiveInfo(true); + graphics()->setEmptyState(); + dive_list()->reload(DiveTripModel::TREE); + GlobeGPS::instance()->reload(); + if (!existing_filename) + setTitle(MWTF_DEFAULT); + disableShortcuts(); +} + +bool MainWindow::okToClose(QString message) +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing() ) { + QMessageBox::warning(this, tr("Warning"), message); + return false; + } + if (unsaved_changes() && askSaveChanges() == false) + return false; + + return true; +} + +void MainWindow::closeCurrentFile() +{ + graphics()->setEmptyState(); + /* free the dives and trips */ + clear_git_id(); + clear_dive_file_data(); + cleanUpEmpty(); + mark_divelist_changed(false); + + clear_events(); + + dcList.dcMap.clear(); +} + +void MainWindow::on_actionClose_triggered() +{ + if (okToClose(tr("Please save or cancel the current dive edit before closing the file."))) { + closeCurrentFile(); + // hide any pictures and the filter + DivePictureModel::instance()->updateDivePictures(); + ui.multiFilter->closeFilter(); + recreateDiveList(); + } +} + +QString MainWindow::lastUsedDir() +{ + QSettings settings; + QString lastDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastDir")) + if (QDir::setCurrent(settings.value("LastDir").toString())) + lastDir = settings.value("LastDir").toString(); + return lastDir; +} + +void MainWindow::updateLastUsedDir(const QString &dir) +{ + QSettings s; + s.beginGroup("FileDialog"); + s.setValue("LastDir", dir); +} + +void MainWindow::on_actionPrint_triggered() +{ +#ifndef NO_PRINTING + PrintDialog dlg(this); + + dlg.exec(); +#endif +} + +void MainWindow::disableShortcuts(bool disablePaste) +{ + ui.actionPreviousDC->setShortcut(QKeySequence()); + ui.actionNextDC->setShortcut(QKeySequence()); + ui.copy->setShortcut(QKeySequence()); + if (disablePaste) + ui.paste->setShortcut(QKeySequence()); +} + +void MainWindow::enableShortcuts() +{ + ui.actionPreviousDC->setShortcut(Qt::Key_Left); + ui.actionNextDC->setShortcut(Qt::Key_Right); + ui.copy->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C)); + ui.paste->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V)); +} + +void MainWindow::showProfile() +{ + enableShortcuts(); + graphics()->setProfileState(); + setApplicationState("Default"); +} + +void MainWindow::on_actionPreferences_triggered() +{ + PreferencesDialog::instance()->show(); + PreferencesDialog::instance()->raise(); +} + +void MainWindow::on_actionQuit_triggered() +{ + if (information()->isEditing()) { + information()->rejectChanges(); + if (information()->isEditing()) + // didn't discard the edits + return; + } + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + DivePlannerPointsModel::instance()->cancelPlan(); + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) + // The planned dive was not discarded + return; + } + + if (unsaved_changes() && (askSaveChanges() == false)) + return; + writeSettings(); + QApplication::quit(); +} + +void MainWindow::on_actionDownloadDC_triggered() +{ + DownloadFromDCWidget dlg(this); + + dlg.exec(); +} + +void MainWindow::on_actionDownloadWeb_triggered() +{ + SubsurfaceWebServices dlg(this); + + dlg.exec(); +} + +void MainWindow::on_actionDivelogs_de_triggered() +{ + DivelogsDeWebServices::instance()->downloadDives(); +} + +void MainWindow::on_actionEditDeviceNames_triggered() +{ + DiveComputerManagementDialog::instance()->init(); + DiveComputerManagementDialog::instance()->update(); + DiveComputerManagementDialog::instance()->show(); +} + +bool MainWindow::plannerStateClean() +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing()) { + QMessageBox::warning(this, tr("Warning"), tr("Please save or cancel the current dive edit before trying to add a dive.")); + return false; + } + return true; +} + +void MainWindow::refreshProfile() +{ + showProfile(); + configureToolbar(); + graphics()->replot(get_dive(selected_dive)); + DivePictureModel::instance()->updateDivePictures(); +} + +void MainWindow::planCanceled() +{ + // while planning we might have modified the displayed_dive + // let's refresh what's shown on the profile + refreshProfile(); + refreshDisplay(false); +} + +void MainWindow::planCreated() +{ + // get the new dive selected and assign a number if reasonable + graphics()->setProfileState(); + if (displayed_dive.id == 0) { + // we might have added a new dive (so displayed_dive was cleared out by clone_dive() + dive_list()->unselectDives(); + select_dive(dive_table.nr - 1); + dive_list()->selectDive(selected_dive); + set_dive_nr_for_current_dive(); + } + // make sure our UI is in a consistent state + information()->updateDiveInfo(); + showProfile(); + refreshDisplay(); +} + +void MainWindow::setPlanNotes() +{ + plannerDetails()->divePlanOutput()->setHtml(displayed_dive.notes); +} + +void MainWindow::printPlan() +{ +#ifndef NO_PRINTING + QString diveplan = plannerDetails()->divePlanOutput()->toHtml(); + QString withDisclaimer = QString(" ") + diveplan + QString(disclaimer); + + QPrinter printer; + QPrintDialog *dialog = new QPrintDialog(&printer, this); + dialog->setWindowTitle(tr("Print runtime table")); + if (dialog->exec() != QDialog::Accepted) + return; + + plannerDetails()->divePlanOutput()->setHtml(withDisclaimer); + plannerDetails()->divePlanOutput()->print(&printer); + plannerDetails()->divePlanOutput()->setHtml(diveplan); +#endif +} + +void MainWindow::setupForAddAndPlan(const char *model) +{ + // clean out the dive and give it an id and the correct dc model + clear_dive(&displayed_dive); + clear_dive_site(&displayed_dive_site); + displayed_dive.id = dive_getUniqID(&displayed_dive); + displayed_dive.when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset() + 3600; + displayed_dive.dc.model = model; // don't translate! this is stored in the XML file + // setup the dive cylinders + DivePlannerPointsModel::instance()->clear(); + DivePlannerPointsModel::instance()->setupCylinders(); +} + +void MainWindow::on_actionReplanDive_triggered() +{ + if (!plannerStateClean() || !current_dive || !current_dive->dc.model) + return; + else if (strcmp(current_dive->dc.model, "planned dive")) { + if (QMessageBox::warning(this, tr("Warning"), tr("Trying to replan a dive that's not a planned dive."), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) + return; + } + // put us in PLAN mode + DivePlannerPointsModel::instance()->clear(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + + graphics()->setPlanState(); + graphics()->clearHandlers(); + setApplicationState("PlanDive"); + divePlannerWidget()->setReplanButton(true); + DivePlannerPointsModel::instance()->loadFromDive(current_dive); + reset_cylinders(&displayed_dive, true); +} + +void MainWindow::on_actionDivePlanner_triggered() +{ + if (!plannerStateClean()) + return; + + // put us in PLAN mode + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + setApplicationState("PlanDive"); + + graphics()->setPlanState(); + + // create a simple starting dive, using the first gas from the just copied cylinders + setupForAddAndPlan("planned dive"); // don't translate, stored in XML file + DivePlannerPointsModel::instance()->setupStartTime(); + DivePlannerPointsModel::instance()->createSimpleDive(); + DivePictureModel::instance()->updateDivePictures(); + divePlannerWidget()->setReplanButton(false); +} + +DivePlannerWidget* MainWindow::divePlannerWidget() { + return qobject_cast(applicationState["PlanDive"].topLeft); +} + +void MainWindow::on_actionAddDive_triggered() +{ + if (!plannerStateClean()) + return; + + if (dive_list()->selectedTrips().count() >= 1) { + dive_list()->rememberSelection(); + dive_list()->clearSelection(); + } + + setApplicationState("AddDive"); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + + // setup things so we can later create our starting dive + setupForAddAndPlan("manually added dive"); // don't translate, stored in the XML file + + // now show the mostly empty main tab + information()->updateDiveInfo(); + + // show main tab + information()->setCurrentIndex(0); + + information()->addDiveStarted(); + + graphics()->setAddState(); + DivePlannerPointsModel::instance()->createSimpleDive(); + configureToolbar(); + graphics()->plotDive(); +} + +void MainWindow::on_actionEditDive_triggered() +{ + if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); + return; + } + + const bool isTripEdit = dive_list()->selectedTrips().count() >= 1; + if (!current_dive || isTripEdit || (current_dive->dc.model && strcmp(current_dive->dc.model, "manually added dive"))) { + QMessageBox::warning(this, tr("Warning"), tr("Trying to edit a dive that's not a manually added dive.")); + return; + } + + DivePlannerPointsModel::instance()->clear(); + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + graphics()->setAddState(); + GlobeGPS::instance()->endGetDiveCoordinates(); + setApplicationState("EditDive"); + DivePlannerPointsModel::instance()->loadFromDive(current_dive); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); +} + +void MainWindow::on_actionRenumber_triggered() +{ + RenumberDialog::instance()->renumberOnlySelected(false); + RenumberDialog::instance()->show(); +} + +void MainWindow::on_actionAutoGroup_triggered() +{ + autogroup = ui.actionAutoGroup->isChecked(); + if (autogroup) + autogroup_dives(); + else + remove_autogen_trips(); + refreshDisplay(); + mark_divelist_changed(true); +} + +void MainWindow::on_actionYearlyStatistics_triggered() +{ + QDialog d; + QVBoxLayout *l = new QVBoxLayout(&d); + YearlyStatisticsModel *m = new YearlyStatisticsModel(); + QTreeView *view = new QTreeView(); + view->setModel(m); + l->addWidget(view); + d.resize(width() * .8, height() / 2); + d.move(width() * .1, height() / 4); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), &d); + connect(close, SIGNAL(activated()), &d, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), &d); + connect(quit, SIGNAL(activated()), this, SLOT(close())); + d.setWindowFlags(Qt::Window | Qt::CustomizeWindowHint + | Qt::WindowCloseButtonHint | Qt::WindowTitleHint); + d.setWindowTitle(tr("Yearly statistics")); + d.setWindowIcon(QIcon(":/subsurface-icon")); + d.exec(); +} + +#define BEHAVIOR QList() + +#define TOGGLE_COLLAPSABLE( X ) \ + ui.mainSplitter->setCollapsible(0, X); \ + ui.mainSplitter->setCollapsible(1, X); \ + ui.topSplitter->setCollapsible(0, X); \ + ui.topSplitter->setCollapsible(1, X); \ + ui.bottomSplitter->setCollapsible(0, X); \ + ui.bottomSplitter->setCollapsible(1, X); + +void MainWindow::on_actionViewList_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(LIST_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); + ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); +} + +void MainWindow::on_actionViewProfile_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(PROFILE_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); + ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); +} + +void MainWindow::on_actionViewInfo_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(INFO_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); + ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); +} + +void MainWindow::on_actionViewGlobe_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(GLOBE_MAXIMIZED); + ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); + ui.bottomSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); +} +#undef BEHAVIOR + +void MainWindow::on_actionViewAll_triggered() +{ + TOGGLE_COLLAPSABLE( false ); + beginChangeState(VIEWALL); + static QList mainSizes; + const int appH = qApp->desktop()->size().height(); + const int appW = qApp->desktop()->size().width(); + if (mainSizes.empty()) { + mainSizes.append(appH * 0.7); + mainSizes.append(appH * 0.3); + } + static QList infoProfileSizes; + if (infoProfileSizes.empty()) { + infoProfileSizes.append(appW * 0.3); + infoProfileSizes.append(appW * 0.7); + } + + static QList listGlobeSizes; + if (listGlobeSizes.empty()) { + listGlobeSizes.append(appW * 0.7); + listGlobeSizes.append(appW * 0.3); + } + + QSettings settings; + settings.beginGroup("MainWindow"); + if (settings.value("mainSplitter").isValid()) { + ui.mainSplitter->restoreState(settings.value("mainSplitter").toByteArray()); + ui.topSplitter->restoreState(settings.value("topSplitter").toByteArray()); + ui.bottomSplitter->restoreState(settings.value("bottomSplitter").toByteArray()); + if (ui.mainSplitter->sizes().first() == 0 || ui.mainSplitter->sizes().last() == 0) + ui.mainSplitter->setSizes(mainSizes); + if (ui.topSplitter->sizes().first() == 0 || ui.topSplitter->sizes().last() == 0) + ui.topSplitter->setSizes(infoProfileSizes); + if (ui.bottomSplitter->sizes().first() == 0 || ui.bottomSplitter->sizes().last() == 0) + ui.bottomSplitter->setSizes(listGlobeSizes); + + } else { + ui.mainSplitter->setSizes(mainSizes); + ui.topSplitter->setSizes(infoProfileSizes); + ui.bottomSplitter->setSizes(listGlobeSizes); + } + ui.mainSplitter->setCollapsible(0, false); + ui.mainSplitter->setCollapsible(1, false); + ui.topSplitter->setCollapsible(0, false); + ui.topSplitter->setCollapsible(1, false); + ui.bottomSplitter->setCollapsible(0,false); + ui.bottomSplitter->setCollapsible(1,false); +} + +#undef TOGGLE_COLLAPSABLE + +void MainWindow::beginChangeState(CurrentState s) +{ + if (state == VIEWALL && state != s) { + saveSplitterSizes(); + } + state = s; +} + +void MainWindow::saveSplitterSizes() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + settings.setValue("mainSplitter", ui.mainSplitter->saveState()); + settings.setValue("topSplitter", ui.topSplitter->saveState()); + settings.setValue("bottomSplitter", ui.bottomSplitter->saveState()); +} + +void MainWindow::on_actionPreviousDC_triggered() +{ + unsigned nrdc = number_of_computers(current_dive); + dc_number = (dc_number + nrdc - 1) % nrdc; + configureToolbar(); + graphics()->plotDive(); + information()->updateDiveInfo(); +} + +void MainWindow::on_actionNextDC_triggered() +{ + unsigned nrdc = number_of_computers(current_dive); + dc_number = (dc_number + 1) % nrdc; + configureToolbar(); + graphics()->plotDive(); + information()->updateDiveInfo(); +} + +void MainWindow::on_actionFullScreen_triggered(bool checked) +{ + if (checked) { + setWindowState(windowState() | Qt::WindowFullScreen); + } else { + setWindowState(windowState() & ~Qt::WindowFullScreen); + } +} + +void MainWindow::on_actionAboutSubsurface_triggered() +{ + SubsurfaceAbout dlg(this); + + dlg.exec(); +} + +void MainWindow::on_action_Check_for_Updates_triggered() +{ + if (!updateManager) + updateManager = new UpdateManager(this); + + updateManager->checkForUpdates(); +} + +void MainWindow::on_actionUserManual_triggered() +{ +#ifndef NO_USERMANUAL + if (!helpView) { + helpView = new UserManual(); + } + helpView->show(); +#endif +} + +void MainWindow::on_actionUserSurvey_triggered() +{ + if(!survey) { + survey = new UserSurvey(this); + } + survey->show(); +} + +QString MainWindow::filter() +{ + QString f; + f += "Dive log files ( *.ssrf "; + f += "*.can *.CAN "; + f += "*.db *.DB " ; + f += "*.sql *.SQL " ; + f += "*.dld *.DLD "; + f += "*.jlb *.JLB "; + f += "*.lvd *.LVD "; + f += "*.sde *.SDE "; + f += "*.udcf *.UDCF "; + f += "*.uddf *.UDDF "; + f += "*.xml *.XML "; + f += "*.dlf *.DLF "; + f += ");;"; + + f += "Subsurface (*.ssrf);;"; + f += "Cochran (*.can *.CAN);;"; + f += "DiveLogs.de (*.dld *.DLD);;"; + f += "JDiveLog (*.jlb *.JLB);;"; + f += "Liquivision (*.lvd *.LVD);;"; + f += "Suunto (*.sde *.SDE *.db *.DB);;"; + f += "UDCF (*.udcf *.UDCF);;"; + f += "UDDF (*.uddf *.UDDF);;"; + f += "XML (*.xml *.XML)"; + f += "Divesoft (*.dlf *.DLF)"; + f += "Datatrak/WLog Files (*.log *.LOG)"; + + return f; +} + +bool MainWindow::askSaveChanges() +{ + QString message; + QMessageBox response(this); + + if (existing_filename) + message = tr("Do you want to save the changes that you made in the file %1?") + .arg(displayedFilename(existing_filename)); + else + message = tr("Do you want to save the changes that you made in the data file?"); + + response.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + response.setDefaultButton(QMessageBox::Save); + response.setText(message); + response.setWindowTitle(tr("Save changes?")); // Not displayed on MacOSX as described in Qt API + response.setInformativeText(tr("Changes will be lost if you don't save them.")); + response.setIcon(QMessageBox::Warning); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + + switch (ret) { + case QMessageBox::Save: + file_save(); + return true; + case QMessageBox::Discard: + return true; + } + return false; +} + +void MainWindow::initialUiSetup() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + if (settings.value("maximized", isMaximized()).value()) { + showMaximized(); + } else { + restoreGeometry(settings.value("geometry").toByteArray()); + restoreState(settings.value("windowState", 0).toByteArray()); + } + + state = (CurrentState)settings.value("lastState", 0).toInt(); + switch (state) { + case VIEWALL: + on_actionViewAll_triggered(); + break; + case GLOBE_MAXIMIZED: + on_actionViewGlobe_triggered(); + break; + case INFO_MAXIMIZED: + on_actionViewInfo_triggered(); + break; + case LIST_MAXIMIZED: + on_actionViewList_triggered(); + break; + case PROFILE_MAXIMIZED: + on_actionViewProfile_triggered(); + break; + } + settings.endGroup(); + show(); +} + +const char *getSetting(QSettings &s, QString name) +{ + QVariant v; + v = s.value(name); + if (v.isValid()) { + return strdup(v.toString().toUtf8().data()); + } + return NULL; +} + +#define TOOLBOX_PREF_BUTTON(pref, setting, button) \ + prefs.pref = s.value(#setting).toBool(); \ + ui.button->setChecked(prefs.pref); + +void MainWindow::readSettings() +{ + static bool firstRun = true; + QSettings s; + // the static object for preferences already reads in the settings + // and sets up the font, so just get what we need for the toolbox and other widgets here + + s.beginGroup("TecDetails"); + TOOLBOX_PREF_BUTTON(calcalltissues, calcalltissues, profCalcAllTissues); + TOOLBOX_PREF_BUTTON(calcceiling, calcceiling, profCalcCeiling); + TOOLBOX_PREF_BUTTON(dcceiling, dcceiling, profDcCeiling); + TOOLBOX_PREF_BUTTON(ead, ead, profEad); + TOOLBOX_PREF_BUTTON(calcceiling3m, calcceiling3m, profIncrement3m); + TOOLBOX_PREF_BUTTON(mod, mod, profMod); + TOOLBOX_PREF_BUTTON(calcndltts, calcndltts, profNdl_tts); + TOOLBOX_PREF_BUTTON(pp_graphs.phe, phegraph, profPhe); + TOOLBOX_PREF_BUTTON(pp_graphs.pn2, pn2graph, profPn2); + TOOLBOX_PREF_BUTTON(pp_graphs.po2, po2graph, profPO2); + TOOLBOX_PREF_BUTTON(hrgraph, hrgraph, profHR); + TOOLBOX_PREF_BUTTON(rulergraph, rulergraph, profRuler); + TOOLBOX_PREF_BUTTON(show_sac, show_sac, profSAC); + TOOLBOX_PREF_BUTTON(show_pictures_in_profile, show_pictures_in_profile, profTogglePicture); + TOOLBOX_PREF_BUTTON(tankbar, tankbar, profTankbar); + TOOLBOX_PREF_BUTTON(percentagegraph, percentagegraph, profTissues); + TOOLBOX_PREF_BUTTON(zoomed_plot, zoomed_plot, profScaled); + s.endGroup(); // note: why doesn't the list of 17 buttons match the order in the gui? + s.beginGroup("DiveComputer"); + default_dive_computer_vendor = getSetting(s, "dive_computer_vendor"); + default_dive_computer_product = getSetting(s, "dive_computer_product"); + default_dive_computer_device = getSetting(s, "dive_computer_device"); + default_dive_computer_download_mode = s.value("dive_computer_download_mode").toInt(); + s.endGroup(); + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::ProxyType(prefs.proxy_type)); + proxy.setHostName(prefs.proxy_host); + proxy.setPort(prefs.proxy_port); + if (prefs.proxy_auth) { + proxy.setUser(prefs.proxy_user); + proxy.setPassword(prefs.proxy_pass); + } + QNetworkProxy::setApplicationProxy(proxy); + +#if !defined(SUBSURFACE_MOBILE) + loadRecentFiles(&s); + if (firstRun) { + checkSurvey(&s); + firstRun = false; + } +#endif +} + +#undef TOOLBOX_PREF_BUTTON + +void MainWindow::checkSurvey(QSettings *s) +{ + s->beginGroup("UserSurvey"); + if (!s->contains("FirstUse42")) { + QVariant value = QDate().currentDate(); + s->setValue("FirstUse42", value); + } + // wait a week for production versions, but not at all for non-tagged builds + QString ver(subsurface_version()); + int waitTime = 7; + QDate firstUse42 = s->value("FirstUse42").toDate(); + if (run_survey || (firstUse42.daysTo(QDate().currentDate()) > waitTime && !s->contains("SurveyDone"))) { + if (!survey) + survey = new UserSurvey(this); + survey->show(); + } + s->endGroup(); +} + +void MainWindow::writeSettings() +{ + QSettings settings; + + settings.beginGroup("MainWindow"); + settings.setValue("geometry", saveGeometry()); + settings.setValue("windowState", saveState()); + settings.setValue("maximized", isMaximized()); + settings.setValue("lastState", (int)state); + if (state == VIEWALL) + saveSplitterSizes(); + settings.endGroup(); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing()) { + on_actionQuit_triggered(); + event->ignore(); + return; + } + +#ifndef NO_USERMANUAL + if (helpView && helpView->isVisible()) { + helpView->close(); + helpView->deleteLater(); + } +#endif + + if (survey && survey->isVisible()) { + survey->close(); + survey->deleteLater(); + } + + if (unsaved_changes() && (askSaveChanges() == false)) { + event->ignore(); + return; + } + event->accept(); + writeSettings(); + QApplication::closeAllWindows(); +} + +DiveListView *MainWindow::dive_list() +{ + return qobject_cast(applicationState["Default"].bottomLeft); +} + +MainTab *MainWindow::information() +{ + return qobject_cast(applicationState["Default"].topLeft); +} + +void MainWindow::loadRecentFiles(QSettings *s) +{ + QStringList files; + bool modified = false; + + s->beginGroup("Recent_Files"); + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + if (s->contains(key)) { + QString file = s->value(key).toString(); + + if (QFile::exists(file)) { + files.append(file); + } else { + modified = true; + } + } else { + break; + } + } + + if (modified) { + for (int c = 0; c < 4; c++) { + QString key = QString("File_%1").arg(c + 1); + + if (files.count() > c) { + s->setValue(key, files.at(c)); + } else { + if (s->contains(key)) { + s->remove(key); + } + } + } + + s->sync(); + } + s->endGroup(); + + for (int c = 0; c < 4; c++) { + QAction *action = this->findChild(QString("actionRecent%1").arg(c + 1)); + + if (files.count() > c) { + QFileInfo fi(files.at(c)); + action->setText(fi.fileName()); + action->setToolTip(fi.absoluteFilePath()); + action->setVisible(true); + } else { + action->setVisible(false); + } + } +} + +void MainWindow::addRecentFile(const QStringList &newFiles) +{ + QStringList files; + QSettings s; + + if (newFiles.isEmpty()) + return; + + s.beginGroup("Recent_Files"); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + if (s.contains(key)) { + QString file = s.value(key).toString(); + + files.append(file); + } else { + break; + } + } + + foreach (const QString &file, newFiles) { + int index = files.indexOf(QDir::toNativeSeparators(file)); + + if (index >= 0) { + files.removeAt(index); + } + } + + foreach (const QString &file, newFiles) { + if (QFile::exists(file)) { + files.prepend(QDir::toNativeSeparators(file)); + } + } + + while (files.count() > 4) { + files.removeLast(); + } + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (files.count() >= c) { + s.setValue(key, files.at(c - 1)); + } else { + if (s.contains(key)) { + s.remove(key); + } + } + } + s.endGroup(); + s.sync(); + + loadRecentFiles(&s); +} + +void MainWindow::removeRecentFile(QStringList failedFiles) +{ + QStringList files; + QSettings s; + + if (failedFiles.isEmpty()) + return; + + s.beginGroup("Recent_Files"); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (s.contains(key)) { + QString file = s.value(key).toString(); + files.append(file); + } else { + break; + } + } + + foreach (const QString &file, failedFiles) + files.removeAll(file); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (files.count() >= c) + s.setValue(key, files.at(c - 1)); + else if (s.contains(key)) + s.remove(key); + } + + s.endGroup(); + s.sync(); + + loadRecentFiles(&s); +} + +void MainWindow::recentFileTriggered(bool checked) +{ + Q_UNUSED(checked); + + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + QAction *actionRecent = (QAction *)sender(); + + const QString &filename = actionRecent->toolTip(); + + updateLastUsedDir(QFileInfo(filename).dir().path()); + closeCurrentFile(); + loadFiles(QStringList() << filename); +} + +int MainWindow::file_save_as(void) +{ + QString filename; + const char *default_filename = existing_filename; + + // if the default is to save to cloud storage, pick something that will work as local file: + // simply extract the branch name which should be the users email address + if (default_filename && strstr(default_filename, prefs.cloud_git_url)) { + QString filename(default_filename); + filename.remove(prefs.cloud_git_url); + filename.remove(0, filename.indexOf("[") + 1); + filename.replace("]", ".ssrf"); + default_filename = strdup(qPrintable(filename)); + } + // create a file dialog that allows us to save to a new file + QFileDialog selection_dialog(this, tr("Save file as"), default_filename, + tr("Subsurface XML files (*.ssrf *.xml *.XML)")); + selection_dialog.setAcceptMode(QFileDialog::AcceptSave); + selection_dialog.setFileMode(QFileDialog::AnyFile); + selection_dialog.setDefaultSuffix(""); + if (same_string(default_filename, "")) { + QFileInfo defaultFile(system_default_filename()); + selection_dialog.setDirectory(qPrintable(defaultFile.absolutePath())); + } + /* if the exit/cancel button is pressed return */ + if (!selection_dialog.exec()) + return 0; + + /* get the first selected file */ + filename = selection_dialog.selectedFiles().at(0); + + /* now for reasons I don't understand we appear to add a .ssrf to + * git style filenames /directory[branch] + * so let's remove that */ + QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); + if (reg.match(filename).hasMatch()) + filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); + if (filename.isNull() || filename.isEmpty()) + return report_error("No filename to save into"); + + if (information()->isEditing()) + information()->acceptChanges(); + + if (save_dives(filename.toUtf8().data())) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return -1; + } + + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + set_filename(filename.toUtf8().data(), true); + setTitle(MWTF_FILENAME); + mark_divelist_changed(false); + addRecentFile(QStringList() << filename); + return 0; +} + +int MainWindow::file_save(void) +{ + const char *current_default; + bool is_cloud = false; + + if (!existing_filename) + return file_save_as(); + + is_cloud = (strncmp(existing_filename, "http", 4) == 0); + + if (information()->isEditing()) + information()->acceptChanges(); + + current_default = prefs.default_filename; + if (strcmp(existing_filename, current_default) == 0) { + /* if we are using the default filename the directory + * that we are creating the file in may not exist */ + QDir current_def_dir = QFileInfo(current_default).absoluteDir(); + if (!current_def_dir.exists()) + current_def_dir.mkpath(current_def_dir.absolutePath()); + } + if (is_cloud) + showProgressBar(); + if (save_dives(existing_filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + if (is_cloud) + hideProgressBar(); + return -1; + } + if (is_cloud) + hideProgressBar(); + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + mark_divelist_changed(false); + addRecentFile(QStringList() << QString(existing_filename)); + return 0; +} + +NotificationWidget *MainWindow::getNotificationWidget() +{ + return ui.mainErrorMessage; +} + +void MainWindow::showError() +{ + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); +} + +QString MainWindow::displayedFilename(QString fullFilename) +{ + QFile f(fullFilename); + QFileInfo fileInfo(f); + QString fileName(fileInfo.fileName()); + + if (fullFilename.contains(prefs.cloud_git_url)) + return tr("[cloud storage for] %1").arg(fileName.left(fileName.indexOf('['))); + else + return fileName; +} + +void MainWindow::setAutomaticTitle() +{ + setTitle(); +} + +void MainWindow::setTitle(enum MainWindowTitleFormat format) +{ + switch (format) { + case MWTF_DEFAULT: + setWindowTitle("Subsurface"); + break; + case MWTF_FILENAME: + if (!existing_filename) { + setTitle(MWTF_DEFAULT); + return; + } + QString unsaved = (unsaved_changes() ? " *" : ""); + setWindowTitle("Subsurface: " + displayedFilename(existing_filename) + unsaved); + break; + } +} + +void MainWindow::importFiles(const QStringList fileNames) +{ + if (fileNames.isEmpty()) + return; + + QByteArray fileNamePtr; + + for (int i = 0; i < fileNames.size(); ++i) { + fileNamePtr = QFile::encodeName(fileNames.at(i)); + parse_file(fileNamePtr.data()); + } + process_dives(true, false); + refreshDisplay(); +} + +void MainWindow::importTxtFiles(const QStringList fileNames) +{ + if (fileNames.isEmpty()) + return; + + QByteArray fileNamePtr, csv; + + for (int i = 0; i < fileNames.size(); ++i) { + fileNamePtr = QFile::encodeName(fileNames.at(i)); + csv = fileNamePtr.data(); + csv.replace(strlen(csv.data()) - 3, 3, "csv"); + parse_txt_file(fileNamePtr.data(), csv); + } + process_dives(true, false); + refreshDisplay(); +} + +void MainWindow::loadFiles(const QStringList fileNames) +{ + bool showWarning = false; + if (fileNames.isEmpty()) { + refreshDisplay(); + return; + } + QByteArray fileNamePtr; + QStringList failedParses; + + showProgressBar(); + for (int i = 0; i < fileNames.size(); ++i) { + int error; + + fileNamePtr = QFile::encodeName(fileNames.at(i)); + error = parse_file(fileNamePtr.data()); + if (!error) { + set_filename(fileNamePtr.data(), true); + setTitle(MWTF_FILENAME); + // if there were any messages, show them + QString warning = get_error_string(); + if (!warning.isEmpty()) { + showWarning = true; + getNotificationWidget()->showNotification(warning , KMessageWidget::Information); + } + } else { + failedParses.append(fileNames.at(i)); + } + } + hideProgressBar(); + if (!showWarning) + getNotificationWidget()->hideNotification(); + process_dives(false, false); + addRecentFile(fileNames); + removeRecentFile(failedParses); + + refreshDisplay(); + ui.actionAutoGroup->setChecked(autogroup); + + int min_datafile_version = get_min_datafile_version(); + if (min_datafile_version >0 && min_datafile_version < DATAFORMAT_VERSION) { + QMessageBox::warning(this, tr("Opening datafile from older version"), + tr("You opened a data file from an older version of Subsurface. We recommend " + "you read the manual to learn about the changes in the new version, especially " + "about dive site management which has changed significantly.\n" + "Subsurface has already tried to pre-populate the data but it might be worth " + "while taking a look at the new dive site management system and to make " + "sure that everything looks correct.")); + } +} + +void MainWindow::on_actionImportDiveLog_triggered() +{ + QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open dive log file"), lastUsedDir(), + tr("Dive log files (*.ssrf *.can *.csv *.db *.sql *.dld *.jlb *.lvd *.sde *.udcf *.uddf *.xml *.txt *.dlf *.apd" + "*.SSRF *.CAN *.CSV *.DB *.SQL *.DLD *.JLB *.LVD *.SDE *.UDCF *.UDDF *.xml *.TXT *.DLF *.APD);;" + "Cochran files (*.can *.CAN);;" + "CSV files (*.csv *.CSV);;" + "DiveLog.de files (*.dld *.DLD);;" + "JDiveLog files (*.jlb *.JLB);;" + "Liquivision files (*.lvd *.LVD);;" + "MkVI files (*.txt *.TXT);;" + "Suunto files (*.sde *.db *.SDE *.DB);;" + "Divesoft files (*.dlf *.DLF);;" + "UDDF/UDCF files (*.uddf *.udcf *.UDDF *.UDCF);;" + "XML files (*.xml *.XML);;" + "APD log viewer (*.apd *.APD);;" + "Datatrak/WLog Files (*.log *.LOG);;" + "OSTCtools Files (*.dive *.DIVE);;" + "All files (*)")); + + if (fileNames.isEmpty()) + return; + updateLastUsedDir(QFileInfo(fileNames[0]).dir().path()); + + QStringList logFiles = fileNames.filter(QRegExp("^.*\\.(?!(csv|txt|apd))", Qt::CaseInsensitive)); + QStringList csvFiles = fileNames.filter(".csv", Qt::CaseInsensitive); + csvFiles += fileNames.filter(".apd", Qt::CaseInsensitive); + QStringList txtFiles = fileNames.filter(".txt", Qt::CaseInsensitive); + + if (logFiles.size()) { + importFiles(logFiles); + } + + if (csvFiles.size()) { + DiveLogImportDialog *diveLogImport = new DiveLogImportDialog(csvFiles, this); + diveLogImport->show(); + process_dives(true, false); + refreshDisplay(); + } + + if (txtFiles.size()) { + importTxtFiles(txtFiles); + } +} + +void MainWindow::editCurrentDive() +{ + if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); + return; + } + + struct dive *d = current_dive; + QString defaultDC(d->dc.model); + DivePlannerPointsModel::instance()->clear(); + if (defaultDC == "manually added dive") { + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + graphics()->setAddState(); + setApplicationState("EditDive"); + DivePlannerPointsModel::instance()->loadFromDive(d); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); + } else if (defaultDC == "planned dive") { + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + setApplicationState("EditPlannedDive"); + DivePlannerPointsModel::instance()->loadFromDive(d); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); + } +} + +#define PREF_PROFILE(QT_PREFS) \ + QSettings s; \ + s.beginGroup("TecDetails"); \ + s.setValue(#QT_PREFS, triggered); \ + PreferencesDialog::instance()->emitSettingsChanged(); + +#define TOOLBOX_PREF_PROFILE(METHOD, INTERNAL_PREFS, QT_PREFS) \ + void MainWindow::on_##METHOD##_triggered(bool triggered) \ + { \ + prefs.INTERNAL_PREFS = triggered; \ + PREF_PROFILE(QT_PREFS); \ + } + +// note: why doesn't the list of 17 buttons match the order in the gui? or the order above? (line 1136) +TOOLBOX_PREF_PROFILE(profCalcAllTissues, calcalltissues, calcalltissues); +TOOLBOX_PREF_PROFILE(profCalcCeiling, calcceiling, calcceiling); +TOOLBOX_PREF_PROFILE(profDcCeiling, dcceiling, dcceiling); +TOOLBOX_PREF_PROFILE(profEad, ead, ead); +TOOLBOX_PREF_PROFILE(profIncrement3m, calcceiling3m, calcceiling3m); +TOOLBOX_PREF_PROFILE(profMod, mod, mod); +TOOLBOX_PREF_PROFILE(profNdl_tts, calcndltts, calcndltts); +TOOLBOX_PREF_PROFILE(profPhe, pp_graphs.phe, phegraph); +TOOLBOX_PREF_PROFILE(profPn2, pp_graphs.pn2, pn2graph); +TOOLBOX_PREF_PROFILE(profPO2, pp_graphs.po2, po2graph); +TOOLBOX_PREF_PROFILE(profHR, hrgraph, hrgraph); +TOOLBOX_PREF_PROFILE(profRuler, rulergraph, rulergraph); +TOOLBOX_PREF_PROFILE(profSAC, show_sac, show_sac); +TOOLBOX_PREF_PROFILE(profScaled, zoomed_plot, zoomed_plot); +TOOLBOX_PREF_PROFILE(profTogglePicture, show_pictures_in_profile, show_pictures_in_profile); +TOOLBOX_PREF_PROFILE(profTankbar, tankbar, tankbar); +TOOLBOX_PREF_PROFILE(profTissues, percentagegraph, percentagegraph); +// couldn't the args to TOOLBOX_PREF_PROFILE be made to go in the same sequence as TOOLBOX_PREF_BUTTON? + +void MainWindow::turnOffNdlTts() +{ + const bool triggered = false; + prefs.calcndltts = triggered; + PREF_PROFILE(calcndltts); +} + +#undef TOOLBOX_PREF_PROFILE +#undef PERF_PROFILE + +void MainWindow::on_actionExport_triggered() +{ + DiveLogExportDialog diveLogExport; + diveLogExport.exec(); +} + +void MainWindow::on_actionConfigure_Dive_Computer_triggered() +{ + ConfigureDiveComputerDialog *dcConfig = new ConfigureDiveComputerDialog(this); + dcConfig->show(); +} + +void MainWindow::setEnabledToolbar(bool arg1) +{ + Q_FOREACH (QAction *b, profileToolbarActions) + b->setEnabled(arg1); +} + +void MainWindow::on_copy_triggered() +{ + // open dialog to select what gets copied + // copy the displayed dive + DiveComponentSelection dialog(this, ©PasteDive, &what); + dialog.exec(); +} + +void MainWindow::on_paste_triggered() +{ + // take the data in our copyPasteDive and apply it to selected dives + selective_copy_dive(©PasteDive, &displayed_dive, what, false); + information()->showAndTriggerEditSelective(what); +} + +void MainWindow::on_actionFilterTags_triggered() +{ + if (ui.multiFilter->isVisible()) + ui.multiFilter->closeFilter(); + else + ui.multiFilter->setVisible(true); +} + +void MainWindow::registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight) +{ + applicationState[state] = WidgetForQuadrant(topLeft, topRight, bottomLeft, bottomRight); + if (ui.topLeft->indexOf(topLeft) == -1 && topLeft) { + ui.topLeft->addWidget(topLeft); + } + if (ui.topRight->indexOf(topRight) == -1 && topRight) { + ui.topRight->addWidget(topRight); + } + if (ui.bottomLeft->indexOf(bottomLeft) == -1 && bottomLeft) { + ui.bottomLeft->addWidget(bottomLeft); + } + if(ui.bottomRight->indexOf(bottomRight) == -1 && bottomRight) { + ui.bottomRight->addWidget(bottomRight); + } +} + +void MainWindow::setApplicationState(const QByteArray& state) { + if (!applicationState.keys().contains(state)) + return; + + if (getCurrentAppState() == state) + return; + + setCurrentAppState(state); + +#define SET_CURRENT_INDEX( X ) \ + if (applicationState[state].X) { \ + ui.X->setCurrentWidget( applicationState[state].X); \ + ui.X->show(); \ + } else { \ + ui.X->hide(); \ + } + + SET_CURRENT_INDEX( topLeft ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].topLeft) { + ui.topLeft->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( topRight ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].topRight) { + ui.topRight->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( bottomLeft ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomLeft) { + ui.bottomLeft->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( bottomRight ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomRight) { + ui.bottomRight->currentWidget()->setProperty( p.first.data(), p.second); + } +#undef SET_CURRENT_INDEX +} + +void MainWindow::showProgressBar() +{ + if (progressDialog) + delete progressDialog; + + progressDialog = new QProgressDialog(tr("Contacting cloud service..."), tr("Cancel"), 0, 100, this); + progressDialog->setWindowModality(Qt::WindowModal); + progressDialog->setMinimumDuration(200); + progressDialogCanceled = false; + connect(progressDialog, SIGNAL(canceled()), this, SLOT(cancelCloudStorageOperation())); +} + +void MainWindow::cancelCloudStorageOperation() +{ + progressDialogCanceled = true; +} + +void MainWindow::hideProgressBar() +{ + if (progressDialog) { + progressDialog->setValue(100); + progressDialog->deleteLater(); + progressDialog = NULL; + } +} diff --git a/desktop-widgets/mainwindow.h b/desktop-widgets/mainwindow.h new file mode 100644 index 000000000..02ec2478c --- /dev/null +++ b/desktop-widgets/mainwindow.h @@ -0,0 +1,258 @@ +/* + * mainwindow.h + * + * header file for the main window of Subsurface + * + */ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include + +#include "ui_mainwindow.h" +#include "notificationwidget.h" +#include "windowtitleupdate.h" + +struct DiveList; +class QSortFilterProxyModel; +class DiveTripModel; + +class DiveInfo; +class DiveNotes; +class Stats; +class Equipment; +class QItemSelection; +class DiveListView; +class MainTab; +class ProfileGraphicsView; +class QWebView; +class QSettings; +class UpdateManager; +class UserManual; +class DivePlannerWidget; +class ProfileWidget2; +class PlannerDetails; +class PlannerSettingsWidget; +class QUndoStack; +class LocationInformationWidget; + +typedef std::pair WidgetProperty; +typedef QVector PropertyList; + +enum MainWindowTitleFormat { + MWTF_DEFAULT, + MWTF_FILENAME +}; + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + enum { + COLLAPSED, + EXPANDED + }; + + enum CurrentState { + VIEWALL, + GLOBE_MAXIMIZED, + INFO_MAXIMIZED, + PROFILE_MAXIMIZED, + LIST_MAXIMIZED + }; + + MainWindow(); + virtual ~MainWindow(); + static MainWindow *instance(); + MainTab *information(); + void loadRecentFiles(QSettings *s); + void addRecentFile(const QStringList &newFiles); + void removeRecentFile(QStringList failedFiles); + DiveListView *dive_list(); + DivePlannerWidget *divePlannerWidget(); + PlannerSettingsWidget *divePlannerSettingsWidget(); + LocationInformationWidget *locationInformationWidget(); + void setTitle(enum MainWindowTitleFormat format = MWTF_FILENAME); + + // Some shortcuts like "change DC" or "copy/paste dive components" + // should only be enabled when the profile's visible. + void disableShortcuts(bool disablePaste = true); + void enableShortcuts(); + void loadFiles(const QStringList files); + void importFiles(const QStringList importFiles); + void importTxtFiles(const QStringList fileNames); + void cleanUpEmpty(); + void setToolButtonsEnabled(bool enabled); + ProfileWidget2 *graphics() const; + PlannerDetails *plannerDetails() const; + void setLoadedWithFiles(bool filesFromCommandLine); + bool filesFromCommandLine() const; + void printPlan(); + void checkSurvey(QSettings *s); + void setApplicationState(const QByteArray& state); + void setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl,const PropertyList& br); + bool inPlanner(); + QUndoStack *undoStack; + NotificationWidget *getNotificationWidget(); + void enableDisableCloudActions(); + void showError(); +private +slots: + /* file menu action */ + void recentFileTriggered(bool checked); + void on_actionNew_triggered(); + void on_actionOpen_triggered(); + void on_actionSave_triggered(); + void on_actionSaveAs_triggered(); + void on_actionClose_triggered(); + void on_actionCloudstorageopen_triggered(); + void on_actionCloudstoragesave_triggered(); + void on_actionPrint_triggered(); + void on_actionPreferences_triggered(); + void on_actionQuit_triggered(); + void on_actionHash_images_triggered(); + + /* log menu actions */ + void on_actionDownloadDC_triggered(); + void on_actionDownloadWeb_triggered(); + void on_actionDivelogs_de_triggered(); + void on_actionEditDeviceNames_triggered(); + void on_actionAddDive_triggered(); + void on_actionEditDive_triggered(); + void on_actionRenumber_triggered(); + void on_actionAutoGroup_triggered(); + void on_actionYearlyStatistics_triggered(); + + /* view menu actions */ + void on_actionViewList_triggered(); + void on_actionViewProfile_triggered(); + void on_actionViewInfo_triggered(); + void on_actionViewGlobe_triggered(); + void on_actionViewAll_triggered(); + void on_actionPreviousDC_triggered(); + void on_actionNextDC_triggered(); + void on_actionFullScreen_triggered(bool checked); + + /* other menu actions */ + void on_actionAboutSubsurface_triggered(); + void on_actionUserManual_triggered(); + void on_actionUserSurvey_triggered(); + void on_actionDivePlanner_triggered(); + void on_actionReplanDive_triggered(); + void on_action_Check_for_Updates_triggered(); + + void on_actionDiveSiteEdit_triggered(); + void current_dive_changed(int divenr); + void initialUiSetup(); + + void on_actionImportDiveLog_triggered(); + + /* TODO: Move those slots below to it's own class */ + void on_profCalcAllTissues_triggered(bool triggered); + void on_profCalcCeiling_triggered(bool triggered); + void on_profDcCeiling_triggered(bool triggered); + void on_profEad_triggered(bool triggered); + void on_profIncrement3m_triggered(bool triggered); + void on_profMod_triggered(bool triggered); + void on_profNdl_tts_triggered(bool triggered); + void on_profPO2_triggered(bool triggered); + void on_profPhe_triggered(bool triggered); + void on_profPn2_triggered(bool triggered); + void on_profHR_triggered(bool triggered); + void on_profRuler_triggered(bool triggered); + void on_profSAC_triggered(bool triggered); + void on_profScaled_triggered(bool triggered); + void on_profTogglePicture_triggered(bool triggered); + void on_profTankbar_triggered(bool triggered); + void on_profTissues_triggered(bool triggered); + void on_actionExport_triggered(); + void on_copy_triggered(); + void on_paste_triggered(); + void on_actionFilterTags_triggered(); + void on_actionConfigure_Dive_Computer_triggered(); + void setDefaultState(); + void setAutomaticTitle(); + void cancelCloudStorageOperation(); + +protected: + void closeEvent(QCloseEvent *); + +signals: + void startDiveSiteEdit(); + +public +slots: + void turnOffNdlTts(); + void readSettings(); + void refreshDisplay(bool doRecreateDiveList = true); + void recreateDiveList(); + void showProfile(); + void refreshProfile(); + void editCurrentDive(); + void planCanceled(); + void planCreated(); + void setEnabledToolbar(bool arg1); + void setPlanNotes(); + +private: + Ui::MainWindow ui; + QAction *actionNextDive; + QAction *actionPreviousDive; + UserManual *helpView; + CurrentState state; + QString filter(); + static MainWindow *m_Instance; + QString displayedFilename(QString fullFilename); + bool askSaveChanges(); + bool okToClose(QString message); + void closeCurrentFile(); + void showProgressBar(); + void hideProgressBar(); + void writeSettings(); + int file_save(); + int file_save_as(); + void beginChangeState(CurrentState s); + void saveSplitterSizes(); + QString lastUsedDir(); + void updateLastUsedDir(const QString &s); + void registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight); + bool filesAsArguments; + UpdateManager *updateManager; + + bool plannerStateClean(); + void setupForAddAndPlan(const char *model); + void configureToolbar(); + QDialog *survey; + struct dive copyPasteDive; + struct dive_components what; + QList profileToolbarActions; + + struct WidgetForQuadrant { + WidgetForQuadrant(QWidget *tl = 0, QWidget *tr = 0, QWidget *bl = 0, QWidget *br = 0) : + topLeft(tl), topRight(tr), bottomLeft(bl), bottomRight(br) {} + QWidget *topLeft; + QWidget *topRight; + QWidget *bottomLeft; + QWidget *bottomRight; + }; + + struct PropertiesForQuadrant { + PropertiesForQuadrant(){} + PropertiesForQuadrant(const PropertyList& tl, const PropertyList& tr,const PropertyList& bl,const PropertyList& br) : + topLeft(tl), topRight(tr), bottomLeft(bl), bottomRight(br) {} + PropertyList topLeft; + PropertyList topRight; + PropertyList bottomLeft; + PropertyList bottomRight; + }; + + QHash applicationState; + QHash stateProperties; + + WindowTitleUpdate *wtu; +}; + +#endif // MAINWINDOW_H diff --git a/desktop-widgets/mainwindow.ui b/desktop-widgets/mainwindow.ui new file mode 100644 index 000000000..5e3200cfc --- /dev/null +++ b/desktop-widgets/mainwindow.ui @@ -0,0 +1,761 @@ + + + MainWindow + + + + 0 + 0 + 861 + 800 + + + + + + 0 + + + 0 + + + + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + + 0 + 0 + 861 + 23 + + + + + &File + + + + + + + + + + + + + + + + + + + + + + + + + &Log + + + + + + + + + + + + + + + + + &View + + + + + + + + + + + + + + + + &Help + + + + + + + + + &Import + + + + + + + + + &Edit + + + + + Share on + + + + + + + + + + + + + + + &New logbook + + + New + + + Ctrl+N + + + + + &Open logbook + + + Open + + + Ctrl+O + + + + + &Save + + + Save + + + Ctrl+S + + + + + Sa&ve as + + + Save as + + + Ctrl+Shift+S + + + + + &Close + + + Close + + + Ctrl+W + + + + + &Print + + + Ctrl+P + + + + + P&references + + + Ctrl+, + + + QAction::PreferencesRole + + + + + &Quit + + + Ctrl+Q + + + QAction::QuitRole + + + + + Import from &dive computer + + + Ctrl+D + + + + + Import &GPS data from Subsurface web service + + + Ctrl+G + + + + + Edit device &names + + + + + &Add dive + + + Ctrl++ + + + + + &Edit dive + + + + + &Copy dive components + + + Ctrl+C + + + + + &Paste dive components + + + Ctrl+V + + + + + &Renumber + + + Ctrl+R + + + + + true + + + Auto &group + + + + + &Yearly statistics + + + Ctrl+Y + + + + + &Dive list + + + Ctrl+2 + + + + + &Profile + + + Ctrl+3 + + + + + &Info + + + Ctrl+4 + + + + + &All + + + Ctrl+1 + + + + + P&revious DC + + + Left + + + + + &Next DC + + + Right + + + + + &About Subsurface + + + QAction::AboutRole + + + + + User &manual + + + F1 + + + + + &Globe + + + Ctrl+5 + + + + + P&lan dive + + + Ctrl+L + + + + + &Import log files + + + Import divelog files from other applications + + + Ctrl+I + + + + + Import &from divelogs.de + + + + + true + + + &Full screen + + + Toggle full screen + + + F11 + + + + + false + + + + + false + + + + + false + + + + + false + + + + + &Check for updates + + + + + &Export + + + Export dive logs + + + Ctrl+E + + + + + Configure &dive computer + + + Ctrl+Shift+C + + + QAction::NoRole + + + + + Edit &dive in planner + + + + + true + + + + :/icon_o2:/icon_o2 + + + Toggle pOâ‚‚ graph + + + + + true + + + + :/icon_n2:/icon_n2 + + + Toggle pNâ‚‚ graph + + + + + true + + + + :/icon_he:/icon_he + + + Toggle pHe graph + + + + + true + + + + :/icon_ceiling_dc:/icon_ceiling_dc + + + Toggle DC reported ceiling + + + + + true + + + + :/icon_ceiling_calculated:/icon_ceiling_calculated + + + Toggle calculated ceiling + + + + + true + + + + :/icon_ceiling_alltissues:/icon_ceiling_alltissues + + + Toggle calculating all tissues + + + + + true + + + + :/icon_ceiling_3m:/icon_ceiling_3m + + + Toggle calculated ceiling with 3m increments + + + + + true + + + + :/icon_HR:/icon_HR + + + Toggle heart rate + + + + + true + + + + :/icon_mod:/icon_mod + + + Toggle MOD + + + + + true + + + + :/icon_ead:/icon_ead + + + Toggle EAD, END, EADD + + + + + true + + + + :/icon_NDLTTS:/icon_NDLTTS + + + Toggle NDL, TTS + + + + + true + + + + :/icon_lung:/icon_lung + + + Toggle SAC rate + + + + + true + + + + :/ruler:/ruler + + + Toggle ruler + + + + + true + + + + :/scale:/scale + + + Scale graph + + + + + true + + + + :/pictures:/pictures + + + Toggle pictures + + + + + true + + + + :/gaschange:/gaschange + + + Toggle tank bar + + + + + true + + + &Filter divelist + + + Ctrl+F + + + + + true + + + + :/icon_tissue:/icon_tissue + + + Toggle tissue graph + + + + + User &survey + + + + + &Undo + + + Ctrl+Z + + + + + &Redo + + + Ctrl+Shift+Z + + + + + &Find moved images + + + + + Open c&loud storage + + + + + Save to clo&ud storage + + + + + &Manage dive sites + + + + + Dive Site &Edit + + + + + Facebook + + + + + + NotificationWidget + QWidget +
notificationwidget.h
+ 1 +
+ + MultiFilter + QWidget +
simplewidgets.h
+ 1 +
+
+ + + + +
diff --git a/desktop-widgets/marble/GeoDataTreeModel.h b/desktop-widgets/marble/GeoDataTreeModel.h new file mode 100644 index 000000000..39eff8388 --- /dev/null +++ b/desktop-widgets/marble/GeoDataTreeModel.h @@ -0,0 +1,118 @@ +// +// This file is part of the Marble Virtual Globe. +// +// This program is free software licensed under the GNU LGPL. You can +// find a copy of this license in LICENSE.txt in the top directory of +// the source code. +// +// Copyright 2010 Thibaut Gridel +// + +#ifndef MARBLE_GEODATATREEMODEL_H +#define MARBLE_GEODATATREEMODEL_H + +// -> does not appear to be needed #include "marble_export.h" + +#include + +class QItemSelectionModel; + +namespace Marble +{ +class GeoDataObject; +class GeoDataDocument; +class GeoDataFeature; +class GeoDataContainer; + + +/** + * @short The representation of GeoData in a model + * This class represents all available data given by kml-data files. + */ +class MARBLE_EXPORT GeoDataTreeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** + * Creates a new GeoDataTreeModel. + * + * @param parent The parent object. + */ + explicit GeoDataTreeModel( QObject *parent = 0 ); + + /** + * Destroys the GeoDataModel. + */ + ~GeoDataTreeModel(); + + virtual bool hasChildren( const QModelIndex &parent ) const; + + /** + * Return the number of Items in the Model. + */ + int rowCount( const QModelIndex &parent = QModelIndex() ) const; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + + QVariant data( const QModelIndex &index, int role ) const; + + QModelIndex index( int row, int column, + const QModelIndex &parent = QModelIndex() ) const; + + QModelIndex index( GeoDataObject* object ); + + QModelIndex parent( const QModelIndex &index ) const; + + int columnCount( const QModelIndex &parent = QModelIndex() ) const; + + Qt::ItemFlags flags ( const QModelIndex & index ) const; + + bool setData ( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole ); + + void reset(); + + QItemSelectionModel *selectionModel(); + +public Q_SLOTS: + + /** + * Sets the root document to use. This replaces previously loaded data, if any. + * @param document The new root document. Ownership retains with the caller, + * i.e. GeoDataTreeModel will not delete the passed document at its destruction. + */ + void setRootDocument( GeoDataDocument *document ); + GeoDataDocument* rootDocument(); + + int addFeature( GeoDataContainer *parent, GeoDataFeature *feature, int row = -1 ); + + bool removeFeature( GeoDataContainer *parent, int index ); + + int removeFeature( const GeoDataFeature *feature ); + + void updateFeature( GeoDataFeature *feature ); + + int addDocument( GeoDataDocument *document ); + + void removeDocument( int index ); + + void removeDocument( GeoDataDocument* document ); + + void update(); + +Q_SIGNALS: + /// insert and remove row don't trigger any signal that proxies forward + /// this signal will refresh geometry layer and placemark layout + void removed( GeoDataObject *object ); + void added( GeoDataObject *object ); + private: + Q_DISABLE_COPY( GeoDataTreeModel ) + class Private; + Private* const d; +}; + +} + +#endif // MARBLE_GEODATATREEMODEL_H diff --git a/desktop-widgets/modeldelegates.cpp b/desktop-widgets/modeldelegates.cpp new file mode 100644 index 000000000..881037a83 --- /dev/null +++ b/desktop-widgets/modeldelegates.cpp @@ -0,0 +1,617 @@ +#include "modeldelegates.h" +#include "dive.h" +#include "gettextfromc.h" +#include "mainwindow.h" +#include "cylindermodel.h" +#include "models.h" +#include "starwidget.h" +#include "profile/profilewidget2.h" +#include "tankinfomodel.h" +#include "weigthsysteminfomodel.h" +#include "weightmodel.h" +#include "divetripmodel.h" +#include "qthelper.h" +#ifndef NO_MARBLE +#include "globe.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +QSize DiveListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return QSize(50, 22); +} + +// Gets the index of the model in the currentRow and column. +// currCombo is defined below. +#define IDX(_XX) mymodel->index(currCombo.currRow, (_XX)) +static bool keyboardFinished = false; + +StarWidgetsDelegate::StarWidgetsDelegate(QWidget *parent) : QStyledItemDelegate(parent), + parentWidget(parent) +{ + const IconMetrics& metrics = defaultIconMetrics(); + minStarSize = QSize(metrics.sz_small * TOTALSTARS + metrics.spacing * (TOTALSTARS - 1), metrics.sz_small); +} + +void StarWidgetsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyledItemDelegate::paint(painter, option, index); + if (!index.isValid()) + return; + + QVariant value = index.model()->data(index, DiveTripModel::STAR_ROLE); + if (!value.isValid()) + return; + + int rating = value.toInt(); + int deltaY = option.rect.height() / 2 - StarWidget::starActive().height() / 2; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + const QPixmap active = QPixmap::fromImage(StarWidget::starActive()); + const QPixmap inactive = QPixmap::fromImage(StarWidget::starInactive()); + const IconMetrics& metrics = defaultIconMetrics(); + + for (int i = 0; i < rating; i++) + painter->drawPixmap(option.rect.x() + i * metrics.sz_small + metrics.spacing, option.rect.y() + deltaY, active); + for (int i = rating; i < TOTALSTARS; i++) + painter->drawPixmap(option.rect.x() + i * metrics.sz_small + metrics.spacing, option.rect.y() + deltaY, inactive); + painter->restore(); +} + +QSize StarWidgetsDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return minStarSize; +} + +const QSize& StarWidgetsDelegate::starSize() const +{ + return minStarSize; +} + +ComboBoxDelegate::ComboBoxDelegate(QAbstractItemModel *model, QObject *parent) : QStyledItemDelegate(parent), model(model) +{ + connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), + this, SLOT(revertModelData(QWidget *, QAbstractItemDelegate::EndEditHint))); + connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), + this, SLOT(fixTabBehavior())); +} + +void ComboBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QComboBox *c = qobject_cast(editor); + QString data = index.model()->data(index, Qt::DisplayRole).toString(); + int i = c->findText(data); + if (i != -1) + c->setCurrentIndex(i); + else + c->setEditText(data); + c->lineEdit()->setSelection(0, c->lineEdit()->text().length()); +} + +struct CurrSelected { + QComboBox *comboEditor; + int currRow; + QString activeText; + QAbstractItemModel *model; + bool ignoreSelection; +} currCombo; + +QWidget *ComboBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + MainWindow *m = MainWindow::instance(); + QComboBox *comboDelegate = new QComboBox(parent); + comboDelegate->setModel(model); + comboDelegate->setEditable(true); + comboDelegate->setAutoCompletion(true); + comboDelegate->setAutoCompletionCaseSensitivity(Qt::CaseInsensitive); + comboDelegate->completer()->setCompletionMode(QCompleter::PopupCompletion); + comboDelegate->view()->setEditTriggers(QAbstractItemView::AllEditTriggers); + comboDelegate->lineEdit()->installEventFilter(const_cast(qobject_cast(this))); + if ((m->graphics()->currentState != ProfileWidget2::PROFILE)) + comboDelegate->lineEdit()->setEnabled(false); + comboDelegate->view()->installEventFilter(const_cast(qobject_cast(this))); + QAbstractItemView *comboPopup = comboDelegate->lineEdit()->completer()->popup(); + comboPopup->setMouseTracking(true); + connect(comboDelegate, SIGNAL(highlighted(QString)), this, SLOT(testActivation(QString))); + connect(comboDelegate, SIGNAL(activated(QString)), this, SLOT(fakeActivation())); + connect(comboPopup, SIGNAL(entered(QModelIndex)), this, SLOT(testActivation(QModelIndex))); + connect(comboPopup, SIGNAL(activated(QModelIndex)), this, SLOT(fakeActivation())); + currCombo.comboEditor = comboDelegate; + currCombo.currRow = index.row(); + currCombo.model = const_cast(index.model()); + keyboardFinished = false; + + // Current display of things on Gnome3 looks like shit, so + // let`s fix that. + if (isGnome3Session()) { + QPalette p; + p.setColor(QPalette::Window, QColor(Qt::white)); + p.setColor(QPalette::Base, QColor(Qt::white)); + comboDelegate->lineEdit()->setPalette(p); + comboDelegate->setPalette(p); + } + return comboDelegate; +} + +/* This Method is being called when the user *writes* something and press enter or tab, + * and it`s also called when the mouse walks over the list of choices from the ComboBox, + * One thing is important, if the user writes a *new* cylinder or weight type, it will + * be ADDED to the list, and the user will need to fill the other data. + */ +void ComboBoxDelegate::testActivation(const QString &currText) +{ + currCombo.activeText = currText.isEmpty() ? currCombo.comboEditor->currentText() : currText; + setModelData(currCombo.comboEditor, currCombo.model, QModelIndex()); +} + +void ComboBoxDelegate::testActivation(const QModelIndex &currIndex) +{ + testActivation(currIndex.data().toString()); +} + +// HACK, send a fake event so Qt thinks we hit 'enter' on the line edit. +void ComboBoxDelegate::fakeActivation() +{ + /* this test is needed because as soon as I show the selector, + * the first item gots selected, this sending an activated signal, + * calling this fakeActivation code and setting as the current, + * thig that we don't want. so, let's just set the ignoreSelection + * to false and be happy, because the next activation ( by click + * or keypress) is real. + */ + if (currCombo.ignoreSelection) { + currCombo.ignoreSelection = false; + return; + } + QKeyEvent ev(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + QStyledItemDelegate::eventFilter(currCombo.comboEditor, &ev); +} + +// This 'reverts' the model data to what we actually choosed, +// becaus e a TAB is being understood by Qt as 'cancel' while +// we are on a QComboBox ( but not on a QLineEdit. +void ComboBoxDelegate::fixTabBehavior() +{ + if (keyboardFinished) { + setModelData(0, 0, QModelIndex()); + } +} + +bool ComboBoxDelegate::eventFilter(QObject *object, QEvent *event) +{ + // Reacts on Key_UP and Key_DOWN to show the QComboBox - list of choices. + if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { + if (object == currCombo.comboEditor) { // the 'LineEdit' part + QKeyEvent *ev = static_cast(event); + if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down) { + currCombo.ignoreSelection = true; + if (!currCombo.comboEditor->completer()->popup()->isVisible()) { + currCombo.comboEditor->showPopup(); + return true; + } + } + if (ev->key() == Qt::Key_Tab || ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { + currCombo.activeText = currCombo.comboEditor->currentText(); + keyboardFinished = true; + } + } else { // the 'Drop Down Menu' part. + QKeyEvent *ev = static_cast(event); + if (ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return || + ev->key() == Qt::Key_Tab || ev->key() == Qt::Key_Backtab || + ev->key() == Qt::Key_Escape) { + // treat Qt as a silly little boy - pretending that the key_return nwas pressed on the combo, + // instead of the list of choices. this can be extended later for + // other imputs, like tab navigation and esc. + QStyledItemDelegate::eventFilter(currCombo.comboEditor, event); + } + } + } + + return QStyledItemDelegate::eventFilter(object, event); +} + +void ComboBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QRect defaultRect = option.rect; + defaultRect.setX(defaultRect.x() - 1); + defaultRect.setY(defaultRect.y() - 1); + defaultRect.setWidth(defaultRect.width() + 2); + defaultRect.setHeight(defaultRect.height() + 2); + editor->setGeometry(defaultRect); +} + +struct RevertCylinderData { + QString type; + int pressure; + int size; +} currCylinderData; + +void TankInfoDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &thisindex) const +{ + CylindersModel *mymodel = qobject_cast(currCombo.model); + TankInfoModel *tanks = TankInfoModel::instance(); + QModelIndexList matches = tanks->match(tanks->index(0, 0), Qt::DisplayRole, currCombo.activeText); + int row; + QString cylinderName = currCombo.activeText; + if (matches.isEmpty()) { + tanks->insertRows(tanks->rowCount(), 1); + tanks->setData(tanks->index(tanks->rowCount() - 1, 0), currCombo.activeText); + row = tanks->rowCount() - 1; + } else { + row = matches.first().row(); + cylinderName = matches.first().data().toString(); + } + int tankSize = tanks->data(tanks->index(row, TankInfoModel::ML)).toInt(); + int tankPressure = tanks->data(tanks->index(row, TankInfoModel::BAR)).toInt(); + + mymodel->setData(IDX(CylindersModel::TYPE), cylinderName, Qt::EditRole); + mymodel->passInData(IDX(CylindersModel::WORKINGPRESS), tankPressure); + mymodel->passInData(IDX(CylindersModel::SIZE), tankSize); +} + +TankInfoDelegate::TankInfoDelegate(QObject *parent) : ComboBoxDelegate(TankInfoModel::instance(), parent) +{ + connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), + this, SLOT(reenableReplot(QWidget *, QAbstractItemDelegate::EndEditHint))); +} + +void TankInfoDelegate::reenableReplot(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ + MainWindow::instance()->graphics()->setReplot(true); + // FIXME: We need to replot after a cylinder is selected but the replot below overwrites + // the newly selected cylinder. + // MainWindow::instance()->graphics()->replot(); +} + +void TankInfoDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ + if (hint == QAbstractItemDelegate::NoHint || + hint == QAbstractItemDelegate::RevertModelCache) { + CylindersModel *mymodel = qobject_cast(currCombo.model); + mymodel->setData(IDX(CylindersModel::TYPE), currCylinderData.type, Qt::EditRole); + mymodel->passInData(IDX(CylindersModel::WORKINGPRESS), currCylinderData.pressure); + mymodel->passInData(IDX(CylindersModel::SIZE), currCylinderData.size); + } +} + +QWidget *TankInfoDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // ncreate editor needs to be called before because it will populate a few + // things in the currCombo global var. + QWidget *delegate = ComboBoxDelegate::createEditor(parent, option, index); + CylindersModel *mymodel = qobject_cast(currCombo.model); + cylinder_t *cyl = mymodel->cylinderAt(index); + currCylinderData.type = copy_string(cyl->type.description); + currCylinderData.pressure = cyl->type.workingpressure.mbar; + currCylinderData.size = cyl->type.size.mliter; + MainWindow::instance()->graphics()->setReplot(false); + return delegate; +} + +TankUseDelegate::TankUseDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +QWidget *TankUseDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + QComboBox *comboBox = new QComboBox(parent); + for (int i = 0; i < NUM_GAS_USE; i++) + comboBox->addItem(gettextFromC::instance()->trGettext(cylinderuse_text[i])); + return comboBox; +} + +void TankUseDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + QComboBox *comboBox = qobject_cast(editor); + QString indexString = index.data().toString(); + comboBox->setCurrentIndex(cylinderuse_from_text(indexString.toUtf8().data())); +} + +void TankUseDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + QComboBox *comboBox = qobject_cast(editor); + model->setData(index, comboBox->currentIndex()); +} + +struct RevertWeightData { + QString type; + int weight; +} currWeight; + +void WSInfoDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ + if (hint == QAbstractItemDelegate::NoHint || + hint == QAbstractItemDelegate::RevertModelCache) { + WeightModel *mymodel = qobject_cast(currCombo.model); + mymodel->setData(IDX(WeightModel::TYPE), currWeight.type, Qt::EditRole); + mymodel->passInData(IDX(WeightModel::WEIGHT), currWeight.weight); + } +} + +void WSInfoDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &thisindex) const +{ + WeightModel *mymodel = qobject_cast(currCombo.model); + WSInfoModel *wsim = WSInfoModel::instance(); + QModelIndexList matches = wsim->match(wsim->index(0, 0), Qt::DisplayRole, currCombo.activeText); + int row; + if (matches.isEmpty()) { + // we need to add this puppy + wsim->insertRows(wsim->rowCount(), 1); + wsim->setData(wsim->index(wsim->rowCount() - 1, 0), currCombo.activeText); + row = wsim->rowCount() - 1; + } else { + row = matches.first().row(); + } + int grams = wsim->data(wsim->index(row, WSInfoModel::GR)).toInt(); + QVariant v = QString(currCombo.activeText); + + mymodel->setData(IDX(WeightModel::TYPE), v, Qt::EditRole); + mymodel->passInData(IDX(WeightModel::WEIGHT), grams); +} + +WSInfoDelegate::WSInfoDelegate(QObject *parent) : ComboBoxDelegate(WSInfoModel::instance(), parent) +{ +} + +QWidget *WSInfoDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + /* First, call the combobox-create editor, it will setup our globals. */ + QWidget *editor = ComboBoxDelegate::createEditor(parent, option, index); + WeightModel *mymodel = qobject_cast(currCombo.model); + weightsystem_t *ws = mymodel->weightSystemAt(index); + currWeight.type = copy_string(ws->description); + currWeight.weight = ws->weight.grams; + return editor; +} + +void AirTypesDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ +} + +void AirTypesDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if (!index.isValid()) + return; + QComboBox *combo = qobject_cast(editor); + model->setData(index, QVariant(combo->currentText())); +} + +AirTypesDelegate::AirTypesDelegate(QObject *parent) : ComboBoxDelegate(GasSelectionModel::instance(), parent) +{ +} + +ProfilePrintDelegate::ProfilePrintDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +static void paintRect(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) +{ + const QRect rect(option.rect); + const int row = index.row(); + const int col = index.column(); + + painter->save(); + // grid color + painter->setPen(QPen(QColor(0xff999999))); + // horizontal lines + if (row == 2 || row == 4 || row == 6) + painter->drawLine(rect.topLeft(), rect.topRight()); + if (row == 7) + painter->drawLine(rect.bottomLeft(), rect.bottomRight()); + // vertical lines + if (row > 1) { + painter->drawLine(rect.topLeft(), rect.bottomLeft()); + if (col == 4 || (col == 0 && row > 5)) + painter->drawLine(rect.topRight(), rect.bottomRight()); + } + painter->restore(); +} + +/* this method overrides the default table drawing method and places grid lines only at certain rows and columns */ +void ProfilePrintDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + paintRect(painter, option, index); + QStyledItemDelegate::paint(painter, option, index); +} + +SpinBoxDelegate::SpinBoxDelegate(int min, int max, int step, QObject *parent): + QStyledItemDelegate(parent), + min(min), + max(max), + step(step) +{ +} + +QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QSpinBox *w = qobject_cast(QStyledItemDelegate::createEditor(parent, option, index)); + w->setRange(min,max); + w->setSingleStep(step); + return w; +} + +DoubleSpinBoxDelegate::DoubleSpinBoxDelegate(double min, double max, double step, QObject *parent): + QStyledItemDelegate(parent), + min(min), + max(max), + step(step) +{ +} + +QWidget *DoubleSpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QDoubleSpinBox *w = qobject_cast(QStyledItemDelegate::createEditor(parent, option, index)); + w->setRange(min,max); + w->setSingleStep(step); + return w; +} + +HTMLDelegate::HTMLDelegate(QObject *parent) : ProfilePrintDelegate(parent) +{ +} + +void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const +{ + paintRect(painter, option, index); + QStyleOptionViewItemV4 options = option; + initStyleOption(&options, index); + painter->save(); + QTextDocument doc; + doc.setHtml(options.text); + doc.setTextWidth(options.rect.width()); + doc.setDefaultFont(options.font); + options.text.clear(); + options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter); + painter->translate(options.rect.left(), options.rect.top()); + QRect clip(0, 0, options.rect.width(), options.rect.height()); + doc.drawContents(painter, clip); + painter->restore(); +} + +QSize HTMLDelegate::sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const +{ + QStyleOptionViewItemV4 options = option; + initStyleOption(&options, index); + QTextDocument doc; + doc.setHtml(options.text); + doc.setTextWidth(options.rect.width()); + return QSize(doc.idealWidth(), doc.size().height()); +} + +LocationFilterDelegate::LocationFilterDelegate(QObject *parent) +{ +} + +void LocationFilterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &origIdx) const +{ + QFont fontBigger = qApp->font(); + QFont fontSmaller = qApp->font(); + QFontMetrics fmBigger(fontBigger); + QStyleOptionViewItemV4 opt = option; + const QAbstractProxyModel *proxyModel = dynamic_cast(origIdx.model()); + QModelIndex index = proxyModel->mapToSource(origIdx); + QStyledItemDelegate::initStyleOption(&opt, index); + QString diveSiteName = index.data().toString(); + QString bottomText; + QIcon icon = index.data(Qt::DecorationRole).value(); + struct dive_site *ds = get_dive_site_by_uuid( + index.model()->data(index.model()->index(index.row(),0)).toInt() + ); + //Special case: do not show name, but instead, show + if (index.row() < 2) { + diveSiteName = index.data().toString(); + bottomText = index.data(Qt::ToolTipRole).toString(); + goto print_part; + } + + if (!ds) + return; + + for (int i = 0; i < 3; i++) { + if (prefs.geocoding.category[i] == TC_NONE) + continue; + int idx = taxonomy_index_for_category(&ds->taxonomy, prefs.geocoding.category[i]); + if (idx == -1) + continue; + if(!bottomText.isEmpty()) + bottomText += " / "; + bottomText += QString(ds->taxonomy.category[idx].value); + } + + if (bottomText.isEmpty()) { + const char *gpsCoords = printGPSCoords(ds->latitude.udeg, ds->longitude.udeg); + bottomText = QString(gpsCoords); + free( (void*) gpsCoords); + } + + if (dive_site_has_gps_location(ds) && dive_site_has_gps_location(&displayed_dive_site)) { + // so we are showing a completion and both the current dive site and the completion + // have a GPS fix... so let's show the distance + if (ds->latitude.udeg == displayed_dive_site.latitude.udeg && + ds->longitude.udeg == displayed_dive_site.longitude.udeg) { + bottomText += tr(" (same GPS fix)"); + } else { + int distanceMeters = get_distance(ds->latitude, ds->longitude, displayed_dive_site.latitude, displayed_dive_site.longitude); + QString distance = distance_string(distanceMeters); + int nr = nr_of_dives_at_dive_site(ds->uuid, false); + bottomText += tr(" (~%1 away").arg(distance); + bottomText += tr(", %n dive(s) here)", "", nr); + } + } + if (bottomText.isEmpty()) { + if (dive_site_has_gps_location(&displayed_dive_site)) + bottomText = tr("(no existing GPS data, add GPS fix from this dive)"); + else + bottomText = tr("(no GPS data)"); + } + bottomText = tr("Pick site: ") + bottomText; + +print_part: + + fontBigger.setPointSize(fontBigger.pointSize() + 1); + fontBigger.setBold(true); + QPen textPen = QPen(option.state & QStyle::State_Selected ? option.palette.highlightedText().color() : option.palette.text().color(), 1); + + initStyleOption(&opt, index); + opt.text = QString(); + opt.icon = QIcon(); + painter->setClipRect(option.rect); + + painter->save(); + if (option.state & QStyle::State_Selected) { + painter->setPen(QPen(opt.palette.highlight().color().darker())); + painter->setBrush(opt.palette.highlight()); + const qreal pad = 1.0; + const qreal pad2 = pad * 2.0; + const qreal rounding = 5.0; + painter->drawRoundedRect(option.rect.x() + pad, + option.rect.y() + pad, + option.rect.width() - pad2, + option.rect.height() - pad2, + rounding, rounding); + } + painter->setPen(textPen); + painter->setFont(fontBigger); + const qreal textPad = 5.0; + painter->drawText(option.rect.x() + textPad, option.rect.y() + fmBigger.boundingRect("YH").height(), diveSiteName); + double pointSize = fontSmaller.pointSizeF(); + fontSmaller.setPointSizeF(0.9 * pointSize); + painter->setFont(fontSmaller); + painter->drawText(option.rect.x() + textPad, option.rect.y() + fmBigger.boundingRect("YH").height() * 2, bottomText); + painter->restore(); + + if (!icon.isNull()) { + painter->save(); + painter->drawPixmap( + option.rect.x() + option.rect.width() - 24, + option.rect.y() + option.rect.height() - 24, icon.pixmap(20,20)); + painter->restore(); + } +} + +QSize LocationFilterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QFont fontBigger = qApp->font(); + fontBigger.setPointSize(fontBigger.pointSize()); + fontBigger.setBold(true); + + QFontMetrics fmBigger(fontBigger); + + QFont fontSmaller = qApp->font(); + QFontMetrics fmSmaller(fontSmaller); + + QSize retSize = QStyledItemDelegate::sizeHint(option, index); + retSize.setHeight( + fmBigger.boundingRect("Yellow House").height() + 5 /*spacing*/ + + fmSmaller.boundingRect("Yellow House").height()); + + return retSize; +} diff --git a/desktop-widgets/modeldelegates.h b/desktop-widgets/modeldelegates.h new file mode 100644 index 000000000..95701775a --- /dev/null +++ b/desktop-widgets/modeldelegates.h @@ -0,0 +1,141 @@ +#ifndef MODELDELEGATES_H +#define MODELDELEGATES_H + +#include +#include +class QPainter; + +class DiveListDelegate : public QStyledItemDelegate { +public: + explicit DiveListDelegate(QObject *parent = 0) + : QStyledItemDelegate(parent) + { + } + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +class StarWidgetsDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit StarWidgetsDelegate(QWidget *parent = 0); + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + const QSize& starSize() const; + +private: + QWidget *parentWidget; + QSize minStarSize; +}; + +class ComboBoxDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit ComboBoxDelegate(QAbstractItemModel *model, QObject *parent = 0); + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; + virtual void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual bool eventFilter(QObject *object, QEvent *event); +public +slots: + void testActivation(const QString &currString = QString()); + void testActivation(const QModelIndex &currIndex); + //HACK: try to get rid of this in the future. + void fakeActivation(); + void fixTabBehavior(); + virtual void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) = 0; + +protected: + QAbstractItemModel *model; +}; + +class TankInfoDelegate : public ComboBoxDelegate { + Q_OBJECT +public: + explicit TankInfoDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +public +slots: + void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); + void reenableReplot(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); +}; + +class TankUseDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit TankUseDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget * editor, const QModelIndex & index) const; +}; + +class WSInfoDelegate : public ComboBoxDelegate { + Q_OBJECT +public: + explicit WSInfoDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +public +slots: + void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); +}; + +class AirTypesDelegate : public ComboBoxDelegate { + Q_OBJECT +public: + explicit AirTypesDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; +public +slots: + void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); +}; + +/* ProfilePrintDelagate: + * this delegate is used to modify the look of the table that is printed + * bellow profiles. + */ +class ProfilePrintDelegate : public QStyledItemDelegate { +public: + explicit ProfilePrintDelegate(QObject *parent = 0); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +class SpinBoxDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + SpinBoxDelegate(int min, int max, int step, QObject *parent = 0); + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +private: + int min; + int max; + int step; +}; + +class DoubleSpinBoxDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + DoubleSpinBoxDelegate(double min, double max, double step, QObject *parent = 0); + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +private: + double min; + double max; + double step; +}; + +class HTMLDelegate : public ProfilePrintDelegate { + Q_OBJECT +public: + explicit HTMLDelegate(QObject *parent = 0); + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +class LocationFilterDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + LocationFilterDelegate(QObject *parent = 0); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; +}; + +#endif // MODELDELEGATES_H diff --git a/desktop-widgets/notificationwidget.cpp b/desktop-widgets/notificationwidget.cpp new file mode 100644 index 000000000..103c0d068 --- /dev/null +++ b/desktop-widgets/notificationwidget.cpp @@ -0,0 +1,42 @@ +#include "notificationwidget.h" + +NotificationWidget::NotificationWidget(QWidget *parent) : KMessageWidget(parent) +{ + future_watcher = new QFutureWatcher(); + connect(future_watcher, SIGNAL(finished()), this, SLOT(finish())); +} + +void NotificationWidget::showNotification(QString message, KMessageWidget::MessageType type) +{ + if (message.isEmpty()) + return; + setText(message); + setCloseButtonVisible(true); + setMessageType(type); + animatedShow(); +} + +void NotificationWidget::hideNotification() +{ + animatedHide(); +} + +QString NotificationWidget::getNotificationText() +{ + return text(); +} + +void NotificationWidget::setFuture(const QFuture &future) +{ + future_watcher->setFuture(future); +} + +void NotificationWidget::finish() +{ + hideNotification(); +} + +NotificationWidget::~NotificationWidget() +{ + delete future_watcher; +} diff --git a/desktop-widgets/notificationwidget.h b/desktop-widgets/notificationwidget.h new file mode 100644 index 000000000..8a551a0b3 --- /dev/null +++ b/desktop-widgets/notificationwidget.h @@ -0,0 +1,32 @@ +#ifndef NOTIFICATIONWIDGET_H +#define NOTIFICATIONWIDGET_H + +#include +#include + +#include + +namespace Ui { + class NotificationWidget; +} + +class NotificationWidget : public KMessageWidget { + Q_OBJECT + +public: + explicit NotificationWidget(QWidget *parent = 0); + void setFuture(const QFuture &future); + void showNotification(QString message, KMessageWidget::MessageType type); + void hideNotification(); + QString getNotificationText(); + ~NotificationWidget(); + +private: + QFutureWatcher *future_watcher; + +private +slots: + void finish(); +}; + +#endif // NOTIFICATIONWIDGET_H diff --git a/desktop-widgets/plannerDetails.ui b/desktop-widgets/plannerDetails.ui new file mode 100644 index 000000000..1f2790d85 --- /dev/null +++ b/desktop-widgets/plannerDetails.ui @@ -0,0 +1,104 @@ + + + plannerDetails + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + + + 16777215 + 20 + + + + <html><head/><body><p><span style=" font-weight:600;">Dive plan details</span></p></body></html> + + + Qt::RichText + + + + + + + + 0 + 0 + + + + Print + + + false + + + false + + + false + + + + + + + + + true + + + + 0 + 0 + + + + font: 13pt "Courier"; + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Courier'; font-size:13pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.Curier New';"><br /></p></body></html> + + + + + + + + diff --git a/desktop-widgets/plannerSettings.ui b/desktop-widgets/plannerSettings.ui new file mode 100644 index 000000000..4db69f883 --- /dev/null +++ b/desktop-widgets/plannerSettings.ui @@ -0,0 +1,749 @@ + + + plannerSettingsWidget + + + + 0 + 0 + 1102 + 442 + + + + Form + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + 1092 + 432 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Rates + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Ascent + + + + 12 + + + + + below 75% avg. depth + + + + + + + m/min + + + 1 + + + + + + + 75% to 50% avg. depth + + + + + + + m/min + + + 1 + + + + + + + 50% avg. depth to 6m + + + + + + + m/min + + + 1 + + + + + + + 6m to surface + + + + + + + m/min + + + 1 + + + + + + + + + + Descent + + + + + + + 0 + 0 + + + + surface to the bottom + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + m/min + + + 1 + + + 18 + + + + + descRate + descent + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Planning + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + VPM-B deco + + + + + + + Bühlmann deco + + + true + + + + + + + Reserve gas + + + 26 + + + + + + + bar + + + + + + 10 + + + 99 + + + 40 + + + + + + + % + + + 1 + + + 150 + + + + + + + Postpone gas change if a stop is not required + + + Only switch at required stops + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + GF low + + + 26 + + + + + + + Plan backgas breaks + + + + + + + GF high + + + 25 + + + + + + + min + + + + + + 0 + + + 9 + + + 1 + + + + + + + Last stop at 6m + + + + + + + Maximize bottom time allowed by gas and no decompression limits + + + Recreational mode + + + + + + + + + + 6 + + + + + + + % + + + 1 + + + 150 + + + + + + + Drop to first depth + + + + + + + Min. switch duration + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::LeftToRight + + + Safety stop + + + false + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + Conservatism level + + + 25 + + + + + + + 4 + + + + + + + + + + Gas options + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + bar + + + 2.000000000000000 + + + 0.100000000000000 + + + 1.400000000000000 + + + + + + + â„“/min + + + 0 + + + 99.000000000000000 + + + + + + + bar + + + 2.000000000000000 + + + 0.100000000000000 + + + 1.600000000000000 + + + + + + + Bottom SAC + + + + + + + Bottom pOâ‚‚ + + + + + + + Notes + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + In dive plan, show runtime (absolute time) of stops + + + Display runtime + + + + + + + true + + + In dive plan, show duration (relative time) of stops + + + Display segment duration + + + + + + + In diveplan, list transitions or treat them as implicit + + + Display transitions in deco + + + + + + + Verbatim dive plan + + + + + + + + + + â„“/min + + + 0 + + + 99.000000000000000 + + + + + + + Deco pOâ‚‚ + + + + + + + Deco SAC + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + + + + + + + scrollArea + ascRate75 + ascRate50 + ascRateStops + ascRateLast6m + descRate + recreational_deco + reserve_gas + safetystop + buehlmann_deco + gflow + gfhigh + vpmb_deco + drop_stone_mode + lastStop + backgasBreaks + switch_at_req_stop + min_switch_duration + rebreathermode + bottomSAC + decoStopSAC + bottompo2 + decopo2 + display_runtime + display_duration + display_transitions + verbatim_plan + + + + diff --git a/desktop-widgets/preferences.cpp b/desktop-widgets/preferences.cpp new file mode 100644 index 000000000..6450c41cb --- /dev/null +++ b/desktop-widgets/preferences.cpp @@ -0,0 +1,559 @@ +#include "preferences.h" +#include "mainwindow.h" +#include "models.h" +#include "divelocationmodel.h" +#include "prefs-macros.h" +#include "qthelper.h" +#include "subsurfacestartup.h" + +#include +#include +#include +#include +#include +#include + +#include "subsurfacewebservices.h" + +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) +#include "socialnetworks.h" +#include +#endif + +PreferencesDialog *PreferencesDialog::instance() +{ + static PreferencesDialog *dialog = new PreferencesDialog(MainWindow::instance()); + return dialog; +} + +PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + ui.setupUi(this); + setAttribute(Qt::WA_QuitOnClose, false); + +#if defined(Q_OS_ANDROID) || !defined(FBSUPPORT) + for (int i = 0; i < ui.listWidget->count(); i++) { + if (ui.listWidget->item(i)->text() == "Facebook") { + delete ui.listWidget->item(i); + QWidget *fbpage = ui.stackedWidget->widget(i); + ui.stackedWidget->removeWidget(fbpage); + } + } +#endif + + ui.proxyType->clear(); + ui.proxyType->addItem(tr("No proxy"), QNetworkProxy::NoProxy); + ui.proxyType->addItem(tr("System proxy"), QNetworkProxy::DefaultProxy); + ui.proxyType->addItem(tr("HTTP proxy"), QNetworkProxy::HttpProxy); + ui.proxyType->addItem(tr("SOCKS proxy"), QNetworkProxy::Socks5Proxy); + ui.proxyType->setCurrentIndex(-1); + + ui.first_item->setModel(GeoReferencingOptionsModel::instance()); + ui.second_item->setModel(GeoReferencingOptionsModel::instance()); + ui.third_item->setModel(GeoReferencingOptionsModel::instance()); + // Facebook stuff: +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) + FacebookManager *fb = FacebookManager::instance(); + facebookWebView = new QWebView(this); + ui.fbWebviewContainer->layout()->addWidget(facebookWebView); + if (fb->loggedIn()) { + facebookLoggedIn(); + } else { + facebookDisconnect(); + } + connect(facebookWebView, &QWebView::urlChanged, fb, &FacebookManager::tryLogin); + connect(fb, &FacebookManager::justLoggedIn, this, &PreferencesDialog::facebookLoggedIn); + connect(ui.fbDisconnect, &QPushButton::clicked, fb, &FacebookManager::logout); + connect(fb, &FacebookManager::justLoggedOut, this, &PreferencesDialog::facebookDisconnect); +#endif + connect(ui.proxyType, SIGNAL(currentIndexChanged(int)), this, SLOT(proxyType_changed(int))); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.gflow, SIGNAL(valueChanged(int)), this, SLOT(gflowChanged(int))); + connect(ui.gfhigh, SIGNAL(valueChanged(int)), this, SLOT(gfhighChanged(int))); + // connect(ui.defaultSetpoint, SIGNAL(valueChanged(double)), this, SLOT(defaultSetpointChanged(double))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + loadSettings(); + setUiFromPrefs(); + rememberPrefs(); +} + +void PreferencesDialog::facebookLoggedIn() +{ +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) + ui.fbDisconnect->show(); + ui.fbWebviewContainer->hide(); + ui.fbWebviewContainer->setEnabled(false); + ui.FBLabel->setText(tr("To disconnect Subsurface from your Facebook account, use the button below")); +#endif +} + +void PreferencesDialog::facebookDisconnect() +{ +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) + // remove the connect/disconnect button + // and instead add the login view + ui.fbDisconnect->hide(); + ui.fbWebviewContainer->show(); + ui.fbWebviewContainer->setEnabled(true); + ui.FBLabel->setText(tr("To connect to Facebook, please log in. This enables Subsurface to publish dives to your timeline")); + if (facebookWebView) { + facebookWebView->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar()); + facebookWebView->setUrl(FacebookManager::instance()->connectUrl()); + } +#endif +} + +void PreferencesDialog::cloudPinNeeded() +{ + ui.cloud_storage_pin->setEnabled(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_pin->setVisible(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_pin_label->setEnabled(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_pin_label->setVisible(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_new_passwd->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.cloud_storage_new_passwd->setVisible(prefs.cloud_verification_status == CS_VERIFIED); + ui.cloud_storage_new_passwd_label->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.cloud_storage_new_passwd_label->setVisible(prefs.cloud_verification_status == CS_VERIFIED); + if (prefs.cloud_verification_status == CS_VERIFIED) { + ui.cloudStorageGroupBox->setTitle(tr("Subsurface cloud storage (credentials verified)")); + ui.cloudDefaultFile->setEnabled(true); + } else { + ui.cloudStorageGroupBox->setTitle(tr("Subsurface cloud storage")); + if (ui.cloudDefaultFile->isChecked()) + ui.noDefaultFile->setChecked(true); + ui.cloudDefaultFile->setEnabled(false); + } + MainWindow::instance()->enableDisableCloudActions(); +} + +#define DANGER_GF (gf > 100) ? "* { color: red; }" : "" +void PreferencesDialog::gflowChanged(int gf) +{ + ui.gflow->setStyleSheet(DANGER_GF); +} +void PreferencesDialog::gfhighChanged(int gf) +{ + ui.gfhigh->setStyleSheet(DANGER_GF); +} +#undef DANGER_GF + +void PreferencesDialog::showEvent(QShowEvent *event) +{ + setUiFromPrefs(); + rememberPrefs(); + QDialog::showEvent(event); +} + +void PreferencesDialog::setUiFromPrefs() +{ + // graphs + ui.pheThreshold->setValue(prefs.pp_graphs.phe_threshold); + ui.po2Threshold->setValue(prefs.pp_graphs.po2_threshold); + ui.pn2Threshold->setValue(prefs.pp_graphs.pn2_threshold); + ui.maxpo2->setValue(prefs.modpO2); + ui.red_ceiling->setChecked(prefs.redceiling); + ui.units_group->setEnabled(ui.personalize->isChecked()); + + ui.gflow->setValue(prefs.gflow); + ui.gfhigh->setValue(prefs.gfhigh); + ui.gf_low_at_maxdepth->setChecked(prefs.gf_low_at_maxdepth); + ui.show_ccr_setpoint->setChecked(prefs.show_ccr_setpoint); + ui.show_ccr_sensors->setChecked(prefs.show_ccr_sensors); + ui.defaultSetpoint->setValue((double)prefs.defaultsetpoint / 1000.0); + ui.psro2rate->setValue(prefs.o2consumption / 1000.0); + ui.pscrfactor->setValue(rint(1000.0 / prefs.pscr_ratio)); + + // units + if (prefs.unit_system == METRIC) + ui.metric->setChecked(true); + else if (prefs.unit_system == IMPERIAL) + ui.imperial->setChecked(true); + else + ui.personalize->setChecked(true); + ui.gpsTraditional->setChecked(prefs.coordinates_traditional); + ui.gpsDecimal->setChecked(!prefs.coordinates_traditional); + + ui.celsius->setChecked(prefs.units.temperature == units::CELSIUS); + ui.fahrenheit->setChecked(prefs.units.temperature == units::FAHRENHEIT); + ui.meter->setChecked(prefs.units.length == units::METERS); + ui.feet->setChecked(prefs.units.length == units::FEET); + ui.bar->setChecked(prefs.units.pressure == units::BAR); + ui.psi->setChecked(prefs.units.pressure == units::PSI); + ui.liter->setChecked(prefs.units.volume == units::LITER); + ui.cuft->setChecked(prefs.units.volume == units::CUFT); + ui.kg->setChecked(prefs.units.weight == units::KG); + ui.lbs->setChecked(prefs.units.weight == units::LBS); + + ui.font->setCurrentFont(QString(prefs.divelist_font)); + ui.fontsize->setValue(prefs.font_size); + ui.defaultfilename->setText(prefs.default_filename); + ui.noDefaultFile->setChecked(prefs.default_file_behavior == NO_DEFAULT_FILE); + ui.cloudDefaultFile->setChecked(prefs.default_file_behavior == CLOUD_DEFAULT_FILE); + ui.localDefaultFile->setChecked(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); + ui.default_cylinder->clear(); + for (int i = 0; tank_info[i].name != NULL; i++) { + ui.default_cylinder->addItem(tank_info[i].name); + if (prefs.default_cylinder && strcmp(tank_info[i].name, prefs.default_cylinder) == 0) + ui.default_cylinder->setCurrentIndex(i); + } + ui.displayinvalid->setChecked(prefs.display_invalid_dives); + ui.display_unused_tanks->setChecked(prefs.display_unused_tanks); + ui.show_average_depth->setChecked(prefs.show_average_depth); + ui.vertical_speed_minutes->setChecked(prefs.units.vertical_speed_time == units::MINUTES); + ui.vertical_speed_seconds->setChecked(prefs.units.vertical_speed_time == units::SECONDS); + ui.velocitySlider->setValue(prefs.animation_speed); + + QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(); + filterModel->setSourceModel(LanguageModel::instance()); + filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui.languageView->setModel(filterModel); + filterModel->sort(0); + connect(ui.languageFilter, SIGNAL(textChanged(QString)), filterModel, SLOT(setFilterFixedString(QString))); + + QSettings s; + + ui.save_uid_local->setChecked(s.value("save_uid_local").toBool()); + ui.default_uid->setText(s.value("subsurface_webservice_uid").toString().toUpper()); + + s.beginGroup("Language"); + ui.languageSystemDefault->setChecked(s.value("UseSystemLanguage", true).toBool()); + QAbstractItemModel *m = ui.languageView->model(); + QModelIndexList languages = m->match(m->index(0, 0), Qt::UserRole, s.value("UiLanguage").toString()); + if (languages.count()) + ui.languageView->setCurrentIndex(languages.first()); + + s.endGroup(); + + ui.proxyHost->setText(prefs.proxy_host); + ui.proxyPort->setValue(prefs.proxy_port); + ui.proxyAuthRequired->setChecked(prefs.proxy_auth); + ui.proxyUsername->setText(prefs.proxy_user); + ui.proxyPassword->setText(prefs.proxy_pass); + ui.proxyType->setCurrentIndex(ui.proxyType->findData(prefs.proxy_type)); + ui.btnUseDefaultFile->setChecked(prefs.use_default_file); + + ui.cloud_storage_email->setText(prefs.cloud_storage_email); + ui.cloud_storage_password->setText(prefs.cloud_storage_password); + ui.save_password_local->setChecked(prefs.save_password_local); + cloudPinNeeded(); + ui.cloud_background_sync->setChecked(prefs.cloud_background_sync); + ui.default_uid->setText(prefs.userid); + + // GeoManagement +#ifdef DISABLED + ui.enable_geocoding->setChecked( prefs.geocoding.enable_geocoding ); + ui.parse_without_gps->setChecked(prefs.geocoding.parse_dive_without_gps); + ui.tag_existing_dives->setChecked(prefs.geocoding.tag_existing_dives); +#endif + ui.first_item->setCurrentIndex(prefs.geocoding.category[0]); + ui.second_item->setCurrentIndex(prefs.geocoding.category[1]); + ui.third_item->setCurrentIndex(prefs.geocoding.category[2]); +} + +void PreferencesDialog::restorePrefs() +{ + prefs = oldPrefs; + setUiFromPrefs(); +} + +void PreferencesDialog::rememberPrefs() +{ + oldPrefs = prefs; +} + +void PreferencesDialog::syncSettings() +{ + QSettings s; + + s.setValue("subsurface_webservice_uid", ui.default_uid->text().toUpper()); + set_save_userid_local(ui.save_uid_local->checkState()); + + // Graph + s.beginGroup("TecDetails"); + SAVE_OR_REMOVE("phethreshold", default_prefs.pp_graphs.phe_threshold, ui.pheThreshold->value()); + SAVE_OR_REMOVE("po2threshold", default_prefs.pp_graphs.po2_threshold, ui.po2Threshold->value()); + SAVE_OR_REMOVE("pn2threshold", default_prefs.pp_graphs.pn2_threshold, ui.pn2Threshold->value()); + SAVE_OR_REMOVE("modpO2", default_prefs.modpO2, ui.maxpo2->value()); + SAVE_OR_REMOVE("redceiling", default_prefs.redceiling, ui.red_ceiling->isChecked()); + SAVE_OR_REMOVE("gflow", default_prefs.gflow, ui.gflow->value()); + SAVE_OR_REMOVE("gfhigh", default_prefs.gfhigh, ui.gfhigh->value()); + SAVE_OR_REMOVE("gf_low_at_maxdepth", default_prefs.gf_low_at_maxdepth, ui.gf_low_at_maxdepth->isChecked()); + SAVE_OR_REMOVE("show_ccr_setpoint", default_prefs.show_ccr_setpoint, ui.show_ccr_setpoint->isChecked()); + SAVE_OR_REMOVE("show_ccr_sensors", default_prefs.show_ccr_sensors, ui.show_ccr_sensors->isChecked()); + SAVE_OR_REMOVE("display_unused_tanks", default_prefs.display_unused_tanks, ui.display_unused_tanks->isChecked()); + SAVE_OR_REMOVE("show_average_depth", default_prefs.show_average_depth, ui.show_average_depth->isChecked()); + s.endGroup(); + + // Units + s.beginGroup("Units"); + QString unitSystem[] = {"metric", "imperial", "personal"}; + short unitValue = ui.metric->isChecked() ? METRIC : (ui.imperial->isChecked() ? IMPERIAL : PERSONALIZE); + SAVE_OR_REMOVE_SPECIAL("unit_system", default_prefs.unit_system, unitValue, unitSystem[unitValue]); + s.setValue("temperature", ui.fahrenheit->isChecked() ? units::FAHRENHEIT : units::CELSIUS); + s.setValue("length", ui.feet->isChecked() ? units::FEET : units::METERS); + s.setValue("pressure", ui.psi->isChecked() ? units::PSI : units::BAR); + s.setValue("volume", ui.cuft->isChecked() ? units::CUFT : units::LITER); + s.setValue("weight", ui.lbs->isChecked() ? units::LBS : units::KG); + s.setValue("vertical_speed_time", ui.vertical_speed_minutes->isChecked() ? units::MINUTES : units::SECONDS); + s.setValue("coordinates", ui.gpsTraditional->isChecked()); + s.endGroup(); + + // Defaults + s.beginGroup("GeneralSettings"); + s.setValue("default_filename", ui.defaultfilename->text()); + s.setValue("default_cylinder", ui.default_cylinder->currentText()); + s.setValue("use_default_file", ui.btnUseDefaultFile->isChecked()); + if (ui.noDefaultFile->isChecked()) + s.setValue("default_file_behavior", NO_DEFAULT_FILE); + else if (ui.localDefaultFile->isChecked()) + s.setValue("default_file_behavior", LOCAL_DEFAULT_FILE); + else if (ui.cloudDefaultFile->isChecked()) + s.setValue("default_file_behavior", CLOUD_DEFAULT_FILE); + s.setValue("defaultsetpoint", rint(ui.defaultSetpoint->value() * 1000.0)); + s.setValue("o2consumption", rint(ui.psro2rate->value() *1000.0)); + s.setValue("pscr_ratio", rint(1000.0 / ui.pscrfactor->value())); + s.endGroup(); + + s.beginGroup("Display"); + SAVE_OR_REMOVE_SPECIAL("divelist_font", system_divelist_default_font, ui.font->currentFont().toString(), ui.font->currentFont()); + SAVE_OR_REMOVE("font_size", system_divelist_default_font_size, ui.fontsize->value()); + s.setValue("displayinvalid", ui.displayinvalid->isChecked()); + s.endGroup(); + s.sync(); + + // Locale + QLocale loc; + s.beginGroup("Language"); + bool useSystemLang = s.value("UseSystemLanguage", true).toBool(); + if (useSystemLang != ui.languageSystemDefault->isChecked() || + (!useSystemLang && s.value("UiLanguage").toString() != ui.languageView->currentIndex().data(Qt::UserRole))) { + QMessageBox::warning(MainWindow::instance(), tr("Restart required"), + tr("To correctly load a new language you must restart Subsurface.")); + } + s.setValue("UseSystemLanguage", ui.languageSystemDefault->isChecked()); + s.setValue("UiLanguage", ui.languageView->currentIndex().data(Qt::UserRole)); + s.endGroup(); + + // Animation + s.beginGroup("Animations"); + s.setValue("animation_speed", ui.velocitySlider->value()); + s.endGroup(); + + s.beginGroup("Network"); + s.setValue("proxy_type", ui.proxyType->itemData(ui.proxyType->currentIndex()).toInt()); + s.setValue("proxy_host", ui.proxyHost->text()); + s.setValue("proxy_port", ui.proxyPort->value()); + SB("proxy_auth", ui.proxyAuthRequired); + s.setValue("proxy_user", ui.proxyUsername->text()); + s.setValue("proxy_pass", ui.proxyPassword->text()); + s.endGroup(); + + s.beginGroup("CloudStorage"); + QString email = ui.cloud_storage_email->text(); + QString password = ui.cloud_storage_password->text(); + QString newpassword = ui.cloud_storage_new_passwd->text(); + if (prefs.cloud_verification_status == CS_VERIFIED && !newpassword.isEmpty()) { + // deal with password change + if (!email.isEmpty() && !password.isEmpty()) { + // connect to backend server to check / create credentials + QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); + if (!reg.match(email).hasMatch() || (!password.isEmpty() && !reg.match(password).hasMatch())) { + report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); + } else { + CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); + connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); + connect(cloudAuth, SIGNAL(passwordChangeSuccessful()), this, SLOT(passwordUpdateSuccessfull())); + QNetworkReply *reply = cloudAuth->backend(email, password, "", newpassword); + ui.cloud_storage_new_passwd->setText(""); + free(prefs.cloud_storage_newpassword); + prefs.cloud_storage_newpassword = strdup(qPrintable(newpassword)); + } + } + } else if (prefs.cloud_verification_status == CS_UNKNOWN || + prefs.cloud_verification_status == CS_INCORRECT_USER_PASSWD || + email != prefs.cloud_storage_email || + password != prefs.cloud_storage_password) { + + // different credentials - reset verification status + prefs.cloud_verification_status = CS_UNKNOWN; + if (!email.isEmpty() && !password.isEmpty()) { + // connect to backend server to check / create credentials + QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); + if (!reg.match(email).hasMatch() || (!password.isEmpty() && !reg.match(password).hasMatch())) { + report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); + } else { + CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); + connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); + QNetworkReply *reply = cloudAuth->backend(email, password); + } + } + } else if (prefs.cloud_verification_status == CS_NEED_TO_VERIFY) { + QString pin = ui.cloud_storage_pin->text(); + if (!pin.isEmpty()) { + // connect to backend server to check / create credentials + QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); + if (!reg.match(email).hasMatch() || !reg.match(password).hasMatch()) { + report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); + } + CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); + connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); + QNetworkReply *reply = cloudAuth->backend(email, password, pin); + } + } + SAVE_OR_REMOVE("email", default_prefs.cloud_storage_email, email); + SAVE_OR_REMOVE("save_password_local", default_prefs.save_password_local, ui.save_password_local->isChecked()); + if (ui.save_password_local->isChecked()) { + SAVE_OR_REMOVE("password", default_prefs.cloud_storage_password, password); + } else { + s.remove("password"); + free(prefs.cloud_storage_password); + prefs.cloud_storage_password = strdup(qPrintable(password)); + } + SAVE_OR_REMOVE("cloud_verification_status", default_prefs.cloud_verification_status, prefs.cloud_verification_status); + SAVE_OR_REMOVE("cloud_background_sync", default_prefs.cloud_background_sync, ui.cloud_background_sync->isChecked()); + + // at this point we intentionally do not have a UI for changing this + // it could go into some sort of "advanced setup" or something + SAVE_OR_REMOVE("cloud_base_url", default_prefs.cloud_base_url, prefs.cloud_base_url); + s.endGroup(); + + s.beginGroup("geocoding"); +#ifdef DISABLED + s.setValue("enable_geocoding", ui.enable_geocoding->isChecked()); + s.setValue("parse_dive_without_gps", ui.parse_without_gps->isChecked()); + s.setValue("tag_existing_dives", ui.tag_existing_dives->isChecked()); +#endif + s.setValue("cat0", ui.first_item->currentIndex()); + s.setValue("cat1", ui.second_item->currentIndex()); + s.setValue("cat2", ui.third_item->currentIndex()); + s.endGroup(); + + loadSettings(); + emit settingsChanged(); +} + +void PreferencesDialog::loadSettings() +{ + // This code was on the mainwindow, it should belong nowhere, but since we didn't + // correctly fixed this code yet ( too much stuff on the code calling preferences ) + // force this here. + loadPreferences(); + QSettings s; + QVariant v; + + ui.save_uid_local->setChecked(s.value("save_uid_local").toBool()); + ui.default_uid->setText(s.value("subsurface_webservice_uid").toString().toUpper()); + + ui.defaultfilename->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); + ui.btnUseDefaultFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); + ui.chooseFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); +} + +void PreferencesDialog::buttonClicked(QAbstractButton *button) +{ + switch (ui.buttonBox->standardButton(button)) { + case QDialogButtonBox::Discard: + restorePrefs(); + syncSettings(); + close(); + break; + case QDialogButtonBox::Apply: + syncSettings(); + break; + case QDialogButtonBox::FirstButton: + syncSettings(); + close(); + break; + default: + break; // ignore warnings. + } +} +#undef SB + +void PreferencesDialog::on_chooseFile_clicked() +{ + QFileInfo fi(system_default_filename()); + QString choosenFileName = QFileDialog::getOpenFileName(this, tr("Open default log file"), fi.absolutePath(), tr("Subsurface XML files (*.ssrf *.xml *.XML)")); + + if (!choosenFileName.isEmpty()) + ui.defaultfilename->setText(choosenFileName); +} + +void PreferencesDialog::on_resetSettings_clicked() +{ + QSettings s; + + QMessageBox response(this); + response.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + response.setDefaultButton(QMessageBox::Cancel); + response.setWindowTitle(tr("Warning")); + response.setText(tr("If you click OK, all settings of Subsurface will be reset to their default values. This will be applied immediately.")); + response.setWindowModality(Qt::WindowModal); + + int result = response.exec(); + if (result == QMessageBox::Ok) { + copy_prefs(&default_prefs, &prefs); + setUiFromPrefs(); + Q_FOREACH (QString key, s.allKeys()) { + s.remove(key); + } + syncSettings(); + close(); + } +} + +void PreferencesDialog::passwordUpdateSuccessfull() +{ + ui.cloud_storage_password->setText(prefs.cloud_storage_password); +} + +void PreferencesDialog::emitSettingsChanged() +{ + emit settingsChanged(); +} + +void PreferencesDialog::proxyType_changed(int idx) +{ + if (idx == -1) { + return; + } + + int proxyType = ui.proxyType->itemData(idx).toInt(); + bool hpEnabled = (proxyType == QNetworkProxy::Socks5Proxy || proxyType == QNetworkProxy::HttpProxy); + ui.proxyHost->setEnabled(hpEnabled); + ui.proxyPort->setEnabled(hpEnabled); + ui.proxyAuthRequired->setEnabled(hpEnabled); + ui.proxyUsername->setEnabled(hpEnabled & ui.proxyAuthRequired->isChecked()); + ui.proxyPassword->setEnabled(hpEnabled & ui.proxyAuthRequired->isChecked()); + ui.proxyAuthRequired->setChecked(ui.proxyAuthRequired->isChecked()); +} + +void PreferencesDialog::on_btnUseDefaultFile_toggled(bool toggle) +{ + if (toggle) { + ui.defaultfilename->setText(system_default_filename()); + ui.defaultfilename->setEnabled(false); + } else { + ui.defaultfilename->setEnabled(true); + } +} + +void PreferencesDialog::on_noDefaultFile_toggled(bool toggle) +{ + prefs.default_file_behavior = NO_DEFAULT_FILE; +} + +void PreferencesDialog::on_localDefaultFile_toggled(bool toggle) +{ + ui.defaultfilename->setEnabled(toggle); + ui.btnUseDefaultFile->setEnabled(toggle); + ui.chooseFile->setEnabled(toggle); + prefs.default_file_behavior = LOCAL_DEFAULT_FILE; +} + +void PreferencesDialog::on_cloudDefaultFile_toggled(bool toggle) +{ + prefs.default_file_behavior = CLOUD_DEFAULT_FILE; +} diff --git a/desktop-widgets/preferences.h b/desktop-widgets/preferences.h new file mode 100644 index 000000000..326b1f964 --- /dev/null +++ b/desktop-widgets/preferences.h @@ -0,0 +1,54 @@ +#ifndef PREFERENCES_H +#define PREFERENCES_H + +#include +#include "pref.h" + +#include "ui_preferences.h" + +#ifndef Q_OS_ANDROID + class QWebView; +#endif + +class QAbstractButton; + +class PreferencesDialog : public QDialog { + Q_OBJECT +public: + static PreferencesDialog *instance(); + void showEvent(QShowEvent *); + void emitSettingsChanged(); + +signals: + void settingsChanged(); +public +slots: + void buttonClicked(QAbstractButton *button); + void on_chooseFile_clicked(); + void on_resetSettings_clicked(); + void syncSettings(); + void loadSettings(); + void restorePrefs(); + void rememberPrefs(); + void gflowChanged(int gf); + void gfhighChanged(int gf); + void proxyType_changed(int idx); + void on_btnUseDefaultFile_toggled(bool toggle); + void on_noDefaultFile_toggled(bool toggle); + void on_localDefaultFile_toggled(bool toggle); + void on_cloudDefaultFile_toggled(bool toggle); + void facebookLoggedIn(); + void facebookDisconnect(); + void cloudPinNeeded(); + void passwordUpdateSuccessfull(); +private: + explicit PreferencesDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + void setUiFromPrefs(); + Ui::PreferencesDialog ui; + struct preferences oldPrefs; + #ifndef Q_OS_ANDROID + QWebView *facebookWebView; + #endif +}; + +#endif // PREFERENCES_H diff --git a/desktop-widgets/preferences.ui b/desktop-widgets/preferences.ui new file mode 100644 index 000000000..de2d79b91 --- /dev/null +++ b/desktop-widgets/preferences.ui @@ -0,0 +1,1883 @@ + + + PreferencesDialog + + + + 0 + 0 + 711 + 662 + + + + Preferences + + + + :/subsurface-icon + + + + + 5 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 24 + 24 + + + + Qt::ElideNone + + + QListView::Static + + + true + + + QListView::Batched + + + 0 + + + + 110 + 40 + + + + QListView::ListMode + + + true + + + true + + + -1 + + + + Defaults + + + + :/subsurface-icon + + + + + + Units + + + + :/units + + + + + + Graph + + + + :/graph + + + + + + Language + + + + :/advanced + + + + + + Network + + + + :/network + + + + + + Facebook + + + + :/facebook + + + + + + Georeference + + + + :/georeference + + + + + + + + + + 0 + 0 + + + + 4 + + + + + 0 + 0 + + + + + 5 + + + 5 + + + + + Lists and tables + + + + 5 + + + + + Font + + + + + + + + + + Font size + + + + + + + + + + + + + Dives + + + + 5 + + + 5 + + + 5 + + + + + Default dive log file + + + + + + + + + No default file + + + defaultFileGroup + + + + + + + &Local default file + + + defaultFileGroup + + + + + + + Clo&ud storage default file + + + defaultFileGroup + + + + + + + + + Local dive log file + + + + + + + + + + + + Use default + + + true + + + + + + + ... + + + + + + + + + Display invalid + + + + + + + + + + + + + + + + + Default cylinder + + + + 5 + + + 5 + + + 5 + + + + + Use default cylinder + + + + + + + + + + + + + + + + + Animations + + + + 5 + + + + + Speed + + + + + + + 500 + + + Qt::Horizontal + + + + + + + 500 + + + + + + + + + + Clear all settings + + + + 5 + + + 5 + + + + + Reset all settings to their default value + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 5 + + + 5 + + + + + Unit system + + + + + + System + + + + + + + &Metric + + + buttonGroup_6 + + + + + + + Imperial + + + buttonGroup_6 + + + + + + + Personali&ze + + + buttonGroup_6 + + + + + + + + + + Individual settings + + + false + + + false + + + + + + Depth + + + + + + + meter + + + buttonGroup + + + + + + + feet + + + buttonGroup + + + + + + + Pressure + + + + + + + bar + + + buttonGroup_2 + + + + + + + psi + + + buttonGroup_2 + + + + + + + Volume + + + + + + + &liter + + + buttonGroup_3 + + + + + + + cu ft + + + buttonGroup_3 + + + + + + + Temperature + + + + + + + celsius + + + buttonGroup_4 + + + + + + + fahrenheit + + + buttonGroup_4 + + + + + + + Weight + + + + + + + kg + + + buttonGroup_5 + + + + + + + lbs + + + buttonGroup_5 + + + + + + + + + + + + Time units + + + + + + Ascent/descent speed denominator + + + + + + + Minutes + + + verticalSpeed + + + + + + + Seconds + + + verticalSpeed + + + + + + + + + + + + GPS coordinates + + + + + + Location Display + + + + + + + traditional (dms) + + + buttonGroup_7 + + + + + + + decimal + + + buttonGroup_7 + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 5 + + + 5 + + + + + Show + + + + + + + + true + + + Threshold when showing pOâ‚‚ + + + + + + + true + + + 0.100000000000000 + + + + + + + + + + + true + + + Threshold when showing pNâ‚‚ + + + + + + + true + + + 0.100000000000000 + + + + + + + + + + + true + + + Threshold when showing pHe + + + + + + + true + + + 0.100000000000000 + + + + + + + + + + + true + + + Max pOâ‚‚ when showing MOD + + + + + + + true + + + 0.100000000000000 + + + + + + + + + + + true + + + Draw dive computer reported ceiling red + + + + + + + + + + + Show unused cylinders in Equipment tab + + + + + + + + + + + Show average depth + + + + + + + + + + + + Misc + + + + + + GFLow + + + + + + + 1 + + + 150 + + + + + + + GFHigh + + + + + + + 1 + + + 150 + + + + + + + GFLow at max depth + + + + + + + CCR: show setpoints when viewing pOâ‚‚ + + + + + + + CCR: show individual Oâ‚‚ sensor values when viewing pOâ‚‚ + + + + + + + Default CCR set-point for dive planning + + + + + + + bar + + + 2 + + + 10.000000000000000 + + + 0.100000000000000 + + + + + + + pSCR Oâ‚‚ metabolism rate + + + + + + + pSCR ratio + + + + + + + â„“/min + + + 3 + + + + + + + + + + 1: + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 5 + + + QLayout::SetNoConstraint + + + 5 + + + + + + 0 + 0 + + + + UI language + + + + + + System default + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Filter + + + + + + + + + + + + + + 0 + 0 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + 0 + 0 + + + + + 5 + + + 5 + + + + + Proxy + + + + + + + 0 + 0 + + + + Port + + + + + + + + + + Host + + + + + + + Qt::LeftToRight + + + Requires authentication + + + + + + + Proxy type + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Username + + + + + + + + 1 + 0 + + + + 65535 + + + 80 + + + + + + + + 0 + 0 + + + + 32 + + + + + + + + 2 + 0 + + + + 64 + + + + + + + Password + + + + + + + + 0 + 0 + + + + 32 + + + QLineEdit::Password + + + + + + + + + + + 0 + 0 + + + + + 0 + 129 + + + + Subsurface cloud storage + + + + + + + + + Email address + + + + + + + Password + + + + + + + Verification PIN + + + + + + + New password + + + + + + + + + + + + + + QLineEdit::Password + + + + + + + + + + + + + + QLineEdit::Password + + + + + + + Sync to cloud in the background? + + + + + + + Save Password locally? + + + + + + + + + + Subsurface web service + + + + 5 + + + 5 + + + + + Default user ID + + + + + + + + + + Save user ID locally? + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 5 + + + 5 + + + + + + + + + 0 + 0 + + + + Connect to facebook text placeholder + + + + + + + + + + + + Disconnect + + + + + + + + + + + + 0 + 0 + + + + + 5 + + + 5 + + + + + Dive Site Layout + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + / + + + + + + + + 0 + 0 + + + + + + + + / + + + + + + + + 0 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Discard|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + PreferencesDialog + accept() + + + 264 + 720 + + + 157 + 274 + + + + + buttonBox + rejected() + PreferencesDialog + reject() + + + 332 + 720 + + + 286 + 274 + + + + + listWidget + currentRowChanged(int) + stackedWidget + setCurrentIndex(int) + + + 37 + 97 + + + 282 + 18 + + + + + personalize + toggled(bool) + units_group + setEnabled(bool) + + + 185 + 19 + + + 186 + 23 + + + + + languageSystemDefault + toggled(bool) + languageView + setDisabled(bool) + + + 231 + 26 + + + 186 + 30 + + + + + languageSystemDefault + toggled(bool) + languageFilter + setDisabled(bool) + + + 231 + 26 + + + 185 + 20 + + + + + imperial + toggled(bool) + feet + setChecked(bool) + + + 164 + 19 + + + 175 + 34 + + + + + metric + toggled(bool) + meter + setChecked(bool) + + + 142 + 19 + + + 153 + 34 + + + + + imperial + toggled(bool) + psi + setChecked(bool) + + + 164 + 19 + + + 175 + 33 + + + + + metric + toggled(bool) + bar + setChecked(bool) + + + 142 + 19 + + + 153 + 33 + + + + + imperial + toggled(bool) + cuft + setChecked(bool) + + + 164 + 19 + + + 175 + 31 + + + + + metric + toggled(bool) + liter + setChecked(bool) + + + 142 + 19 + + + 153 + 31 + + + + + imperial + toggled(bool) + fahrenheit + setChecked(bool) + + + 164 + 19 + + + 175 + 29 + + + + + metric + toggled(bool) + celsius + setChecked(bool) + + + 142 + 19 + + + 153 + 29 + + + + + imperial + toggled(bool) + lbs + setChecked(bool) + + + 164 + 19 + + + 175 + 28 + + + + + metric + toggled(bool) + kg + setChecked(bool) + + + 142 + 19 + + + 153 + 28 + + + + + velocitySlider + valueChanged(int) + velocitySpinBox + setValue(int) + + + 236 + 52 + + + 236 + 52 + + + + + velocitySpinBox + valueChanged(int) + velocitySlider + setValue(int) + + + 236 + 52 + + + 236 + 52 + + + + + proxyAuthRequired + toggled(bool) + proxyUsername + setEnabled(bool) + + + 409 + 123 + + + 409 + 153 + + + + + proxyAuthRequired + toggled(bool) + proxyPassword + setEnabled(bool) + + + 409 + 123 + + + 409 + 183 + + + + + btnUseDefaultFile + toggled(bool) + chooseFile + setHidden(bool) + + + 236 + 44 + + + 236 + 44 + + + + + + + + + + + + + + + + diff --git a/desktop-widgets/printdialog.cpp b/desktop-widgets/printdialog.cpp new file mode 100644 index 000000000..cf08062d2 --- /dev/null +++ b/desktop-widgets/printdialog.cpp @@ -0,0 +1,194 @@ +#include "printdialog.h" +#include "printoptions.h" +#include "mainwindow.h" + +#ifndef NO_PRINTING +#include +#include +#include +#include +#include +#include + +#define SETTINGS_GROUP "PrintDialog" + +template_options::color_palette_struct ssrf_colors, almond_colors, blueshades_colors, custom_colors; + +PrintDialog::PrintDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + // initialize const colors + ssrf_colors.color1 = QColor::fromRgb(0xff, 0xff, 0xff); + ssrf_colors.color2 = QColor::fromRgb(0xa6, 0xbc, 0xd7); + ssrf_colors.color3 = QColor::fromRgb(0xef, 0xf7, 0xff); + ssrf_colors.color4 = QColor::fromRgb(0x34, 0x65, 0xa4); + ssrf_colors.color5 = QColor::fromRgb(0x20, 0x4a, 0x87); + ssrf_colors.color6 = QColor::fromRgb(0x17, 0x37, 0x64); + almond_colors.color1 = QColor::fromRgb(255, 255, 255); + almond_colors.color2 = QColor::fromRgb(253, 204, 156); + almond_colors.color3 = QColor::fromRgb(243, 234, 207); + almond_colors.color4 = QColor::fromRgb(136, 160, 150); + almond_colors.color5 = QColor::fromRgb(187, 171, 139); + almond_colors.color6 = QColor::fromRgb(0, 0, 0); + blueshades_colors.color1 = QColor::fromRgb(255, 255, 255); + blueshades_colors.color2 = QColor::fromRgb(142, 152, 166); + blueshades_colors.color3 = QColor::fromRgb(182, 192, 206); + blueshades_colors.color4 = QColor::fromRgb(31, 49, 75); + blueshades_colors.color5 = QColor::fromRgb(21, 45, 84); + blueshades_colors.color6 = QColor::fromRgb(0, 0, 0); + + // check if the options were previously stored in the settings; if not use some defaults. + QSettings s; + bool stored = s.childGroups().contains(SETTINGS_GROUP); + if (!stored) { + printOptions.print_selected = true; + printOptions.color_selected = true; + printOptions.landscape = false; + printOptions.p_template = "one_dive.html"; + printOptions.type = print_options::DIVELIST; + templateOptions.font_index = 0; + templateOptions.font_size = 9; + templateOptions.color_palette_index = SSRF_COLORS; + templateOptions.line_spacing = 1; + custom_colors = ssrf_colors; + } else { + s.beginGroup(SETTINGS_GROUP); + printOptions.type = (print_options::print_type)s.value("type").toInt(); + printOptions.print_selected = s.value("print_selected").toBool(); + printOptions.color_selected = s.value("color_selected").toBool(); + printOptions.landscape = s.value("landscape").toBool(); + printOptions.p_template = s.value("template_selected").toString(); + qprinter.setOrientation((QPrinter::Orientation)printOptions.landscape); + templateOptions.font_index = s.value("font").toInt(); + templateOptions.font_size = s.value("font_size").toDouble(); + templateOptions.color_palette_index = s.value("color_palette").toInt(); + templateOptions.line_spacing = s.value("line_spacing").toDouble(); + custom_colors.color1 = QColor(s.value("custom_color_1").toString()); + custom_colors.color2 = QColor(s.value("custom_color_2").toString()); + custom_colors.color3 = QColor(s.value("custom_color_3").toString()); + custom_colors.color4 = QColor(s.value("custom_color_4").toString()); + custom_colors.color5 = QColor(s.value("custom_color_5").toString()); + } + + // handle cases from old QSettings group + if (templateOptions.font_size < 9) { + templateOptions.font_size = 9; + } + if (templateOptions.line_spacing < 1) { + templateOptions.line_spacing = 1; + } + + switch (templateOptions.color_palette_index) { + case SSRF_COLORS: // default Subsurface derived colors + templateOptions.color_palette = ssrf_colors; + break; + case ALMOND: // almond + templateOptions.color_palette = almond_colors; + break; + case BLUESHADES: // blueshades + templateOptions.color_palette = blueshades_colors; + break; + case CUSTOM: // custom + templateOptions.color_palette = custom_colors; + break; + } + + // create a print options object and pass our options struct + optionsWidget = new PrintOptions(this, &printOptions, &templateOptions); + + // create a new printer object + printer = new Printer(&qprinter, &printOptions, &templateOptions, Printer::PRINT); + + QVBoxLayout *layout = new QVBoxLayout(this); + setLayout(layout); + + layout->addWidget(optionsWidget); + + progressBar = new QProgressBar(); + progressBar->setMinimum(0); + progressBar->setMaximum(100); + progressBar->setValue(0); + progressBar->setTextVisible(false); + layout->addWidget(progressBar); + + QHBoxLayout *hLayout = new QHBoxLayout(); + layout->addLayout(hLayout); + + QPushButton *printButton = new QPushButton(tr("P&rint")); + connect(printButton, SIGNAL(clicked(bool)), this, SLOT(printClicked())); + + QPushButton *previewButton = new QPushButton(tr("&Preview")); + connect(previewButton, SIGNAL(clicked(bool)), this, SLOT(previewClicked())); + + QDialogButtonBox *buttonBox = new QDialogButtonBox; + buttonBox->addButton(QDialogButtonBox::Cancel); + buttonBox->addButton(printButton, QDialogButtonBox::AcceptRole); + buttonBox->addButton(previewButton, QDialogButtonBox::ActionRole); + + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + hLayout->addWidget(buttonBox); + + setWindowTitle(tr("Print")); + setWindowIcon(QIcon(":subsurface-icon")); + + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + + // seems to be the most reliable way to track for all sorts of dialog disposal. + connect(this, SIGNAL(finished(int)), this, SLOT(onFinished())); +} + +void PrintDialog::onFinished() +{ + QSettings s; + s.beginGroup(SETTINGS_GROUP); + + // save print paper settings + s.setValue("type", printOptions.type); + s.setValue("print_selected", printOptions.print_selected); + s.setValue("color_selected", printOptions.color_selected); + s.setValue("template_selected", printOptions.p_template); + + // save template settings + s.setValue("font", templateOptions.font_index); + s.setValue("font_size", templateOptions.font_size); + s.setValue("color_palette", templateOptions.color_palette_index); + s.setValue("line_spacing", templateOptions.line_spacing); + + // save custom colors + s.setValue("custom_color_1", custom_colors.color1.name()); + s.setValue("custom_color_2", custom_colors.color2.name()); + s.setValue("custom_color_3", custom_colors.color3.name()); + s.setValue("custom_color_4", custom_colors.color4.name()); + s.setValue("custom_color_5", custom_colors.color5.name()); +} + +void PrintDialog::previewClicked(void) +{ + QPrintPreviewDialog previewDialog(&qprinter, this, Qt::Window + | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint + | Qt::WindowTitleHint); + connect(&previewDialog, SIGNAL(paintRequested(QPrinter *)), this, SLOT(onPaintRequested(QPrinter *))); + previewDialog.exec(); +} + +void PrintDialog::printClicked(void) +{ + QPrintDialog printDialog(&qprinter, this); + if (printDialog.exec() == QDialog::Accepted) { + connect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); + printer->print(); + close(); + } +} + +void PrintDialog::onPaintRequested(QPrinter *printerPtr) +{ + connect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); + printer->print(); + progressBar->setValue(0); + disconnect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); +} +#endif diff --git a/desktop-widgets/printdialog.h b/desktop-widgets/printdialog.h new file mode 100644 index 000000000..a00c4c5d9 --- /dev/null +++ b/desktop-widgets/printdialog.h @@ -0,0 +1,38 @@ +#ifndef PRINTDIALOG_H +#define PRINTDIALOG_H + +#ifndef NO_PRINTING +#include +#include +#include "printoptions.h" +#include "printer.h" +#include "templateedit.h" + +class QProgressBar; +class PrintOptions; +class PrintLayout; + +// should be based on a custom QPrintDialog class +class PrintDialog : public QDialog { + Q_OBJECT + +public: + explicit PrintDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + +private: + PrintOptions *optionsWidget; + QProgressBar *progressBar; + Printer *printer; + QPrinter qprinter; + struct print_options printOptions; + struct template_options templateOptions; + +private +slots: + void onFinished(); + void previewClicked(); + void printClicked(); + void onPaintRequested(QPrinter *); +}; +#endif +#endif // PRINTDIALOG_H diff --git a/desktop-widgets/printer.cpp b/desktop-widgets/printer.cpp new file mode 100644 index 000000000..f0197d446 --- /dev/null +++ b/desktop-widgets/printer.cpp @@ -0,0 +1,273 @@ +#include "printer.h" +#include "templatelayout.h" +#include "statistics.h" +#include "helpers.h" + +#include +#include +#include +#include +#include + +Printer::Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode) +{ + this->paintDevice = paintDevice; + this->printOptions = printOptions; + this->templateOptions = templateOptions; + this->printMode = printMode; + dpi = 0; + done = 0; + webView = new QWebView(); +} + +Printer::~Printer() +{ + delete webView; +} + +void Printer::putProfileImage(QRect profilePlaceholder, QRect viewPort, QPainter *painter, struct dive *dive, QPointer profile) +{ + int x = profilePlaceholder.x() - viewPort.x(); + int y = profilePlaceholder.y() - viewPort.y(); + // use the placeHolder and the viewPort position to calculate the relative position of the dive profile. + QRect pos(x, y, profilePlaceholder.width(), profilePlaceholder.height()); + profile->plotDive(dive, true); + + if (!printOptions->color_selected) { + QImage image(pos.width(), pos.height(), QImage::Format_ARGB32); + QPainter imgPainter(&image); + imgPainter.setRenderHint(QPainter::Antialiasing); + imgPainter.setRenderHint(QPainter::SmoothPixmapTransform); + profile->render(&imgPainter, QRect(0, 0, pos.width(), pos.height())); + imgPainter.end(); + + // convert QImage to grayscale before rendering + for (int i = 0; i < image.height(); i++) { + QRgb *pixel = reinterpret_cast(image.scanLine(i)); + QRgb *end = pixel + image.width(); + for (; pixel != end; pixel++) { + int gray_val = qGray(*pixel); + *pixel = QColor(gray_val, gray_val, gray_val).rgb(); + } + } + + painter->drawImage(pos, image); + } else { + profile->render(painter, pos); + } +} + +void Printer::flowRender() +{ + // add extra padding at the bottom to pages with height not divisible by view port + int paddingBottom = pageSize.height() - (webView->page()->mainFrame()->contentsSize().height() % pageSize.height()); + QString styleString = QString::fromUtf8("padding-bottom: ") + QString::number(paddingBottom) + "px;"; + webView->page()->mainFrame()->findFirstElement("body").setAttribute("style", styleString); + + // render the Qwebview + QPainter painter; + QRect viewPort(0, 0, 0, 0); + painter.begin(paintDevice); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // get all references to dontbreak divs + int start = 0, end = 0; + int fullPageResolution = webView->page()->mainFrame()->contentsSize().height(); + QWebElementCollection dontbreak = webView->page()->mainFrame()->findAllElements(".dontbreak"); + foreach (QWebElement dontbreakElement, dontbreak) { + if ((dontbreakElement.geometry().y() + dontbreakElement.geometry().height()) - start < pageSize.height()) { + // One more element can be placed + end = dontbreakElement.geometry().y() + dontbreakElement.geometry().height(); + } else { + // fill the page with background color + QRect fullPage(0, 0, pageSize.width(), pageSize.height()); + QBrush fillBrush(templateOptions->color_palette.color1); + painter.fillRect(fullPage, fillBrush); + QRegion reigon(0, 0, pageSize.width(), end - start); + viewPort.setRect(0, start, pageSize.width(), end - start); + + // render the base Html template + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); + + // scroll the webview to the next page + webView->page()->mainFrame()->scroll(0, dontbreakElement.geometry().y() - start); + + // rendering progress is 4/5 of total work + emit(progessUpdated((end * 80.0 / fullPageResolution) + done)); + + // add new pages only in print mode, while previewing we don't add new pages + if (printMode == Printer::PRINT) + static_cast(paintDevice)->newPage(); + else { + painter.end(); + return; + } + start = dontbreakElement.geometry().y(); + } + } + // render the remianing page + QRect fullPage(0, 0, pageSize.width(), pageSize.height()); + QBrush fillBrush(templateOptions->color_palette.color1); + painter.fillRect(fullPage, fillBrush); + QRegion reigon(0, 0, pageSize.width(), end - start); + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); + + painter.end(); +} + +void Printer::render(int Pages = 0) +{ + // keep original preferences + QPointer profile = MainWindow::instance()->graphics(); + int profileFrameStyle = profile->frameStyle(); + int animationOriginal = prefs.animation_speed; + double fontScale = profile->getFontPrintScale(); + double printFontScale = 1.0; + + // apply printing settings to profile + profile->setFrameStyle(QFrame::NoFrame); + profile->setPrintMode(true, !printOptions->color_selected); + profile->setToolTipVisibile(false); + prefs.animation_speed = 0; + + // render the Qwebview + QPainter painter; + QRect viewPort(0, 0, pageSize.width(), pageSize.height()); + painter.begin(paintDevice); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // get all refereces to diveprofile class in the Html template + QWebElementCollection collection = webView->page()->mainFrame()->findAllElements(".diveprofile"); + + QSize originalSize = profile->size(); + if (collection.count() > 0) { + printFontScale = (double)collection.at(0).geometry().size().height() / (double)profile->size().height(); + profile->resize(collection.at(0).geometry().size()); + } + profile->setFontPrintScale(printFontScale); + + int elemNo = 0; + for (int i = 0; i < Pages; i++) { + // render the base Html template + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer); + + // render all the dive profiles in the current page + while (elemNo < collection.count() && collection.at(elemNo).geometry().y() < viewPort.y() + viewPort.height()) { + // dive id field should be dive_{{dive_no}} se we remove the first 5 characters + QString diveIdString = collection.at(elemNo).attribute("id"); + int diveId = diveIdString.remove(0, 5).toInt(0, 10); + putProfileImage(collection.at(elemNo).geometry(), viewPort, &painter, get_dive_by_uniq_id(diveId), profile); + elemNo++; + } + + // scroll the webview to the next page + webView->page()->mainFrame()->scroll(0, pageSize.height()); + viewPort.adjust(0, pageSize.height(), 0, pageSize.height()); + + // rendering progress is 4/5 of total work + emit(progessUpdated((i * 80.0 / Pages) + done)); + if (i < Pages - 1 && printMode == Printer::PRINT) + static_cast(paintDevice)->newPage(); + } + painter.end(); + + // return profle settings + profile->setFrameStyle(profileFrameStyle); + profile->setPrintMode(false); + profile->setFontPrintScale(fontScale); + profile->setToolTipVisibile(true); + profile->resize(originalSize); + prefs.animation_speed = animationOriginal; + + //replot the dive after returning the settings + profile->plotDive(0, true); +} + +//value: ranges from 0 : 100 and shows the progress of the templating engine +void Printer::templateProgessUpdated(int value) +{ + done = value / 5; //template progess if 1/5 of total work + emit progessUpdated(done); +} + +void Printer::print() +{ + // we can only print if "PRINT" mode is selected + if (printMode != Printer::PRINT) { + return; + } + + + QPrinter *printerPtr; + printerPtr = static_cast(paintDevice); + + TemplateLayout t(printOptions, templateOptions); + connect(&t, SIGNAL(progressUpdated(int)), this, SLOT(templateProgessUpdated(int))); + dpi = printerPtr->resolution(); + //rendering resolution = selected paper size in inchs * printer dpi + pageSize.setHeight(qCeil(printerPtr->pageRect(QPrinter::Inch).height() * dpi)); + pageSize.setWidth(qCeil(printerPtr->pageRect(QPrinter::Inch).width() * dpi)); + webView->page()->setViewportSize(pageSize); + webView->page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff); + // export border width with at least 1 pixel + templateOptions->border_width = std::max(1, pageSize.width() / 1000); + if (printOptions->type == print_options::DIVELIST) { + webView->setHtml(t.generate()); + } else if (printOptions->type == print_options::STATISTICS ) { + webView->setHtml(t.generateStatistics()); + } + if (printOptions->color_selected && printerPtr->colorMode()) { + printerPtr->setColorMode(QPrinter::Color); + } else { + printerPtr->setColorMode(QPrinter::GrayScale); + } + // apply user settings + int divesPerPage; + + // get number of dives per page from data-numberofdives attribute in the body of the selected template + bool ok; + divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); + if (!ok) { + divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed + //TODO: show warning + } + int Pages; + if (divesPerPage == 0) { + flowRender(); + } else { + Pages = qCeil(getTotalWork(printOptions) / (float)divesPerPage); + render(Pages); + } +} + +void Printer::previewOnePage() +{ + if (printMode == PREVIEW) { + TemplateLayout t(printOptions, templateOptions); + + pageSize.setHeight(paintDevice->height()); + pageSize.setWidth(paintDevice->width()); + webView->page()->setViewportSize(pageSize); + // initialize the border settings + templateOptions->border_width = std::max(1, pageSize.width() / 1000); + if (printOptions->type == print_options::DIVELIST) { + webView->setHtml(t.generate()); + } else if (printOptions->type == print_options::STATISTICS ) { + webView->setHtml(t.generateStatistics()); + } + + bool ok; + int divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); + if (!ok) { + divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed + //TODO: show warning + } + if (divesPerPage == 0) { + flowRender(); + } else { + render(1); + } + } +} diff --git a/desktop-widgets/printer.h b/desktop-widgets/printer.h new file mode 100644 index 000000000..979cacd6a --- /dev/null +++ b/desktop-widgets/printer.h @@ -0,0 +1,48 @@ +#ifndef PRINTER_H +#define PRINTER_H + +#include +#include +#include +#include + +#include "profile/profilewidget2.h" +#include "printoptions.h" +#include "templateedit.h" + +class Printer : public QObject { + Q_OBJECT + +public: + enum PrintMode { + PRINT, + PREVIEW + }; + +private: + QPaintDevice *paintDevice; + QWebView *webView; + print_options *printOptions; + template_options *templateOptions; + QSize pageSize; + PrintMode printMode; + int done; + int dpi; + void render(int Pages); + void flowRender(); + void putProfileImage(QRect box, QRect viewPort, QPainter *painter, struct dive *dive, QPointer profile); + +private slots: + void templateProgessUpdated(int value); + +public: + Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode); + ~Printer(); + void print(); + void previewOnePage(); + +signals: + void progessUpdated(int value); +}; + +#endif //PRINTER_H diff --git a/desktop-widgets/printoptions.cpp b/desktop-widgets/printoptions.cpp new file mode 100644 index 000000000..769c89ff4 --- /dev/null +++ b/desktop-widgets/printoptions.cpp @@ -0,0 +1,189 @@ +#include "printoptions.h" +#include "templateedit.h" +#include "helpers.h" + +#include +#include +#include + +PrintOptions::PrintOptions(QWidget *parent, struct print_options *printOpt, struct template_options *templateOpt) +{ + hasSetupSlots = false; + ui.setupUi(this); + if (parent) + setParent(parent); + if (!printOpt || !templateOpt) + return; + templateOptions = templateOpt; + printOptions = printOpt; + setup(); +} + +void PrintOptions::setup() +{ + // print type radio buttons + switch (printOptions->type) { + case print_options::DIVELIST: + ui.radioDiveListPrint->setChecked(true); + break; + case print_options::STATISTICS: + ui.radioStatisticsPrint->setChecked(true); + break; + } + + setupTemplates(); + + // general print option checkboxes + if (printOptions->color_selected) + ui.printInColor->setChecked(true); + if (printOptions->print_selected) + ui.printSelected->setChecked(true); + + // connect slots only once + if (hasSetupSlots) + return; + + connect(ui.printInColor, SIGNAL(clicked(bool)), this, SLOT(printInColorClicked(bool))); + connect(ui.printSelected, SIGNAL(clicked(bool)), this, SLOT(printSelectedClicked(bool))); + + hasSetupSlots = true; +} + +void PrintOptions::setupTemplates() +{ + if (printOptions->type == print_options::DIVELIST) { + // insert dive list templates in the UI and select the current template + qSort(grantlee_templates); + int current_index = 0, index = 0; + for (QList::iterator i = grantlee_templates.begin(); i != grantlee_templates.end(); ++i) { + if ((*i).compare(printOptions->p_template) == 0) { + current_index = index; + break; + } + index++; + } + ui.printTemplate->clear(); + for (QList::iterator i = grantlee_templates.begin(); i != grantlee_templates.end(); ++i) { + ui.printTemplate->addItem((*i).split('.')[0], QVariant::fromValue(*i)); + } + ui.printTemplate->setCurrentIndex(current_index); + } else if (printOptions->type == print_options::STATISTICS) { + // insert statistics templates in the UI and select the current template + qSort(grantlee_statistics_templates); + int current_index = 0, index = 0; + for (QList::iterator i = grantlee_statistics_templates.begin(); i != grantlee_statistics_templates.end(); ++i) { + if ((*i).compare(printOptions->p_template) == 0) { + current_index = index; + break; + } + index++; + } + ui.printTemplate->clear(); + for (QList::iterator i = grantlee_statistics_templates.begin(); i != grantlee_statistics_templates.end(); ++i) { + ui.printTemplate->addItem((*i).split('.')[0], QVariant::fromValue(*i)); + } + ui.printTemplate->setCurrentIndex(current_index); + } +} + +// print type radio buttons +void PrintOptions::on_radioDiveListPrint_toggled(bool check) +{ + if (check) { + printOptions->type = print_options::DIVELIST; + + // print options + ui.printSelected->setEnabled(true); + + // print template + ui.deleteButton->setEnabled(true); + ui.exportButton->setEnabled(true); + ui.importButton->setEnabled(true); + + setupTemplates(); + } +} + +void PrintOptions::on_radioStatisticsPrint_toggled(bool check) +{ + if (check) { + printOptions->type = print_options::STATISTICS; + + // print options + ui.printSelected->setEnabled(false); + + // print template + ui.deleteButton->setEnabled(false); + ui.exportButton->setEnabled(false); + ui.importButton->setEnabled(false); + + setupTemplates(); + } +} + +// general print option checkboxes +void PrintOptions::printInColorClicked(bool check) +{ + printOptions->color_selected = check; +} + +void PrintOptions::printSelectedClicked(bool check) +{ + printOptions->print_selected = check; +} + + +void PrintOptions::on_printTemplate_currentIndexChanged(int index) +{ + printOptions->p_template = ui.printTemplate->itemData(index).toString(); +} + +void PrintOptions::on_editButton_clicked() +{ + TemplateEdit te(this, printOptions, templateOptions); + te.exec(); + setup(); +} + +void PrintOptions::on_importButton_clicked() +{ + QString filename = QFileDialog::getOpenFileName(this, tr("Import template file"), "", + tr("HTML files (*.html)")); + if (filename.isEmpty()) + return; + QFileInfo fileInfo(filename); + QFile::copy(filename, getPrintingTemplatePathUser() + QDir::separator() + fileInfo.fileName()); + printOptions->p_template = fileInfo.fileName(); + find_all_templates(); + setup(); +} + +void PrintOptions::on_exportButton_clicked() +{ + QString filename = QFileDialog::getSaveFileName(this, tr("Export template files as"), "", + tr("HTML files (*.html)")); + if (filename.isEmpty()) + return; + QFile::copy(getPrintingTemplatePathUser() + QDir::separator() + getSelectedTemplate(), filename); +} + +void PrintOptions::on_deleteButton_clicked() +{ + QString templateName = getSelectedTemplate(); + QMessageBox msgBox; + msgBox.setText(tr("This action cannot be undone!")); + msgBox.setInformativeText(tr("Delete template: %1?").arg(templateName)); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if (msgBox.exec() == QMessageBox::Ok) { + QFile f(getPrintingTemplatePathUser() + QDir::separator() + templateName); + f.remove(); + find_all_templates(); + setup(); + } +} + +QString PrintOptions::getSelectedTemplate() +{ + return ui.printTemplate->currentData().toString(); +} diff --git a/desktop-widgets/printoptions.h b/desktop-widgets/printoptions.h new file mode 100644 index 000000000..9c50b10f3 --- /dev/null +++ b/desktop-widgets/printoptions.h @@ -0,0 +1,88 @@ +#ifndef PRINTOPTIONS_H +#define PRINTOPTIONS_H + +#include + +#include "ui_printoptions.h" + +struct print_options { + enum print_type { + DIVELIST, + STATISTICS + } type; + QString p_template; + bool print_selected; + bool color_selected; + bool landscape; +}; + +struct template_options { + int font_index; + int color_palette_index; + int border_width; + double font_size; + double line_spacing; + struct color_palette_struct { + QColor color1; + QColor color2; + QColor color3; + QColor color4; + QColor color5; + QColor color6; + bool operator!=(const color_palette_struct &other) const { + return other.color1 != color1 + || other.color2 != color2 + || other.color3 != color3 + || other.color4 != color4 + || other.color5 != color5 + || other.color6 != color6; + } + } color_palette; + bool operator!=(const template_options &other) const { + return other.font_index != font_index + || other.color_palette_index != color_palette_index + || other.font_size != font_size + || other.line_spacing != line_spacing + || other.color_palette != color_palette; + } + }; + +extern template_options::color_palette_struct ssrf_colors, almond_colors, blueshades_colors, custom_colors; + +enum color_palette { + SSRF_COLORS, + ALMOND, + BLUESHADES, + CUSTOM +}; + +// should be based on a custom QPrintDialog class +class PrintOptions : public QWidget { + Q_OBJECT + +public: + explicit PrintOptions(QWidget *parent, struct print_options *printOpt, struct template_options *templateOpt); + void setup(); + QString getSelectedTemplate(); + +private: + Ui::PrintOptions ui; + struct print_options *printOptions; + struct template_options *templateOptions; + bool hasSetupSlots; + void setupTemplates(); + +private +slots: + void printInColorClicked(bool check); + void printSelectedClicked(bool check); + void on_radioStatisticsPrint_toggled(bool check); + void on_radioDiveListPrint_toggled(bool check); + void on_printTemplate_currentIndexChanged(int index); + void on_editButton_clicked(); + void on_importButton_clicked(); + void on_exportButton_clicked(); + void on_deleteButton_clicked(); +}; + +#endif // PRINTOPTIONS_H diff --git a/desktop-widgets/printoptions.ui b/desktop-widgets/printoptions.ui new file mode 100644 index 000000000..1c2523d39 --- /dev/null +++ b/desktop-widgets/printoptions.ui @@ -0,0 +1,168 @@ + + + PrintOptions + + + + 0 + 0 + 367 + 433 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Print type + + + + + + + 0 + 0 + + + + &Dive list print + + + true + + + + + + + + 0 + 0 + + + + &Statistics print + + + + + + + + + + Print options + + + + + + + 0 + 0 + + + + Print only selected dives + + + + + + + + 0 + 0 + + + + Print in color + + + + + + + + + + Template + + + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Edit + + + + + + + Delete + + + + + + + Export + + + + + + + Import + + + + + + + + + + + + + + radioDiveListPrint + printSelected + printInColor + + + + diff --git a/desktop-widgets/profile/animationfunctions.cpp b/desktop-widgets/profile/animationfunctions.cpp new file mode 100644 index 000000000..a19d50c9d --- /dev/null +++ b/desktop-widgets/profile/animationfunctions.cpp @@ -0,0 +1,75 @@ +#include "animationfunctions.h" +#include "pref.h" +#include + +namespace Animations { + + void hide(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 0); + } + } + + void show(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + animation->setStartValue(0); + animation->setEndValue(1); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 1); + } + } + + void animDelete(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + obj->connect(animation, SIGNAL(finished()), SLOT(deleteLater())); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 0); + } + } + + void moveTo(QObject *obj, qreal x, qreal y) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "pos"); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(obj->property("pos").toPointF()); + animation->setEndValue(QPointF(x, y)); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("pos", QPointF(x, y)); + } + } + + void scaleTo(QObject *obj, qreal scale) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "scale"); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(obj->property("scale").toReal()); + animation->setEndValue(QVariant::fromValue(scale)); + animation->setEasingCurve(QEasingCurve::InCubic); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("scale", QVariant::fromValue(scale)); + } + } + + void moveTo(QObject *obj, const QPointF &pos) + { + moveTo(obj, pos.x(), pos.y()); + } +} diff --git a/desktop-widgets/profile/animationfunctions.h b/desktop-widgets/profile/animationfunctions.h new file mode 100644 index 000000000..3cfcff563 --- /dev/null +++ b/desktop-widgets/profile/animationfunctions.h @@ -0,0 +1,18 @@ +#ifndef ANIMATIONFUNCTIONS_H +#define ANIMATIONFUNCTIONS_H + +#include +#include + +class QObject; + +namespace Animations { + void hide(QObject *obj); + void show(QObject *obj); + void moveTo(QObject *obj, qreal x, qreal y); + void moveTo(QObject *obj, const QPointF &pos); + void animDelete(QObject *obj); + void scaleTo(QObject *obj, qreal scale); +} + +#endif // ANIMATIONFUNCTIONS_H diff --git a/desktop-widgets/profile/divecartesianaxis.cpp b/desktop-widgets/profile/divecartesianaxis.cpp new file mode 100644 index 000000000..bf5a5380c --- /dev/null +++ b/desktop-widgets/profile/divecartesianaxis.cpp @@ -0,0 +1,459 @@ +#include "divecartesianaxis.h" +#include "divetextitem.h" +#include "helpers.h" +#include "preferences.h" +#include "diveplotdatamodel.h" +#include "animationfunctions.h" +#include "mainwindow.h" +#include "divelineitem.h" +#include "profilewidget2.h" + +QPen DiveCartesianAxis::gridPen() +{ + QPen pen; + pen.setColor(getColor(TIME_GRID)); + /* cosmetic width() == 0 for lines in printMode + * having setCosmetic(true) and width() > 0 does not work when + * printing on OSX and Linux */ + pen.setWidth(DiveCartesianAxis::printMode ? 0 : 2); + pen.setCosmetic(true); + return pen; +} + +double DiveCartesianAxis::tickInterval() const +{ + return interval; +} + +double DiveCartesianAxis::tickSize() const +{ + return tick_size; +} + +void DiveCartesianAxis::setFontLabelScale(qreal scale) +{ + labelScale = scale; + changed = true; +} + +void DiveCartesianAxis::setPrintMode(bool mode) +{ + printMode = mode; + // update the QPen of all lines depending on printMode + QPen newPen = gridPen(); + QColor oldColor = pen().brush().color(); + newPen.setBrush(oldColor); + setPen(newPen); + Q_FOREACH (DiveLineItem *item, lines) + item->setPen(pen()); +} + +void DiveCartesianAxis::setMaximum(double maximum) +{ + if (IS_FP_SAME(max, maximum)) + return; + max = maximum; + changed = true; + emit maxChanged(); +} + +void DiveCartesianAxis::setMinimum(double minimum) +{ + if (IS_FP_SAME(min, minimum)) + return; + min = minimum; + changed = true; +} + +void DiveCartesianAxis::setTextColor(const QColor &color) +{ + textColor = color; +} + +DiveCartesianAxis::DiveCartesianAxis() : QObject(), + QGraphicsLineItem(), + printMode(false), + unitSystem(0), + orientation(LeftToRight), + min(0), + max(0), + interval(1), + tick_size(0), + textVisibility(true), + lineVisibility(true), + labelScale(1.0), + line_size(1), + changed(true) +{ + setPen(gridPen()); +} + +DiveCartesianAxis::~DiveCartesianAxis() +{ +} + +void DiveCartesianAxis::setLineSize(qreal lineSize) +{ + line_size = lineSize; + changed = true; +} + +void DiveCartesianAxis::setOrientation(Orientation o) +{ + orientation = o; + changed = true; +} + +QColor DiveCartesianAxis::colorForValue(double value) +{ + return QColor(Qt::black); +} + +void DiveCartesianAxis::setTextVisible(bool arg1) +{ + if (textVisibility == arg1) { + return; + } + textVisibility = arg1; + Q_FOREACH (DiveTextItem *item, labels) { + item->setVisible(textVisibility); + } +} + +void DiveCartesianAxis::setLinesVisible(bool arg1) +{ + if (lineVisibility == arg1) { + return; + } + lineVisibility = arg1; + Q_FOREACH (DiveLineItem *item, lines) { + item->setVisible(lineVisibility); + } +} + +template +void emptyList(QList &list, double steps) +{ + if (!list.isEmpty() && list.size() > steps) { + while (list.size() > steps) { + T *removedItem = list.takeLast(); + Animations::animDelete(removedItem); + } + } +} + +void DiveCartesianAxis::updateTicks(color_indice_t color) +{ + if (!scene() || (!changed && !MainWindow::instance()->graphics()->getPrintMode())) + return; + QLineF m = line(); + // unused so far: + // QGraphicsView *view = scene()->views().first(); + double steps = (max - min) / interval; + double currValueText = min; + double currValueLine = min; + + if (steps < 1) + return; + + emptyList(labels, steps); + emptyList(lines, steps); + + // Move the remaining Ticks / Text to it's corerct position + // Regartind the possibly new values for the Axis + qreal begin, stepSize; + if (orientation == TopToBottom) { + begin = m.y1(); + stepSize = (m.y2() - m.y1()); + } else if (orientation == BottomToTop) { + begin = m.y2(); + stepSize = (m.y2() - m.y1()); + } else if (orientation == LeftToRight) { + begin = m.x1(); + stepSize = (m.x2() - m.x1()); + } else /* if (orientation == RightToLeft) */ { + begin = m.x2(); + stepSize = (m.x2() - m.x1()); + } + stepSize = stepSize / steps; + + for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) { + qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? + begin + i * stepSize : + begin - i * stepSize; + + labels[i]->setText(textForValue(currValueText)); + if (orientation == LeftToRight || orientation == RightToLeft) { + Animations::moveTo(labels[i],childPos, m.y1() + tick_size); + } else { + Animations::moveTo(labels[i],m.x1() - tick_size, childPos); + } + } + + for (int i = 0, count = lines.size(); i < count; i++, currValueLine += interval) { + qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? + begin + i * stepSize : + begin - i * stepSize; + + if (orientation == LeftToRight || orientation == RightToLeft) { + Animations::moveTo(lines[i],childPos, m.y1()); + } else { + Animations::moveTo(lines[i],m.x1(), childPos); + } + } + + // Add's the rest of the needed Ticks / Text. + for (int i = labels.size(); i < steps; i++, currValueText += interval) { + qreal childPos; + if (orientation == TopToBottom || orientation == LeftToRight) { + childPos = begin + i * stepSize; + } else { + childPos = begin - i * stepSize; + } + DiveTextItem *label = new DiveTextItem(this); + label->setText(textForValue(currValueText)); + label->setBrush(colorForValue(currValueText)); + label->setScale(fontLabelScale()); + label->setZValue(1); + labels.push_back(label); + if (orientation == RightToLeft || orientation == LeftToRight) { + label->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); + label->setPos(scene()->sceneRect().width() + 10, m.y1() + tick_size); // position it outside of the scene); + Animations::moveTo(label,childPos, m.y1() + tick_size); + } else { + label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); + label->setPos(m.x1() - tick_size, scene()->sceneRect().height() + 10); + Animations::moveTo(label,m.x1() - tick_size, childPos); + } + } + + // Add's the rest of the needed Ticks / Text. + for (int i = lines.size(); i < steps; i++, currValueText += interval) { + qreal childPos; + if (orientation == TopToBottom || orientation == LeftToRight) { + childPos = begin + i * stepSize; + } else { + childPos = begin - i * stepSize; + } + DiveLineItem *line = new DiveLineItem(this); + QPen pen = gridPen(); + pen.setBrush(getColor(color)); + line->setPen(pen); + line->setZValue(0); + lines.push_back(line); + if (orientation == RightToLeft || orientation == LeftToRight) { + line->setLine(0, -line_size, 0, 0); + line->setPos(scene()->sceneRect().width() + 10, m.y1()); // position it outside of the scene); + Animations::moveTo(line,childPos, m.y1()); + } else { + QPointF p1 = mapFromScene(3, 0); + QPointF p2 = mapFromScene(line_size, 0); + line->setLine(p1.x(), 0, p2.x(), 0); + line->setPos(m.x1(), scene()->sceneRect().height() + 10); + Animations::moveTo(line,m.x1(), childPos); + } + } + + Q_FOREACH (DiveTextItem *item, labels) + item->setVisible(textVisibility); + Q_FOREACH (DiveLineItem *item, lines) + item->setVisible(lineVisibility); + changed = false; +} + +void DiveCartesianAxis::setLine(const QLineF &line) +{ + QGraphicsLineItem::setLine(line); + changed = true; +} + +void DiveCartesianAxis::animateChangeLine(const QLineF &newLine) +{ + setLine(newLine); + updateTicks(); + sizeChanged(); +} + +QString DiveCartesianAxis::textForValue(double value) +{ + return QString::number(value); +} + +void DiveCartesianAxis::setTickSize(qreal size) +{ + tick_size = size; +} + +void DiveCartesianAxis::setTickInterval(double i) +{ + interval = i; +} + +qreal DiveCartesianAxis::valueAt(const QPointF &p) const +{ + QLineF m = line(); + QPointF relativePosition = p; + relativePosition -= pos(); // normalize p based on the axis' offset on screen + + double retValue = (orientation == LeftToRight || orientation == RightToLeft) ? + max * (relativePosition.x() - m.x1()) / (m.x2() - m.x1()) : + max * (relativePosition.y() - m.y1()) / (m.y2() - m.y1()); + return retValue; +} + +qreal DiveCartesianAxis::posAtValue(qreal value) +{ + QLineF m = line(); + QPointF p = pos(); + + double size = max - min; + // unused for now: + // double distanceFromOrigin = value - min; + double percent = IS_FP_SAME(min, max) ? 0.0 : (value - min) / size; + + + double realSize = orientation == LeftToRight || orientation == RightToLeft ? + m.x2() - m.x1() : + m.y2() - m.y1(); + + // Inverted axis, just invert the percentage. + if (orientation == RightToLeft || orientation == BottomToTop) + percent = 1 - percent; + + double retValue = realSize * percent; + double adjusted = + orientation == LeftToRight ? retValue + m.x1() + p.x() : + orientation == RightToLeft ? retValue + m.x1() + p.x() : + orientation == TopToBottom ? retValue + m.y1() + p.y() : + /* entation == BottomToTop */ retValue + m.y1() + p.y(); + return adjusted; +} + +qreal DiveCartesianAxis::percentAt(const QPointF &p) +{ + qreal value = valueAt(p); + double size = max - min; + double percent = value / size; + return percent; +} + +double DiveCartesianAxis::maximum() const +{ + return max; +} + +double DiveCartesianAxis::minimum() const +{ + return min; +} + +double DiveCartesianAxis::fontLabelScale() const +{ + return labelScale; +} + +void DiveCartesianAxis::setColor(const QColor &color) +{ + QPen defaultPen = gridPen(); + defaultPen.setColor(color); + defaultPen.setJoinStyle(Qt::RoundJoin); + defaultPen.setCapStyle(Qt::RoundCap); + setPen(defaultPen); +} + +QString DepthAxis::textForValue(double value) +{ + if (value == 0) + return QString(); + return get_depth_string(value, false, false); +} + +QColor DepthAxis::colorForValue(double value) +{ + Q_UNUSED(value); + return QColor(Qt::red); +} + +DepthAxis::DepthAxis() +{ + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + changed = true; + settingsChanged(); +} + +void DepthAxis::settingsChanged() +{ + static int unitSystem = prefs.units.length; + if ( unitSystem == prefs.units.length ) + return; + changed = true; + updateTicks(); + unitSystem = prefs.units.length; +} + +QColor TimeAxis::colorForValue(double value) +{ + Q_UNUSED(value); + return QColor(Qt::blue); +} + +QString TimeAxis::textForValue(double value) +{ + int nr = value / 60; + if (maximum() < 600) + return QString("%1:%2").arg(nr).arg((int)value % 60, 2, 10, QChar('0')); + return QString::number(nr); +} + +void TimeAxis::updateTicks() +{ + DiveCartesianAxis::updateTicks(); + if (maximum() > 600) { + for (int i = 0; i < labels.count(); i++) { + labels[i]->setVisible(i % 2); + } + } +} + +QString TemperatureAxis::textForValue(double value) +{ + return QString::number(mkelvin_to_C((int)value)); +} + +PartialGasPressureAxis::PartialGasPressureAxis() : + DiveCartesianAxis(), + model(NULL) +{ + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void PartialGasPressureAxis::setModel(DivePlotDataModel *m) +{ + model = m; + connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(settingsChanged())); + settingsChanged(); +} + +void PartialGasPressureAxis::settingsChanged() +{ + bool showPhe = prefs.pp_graphs.phe; + bool showPn2 = prefs.pp_graphs.pn2; + bool showPo2 = prefs.pp_graphs.po2; + setVisible(showPhe || showPn2 || showPo2); + if (!model->rowCount()) + return; + + double max = showPhe ? model->pheMax() : -1; + if (showPn2 && model->pn2Max() > max) + max = model->pn2Max(); + if (showPo2 && model->po2Max() > max) + max = model->po2Max(); + + qreal pp = floor(max * 10.0) / 10.0 + 0.2; + if (IS_FP_SAME(maximum(), pp)) + return; + + setMaximum(pp); + setTickInterval(pp > 4 ? 0.5 : 0.25); + updateTicks(); +} diff --git a/desktop-widgets/profile/divecartesianaxis.h b/desktop-widgets/profile/divecartesianaxis.h new file mode 100644 index 000000000..cc7d0bcf7 --- /dev/null +++ b/desktop-widgets/profile/divecartesianaxis.h @@ -0,0 +1,122 @@ +#ifndef DIVECARTESIANAXIS_H +#define DIVECARTESIANAXIS_H + +#include +#include +#include "subsurface-core/color.h" + +class QPropertyAnimation; +class DiveTextItem; +class DiveLineItem; +class DivePlotDataModel; + +class DiveCartesianAxis : public QObject, public QGraphicsLineItem { + Q_OBJECT + Q_PROPERTY(QLineF line WRITE setLine READ line) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +private: + bool printMode; + QPen gridPen(); +public: + enum Orientation { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft + }; + DiveCartesianAxis(); + virtual ~DiveCartesianAxis(); + void setPrintMode(bool mode); + void setMinimum(double minimum); + void setMaximum(double maximum); + void setTickInterval(double interval); + void setOrientation(Orientation orientation); + void setTickSize(qreal size); + void setFontLabelScale(qreal scale); + double minimum() const; + double maximum() const; + double tickInterval() const; + double tickSize() const; + double fontLabelScale() const; + qreal valueAt(const QPointF &p) const; + qreal percentAt(const QPointF &p); + qreal posAtValue(qreal value); + void setColor(const QColor &color); + void setTextColor(const QColor &color); + void animateChangeLine(const QLineF &newLine); + void setTextVisible(bool arg1); + void setLinesVisible(bool arg1); + void setLineSize(qreal lineSize); + void setLine(const QLineF& line); + int unitSystem; +public +slots: + virtual void updateTicks(color_indice_t color = TIME_GRID); + +signals: + void sizeChanged(); + void maxChanged(); + +protected: + virtual QString textForValue(double value); + virtual QColor colorForValue(double value); + Orientation orientation; + QList labels; + QList lines; + double min; + double max; + double interval; + double tick_size; + QColor textColor; + bool textVisibility; + bool lineVisibility; + double labelScale; + qreal line_size; + bool changed; +}; + +class DepthAxis : public DiveCartesianAxis { + Q_OBJECT +public: + DepthAxis(); + +protected: + QString textForValue(double value); + QColor colorForValue(double value); +private +slots: + void settingsChanged(); +}; + +class TimeAxis : public DiveCartesianAxis { + Q_OBJECT +public: + virtual void updateTicks(); + +protected: + QString textForValue(double value); + QColor colorForValue(double value); +}; + +class TemperatureAxis : public DiveCartesianAxis { + Q_OBJECT +protected: + QString textForValue(double value); +}; + +class PartialGasPressureAxis : public DiveCartesianAxis { + Q_OBJECT +public: + PartialGasPressureAxis(); + void setModel(DivePlotDataModel *model); +public +slots: + void settingsChanged(); + +private: + DivePlotDataModel *model; +}; + +#endif // DIVECARTESIANAXIS_H diff --git a/desktop-widgets/profile/diveeventitem.cpp b/desktop-widgets/profile/diveeventitem.cpp new file mode 100644 index 000000000..0bbc84267 --- /dev/null +++ b/desktop-widgets/profile/diveeventitem.cpp @@ -0,0 +1,172 @@ +#include "diveeventitem.h" +#include "diveplotdatamodel.h" +#include "divecartesianaxis.h" +#include "animationfunctions.h" +#include "libdivecomputer.h" +#include "profile.h" +#include "gettextfromc.h" +#include "metrics.h" + +extern struct ev_select *ev_namelist; +extern int evn_used; + +DiveEventItem::DiveEventItem(QObject *parent) : DivePixmapItem(parent), + vAxis(NULL), + hAxis(NULL), + dataModel(NULL), + internalEvent(NULL) +{ + setFlag(ItemIgnoresTransformations); +} + + +void DiveEventItem::setHorizontalAxis(DiveCartesianAxis *axis) +{ + hAxis = axis; + recalculatePos(true); +} + +void DiveEventItem::setModel(DivePlotDataModel *model) +{ + dataModel = model; + recalculatePos(true); +} + +void DiveEventItem::setVerticalAxis(DiveCartesianAxis *axis) +{ + vAxis = axis; + recalculatePos(true); + connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(recalculatePos())); +} + +struct event *DiveEventItem::getEvent() +{ + return internalEvent; +} + +void DiveEventItem::setEvent(struct event *ev) +{ + if (!ev) + return; + internalEvent = ev; + setupPixmap(); + setupToolTipString(); + recalculatePos(true); +} + +void DiveEventItem::setupPixmap() +{ + const IconMetrics& metrics = defaultIconMetrics(); + int sz_bigger = metrics.sz_med + metrics.sz_small; // ex 40px + int sz_pix = sz_bigger/2; // ex 20px + +#define EVENT_PIXMAP(PIX) QPixmap(QString(PIX)).scaled(sz_pix, sz_pix, Qt::KeepAspectRatio, Qt::SmoothTransformation) +#define EVENT_PIXMAP_BIGGER(PIX) QPixmap(QString(PIX)).scaled(sz_bigger, sz_bigger, Qt::KeepAspectRatio, Qt::SmoothTransformation) + if (same_string(internalEvent->name, "")) { + setPixmap(EVENT_PIXMAP(":warning")); + } else if (internalEvent->type == SAMPLE_EVENT_BOOKMARK) { + setPixmap(EVENT_PIXMAP(":flag")); + } else if (strcmp(internalEvent->name, "heading") == 0 || + (same_string(internalEvent->name, "SP change") && internalEvent->time.seconds == 0)) { + // 2 cases: + // a) some dive computers have heading in every sample + // b) at t=0 we might have an "SP change" to indicate dive type + // in both cases we want to get the right data into the tooltip but don't want the visual clutter + // so set an "almost invisible" pixmap (a narrow but somewhat tall, basically transparent pixmap) + // that allows tooltips to work when we don't want to show a specific + // pixmap for an event, but want to show the event value in the tooltip + QPixmap transparentPixmap(4, 20); + transparentPixmap.fill(QColor::fromRgbF(1.0, 1.0, 1.0, 0.01)); + setPixmap(transparentPixmap); + } else if (event_is_gaschange(internalEvent)) { + if (internalEvent->gas.mix.he.permille) + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeTrimix")); + else if (gasmix_is_air(&internalEvent->gas.mix)) + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeAir")); + else + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeNitrox")); + } else { + setPixmap(EVENT_PIXMAP(":warning")); + } +#undef EVENT_PIXMAP +} + +void DiveEventItem::setupToolTipString() +{ + // we display the event on screen - so translate + QString name = gettextFromC::instance()->tr(internalEvent->name); + int value = internalEvent->value; + int type = internalEvent->type; + if (value) { + if (event_is_gaschange(internalEvent)) { + name += ": "; + name += gasname(&internalEvent->gas.mix); + + /* Do we have an explicit cylinder index? Show it. */ + if (internalEvent->gas.index >= 0) + name += QString(" (cyl %1)").arg(internalEvent->gas.index+1); + } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { + name += QString(":%1").arg((double)value / 1000); + } else { + name += QString(":%1").arg(value); + } + } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { + // this is a bad idea - we are abusing an existing event type that is supposed to + // warn of high or low pOâ‚‚ and are turning it into a set point change event + name += "\n" + tr("Manual switch to OC"); + } else { + name += internalEvent->flags == SAMPLE_FLAGS_BEGIN ? tr(" begin", "Starts with space!") : + internalEvent->flags == SAMPLE_FLAGS_END ? tr(" end", "Starts with space!") : ""; + } + // qDebug() << name; + setToolTip(name); +} + +void DiveEventItem::eventVisibilityChanged(const QString &eventName, bool visible) +{ +} + +bool DiveEventItem::shouldBeHidden() +{ + struct event *event = internalEvent; + + /* + * Some gas change events are special. Some dive computers just tell us the initial gas this way. + * Don't bother showing those + */ + struct sample *first_sample = &get_dive_dc(&displayed_dive, dc_number)->sample[0]; + if (!strcmp(event->name, "gaschange") && + (event->time.seconds == 0 || + (first_sample && event->time.seconds == first_sample->time.seconds))) + return true; + + for (int i = 0; i < evn_used; i++) { + if (!strcmp(event->name, ev_namelist[i].ev_name) && ev_namelist[i].plot_ev == false) + return true; + } + return false; +} + +void DiveEventItem::recalculatePos(bool instant) +{ + if (!vAxis || !hAxis || !internalEvent || !dataModel) + return; + + QModelIndexList result = dataModel->match(dataModel->index(0, DivePlotDataModel::TIME), Qt::DisplayRole, internalEvent->time.seconds); + if (result.isEmpty()) { + Q_ASSERT("can't find a spot in the dataModel"); + hide(); + return; + } + if (!isVisible() && !shouldBeHidden()) + show(); + int depth = dataModel->data(dataModel->index(result.first().row(), DivePlotDataModel::DEPTH)).toInt(); + qreal x = hAxis->posAtValue(internalEvent->time.seconds); + qreal y = vAxis->posAtValue(depth); + if (!instant) + Animations::moveTo(this, x, y); + else + setPos(x, y); + if (isVisible() && shouldBeHidden()) + hide(); +} diff --git a/desktop-widgets/profile/diveeventitem.h b/desktop-widgets/profile/diveeventitem.h new file mode 100644 index 000000000..f358fee6d --- /dev/null +++ b/desktop-widgets/profile/diveeventitem.h @@ -0,0 +1,34 @@ +#ifndef DIVEEVENTITEM_H +#define DIVEEVENTITEM_H + +#include "divepixmapitem.h" + +class DiveCartesianAxis; +class DivePlotDataModel; +struct event; + +class DiveEventItem : public DivePixmapItem { + Q_OBJECT +public: + DiveEventItem(QObject *parent = 0); + void setEvent(struct event *ev); + struct event *getEvent(); + void eventVisibilityChanged(const QString &eventName, bool visible); + void setVerticalAxis(DiveCartesianAxis *axis); + void setHorizontalAxis(DiveCartesianAxis *axis); + void setModel(DivePlotDataModel *model); + bool shouldBeHidden(); +public +slots: + void recalculatePos(bool instant = false); + +private: + void setupToolTipString(); + void setupPixmap(); + DiveCartesianAxis *vAxis; + DiveCartesianAxis *hAxis; + DivePlotDataModel *dataModel; + struct event *internalEvent; +}; + +#endif // DIVEEVENTITEM_H diff --git a/desktop-widgets/profile/divelineitem.cpp b/desktop-widgets/profile/divelineitem.cpp new file mode 100644 index 000000000..f9e288a44 --- /dev/null +++ b/desktop-widgets/profile/divelineitem.cpp @@ -0,0 +1,5 @@ +#include "divelineitem.h" + +DiveLineItem::DiveLineItem(QGraphicsItem *parent) : QGraphicsLineItem(parent) +{ +} diff --git a/desktop-widgets/profile/divelineitem.h b/desktop-widgets/profile/divelineitem.h new file mode 100644 index 000000000..ec88e9da5 --- /dev/null +++ b/desktop-widgets/profile/divelineitem.h @@ -0,0 +1,15 @@ +#ifndef DIVELINEITEM_H +#define DIVELINEITEM_H + +#include +#include + +class DiveLineItem : public QObject, public QGraphicsLineItem { + Q_OBJECT + Q_PROPERTY(QPointF pos READ pos WRITE setPos) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + DiveLineItem(QGraphicsItem *parent = 0); +}; + +#endif // DIVELINEITEM_H diff --git a/desktop-widgets/profile/divepixmapitem.cpp b/desktop-widgets/profile/divepixmapitem.cpp new file mode 100644 index 000000000..581f6f9b4 --- /dev/null +++ b/desktop-widgets/profile/divepixmapitem.cpp @@ -0,0 +1,130 @@ +#include "divepixmapitem.h" +#include "animationfunctions.h" +#include "divepicturemodel.h" +#include + +#include +#include +#include + +DivePixmapItem::DivePixmapItem(QObject *parent) : QObject(parent), QGraphicsPixmapItem() +{ +} + +DiveButtonItem::DiveButtonItem(QObject *parent): DivePixmapItem(parent) +{ +} + +void DiveButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mousePressEvent(event); + emit clicked(); +} + +// If we have many many pictures on screen, maybe a shared-pixmap would be better to +// paint on screen, but for now, this. +CloseButtonItem::CloseButtonItem(QObject *parent): DiveButtonItem(parent) +{ + static QPixmap p = QPixmap(":trash"); + setPixmap(p); + setFlag(ItemIgnoresTransformations); +} + +void CloseButtonItem::hide() +{ + DiveButtonItem::hide(); +} + +void CloseButtonItem::show() +{ + DiveButtonItem::show(); +} + +DivePictureItem::DivePictureItem(QObject *parent): DivePixmapItem(parent), + canvas(new QGraphicsRectItem(this)), + shadow(new QGraphicsRectItem(this)) +{ + setFlag(ItemIgnoresTransformations); + setAcceptHoverEvents(true); + setScale(0.2); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + setVisible(prefs.show_pictures_in_profile); + + canvas->setPen(Qt::NoPen); + canvas->setBrush(QColor(Qt::white)); + canvas->setFlag(ItemStacksBehindParent); + canvas->setZValue(-1); + + shadow->setPos(5,5); + shadow->setPen(Qt::NoPen); + shadow->setBrush(QColor(Qt::lightGray)); + shadow->setFlag(ItemStacksBehindParent); + shadow->setZValue(-2); +} + +void DivePictureItem::settingsChanged() +{ + setVisible(prefs.show_pictures_in_profile); +} + +void DivePictureItem::setPixmap(const QPixmap &pix) +{ + DivePixmapItem::setPixmap(pix); + QRectF r = boundingRect(); + canvas->setRect(0 - 10, 0 -10, r.width() + 20, r.height() + 20); + shadow->setRect(canvas->rect()); +} + +CloseButtonItem *button = NULL; +void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + Animations::scaleTo(this, 1.0); + setZValue(5); + + if(!button) { + button = new CloseButtonItem(); + button->setScale(0.2); + button->setZValue(7); + scene()->addItem(button); + } + button->setParentItem(this); + button->setPos(boundingRect().width() - button->boundingRect().width() * 0.2, + boundingRect().height() - button->boundingRect().height() * 0.2); + button->setOpacity(0); + button->show(); + Animations::show(button); + button->disconnect(); + connect(button, SIGNAL(clicked()), this, SLOT(removePicture())); +} + +void DivePictureItem::setFileUrl(const QString &s) +{ + fileUrl = s; +} + +void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + Animations::scaleTo(this, 0.2); + setZValue(0); + if(button){ + button->setParentItem(NULL); + Animations::hide(button); + } +} + +DivePictureItem::~DivePictureItem(){ + if(button){ + button->setParentItem(NULL); + Animations::hide(button); + } +} + +void DivePictureItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(fileUrl)); +} + +void DivePictureItem::removePicture() +{ + DivePictureModel::instance()->removePicture(fileUrl); +} diff --git a/desktop-widgets/profile/divepixmapitem.h b/desktop-widgets/profile/divepixmapitem.h new file mode 100644 index 000000000..02c1523f7 --- /dev/null +++ b/desktop-widgets/profile/divepixmapitem.h @@ -0,0 +1,57 @@ +#ifndef DIVEPIXMAPITEM_H +#define DIVEPIXMAPITEM_H + +#include +#include + +class DivePixmapItem : public QObject, public QGraphicsPixmapItem { + Q_OBJECT + Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +public: + DivePixmapItem(QObject *parent = 0); +}; + +class DivePictureItem : public DivePixmapItem { + Q_OBJECT + Q_PROPERTY(qreal scale WRITE setScale READ scale) +public: + DivePictureItem(QObject *parent = 0); + virtual ~DivePictureItem(); + void setPixmap(const QPixmap& pix); +public slots: + void settingsChanged(); + void removePicture(); + void setFileUrl(const QString& s); +protected: + void hoverEnterEvent(QGraphicsSceneHoverEvent *event); + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); + void mousePressEvent(QGraphicsSceneMouseEvent *event); +private: + QString fileUrl; + QGraphicsRectItem *canvas; + QGraphicsRectItem *shadow; +}; + +class DiveButtonItem : public DivePixmapItem { + Q_OBJECT +public: + DiveButtonItem(QObject *parent = 0); +protected: + virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); +signals: + void clicked(); +}; + +class CloseButtonItem : public DiveButtonItem { + Q_OBJECT +public: + CloseButtonItem(QObject *parent = 0); +public slots: + void hide(); + void show(); +}; + +#endif // DIVEPIXMAPITEM_H diff --git a/desktop-widgets/profile/diveprofileitem.cpp b/desktop-widgets/profile/diveprofileitem.cpp new file mode 100644 index 000000000..2c814678a --- /dev/null +++ b/desktop-widgets/profile/diveprofileitem.cpp @@ -0,0 +1,979 @@ +#include "diveprofileitem.h" +#include "diveplotdatamodel.h" +#include "divecartesianaxis.h" +#include "divetextitem.h" +#include "animationfunctions.h" +#include "dive.h" +#include "profile.h" +#include "preferences.h" +#include "diveplannermodel.h" +#include "helpers.h" +#include "libdivecomputer/parser.h" +#include "mainwindow.h" +#include "maintab.h" +#include "profile/profilewidget2.h" +#include "diveplanner.h" + +#include + +AbstractProfilePolygonItem::AbstractProfilePolygonItem() : QObject(), QGraphicsPolygonItem(), hAxis(NULL), vAxis(NULL), dataModel(NULL), hDataColumn(-1), vDataColumn(-1) +{ + setCacheMode(DeviceCoordinateCache); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void AbstractProfilePolygonItem::settingsChanged() +{ +} + +void AbstractProfilePolygonItem::setHorizontalAxis(DiveCartesianAxis *horizontal) +{ + hAxis = horizontal; + connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setHorizontalDataColumn(int column) +{ + hDataColumn = column; + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setModel(DivePlotDataModel *model) +{ + dataModel = model; + connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex))); + connect(dataModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(modelDataRemoved(QModelIndex, int, int))); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::modelDataRemoved(const QModelIndex &parent, int from, int to) +{ + setPolygon(QPolygonF()); + qDeleteAll(texts); + texts.clear(); +} + +void AbstractProfilePolygonItem::setVerticalAxis(DiveCartesianAxis *vertical) +{ + vAxis = vertical; + connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + connect(vAxis, SIGNAL(maxChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setVerticalDataColumn(int column) +{ + vDataColumn = column; + modelDataChanged(); +} + +bool AbstractProfilePolygonItem::shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!hAxis || !vAxis) + return false; + if (!dataModel || dataModel->rowCount() == 0) + return false; + if (hDataColumn == -1 || vDataColumn == -1) + return false; + if (topLeft.isValid() && bottomRight.isValid()) { + if ((topLeft.column() >= vDataColumn || topLeft.column() >= hDataColumn) && + (bottomRight.column() <= vDataColumn || topLeft.column() <= hDataColumn)) { + return true; + } + } + return true; +} + +void AbstractProfilePolygonItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + + // 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 = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + qreal horizontalValue = dataModel->index(i, hDataColumn).data().toReal(); + qreal verticalValue = dataModel->index(i, vDataColumn).data().toReal(); + QPointF point(hAxis->posAtValue(horizontalValue), vAxis->posAtValue(verticalValue)); + poly.append(point); + } + setPolygon(poly); + + qDeleteAll(texts); + texts.clear(); +} + +DiveProfileItem::DiveProfileItem() : show_reported_ceiling(0), reported_ceiling_in_red(0) +{ +} + +void DiveProfileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(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 = 1, count = dataModel->rowCount(); i < count; i++) { + QModelIndex colorIndex = dataModel->index(i, DivePlotDataModel::COLOR); + pen.setBrush(QBrush(colorIndex.data(Qt::BackgroundRole).value())); + painter->setPen(pen); + painter->drawLine(poly[i - 1], poly[i]); + } + painter->restore(); +} + +int DiveProfileItem::maxCeiling(int row) +{ + int max = -1; + plot_data *entry = dataModel->data().entry + row; + for (int tissue = 0; tissue < 16; tissue++) { + if (max < entry->ceilings[tissue]) + max = entry->ceilings[tissue]; + } + return max; +} + +void DiveProfileItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + bool eventAdded = false; + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); + if (polygon().isEmpty()) + return; + + show_reported_ceiling = prefs.dcceiling; + reported_ceiling_in_red = prefs.redceiling; + profileColor = getColor(DEPTH_BOTTOM); + + int currState = qobject_cast(scene()->views().first())->currentState; + if (currState == ProfileWidget2::PLAN) { + plot_data *entry = dataModel->data().entry; + for (int i = 0; i < dataModel->rowCount(); i++, entry++) { + int max = maxCeiling(i); + // Don't scream if we violate the ceiling by a few cm + if (entry->depth < max - 100 && entry->sec > 0) { + profileColor = QColor(Qt::red); + if (!eventAdded) { + add_event(&displayed_dive.dc, entry->sec, SAMPLE_EVENT_CEILING, -1, max / 1000, "planned waypoint above ceiling"); + eventAdded = true; + } + } + } + } + + /* Show any ceiling we may have encountered */ + if (prefs.dcceiling && !prefs.redceiling) { + QPolygonF p = polygon(); + plot_data *entry = dataModel->data().entry + dataModel->rowCount() - 1; + for (int i = dataModel->rowCount() - 1; i >= 0; 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 = 0, count = dataModel->rowCount(); i < count; i++) { + + struct plot_data *entry = dataModel->data().entry + i; + if (entry->depth < 2000) + continue; + + if ((entry == entry->max[2]) && entry->depth / 100 != last) { + plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_DEEP)); + last = entry->depth / 100; + } + + if ((entry == entry->min[2]) && 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::settingsChanged() +{ + //TODO: Only modelDataChanged() here if we need to rebuild the graph ( for instance, + // if the prefs.dcceiling are enabled, but prefs.redceiling is disabled + // and only if it changed something. let's not waste cpu cycles repoloting something we don't need to. + modelDataChanged(); +} + +void DiveProfileItem::plot_depth_sample(struct plot_data *entry, QFlags flags, const QColor &color) +{ + int decimals; + double d = get_depth_units(entry->depth, &decimals, NULL); + DiveTextItem *item = new DiveTextItem(this); + item->setPos(hAxis->posAtValue(entry->sec), vAxis->posAtValue(entry->depth)); + item->setText(QString("%1").arg(d, 0, 'f', 1)); + item->setAlignment(flags); + item->setBrush(color); + texts.append(item); +} + +DiveHeartrateItem::DiveHeartrateItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::HR_PLOT))); + pen.setCosmetic(true); + pen.setWidth(1); + setPen(pen); + settingsChanged(); +} + +void DiveHeartrateItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int last = -300, last_printed_hr = 0, sec = 0; + struct { + int sec; + int hr; + } hist[3] = {}; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + qDeleteAll(texts); + texts.clear(); + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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; + createTextItem(sec, hr); + last_printed_hr = hr; + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveHeartrateItem::createTextItem(int sec, int hr) +{ + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignBottom); + text->setBrush(getColor(HR_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(hr))); + text->setScale(0.7); // need to call this BEFORE setText() + text->setText(QString("%1").arg(hr)); + texts.append(text); +} + +void DiveHeartrateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveHeartrateItem::settingsChanged() +{ + setVisible(prefs.hrgraph); +} + +DivePercentageItem::DivePercentageItem(int i) +{ + QPen pen; + QColor color; + color.setHsl(100 + 10 * i, 200, 100); + pen.setBrush(QBrush(color)); + pen.setCosmetic(true); + pen.setWidth(1); + setPen(pen); + settingsChanged(); +} + +void DivePercentageItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DivePercentageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DivePercentageItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveAmbPressureItem::DiveAmbPressureItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::AMB_PRESSURE_LINE))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveAmbPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveAmbPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveAmbPressureItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveGFLineItem::DiveGFLineItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::GF_LINE))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveGFLineItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveGFLineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveGFLineItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveTemperatureItem::DiveTemperatureItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::TEMP_PLOT))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); +} + +void DiveTemperatureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int last = -300, last_printed_temp = 0, sec = 0, last_valid_temp = 0; + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + qDeleteAll(texts); + texts.clear(); + // Ignore empty values. things do not look good with '0' as temperature in kelvin... + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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) + createTextItem(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))) { + createTextItem(sec, last_valid_temp); + } + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveTemperatureItem::createTextItem(int sec, int mkelvin) +{ + double deg; + const char *unit; + deg = get_temp_units(mkelvin, &unit); + + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignBottom); + text->setBrush(getColor(TEMP_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(mkelvin))); + text->setScale(0.8); // need to call this BEFORE setText() + text->setText(QString("%1%2").arg(deg, 0, 'f', 1).arg(unit)); + texts.append(text); +} + +void DiveTemperatureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +DiveMeanDepthItem::DiveMeanDepthItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::HR_AXIS))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveMeanDepthItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + double meandepthvalue = 0.0; + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + QPolygonF poly; + plot_data *entry = dataModel->data().entry; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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 *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveMeanDepthItem::settingsChanged() +{ + setVisible(prefs.show_average_depth); +} + +void DiveMeanDepthItem::createTextItem() { + plot_data *entry = dataModel->data().entry; + int sec = entry[dataModel->rowCount()-1].sec; + qDeleteAll(texts); + texts.clear(); + int decimals; + const char *unitText; + double d = get_depth_units(lastRunningSum, &decimals, &unitText); + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignTop); + text->setBrush(getColor(TEMP_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec) + 1, vAxis->posAtValue(lastRunningSum))); + text->setScale(0.8); // need to call this BEFORE setText() + text->setText(QString("%1%2").arg(d, 0, 'f', 1).arg(unitText)); + texts.append(text); +} + +void DiveGasPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + int last_index = -1; + int o2mbar; + QPolygonF boundingPoly, o2Poly; // This is the "Whole Item", but a pressure can be divided in N Polygons. + polygons.clear(); + if (displayed_dive.dc.divemode == CCR) + polygons.append(o2Poly); + + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + o2mbar = 0; + plot_data *entry = dataModel->data().entry + i; + int mbar = GET_PRESSURE(entry); + if (displayed_dive.dc.divemode == CCR) + o2mbar = GET_O2CYLINDER_PRESSURE(entry); + + if (entry->cylinderindex != last_index) { + polygons.append(QPolygonF()); // this is the polygon that will be actually drawn on screen. + last_index = entry->cylinderindex; + } + if (!mbar) { + continue; + } + if (o2mbar) { + QPointF o2point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(o2mbar)); + boundingPoly.push_back(o2point); + polygons.first().push_back(o2point); + } + + QPointF point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(mbar)); + boundingPoly.push_back(point); // The BoundingRect + polygons.last().push_back(point); // The polygon thta will be plotted. + } + setPolygon(boundingPoly); + qDeleteAll(texts); + texts.clear(); + int mbar, cyl; + int seen_cyl[MAX_CYLINDERS] = { false, }; + int last_pressure[MAX_CYLINDERS] = { 0, }; + int last_time[MAX_CYLINDERS] = { 0, }; + struct plot_data *entry; + + cyl = -1; + o2mbar = 0; + + double print_y_offset[8][2] = { { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } ,{ 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } }; + // CCR dives: These are offset values used to print the gas lables and pressures on a CCR dive profile at + // appropriate Y-coordinates: One doublet of values for each of 8 cylinders. + // Order of offsets within a doublet: gas lable offset; gas pressure offset. + // The array is initialised with default values that apply to non-CCR dives. + + bool offsets_initialised = false; + int o2cyl = -1, dilcyl = -1; + QFlags alignVar= Qt::AlignTop, align_dil = Qt::AlignBottom, align_o2 = Qt::AlignTop; + double axisRange = (vAxis->maximum() - vAxis->minimum())/1000; // Convert axis pressure range to bar + double axisLog = log10(log10(axisRange)); + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + entry = dataModel->data().entry + i; + mbar = GET_PRESSURE(entry); + if (displayed_dive.dc.divemode == CCR && displayed_dive.oxygen_cylinder_index >= 0) + o2mbar = GET_O2CYLINDER_PRESSURE(entry); + + if (o2mbar) { // If there is an o2mbar value then this is a CCR dive. Then do: + // The first time an o2 value is detected, see if the oxygen cyl pressure graph starts above or below the dil graph + if (!offsets_initialised) { // Initialise the parameters for placing the text correctly near the graph line: + o2cyl = displayed_dive.oxygen_cylinder_index; + dilcyl = displayed_dive.diluent_cylinder_index; + if ((o2mbar > mbar)) { // If above, write o2 start cyl pressure above graph and diluent pressure below graph: + print_y_offset[o2cyl][0] = -7 * axisLog; // y offset for oxygen gas lable (above); pressure offsets=-0.5, already initialised + print_y_offset[dilcyl][0] = 5 * axisLog; // y offset for diluent gas lable (below) + } else { // ... else write o2 start cyl pressure below graph: + print_y_offset[o2cyl][0] = 5 * axisLog; // o2 lable & pressure below graph; pressure offsets=-0.5, already initialised + print_y_offset[dilcyl][0] = -7.8 * axisLog; // and diluent lable above graph. + align_dil = Qt::AlignTop; + align_o2 = Qt::AlignBottom; + } + offsets_initialised = true; + } + + if (!seen_cyl[displayed_dive.oxygen_cylinder_index]) { //For o2, on the left of profile, write lable and pressure + plotPressureValue(o2mbar, entry->sec, align_o2, print_y_offset[o2cyl][1]); + plotGasValue(o2mbar, entry->sec, displayed_dive.cylinder[displayed_dive.oxygen_cylinder_index].gasmix, align_o2, print_y_offset[o2cyl][0]); + seen_cyl[displayed_dive.oxygen_cylinder_index] = true; + } + last_pressure[displayed_dive.oxygen_cylinder_index] = o2mbar; + last_time[displayed_dive.oxygen_cylinder_index] = entry->sec; + alignVar = align_dil; + } + + if (!mbar) + continue; + + if (cyl != entry->cylinderindex) { // Pressure value near the left hand edge of the profile - other cylinders: + cyl = entry->cylinderindex; // For each other cylinder, write the gas lable and pressure + if (!seen_cyl[cyl]) { + plotPressureValue(mbar, entry->sec, alignVar, print_y_offset[cyl][1]); + plotGasValue(mbar, entry->sec, displayed_dive.cylinder[cyl].gasmix, align_dil, print_y_offset[cyl][0]); + seen_cyl[cyl] = true; + } + } + last_pressure[cyl] = mbar; + last_time[cyl] = entry->sec; + } + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { // For each cylinder, on right hand side of profile, write cylinder pressure + alignVar = ((o2cyl >= 0) && (cyl == displayed_dive.oxygen_cylinder_index)) ? align_o2 : align_dil; + if (last_time[cyl]) { + plotPressureValue(last_pressure[cyl], last_time[cyl], (alignVar | Qt::AlignLeft), print_y_offset[cyl][1]); + } + } +} + +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(this); + text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + pressure_offset ); + text->setText(QString("%1 %2").arg(pressure).arg(unit)); + text->setAlignment(align); + text->setBrush(getColor(PRESSURE_TEXT)); + 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(this); + text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + gasname_offset ); + text->setText(gas); + text->setAlignment(align); + text->setBrush(getColor(PRESSURE_TEXT)); + texts.push_back(text); +} + +void DiveGasPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QPen pen; + pen.setCosmetic(true); + pen.setWidth(2); + painter->save(); + struct plot_data *entry; + Q_FOREACH (const QPolygonF &poly, polygons) { + entry = dataModel->data().entry; + for (int i = 1, count = poly.count(); i < count; i++, entry++) { + if (entry->sac) + pen.setBrush(getSacColor(entry->sac, displayed_dive.sac)); + else + pen.setBrush(MED_GRAY_HIGH_TRANS); + painter->setPen(pen); + painter->drawLine(poly[i - 1], poly[i]); + } + } + painter->restore(); +} + +DiveCalculatedCeiling::DiveCalculatedCeiling() : is3mIncrement(false) +{ + settingsChanged(); +} + +void DiveCalculatedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (MainWindow::instance()->information()) + connect(MainWindow::instance()->information(), SIGNAL(dateTimeChanged()), this, SLOT(recalc()), Qt::UniqueConnection); + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); + // Add 2 points to close the polygon. + QPolygonF poly = polygon(); + if (poly.isEmpty()) + return; + QPointF p1 = poly.first(); + QPointF p2 = poly.last(); + + poly.prepend(QPointF(p1.x(), vAxis->posAtValue(0))); + poly.append(QPointF(p2.x(), vAxis->posAtValue(0))); + setPolygon(poly); + + 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() +{ + settingsChanged(); +} + +void DiveCalculatedTissue::settingsChanged() +{ + setVisible(prefs.calcalltissues && prefs.calcceiling); +} + +void DiveReportedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + QPolygonF p; + p.append(QPointF(hAxis->posAtValue(0), vAxis->posAtValue(0))); + plot_data *entry = dataModel->data().entry; + for (int i = 0, count = dataModel->rowCount(); i < count; i++, entry++) { + if (entry->in_deco && entry->stopdepth) { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(qMin(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 DiveCalculatedCeiling::recalc() +{ + dataModel->calculateDecompression(); +} + +void DiveCalculatedCeiling::settingsChanged() +{ + if (dataModel && is3mIncrement != prefs.calcceiling3m) { + // recalculate that part. + recalc(); + } + is3mIncrement = prefs.calcceiling3m; + setVisible(prefs.calcceiling); +} + +void DiveReportedCeiling::settingsChanged() +{ + setVisible(prefs.dcceiling); +} + +void DiveReportedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QGraphicsPolygonItem::paint(painter, option, widget); +} + +void PartialPressureGasItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + //AbstractProfilePolygonItem::modelDataChanged(); + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + plot_data *entry = dataModel->data().entry; + QPolygonF poly; + QPolygonF alertpoly; + alertPolygons.clear(); + QSettings s; + s.beginGroup("TecDetails"); + double threshold = 0.0; + if (thresholdPtr) + threshold = *thresholdPtr; + bool inAlertFragment = false; + for (int i = 0; i < dataModel->rowCount(); 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 (value >= threshold) { + 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" UTF8_SUBSCRIPT_2),getColor(PN2), legendPos); + */ +} + +void PartialPressureGasItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + 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::setThreshouldSettingsKey(double *prefPointer) +{ + thresholdPtr = prefPointer; +} + +PartialPressureGasItem::PartialPressureGasItem() : + thresholdPtr(NULL) +{ +} + +void PartialPressureGasItem::settingsChanged() +{ + QSettings s; + s.beginGroup("TecDetails"); + setVisible(s.value(visibilityKey).toBool()); +} + +void PartialPressureGasItem::setVisibilitySettingsKey(const QString &key) +{ + visibilityKey = key; +} + +void PartialPressureGasItem::setColors(const QColor &normal, const QColor &alert) +{ + normalColor = normal; + alertColor = alert; +} diff --git a/desktop-widgets/profile/diveprofileitem.h b/desktop-widgets/profile/diveprofileitem.h new file mode 100644 index 000000000..0bba7f7a3 --- /dev/null +++ b/desktop-widgets/profile/diveprofileitem.h @@ -0,0 +1,225 @@ +#ifndef DIVEPROFILEITEM_H +#define DIVEPROFILEITEM_H + +#include +#include +#include + +#include "divelineitem.h" + +/* This is the Profile Item, it should be used for quite a lot of things + on the profile view. The usage should be pretty simple: + + DiveProfileItem *profile = new DiveProfileItem(); + profile->setVerticalAxis( profileYAxis ); + profile->setHorizontalAxis( timeAxis ); + profile->setModel( DiveDataModel ); + profile->setHorizontalDataColumn( DiveDataModel::TIME ); + profile->setVerticalDataColumn( DiveDataModel::DEPTH ); + scene()->addItem(profile); + + This is a generically item and should be used as a base for others, I think... +*/ + +class DivePlotDataModel; +class DiveTextItem; +class DiveCartesianAxis; +class QAbstractTableModel; +struct plot_data; + +class AbstractProfilePolygonItem : public QObject, public QGraphicsPolygonItem { + Q_OBJECT + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +public: + AbstractProfilePolygonItem(); + void setVerticalAxis(DiveCartesianAxis *vertical); + void setHorizontalAxis(DiveCartesianAxis *horizontal); + void setModel(DivePlotDataModel *model); + void setHorizontalDataColumn(int column); + void setVerticalDataColumn(int column); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) = 0; + virtual void clear() + { + } +public +slots: + virtual void settingsChanged(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void modelDataRemoved(const QModelIndex &parent, int from, int to); + +protected: + /* when the model emits a 'datachanged' signal, this method below should be used to check if the + * modified data affects this particular item ( for example, when setting the '3m increment' + * the data for Ceiling and tissues will be changed, and only those. so, the topLeft will be the CEILING + * column and the bottomRight will have the TISSUE_16 column. this method takes the vDataColumn and hDataColumn + * into consideration when returning 'true' for "yes, continue the calculation', and 'false' for + * 'do not recalculate, we already have the right data. + */ + bool shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + DiveCartesianAxis *hAxis; + DiveCartesianAxis *vAxis; + DivePlotDataModel *dataModel; + int hDataColumn; + int vDataColumn; + QList texts; +}; + +class DiveProfileItem : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + DiveProfileItem(); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void settingsChanged(); + void plot_depth_sample(struct plot_data *entry, QFlags flags, const QColor &color); + int maxCeiling(int row); + +private: + unsigned int show_reported_ceiling; + unsigned int reported_ceiling_in_red; + QColor profileColor; +}; + +class DiveMeanDepthItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveMeanDepthItem(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); + +private: + void createTextItem(); + double lastRunningSum; + QString visibilityKey; +}; + +class DiveTemperatureItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveTemperatureItem(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + +private: + void createTextItem(int seconds, int mkelvin); +}; + +class DiveHeartrateItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveHeartrateItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + void createTextItem(int seconds, int hr); + QString visibilityKey; +}; + +class DivePercentageItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DivePercentageItem(int i); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveAmbPressureItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveAmbPressureItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveGFLineItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveGFLineItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveGasPressureItem : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + +private: + void plotPressureValue(int mbar, int sec, QFlags align, double offset); + void plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags align, double offset); + QVector polygons; +}; + +class DiveCalculatedCeiling : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + DiveCalculatedCeiling(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); + +public +slots: + void recalc(); + +private: + bool is3mIncrement; +}; + +class DiveReportedCeiling : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); +}; + +class DiveCalculatedTissue : public DiveCalculatedCeiling { + Q_OBJECT +public: + DiveCalculatedTissue(); + virtual void settingsChanged(); +}; + +class PartialPressureGasItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + PartialPressureGasItem(); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void settingsChanged(); + void setThreshouldSettingsKey(double *prefPointer); + void setVisibilitySettingsKey(const QString &setVisibilitySettingsKey); + void setColors(const QColor &normalColor, const QColor &alertColor); + +private: + QVector alertPolygons; + double *thresholdPtr; + QString visibilityKey; + QColor normalColor; + QColor alertColor; +}; +#endif // DIVEPROFILEITEM_H diff --git a/desktop-widgets/profile/diverectitem.cpp b/desktop-widgets/profile/diverectitem.cpp new file mode 100644 index 000000000..8cb60c3f5 --- /dev/null +++ b/desktop-widgets/profile/diverectitem.cpp @@ -0,0 +1,5 @@ +#include "diverectitem.h" + +DiveRectItem::DiveRectItem(QObject *parent, QGraphicsItem *parentItem) : QObject(parent), QGraphicsRectItem(parentItem) +{ +} diff --git a/desktop-widgets/profile/diverectitem.h b/desktop-widgets/profile/diverectitem.h new file mode 100644 index 000000000..e616cf591 --- /dev/null +++ b/desktop-widgets/profile/diverectitem.h @@ -0,0 +1,17 @@ +#ifndef DIVERECTITEM_H +#define DIVERECTITEM_H + +#include +#include + +class DiveRectItem : public QObject, public QGraphicsRectItem { + Q_OBJECT + Q_PROPERTY(QRectF rect WRITE setRect READ rect) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +public: + DiveRectItem(QObject *parent = 0, QGraphicsItem *parentItem = 0); +}; + +#endif // DIVERECTITEM_H diff --git a/desktop-widgets/profile/divetextitem.cpp b/desktop-widgets/profile/divetextitem.cpp new file mode 100644 index 000000000..3bf00d68f --- /dev/null +++ b/desktop-widgets/profile/divetextitem.cpp @@ -0,0 +1,113 @@ +#include "divetextitem.h" +#include "mainwindow.h" +#include "profilewidget2.h" +#include "subsurface-core/color.h" + +#include + +DiveTextItem::DiveTextItem(QGraphicsItem *parent) : QGraphicsItemGroup(parent), + internalAlignFlags(Qt::AlignHCenter | Qt::AlignVCenter), + textBackgroundItem(new QGraphicsPathItem(this)), + textItem(new QGraphicsPathItem(this)), + printScale(1.0), + scale(1.0), + connected(false) +{ + setFlag(ItemIgnoresTransformations); + textBackgroundItem->setBrush(QBrush(getColor(TEXT_BACKGROUND))); + textBackgroundItem->setPen(Qt::NoPen); + textItem->setPen(Qt::NoPen); +} + +void DiveTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + updateText(); + QGraphicsItemGroup::paint(painter, option, widget); +} + +void DiveTextItem::fontPrintScaleUpdate(double scale) +{ + printScale = scale; +} + +void DiveTextItem::setAlignment(int alignFlags) +{ + if (alignFlags != internalAlignFlags) { + internalAlignFlags = alignFlags; + } +} + +void DiveTextItem::setBrush(const QBrush &b) +{ + textItem->setBrush(b); +} + +void DiveTextItem::setScale(double newscale) +{ + if (scale != newscale) { + scale = newscale; + } +} + +void DiveTextItem::setText(const QString &t) +{ + if (internalText != t) { + if (!connected) { + if (scene()) { + // by now we should be on a scene. grab the profile widget from it and setup our printScale + // and connect to the signal that makes sure we keep track if that changes + ProfileWidget2 *profile = qobject_cast(scene()->views().first()); + connect(profile, SIGNAL(fontPrintScaleChanged(double)), this, SLOT(fontPrintScaleUpdate(double)), Qt::UniqueConnection); + fontPrintScaleUpdate(profile->getFontPrintScale()); + connected = true; + } else { + qDebug() << "called before scene was set up" << t; + } + } + internalText = t; + updateText(); + } +} + +const QString &DiveTextItem::text() +{ + return internalText; +} + +void DiveTextItem::updateText() +{ + double size; + if (internalText.isEmpty()) { + return; + } + + QFont fnt(qApp->font()); + if ((size = fnt.pixelSize()) > 0) { + // set in pixels - so the scale factor may not make a difference if it's too close to 1 + size *= scale * printScale; + fnt.setPixelSize(size); + } else { + size = fnt.pointSizeF(); + size *= scale * printScale; + fnt.setPointSizeF(size); + } + QFontMetrics fm(fnt); + + QPainterPath textPath; + qreal xPos = 0, yPos = 0; + + QRectF rect = fm.boundingRect(internalText); + yPos = (internalAlignFlags & Qt::AlignTop) ? 0 : + (internalAlignFlags & Qt::AlignBottom) ? +rect.height() : + /*(internalAlignFlags & Qt::AlignVCenter ? */ +rect.height() / 4; + + xPos = (internalAlignFlags & Qt::AlignLeft) ? -rect.width() : + (internalAlignFlags & Qt::AlignHCenter) ? -rect.width() / 2 : + /* (internalAlignFlags & Qt::AlignRight) */ 0; + + textPath.addText(xPos, yPos, fnt, internalText); + QPainterPathStroker stroker; + stroker.setWidth(3); + textBackgroundItem->setPath(stroker.createStroke(textPath)); + textItem->setPath(textPath); +} diff --git a/desktop-widgets/profile/divetextitem.h b/desktop-widgets/profile/divetextitem.h new file mode 100644 index 000000000..be0adf292 --- /dev/null +++ b/desktop-widgets/profile/divetextitem.h @@ -0,0 +1,38 @@ +#ifndef DIVETEXTITEM_H +#define DIVETEXTITEM_H + +#include +#include + +class QBrush; + +/* A Line Item that has animated-properties. */ +class DiveTextItem : public QObject, public QGraphicsItemGroup { + Q_OBJECT + Q_PROPERTY(QPointF pos READ pos WRITE setPos) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + DiveTextItem(QGraphicsItem *parent = 0); + void setText(const QString &text); + void setAlignment(int alignFlags); + void setScale(double newscale); + void setBrush(const QBrush &brush); + const QString &text(); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + +private +slots: + void fontPrintScaleUpdate(double scale); + +private: + void updateText(); + int internalAlignFlags; + QGraphicsPathItem *textBackgroundItem; + QGraphicsPathItem *textItem; + QString internalText; + double printScale; + double scale; + bool connected; +}; + +#endif // DIVETEXTITEM_H diff --git a/desktop-widgets/profile/divetooltipitem.cpp b/desktop-widgets/profile/divetooltipitem.cpp new file mode 100644 index 000000000..d4818422b --- /dev/null +++ b/desktop-widgets/profile/divetooltipitem.cpp @@ -0,0 +1,285 @@ +#include "divetooltipitem.h" +#include "divecartesianaxis.h" +#include "dive.h" +#include "profile.h" +#include "membuffer.h" +#include "metrics.h" +#include +#include +#include +#include + +#define PORT_IN_PROGRESS 1 +#ifdef PORT_IN_PROGRESS +#include "display.h" +#endif + +void ToolTipItem::addToolTip(const QString &toolTip, const QIcon &icon, const QPixmap& pixmap) +{ + const IconMetrics& iconMetrics = defaultIconMetrics(); + + QGraphicsPixmapItem *iconItem = 0; + double yValue = title->boundingRect().height() + iconMetrics.spacing; + Q_FOREACH (ToolTip t, toolTips) { + yValue += t.second->boundingRect().height(); + } + if (entryToolTip.second) { + yValue += entryToolTip.second->boundingRect().height(); + } + iconItem = new QGraphicsPixmapItem(this); + if (!icon.isNull()) { + iconItem->setPixmap(icon.pixmap(iconMetrics.sz_small, iconMetrics.sz_small)); + } else if (!pixmap.isNull()) { + iconItem->setPixmap(pixmap); + } + iconItem->setPos(iconMetrics.spacing, yValue); + + QGraphicsSimpleTextItem *textItem = new QGraphicsSimpleTextItem(toolTip, this); + textItem->setPos(iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing, yValue); + textItem->setBrush(QBrush(Qt::white)); + textItem->setFlag(ItemIgnoresTransformations); + toolTips.push_back(qMakePair(iconItem, textItem)); +} + +void ToolTipItem::clear() +{ + Q_FOREACH (ToolTip t, toolTips) { + delete t.first; + delete t.second; + } + toolTips.clear(); +} + +void ToolTipItem::setRect(const QRectF &r) +{ + if( r == rect() ) { + return; + } + + QGraphicsRectItem::setRect(r); + updateTitlePosition(); +} + +void ToolTipItem::collapse() +{ + int dim = defaultIconMetrics().sz_small; + + if (prefs.animation_speed) { + QPropertyAnimation *animation = new QPropertyAnimation(this, "rect"); + animation->setDuration(100); + animation->setStartValue(nextRectangle); + animation->setEndValue(QRect(0, 0, dim, dim)); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + setRect(nextRectangle); + } + clear(); + + status = COLLAPSED; +} + +void ToolTipItem::expand() +{ + if (!title) + return; + + const IconMetrics& iconMetrics = defaultIconMetrics(); + + double width = 0, height = title->boundingRect().height() + iconMetrics.spacing; + Q_FOREACH (const ToolTip& t, toolTips) { + QRectF sRect = t.second->boundingRect(); + if (sRect.width() > width) + width = sRect.width(); + height += sRect.height(); + } + + if (entryToolTip.first) { + QRectF sRect = entryToolTip.second->boundingRect(); + if (sRect.width() > width) + width = sRect.width(); + height += sRect.height(); + } + + /* Left padding, Icon Size, space, right padding */ + width += iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing + iconMetrics.spacing; + + if (width < title->boundingRect().width() + iconMetrics.spacing * 2) + width = title->boundingRect().width() + iconMetrics.spacing * 2; + + if (height < iconMetrics.sz_small) + height = iconMetrics.sz_small; + + nextRectangle.setWidth(width); + nextRectangle.setHeight(height); + + if (nextRectangle != rect()) { + if (prefs.animation_speed) { + QPropertyAnimation *animation = new QPropertyAnimation(this, "rect", this); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(rect()); + animation->setEndValue(nextRectangle); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + setRect(nextRectangle); + } + } + + status = EXPANDED; +} + +ToolTipItem::ToolTipItem(QGraphicsItem *parent) : QGraphicsRectItem(parent), + title(new QGraphicsSimpleTextItem(tr("Information"), this)), + status(COLLAPSED), + timeAxis(0), + lastTime(-1) +{ + memset(&pInfo, 0, sizeof(pInfo)); + entryToolTip.first = NULL; + entryToolTip.second = NULL; + setFlags(ItemIgnoresTransformations | ItemIsMovable | ItemClipsChildrenToShape); + + QColor c = QColor(Qt::black); + c.setAlpha(155); + setBrush(c); + + setZValue(99); + + addToolTip(QString(), QIcon(), QPixmap(16,60)); + entryToolTip = toolTips.first(); + toolTips.clear(); + + title->setFlag(ItemIgnoresTransformations); + title->setPen(QPen(Qt::white, 1)); + title->setBrush(Qt::white); + + setPen(QPen(Qt::white, 2)); + refreshTime.start(); +} + +ToolTipItem::~ToolTipItem() +{ + clear(); +} + +void ToolTipItem::updateTitlePosition() +{ + const IconMetrics& iconMetrics = defaultIconMetrics(); + if (rect().width() < title->boundingRect().width() + iconMetrics.spacing * 4) { + QRectF newRect = rect(); + newRect.setWidth(title->boundingRect().width() + iconMetrics.spacing * 4); + newRect.setHeight((newRect.height() && isExpanded()) ? newRect.height() : iconMetrics.sz_small); + setRect(newRect); + } + + title->setPos(rect().width() / 2 - title->boundingRect().width() / 2 - 1, 0); +} + +bool ToolTipItem::isExpanded() const +{ + return status == EXPANDED; +} + +void ToolTipItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + persistPos(); + QGraphicsRectItem::mouseReleaseEvent(event); + Q_FOREACH (QGraphicsItem *item, oldSelection) { + item->setSelected(true); + } +} + +void ToolTipItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(widget); + painter->save(); + painter->setClipRect(option->rect); + painter->setPen(pen()); + painter->setBrush(brush()); + painter->drawRoundedRect(rect(), 10, 10, Qt::AbsoluteSize); + painter->restore(); +} + +void ToolTipItem::persistPos() +{ + QSettings s; + s.beginGroup("ProfileMap"); + s.setValue("tooltip_position", pos()); + s.endGroup(); +} + +void ToolTipItem::readPos() +{ + QSettings s; + s.beginGroup("ProfileMap"); + QPointF value = s.value("tooltip_position").toPoint(); + if (!scene()->sceneRect().contains(value)) { + value = QPointF(0, 0); + } + setPos(value); +} + +void ToolTipItem::setPlotInfo(const plot_info &plot) +{ + pInfo = plot; +} + +void ToolTipItem::setTimeAxis(DiveCartesianAxis *axis) +{ + timeAxis = axis; +} + +void ToolTipItem::refresh(const QPointF &pos) +{ + struct plot_data *entry; + static QPixmap tissues(16,60); + static QPainter painter(&tissues); + static struct membuffer mb = { 0 }; + + if(refreshTime.elapsed() < 40) + return; + refreshTime.start(); + + int time = timeAxis->valueAt(pos); + if (time == lastTime) + return; + + lastTime = time; + clear(); + + mb.len = 0; + entry = get_plot_details_new(&pInfo, time, &mb); + if (entry) { + tissues.fill(); + painter.setPen(QColor(0, 0, 0, 0)); + painter.setBrush(QColor(LIMENADE1)); + painter.drawRect(0, 10 + (100 - AMB_PERCENTAGE) / 2, 16, AMB_PERCENTAGE / 2); + painter.setBrush(QColor(SPRINGWOOD1)); + painter.drawRect(0, 10, 16, (100 - AMB_PERCENTAGE) / 2); + painter.setBrush(QColor(Qt::red)); + painter.drawRect(0,0,16,10); + painter.setPen(QColor(0, 0, 0, 255)); + painter.drawLine(0, 60 - entry->gfline / 2, 16, 60 - entry->gfline / 2); + painter.drawLine(0, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure / 2, + 16, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure /2); + painter.setPen(QColor(0, 0, 0, 127)); + for (int i=0; i<16; i++) { + painter.drawLine(i, 60, i, 60 - entry->percentages[i] / 2); + } + entryToolTip.first->setPixmap(tissues); + entryToolTip.second->setText(QString::fromUtf8(mb.buffer, mb.len)); + } + + Q_FOREACH (QGraphicsItem *item, scene()->items(pos, Qt::IntersectsItemBoundingRect + ,Qt::DescendingOrder, scene()->views().first()->transform())) { + if (!item->toolTip().isEmpty()) + addToolTip(item->toolTip()); + } + expand(); +} + +void ToolTipItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + oldSelection = scene()->selectedItems(); + scene()->clearSelection(); + QGraphicsItem::mousePressEvent(event); +} diff --git a/desktop-widgets/profile/divetooltipitem.h b/desktop-widgets/profile/divetooltipitem.h new file mode 100644 index 000000000..4fa7ec2d7 --- /dev/null +++ b/desktop-widgets/profile/divetooltipitem.h @@ -0,0 +1,67 @@ +#ifndef DIVETOOLTIPITEM_H +#define DIVETOOLTIPITEM_H + +#include +#include +#include +#include +#include +#include +#include "display.h" + +class DiveCartesianAxis; +class QGraphicsLineItem; +class QGraphicsSimpleTextItem; +class QGraphicsPixmapItem; +struct graphics_context; + +/* To use a tooltip, simply ->setToolTip on the QGraphicsItem that you want + * or, if it's a "global" tooltip, set it on the mouseMoveEvent of the ProfileGraphicsView. + */ +class ToolTipItem : public QObject, public QGraphicsRectItem { + Q_OBJECT + void updateTitlePosition(); + Q_PROPERTY(QRectF rect READ rect WRITE setRect) + +public: + enum Status { + COLLAPSED, + EXPANDED + }; + + explicit ToolTipItem(QGraphicsItem *parent = 0); + virtual ~ToolTipItem(); + + void collapse(); + void expand(); + void clear(); + void addToolTip(const QString &toolTip, const QIcon &icon = QIcon(), const QPixmap &pixmap = QPixmap()); + void refresh(const QPointF &pos); + bool isExpanded() const; + void persistPos(); + void readPos(); + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); + void setTimeAxis(DiveCartesianAxis *axis); + void setPlotInfo(const plot_info &plot); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); +public +slots: + void setRect(const QRectF &rect); + +private: + typedef QPair ToolTip; + QVector toolTips; + ToolTip entryToolTip; + QGraphicsSimpleTextItem *title; + Status status; + QRectF rectangle; + QRectF nextRectangle; + DiveCartesianAxis *timeAxis; + plot_info pInfo; + int lastTime; + QTime refreshTime; + QList oldSelection; +}; + +#endif // DIVETOOLTIPITEM_H diff --git a/desktop-widgets/profile/profilewidget2.cpp b/desktop-widgets/profile/profilewidget2.cpp new file mode 100644 index 000000000..3ccd1bb6d --- /dev/null +++ b/desktop-widgets/profile/profilewidget2.cpp @@ -0,0 +1,1836 @@ +#include "profilewidget2.h" +#include "diveplotdatamodel.h" +#include "helpers.h" +#include "profile.h" +#include "diveeventitem.h" +#include "divetextitem.h" +#include "divetooltipitem.h" +#include "planner.h" +#include "device.h" +#include "ruleritem.h" +#include "tankitem.h" +#include "pref.h" +#include "divepicturewidget.h" +#include "diveplannermodel.h" +#include "models.h" +#include "divepicturemodel.h" +#include "maintab.h" +#include "diveplanner.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifndef QT_NO_DEBUG +#include +#endif +#include "mainwindow.h" +#include + +/* This is the global 'Item position' variable. + * it should tell you where to position things up + * on the canvas. + * + * please, please, please, use this instead of + * hard coding the item on the scene with a random + * value. + */ +static struct _ItemPos { + struct _Pos { + QPointF on; + QPointF off; + }; + struct _Axis { + _Pos pos; + QLineF shrinked; + QLineF expanded; + QLineF intermediate; + }; + _Pos background; + _Pos dcLabel; + _Pos tankBar; + _Axis depth; + _Axis partialPressure; + _Axis partialPressureTissue; + _Axis partialPressureWithTankBar; + _Axis percentage; + _Axis percentageWithTankBar; + _Axis time; + _Axis cylinder; + _Axis temperature; + _Axis temperatureAll; + _Axis heartBeat; + _Axis heartBeatWithTankBar; +} itemPos; + +ProfileWidget2::ProfileWidget2(QWidget *parent) : QGraphicsView(parent), + currentState(INVALID), + dataModel(new DivePlotDataModel(this)), + zoomLevel(0), + zoomFactor(1.15), + background(new DivePixmapItem()), + backgroundFile(":poster"), + toolTipItem(new ToolTipItem()), + isPlotZoomed(prefs.zoomed_plot),// no! bad use of prefs. 'PreferencesDialog::loadSettings' NOT CALLED yet. + profileYAxis(new DepthAxis()), + gasYAxis(new PartialGasPressureAxis()), + temperatureAxis(new TemperatureAxis()), + timeAxis(new TimeAxis()), + diveProfileItem(new DiveProfileItem()), + temperatureItem(new DiveTemperatureItem()), + meanDepthItem(new DiveMeanDepthItem()), + cylinderPressureAxis(new DiveCartesianAxis()), + gasPressureItem(new DiveGasPressureItem()), + diveComputerText(new DiveTextItem()), + diveCeiling(new DiveCalculatedCeiling()), + gradientFactor(new DiveTextItem()), + reportedCeiling(new DiveReportedCeiling()), + pn2GasItem(new PartialPressureGasItem()), + pheGasItem(new PartialPressureGasItem()), + po2GasItem(new PartialPressureGasItem()), + o2SetpointGasItem(new PartialPressureGasItem()), + ccrsensor1GasItem(new PartialPressureGasItem()), + ccrsensor2GasItem(new PartialPressureGasItem()), + ccrsensor3GasItem(new PartialPressureGasItem()), + heartBeatAxis(new DiveCartesianAxis()), + heartBeatItem(new DiveHeartrateItem()), + percentageAxis(new DiveCartesianAxis()), + ambPressureItem(new DiveAmbPressureItem()), + gflineItem(new DiveGFLineItem()), + mouseFollowerVertical(new DiveLineItem()), + mouseFollowerHorizontal(new DiveLineItem()), + rulerItem(new RulerItem2()), + tankItem(new TankItem()), + isGrayscale(false), + printMode(false), + shouldCalculateMaxTime(true), + shouldCalculateMaxDepth(true), + fontPrintScale(1.0) +{ + // would like to be able to ASSERT here that PreferencesDialog::loadSettings has been called. + isPlotZoomed = prefs.zoomed_plot; // now it seems that 'prefs' has loaded our preferences + + memset(&plotInfo, 0, sizeof(plotInfo)); + + setupSceneAndFlags(); + setupItemSizes(); + setupItemOnScene(); + addItemsToScene(); + scene()->installEventFilter(this); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + QAction *action = NULL; +#define ADD_ACTION(SHORTCUT, Slot) \ + action = new QAction(this); \ + action->setShortcut(SHORTCUT); \ + action->setShortcutContext(Qt::WindowShortcut); \ + addAction(action); \ + connect(action, SIGNAL(triggered(bool)), this, SLOT(Slot)); \ + actionsForKeys[SHORTCUT] = action; + + ADD_ACTION(Qt::Key_Escape, keyEscAction()); + ADD_ACTION(Qt::Key_Delete, keyDeleteAction()); + ADD_ACTION(Qt::Key_Up, keyUpAction()); + ADD_ACTION(Qt::Key_Down, keyDownAction()); + ADD_ACTION(Qt::Key_Left, keyLeftAction()); + ADD_ACTION(Qt::Key_Right, keyRightAction()); +#undef ADD_ACTION + +#if !defined(QT_NO_DEBUG) && defined(SHOW_PLOT_INFO_TABLE) + QTableView *diveDepthTableView = new QTableView(); + diveDepthTableView->setModel(dataModel); + diveDepthTableView->show(); +#endif +} + + +ProfileWidget2::~ProfileWidget2() +{ + delete background; + delete toolTipItem; + delete profileYAxis; + delete gasYAxis; + delete temperatureAxis; + delete timeAxis; + delete diveProfileItem; + delete temperatureItem; + delete meanDepthItem; + delete cylinderPressureAxis; + delete gasPressureItem; + delete diveComputerText; + delete diveCeiling; + delete reportedCeiling; + delete pn2GasItem; + delete pheGasItem; + delete po2GasItem; + delete o2SetpointGasItem; + delete ccrsensor1GasItem; + delete ccrsensor2GasItem; + delete ccrsensor3GasItem; + delete heartBeatAxis; + delete heartBeatItem; + delete percentageAxis; + delete ambPressureItem; + delete gflineItem; + delete mouseFollowerVertical; + delete mouseFollowerHorizontal; + delete rulerItem; + delete tankItem; +} + +#define SUBSURFACE_OBJ_DATA 1 +#define SUBSURFACE_OBJ_DC_TEXT 0x42 + +void ProfileWidget2::addItemsToScene() +{ + scene()->addItem(background); + scene()->addItem(toolTipItem); + scene()->addItem(profileYAxis); + scene()->addItem(gasYAxis); + scene()->addItem(temperatureAxis); + scene()->addItem(timeAxis); + scene()->addItem(diveProfileItem); + scene()->addItem(cylinderPressureAxis); + scene()->addItem(temperatureItem); + scene()->addItem(meanDepthItem); + scene()->addItem(gasPressureItem); + // I cannot seem to figure out if an object that I find with itemAt() on the scene + // is the object I am looking for - my guess is there's a simple way in Qt to do that + // but nothing I tried worked. + // so instead this adds a special magic key/value pair to the object to mark it + diveComputerText->setData(SUBSURFACE_OBJ_DATA, SUBSURFACE_OBJ_DC_TEXT); + scene()->addItem(diveComputerText); + scene()->addItem(diveCeiling); + scene()->addItem(gradientFactor); + scene()->addItem(reportedCeiling); + scene()->addItem(pn2GasItem); + scene()->addItem(pheGasItem); + scene()->addItem(po2GasItem); + scene()->addItem(o2SetpointGasItem); + scene()->addItem(ccrsensor1GasItem); + scene()->addItem(ccrsensor2GasItem); + scene()->addItem(ccrsensor3GasItem); + scene()->addItem(percentageAxis); + scene()->addItem(heartBeatAxis); + scene()->addItem(heartBeatItem); + scene()->addItem(rulerItem); + scene()->addItem(rulerItem->sourceNode()); + scene()->addItem(rulerItem->destNode()); + scene()->addItem(tankItem); + scene()->addItem(mouseFollowerHorizontal); + scene()->addItem(mouseFollowerVertical); + QPen pen(QColor(Qt::red).lighter()); + pen.setWidth(0); + mouseFollowerHorizontal->setPen(pen); + mouseFollowerVertical->setPen(pen); + Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { + scene()->addItem(tissue); + } + Q_FOREACH (DivePercentageItem *percentage, allPercentages) { + scene()->addItem(percentage); + } + scene()->addItem(ambPressureItem); + scene()->addItem(gflineItem); +} + +void ProfileWidget2::setupItemOnScene() +{ + background->setZValue(9999); + toolTipItem->setZValue(9998); + toolTipItem->setTimeAxis(timeAxis); + rulerItem->setZValue(9997); + tankItem->setZValue(100); + + profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom); + profileYAxis->setMinimum(0); + profileYAxis->setTickInterval(M_OR_FT(10, 30)); + profileYAxis->setTickSize(0.5); + profileYAxis->setLineSize(96); + + timeAxis->setLineSize(92); + timeAxis->setTickSize(-0.5); + + gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop); + gasYAxis->setTickInterval(1); + gasYAxis->setTickSize(1); + gasYAxis->setMinimum(0); + gasYAxis->setModel(dataModel); + gasYAxis->setFontLabelScale(0.7); + gasYAxis->setLineSize(96); + + heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop); + heartBeatAxis->setTickSize(0.2); + heartBeatAxis->setTickInterval(10); + heartBeatAxis->setFontLabelScale(0.7); + heartBeatAxis->setLineSize(96); + + percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop); + percentageAxis->setTickSize(0.2); + percentageAxis->setTickInterval(10); + percentageAxis->setFontLabelScale(0.7); + percentageAxis->setLineSize(96); + + temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop); + temperatureAxis->setTickSize(2); + temperatureAxis->setTickInterval(300); + + cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop); + cylinderPressureAxis->setTickSize(2); + cylinderPressureAxis->setTickInterval(30000); + + + diveComputerText->setAlignment(Qt::AlignRight | Qt::AlignTop); + diveComputerText->setBrush(getColor(TIME_TEXT, isGrayscale)); + + rulerItem->setAxis(timeAxis, profileYAxis); + tankItem->setHorizontalAxis(timeAxis); + + // show the gradient factor at the top in the center + gradientFactor->setY(0); + gradientFactor->setX(50); + gradientFactor->setBrush(getColor(PRESSURE_TEXT)); + gradientFactor->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + + setupItem(reportedCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); + setupItem(diveCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); + for (int i = 0; i < 16; i++) { + DiveCalculatedTissue *tissueItem = new DiveCalculatedTissue(); + setupItem(tissueItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::TISSUE_1 + i, DivePlotDataModel::TIME, 1 + i); + allTissues.append(tissueItem); + DivePercentageItem *percentageItem = new DivePercentageItem(i); + setupItem(percentageItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::PERCENTAGE_1 + i, DivePlotDataModel::TIME, 1 + i); + allPercentages.append(percentageItem); + } + setupItem(gasPressureItem, timeAxis, cylinderPressureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); + setupItem(temperatureItem, timeAxis, temperatureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); + setupItem(heartBeatItem, timeAxis, heartBeatAxis, dataModel, DivePlotDataModel::HEARTBEAT, DivePlotDataModel::TIME, 1); + setupItem(ambPressureItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::AMBPRESSURE, DivePlotDataModel::TIME, 1); + setupItem(gflineItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::GFLINE, DivePlotDataModel::TIME, 1); + setupItem(diveProfileItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::DEPTH, DivePlotDataModel::TIME, 0); + setupItem(meanDepthItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::INSTANT_MEANDEPTH, DivePlotDataModel::TIME, 1); + + +#define CREATE_PP_GAS(ITEM, VERTICAL_COLUMN, COLOR, COLOR_ALERT, THRESHOULD_SETTINGS, VISIBILITY_SETTINGS) \ + setupItem(ITEM, timeAxis, gasYAxis, dataModel, DivePlotDataModel::VERTICAL_COLUMN, DivePlotDataModel::TIME, 0); \ + ITEM->setThreshouldSettingsKey(THRESHOULD_SETTINGS); \ + ITEM->setVisibilitySettingsKey(VISIBILITY_SETTINGS); \ + ITEM->setColors(getColor(COLOR, isGrayscale), getColor(COLOR_ALERT, isGrayscale)); \ + ITEM->settingsChanged(); \ + ITEM->setZValue(99); + + CREATE_PP_GAS(pn2GasItem, PN2, PN2, PN2_ALERT, &prefs.pp_graphs.pn2_threshold, "pn2graph"); + CREATE_PP_GAS(pheGasItem, PHE, PHE, PHE_ALERT, &prefs.pp_graphs.phe_threshold, "phegraph"); + CREATE_PP_GAS(po2GasItem, PO2, PO2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); + CREATE_PP_GAS(o2SetpointGasItem, O2SETPOINT, PO2_ALERT, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); + CREATE_PP_GAS(ccrsensor1GasItem, CCRSENSOR1, CCRSENSOR1, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); + CREATE_PP_GAS(ccrsensor2GasItem, CCRSENSOR2, CCRSENSOR2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); + CREATE_PP_GAS(ccrsensor3GasItem, CCRSENSOR3, CCRSENSOR3, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); +#undef CREATE_PP_GAS + + temperatureAxis->setTextVisible(false); + temperatureAxis->setLinesVisible(false); + cylinderPressureAxis->setTextVisible(false); + cylinderPressureAxis->setLinesVisible(false); + timeAxis->setLinesVisible(true); + profileYAxis->setLinesVisible(true); + gasYAxis->setZValue(timeAxis->zValue() + 1); + heartBeatAxis->setTextVisible(true); + heartBeatAxis->setLinesVisible(true); + percentageAxis->setTextVisible(true); + percentageAxis->setLinesVisible(true); + + replotEnabled = true; +} + +void ProfileWidget2::replot(struct dive *d) +{ + if (!replotEnabled) + return; + dataModel->clear(); + plotDive(d, true); +} + +void ProfileWidget2::setupItemSizes() +{ + // Scene is *always* (double) 100 / 100. + // Background Config + /* Much probably a better math is needed here. + * good thing is that we only need to change the + * Axis and everything else is auto-adjusted.* + */ + + itemPos.background.on.setX(0); + itemPos.background.on.setY(0); + itemPos.background.off.setX(0); + itemPos.background.off.setY(110); + + //Depth Axis Config + itemPos.depth.pos.on.setX(3); + itemPos.depth.pos.on.setY(3); + itemPos.depth.pos.off.setX(-2); + itemPos.depth.pos.off.setY(3); + itemPos.depth.expanded.setP1(QPointF(0, 0)); + itemPos.depth.expanded.setP2(QPointF(0, 85)); + itemPos.depth.shrinked.setP1(QPointF(0, 0)); + itemPos.depth.shrinked.setP2(QPointF(0, 55)); + itemPos.depth.intermediate.setP1(QPointF(0, 0)); + itemPos.depth.intermediate.setP2(QPointF(0, 65)); + + // Time Axis Config + itemPos.time.pos.on.setX(3); + itemPos.time.pos.on.setY(95); + itemPos.time.pos.off.setX(3); + itemPos.time.pos.off.setY(110); + itemPos.time.expanded.setP1(QPointF(0, 0)); + itemPos.time.expanded.setP2(QPointF(94, 0)); + + // Partial Gas Axis Config + itemPos.partialPressure.pos.on.setX(97); + itemPos.partialPressure.pos.on.setY(75); + itemPos.partialPressure.pos.off.setX(110); + itemPos.partialPressure.pos.off.setY(63); + itemPos.partialPressure.expanded.setP1(QPointF(0, 0)); + itemPos.partialPressure.expanded.setP2(QPointF(0, 19)); + itemPos.partialPressureWithTankBar = itemPos.partialPressure; + itemPos.partialPressureWithTankBar.expanded.setP2(QPointF(0, 17)); + itemPos.partialPressureTissue = itemPos.partialPressure; + itemPos.partialPressureTissue.pos.on.setX(97); + itemPos.partialPressureTissue.pos.on.setY(65); + itemPos.partialPressureTissue.expanded.setP2(QPointF(0, 16)); + + // cylinder axis config + itemPos.cylinder.pos.on.setX(3); + itemPos.cylinder.pos.on.setY(20); + itemPos.cylinder.pos.off.setX(-10); + itemPos.cylinder.pos.off.setY(20); + itemPos.cylinder.expanded.setP1(QPointF(0, 15)); + itemPos.cylinder.expanded.setP2(QPointF(0, 50)); + itemPos.cylinder.shrinked.setP1(QPointF(0, 0)); + itemPos.cylinder.shrinked.setP2(QPointF(0, 20)); + itemPos.cylinder.intermediate.setP1(QPointF(0, 0)); + itemPos.cylinder.intermediate.setP2(QPointF(0, 20)); + + // Temperature axis config + itemPos.temperature.pos.on.setX(3); + itemPos.temperature.pos.on.setY(60); + itemPos.temperatureAll.pos.on.setY(51); + itemPos.temperature.pos.off.setX(-10); + itemPos.temperature.pos.off.setY(40); + itemPos.temperature.expanded.setP1(QPointF(0, 20)); + itemPos.temperature.expanded.setP2(QPointF(0, 33)); + itemPos.temperature.shrinked.setP1(QPointF(0, 2)); + itemPos.temperature.shrinked.setP2(QPointF(0, 12)); + itemPos.temperature.intermediate.setP1(QPointF(0, 2)); + itemPos.temperature.intermediate.setP2(QPointF(0, 12)); + + // Heartbeat axis config + itemPos.heartBeat.pos.on.setX(3); + itemPos.heartBeat.pos.on.setY(82); + itemPos.heartBeat.expanded.setP1(QPointF(0, 0)); + itemPos.heartBeat.expanded.setP2(QPointF(0, 10)); + itemPos.heartBeatWithTankBar = itemPos.heartBeat; + itemPos.heartBeatWithTankBar.expanded.setP2(QPointF(0, 7)); + + // Percentage axis config + itemPos.percentage.pos.on.setX(3); + itemPos.percentage.pos.on.setY(80); + itemPos.percentage.expanded.setP1(QPointF(0, 0)); + itemPos.percentage.expanded.setP2(QPointF(0, 15)); + itemPos.percentageWithTankBar = itemPos.percentage; + itemPos.percentageWithTankBar.expanded.setP2(QPointF(0, 12)); + + itemPos.dcLabel.on.setX(3); + itemPos.dcLabel.on.setY(100); + itemPos.dcLabel.off.setX(-10); + itemPos.dcLabel.off.setY(100); + + itemPos.tankBar.on.setX(0); + itemPos.tankBar.on.setY(91.5); +} + +void ProfileWidget2::setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, + DiveCartesianAxis *vAxis, DivePlotDataModel *model, + int vData, int hData, int zValue) +{ + item->setHorizontalAxis(hAxis); + item->setVerticalAxis(vAxis); + item->setModel(model); + item->setVerticalDataColumn(vData); + item->setHorizontalDataColumn(hData); + item->setZValue(zValue); +} + +void ProfileWidget2::setupSceneAndFlags() +{ + setScene(new QGraphicsScene(this)); + scene()->setSceneRect(0, 0, 100, 100); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scene()->setItemIndexMethod(QGraphicsScene::NoIndex); + setOptimizationFlags(QGraphicsView::DontSavePainterState); + setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + setMouseTracking(true); + background->setFlag(QGraphicsItem::ItemIgnoresTransformations); +} + +void ProfileWidget2::resetZoom() +{ + if (!zoomLevel) + return; + const qreal defScale = 1.0 / qPow(zoomFactor, (qreal)zoomLevel); + scale(defScale, defScale); + zoomLevel = 0; +} + +// Currently just one dive, but the plan is to enable All of the selected dives. +void ProfileWidget2::plotDive(struct dive *d, bool force) +{ + static bool firstCall = true; + QTime measureDuration; // let's measure how long this takes us (maybe we'll turn of TTL calculation later + measureDuration.start(); + + if (currentState != ADD && currentState != PLAN) { + if (!d) { + if (selected_dive == -1) + return; + d = current_dive; // display the current dive + } + + // No need to do this again if we are already showing the same dive + // computer of the same dive, so we check the unique id of the dive + // and the selected dive computer number against the ones we are + // showing (can't compare the dive pointers as those might change). + if (d->id == displayed_dive.id && dc_number == dataModel->dcShown() && !force) + return; + + // this copies the dive and makes copies of all the relevant additional data + copy_dive(d, &displayed_dive); + gradientFactor->setText(QString("GF %1/%2").arg(prefs.gflow).arg(prefs.gfhigh)); + } else { + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + plannerModel->createTemporaryPlan(); + struct diveplan &diveplan = plannerModel->getDiveplan(); + if (!diveplan.dp) { + plannerModel->deleteTemporaryPlan(); + return; + } + gradientFactor->setText(QString("GF %1/%2").arg(diveplan.gflow).arg(diveplan.gfhigh)); + } + + // special handling for the first time we display things + int animSpeedBackup = 0; + if (firstCall && MainWindow::instance()->filesFromCommandLine()) { + animSpeedBackup = prefs.animation_speed; + prefs.animation_speed = 0; + firstCall = false; + } + + // restore default zoom level + resetZoom(); + + // reset some item visibility on printMode changes + toolTipItem->setVisible(!printMode); + rulerItem->setVisible(prefs.rulergraph && !printMode && currentState != PLAN && currentState != ADD); + + if (currentState == EMPTY) + setProfileState(); + + // next get the dive computer structure - if there are no samples + // let's create a fake profile that's somewhat reasonable for the + // data that we have + struct divecomputer *currentdc = select_dc(&displayed_dive); + Q_ASSERT(currentdc); + if (!currentdc || !currentdc->samples) { + currentdc = fake_dc(currentdc); + } + + bool setpointflag = (currentdc->divemode == CCR) && prefs.pp_graphs.po2 && current_dive; + bool sensorflag = setpointflag && prefs.show_ccr_sensors; + o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); + ccrsensor1GasItem->setVisible(sensorflag); + ccrsensor2GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 1)); + ccrsensor3GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 2)); + + /* This struct holds all the data that's about to be plotted. + * I'm not sure this is the best approach ( but since we are + * interpolating some points of the Dive, maybe it is... ) + * The Calculation of the points should be done per graph, + * so I'll *not* calculate everything if something is not being + * shown. + */ + plotInfo = calculate_max_limits_new(&displayed_dive, currentdc); + create_plot_info_new(&displayed_dive, currentdc, &plotInfo, !shouldCalculateMaxDepth); + if (shouldCalculateMaxTime) + maxtime = get_maxtime(&plotInfo); + + /* Only update the max depth if it's bigger than the current ones + * when we are dragging the handler to plan / add dive. + * otherwhise, update normally. + */ + int newMaxDepth = get_maxdepth(&plotInfo); + if (!shouldCalculateMaxDepth) { + if (maxdepth < newMaxDepth) { + maxdepth = newMaxDepth; + } + } else { + maxdepth = newMaxDepth; + } + + dataModel->setDive(&displayed_dive, plotInfo); + toolTipItem->setPlotInfo(plotInfo); + + // It seems that I'll have a lot of boilerplate setting the model / axis for + // each item, I'll mostly like to fix this in the future, but I'll keep at this for now. + profileYAxis->setMaximum(maxdepth); + profileYAxis->updateTicks(); + + temperatureAxis->setMinimum(plotInfo.mintemp); + temperatureAxis->setMaximum(plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000); + + if (plotInfo.maxhr) { + heartBeatAxis->setMinimum(plotInfo.minhr); + heartBeatAxis->setMaximum(plotInfo.maxhr); + heartBeatAxis->updateTicks(HR_AXIS); // this shows the ticks + } + heartBeatAxis->setVisible(prefs.hrgraph && plotInfo.maxhr); + + percentageAxis->setMinimum(0); + percentageAxis->setMaximum(100); + percentageAxis->setVisible(false); + percentageAxis->updateTicks(HR_AXIS); + + timeAxis->setMaximum(maxtime); + int i, incr; + static int increments[8] = { 10, 20, 30, 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60 }; + /* Time markers: at most every 10 seconds, but no more than 12 markers. + * We start out with 10 seconds and increment up to 30 minutes, + * depending on the dive time. + * This allows for 6h dives - enough (I hope) for even the craziest + * divers - but just in case, for those 8h depth-record-breaking dives, + * we double the interval if this still doesn't get us to 12 or fewer + * time markers */ + i = 0; + while (i < 7 && maxtime / increments[i] > 12) + i++; + incr = increments[i]; + while (maxtime / incr > 12) + incr *= 2; + timeAxis->setTickInterval(incr); + timeAxis->updateTicks(); + cylinderPressureAxis->setMinimum(plotInfo.minpressure); + cylinderPressureAxis->setMaximum(plotInfo.maxpressure); + + rulerItem->setPlotInfo(plotInfo); + tankItem->setData(dataModel, &plotInfo, &displayed_dive); + + dataModel->emitDataChanged(); + // The event items are a bit special since we don't know how many events are going to + // exist on a dive, so I cant create cache items for that. that's why they are here + // while all other items are up there on the constructor. + qDeleteAll(eventItems); + eventItems.clear(); + struct event *event = currentdc->events; + while (event) { + // if print mode is selected only draw headings, SP change, gas events or bookmark event + if (printMode) { + if (same_string(event->name, "") || + !(strcmp(event->name, "heading") == 0 || + (same_string(event->name, "SP change") && event->time.seconds == 0) || + event_is_gaschange(event) || + event->type == SAMPLE_EVENT_BOOKMARK)) { + event = event->next; + continue; + } + } + DiveEventItem *item = new DiveEventItem(); + item->setHorizontalAxis(timeAxis); + item->setVerticalAxis(profileYAxis); + item->setModel(dataModel); + item->setEvent(event); + item->setZValue(2); + scene()->addItem(item); + eventItems.push_back(item); + event = event->next; + } + // Only set visible the events that should be visible + Q_FOREACH (DiveEventItem *event, eventItems) { + event->setVisible(!event->shouldBeHidden()); + } + QString dcText = get_dc_nickname(currentdc->model, currentdc->deviceid); + int nr; + if ((nr = number_of_computers(&displayed_dive)) > 1) + dcText += tr(" (#%1 of %2)").arg(dc_number + 1).arg(nr); + if (dcText.isEmpty()) + dcText = tr("Unknown dive computer"); + diveComputerText->setText(dcText); + if (MainWindow::instance()->filesFromCommandLine() && animSpeedBackup != 0) { + prefs.animation_speed = animSpeedBackup; + } + + if (currentState == ADD || currentState == PLAN) { // TODO: figure a way to move this from here. + repositionDiveHandlers(); + DivePlannerPointsModel *model = DivePlannerPointsModel::instance(); + model->deleteTemporaryPlan(); + } + plotPictures(); + + // 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. + if (measureDuration.elapsed() > 1000 && prefs.calcndltts) { + MainWindow::instance()->turnOffNdlTts(); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Show NDL / TTS was disabled because of excessive processing time"), KMessageWidget::Error); + } + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + +} + +void ProfileWidget2::recalcCeiling() +{ + diveCeiling->recalc(); +} + +void ProfileWidget2::settingsChanged() +{ + // if we are showing calculated ceilings then we have to replot() + // because the GF could have changed; otherwise we try to avoid replot() + bool needReplot = prefs.calcceiling; + if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { + profileYAxis->animateChangeLine(itemPos.depth.shrinked); + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); + + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + }else { + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->animateChangeLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); + + } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { + profileYAxis->animateChangeLine(itemPos.depth.intermediate); + temperatureAxis->setPos(itemPos.temperature.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); + gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + } else { + gasYAxis->setPos(itemPos.partialPressure.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->setLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + } else { + profileYAxis->animateChangeLine(itemPos.depth.expanded); + if (prefs.tankbar) { + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + } else { + temperatureAxis->setPos(itemPos.temperature.pos.on); + } + temperatureAxis->animateChangeLine(itemPos.temperature.expanded); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); + } + + tankItem->setVisible(prefs.tankbar); + if (prefs.zoomed_plot != isPlotZoomed) { + isPlotZoomed = prefs.zoomed_plot; + needReplot = true; + } + if (needReplot) + replot(); +} + +void ProfileWidget2::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + fitInView(sceneRect(), Qt::IgnoreAspectRatio); + fixBackgroundPos(); +} + +void ProfileWidget2::mousePressEvent(QMouseEvent *event) +{ + if (zoomLevel) + return; + QGraphicsView::mousePressEvent(event); + if (currentState == PLAN) + shouldCalculateMaxTime = false; +} + +void ProfileWidget2::divePlannerHandlerClicked() +{ + if (zoomLevel) + return; + shouldCalculateMaxDepth = false; + replot(); +} + +void ProfileWidget2::divePlannerHandlerReleased() +{ + if (zoomLevel) + return; + shouldCalculateMaxDepth = true; + replot(); +} + +void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event) +{ + if (zoomLevel) + return; + QGraphicsView::mouseReleaseEvent(event); + if (currentState == PLAN) { + shouldCalculateMaxTime = true; + replot(); + } +} + +void ProfileWidget2::fixBackgroundPos() +{ + static QPixmap toBeScaled(backgroundFile); + if (currentState != EMPTY) + return; + QPixmap p = toBeScaled.scaledToHeight(viewport()->height() - 40, Qt::SmoothTransformation); + int x = viewport()->width() / 2 - p.width() / 2; + int y = viewport()->height() / 2 - p.height() / 2; + background->setPixmap(p); + background->setX(mapToScene(x, 0).x()); + background->setY(mapToScene(y, 20).y()); +} + +void ProfileWidget2::wheelEvent(QWheelEvent *event) +{ + if (currentState == EMPTY) + return; + QPoint toolTipPos = mapFromScene(toolTipItem->pos()); + if (event->buttons() == Qt::LeftButton) + return; + if (event->delta() > 0 && zoomLevel < 20) { + scale(zoomFactor, zoomFactor); + zoomLevel++; + } else if (event->delta() < 0 && zoomLevel > 0) { + // Zooming out + scale(1.0 / zoomFactor, 1.0 / zoomFactor); + zoomLevel--; + } + scrollViewTo(event->pos()); + toolTipItem->setPos(mapToScene(toolTipPos)); +} + +void ProfileWidget2::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (currentState == PLAN || currentState == ADD) { + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + QPointF mappedPos = mapToScene(event->pos()); + if (isPointOutOfBoundaries(mappedPos)) + return; + + int minutes = rint(timeAxis->valueAt(mappedPos) / 60); + int milimeters = rint(profileYAxis->valueAt(mappedPos) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); + plannerModel->addStop(milimeters, minutes * 60, 0, 0, true); + } +} + +bool ProfileWidget2::isPointOutOfBoundaries(const QPointF &point) const +{ + double xpos = timeAxis->valueAt(point); + double ypos = profileYAxis->valueAt(point); + return (xpos > timeAxis->maximum() || + xpos < timeAxis->minimum() || + ypos > profileYAxis->maximum() || + ypos < profileYAxis->minimum()); +} + +void ProfileWidget2::scrollViewTo(const QPoint &pos) +{ + /* since we cannot use translate() directly on the scene we hack on + * the scroll bars (hidden) functionality */ + if (!zoomLevel || currentState == EMPTY) + return; + QScrollBar *vs = verticalScrollBar(); + QScrollBar *hs = horizontalScrollBar(); + const qreal yRat = (qreal)pos.y() / viewport()->height(); + const qreal xRat = (qreal)pos.x() / viewport()->width(); + vs->setValue(yRat * vs->maximum()); + hs->setValue(xRat * hs->maximum()); +} + +void ProfileWidget2::mouseMoveEvent(QMouseEvent *event) +{ + QPointF pos = mapToScene(event->pos()); + toolTipItem->refresh(pos); + if (zoomLevel == 0) { + QGraphicsView::mouseMoveEvent(event); + } else { + QPoint toolTipPos = mapFromScene(toolTipItem->pos()); + scrollViewTo(event->pos()); + toolTipItem->setPos(mapToScene(toolTipPos)); + } + + qreal vValue = profileYAxis->valueAt(pos); + qreal hValue = timeAxis->valueAt(pos); + if (profileYAxis->maximum() >= vValue && profileYAxis->minimum() <= vValue) { + mouseFollowerHorizontal->setPos(timeAxis->pos().x(), pos.y()); + } + if (timeAxis->maximum() >= hValue && timeAxis->minimum() <= hValue) { + mouseFollowerVertical->setPos(pos.x(), profileYAxis->line().y1()); + } +} + +bool ProfileWidget2::eventFilter(QObject *object, QEvent *event) +{ + QGraphicsScene *s = qobject_cast(object); + if (s && event->type() == QEvent::GraphicsSceneHelp) { + event->ignore(); + return true; + } + return QGraphicsView::eventFilter(object, event); +} + +void ProfileWidget2::setEmptyState() +{ + // Then starting Empty State, move the background up. + if (currentState == EMPTY) + return; + + disconnectTemporaryConnections(); + setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); + dataModel->clear(); + currentState = EMPTY; + MainWindow::instance()->setEnabledToolbar(false); + + fixBackgroundPos(); + background->setVisible(true); + + profileYAxis->setVisible(false); + gasYAxis->setVisible(false); + timeAxis->setVisible(false); + temperatureAxis->setVisible(false); + cylinderPressureAxis->setVisible(false); + toolTipItem->setVisible(false); + diveComputerText->setVisible(false); + diveCeiling->setVisible(false); + gradientFactor->setVisible(false); + reportedCeiling->setVisible(false); + rulerItem->setVisible(false); + tankItem->setVisible(false); + pn2GasItem->setVisible(false); + po2GasItem->setVisible(false); + o2SetpointGasItem->setVisible(false); + ccrsensor1GasItem->setVisible(false); + ccrsensor2GasItem->setVisible(false); + ccrsensor3GasItem->setVisible(false); + pheGasItem->setVisible(false); + ambPressureItem->setVisible(false); + gflineItem->setVisible(false); + mouseFollowerHorizontal->setVisible(false); + mouseFollowerVertical->setVisible(false); + +#define HIDE_ALL(TYPE, CONTAINER) \ + Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); + HIDE_ALL(DiveCalculatedTissue, allTissues); + HIDE_ALL(DivePercentageItem, allPercentages); + HIDE_ALL(DiveEventItem, eventItems); + HIDE_ALL(DiveHandler, handles); + HIDE_ALL(QGraphicsSimpleTextItem, gases); +#undef HIDE_ALL +} + +void ProfileWidget2::setProfileState() +{ + // Then starting Empty State, move the background up. + if (currentState == PROFILE) + return; + + disconnectTemporaryConnections(); + connect(DivePictureModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(plotPictures())); + connect(DivePictureModel::instance(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(plotPictures())); + connect(DivePictureModel::instance(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(plotPictures())); + /* show the same stuff that the profile shows. */ + + //TODO: Move the DC handling to another method. + MainWindow::instance()->enableShortcuts(); + + currentState = PROFILE; + MainWindow::instance()->setEnabledToolbar(true); + toolTipItem->readPos(); + setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); + + background->setVisible(false); + toolTipItem->setVisible(true); + profileYAxis->setVisible(true); + gasYAxis->setVisible(true); + timeAxis->setVisible(true); + temperatureAxis->setVisible(true); + cylinderPressureAxis->setVisible(true); + + profileYAxis->setPos(itemPos.depth.pos.on); + if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { + profileYAxis->animateChangeLine(itemPos.depth.shrinked); + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); + + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + }else { + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->animateChangeLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); + + } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { + profileYAxis->animateChangeLine(itemPos.depth.intermediate); + temperatureAxis->setPos(itemPos.temperature.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); + gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + } else { + gasYAxis->setPos(itemPos.partialPressure.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->setLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + } else { + profileYAxis->animateChangeLine(itemPos.depth.expanded); + if (prefs.tankbar) { + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + } else { + temperatureAxis->setPos(itemPos.temperature.pos.on); + } + temperatureAxis->animateChangeLine(itemPos.temperature.expanded); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); + } + pn2GasItem->setVisible(prefs.pp_graphs.pn2); + po2GasItem->setVisible(prefs.pp_graphs.po2); + pheGasItem->setVisible(prefs.pp_graphs.phe); + + bool setpointflag = current_dive && (current_dc->divemode == CCR) && prefs.pp_graphs.po2; + bool sensorflag = setpointflag && prefs.show_ccr_sensors; + o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); + ccrsensor1GasItem->setVisible(sensorflag); + ccrsensor2GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 1)); + ccrsensor3GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 2)); + + timeAxis->setPos(itemPos.time.pos.on); + timeAxis->setLine(itemPos.time.expanded); + + cylinderPressureAxis->setPos(itemPos.cylinder.pos.on); + heartBeatItem->setVisible(prefs.hrgraph); + meanDepthItem->setVisible(prefs.show_average_depth); + + diveComputerText->setVisible(true); + diveComputerText->setPos(itemPos.dcLabel.on); + + diveCeiling->setVisible(prefs.calcceiling); + gradientFactor->setVisible(prefs.calcceiling); + reportedCeiling->setVisible(prefs.dcceiling); + + if (prefs.calcalltissues) { + Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { + tissue->setVisible(true); + } + } + + if (prefs.percentagegraph) { + Q_FOREACH (DivePercentageItem *percentage, allPercentages) { + percentage->setVisible(true); + } + + ambPressureItem->setVisible(true); + gflineItem->setVisible(true); + } + + rulerItem->setVisible(prefs.rulergraph); + tankItem->setVisible(prefs.tankbar); + tankItem->setPos(itemPos.tankBar.on); + +#define HIDE_ALL(TYPE, CONTAINER) \ + Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); + HIDE_ALL(DiveHandler, handles); + HIDE_ALL(QGraphicsSimpleTextItem, gases); +#undef HIDE_ALL + mouseFollowerHorizontal->setVisible(false); + mouseFollowerVertical->setVisible(false); +} + +void ProfileWidget2::clearHandlers() +{ + if (handles.count()) { + foreach (DiveHandler *handle, handles) { + scene()->removeItem(handle); + delete handle; + } + handles.clear(); + } +} + +void ProfileWidget2::setToolTipVisibile(bool visible) +{ + toolTipItem->setVisible(visible); +} + +void ProfileWidget2::setAddState() +{ + if (currentState == ADD) + return; + + clearHandlers(); + setProfileState(); + mouseFollowerHorizontal->setVisible(true); + mouseFollowerVertical->setVisible(true); + mouseFollowerHorizontal->setLine(timeAxis->line()); + mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); + disconnectTemporaryConnections(); + //TODO: Move this method to another place, shouldn't be on mainwindow. + MainWindow::instance()->disableShortcuts(false); + actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); + actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); + actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); + actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); + actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); + actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + /* show the same stuff that the profile shows. */ + currentState = ADD; /* enable the add state. */ + diveCeiling->setVisible(true); + gradientFactor->setVisible(true); + setBackgroundBrush(QColor("#A7DCFF")); +} + +void ProfileWidget2::setPlanState() +{ + if (currentState == PLAN) + return; + + setProfileState(); + mouseFollowerHorizontal->setVisible(true); + mouseFollowerVertical->setVisible(true); + mouseFollowerHorizontal->setLine(timeAxis->line()); + mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); + disconnectTemporaryConnections(); + //TODO: Move this method to another place, shouldn't be on mainwindow. + MainWindow::instance()->disableShortcuts(); + actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); + actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); + actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); + actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); + actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); + actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + /* show the same stuff that the profile shows. */ + currentState = PLAN; /* enable the add state. */ + diveCeiling->setVisible(true); + gradientFactor->setVisible(true); + setBackgroundBrush(QColor("#D7E3EF")); +} + +extern struct ev_select *ev_namelist; +extern int evn_allocated; +extern int evn_used; + +bool ProfileWidget2::isPlanner() +{ + return currentState == PLAN; +} + +bool ProfileWidget2::isAddOrPlanner() +{ + return currentState == PLAN || currentState == ADD; +} + +struct plot_data *ProfileWidget2::getEntryFromPos(QPointF pos) +{ + // find the time stamp corresponding to the mouse position + int seconds = timeAxis->valueAt(pos); + struct plot_data *entry = NULL; + + for (int i = 0; i < plotInfo.nr; i++) { + entry = plotInfo.entry + i; + if (entry->sec >= seconds) + break; + } + return entry; +} + +void ProfileWidget2::setReplot(bool state) +{ + replotEnabled = state; +} + +void ProfileWidget2::contextMenuEvent(QContextMenuEvent *event) +{ + if (currentState == ADD || currentState == PLAN) { + QGraphicsView::contextMenuEvent(event); + return; + } + QMenu m; + bool isDCName = false; + if (selected_dive == -1) + return; + // figure out if we are ontop of the dive computer name in the profile + QGraphicsItem *sceneItem = itemAt(mapFromGlobal(event->globalPos())); + if (sceneItem) { + QGraphicsItem *parentItem = sceneItem; + while (parentItem) { + if (parentItem->data(SUBSURFACE_OBJ_DATA) == SUBSURFACE_OBJ_DC_TEXT) { + isDCName = true; + break; + } + parentItem = parentItem->parentItem(); + } + if (isDCName) { + if (dc_number == 0 && count_divecomputers() == 1) + // nothing to do, can't delete or reorder + return; + // create menu to show when right clicking on dive computer name + if (dc_number > 0) + m.addAction(tr("Make first divecomputer"), this, SLOT(makeFirstDC())); + if (count_divecomputers() > 1) + m.addAction(tr("Delete this divecomputer"), this, SLOT(deleteCurrentDC())); + m.exec(event->globalPos()); + // don't show the regular profile context menu + return; + } + } + // create the profile context menu + QPointF scenePos = mapToScene(event->pos()); + struct plot_data *entry = getEntryFromPos(scenePos); + GasSelectionModel *model = GasSelectionModel::instance(); + model->repopulate(); + int rowCount = model->rowCount(); + if (rowCount > 1) { + // if we have more than one gas, offer to switch to another one + QMenu *gasChange = m.addMenu(tr("Add gas change")); + for (int i = 0; i < rowCount; i++) { + QAction *action = new QAction(&m); + action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString() + QString(tr(" (Tank %1)")).arg(i + 1)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); + action->setData(event->globalPos()); + if (i == entry->cylinderindex) + action->setDisabled(true); + gasChange->addAction(action); + } + } + QAction *setpointAction = m.addAction(tr("Add set-point change"), this, SLOT(addSetpointChange())); + setpointAction->setData(event->globalPos()); + QAction *action = m.addAction(tr("Add bookmark"), this, SLOT(addBookmark())); + action->setData(event->globalPos()); + + if (same_string(current_dc->model, "manually added dive")) + QAction *editProfileAction = m.addAction(tr("Edit the profile"), MainWindow::instance(), SLOT(editCurrentDive())); + + if (DiveEventItem *item = dynamic_cast(sceneItem)) { + action = new QAction(&m); + action->setText(tr("Remove event")); + action->setData(QVariant::fromValue(item)); // so we know what to remove. + connect(action, SIGNAL(triggered(bool)), this, SLOT(removeEvent())); + m.addAction(action); + action = new QAction(&m); + action->setText(tr("Hide similar events")); + action->setData(QVariant::fromValue(item)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(hideEvents())); + m.addAction(action); + struct event *dcEvent = item->getEvent(); + if (dcEvent->type == SAMPLE_EVENT_BOOKMARK) { + action = new QAction(&m); + action->setText(tr("Edit name")); + action->setData(QVariant::fromValue(item)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(editName())); + m.addAction(action); + } +#if 0 // FIXME::: FINISH OR DISABLE + // 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 (dcEvent->type == SAMPLE_EVENT_GASCHANGE || dcEvent->type == SAMPLE_EVENT_GASCHANGE2) { + qDebug() << "figure out if there are interpolated pressures"; + struct plot_data *gasChangeEntry = entry; + struct plot_data *newGasEntry; + while (gasChangeEntry > plotInfo.entry) { + --gasChangeEntry; + if (gasChangeEntry->sec <= dcEvent->time.seconds) + break; + } + qDebug() << "at gas change at" << gasChangeEntry->sec << ": sensor pressure" << gasChangeEntry->pressure[0] << "interpolated" << gasChangeEntry->pressure[1]; + // 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 (gasChangeEntry + 1 <= plotInfo.entry + plotInfo.nr) { + newGasEntry = gasChangeEntry + 1; + qDebug() << "after gas change at " << newGasEntry->sec << ": sensor pressure" << newGasEntry->pressure[0] << "interpolated" << newGasEntry->pressure[1]; + if (SENSOR_PRESSURE(gasChangeEntry) == 0 || displayed_dive.cylinder[gasChangeEntry->cylinderindex].sample_start.mbar == 0) { + // 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 = INTERPOLATED_PRESSURE(gasChangeEntry) ? : SENSOR_PRESSURE(gasChangeEntry); + QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") + .arg(gasChangeEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); + } + if (SENSOR_PRESSURE(newGasEntry) == 0 || displayed_dive.cylinder[newGasEntry->cylinderindex].sample_start.mbar == 0) { + // we only have interpolated press -- see commend above + pressure_t pressure; + pressure.mbar = INTERPOLATED_PRESSURE(newGasEntry) ? : SENSOR_PRESSURE(newGasEntry); + QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") + .arg(newGasEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); + } + } + } +#endif + } + bool some_hidden = false; + for (int i = 0; i < evn_used; i++) { + if (ev_namelist[i].plot_ev == false) { + some_hidden = true; + break; + } + } + if (some_hidden) { + action = m.addAction(tr("Unhide all events"), this, SLOT(unhideEvents())); + action->setData(event->globalPos()); + } + m.exec(event->globalPos()); +} + +void ProfileWidget2::deleteCurrentDC() +{ + delete_current_divecomputer(); + mark_divelist_changed(true); + // we need to force it since it's likely the same dive and same dc_number - but that's a different dive computer now + MainWindow::instance()->graphics()->plotDive(0, true); + MainWindow::instance()->refreshDisplay(); +} + +void ProfileWidget2::makeFirstDC() +{ + make_first_dc(); + mark_divelist_changed(true); + // this is now the first DC, so we need to redraw the profile and refresh the dive list + // (and no, it's not just enough to rewrite the text - the first DC is special so values in the + // dive list may change). + // As a side benefit, this returns focus to the dive list. + dc_number = 0; + MainWindow::instance()->refreshDisplay(); +} + +void ProfileWidget2::hideEvents() +{ + QAction *action = qobject_cast(sender()); + DiveEventItem *item = static_cast(action->data().value()); + struct event *event = item->getEvent(); + + if (QMessageBox::question(MainWindow::instance(), + TITLE_OR_TEXT(tr("Hide events"), tr("Hide all %1 events?").arg(event->name)), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + if (!same_string(event->name, "")) { + for (int i = 0; i < evn_used; i++) { + if (same_string(event->name, ev_namelist[i].ev_name)) { + ev_namelist[i].plot_ev = false; + break; + } + } + Q_FOREACH (DiveEventItem *evItem, eventItems) { + if (same_string(evItem->getEvent()->name, event->name)) + evItem->hide(); + } + } else { + item->hide(); + } + } +} + +void ProfileWidget2::unhideEvents() +{ + for (int i = 0; i < evn_used; i++) { + ev_namelist[i].plot_ev = true; + } + Q_FOREACH (DiveEventItem *item, eventItems) + item->show(); +} + +void ProfileWidget2::removeEvent() +{ + QAction *action = qobject_cast(sender()); + DiveEventItem *item = static_cast(action->data().value()); + struct event *event = item->getEvent(); + + if (QMessageBox::question(MainWindow::instance(), TITLE_OR_TEXT( + tr("Remove the selected event?"), + tr("%1 @ %2:%3").arg(event->name).arg(event->time.seconds / 60).arg(event->time.seconds % 60, 2, 10, QChar('0'))), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + remove_event(event); + mark_divelist_changed(true); + replot(); + } +} + +void ProfileWidget2::addBookmark() +{ + QAction *action = qobject_cast(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + add_event(current_dc, timeAxis->valueAt(scenePos), SAMPLE_EVENT_BOOKMARK, 0, 0, "bookmark"); + mark_divelist_changed(true); + replot(); +} + +void ProfileWidget2::addSetpointChange() +{ + QAction *action = qobject_cast(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + SetpointDialog::instance()->setpointData(current_dc, timeAxis->valueAt(scenePos)); + SetpointDialog::instance()->show(); +} + +void ProfileWidget2::changeGas() +{ + QAction *action = qobject_cast(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + QString gas = action->text(); + gas.remove(QRegExp(" \\(.*\\)")); + + // backup the things on the dataModel, since we will clear that out. + struct gasmix gasmix; + qreal sec_val = timeAxis->valueAt(scenePos); + + // no gas changes before the dive starts + unsigned int seconds = (sec_val < 0.0) ? 0 : (unsigned int)sec_val; + + // if there is a gas change at this time stamp, remove it before adding the new one + struct event *gasChangeEvent = current_dc->events; + while ((gasChangeEvent = get_next_event(gasChangeEvent, "gaschange")) != NULL) { + if (gasChangeEvent->time.seconds == seconds) { + remove_event(gasChangeEvent); + gasChangeEvent = current_dc->events; + } else { + gasChangeEvent = gasChangeEvent->next; + } + } + validate_gas(gas.toUtf8().constData(), &gasmix); + QRegExp rx("\\(\\D*(\\d+)"); + int tank; + if (rx.indexIn(action->text()) > -1) { + tank = rx.cap(1).toInt() - 1; // we display the tank 1 based + } else { + qDebug() << "failed to parse tank number"; + tank = get_gasidx(&displayed_dive, &gasmix); + } + // add this both to the displayed dive and the current dive + add_gas_switch_event(current_dive, current_dc, seconds, tank); + add_gas_switch_event(&displayed_dive, get_dive_dc(&displayed_dive, dc_number), seconds, tank); + // this means we potentially have a new tank that is being used and needs to be shown + fixup_dive(&displayed_dive); + + // FIXME - this no longer gets written to the dive list - so we need to enableEdition() here + + MainWindow::instance()->information()->updateDiveInfo(); + mark_divelist_changed(true); + replot(); +} + +bool ProfileWidget2::getPrintMode() +{ + return printMode; +} + +void ProfileWidget2::setPrintMode(bool mode, bool grayscale) +{ + printMode = mode; + resetZoom(); + + // set printMode for axes + profileYAxis->setPrintMode(mode); + gasYAxis->setPrintMode(mode); + temperatureAxis->setPrintMode(mode); + timeAxis->setPrintMode(mode); + cylinderPressureAxis->setPrintMode(mode); + heartBeatAxis->setPrintMode(mode); + percentageAxis->setPrintMode(mode); + + isGrayscale = mode ? grayscale : false; + mouseFollowerHorizontal->setVisible(!mode); + mouseFollowerVertical->setVisible(!mode); +} + +void ProfileWidget2::setFontPrintScale(double scale) +{ + fontPrintScale = scale; + emit fontPrintScaleChanged(scale); +} + +double ProfileWidget2::getFontPrintScale() +{ + if (printMode) + return fontPrintScale; + else + return 1.0; +} + +void ProfileWidget2::editName() +{ + QAction *action = qobject_cast(sender()); + DiveEventItem *item = static_cast(action->data().value()); + struct event *event = item->getEvent(); + bool ok; + QString newName = QInputDialog::getText(MainWindow::instance(), tr("Edit name of bookmark"), + tr("Custom name:"), QLineEdit::Normal, + event->name, &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; + } + // order is important! first update the current dive (by matching the unchanged event), + // then update the displayed dive (as event is part of the events on displayed dive + // and will be freed as part of changing the name! + update_event_name(current_dive, event, newName.toUtf8().data()); + update_event_name(&displayed_dive, event, newName.toUtf8().data()); + mark_divelist_changed(true); + replot(); + } +} + +void ProfileWidget2::disconnectTemporaryConnections() +{ + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + disconnect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + disconnect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + + disconnect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + disconnect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + + Q_FOREACH (QAction *action, actionsForKeys.values()) { + action->setShortcut(QKeySequence()); + action->setShortcutContext(Qt::WidgetShortcut); + } +} + +void ProfileWidget2::pointInserted(const QModelIndex &parent, int start, int end) +{ + DiveHandler *item = new DiveHandler(); + scene()->addItem(item); + handles << item; + + connect(item, SIGNAL(moved()), this, SLOT(recreatePlannedDive())); + connect(item, SIGNAL(clicked()), this, SLOT(divePlannerHandlerClicked())); + connect(item, SIGNAL(released()), this, SLOT(divePlannerHandlerReleased())); + QGraphicsSimpleTextItem *gasChooseBtn = new QGraphicsSimpleTextItem(); + scene()->addItem(gasChooseBtn); + gasChooseBtn->setZValue(10); + gasChooseBtn->setFlag(QGraphicsItem::ItemIgnoresTransformations); + gases << gasChooseBtn; + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + if (plannerModel->recalcQ()) + replot(); +} + +void ProfileWidget2::pointsRemoved(const QModelIndex &, int start, int end) +{ // start and end are inclusive. + int num = (end - start) + 1; + for (int i = num; i != 0; i--) { + delete handles.back(); + handles.pop_back(); + delete gases.back(); + gases.pop_back(); + } + scene()->clearSelection(); + replot(); +} + +void ProfileWidget2::repositionDiveHandlers() +{ + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + // Re-position the user generated dive handlers + struct gasmix mix, lastmix; + 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.at(i); + h->setVisible(datapoint.entered); + h->setPos(timeAxis->posAtValue(datapoint.time), profileYAxis->posAtValue(datapoint.depth)); + 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(timeAxis->posAtValue(datapoint.depth / prefs.descrate), + profileYAxis->posAtValue(datapoint.depth)); + else + // place the text on the straight line from the origin to the first position + p1 = QPointF(timeAxis->posAtValue(0), 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); + gases[i]->setText(get_divepoint_gas_string(datapoint)); + gases[i]->setVisible(datapoint.entered && + (i == 0 || gases[i]->text() != gases[i-1]->text())); + } +} + +int ProfileWidget2::fixHandlerIndex(DiveHandler *activeHandler) +{ + int index = handles.indexOf(activeHandler); + if (index > 0 && index < handles.count() - 1) { + DiveHandler *before = handles[index - 1]; + if (before->pos().x() > activeHandler->pos().x()) { + handles.swap(index, index - 1); + return index - 1; + } + DiveHandler *after = handles[index + 1]; + if (after->pos().x() < activeHandler->pos().x()) { + handles.swap(index, index + 1); + return index + 1; + } + } + return index; +} + +void ProfileWidget2::recreatePlannedDive() +{ + DiveHandler *activeHandler = qobject_cast(sender()); + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + int index = fixHandlerIndex(activeHandler); + int mintime = 0, maxtime = (timeAxis->maximum() + 10) * 60; + if (index > 0) + mintime = plannerModel->at(index - 1).time; + if (index < plannerModel->size() - 1) + maxtime = plannerModel->at(index + 1).time; + + int minutes = rint(timeAxis->valueAt(activeHandler->pos()) / 60); + if (minutes * 60 <= mintime || minutes * 60 >= maxtime) + return; + + divedatapoint data = plannerModel->at(index); + data.depth = rint(profileYAxis->valueAt(activeHandler->pos()) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); + data.time = rint(timeAxis->valueAt(activeHandler->pos())); + + plannerModel->editStop(index, data); +} + +void ProfileWidget2::keyDownAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + if (dp.depth >= profileYAxis->maximum()) + continue; + + dp.depth += M_OR_FT(1, 5); + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyUpAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + + if (dp.depth <= 0) + continue; + + dp.depth -= M_OR_FT(1, 5); + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyLeftAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + + if (dp.time / 60 <= 0) + continue; + + // don't overlap positions. + // maybe this is a good place for a 'goto'? + double xpos = timeAxis->posAtValue((dp.time - 60) / 60); + bool nextStep = false; + Q_FOREACH (DiveHandler *h, handles) { + if (IS_FP_SAME(h->pos().x(), xpos)) { + nextStep = true; + break; + } + } + if (nextStep) + continue; + + dp.time -= 60; + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyRightAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + if (dp.time / 60.0 >= timeAxis->maximum()) + continue; + + // don't overlap positions. + // maybe this is a good place for a 'goto'? + double xpos = timeAxis->posAtValue((dp.time + 60) / 60); + bool nextStep = false; + Q_FOREACH (DiveHandler *h, handles) { + if (IS_FP_SAME(h->pos().x(), xpos)) { + nextStep = true; + break; + } + } + if (nextStep) + continue; + + dp.time += 60; + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyDeleteAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + int selCount = scene()->selectedItems().count(); + if (selCount) { + QVector selectedIndexes; + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast(i)) { + selectedIndexes.push_back(handles.indexOf(handler)); + handler->hide(); + } + } + plannerModel->removeSelectedPoints(selectedIndexes); + } +} + +void ProfileWidget2::keyEscAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + if (scene()->selectedItems().count()) { + scene()->clearSelection(); + return; + } + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + if (plannerModel->isPlanner()) + plannerModel->cancelPlan(); +} + +void ProfileWidget2::plotPictures() +{ + Q_FOREACH (DivePictureItem *item, pictures) { + item->hide(); + item->deleteLater(); + } + pictures.clear(); + + if (printMode) + return; + + double x, y, lastX = -1.0, lastY = -1.0; + DivePictureModel *m = DivePictureModel::instance(); + for (int i = 0; i < m->rowCount(); i++) { + int offsetSeconds = m->index(i, 1).data(Qt::UserRole).value(); + // it's a correct picture, but doesn't have a timestamp: only show on the widget near the + // information area. + if (!offsetSeconds) + continue; + DivePictureItem *item = new DivePictureItem(); + item->setPixmap(m->index(i, 0).data(Qt::DecorationRole).value()); + item->setFileUrl(m->index(i, 1).data().toString()); + // 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. + x = timeAxis->posAtValue(offsetSeconds); + if (i == 0) + y = 10; + else if (fabs(x - lastX) < 4) + y = lastY + 3; + else + y = 10; + lastX = x; + lastY = y; + item->setPos(x, y); + scene()->addItem(item); + pictures.push_back(item); + } +} diff --git a/desktop-widgets/profile/profilewidget2.h b/desktop-widgets/profile/profilewidget2.h new file mode 100644 index 000000000..f11ec5be1 --- /dev/null +++ b/desktop-widgets/profile/profilewidget2.h @@ -0,0 +1,211 @@ +#ifndef PROFILEWIDGET2_H +#define PROFILEWIDGET2_H + +#include + +// /* The idea of this widget is to display and edit the profile. +// * It has: +// * 1 - ToolTip / Legend item, displays every information of the current mouse position on it, plus the legends of the maps. +// * 2 - ToolBox, displays the QActions that are used to do special stuff on the profile ( like activating the plugins. ) +// * 3 - Cartesian Axis for depth ( y ) +// * 4 - Cartesian Axis for Gases ( y ) +// * 5 - Cartesian Axis for Time ( x ) +// * +// * It needs to be dynamic, things should *flow* on it, not just appear / disappear. +// */ +#include "divelineitem.h" +#include "diveprofileitem.h" +#include "display.h" + +class RulerItem2; +struct dive; +struct plot_info; +class ToolTipItem; +class DiveMeanDepth; +class DiveReportedCeiling; +class DiveTextItem; +class TemperatureAxis; +class DiveEventItem; +class DivePlotDataModel; +class DivePixmapItem; +class DiveRectItem; +class DepthAxis; +class DiveCartesianAxis; +class DiveProfileItem; +class TimeAxis; +class DiveTemperatureItem; +class DiveHeartrateItem; +class PercentageItem; +class DiveGasPressureItem; +class DiveCalculatedCeiling; +class DiveCalculatedTissue; +class PartialPressureGasItem; +class PartialGasPressureAxis; +class AbstractProfilePolygonItem; +class TankItem; +class DiveHandler; +class QGraphicsSimpleTextItem; +class QModelIndex; +class DivePictureItem; + +class ProfileWidget2 : public QGraphicsView { + Q_OBJECT +public: + enum State { + EMPTY, + PROFILE, + EDIT, + ADD, + PLAN, + INVALID + }; + enum Items { + BACKGROUND, + PROFILE_Y_AXIS, + GAS_Y_AXIS, + TIME_AXIS, + DEPTH_CONTROLLER, + TIME_CONTROLLER, + COLUMNS + }; + + ProfileWidget2(QWidget *parent = 0); + void resetZoom(); + void plotDive(struct dive *d = 0, bool force = false); + virtual bool eventFilter(QObject *, QEvent *); + void setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis, DivePlotDataModel *model, int vData, int hData, int zValue); + void setPrintMode(bool mode, bool grayscale = false); + bool getPrintMode(); + bool isPointOutOfBoundaries(const QPointF &point) const; + bool isPlanner(); + bool isAddOrPlanner(); + double getFontPrintScale(); + void setFontPrintScale(double scale); + void clearHandlers(); + void recalcCeiling(); + void setToolTipVisibile(bool visible); + State currentState; + +signals: + void fontPrintScaleChanged(double scale); + +public +slots: // Necessary to call from QAction's signals. + void settingsChanged(); + void setEmptyState(); + void setProfileState(); + void setPlanState(); + void setAddState(); + void changeGas(); + void addSetpointChange(); + void addBookmark(); + void hideEvents(); + void unhideEvents(); + void removeEvent(); + void editName(); + void makeFirstDC(); + void deleteCurrentDC(); + void pointInserted(const QModelIndex &parent, int start, int end); + void pointsRemoved(const QModelIndex &, int start, int end); + void plotPictures(); + void setReplot(bool state); + void replot(dive *d = 0); + + /* this is called for every move on the handlers. maybe we can speed up this a bit? */ + void recreatePlannedDive(); + + /* key press handlers */ + void keyEscAction(); + void keyDeleteAction(); + void keyUpAction(); + void keyDownAction(); + void keyLeftAction(); + void keyRightAction(); + + void divePlannerHandlerClicked(); + void divePlannerHandlerReleased(); + +protected: + virtual ~ProfileWidget2(); + virtual void resizeEvent(QResizeEvent *event); + virtual void wheelEvent(QWheelEvent *event); + virtual void mouseMoveEvent(QMouseEvent *event); + virtual void contextMenuEvent(QContextMenuEvent *event); + virtual void mouseDoubleClickEvent(QMouseEvent *event); + virtual void mousePressEvent(QMouseEvent *event); + virtual void mouseReleaseEvent(QMouseEvent *event); + +private: /*methods*/ + void fixBackgroundPos(); + void scrollViewTo(const QPoint &pos); + void setupSceneAndFlags(); + void setupItemSizes(); + void addItemsToScene(); + void setupItemOnScene(); + void disconnectTemporaryConnections(); + struct plot_data *getEntryFromPos(QPointF pos); + +private: + DivePlotDataModel *dataModel; + int zoomLevel; + qreal zoomFactor; + DivePixmapItem *background; + QString backgroundFile; + ToolTipItem *toolTipItem; + bool isPlotZoomed; + bool replotEnabled; + // All those here should probably be merged into one structure, + // So it's esyer to replicate for more dives later. + // In the meantime, keep it here. + struct plot_info plotInfo; + DepthAxis *profileYAxis; + PartialGasPressureAxis *gasYAxis; + TemperatureAxis *temperatureAxis; + TimeAxis *timeAxis; + DiveProfileItem *diveProfileItem; + DiveTemperatureItem *temperatureItem; + DiveMeanDepthItem *meanDepthItem; + DiveCartesianAxis *cylinderPressureAxis; + DiveGasPressureItem *gasPressureItem; + QList eventItems; + DiveTextItem *diveComputerText; + DiveCalculatedCeiling *diveCeiling; + DiveTextItem *gradientFactor; + QList allTissues; + DiveReportedCeiling *reportedCeiling; + PartialPressureGasItem *pn2GasItem; + PartialPressureGasItem *pheGasItem; + PartialPressureGasItem *po2GasItem; + PartialPressureGasItem *o2SetpointGasItem; + PartialPressureGasItem *ccrsensor1GasItem; + PartialPressureGasItem *ccrsensor2GasItem; + PartialPressureGasItem *ccrsensor3GasItem; + DiveCartesianAxis *heartBeatAxis; + DiveHeartrateItem *heartBeatItem; + DiveCartesianAxis *percentageAxis; + QList allPercentages; + DiveAmbPressureItem *ambPressureItem; + DiveGFLineItem *gflineItem; + DiveLineItem *mouseFollowerVertical; + DiveLineItem *mouseFollowerHorizontal; + RulerItem2 *rulerItem; + TankItem *tankItem; + bool isGrayscale; + bool printMode; + + //specifics for ADD and PLAN + QList handles; + QList gases; + QList pictures; + void repositionDiveHandlers(); + int fixHandlerIndex(DiveHandler *activeHandler); + friend class DiveHandler; + QHash actionsForKeys; + bool shouldCalculateMaxTime; + bool shouldCalculateMaxDepth; + int maxtime; + int maxdepth; + double fontPrintScale; +}; + +#endif // PROFILEWIDGET2_H diff --git a/desktop-widgets/profile/ruleritem.cpp b/desktop-widgets/profile/ruleritem.cpp new file mode 100644 index 000000000..830985552 --- /dev/null +++ b/desktop-widgets/profile/ruleritem.cpp @@ -0,0 +1,179 @@ +#include "ruleritem.h" +#include "preferences.h" +#include "mainwindow.h" +#include "profilewidget2.h" +#include "display.h" + +#include + +#include "profile.h" + +RulerNodeItem2::RulerNodeItem2() : + entry(NULL), + ruler(NULL), + timeAxis(NULL), + depthAxis(NULL) +{ + memset(&pInfo, 0, sizeof(pInfo)); + setRect(-8, -8, 16, 16); + setBrush(QColor(0xff, 0, 0, 127)); + setPen(QColor(Qt::red)); + setFlag(ItemIsMovable); + setFlag(ItemSendsGeometryChanges); + setFlag(ItemIgnoresTransformations); +} + +void RulerNodeItem2::setPlotInfo(plot_info &info) +{ + pInfo = info; + entry = pInfo.entry; +} + +void RulerNodeItem2::setRuler(RulerItem2 *r) +{ + ruler = r; +} + +void RulerNodeItem2::recalculate() +{ + struct plot_data *data = pInfo.entry + (pInfo.nr - 1); + uint16_t count = 0; + if (x() < 0) { + setPos(0, y()); + } else if (x() > timeAxis->posAtValue(data->sec)) { + setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); + } else { + data = pInfo.entry; + count = 0; + while (timeAxis->posAtValue(data->sec) < x() && count < pInfo.nr) { + data = pInfo.entry + count; + count++; + } + setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); + entry = data; + } +} + +void RulerNodeItem2::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + qreal x = event->scenePos().x(); + if (x < 0.0) + x = 0.0; + setPos(x, event->scenePos().y()); + recalculate(); + ruler->recalculate(); +} + +RulerItem2::RulerItem2() : source(new RulerNodeItem2()), + dest(new RulerNodeItem2()), + timeAxis(NULL), + depthAxis(NULL), + textItemBack(new QGraphicsRectItem(this)), + textItem(new QGraphicsSimpleTextItem(this)) +{ + memset(&pInfo, 0, sizeof(pInfo)); + source->setRuler(this); + dest->setRuler(this); + textItem->setFlag(QGraphicsItem::ItemIgnoresTransformations); + textItemBack->setBrush(QColor(0xff, 0xff, 0xff, 190)); + textItemBack->setPen(QColor(Qt::white)); + textItemBack->setFlag(QGraphicsItem::ItemIgnoresTransformations); + setPen(QPen(QColor(Qt::black), 0.0)); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void RulerItem2::settingsChanged() +{ + ProfileWidget2 *profWidget = NULL; + if (scene() && scene()->views().count()) + profWidget = qobject_cast(scene()->views().first()); + + if (profWidget && profWidget->currentState == ProfileWidget2::PROFILE) + setVisible(prefs.rulergraph); + else + setVisible(false); +} + +void RulerItem2::recalculate() +{ + char buffer[500]; + QPointF tmp; + QFont font; + QFontMetrics fm(font); + + if (timeAxis == NULL || depthAxis == NULL || pInfo.nr == 0) + return; + + prepareGeometryChange(); + startPoint = mapFromItem(source, 0, 0); + endPoint = mapFromItem(dest, 0, 0); + + if (startPoint.x() > endPoint.x()) { + tmp = endPoint; + endPoint = startPoint; + startPoint = tmp; + } + QLineF line(startPoint, endPoint); + setLine(line); + compare_samples(source->entry, dest->entry, buffer, 500, 1); + text = QString(buffer); + + // draw text + QGraphicsView *view = scene()->views().first(); + QPoint begin = view->mapFromScene(mapToScene(startPoint)); + textItem->setText(text); + qreal tgtX = startPoint.x(); + const qreal diff = begin.x() + textItem->boundingRect().width(); + // clamp so that the text doesn't go out of the screen to the right + if (diff > view->width()) { + begin.setX(begin.x() - (diff - view->width())); + tgtX = mapFromScene(view->mapToScene(begin)).x(); + } + // always show the text bellow the lowest of the start and end points + qreal tgtY = (startPoint.y() >= endPoint.y()) ? startPoint.y() : endPoint.y(); + // this isn't exactly optimal, since we want to scale the 1.0, 4.0 distances as well + textItem->setPos(tgtX - 1.0, tgtY + 4.0); + + // setup the text background + textItemBack->setVisible(startPoint.x() != endPoint.x()); + textItemBack->setPos(textItem->x(), textItem->y()); + textItemBack->setRect(0, 0, textItem->boundingRect().width(), textItem->boundingRect().height()); +} + +RulerNodeItem2 *RulerItem2::sourceNode() const +{ + return source; +} + +RulerNodeItem2 *RulerItem2::destNode() const +{ + return dest; +} + +void RulerItem2::setPlotInfo(plot_info info) +{ + pInfo = info; + dest->setPlotInfo(info); + source->setPlotInfo(info); + dest->recalculate(); + source->recalculate(); + recalculate(); +} + +void RulerItem2::setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth) +{ + timeAxis = time; + depthAxis = depth; + dest->depthAxis = depth; + dest->timeAxis = time; + source->depthAxis = depth; + source->timeAxis = time; + recalculate(); +} + +void RulerItem2::setVisible(bool visible) +{ + QGraphicsLineItem::setVisible(visible); + source->setVisible(visible); + dest->setVisible(visible); +} diff --git a/desktop-widgets/profile/ruleritem.h b/desktop-widgets/profile/ruleritem.h new file mode 100644 index 000000000..4fad0451c --- /dev/null +++ b/desktop-widgets/profile/ruleritem.h @@ -0,0 +1,59 @@ +#ifndef RULERITEM_H +#define RULERITEM_H + +#include +#include +#include +#include "divecartesianaxis.h" +#include "display.h" + +struct plot_data; +class RulerItem2; + +class RulerNodeItem2 : public QObject, public QGraphicsEllipseItem { + Q_OBJECT + friend class RulerItem2; + +public: + explicit RulerNodeItem2(); + void setRuler(RulerItem2 *r); + void setPlotInfo(struct plot_info &info); + void recalculate(); + +protected: + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); +private: + struct plot_info pInfo; + struct plot_data *entry; + RulerItem2 *ruler; + DiveCartesianAxis *timeAxis; + DiveCartesianAxis *depthAxis; +}; + +class RulerItem2 : public QObject, public QGraphicsLineItem { + Q_OBJECT +public: + explicit RulerItem2(); + void recalculate(); + + void setPlotInfo(struct plot_info pInfo); + RulerNodeItem2 *sourceNode() const; + RulerNodeItem2 *destNode() const; + void setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth); + void setVisible(bool visible); + +public +slots: + void settingsChanged(); + +private: + struct plot_info pInfo; + QPointF startPoint, endPoint; + RulerNodeItem2 *source, *dest; + QString text; + DiveCartesianAxis *timeAxis; + DiveCartesianAxis *depthAxis; + QGraphicsRectItem *textItemBack; + QGraphicsSimpleTextItem *textItem; +}; +#endif diff --git a/desktop-widgets/profile/tankitem.cpp b/desktop-widgets/profile/tankitem.cpp new file mode 100644 index 000000000..c0e75a371 --- /dev/null +++ b/desktop-widgets/profile/tankitem.cpp @@ -0,0 +1,120 @@ +#include "tankitem.h" +#include "diveplotdatamodel.h" +#include "divetextitem.h" +#include "profile.h" +#include + +TankItem::TankItem(QObject *parent) : + QGraphicsRectItem(), + dataModel(0), + pInfoEntry(0), + pInfoNr(0) +{ + height = 3; + QColor red(PERSIANRED1); + QColor blue(AIR_BLUE); + QColor yellow(NITROX_YELLOW); + QColor green(NITROX_GREEN); + QLinearGradient nitroxGradient(QPointF(0, 0), QPointF(0, height)); + nitroxGradient.setColorAt(0.0, green); + nitroxGradient.setColorAt(0.49, green); + nitroxGradient.setColorAt(0.5, yellow); + nitroxGradient.setColorAt(1.0, yellow); + nitrox = nitroxGradient; + oxygen = green; + QLinearGradient trimixGradient(QPointF(0, 0), QPointF(0, height)); + trimixGradient.setColorAt(0.0, green); + trimixGradient.setColorAt(0.49, green); + trimixGradient.setColorAt(0.5, red); + trimixGradient.setColorAt(1.0, red); + trimix = trimixGradient; + air = blue; + memset(&diveCylinderStore, 0, sizeof(diveCylinderStore)); +} + +TankItem::~TankItem() +{ + // Should this be clear_dive(diveCylinderStore)? + for (int i = 0; i < MAX_CYLINDERS; i++) + free((void *)diveCylinderStore.cylinder[i].type.description); +} + +void TankItem::setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d) +{ + free(pInfoEntry); + // the plotInfo and dive structures passed in could become invalid before we stop using them, + // so copy the data that we need + int size = plotInfo->nr * sizeof(plotInfo->entry[0]); + pInfoEntry = (struct plot_data *)malloc(size); + pInfoNr = plotInfo->nr; + memcpy(pInfoEntry, plotInfo->entry, size); + copy_cylinders(d, &diveCylinderStore, false); + dataModel = model; + connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex)), Qt::UniqueConnection); + modelDataChanged(); +} + +void TankItem::createBar(qreal x, qreal w, struct gasmix *gas) +{ + // pick the right gradient, size, position and text + QGraphicsRectItem *rect = new QGraphicsRectItem(x, 0, w, height, this); + if (gasmix_is_air(gas)) + rect->setBrush(air); + else if (gas->he.permille) + rect->setBrush(trimix); + else if (gas->o2.permille == 1000) + rect->setBrush(oxygen); + else + rect->setBrush(nitrox); + rect->setPen(QPen(QBrush(), 0.0)); // get rid of the thick line around the rectangle + rects.push_back(rect); + DiveTextItem *label = new DiveTextItem(rect); + label->setText(gasname(gas)); + label->setBrush(Qt::black); + label->setPos(x + 1, 0); + label->setAlignment(Qt::AlignBottom | Qt::AlignRight); + label->setZValue(101); +} + +void TankItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + + if (!dataModel || !pInfoEntry || !pInfoNr) + return; + + // remove the old rectangles + foreach (QGraphicsRectItem *r, rects) { + delete(r); + } + rects.clear(); + + // walk the list and figure out which tanks go where + struct plot_data *entry = pInfoEntry; + int cylIdx = entry->cylinderindex; + int i = -1; + int startTime = 0; + struct gasmix *gas = &diveCylinderStore.cylinder[cylIdx].gasmix; + qreal width, left; + while (++i < pInfoNr) { + entry = &pInfoEntry[i]; + if (entry->cylinderindex == cylIdx) + continue; + width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); + left = hAxis->posAtValue(startTime); + createBar(left, width, gas); + cylIdx = entry->cylinderindex; + gas = &diveCylinderStore.cylinder[cylIdx].gasmix; + startTime = entry->sec; + } + width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); + left = hAxis->posAtValue(startTime); + createBar(left, width, gas); +} + +void TankItem::setHorizontalAxis(DiveCartesianAxis *horizontal) +{ + hAxis = horizontal; + connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} diff --git a/desktop-widgets/profile/tankitem.h b/desktop-widgets/profile/tankitem.h new file mode 100644 index 000000000..fd685fc82 --- /dev/null +++ b/desktop-widgets/profile/tankitem.h @@ -0,0 +1,39 @@ +#ifndef TANKITEM_H +#define TANKITEM_H + +#include +#include +#include +#include "divelineitem.h" +#include "divecartesianaxis.h" +#include "dive.h" + +class TankItem : public QObject, public QGraphicsRectItem +{ + Q_OBJECT + +public: + explicit TankItem(QObject *parent = 0); + ~TankItem(); + void setHorizontalAxis(DiveCartesianAxis *horizontal); + void setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d); + +signals: + +public slots: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + +private: + void createBar(qreal x, qreal w, struct gasmix *gas); + DivePlotDataModel *dataModel; + DiveCartesianAxis *hAxis; + int hDataColumn; + struct dive diveCylinderStore; + struct plot_data *pInfoEntry; + int pInfoNr; + qreal height; + QBrush air, nitrox, oxygen, trimix; + QList rects; +}; + +#endif // TANKITEM_H diff --git a/desktop-widgets/qtwaitingspinner.cpp b/desktop-widgets/qtwaitingspinner.cpp new file mode 100644 index 000000000..14e8669b0 --- /dev/null +++ b/desktop-widgets/qtwaitingspinner.cpp @@ -0,0 +1,288 @@ + +/* Original Work Copyright (c) 2012-2014 Alexander Turkin + Modified 2014 by William Hallatt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#include +#include + +#include +#include + +#include "qtwaitingspinner.h" + +/*----------------------------------------------------------------------------*/ + +// Defaults +const QColor c_color(Qt::black); +const qreal c_roundness(70.0); +const qreal c_minTrailOpacity(15.0); +const qreal c_trailFadePercentage(70.0); +const int c_lines(12); +const int c_lineLength(10); +const int c_lineWidth(5); +const int c_innerRadius(10); +const int c_revPerSec(1); + +/*----------------------------------------------------------------------------*/ + +QtWaitingSpinner::QtWaitingSpinner(QWidget *parent) + : QWidget(parent), + + // Configurable settings. + m_color(c_color), m_roundness(c_roundness), + m_minTrailOpacity(c_minTrailOpacity), + m_trailFadePercentage(c_trailFadePercentage), m_revPerSec(c_revPerSec), + m_numberOfLines(c_lines), m_lineLength(c_lineLength + c_lineWidth), + m_lineWidth(c_lineWidth), m_innerRadius(c_innerRadius), + + // Other + m_timer(NULL), m_parent(parent), m_centreOnParent(false), + m_currentCounter(0), m_isSpinning(false) { + initialise(); +} + +/*----------------------------------------------------------------------------*/ + +QtWaitingSpinner::QtWaitingSpinner(Qt::WindowModality modality, QWidget *parent, + bool centreOnParent) + : QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint), + + // Configurable settings. + m_color(c_color), m_roundness(c_roundness), + m_minTrailOpacity(c_minTrailOpacity), + m_trailFadePercentage(c_trailFadePercentage), m_revPerSec(c_revPerSec), + m_numberOfLines(c_lines), m_lineLength(c_lineLength + c_lineWidth), + m_lineWidth(c_lineWidth), m_innerRadius(c_innerRadius), + + // Other + m_timer(NULL), m_parent(parent), m_centreOnParent(centreOnParent), + m_currentCounter(0) { + initialise(); + + // We need to set the window modality AFTER we've hidden the + // widget for the first time since changing this property while + // the widget is visible has no effect. + this->setWindowModality(modality); + this->setAttribute(Qt::WA_TranslucentBackground); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::initialise() { + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), this, SLOT(rotate())); + updateSize(); + updateTimer(); + this->hide(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::paintEvent(QPaintEvent * /*ev*/) { + QPainter painter(this); + painter.fillRect(this->rect(), Qt::transparent); + painter.setRenderHint(QPainter::Antialiasing, true); + + if (m_currentCounter >= m_numberOfLines) { + m_currentCounter = 0; + } + painter.setPen(Qt::NoPen); + for (int i = 0; i < m_numberOfLines; ++i) { + painter.save(); + painter.translate(m_innerRadius + m_lineLength, + m_innerRadius + m_lineLength); + qreal rotateAngle = + static_cast(360 * i) / static_cast(m_numberOfLines); + painter.rotate(rotateAngle); + painter.translate(m_innerRadius, 0); + int distance = + lineCountDistanceFromPrimary(i, m_currentCounter, m_numberOfLines); + QColor color = + currentLineColor(distance, m_numberOfLines, m_trailFadePercentage, + m_minTrailOpacity, m_color); + painter.setBrush(color); + // TODO improve the way rounded rect is painted + painter.drawRoundedRect( + QRect(0, -m_lineWidth / 2, m_lineLength, m_lineWidth), m_roundness, + m_roundness, Qt::RelativeSize); + painter.restore(); + } +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::start() { + updatePosition(); + m_isSpinning = true; + this->show(); + if (!m_timer->isActive()) { + m_timer->start(); + m_currentCounter = 0; + } +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::stop() { + m_isSpinning = false; + this->hide(); + if (m_timer->isActive()) { + m_timer->stop(); + m_currentCounter = 0; + } +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setNumberOfLines(int lines) { + m_numberOfLines = lines; + m_currentCounter = 0; + updateTimer(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setLineLength(int length) { + m_lineLength = length; + updateSize(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setLineWidth(int width) { + m_lineWidth = width; + updateSize(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setInnerRadius(int radius) { + m_innerRadius = radius; + updateSize(); +} + +/*----------------------------------------------------------------------------*/ + +bool QtWaitingSpinner::isSpinning() const { return m_isSpinning; } + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setRoundness(qreal roundness) { + m_roundness = std::max(0.0, std::min(100.0, roundness)); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setColor(QColor color) { m_color = color; } + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setRevolutionsPerSecond(int rps) { + m_revPerSec = rps; + updateTimer(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setTrailFadePercentage(qreal trail) { + m_trailFadePercentage = trail; +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setMinimumTrailOpacity(qreal minOpacity) { + m_minTrailOpacity = minOpacity; +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::rotate() { + ++m_currentCounter; + if (m_currentCounter >= m_numberOfLines) { + m_currentCounter = 0; + } + update(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::updateSize() { + int size = (m_innerRadius + m_lineLength) * 2; + setFixedSize(size, size); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::updateTimer() { + m_timer->setInterval(calculateTimerInterval(m_numberOfLines, m_revPerSec)); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::updatePosition() { + if (m_parent && m_centreOnParent) { + this->move(m_parent->frameGeometry().topLeft() + m_parent->rect().center() - + this->rect().center()); + } +} + +/*----------------------------------------------------------------------------*/ + +int QtWaitingSpinner::calculateTimerInterval(int lines, int speed) { + return 1000 / (lines * speed); +} + +/*----------------------------------------------------------------------------*/ + +int QtWaitingSpinner::lineCountDistanceFromPrimary(int current, int primary, + int totalNrOfLines) { + int distance = primary - current; + if (distance < 0) { + distance += totalNrOfLines; + } + return distance; +} + +/*----------------------------------------------------------------------------*/ + +QColor QtWaitingSpinner::currentLineColor(int countDistance, int totalNrOfLines, + qreal trailFadePerc, qreal minOpacity, + QColor color) { + if (countDistance == 0) { + return color; + } + const qreal minAlphaF = minOpacity / 100.0; + int distanceThreshold = + static_cast(ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)); + if (countDistance > distanceThreshold) { + color.setAlphaF(minAlphaF); + } else { + qreal alphaDiff = color.alphaF() - minAlphaF; + qreal gradient = alphaDiff / static_cast(distanceThreshold + 1); + qreal resultAlpha = color.alphaF() - gradient * countDistance; + + // If alpha is out of bounds, clip it. + resultAlpha = std::min(1.0, std::max(0.0, resultAlpha)); + color.setAlphaF(resultAlpha); + } + return color; +} + +/*----------------------------------------------------------------------------*/ diff --git a/desktop-widgets/qtwaitingspinner.h b/desktop-widgets/qtwaitingspinner.h new file mode 100644 index 000000000..254b52ec7 --- /dev/null +++ b/desktop-widgets/qtwaitingspinner.h @@ -0,0 +1,103 @@ +/* Original Work Copyright (c) 2012-2014 Alexander Turkin + Modified 2014 by William Hallatt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#ifndef QTWAITINGSPINNER_H +#define QTWAITINGSPINNER_H + +#include + +#include +#include + +class QtWaitingSpinner : public QWidget { + Q_OBJECT +public: + /*! Constructor for "standard" widget behaviour - use this + * constructor if you wish to, e.g. embed your widget in another. */ + QtWaitingSpinner(QWidget *parent = 0); + + /*! Constructor - use this constructor to automatically create a modal + * ("blocking") spinner on top of the calling widget/window. If a valid + * parent widget is provided, "centreOnParent" will ensure that + * QtWaitingSpinner automatically centres itself on it, if not, + * "centreOnParent" is ignored. */ + QtWaitingSpinner(Qt::WindowModality modality, QWidget *parent = 0, + bool centreOnParent = true); + +public Q_SLOTS: + void start(); + void stop(); + +public: + void setColor(QColor color); + void setRoundness(qreal roundness); + void setMinimumTrailOpacity(qreal minOpacity); + void setTrailFadePercentage(qreal trail); + void setRevolutionsPerSecond(int rps); + void setNumberOfLines(int lines); + void setLineLength(int length); + void setLineWidth(int width); + void setInnerRadius(int radius); + + bool isSpinning() const; + +private Q_SLOTS: + void rotate(); + +protected: + void paintEvent(QPaintEvent *ev); + +private: + static int calculateTimerInterval(int lines, int speed); + static int lineCountDistanceFromPrimary(int current, int primary, + int totalNrOfLines); + static QColor currentLineColor(int distance, int totalNrOfLines, + qreal trailFadePerc, qreal minOpacity, + QColor color); + + void initialise(); + void updateSize(); + void updateTimer(); + void updatePosition(); + +private: + // Configurable settings. + QColor m_color; + qreal m_roundness; // 0..100 + qreal m_minTrailOpacity; + qreal m_trailFadePercentage; + int m_revPerSec; // revolutions per second + int m_numberOfLines; + int m_lineLength; + int m_lineWidth; + int m_innerRadius; + +private: + QtWaitingSpinner(const QtWaitingSpinner&); + QtWaitingSpinner& operator=(const QtWaitingSpinner&); + + QTimer *m_timer; + QWidget *m_parent; + bool m_centreOnParent; + int m_currentCounter; + bool m_isSpinning; +}; + +#endif // QTWAITINGSPINNER_H diff --git a/desktop-widgets/renumber.ui b/desktop-widgets/renumber.ui new file mode 100644 index 000000000..00e7b84bb --- /dev/null +++ b/desktop-widgets/renumber.ui @@ -0,0 +1,120 @@ + + + RenumberDialog + + + Qt::WindowModal + + + + 0 + 0 + 211 + 125 + + + + Renumber + + + + :/subsurface-icon + + + + + 1 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + New starting number + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 99999 + + + 1 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + RenumberDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + RenumberDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/searchbar.ui b/desktop-widgets/searchbar.ui new file mode 100644 index 000000000..22bce39c6 --- /dev/null +++ b/desktop-widgets/searchbar.ui @@ -0,0 +1,134 @@ + + + SearchBar + + + + 0 + 0 + 400 + 34 + + + + Form + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + false + + + + 0 + 0 + + + + + + + + + + + + true + + + + + + + false + + + + 0 + 0 + + + + + + + + + + + + true + + + + + + + + 0 + 0 + + + + + + + + + + + + true + + + + + + + + diff --git a/desktop-widgets/setpoint.ui b/desktop-widgets/setpoint.ui new file mode 100644 index 000000000..d96488a31 --- /dev/null +++ b/desktop-widgets/setpoint.ui @@ -0,0 +1,130 @@ + + + SetpointDialog + + + Qt::WindowModal + + + + 0 + 0 + 211 + 125 + + + + Renumber + + + + :/subsurface-icon + + + + + 1 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + New set-point (0 for OC) + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + bar + + + 1 + + + 0.000000000000000 + + + 2.000000000000000 + + + 0.100000000000000 + + + 1.100000000000000 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SetpointDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SetpointDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/shiftimagetimes.ui b/desktop-widgets/shiftimagetimes.ui new file mode 100644 index 000000000..0478a62b4 --- /dev/null +++ b/desktop-widgets/shiftimagetimes.ui @@ -0,0 +1,293 @@ + + + ShiftImageTimesDialog + + + Qt::WindowModal + + + + 0 + 0 + 693 + 606 + + + + + 0 + 0 + + + + Shift selected image times + + + + :/subsurface-icon + + + + + + + Shift times of image(s) by + + + + + + + 2000 + 1 + 1 + + + + + 23 + 59 + 59 + 2010 + 12 + 31 + + + + + 0 + 0 + 0 + 2000 + 1 + 1 + + + + + 2010 + 12 + 31 + + + + + 2000 + 1 + 1 + + + + + + + + + + h:mm + + + Qt::LocalTime + + + + + + + Earlier + + + + + + + Later + + + true + + + + + + + true + + + color: red; + + + Warning! +Not all images have timestamps in the range between +30 minutes before the start and 30 minutes after the end of any selected dive. + + + + + + + Load images even if the time does not match the dive time + + + + + + + color: red; + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Horizontal + + + + + + + + 16777215 + 60 + + + + Qt::LeftToRight + + + To compute the offset between the clocks of your dive computer and your camera use your camera to take a picture of your dive compuer displaying the current time. Download that image to your computer and press this button. + + + true + + + + + + + Determine camera time offset + + + Select image of divecomputer showing time + + + + + + + false + + + + + + + + + + 16777215 + 60 + + + + Which date and time are displayed on the image? + + + true + + + + + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + + + + + + + + Qt::Vertical + + + + 20 + 193 + + + + + + + + + + + + + + + + + + buttonBox + accepted() + ShiftImageTimesDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ShiftImageTimesDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/shifttimes.ui b/desktop-widgets/shifttimes.ui new file mode 100644 index 000000000..486b1f43b --- /dev/null +++ b/desktop-widgets/shifttimes.ui @@ -0,0 +1,214 @@ + + + ShiftTimesDialog + + + Qt::WindowModal + + + + 0 + 0 + 343 + 224 + + + + + 0 + 0 + + + + Shift selected dive times + + + + :/subsurface-icon + + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + Shift times of selected dives by + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + + Shifted time: + + + + + + + Current time: + + + + + + + 0:0 + + + + + + + 0:0 + + + + + + + + + + 2000 + 1 + 1 + + + + + 0 + 0 + 0 + 2000 + 1 + 1 + + + + + 2000 + 1 + 1 + + + + h:mm + + + Qt::LocalTime + + + + + + + Earlier + + + + + + + Later + + + true + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + ShiftTimesDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ShiftTimesDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + timeEdit + timeChanged(const QTime) + ShiftTimesDialog + changeTime() + + + backwards + toggled(bool) + ShiftTimesDialog + changeTime() + + + diff --git a/desktop-widgets/simplewidgets.cpp b/desktop-widgets/simplewidgets.cpp new file mode 100644 index 000000000..62a9cc646 --- /dev/null +++ b/desktop-widgets/simplewidgets.cpp @@ -0,0 +1,736 @@ +#include "simplewidgets.h" +#include "filtermodels.h" + +#include +#include +#include +#include +#include +#include + +#include "file.h" +#include "mainwindow.h" +#include "helpers.h" +#include "libdivecomputer/parser.h" +#include "divelistview.h" +#include "display.h" +#include "profile/profilewidget2.h" +#include "undocommands.h" + +class MinMaxAvgWidgetPrivate { +public: + QLabel *avgIco, *avgValue; + QLabel *minIco, *minValue; + QLabel *maxIco, *maxValue; + + MinMaxAvgWidgetPrivate(MinMaxAvgWidget *owner) + { + avgIco = new QLabel(owner); + avgIco->setPixmap(QIcon(":/average").pixmap(16, 16)); + avgIco->setToolTip(QObject::tr("Average")); + minIco = new QLabel(owner); + minIco->setPixmap(QIcon(":/minimum").pixmap(16, 16)); + minIco->setToolTip(QObject::tr("Minimum")); + maxIco = new QLabel(owner); + maxIco->setPixmap(QIcon(":/maximum").pixmap(16, 16)); + maxIco->setToolTip(QObject::tr("Maximum")); + avgValue = new QLabel(owner); + minValue = new QLabel(owner); + maxValue = new QLabel(owner); + + QGridLayout *formLayout = new QGridLayout(); + formLayout->addWidget(maxIco, 0, 0); + formLayout->addWidget(maxValue, 0, 1); + formLayout->addWidget(avgIco, 1, 0); + formLayout->addWidget(avgValue, 1, 1); + formLayout->addWidget(minIco, 2, 0); + formLayout->addWidget(minValue, 2, 1); + owner->setLayout(formLayout); + } +}; + +double MinMaxAvgWidget::average() const +{ + return d->avgValue->text().toDouble(); +} + +double MinMaxAvgWidget::maximum() const +{ + return d->maxValue->text().toDouble(); +} +double MinMaxAvgWidget::minimum() const +{ + return d->minValue->text().toDouble(); +} + +MinMaxAvgWidget::MinMaxAvgWidget(QWidget *parent) : d(new MinMaxAvgWidgetPrivate(this)) +{ +} + +MinMaxAvgWidget::~MinMaxAvgWidget() +{ +} + +void MinMaxAvgWidget::clear() +{ + d->avgValue->setText(QString()); + d->maxValue->setText(QString()); + d->minValue->setText(QString()); +} + +void MinMaxAvgWidget::setAverage(double average) +{ + d->avgValue->setText(QString::number(average)); +} + +void MinMaxAvgWidget::setMaximum(double maximum) +{ + d->maxValue->setText(QString::number(maximum)); +} +void MinMaxAvgWidget::setMinimum(double minimum) +{ + d->minValue->setText(QString::number(minimum)); +} + +void MinMaxAvgWidget::setAverage(const QString &average) +{ + d->avgValue->setText(average); +} + +void MinMaxAvgWidget::setMaximum(const QString &maximum) +{ + d->maxValue->setText(maximum); +} + +void MinMaxAvgWidget::setMinimum(const QString &minimum) +{ + d->minValue->setText(minimum); +} + +void MinMaxAvgWidget::overrideMinToolTipText(const QString &newTip) +{ + d->minIco->setToolTip(newTip); + d->minValue->setToolTip(newTip); +} + +void MinMaxAvgWidget::overrideAvgToolTipText(const QString &newTip) +{ + d->avgIco->setToolTip(newTip); + d->avgValue->setToolTip(newTip); +} + +void MinMaxAvgWidget::overrideMaxToolTipText(const QString &newTip) +{ + d->maxIco->setToolTip(newTip); + d->maxValue->setToolTip(newTip); +} + +RenumberDialog *RenumberDialog::instance() +{ + static RenumberDialog *self = new RenumberDialog(MainWindow::instance()); + return self; +} + +void RenumberDialog::renumberOnlySelected(bool selected) +{ + if (selected && amount_selected == 1) + ui.groupBox->setTitle(tr("New number")); + else + ui.groupBox->setTitle(tr("New starting number")); + selectedOnly = selected; +} + +void RenumberDialog::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + MainWindow::instance()->dive_list()->rememberSelection(); + // we remember a map from dive uuid to a pair of old number / new number + QMap > renumberedDives; + int i; + int newNr = ui.spinBox->value(); + struct dive *dive = NULL; + for_each_dive (i, dive) { + if (!selectedOnly || dive->selected) + renumberedDives.insert(dive->id, QPair(dive->number, newNr++)); + } + UndoRenumberDives *undoCommand = new UndoRenumberDives(renumberedDives); + MainWindow::instance()->undoStack->push(undoCommand); + + MainWindow::instance()->dive_list()->fixMessyQtModelBehaviour(); + mark_divelist_changed(true); + MainWindow::instance()->dive_list()->restoreSelection(); + } +} + +RenumberDialog::RenumberDialog(QWidget *parent) : QDialog(parent), selectedOnly(false) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +SetpointDialog *SetpointDialog::instance() +{ + static SetpointDialog *self = new SetpointDialog(MainWindow::instance()); + return self; +} + +void SetpointDialog::setpointData(struct divecomputer *divecomputer, int second) +{ + dc = divecomputer; + time = second < 0 ? 0 : second; +} + +void SetpointDialog::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole && dc) + add_event(dc, time, SAMPLE_EVENT_PO2, 0, (int)(1000.0 * ui.spinbox->value()), "SP change"); + mark_divelist_changed(true); + MainWindow::instance()->graphics()->replot(); +} + +SetpointDialog::SetpointDialog(QWidget *parent) : + QDialog(parent), + dc(0) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +ShiftTimesDialog *ShiftTimesDialog::instance() +{ + static ShiftTimesDialog *self = new ShiftTimesDialog(MainWindow::instance()); + return self; +} + +void ShiftTimesDialog::buttonClicked(QAbstractButton *button) +{ + int amount; + + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + amount *= -1; + if (amount != 0) { + // DANGER, DANGER - this could get our dive_table unsorted... + int i; + struct dive *dive; + QList affectedDives; + for_each_dive (i, dive) { + if (!dive->selected) + continue; + + affectedDives.append(dive->id); + } + MainWindow::instance()->undoStack->push(new UndoShiftTime(affectedDives, amount)); + sort_table(&dive_table); + mark_divelist_changed(true); + MainWindow::instance()->dive_list()->rememberSelection(); + MainWindow::instance()->refreshDisplay(); + MainWindow::instance()->dive_list()->restoreSelection(); + } + } +} + +void ShiftTimesDialog::showEvent(QShowEvent *event) +{ + ui.timeEdit->setTime(QTime(0, 0, 0, 0)); + when = get_times(); //get time of first selected dive + ui.currentTime->setText(get_dive_date_string(when)); + ui.shiftedTime->setText(get_dive_date_string(when)); +} + +void ShiftTimesDialog::changeTime() +{ + int amount; + + amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + amount *= -1; + + ui.shiftedTime->setText(get_dive_date_string(amount + when)); +} + +ShiftTimesDialog::ShiftTimesDialog(QWidget *parent) : + QDialog(parent), + when(0) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.timeEdit, SIGNAL(timeChanged(const QTime)), this, SLOT(changeTime())); + connect(ui.backwards, SIGNAL(toggled(bool)), this, SLOT(changeTime())); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void ShiftImageTimesDialog::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + m_amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + m_amount *= -1; + } +} + +void ShiftImageTimesDialog::syncCameraClicked() +{ + QPixmap picture; + QDateTime dcDateTime = QDateTime(); + QStringList fileNames = QFileDialog::getOpenFileNames(this, + tr("Open image file"), + DiveListView::lastUsedImageDir(), + tr("Image files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); + if (fileNames.isEmpty()) + return; + + picture.load(fileNames.at(0)); + ui.displayDC->setEnabled(true); + QGraphicsScene *scene = new QGraphicsScene(this); + + scene->addPixmap(picture.scaled(ui.DCImage->size())); + ui.DCImage->setScene(scene); + + dcImageEpoch = picture_get_timestamp(fileNames.at(0).toUtf8().data()); + dcDateTime.setTime_t(dcImageEpoch - gettimezoneoffset(displayed_dive.when)); + ui.dcTime->setDateTime(dcDateTime); + connect(ui.dcTime, SIGNAL(dateTimeChanged(const QDateTime &)), this, SLOT(dcDateTimeChanged(const QDateTime &))); +} + +void ShiftImageTimesDialog::dcDateTimeChanged(const QDateTime &newDateTime) +{ + QDateTime newtime(newDateTime); + if (!dcImageEpoch) + return; + newtime.setTimeSpec(Qt::UTC); + setOffset(newtime.toTime_t() - dcImageEpoch); +} + +void ShiftImageTimesDialog::matchAllImagesToggled(bool state) +{ + matchAllImages = state; +} + +bool ShiftImageTimesDialog::matchAll() +{ + return matchAllImages; +} + +ShiftImageTimesDialog::ShiftImageTimesDialog(QWidget *parent, QStringList fileNames) : + QDialog(parent), + fileNames(fileNames), + m_amount(0), + matchAllImages(false) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.syncCamera, SIGNAL(clicked()), this, SLOT(syncCameraClicked())); + connect(ui.timeEdit, SIGNAL(timeChanged(const QTime &)), this, SLOT(timeEditChanged(const QTime &))); + connect(ui.matchAllImages, SIGNAL(toggled(bool)), this, SLOT(matchAllImagesToggled(bool))); + dcImageEpoch = (time_t)0; +} + +time_t ShiftImageTimesDialog::amount() const +{ + return m_amount; +} + +void ShiftImageTimesDialog::setOffset(time_t offset) +{ + if (offset >= 0) { + ui.forward->setChecked(true); + } else { + ui.backwards->setChecked(true); + offset *= -1; + } + ui.timeEdit->setTime(QTime(offset / 3600, (offset % 3600) / 60, offset % 60)); +} + +void ShiftImageTimesDialog::updateInvalid() +{ + timestamp_t timestamp; + QDateTime time; + bool allValid = true; + ui.warningLabel->hide(); + ui.invalidLabel->hide(); + time.setTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + ui.invalidLabel->setText("Dive:" + time.toString() + "\n"); + + Q_FOREACH (const QString &fileName, fileNames) { + if (picture_check_valid(fileName.toUtf8().data(), m_amount)) + continue; + + // We've found invalid image + timestamp = picture_get_timestamp(fileName.toUtf8().data()); + time.setTime_t(timestamp + m_amount - gettimezoneoffset(displayed_dive.when)); + ui.invalidLabel->setText(ui.invalidLabel->text() + fileName + " " + time.toString() + "\n"); + allValid = false; + } + + if (!allValid){ + ui.warningLabel->show(); + ui.invalidLabel->show(); + } +} + +void ShiftImageTimesDialog::timeEditChanged(const QTime &time) +{ + m_amount = time.hour() * 3600 + time.minute() * 60; + if (ui.backwards->isChecked()) + m_amount *= -1; + updateInvalid(); +} + +URLDialog::URLDialog(QWidget *parent) : QDialog(parent) +{ + ui.setupUi(this); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +QString URLDialog::url() const +{ + return ui.urlField->toPlainText(); +} + +bool isGnome3Session() +{ +#if defined(QT_OS_WIW) || defined(QT_OS_MAC) + return false; +#else + if (qApp->style()->objectName() != "gtk+") + return false; + QProcess p; + p.start("pidof", QStringList() << "gnome-shell"); + p.waitForFinished(-1); + QString p_stdout = p.readAllStandardOutput(); + return !p_stdout.isEmpty(); +#endif +} + +DateWidget::DateWidget(QWidget *parent) : QWidget(parent), + calendarWidget(new QCalendarWidget()) +{ + setDate(QDate::currentDate()); + setMinimumSize(QSize(80, 64)); + setFocusPolicy(Qt::StrongFocus); + calendarWidget->setWindowFlags(Qt::FramelessWindowHint); + calendarWidget->setFirstDayOfWeek(getLocale().firstDayOfWeek()); + calendarWidget->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader); + + connect(calendarWidget, SIGNAL(activated(QDate)), calendarWidget, SLOT(hide())); + connect(calendarWidget, SIGNAL(clicked(QDate)), calendarWidget, SLOT(hide())); + connect(calendarWidget, SIGNAL(activated(QDate)), this, SLOT(setDate(QDate))); + connect(calendarWidget, SIGNAL(clicked(QDate)), this, SLOT(setDate(QDate))); + calendarWidget->installEventFilter(this); +} + +bool DateWidget::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::FocusOut) { + calendarWidget->hide(); + return true; + } + if (event->type() == QEvent::KeyPress) { + QKeyEvent *ev = static_cast(event); + if (ev->key() == Qt::Key_Escape) { + calendarWidget->hide(); + } + } + return QObject::eventFilter(object, event); +} + + +void DateWidget::setDate(const QDate &date) +{ + mDate = date; + update(); + emit dateChanged(mDate); +} + +QDate DateWidget::date() const +{ + return mDate; +} + +void DateWidget::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::EnabledChange) { + update(); + } +} + +#define DATEWIDGETWIDTH 80 +void DateWidget::paintEvent(QPaintEvent *event) +{ + static QPixmap pix = QPixmap(":/calendar").scaled(DATEWIDGETWIDTH, 64); + + QPainter painter(this); + + painter.drawPixmap(QPoint(0, 0), isEnabled() ? pix : QPixmap::fromImage(grayImage(pix.toImage()))); + + QString month = mDate.toString("MMM"); + QString year = mDate.toString("yyyy"); + QString day = mDate.toString("dd"); + + QFont font = QFont("monospace", 10); + QFontMetrics metrics = QFontMetrics(font); + painter.setFont(font); + painter.setPen(QPen(QBrush(Qt::white), 0)); + painter.setBrush(QBrush(Qt::white)); + painter.drawText(QPoint(6, metrics.height() + 1), month); + painter.drawText(QPoint(DATEWIDGETWIDTH - metrics.width(year) - 6, metrics.height() + 1), year); + + font.setPointSize(14); + metrics = QFontMetrics(font); + painter.setPen(QPen(QBrush(Qt::black), 0)); + painter.setBrush(Qt::black); + painter.setFont(font); + painter.drawText(QPoint(DATEWIDGETWIDTH / 2 - metrics.width(day) / 2, 45), day); + + if (hasFocus()) { + QStyleOptionFocusRect option; + option.initFrom(this); + option.backgroundColor = palette().color(QPalette::Background); + style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this); + } +} + +void DateWidget::mousePressEvent(QMouseEvent *event) +{ + calendarWidget->setSelectedDate(mDate); + calendarWidget->move(event->globalPos()); + calendarWidget->show(); + calendarWidget->raise(); + calendarWidget->setFocus(); +} + +void DateWidget::focusInEvent(QFocusEvent *event) +{ + setFocus(); + QWidget::focusInEvent(event); +} + +void DateWidget::focusOutEvent(QFocusEvent *event) +{ + QWidget::focusOutEvent(event); +} + +void DateWidget::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Return || + event->key() == Qt::Key_Enter || + event->key() == Qt::Key_Space) { + calendarWidget->move(mapToGlobal(QPoint(0, 64))); + calendarWidget->show(); + event->setAccepted(true); + } else { + QWidget::keyPressEvent(event); + } +} + +#define COMPONENT_FROM_UI(_component) what->_component = ui._component->isChecked() +#define UI_FROM_COMPONENT(_component) ui._component->setChecked(what->_component) + +DiveComponentSelection::DiveComponentSelection(QWidget *parent, struct dive *target, struct dive_components *_what) : targetDive(target) +{ + ui.setupUi(this); + what = _what; + UI_FROM_COMPONENT(divesite); + UI_FROM_COMPONENT(divemaster); + UI_FROM_COMPONENT(buddy); + UI_FROM_COMPONENT(rating); + UI_FROM_COMPONENT(visibility); + UI_FROM_COMPONENT(notes); + UI_FROM_COMPONENT(suit); + UI_FROM_COMPONENT(tags); + UI_FROM_COMPONENT(cylinders); + UI_FROM_COMPONENT(weights); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void DiveComponentSelection::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + COMPONENT_FROM_UI(divesite); + COMPONENT_FROM_UI(divemaster); + COMPONENT_FROM_UI(buddy); + COMPONENT_FROM_UI(rating); + COMPONENT_FROM_UI(visibility); + COMPONENT_FROM_UI(notes); + COMPONENT_FROM_UI(suit); + COMPONENT_FROM_UI(tags); + COMPONENT_FROM_UI(cylinders); + COMPONENT_FROM_UI(weights); + selective_copy_dive(&displayed_dive, targetDive, *what, true); + } +} + +TagFilter::TagFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Tags: ")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(TagFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void TagFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(TagFilterModel::instance()); + QWidget::showEvent(event); +} + +void TagFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(TagFilterModel::instance()); + QWidget::hideEvent(event); +} + +BuddyFilter::BuddyFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Person: ")); + ui.label->setToolTip(tr("Searches for buddies and divemasters")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(BuddyFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void BuddyFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(BuddyFilterModel::instance()); + QWidget::showEvent(event); +} + +void BuddyFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(BuddyFilterModel::instance()); + QWidget::hideEvent(event); +} + +LocationFilter::LocationFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Location: ")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(LocationFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void LocationFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(LocationFilterModel::instance()); + QWidget::showEvent(event); +} + +void LocationFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(LocationFilterModel::instance()); + QWidget::hideEvent(event); +} + +SuitFilter::SuitFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Suits: ")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(SuitsFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void SuitFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(SuitsFilterModel::instance()); + QWidget::showEvent(event); +} + +void SuitFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(SuitsFilterModel::instance()); + QWidget::hideEvent(event); +} + +MultiFilter::MultiFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + + QWidget *expandedWidget = new QWidget(); + QHBoxLayout *l = new QHBoxLayout(); + + TagFilter *tagFilter = new TagFilter(this); + int minimumHeight = tagFilter->ui.filterInternalList->height() + + tagFilter->ui.verticalLayout->spacing() * tagFilter->ui.verticalLayout->count(); + + QListView *dummyList = new QListView(); + QStringListModel *dummy = new QStringListModel(QStringList() << "Dummy Text"); + dummyList->setModel(dummy); + + connect(ui.close, SIGNAL(clicked(bool)), this, SLOT(closeFilter())); + connect(ui.clear, SIGNAL(clicked(bool)), MultiFilterSortModel::instance(), SLOT(clearFilter())); + connect(ui.maximize, SIGNAL(clicked(bool)), this, SLOT(adjustHeight())); + + l->addWidget(tagFilter); + l->addWidget(new BuddyFilter()); + l->addWidget(new LocationFilter()); + l->addWidget(new SuitFilter()); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + expandedWidget->setLayout(l); + + ui.scrollArea->setWidget(expandedWidget); + expandedWidget->resize(expandedWidget->width(), minimumHeight + dummyList->sizeHintForRow(0) * 5 ); + ui.scrollArea->setMinimumHeight(expandedWidget->height() + 5); + + connect(MultiFilterSortModel::instance(), SIGNAL(filterFinished()), this, SLOT(filterFinished())); +} + +void MultiFilter::filterFinished() +{ + ui.filterText->setText(tr("Filter shows %1 (of %2) dives").arg(MultiFilterSortModel::instance()->divesDisplayed).arg(dive_table.nr)); +} + +void MultiFilter::adjustHeight() +{ + ui.scrollArea->setVisible(!ui.scrollArea->isVisible()); +} + +void MultiFilter::closeFilter() +{ + MultiFilterSortModel::instance()->clearFilter(); + hide(); +} diff --git a/desktop-widgets/simplewidgets.h b/desktop-widgets/simplewidgets.h new file mode 100644 index 000000000..595c4cd4b --- /dev/null +++ b/desktop-widgets/simplewidgets.h @@ -0,0 +1,237 @@ +#ifndef SIMPLEWIDGETS_H +#define SIMPLEWIDGETS_H + +class MinMaxAvgWidgetPrivate; +class QAbstractButton; +class QNetworkReply; + +#include +#include +#include +#include + +#include "ui_renumber.h" +#include "ui_setpoint.h" +#include "ui_shifttimes.h" +#include "ui_shiftimagetimes.h" +#include "ui_urldialog.h" +#include "ui_divecomponentselection.h" +#include "ui_listfilter.h" +#include "ui_filterwidget.h" +#include "exif.h" +#include + + +class MinMaxAvgWidget : public QWidget { + Q_OBJECT + Q_PROPERTY(double minimum READ minimum WRITE setMinimum) + Q_PROPERTY(double maximum READ maximum WRITE setMaximum) + Q_PROPERTY(double average READ average WRITE setAverage) +public: + MinMaxAvgWidget(QWidget *parent); + ~MinMaxAvgWidget(); + double minimum() const; + double maximum() const; + double average() const; + void setMinimum(double minimum); + void setMaximum(double maximum); + void setAverage(double average); + void setMinimum(const QString &minimum); + void setMaximum(const QString &maximum); + void setAverage(const QString &average); + void overrideMinToolTipText(const QString &newTip); + void overrideAvgToolTipText(const QString &newTip); + void overrideMaxToolTipText(const QString &newTip); + void clear(); + +private: + QScopedPointer d; +}; + +class RenumberDialog : public QDialog { + Q_OBJECT +public: + static RenumberDialog *instance(); + void renumberOnlySelected(bool selected = true); +private +slots: + void buttonClicked(QAbstractButton *button); + +private: + explicit RenumberDialog(QWidget *parent); + Ui::RenumberDialog ui; + bool selectedOnly; +}; + +class SetpointDialog : public QDialog { + Q_OBJECT +public: + static SetpointDialog *instance(); + void setpointData(struct divecomputer *divecomputer, int time); +private +slots: + void buttonClicked(QAbstractButton *button); + +private: + explicit SetpointDialog(QWidget *parent); + Ui::SetpointDialog ui; + struct divecomputer *dc; + int time; +}; + +class ShiftTimesDialog : public QDialog { + Q_OBJECT +public: + static ShiftTimesDialog *instance(); + void showEvent(QShowEvent *event); +private +slots: + void buttonClicked(QAbstractButton *button); + void changeTime(); + +private: + explicit ShiftTimesDialog(QWidget *parent); + int64_t when; + Ui::ShiftTimesDialog ui; +}; + +class ShiftImageTimesDialog : public QDialog { + Q_OBJECT +public: + explicit ShiftImageTimesDialog(QWidget *parent, QStringList fileNames); + time_t amount() const; + void setOffset(time_t offset); + bool matchAll(); +private +slots: + void buttonClicked(QAbstractButton *button); + void syncCameraClicked(); + void dcDateTimeChanged(const QDateTime &); + void timeEditChanged(const QTime &time); + void updateInvalid(); + void matchAllImagesToggled(bool); + +private: + QStringList fileNames; + Ui::ShiftImageTimesDialog ui; + time_t m_amount; + time_t dcImageEpoch; + bool matchAllImages; +}; + +class URLDialog : public QDialog { + Q_OBJECT +public: + explicit URLDialog(QWidget *parent); + QString url() const; +private: + Ui::URLDialog ui; +}; + +class QCalendarWidget; + +class DateWidget : public QWidget { + Q_OBJECT +public: + DateWidget(QWidget *parent = 0); + QDate date() const; +public +slots: + void setDate(const QDate &date); + +protected: + void paintEvent(QPaintEvent *event); + void mousePressEvent(QMouseEvent *event); + void focusInEvent(QFocusEvent *); + void focusOutEvent(QFocusEvent *); + void keyPressEvent(QKeyEvent *); + void changeEvent(QEvent *); + bool eventFilter(QObject *, QEvent *); +signals: + void dateChanged(const QDate &date); + +private: + QDate mDate; + QCalendarWidget *calendarWidget; +}; + +class DiveComponentSelection : public QDialog { + Q_OBJECT +public: + explicit DiveComponentSelection(QWidget *parent, struct dive *target, struct dive_components *_what); +private +slots: + void buttonClicked(QAbstractButton *button); + +private: + Ui::DiveComponentSelectionDialog ui; + struct dive *targetDive; + struct dive_components *what; +}; + +namespace Ui{ + class FilterWidget2; +}; + +class MultiFilter : public QWidget { + Q_OBJECT +public +slots: + void closeFilter(); + void adjustHeight(); + void filterFinished(); + +public: + MultiFilter(QWidget *parent); + Ui::FilterWidget2 ui; +}; + +class TagFilter : public QWidget { + Q_OBJECT +public: + TagFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; + friend class MultiFilter; +}; + +class BuddyFilter : public QWidget { + Q_OBJECT +public: + BuddyFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; +}; + +class SuitFilter : public QWidget { + Q_OBJECT +public: + SuitFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; +}; + +class LocationFilter : public QWidget { + Q_OBJECT +public: + LocationFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; +}; + +bool isGnome3Session(); +QImage grayImage(const QImage &coloredImg); + +#endif // SIMPLEWIDGETS_H diff --git a/desktop-widgets/socialnetworks.cpp b/desktop-widgets/socialnetworks.cpp new file mode 100644 index 000000000..6e191267a --- /dev/null +++ b/desktop-widgets/socialnetworks.cpp @@ -0,0 +1,326 @@ +#include "socialnetworks.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "mainwindow.h" +#include "profile/profilewidget2.h" +#include "pref.h" +#include "helpers.h" +#include "ui_socialnetworksdialog.h" + +#if SAVE_FB_CREDENTIALS +#define GET_TXT(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = strdup(v.toString().toUtf8().constData()); \ + else \ + prefs.field = default_prefs.field +#endif + +FacebookManager *FacebookManager::instance() +{ + static FacebookManager *self = new FacebookManager(); + return self; +} + +FacebookManager::FacebookManager(QObject *parent) : QObject(parent) +{ + sync(); +} + +QUrl FacebookManager::connectUrl() { + return QUrl("https://www.facebook.com/dialog/oauth?" + "client_id=427722490709000" + "&redirect_uri=http://www.facebook.com/connect/login_success.html" + "&response_type=token,granted_scopes" + "&display=popup" + "&scope=publish_actions,user_photos" + ); +} + +bool FacebookManager::loggedIn() { + return prefs.facebook.access_token != NULL; +} + +void FacebookManager::sync() +{ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup("WebApps"); + s.beginGroup("Facebook"); + + QVariant v; + GET_TXT("ConnectToken", facebook.access_token); + GET_TXT("UserId", facebook.user_id); + GET_TXT("AlbumId", facebook.album_id); +#endif +} + +void FacebookManager::tryLogin(const QUrl& loginResponse) +{ + QString result = loginResponse.toString(); + if (!result.contains("access_token")) + return; + + if (result.contains("denied_scopes=publish_actions") || result.contains("denied_scopes=user_photos")) { + qDebug() << "user did not allow us access" << result; + return; + } + int from = result.indexOf("access_token=") + strlen("access_token="); + int to = result.indexOf("&expires_in"); + QString securityToken = result.mid(from, to-from); + +#if SAVE_FB_CREDENTIALS + QSettings settings; + settings.beginGroup("WebApps"); + settings.beginGroup("Facebook"); + settings.setValue("ConnectToken", securityToken); + sync(); +#else + prefs.facebook.access_token = copy_string(securityToken.toUtf8().data()); +#endif + requestUserId(); + sync(); + emit justLoggedIn(true); +} + +void FacebookManager::logout() +{ +#if SAVE_FB_CREDENTIALS + QSettings settings; + settings.beginGroup("WebApps"); + settings.beginGroup("Facebook"); + settings.remove("ConnectToken"); + settings.remove("UserId"); + settings.remove("AlbumId"); + sync(); +#else + free(prefs.facebook.access_token); + free(prefs.facebook.album_id); + free(prefs.facebook.user_id); + prefs.facebook.access_token = NULL; + prefs.facebook.album_id = NULL; + prefs.facebook.user_id = NULL; +#endif + emit justLoggedOut(true); +} + +void FacebookManager::requestAlbumId() +{ + QUrl albumListUrl("https://graph.facebook.com/me/albums?access_token=" + QString(prefs.facebook.access_token)); + QNetworkAccessManager *manager = new QNetworkAccessManager(); + QNetworkReply *reply = manager->get(QNetworkRequest(albumListUrl)); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup("WebApps"); + s.beginGroup("Facebook"); +#endif + + QJsonDocument albumsDoc = QJsonDocument::fromJson(reply->readAll()); + QJsonArray albumObj = albumsDoc.object().value("data").toArray(); + foreach(const QJsonValue &v, albumObj){ + QJsonObject obj = v.toObject(); + if (obj.value("name").toString() == albumName) { +#if SAVE_FB_CREDENTIALS + s.setValue("AlbumId", obj.value("id").toString()); +#else + prefs.facebook.album_id = copy_string(obj.value("id").toString().toUtf8().data()); +#endif + return; + } + } + + QUrlQuery params; + params.addQueryItem("name", albumName ); + params.addQueryItem("description", "Subsurface Album"); + params.addQueryItem("privacy", "{'value': 'SELF'}"); + + QNetworkRequest request(albumListUrl); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); + reply = manager->post(request, params.query().toLocal8Bit()); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + albumsDoc = QJsonDocument::fromJson(reply->readAll()); + QJsonObject album = albumsDoc.object(); + if (album.contains("id")) { +#if SAVE_FB_CREDENTIALS + s.setValue("AlbumId", album.value("id").toString()); +#else + prefs.facebook.album_id = copy_string(album.value("id").toString().toUtf8().data()); +#endif + sync(); + return; + } +} + +void FacebookManager::requestUserId() +{ + QUrl userIdRequest("https://graph.facebook.com/me?fields=id&access_token=" + QString(prefs.facebook.access_token)); + QNetworkAccessManager *getUserID = new QNetworkAccessManager(); + QNetworkReply *reply = getUserID->get(QNetworkRequest(userIdRequest)); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + QJsonObject obj = jsonDoc.object(); + if (obj.keys().contains("id")){ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup("WebApps"); + s.beginGroup("Facebook"); + s.setValue("UserId", obj.value("id").toVariant()); +#else + prefs.facebook.user_id = copy_string(obj.value("id").toString().toUtf8().data()); +#endif + return; + } +} + +void FacebookManager::setDesiredAlbumName(const QString& a) +{ + albumName = a; +} + +/* to be changed to export the currently selected dive as shown on the profile. + * Much much easier, and its also good to people do not select all the dives + * and send erroniously *all* of them to facebook. */ +void FacebookManager::sendDive() +{ + SocialNetworkDialog dialog(qApp->activeWindow()); + if (dialog.exec() != QDialog::Accepted) + return; + + setDesiredAlbumName(dialog.album()); + requestAlbumId(); + + ProfileWidget2 *profile = MainWindow::instance()->graphics(); + profile->setToolTipVisibile(false); + QPixmap pix = QPixmap::grabWidget(profile); + profile->setToolTipVisibile(true); + QByteArray bytes; + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + pix.save(&buffer, "PNG"); + QUrl url("https://graph.facebook.com/v2.2/" + QString(prefs.facebook.album_id) + "/photos?" + + "&access_token=" + QString(prefs.facebook.access_token) + + "&source=image" + + "&message=" + dialog.text().replace(""", "%22")); + + QNetworkAccessManager *am = new QNetworkAccessManager(this); + QNetworkRequest request(url); + + QString bound="margin"; + + //according to rfc 1867 we need to put this string here: + QByteArray data(QString("--" + bound + "\r\n").toLocal8Bit()); + data.append("Content-Disposition: form-data; name=\"action\"\r\n\r\n"); + data.append("https://graph.facebook.com/v2.2/\r\n"); + data.append("--" + bound + "\r\n"); //according to rfc 1867 + data.append("Content-Disposition: form-data; name=\"uploaded\"; filename=\"" + QString::number(qrand()) + ".png\"\r\n"); //name of the input is "uploaded" in my form, next one is a file name. + data.append("Content-Type: image/jpeg\r\n\r\n"); //data type + data.append(bytes); //let's read the file + data.append("\r\n"); + data.append("--" + bound + "--\r\n"); //closing boundary according to rfc 1867 + + request.setRawHeader(QString("Content-Type").toLocal8Bit(),QString("multipart/form-data; boundary=" + bound).toLocal8Bit()); + request.setRawHeader(QString("Content-Length").toLocal8Bit(), QString::number(data.length()).toLocal8Bit()); + QNetworkReply *reply = am->post(request,data); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QByteArray response = reply->readAll(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(response); + QJsonObject obj = jsonDoc.object(); + if (obj.keys().contains("id")){ + QMessageBox::information(qApp->activeWindow(), + tr("Photo upload sucessfull"), + tr("Your dive profile was updated to Facebook."), + QMessageBox::Ok); + } else { + QMessageBox::information(qApp->activeWindow(), + tr("Photo upload failed"), + tr("Your dive profile was not updated to Facebook, \n " + "please send the following to the developer. \n" + + response), + QMessageBox::Ok); + } +} + +SocialNetworkDialog::SocialNetworkDialog(QWidget *parent) : + QDialog(parent), + ui( new Ui::SocialnetworksDialog()) +{ + ui->setupUi(this); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + connect(ui->date, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->duration, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Buddy, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Divemaster, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Location, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Notes, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->album, SIGNAL(textChanged(QString)), this, SLOT(albumChanged())); +} + +void SocialNetworkDialog::albumChanged() +{ + QAbstractButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); + button->setEnabled(!ui->album->text().isEmpty()); +} + +void SocialNetworkDialog::selectionChanged() +{ + struct dive *d = current_dive; + QString fullText; + if (ui->date->isChecked()) { + fullText += tr("Dive date: %1 \n").arg(get_short_dive_date_string(d->when)); + } + if (ui->duration->isChecked()) { + fullText += tr("Duration: %1 \n").arg(get_dive_duration_string(d->duration.seconds, + tr("h:", "abbreviation for hours plus separator"), + tr("min", "abbreviation for minutes"))); + } + if (ui->Location->isChecked()) { + fullText += tr("Dive location: %1 \n").arg(get_dive_location(d)); + } + if (ui->Buddy->isChecked()) { + fullText += tr("Buddy: %1 \n").arg(d->buddy); + } + if (ui->Divemaster->isChecked()) { + fullText += tr("Divemaster: %1 \n").arg(d->divemaster); + } + if (ui->Notes->isChecked()) { + fullText += tr("\n%1").arg(d->notes); + } + ui->text->setPlainText(fullText); +} + +QString SocialNetworkDialog::text() const { + return ui->text->toPlainText().toHtmlEscaped(); +} + +QString SocialNetworkDialog::album() const { + return ui->album->text().toHtmlEscaped(); +} diff --git a/desktop-widgets/socialnetworks.h b/desktop-widgets/socialnetworks.h new file mode 100644 index 000000000..2f63915ca --- /dev/null +++ b/desktop-widgets/socialnetworks.h @@ -0,0 +1,49 @@ +#ifndef FACEBOOKMANAGER_H +#define FACEBOOKMANAGER_H + +#include +#include +#include + +class FacebookManager : public QObject +{ + Q_OBJECT +public: + static FacebookManager *instance(); + void requestAlbumId(); + void requestUserId(); + void sync(); + QUrl connectUrl(); + bool loggedIn(); +signals: + void justLoggedIn(bool triggererd); + void justLoggedOut(bool triggered); + +public slots: + void tryLogin(const QUrl& loginResponse); + void logout(); + void setDesiredAlbumName(const QString& albumName); + void sendDive(); + +private: + explicit FacebookManager(QObject *parent = 0); + QString albumName; +}; + +namespace Ui { + class SocialnetworksDialog; +} + +class SocialNetworkDialog : public QDialog { + Q_OBJECT +public: + SocialNetworkDialog(QWidget *parent); + QString text() const; + QString album() const; +public slots: + void selectionChanged(); + void albumChanged(); +private: + Ui::SocialnetworksDialog *ui; +}; +#endif // FACEBOOKMANAGER_H diff --git a/desktop-widgets/socialnetworksdialog.ui b/desktop-widgets/socialnetworksdialog.ui new file mode 100644 index 000000000..e8953d1c7 --- /dev/null +++ b/desktop-widgets/socialnetworksdialog.ui @@ -0,0 +1,184 @@ + + + SocialnetworksDialog + + + + 0 + 0 + 475 + 416 + + + + Dialog + + + + + 290 + 380 + 166 + 22 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 10 + 15 + 451 + 361 + + + + + 1 + + + 1 + + + 1 + + + 1 + + + + + The text to the right will be posted as the description with your profile picture to Facebook. The album name is required (the profile picture will be posted to that album). + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Album + + + + + + + The profile picture will be posted in this album (required) + + + + + + + Include + + + + + + + Date and time + + + + + + + Duration + + + + + + + Location + + + + + + + Divemaster + + + + + + + Buddy + + + + + + + Notes + + + + + + + + 75 + true + + + + Facebook post preview + + + + + + + + + + + + + buttonBox + accepted() + SocialnetworksDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SocialnetworksDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/starwidget.cpp b/desktop-widgets/starwidget.cpp new file mode 100644 index 000000000..d959ed3b9 --- /dev/null +++ b/desktop-widgets/starwidget.cpp @@ -0,0 +1,164 @@ +#include "starwidget.h" +#include "metrics.h" +#include +#include +#include "simplewidgets.h" + +QImage StarWidget::activeStar; +QImage StarWidget::inactiveStar; + +const QImage& StarWidget::starActive() +{ + return activeStar; +} + +const QImage& StarWidget::starInactive() +{ + return inactiveStar; +} + +QImage focusedImage(const QImage& coloredImg) +{ + QImage img = coloredImg; + for (int i = 0; i < img.width(); ++i) { + for (int j = 0; j < img.height(); ++j) { + QRgb rgb = img.pixel(i, j); + if (!rgb) + continue; + + QColor c(rgb); + c = c.dark(); + img.setPixel(i, j, c.rgb()); + } + } + + return img; +} + + +int StarWidget::currentStars() const +{ + return current; +} + +void StarWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (readOnly) { + return; + } + + int starClicked = event->pos().x() / defaultIconMetrics().sz_small + 1; + if (starClicked > TOTALSTARS) + starClicked = TOTALSTARS; + + if (current == starClicked) + current -= 1; + else + current = starClicked; + + Q_EMIT valueChanged(current); + update(); +} + +void StarWidget::paintEvent(QPaintEvent *event) +{ + QPainter p(this); + QImage star = hasFocus() ? focusedImage(starActive()) : starActive(); + QPixmap selected = QPixmap::fromImage(star); + QPixmap inactive = QPixmap::fromImage(starInactive()); + const IconMetrics& metrics = defaultIconMetrics(); + + + for (int i = 0; i < current; i++) + p.drawPixmap(i * metrics.sz_small + metrics.spacing, 0, selected); + + for (int i = current; i < TOTALSTARS; i++) + p.drawPixmap(i * metrics.sz_small + metrics.spacing, 0, inactive); + + if (hasFocus()) { + QStyleOptionFocusRect option; + option.initFrom(this); + option.backgroundColor = palette().color(QPalette::Background); + style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &p, this); + } +} + +void StarWidget::setCurrentStars(int value) +{ + current = value; + update(); + Q_EMIT valueChanged(current); +} + +StarWidget::StarWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), + current(0), + readOnly(false) +{ + int dim = defaultIconMetrics().sz_small; + + if (activeStar.isNull()) { + QSvgRenderer render(QString(":star")); + QPixmap renderedStar(dim, dim); + + renderedStar.fill(Qt::transparent); + QPainter painter(&renderedStar); + + render.render(&painter, QRectF(0, 0, dim, dim)); + activeStar = renderedStar.toImage(); + } + if (inactiveStar.isNull()) { + inactiveStar = grayImage(activeStar); + } + setFocusPolicy(Qt::StrongFocus); +} + +QImage grayImage(const QImage& coloredImg) +{ + QImage img = coloredImg; + for (int i = 0; i < img.width(); ++i) { + for (int j = 0; j < img.height(); ++j) { + QRgb rgb = img.pixel(i, j); + if (!rgb) + continue; + + QColor c(rgb); + int gray = 204 + (c.red() + c.green() + c.blue()) / 15; + img.setPixel(i, j, qRgb(gray, gray, gray)); + } + } + + return img; +} + +QSize StarWidget::sizeHint() const +{ + const IconMetrics& metrics = defaultIconMetrics(); + return QSize(metrics.sz_small * TOTALSTARS + metrics.spacing * (TOTALSTARS - 1), metrics.sz_small); +} + +void StarWidget::setReadOnly(bool r) +{ + readOnly = r; +} + +void StarWidget::focusInEvent(QFocusEvent *event) +{ + setFocus(); + QWidget::focusInEvent(event); +} + +void StarWidget::focusOutEvent(QFocusEvent *event) +{ + QWidget::focusOutEvent(event); +} + +void StarWidget::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Right) { + if (currentStars() < TOTALSTARS) + setCurrentStars(currentStars() + 1); + } else if (event->key() == Qt::Key_Down || event->key() == Qt::Key_Left) { + if (currentStars() > 0) + setCurrentStars(currentStars() - 1); + } +} diff --git a/desktop-widgets/starwidget.h b/desktop-widgets/starwidget.h new file mode 100644 index 000000000..989aa527d --- /dev/null +++ b/desktop-widgets/starwidget.h @@ -0,0 +1,44 @@ +#ifndef STARWIDGET_H +#define STARWIDGET_H + +#include + +enum StarConfig { + TOTALSTARS = 5 +}; + +class StarWidget : public QWidget { + Q_OBJECT +public: + explicit StarWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + int currentStars() const; + + /*reimp*/ QSize sizeHint() const; + + static const QImage& starActive(); + static const QImage& starInactive(); + +signals: + void valueChanged(int stars); + +public +slots: + void setCurrentStars(int value); + void setReadOnly(bool readOnly); + +protected: + /*reimp*/ void mouseReleaseEvent(QMouseEvent *); + /*reimp*/ void paintEvent(QPaintEvent *); + /*reimp*/ void focusInEvent(QFocusEvent *); + /*reimp*/ void focusOutEvent(QFocusEvent *); + /*reimp*/ void keyPressEvent(QKeyEvent *); + +private: + int current; + bool readOnly; + + static QImage activeStar; + static QImage inactiveStar; +}; + +#endif // STARWIDGET_H diff --git a/desktop-widgets/statistics/monthstatistics.cpp b/desktop-widgets/statistics/monthstatistics.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/statistics/monthstatistics.h b/desktop-widgets/statistics/monthstatistics.h new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/statistics/statisticsbar.cpp b/desktop-widgets/statistics/statisticsbar.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/statistics/statisticsbar.h b/desktop-widgets/statistics/statisticsbar.h new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/statistics/statisticswidget.cpp b/desktop-widgets/statistics/statisticswidget.cpp new file mode 100644 index 000000000..3e91fa317 --- /dev/null +++ b/desktop-widgets/statistics/statisticswidget.cpp @@ -0,0 +1,41 @@ +#include "statisticswidget.h" +#include "yearlystatisticsmodel.h" +#include + +YearlyStatisticsWidget::YearlyStatisticsWidget(QWidget *parent): + QGraphicsView(parent), + m_model(NULL) +{ +} + +void YearlyStatisticsWidget::setModel(YearlyStatisticsModel *m) +{ + m_model = m; + connect(m, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(modelDataChanged(QModelIndex,QModelIndex))); + connect(m, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + scene(), SLOT(clear())); + connect(m, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(modelRowsInserted(QModelIndex,int,int))); + + modelRowsInserted(QModelIndex(),0,m_model->rowCount()-1); +} + +void YearlyStatisticsWidget::modelRowsInserted(const QModelIndex &index, int first, int last) +{ + // stub +} + +void YearlyStatisticsWidget::modelDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight) +{ + Q_UNUSED(topLeft); + Q_UNUSED(bottomRight); + scene()->clear(); + modelRowsInserted(QModelIndex(),0,m_model->rowCount()-1); +} + +void YearlyStatisticsWidget::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + fitInView(sceneRect(), Qt::IgnoreAspectRatio); +} diff --git a/desktop-widgets/statistics/statisticswidget.h b/desktop-widgets/statistics/statisticswidget.h new file mode 100644 index 000000000..ae988292d --- /dev/null +++ b/desktop-widgets/statistics/statisticswidget.h @@ -0,0 +1,23 @@ +#ifndef YEARLYSTATISTICSWIDGET_H +#define YEARLYSTATISTICSWIDGET_H + +#include + +class YearlyStatisticsModel; +class QModelIndex; + +class YearlyStatisticsWidget : public QGraphicsView { + Q_OBJECT +public: + YearlyStatisticsWidget(QWidget *parent = 0); + void setModel(YearlyStatisticsModel *m); +protected: + virtual void resizeEvent(QResizeEvent *event); +public slots: + void modelRowsInserted(const QModelIndex& index, int first, int last); + void modelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); +private: + YearlyStatisticsModel *m_model; +}; + +#endif \ No newline at end of file diff --git a/desktop-widgets/statistics/yearstatistics.cpp b/desktop-widgets/statistics/yearstatistics.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/statistics/yearstatistics.h b/desktop-widgets/statistics/yearstatistics.h new file mode 100644 index 000000000..e69de29bb diff --git a/desktop-widgets/subsurfacewebservices.cpp b/desktop-widgets/subsurfacewebservices.cpp new file mode 100644 index 000000000..ee079cc48 --- /dev/null +++ b/desktop-widgets/subsurfacewebservices.cpp @@ -0,0 +1,1121 @@ +#include "subsurfacewebservices.h" +#include "helpers.h" +#include "webservice.h" +#include "mainwindow.h" +#include "usersurvey.h" +#include "divelist.h" +#include "globe.h" +#include "maintab.h" +#include "display.h" +#include "membuffer.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_UNIX +#include // for dup(2) +#endif + +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +struct dive_table gps_location_table; + +// we don't overwrite any existing GPS info in the dive +// so get the dive site and if there is none or there is one without GPS fix, add it +static void copy_gps_location(struct dive *from, struct dive *to) +{ + struct dive_site *ds = get_dive_site_for_dive(to); + if (!ds || !dive_site_has_gps_location(ds)) { + struct dive_site *gds = get_dive_site_for_dive(from); + if (!ds) { + // simply link to the one created for the fake dive + to->dive_site_uuid = gds->uuid; + } else { + ds->latitude = gds->latitude; + ds->longitude = gds->longitude; + if (same_string(ds->name, "")) + ds->name = copy_string(gds->name); + } + } +} + +#define SAME_GROUP 6 * 3600 // six hours +//TODO: C Code. static functions are not good if we plan to have a test for them. +static bool merge_locations_into_dives(void) +{ + int i, j, tracer=0, changed=0; + struct dive *gpsfix, *nextgpsfix, *dive; + + sort_table(&gps_location_table); + + for_each_dive (i, dive) { + if (!dive_has_gps_location(dive)) { + for (j = tracer; (gpsfix = get_dive_from_table(j, &gps_location_table)) !=NULL; j++) { + if (time_during_dive_with_offset(dive, gpsfix->when, SAME_GROUP)) { + if (verbose) + qDebug() << "processing gpsfix @" << get_dive_date_string(gpsfix->when) << + "which is withing six hours of dive from" << + get_dive_date_string(dive->when) << "until" << + get_dive_date_string(dive->when + dive->duration.seconds); + /* + * If position is fixed during dive. This is the good one. + * Asign and mark position, and end gps_location loop + */ + if (time_during_dive_with_offset(dive, gpsfix->when, 0)) { + if (verbose) + qDebug() << "gpsfix is during the dive, pick that one"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } else { + /* + * If it is not, check if there are more position fixes in SAME_GROUP range + */ + if ((nextgpsfix = get_dive_from_table(j + 1, &gps_location_table)) && + time_during_dive_with_offset(dive, nextgpsfix->when, SAME_GROUP)) { + if (verbose) + qDebug() << "look at the next gps fix @" << get_dive_date_string(nextgpsfix->when); + /* first let's test if this one is during the dive */ + if (time_during_dive_with_offset(dive, nextgpsfix->when, 0)) { + if (verbose) + qDebug() << "which is during the dive, pick that one"; + copy_gps_location(nextgpsfix, dive); + changed++; + tracer = j + 1; + break; + } + /* we know the gps fixes are sorted; if they are both before the dive, ignore the first, + * if theay are both after the dive, take the first, + * if the first is before and the second is after, take the closer one */ + if (nextgpsfix->when < dive->when) { + if (verbose) + qDebug() << "which is closer to the start of the dive, do continue with that"; + continue; + } else if (gpsfix->when > dive->when + dive->duration.seconds) { + if (verbose) + qDebug() << "which is even later after the end of the dive, so pick the previous one"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } else { + /* ok, gpsfix is before, nextgpsfix is after */ + if (dive->when - gpsfix->when <= nextgpsfix->when - (dive->when + dive->duration.seconds)) { + if (verbose) + qDebug() << "pick the one before as it's closer to the start"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } else { + if (verbose) + qDebug() << "pick the one after as it's closer to the start"; + copy_gps_location(nextgpsfix, dive); + changed++; + tracer = j + 1; + break; + } + } + /* + * If no more positions in range, the actual is the one. Asign, mark and end loop. + */ + } else { + if (verbose) + qDebug() << "which seems to be the best one for this dive, so pick it"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } + } + } else { + /* If position is out of SAME_GROUP range and in the future, mark position for + * next dive iteration and end the gps_location loop + */ + if (gpsfix->when >= dive->when + dive->duration.seconds + SAME_GROUP) { + tracer = j; + break; + } + } + } + } + } + return changed > 0; +} + +// TODO: This looks like should be ported to C code. or a big part of it. +bool DivelogsDeWebServices::prepare_dives_for_divelogs(const QString &tempfile, const bool selected) +{ + static const char errPrefix[] = "divelog.de-upload:"; + if (!amount_selected) { + report_error(tr("no dives were selected").toUtf8()); + return false; + } + + xsltStylesheetPtr xslt = NULL; + struct zip *zip; + + xslt = get_stylesheet("divelogs-export.xslt"); + if (!xslt) { + qDebug() << errPrefix << "missing stylesheet"; + report_error(tr("stylesheet to export to divelogs.de is not found").toUtf8()); + return false; + } + + + int error_code; + zip = zip_open(QFile::encodeName(QDir::toNativeSeparators(tempfile)), ZIP_CREATE, &error_code); + if (!zip) { + char buffer[1024]; + zip_error_to_str(buffer, sizeof buffer, error_code, errno); + report_error(tr("failed to create zip file for upload: %s").toUtf8(), buffer); + return false; + } + + /* walk the dive list in chronological order */ + int i; + struct dive *dive; + for_each_dive (i, dive) { + FILE *f; + char filename[PATH_MAX]; + int streamsize; + const char *membuf; + xmlDoc *transformed; + struct zip_source *s; + struct membuffer mb = { 0 }; + + /* + * Get the i'th dive in XML format so we can process it. + * We need to save to a file before we can reload it back into memory... + */ + if (selected && !dive->selected) + continue; + /* make sure the buffer is empty and add the dive */ + mb.len = 0; + + struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); + + if (ds) { + put_format(&mb, "latitude.udeg || ds->longitude.udeg) { + put_degrees(&mb, ds->latitude, " gps='", " "); + put_degrees(&mb, ds->longitude, "", "'"); + } + put_format(&mb, "/>\n\n"); + } + + save_one_dive_to_mb(&mb, dive); + + if (ds) { + put_format(&mb, "\n"); + } + membuf = mb_cstring(&mb); + streamsize = strlen(membuf); + /* + * Parse the memory buffer into XML document and + * transform it to divelogs.de format, finally dumping + * the XML into a character buffer. + */ + xmlDoc *doc = xmlReadMemory(membuf, streamsize, "divelog", NULL, 0); + if (!doc) { + qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!"; + report_error(tr("internal error").toUtf8()); + goto error_close_zip; + } + free((void *)membuf); + + transformed = xsltApplyStylesheet(xslt, doc, NULL); + if (!transformed) { + qWarning() << errPrefix << "XSLT transform failed for dive: " << i; + report_error(tr("Conversion of dive %1 to divelogs.de format failed").arg(i).toUtf8()); + continue; + } + xmlDocDumpMemory(transformed, (xmlChar **)&membuf, &streamsize); + xmlFreeDoc(doc); + xmlFreeDoc(transformed); + + /* + * Save the XML document into a zip file. + */ + snprintf(filename, PATH_MAX, "%d.xml", i + 1); + s = zip_source_buffer(zip, membuf, streamsize, 1); + if (s) { + int64_t ret = zip_add(zip, filename, s); + if (ret == -1) + qDebug() << errPrefix << "failed to include dive:" << i; + } + } + xsltFreeStylesheet(xslt); + if (zip_close(zip)) { + int ze, se; +#if LIBZIP_VERSION_MAJOR >= 1 + zip_error_t *error = zip_get_error(zip); + ze = zip_error_code_zip(error); + se = zip_error_code_system(error); +#else + zip_error_get(zip, &ze, &se); +#endif + report_error(qPrintable(tr("error writing zip file: %s zip error %d system error %d - %s")), + qPrintable(QDir::toNativeSeparators(tempfile)), ze, se, zip_strerror(zip)); + return false; + } + return true; + +error_close_zip: + zip_close(zip); + QFile::remove(tempfile); + xsltFreeStylesheet(xslt); + return false; +} + +WebServices::WebServices(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), reply(0) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.download, SIGNAL(clicked(bool)), this, SLOT(startDownload())); + connect(ui.upload, SIGNAL(clicked(bool)), this, SLOT(startUpload())); + connect(&timeout, SIGNAL(timeout()), this, SLOT(downloadTimedOut())); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + timeout.setSingleShot(true); + defaultApplyText = ui.buttonBox->button(QDialogButtonBox::Apply)->text(); + userAgent = getUserAgent(); +} + +void WebServices::hidePassword() +{ + ui.password->hide(); + ui.passLabel->hide(); +} + +void WebServices::hideUpload() +{ + ui.upload->hide(); + ui.download->show(); +} + +void WebServices::hideDownload() +{ + ui.download->hide(); + ui.upload->show(); +} + +QNetworkAccessManager *WebServices::manager() +{ + static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + return manager; +} + +void WebServices::downloadTimedOut() +{ + if (!reply) + return; + + reply->deleteLater(); + reply = NULL; + resetState(); + ui.status->setText(tr("Operation timed out")); +} + +void WebServices::updateProgress(qint64 current, qint64 total) +{ + if (!reply) + return; + if (total == -1) { + total = INT_MAX / 2 - 1; + } + if (total >= INT_MAX / 2) { + // over a gigabyte! + if (total >= Q_INT64_C(1) << 47) { + total >>= 16; + current >>= 16; + } + total >>= 16; + current >>= 16; + } + ui.progressBar->setRange(0, total); + ui.progressBar->setValue(current); + ui.status->setText(tr("Transferring data...")); + + // reset the timer: 30 seconds after we last got any data + timeout.start(); +} + +void WebServices::connectSignalsForDownload(QNetworkReply *reply) +{ + connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, + SLOT(updateProgress(qint64, qint64))); + + timeout.start(30000); // 30s +} + +void WebServices::resetState() +{ + ui.download->setEnabled(true); + ui.upload->setEnabled(true); + ui.userID->setEnabled(true); + ui.password->setEnabled(true); + ui.progressBar->reset(); + ui.progressBar->setRange(0, 1); + ui.status->setText(QString()); + ui.buttonBox->button(QDialogButtonBox::Apply)->setText(defaultApplyText); +} + +// # +// # +// # Subsurface Web Service Implementation. +// # +// # + +SubsurfaceWebServices::SubsurfaceWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f) +{ + QSettings s; + if (!prefs.save_userid_local || !*prefs.userid) + ui.userID->setText(s.value("subsurface_webservice_uid").toString().toUpper()); + else + ui.userID->setText(prefs.userid); + hidePassword(); + hideUpload(); + ui.progressBar->setFormat(tr("Enter User ID and click Download")); + ui.progressBar->setRange(0, 1); + ui.progressBar->setValue(-1); + ui.progressBar->setAlignment(Qt::AlignCenter); + ui.saveUidLocal->setChecked(prefs.save_userid_local); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void SubsurfaceWebServices::buttonClicked(QAbstractButton *button) +{ + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + switch (ui.buttonBox->buttonRole(button)) { + case QDialogButtonBox::ApplyRole: { + int i; + struct dive *d; + struct dive_site *ds; + bool changed = false; + clear_table(&gps_location_table); + QByteArray url = tr("Webservice").toLocal8Bit(); + parse_xml_buffer(url.data(), downloadedData.data(), downloadedData.length(), &gps_location_table, NULL); + // make sure we mark all the dive sites that were created + for (i = 0; i < gps_location_table.nr; i++) { + d = get_dive_from_table(i, &gps_location_table); + ds = get_dive_site_by_uuid(d->dive_site_uuid); + if (ds) + ds->notes = strdup("SubsurfaceWebservice"); + } + /* now merge the data in the gps_location table into the dive_table */ + if (merge_locations_into_dives()) { + changed = true; + mark_divelist_changed(true); + MainWindow::instance()->information()->updateDiveInfo(); + } + + /* store last entered uid in config */ + QSettings s; + QString qDialogUid = ui.userID->text().toUpper(); + bool qSaveUid = ui.saveUidLocal->checkState(); + set_save_userid_local(qSaveUid); + if (qSaveUid) { + QString qSettingUid = s.value("subsurface_webservice_uid").toString(); + QString qFileUid = QString(prefs.userid); + bool s_eq_d = (qSettingUid == qDialogUid); + bool d_eq_f = (qDialogUid == qFileUid); + if (!d_eq_f || s_eq_d) + s.setValue("subsurface_webservice_uid", qDialogUid); + set_userid(qDialogUid.toLocal8Bit().data()); + } else { + s.setValue("subsurface_webservice_uid", qDialogUid); + } + s.sync(); + hide(); + close(); + resetState(); + /* and now clean up and remove all the extra dive sites that were created */ + QSet usedUuids; + for_each_dive(i, d) { + if (d->dive_site_uuid) + usedUuids.insert(d->dive_site_uuid); + } + for_each_dive_site(i, ds) { + if (!usedUuids.contains(ds->uuid) && same_string(ds->notes, "SubsurfaceWebservice")) { + delete_dive_site(ds->uuid); + i--; // otherwise we skip one site + } + } +#ifndef NO_MARBLE + // finally now that all the extra GPS fixes that weren't used have been deleted + // we can update the globe + if (changed) { + GlobeGPS::instance()->repopulateLabels(); + GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(current_dive->dive_site_uuid)); + } +#endif + + } break; + case QDialogButtonBox::RejectRole: + if (reply != NULL && reply->isOpen()) { + reply->abort(); + delete reply; + reply = NULL; + } + resetState(); + break; + case QDialogButtonBox::HelpRole: + QDesktopServices::openUrl(QUrl("http://api.hohndel.org")); + break; + default: + break; + } +} + +void SubsurfaceWebServices::startDownload() +{ + QUrl url("http://api.hohndel.org/api/dive/get/"); + QUrlQuery query; + query.addQueryItem("login", ui.userID->text().toUpper()); + url.setQuery(query); + + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("Accept", "text/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + reply = manager()->get(request); + ui.status->setText(tr("Connecting...")); + ui.progressBar->setEnabled(true); + ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' + ui.download->setEnabled(false); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + connectSignalsForDownload(reply); +} + +void SubsurfaceWebServices::downloadFinished() +{ + if (!reply) + return; + + ui.progressBar->setRange(0, 1); + ui.progressBar->setValue(1); + ui.progressBar->setFormat("%p%"); + downloadedData = reply->readAll(); + + ui.download->setEnabled(true); + ui.status->setText(tr("Download finished")); + + uint resultCode = download_dialog_parse_response(downloadedData); + setStatusText(resultCode); + if (resultCode == DD_STATUS_OK) { + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + } + reply->deleteLater(); + reply = NULL; +} + +void SubsurfaceWebServices::downloadError(QNetworkReply::NetworkError) +{ + resetState(); + ui.status->setText(tr("Download error: %1").arg(reply->errorString())); + reply->deleteLater(); + reply = NULL; +} + +void SubsurfaceWebServices::setStatusText(int status) +{ + QString text; + switch (status) { + case DD_STATUS_ERROR_CONNECT: + text = tr("Connection error: "); + break; + case DD_STATUS_ERROR_ID: + text = tr("Invalid user identifier!"); + break; + case DD_STATUS_ERROR_PARSE: + text = tr("Cannot parse response!"); + break; + case DD_STATUS_OK: + text = tr("Download successful"); + break; + } + ui.status->setText(text); +} + +//TODO: C-Code. +/* requires that there is a or tag under the tag */ +void SubsurfaceWebServices::download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status) +{ + xmlNodePtr cur_node; + for (cur_node = node; cur_node; cur_node = cur_node->next) { + if ((!strcmp((const char *)cur_node->name, (const char *)"download")) && + (!strcmp((const char *)xmlNodeGetContent(cur_node), (const char *)"ok"))) { + *download_status = DD_STATUS_OK; + return; + } else if (!strcmp((const char *)cur_node->name, (const char *)"error")) { + *download_status = DD_STATUS_ERROR_ID; + return; + } + } +} + +// TODO: C-Code +unsigned int SubsurfaceWebServices::download_dialog_parse_response(const QByteArray &xml) +{ + xmlNodePtr root; + xmlDocPtr doc = xmlParseMemory(xml.data(), xml.length()); + unsigned int status = DD_STATUS_ERROR_PARSE; + + if (!doc) + return DD_STATUS_ERROR_PARSE; + root = xmlDocGetRootElement(doc); + if (!root) { + status = DD_STATUS_ERROR_PARSE; + goto end; + } + if (root->children) + download_dialog_traverse_xml(root->children, &status); +end: + xmlFreeDoc(doc); + return status; +} + +// # +// # +// # Divelogs DE Web Service Implementation. +// # +// # + +struct DiveListResult { + QString errorCondition; + QString errorDetails; + QByteArray idList; // comma-separated, suitable to be sent in the fetch request + int idCount; +}; + +static DiveListResult parseDiveLogsDeDiveList(const QByteArray &xmlData) +{ + /* XML format seems to be: + * + * + * DD.MM.YYYY hh:mm + * [repeat ] + * + * + */ + QXmlStreamReader reader(xmlData); + const QString invalidXmlError = QObject::tr("Invalid response from server"); + bool seenDiveDates = false; + DiveListResult result; + result.idCount = 0; + + if (reader.readNextStartElement() && reader.name() != "DiveDateReader") { + result.errorCondition = invalidXmlError; + result.errorDetails = + QObject::tr("Expected XML tag 'DiveDateReader', got instead '%1") + .arg(reader.name().toString()); + goto out; + } + + while (reader.readNextStartElement()) { + if (reader.name() != "DiveDates") { + if (reader.name() == "Login") { + QString status = reader.readElementText(); + // qDebug() << "Login status:" << status; + + // Note: there has to be a better way to determine a successful login... + if (status == "failed") { + result.errorCondition = "Login failed"; + goto out; + } + } else { + // qDebug() << "Skipping" << reader.name(); + } + continue; + } + + // process + seenDiveDates = true; + while (reader.readNextStartElement()) { + if (reader.name() != "date") { + // qDebug() << "Skipping" << reader.name(); + continue; + } + QStringRef id = reader.attributes().value("divelogsId"); + // qDebug() << "Found" << reader.name() << "with id =" << id; + if (!id.isEmpty()) { + result.idList += id.toLatin1(); + result.idList += ','; + ++result.idCount; + } + + reader.skipCurrentElement(); + } + } + + // chop the ending comma, if any + result.idList.chop(1); + + if (!seenDiveDates) { + result.errorCondition = invalidXmlError; + result.errorDetails = QObject::tr("Expected XML tag 'DiveDates' not found"); + } + +out: + if (reader.hasError()) { + // if there was an XML error, overwrite the result or other error conditions + result.errorCondition = invalidXmlError; + result.errorDetails = QObject::tr("Malformed XML response. Line %1: %2") + .arg(reader.lineNumber()) + .arg(reader.errorString()); + } + return result; +} + +DivelogsDeWebServices *DivelogsDeWebServices::instance() +{ + static DivelogsDeWebServices *self = new DivelogsDeWebServices(MainWindow::instance()); + self->setAttribute(Qt::WA_QuitOnClose, false); + return self; +} + +void DivelogsDeWebServices::downloadDives() +{ + uploadMode = false; + resetState(); + hideUpload(); + exec(); +} + +void DivelogsDeWebServices::prepareDivesForUpload(bool selected) +{ + /* generate a random filename and create/open that file with zip_open */ + QString filename = QDir::tempPath() + "/import-" + QString::number(qrand() % 99999999) + ".dld"; + if (prepare_dives_for_divelogs(filename, selected)) { + QFile f(filename); + if (f.open(QIODevice::ReadOnly)) { + uploadDives((QIODevice *)&f); + f.close(); + f.remove(); + return; + } else { + report_error("Failed to open upload file %s\n", qPrintable(filename)); + } + } else { + report_error("Failed to create upload file %s\n", qPrintable(filename)); + } + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); +} + +void DivelogsDeWebServices::uploadDives(QIODevice *dldContent) +{ + QHttpMultiPart mp(QHttpMultiPart::FormDataType); + QHttpPart part; + QFile *f = (QFile *)dldContent; + QFileInfo fi(*f); + QString args("form-data; name=\"userfile\"; filename=\"" + fi.absoluteFilePath() + "\""); + part.setRawHeader("Content-Disposition", args.toLatin1()); + part.setBodyDevice(dldContent); + mp.append(part); + + multipart = ∓ + hideDownload(); + resetState(); + uploadMode = true; + ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done")); + exec(); + + multipart = NULL; + if (reply != NULL && reply->isOpen()) { + reply->abort(); + delete reply; + reply = NULL; + } +} + +DivelogsDeWebServices::DivelogsDeWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f), + multipart(NULL), + uploadMode(false) +{ + QSettings s; + ui.userID->setText(s.value("divelogde_user").toString()); + ui.password->setText(s.value("divelogde_pass").toString()); + ui.saveUidLocal->hide(); + hideUpload(); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void DivelogsDeWebServices::startUpload() +{ + QSettings s; + s.setValue("divelogde_user", ui.userID->text()); + s.setValue("divelogde_pass", ui.password->text()); + s.sync(); + + ui.status->setText(tr("Uploading dive list...")); + ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' + ui.upload->setEnabled(false); + ui.userID->setEnabled(false); + ui.password->setEnabled(false); + + QNetworkRequest request; + request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php")); + request.setRawHeader("Accept", "text/xml, application/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + + QHttpPart part; + part.setRawHeader("Content-Disposition", "form-data; name=\"user\""); + part.setBody(ui.userID->text().toUtf8()); + multipart->append(part); + + part.setRawHeader("Content-Disposition", "form-data; name=\"pass\""); + part.setBody(ui.password->text().toUtf8()); + multipart->append(part); + + reply = manager()->post(request, multipart); + connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(uploadError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this, + SLOT(updateProgress(qint64, qint64))); + + timeout.start(30000); // 30s +} + +void DivelogsDeWebServices::startDownload() +{ + ui.status->setText(tr("Downloading dive list...")); + ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' + ui.download->setEnabled(false); + ui.userID->setEnabled(false); + ui.password->setEnabled(false); + + QNetworkRequest request; + request.setUrl(QUrl("https://divelogs.de/xml_available_dives.php")); + request.setRawHeader("Accept", "text/xml, application/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QUrlQuery body; + body.addQueryItem("user", ui.userID->text()); + body.addQueryItem("pass", ui.password->text()); + + reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); + connect(reply, SIGNAL(finished()), this, SLOT(listDownloadFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(downloadError(QNetworkReply::NetworkError))); + + timeout.start(30000); // 30s +} + +void DivelogsDeWebServices::listDownloadFinished() +{ + if (!reply) + return; + QByteArray xmlData = reply->readAll(); + reply->deleteLater(); + reply = NULL; + + // parse the XML data we downloaded + DiveListResult diveList = parseDiveLogsDeDiveList(xmlData); + if (!diveList.errorCondition.isEmpty()) { + // error condition + resetState(); + ui.status->setText(diveList.errorCondition); + return; + } + + ui.status->setText(tr("Downloading %1 dives...").arg(diveList.idCount)); + + QNetworkRequest request; + request.setUrl(QUrl("https://divelogs.de/DivelogsDirectExport.php")); + request.setRawHeader("Accept", "application/zip, */*"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QUrlQuery body; + body.addQueryItem("user", ui.userID->text()); + body.addQueryItem("pass", ui.password->text()); + body.addQueryItem("ids", diveList.idList); + + reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); + connect(reply, SIGNAL(readyRead()), this, SLOT(saveToZipFile())); + connectSignalsForDownload(reply); +} + +void DivelogsDeWebServices::saveToZipFile() +{ + if (!zipFile.isOpen()) { + zipFile.setFileTemplate(QDir::tempPath() + "/import-XXXXXX.dld"); + zipFile.open(); + } + + zipFile.write(reply->readAll()); +} + +void DivelogsDeWebServices::downloadFinished() +{ + if (!reply) + return; + + ui.download->setEnabled(true); + ui.status->setText(tr("Download finished - %1").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString())); + reply->deleteLater(); + reply = NULL; + + int errorcode; + zipFile.seek(0); +#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR) + int duppedfd = dup(zipFile.handle()); + struct zip *zip = NULL; + if (duppedfd >= 0) { + zip = zip_fdopen(duppedfd, 0, &errorcode); + if (!zip) + ::close(duppedfd); + } else { + QMessageBox::critical(this, tr("Problem with download"), + tr("The archive could not be opened:\n")); + return; + } +#else + struct zip *zip = zip_open(QFile::encodeName(zipFile.fileName()), 0, &errorcode); +#endif + if (!zip) { + char buf[512]; + zip_error_to_str(buf, sizeof(buf), errorcode, errno); + QMessageBox::critical(this, tr("Corrupted download"), + tr("The archive could not be opened:\n%1").arg(QString::fromLocal8Bit(buf))); + zipFile.close(); + return; + } + // now allow the user to cancel or accept + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + + zip_close(zip); + zipFile.close(); +#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR) + ::close(duppedfd); +#endif +} + +void DivelogsDeWebServices::uploadFinished() +{ + if (!reply) + return; + + ui.progressBar->setRange(0, 1); + ui.upload->setEnabled(true); + ui.userID->setEnabled(true); + ui.password->setEnabled(true); + ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done")); + ui.status->setText(tr("Upload finished")); + + // check what the server sent us: it might contain + // an error condition, such as a failed login + QByteArray xmlData = reply->readAll(); + reply->deleteLater(); + reply = NULL; + char *resp = xmlData.data(); + if (resp) { + char *parsed = strstr(resp, ""); + if (parsed) { + if (strstr(resp, "succeeded")) { + if (strstr(resp, "failed")) { + ui.status->setText(tr("Upload failed")); + return; + } + ui.status->setText(tr("Upload successful")); + return; + } + ui.status->setText(tr("Login failed")); + return; + } + ui.status->setText(tr("Cannot parse response")); + } +} + +void DivelogsDeWebServices::setStatusText(int status) +{ +} + +void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError) +{ + resetState(); + ui.status->setText(tr("Error: %1").arg(reply->errorString())); + reply->deleteLater(); + reply = NULL; +} + +void DivelogsDeWebServices::uploadError(QNetworkReply::NetworkError error) +{ + downloadError(error); +} + +void DivelogsDeWebServices::buttonClicked(QAbstractButton *button) +{ + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + switch (ui.buttonBox->buttonRole(button)) { + case QDialogButtonBox::ApplyRole: { + /* in 'uploadMode' button is called 'Done' and closes the dialog */ + if (uploadMode) { + hide(); + close(); + resetState(); + break; + } + /* parse file and import dives */ + parse_file(QFile::encodeName(zipFile.fileName())); + process_dives(true, false); + MainWindow::instance()->refreshDisplay(); + + /* store last entered user/pass in config */ + QSettings s; + s.setValue("divelogde_user", ui.userID->text()); + s.setValue("divelogde_pass", ui.password->text()); + s.sync(); + hide(); + close(); + resetState(); + } break; + case QDialogButtonBox::RejectRole: + // these two seem to be causing a crash: + // reply->deleteLater(); + resetState(); + break; + case QDialogButtonBox::HelpRole: + QDesktopServices::openUrl(QUrl("http://divelogs.de")); + break; + default: + break; + } +} + +UserSurveyServices::UserSurveyServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f) +{ + +} + +QNetworkReply* UserSurveyServices::sendSurvey(QString values) +{ + QNetworkRequest request; + request.setUrl(QString("http://subsurface-divelog.org/survey?%1").arg(values)); + request.setRawHeader("Accept", "text/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + reply = manager()->get(request); + return reply; +} + +CloudStorageAuthenticate::CloudStorageAuthenticate(QObject *parent) : + QObject(parent), + reply(NULL) +{ + userAgent = getUserAgent(); +} + +#define CLOUDURL QString(prefs.cloud_base_url) +#define CLOUDBACKENDSTORAGE CLOUDURL + "/storage" +#define CLOUDBACKENDVERIFY CLOUDURL + "/verify" +#define CLOUDBACKENDUPDATE CLOUDURL + "/update" + +QNetworkReply* CloudStorageAuthenticate::backend(QString email, QString password, QString pin, QString newpasswd) +{ + QString payload(email + " " + password); + QUrl requestUrl; + if (pin == "" && newpasswd == "") { + requestUrl = QUrl(CLOUDBACKENDSTORAGE); + } else if (newpasswd != "") { + requestUrl = QUrl(CLOUDBACKENDUPDATE); + payload += " " + newpasswd; + } else { + requestUrl = QUrl(CLOUDBACKENDVERIFY); + payload += " " + pin; + } + QNetworkRequest *request = new QNetworkRequest(requestUrl); + request->setRawHeader("Accept", "text/xml, text/plain"); + request->setRawHeader("User-Agent", userAgent.toUtf8()); + request->setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + reply = WebServices::manager()->post(*request, qPrintable(payload)); + connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(uploadError(QNetworkReply::NetworkError))); + return reply; +} + +void CloudStorageAuthenticate::uploadFinished() +{ + static QString myLastError; + + QString cloudAuthReply(reply->readAll()); + qDebug() << "Completed connection with cloud storage backend, response" << cloudAuthReply; + if (cloudAuthReply == "[VERIFIED]" || cloudAuthReply == "[OK]") { + prefs.cloud_verification_status = CS_VERIFIED; + NotificationWidget *nw = MainWindow::instance()->getNotificationWidget(); + if (nw->getNotificationText() == myLastError) + nw->hideNotification(); + myLastError.clear(); + } else if (cloudAuthReply == "[VERIFY]") { + prefs.cloud_verification_status = CS_NEED_TO_VERIFY; + } else if (cloudAuthReply == "[PASSWDCHANGED]") { + free(prefs.cloud_storage_password); + prefs.cloud_storage_password = prefs.cloud_storage_newpassword; + prefs.cloud_storage_newpassword = NULL; + emit passwordChangeSuccessful(); + return; + } else { + prefs.cloud_verification_status = CS_INCORRECT_USER_PASSWD; + myLastError = cloudAuthReply; + report_error("%s", qPrintable(cloudAuthReply)); + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + } + emit finishedAuthenticate(); +} + +void CloudStorageAuthenticate::uploadError(QNetworkReply::NetworkError error) +{ + qDebug() << "Received error response from cloud storage backend:" << reply->errorString(); +} + +void CloudStorageAuthenticate::sslErrors(QList errorList) +{ + if (verbose) { + qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; + Q_FOREACH (QSslError err, errorList) { + qDebug() << err.errorString(); + } + } + QSslConfiguration conf = reply->sslConfiguration(); + QSslCertificate cert = conf.peerCertificate(); + QByteArray hexDigest = cert.digest().toHex(); + if (reply->url().toString().contains(prefs.cloud_base_url) && + hexDigest == "13ff44c62996cfa5cd69d6810675490e") { + if (verbose) + qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; + reply->ignoreSslErrors(); + } else { + if (verbose) + qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; + } +} diff --git a/desktop-widgets/subsurfacewebservices.h b/desktop-widgets/subsurfacewebservices.h new file mode 100644 index 000000000..2b454ebc7 --- /dev/null +++ b/desktop-widgets/subsurfacewebservices.h @@ -0,0 +1,142 @@ +#ifndef SUBSURFACEWEBSERVICES_H +#define SUBSURFACEWEBSERVICES_H + +#include +#include +#include +#include +#include + +#include "ui_webservices.h" + +class QAbstractButton; +class QHttpMultiPart; + +class WebServices : public QDialog { + Q_OBJECT +public: + explicit WebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); + void hidePassword(); + void hideUpload(); + void hideDownload(); + + static QNetworkAccessManager *manager(); + +private +slots: + virtual void startDownload() = 0; + virtual void startUpload() = 0; + virtual void buttonClicked(QAbstractButton *button) = 0; + virtual void downloadTimedOut(); + +protected +slots: + void updateProgress(qint64 current, qint64 total); + +protected: + void resetState(); + void connectSignalsForDownload(QNetworkReply *reply); + void connectSignalsForUpload(); + + Ui::WebServices ui; + QNetworkReply *reply; + QTimer timeout; + QByteArray downloadedData; + QString defaultApplyText; + QString userAgent; +}; + +class SubsurfaceWebServices : public WebServices { + Q_OBJECT +public: + explicit SubsurfaceWebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); + +private +slots: + void startDownload(); + void buttonClicked(QAbstractButton *button); + void downloadFinished(); + void downloadError(QNetworkReply::NetworkError error); + void startUpload() + { + } /*no op*/ +private: + void setStatusText(int status); + void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status); + unsigned int download_dialog_parse_response(const QByteArray &length); +}; + +class DivelogsDeWebServices : public WebServices { + Q_OBJECT +public: + static DivelogsDeWebServices *instance(); + void downloadDives(); + void prepareDivesForUpload(bool selected); + +private +slots: + void startDownload(); + void buttonClicked(QAbstractButton *button); + void saveToZipFile(); + void listDownloadFinished(); + void downloadFinished(); + void uploadFinished(); + void downloadError(QNetworkReply::NetworkError error); + void uploadError(QNetworkReply::NetworkError error); + void startUpload(); + +private: + void uploadDives(QIODevice *dldContent); + explicit DivelogsDeWebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); + void setStatusText(int status); + bool prepare_dives_for_divelogs(const QString &filename, bool selected); + void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status); + unsigned int download_dialog_parse_response(const QByteArray &length); + + QHttpMultiPart *multipart; + QTemporaryFile zipFile; + bool uploadMode; +}; + +class UserSurveyServices : public WebServices { + Q_OBJECT +public: + QNetworkReply* sendSurvey(QString values); + explicit UserSurveyServices(QWidget *parent = 0, Qt::WindowFlags f = 0); +private +slots: + // need to declare them as no ops or Qt4 is unhappy + virtual void startDownload() { } + virtual void startUpload() { } + virtual void buttonClicked(QAbstractButton *button) { } +}; + +class CloudStorageAuthenticate : public QObject { + Q_OBJECT +public: + QNetworkReply* backend(QString email, QString password, QString pin = "", QString newpasswd = ""); + explicit CloudStorageAuthenticate(QObject *parent); +signals: + void finishedAuthenticate(); + void passwordChangeSuccessful(); +private +slots: + void uploadError(QNetworkReply::NetworkError error); + void sslErrors(QList errorList); + void uploadFinished(); +private: + QNetworkReply *reply; + QString userAgent; + +}; + +#ifdef __cplusplus +extern "C" { +#endif +extern void set_save_userid_local(short value); +extern void set_userid(char *user_id); +#ifdef __cplusplus +} +#endif + +#endif // SUBSURFACEWEBSERVICES_H diff --git a/desktop-widgets/tableview.cpp b/desktop-widgets/tableview.cpp new file mode 100644 index 000000000..40d5199ec --- /dev/null +++ b/desktop-widgets/tableview.cpp @@ -0,0 +1,145 @@ +#include "tableview.h" +#include "modeldelegates.h" + +#include +#include + +TableView::TableView(QWidget *parent) : QGroupBox(parent) +{ + ui.setupUi(this); + ui.tableView->setItemDelegate(new DiveListDelegate(this)); + + QFontMetrics fm(defaultModelFont()); + int text_ht = fm.height(); + + metrics.icon = &defaultIconMetrics(); + + metrics.rm_col_width = metrics.icon->sz_small + 2*metrics.icon->spacing; + metrics.header_ht = text_ht + 10; // TODO DPI + + /* We want to get rid of the margin around the table, but + * we must be careful with some styles (e.g. GTK+) where the top + * margin is actually used to hold the label. We thus check the + * rectangles for the label and contents to make sure they do not + * overlap, and adjust the top contentsMargin accordingly + */ + + // start by setting all the margins at zero + QMargins margins; + + // grab the label and contents dimensions and positions + QStyleOptionGroupBox option; + initStyleOption(&option); + QRect labelRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); + QRect contentsRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxContents, this); + + /* we need to ensure that the bottom of the label is higher + * than the top of the contents */ + int delta = contentsRect.top() - labelRect.bottom(); + const int min_gap = metrics.icon->spacing; + if (delta <= min_gap) { + margins.setTop(min_gap - delta); + } + layout()->setContentsMargins(margins); + + QIcon plusIcon(":plus"); + plusBtn = new QPushButton(plusIcon, QString(), this); + plusBtn->setFlat(true); + + /* now determine the icon and button size. Since the button will be + * placed in the label, make sure that we do not overflow, as it might + * get clipped + */ + int iconSize = metrics.icon->sz_small; + int btnSize = iconSize + 2*min_gap; + if (btnSize > labelRect.height()) { + btnSize = labelRect.height(); + iconSize = btnSize - 2*min_gap; + } + plusBtn->setIconSize(QSize(iconSize, iconSize)); + plusBtn->resize(btnSize, btnSize); + connect(plusBtn, SIGNAL(clicked(bool)), this, SIGNAL(addButtonClicked())); +} + +TableView::~TableView() +{ + QSettings s; + s.beginGroup(objectName()); + // remove the old default + bool oldDefault = (ui.tableView->columnWidth(0) == 30); + for (int i = 1; oldDefault && i < ui.tableView->model()->columnCount(); i++) { + if (ui.tableView->columnWidth(i) != 80) + oldDefault = false; + } + if (oldDefault) { + s.remove(""); + } else { + for (int i = 0; i < ui.tableView->model()->columnCount(); i++) { + if (ui.tableView->columnWidth(i) == defaultColumnWidth(i)) + s.remove(QString("colwidth%1").arg(i)); + else + s.setValue(QString("colwidth%1").arg(i), ui.tableView->columnWidth(i)); + } + } + s.endGroup(); +} + +void TableView::setBtnToolTip(const QString &tooltip) +{ + plusBtn->setToolTip(tooltip); +} + +void TableView::setModel(QAbstractItemModel *model) +{ + ui.tableView->setModel(model); + connect(ui.tableView, SIGNAL(clicked(QModelIndex)), model, SLOT(remove(QModelIndex))); + + QSettings s; + s.beginGroup(objectName()); + const int columnCount = ui.tableView->model()->columnCount(); + for (int i = 0; i < columnCount; i++) { + QVariant width = s.value(QString("colwidth%1").arg(i), defaultColumnWidth(i)); + ui.tableView->setColumnWidth(i, width.toInt()); + } + s.endGroup(); + + ui.tableView->horizontalHeader()->setMinimumHeight(metrics.header_ht); +} + +void TableView::fixPlusPosition() +{ + QStyleOptionGroupBox option; + initStyleOption(&option); + QRect labelRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); + QRect contentsRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::QStyle::SC_GroupBoxFrame, this); + plusBtn->setGeometry( contentsRect.width() - plusBtn->width(), labelRect.y(), plusBtn->width(), labelRect.height()); +} + +// We need to manually position the 'plus' on cylinder and weight. +void TableView::resizeEvent(QResizeEvent *event) +{ + fixPlusPosition(); + QWidget::resizeEvent(event); +} + +void TableView::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + fixPlusPosition(); +} + +void TableView::edit(const QModelIndex &index) +{ + ui.tableView->edit(index); +} + +int TableView::defaultColumnWidth(int col) +{ + QString text = ui.tableView->model()->headerData(col, Qt::Horizontal).toString(); + return text.isEmpty() ? metrics.rm_col_width : defaultModelFontMetrics().width(text) + 4; // add small margin +} + +QTableView *TableView::view() +{ + return ui.tableView; +} diff --git a/desktop-widgets/tableview.h b/desktop-widgets/tableview.h new file mode 100644 index 000000000..f72b256ea --- /dev/null +++ b/desktop-widgets/tableview.h @@ -0,0 +1,54 @@ +#ifndef TABLEVIEW_H +#define TABLEVIEW_H + +/* This TableView is prepared to have the CSS, + * the methods to restore / save the state of + * the column widths and the 'plus' button. + */ +#include + +#include "ui_tableview.h" + +#include "metrics.h" + +class QPushButton; +class QAbstractItemModel; +class QModelIndex; +class QTableView; + +class TableView : public QGroupBox { + Q_OBJECT + + struct TableMetrics { + const IconMetrics* icon; // icon metrics + int rm_col_width; // column width of REMOVE column + int header_ht; // height of the header + }; +public: + TableView(QWidget *parent = 0); + virtual ~TableView(); + /* The model is expected to have a 'remove' slot, that takes a QModelIndex as parameter. + * It's also expected to have the column '1' as a trash icon. I most probably should create a + * proxy model and add that column, will mark that as TODO. see? marked. + */ + void setModel(QAbstractItemModel *model); + void setBtnToolTip(const QString &tooltip); + void fixPlusPosition(); + void edit(const QModelIndex &index); + int defaultColumnWidth(int col); // default column width for column col + QTableView *view(); + +protected: + virtual void showEvent(QShowEvent *); + virtual void resizeEvent(QResizeEvent *); + +signals: + void addButtonClicked(); + +private: + Ui::TableView ui; + QPushButton *plusBtn; + TableMetrics metrics; +}; + +#endif // TABLEVIEW_H diff --git a/desktop-widgets/tableview.ui b/desktop-widgets/tableview.ui new file mode 100644 index 000000000..73867231e --- /dev/null +++ b/desktop-widgets/tableview.ui @@ -0,0 +1,27 @@ + + + TableView + + + + 0 + 0 + 400 + 300 + + + + GroupBox + + + GroupBox + + + + + + + + + + diff --git a/desktop-widgets/tagwidget.cpp b/desktop-widgets/tagwidget.cpp new file mode 100644 index 000000000..3b61b492a --- /dev/null +++ b/desktop-widgets/tagwidget.cpp @@ -0,0 +1,210 @@ +#include "tagwidget.h" +#include "mainwindow.h" +#include "maintab.h" +#include + +TagWidget::TagWidget(QWidget *parent) : GroupedLineEdit(parent), m_completer(NULL), lastFinishedTag(false) +{ + connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(reparse())); + connect(this, SIGNAL(textChanged()), this, SLOT(reparse())); + + QColor textColor = palette().color(QPalette::Text); + qreal h, s, l, a; + textColor.getHslF(&h, &s, &l, &a); + // I use dark themes + if (l <= 0.3) { // very dark text. get a brigth background + addColor(QColor(Qt::red).lighter(120)); + addColor(QColor(Qt::green).lighter(120)); + addColor(QColor(Qt::blue).lighter(120)); + } else if (l <= 0.6) { // moderated dark text. get a somewhat bright background + addColor(QColor(Qt::red).lighter(60)); + addColor(QColor(Qt::green).lighter(60)); + addColor(QColor(Qt::blue).lighter(60)); + } else { + addColor(QColor(Qt::red).darker(120)); + addColor(QColor(Qt::green).darker(120)); + addColor(QColor(Qt::blue).darker(120)); + } // light text. get a dark background. + setFocusPolicy(Qt::StrongFocus); +} + +void TagWidget::setCompleter(QCompleter *completer) +{ + m_completer = completer; + m_completer->setWidget(this); + connect(m_completer, SIGNAL(activated(QString)), this, SLOT(completionSelected(QString))); + connect(m_completer, SIGNAL(highlighted(QString)), this, SLOT(completionHighlighted(QString))); +} + +QPair TagWidget::getCursorTagPosition() +{ + int i = 0, start = 0, end = 0; + /* Parse string near cursor */ + i = cursorPosition(); + while (--i > 0) { + if (text().at(i) == ',') { + if (i > 0 && text().at(i - 1) != '\\') { + i++; + break; + } + } + } + start = i; + while (++i < text().length()) { + if (text().at(i) == ',') { + if (i > 0 && text().at(i - 1) != '\\') + break; + } + } + end = i; + if (start < 0 || end < 0) { + start = 0; + end = 0; + } + return qMakePair(start, end); +} + +void TagWidget::highlight() +{ + removeAllBlocks(); + int lastPos = 0; + Q_FOREACH (const QString& s, text().split(QChar(','), QString::SkipEmptyParts)) { + QString trimmed = s.trimmed(); + if (trimmed.isEmpty()) + continue; + int start = text().indexOf(trimmed, lastPos); + addBlock(start, trimmed.size() + start); + lastPos = trimmed.size() + start; + } +} + +void TagWidget::reparse() +{ + highlight(); + QPair pos = getCursorTagPosition(); + QString currentText; + if (pos.first >= 0 && pos.second > 0) + currentText = text().mid(pos.first, pos.second - pos.first).trimmed(); + + /* + * Do not show the completer when not in edit mode - basically + * this returns when we are accepting or discarding the changes. + */ + if (MainWindow::instance()->information()->isEditing() == false || currentText.length() == 0) { + return; + } + + if (m_completer) { + m_completer->setCompletionPrefix(currentText); + if (m_completer->completionCount() == 1) { + if (m_completer->currentCompletion() == currentText) { + QAbstractItemView *popup = m_completer->popup(); + if (popup) + popup->hide(); + } else { + m_completer->complete(); + } + } else { + m_completer->complete(); + } + } +} + +void TagWidget::completionSelected(const QString &completion) +{ + completionHighlighted(completion); + emit textChanged(); +} + +void TagWidget::completionHighlighted(const QString &completion) +{ + QPair pos = getCursorTagPosition(); + setText(text().remove(pos.first, pos.second - pos.first).insert(pos.first, completion)); + setCursorPosition(pos.first + completion.length()); +} + +void TagWidget::setCursorPosition(int position) +{ + blockSignals(true); + GroupedLineEdit::setCursorPosition(position); + blockSignals(false); +} + +void TagWidget::setText(const QString &text) +{ + blockSignals(true); + GroupedLineEdit::setText(text); + blockSignals(false); + highlight(); +} + +void TagWidget::clear() +{ + blockSignals(true); + GroupedLineEdit::clear(); + blockSignals(false); +} + +void TagWidget::keyPressEvent(QKeyEvent *e) +{ + QPair pos; + QAbstractItemView *popup; + bool finishedTag = false; + switch (e->key()) { + case Qt::Key_Escape: + pos = getCursorTagPosition(); + if (pos.first >= 0 && pos.second > 0) { + setText(text().remove(pos.first, pos.second - pos.first)); + setCursorPosition(pos.first); + } + popup = m_completer->popup(); + if (popup) + popup->hide(); + return; + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Tab: + /* + * Fake the QLineEdit behaviour by simply + * closing the QAbstractViewitem + */ + if (m_completer) { + popup = m_completer->popup(); + if (popup) + popup->hide(); + } + finishedTag = true; + break; + case Qt::Key_Comma: { /* if this is the last key, and the previous string is empty, ignore the comma. */ + QString temp = text(); + if (temp.split(QChar(',')).last().trimmed().isEmpty()){ + e->ignore(); + return; + } + } + } + if (e->key() == Qt::Key_Tab && lastFinishedTag) { // if we already end in comma, go to next/prev field + MainWindow::instance()->information()->nextInputField(e); // by sending the key event to the MainTab widget + } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Return) { // otherwise let's pretend this is a comma instead + QKeyEvent fakeEvent(e->type(), Qt::Key_Comma, e->modifiers(), QString(",")); + keyPressEvent(&fakeEvent); + } else { + GroupedLineEdit::keyPressEvent(e); + } + lastFinishedTag = finishedTag; +} + +void TagWidget::wheelEvent(QWheelEvent *event) +{ + if (hasFocus()) { + GroupedLineEdit::wheelEvent(event); + } +} + +void TagWidget::fixPopupPosition(int delta) +{ + if(m_completer->popup()->isVisible()){ + QRect toGlobal = m_completer->popup()->geometry(); + m_completer->popup()->setGeometry(toGlobal.x(), toGlobal.y() + delta +10, toGlobal.width(), toGlobal.height()); + } +} diff --git a/desktop-widgets/tagwidget.h b/desktop-widgets/tagwidget.h new file mode 100644 index 000000000..6a16129f3 --- /dev/null +++ b/desktop-widgets/tagwidget.h @@ -0,0 +1,34 @@ +#ifndef TAGWIDGET_H +#define TAGWIDGET_H + +#include "groupedlineedit.h" +#include + +class QCompleter; + +class TagWidget : public GroupedLineEdit { + Q_OBJECT +public: + explicit TagWidget(QWidget *parent = 0); + void setCompleter(QCompleter *completer); + QPair getCursorTagPosition(); + void highlight(); + void setText(const QString &text); + void clear(); + void setCursorPosition(int position); + void wheelEvent(QWheelEvent *event); + void fixPopupPosition(int delta); +public +slots: + void reparse(); + void completionSelected(const QString &text); + void completionHighlighted(const QString &text); + +protected: + void keyPressEvent(QKeyEvent *e); +private: + QCompleter *m_completer; + bool lastFinishedTag; +}; + +#endif // TAGWIDGET_H diff --git a/desktop-widgets/templateedit.cpp b/desktop-widgets/templateedit.cpp new file mode 100644 index 000000000..4964016b9 --- /dev/null +++ b/desktop-widgets/templateedit.cpp @@ -0,0 +1,227 @@ +#include "templateedit.h" +#include "printoptions.h" +#include "printer.h" +#include "ui_templateedit.h" + +#include +#include + +TemplateEdit::TemplateEdit(QWidget *parent, struct print_options *printOptions, struct template_options *templateOptions) : + QDialog(parent), + ui(new Ui::TemplateEdit) +{ + ui->setupUi(this); + this->templateOptions = templateOptions; + newTemplateOptions = *templateOptions; + this->printOptions = printOptions; + + // restore the settings and init the UI + ui->fontSelection->setCurrentIndex(templateOptions->font_index); + ui->fontsize->setValue(templateOptions->font_size); + ui->colorpalette->setCurrentIndex(templateOptions->color_palette_index); + ui->linespacing->setValue(templateOptions->line_spacing); + + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + if (printOptions->type == print_options::DIVELIST) + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + else if (printOptions->type == print_options::STATISTICS) + grantlee_template = TemplateLayout::readTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template); + + // gui + btnGroup = new QButtonGroup; + btnGroup->addButton(ui->editButton1, 1); + btnGroup->addButton(ui->editButton2, 2); + btnGroup->addButton(ui->editButton3, 3); + btnGroup->addButton(ui->editButton4, 4); + btnGroup->addButton(ui->editButton5, 5); + btnGroup->addButton(ui->editButton6, 6); + connect(btnGroup, SIGNAL(buttonClicked(QAbstractButton*)), this, SLOT(colorSelect(QAbstractButton*))); + + ui->plainTextEdit->setPlainText(grantlee_template); + editingCustomColors = false; + updatePreview(); +} + +TemplateEdit::~TemplateEdit() +{ + delete btnGroup; + delete ui; +} + +void TemplateEdit::updatePreview() +{ + // update Qpixmap preview + int width = ui->label->width(); + int height = ui->label->height(); + QPixmap map(width * 2, height * 2); + map.fill(QColor::fromRgb(255, 255, 255)); + Printer printer(&map, printOptions, &newTemplateOptions, Printer::PREVIEW); + printer.previewOnePage(); + ui->label->setPixmap(map.scaled(width, height, Qt::IgnoreAspectRatio)); + + // update colors tab + ui->colorLable1->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color1.name() + "\";}"); + ui->colorLable2->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color2.name() + "\";}"); + ui->colorLable3->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color3.name() + "\";}"); + ui->colorLable4->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color4.name() + "\";}"); + ui->colorLable5->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color5.name() + "\";}"); + ui->colorLable6->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color6.name() + "\";}"); + + ui->colorLable1->setText(newTemplateOptions.color_palette.color1.name()); + ui->colorLable2->setText(newTemplateOptions.color_palette.color2.name()); + ui->colorLable3->setText(newTemplateOptions.color_palette.color3.name()); + ui->colorLable4->setText(newTemplateOptions.color_palette.color4.name()); + ui->colorLable5->setText(newTemplateOptions.color_palette.color5.name()); + ui->colorLable6->setText(newTemplateOptions.color_palette.color6.name()); + + // update critical UI elements + ui->colorpalette->setCurrentIndex(newTemplateOptions.color_palette_index); + + // update grantlee template string + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + if (printOptions->type == print_options::DIVELIST) + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + else if (printOptions->type == print_options::STATISTICS) + grantlee_template = TemplateLayout::readTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template); +} + +void TemplateEdit::on_fontsize_valueChanged(int font_size) +{ + newTemplateOptions.font_size = font_size; + updatePreview(); +} + +void TemplateEdit::on_linespacing_valueChanged(double line_spacing) +{ + newTemplateOptions.line_spacing = line_spacing; + updatePreview(); +} + +void TemplateEdit::on_fontSelection_currentIndexChanged(int index) +{ + newTemplateOptions.font_index = index; + updatePreview(); +} + +void TemplateEdit::on_colorpalette_currentIndexChanged(int index) +{ + newTemplateOptions.color_palette_index = index; + switch (newTemplateOptions.color_palette_index) { + case SSRF_COLORS: // subsurface derived default colors + newTemplateOptions.color_palette = ssrf_colors; + break; + case ALMOND: // almond + newTemplateOptions.color_palette = almond_colors; + break; + case BLUESHADES: // blueshades + newTemplateOptions.color_palette = blueshades_colors; + break; + case CUSTOM: // custom + if (!editingCustomColors) + newTemplateOptions.color_palette = custom_colors; + else + editingCustomColors = false; + break; + } + updatePreview(); +} + +void TemplateEdit::saveSettings() +{ + if ((*templateOptions) != newTemplateOptions || grantlee_template.compare(ui->plainTextEdit->toPlainText())) { + QMessageBox msgBox(this); + QString message = tr("Do you want to save your changes?"); + bool templateChanged = false; + if (grantlee_template.compare(ui->plainTextEdit->toPlainText())) + templateChanged = true; + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if (msgBox.exec() == QMessageBox::Save) { + memcpy(templateOptions, &newTemplateOptions, sizeof(struct template_options)); + if (templateChanged) { + TemplateLayout::writeTemplate(printOptions->p_template, ui->plainTextEdit->toPlainText()); + if (printOptions->type == print_options::DIVELIST) + TemplateLayout::writeTemplate(printOptions->p_template, ui->plainTextEdit->toPlainText()); + else if (printOptions->type == print_options::STATISTICS) + TemplateLayout::writeTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template, ui->plainTextEdit->toPlainText()); + } + if (templateOptions->color_palette_index == CUSTOM) + custom_colors = templateOptions->color_palette; + } + } +} + +void TemplateEdit::on_buttonBox_clicked(QAbstractButton *button) +{ + QDialogButtonBox::StandardButton standardButton = ui->buttonBox->standardButton(button); + switch (standardButton) { + case QDialogButtonBox::Ok: + saveSettings(); + break; + case QDialogButtonBox::Cancel: + break; + case QDialogButtonBox::Apply: + saveSettings(); + updatePreview(); + break; + default: + ; + } +} + +void TemplateEdit::colorSelect(QAbstractButton *button) +{ + editingCustomColors = true; + // reset custom colors palette + switch (newTemplateOptions.color_palette_index) { + case SSRF_COLORS: // subsurface derived default colors + newTemplateOptions.color_palette = ssrf_colors; + break; + case ALMOND: // almond + newTemplateOptions.color_palette = almond_colors; + break; + case BLUESHADES: // blueshades + newTemplateOptions.color_palette = blueshades_colors; + break; + default: + break; + } + + //change selected color + QColor color; + switch (btnGroup->id(button)) { + case 1: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color1, this); + if (color.isValid()) + newTemplateOptions.color_palette.color1 = color; + break; + case 2: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color2, this); + if (color.isValid()) + newTemplateOptions.color_palette.color2 = color; + break; + case 3: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color3, this); + if (color.isValid()) + newTemplateOptions.color_palette.color3 = color; + break; + case 4: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color4, this); + if (color.isValid()) + newTemplateOptions.color_palette.color4 = color; + break; + case 5: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color5, this); + if (color.isValid()) + newTemplateOptions.color_palette.color5 = color; + break; + case 6: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color6, this); + if (color.isValid()) + newTemplateOptions.color_palette.color6 = color; + break; + } + newTemplateOptions.color_palette_index = CUSTOM; + updatePreview(); +} diff --git a/desktop-widgets/templateedit.h b/desktop-widgets/templateedit.h new file mode 100644 index 000000000..5e548ae19 --- /dev/null +++ b/desktop-widgets/templateedit.h @@ -0,0 +1,44 @@ +#ifndef TEMPLATEEDIT_H +#define TEMPLATEEDIT_H + +#include +#include "templatelayout.h" + +namespace Ui { +class TemplateEdit; +} + +class TemplateEdit : public QDialog +{ + Q_OBJECT + +public: + explicit TemplateEdit(QWidget *parent, struct print_options *printOptions, struct template_options *templateOptions); + ~TemplateEdit(); +private slots: + void on_fontsize_valueChanged(int font_size); + + void on_linespacing_valueChanged(double line_spacing); + + void on_fontSelection_currentIndexChanged(int index); + + void on_colorpalette_currentIndexChanged(int index); + + void on_buttonBox_clicked(QAbstractButton *button); + + void colorSelect(QAbstractButton *button); + +private: + Ui::TemplateEdit *ui; + QButtonGroup *btnGroup; + bool editingCustomColors; + struct template_options *templateOptions; + struct template_options newTemplateOptions; + struct print_options *printOptions; + QString grantlee_template; + void saveSettings(); + void updatePreview(); + +}; + +#endif // TEMPLATEEDIT_H diff --git a/desktop-widgets/templateedit.ui b/desktop-widgets/templateedit.ui new file mode 100644 index 000000000..60a0fc7e6 --- /dev/null +++ b/desktop-widgets/templateedit.ui @@ -0,0 +1,577 @@ + + + TemplateEdit + + + + 0 + 0 + 770 + 433 + + + + Edit template + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Preview + + + + + + + + 0 + 0 + + + + + 180 + 240 + + + + + 180 + 254 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + false + + + 0 + + + + Style + + + + + + + + + + Font + + + + + + + + Arial + + + + + Impact + + + + + Georgia + + + + + Courier + + + + + Verdana + + + + + + + + + + + + Font size + + + + + + + 9 + + + 18 + + + + + + + + + + + Color palette + + + + + + + + Default + + + + + Almond + + + + + Shades of blue + + + + + Custom + + + + + + + + + + + + Line spacing + + + + + + + 1.000000000000000 + + + 3.000000000000000 + + + 0.250000000000000 + + + 1.250000000000000 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Template + + + + + + Qt::ScrollBarAsNeeded + + + QPlainTextEdit::NoWrap + + + + + + + + + 16777215 + 16777215 + + + + Colors + + + + + + + + + + + 0 + 0 + + + + Background + + + + + + + + 0 + 0 + + + + color1 + + + Qt::AlignCenter + + + + + + + Edit + + + + + + + + + + + + 0 + 0 + + + + Table cells 1 + + + + + + + + 0 + 0 + + + + color2 + + + Qt::AlignCenter + + + + + + + Edit + + + + + + + + + + + + 0 + 0 + + + + Table cells 2 + + + + + + + + 0 + 0 + + + + color3 + + + Qt::AlignCenter + + + + + + + Edit + + + + + + + + + + + + 0 + 0 + + + + Text 1 + + + + + + + + 0 + 0 + + + + color4 + + + Qt::AlignCenter + + + + + + + Edit + + + + + + + + + + + + 0 + 0 + + + + Text 2 + + + + + + + + 0 + 0 + + + + color5 + + + Qt::AlignCenter + + + + + + + Edit + + + + + + + + + + + + 0 + 0 + + + + Borders + + + + + + + + 0 + 0 + + + + color6 + + + Qt::AlignCenter + + + + + + + Edit + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + TemplateEdit + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TemplateEdit + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/templatelayout.cpp b/desktop-widgets/templatelayout.cpp new file mode 100644 index 000000000..a376459a6 --- /dev/null +++ b/desktop-widgets/templatelayout.cpp @@ -0,0 +1,182 @@ +#include + +#include "templatelayout.h" +#include "helpers.h" +#include "display.h" + +QList grantlee_templates, grantlee_statistics_templates; + +int getTotalWork(print_options *printOptions) +{ + if (printOptions->print_selected) { + // return the correct number depending on all/selected dives + // but don't return 0 as we might divide by this number + return amount_selected ? amount_selected : 1; + } + int dives = 0, i; + struct dive *dive; + for_each_dive (i, dive) { + dives++; + } + return dives; +} + +void find_all_templates() +{ + grantlee_templates.clear(); + grantlee_statistics_templates.clear(); + QDir dir(getPrintingTemplatePathUser()); + QFileInfoList list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + foreach (QFileInfo finfo, list) { + QString filename = finfo.fileName(); + if (filename.at(filename.size() - 1) != '~') { + grantlee_templates.append(finfo.fileName()); + } + } + // find statistics templates + dir.setPath(getPrintingTemplatePathUser() + QDir::separator() + "statistics"); + list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + foreach (QFileInfo finfo, list) { + QString filename = finfo.fileName(); + if (filename.at(filename.size() - 1) != '~') { + grantlee_statistics_templates.append(finfo.fileName()); + } + } +} + +TemplateLayout::TemplateLayout(print_options *PrintOptions, template_options *templateOptions) : + m_engine(NULL) +{ + this->PrintOptions = PrintOptions; + this->templateOptions = templateOptions; +} + +TemplateLayout::~TemplateLayout() +{ + delete m_engine; +} + +QString TemplateLayout::generate() +{ + int progress = 0; + int totalWork = getTotalWork(PrintOptions); + + QString htmlContent; + m_engine = new Grantlee::Engine(this); + + QSharedPointer m_templateLoader = + QSharedPointer(new Grantlee::FileSystemTemplateLoader()); + m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser()); + m_engine->addTemplateLoader(m_templateLoader); + + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + + QVariantList diveList; + + struct dive *dive; + int i; + for_each_dive (i, dive) { + //TODO check for exporting selected dives only + if (!dive->selected && PrintOptions->print_selected) + continue; + Dive d(dive); + diveList.append(QVariant::fromValue(d)); + progress++; + emit progressUpdated(progress * 100.0 / totalWork); + } + Grantlee::Context c; + c.insert("dives", diveList); + c.insert("template_options", QVariant::fromValue(*templateOptions)); + c.insert("print_options", QVariant::fromValue(*PrintOptions)); + + Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); + if (!t || t->error()) { + qDebug() << "Can't load template"; + return htmlContent; + } + + htmlContent = t->render(&c); + + if (t->error()) { + qDebug() << "Can't render template"; + return htmlContent; + } + return htmlContent; +} + +QString TemplateLayout::generateStatistics() +{ + QString htmlContent; + m_engine = new Grantlee::Engine(this); + + QSharedPointer m_templateLoader = + QSharedPointer(new Grantlee::FileSystemTemplateLoader()); + m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser() + QDir::separator() + QString("statistics")); + m_engine->addTemplateLoader(m_templateLoader); + + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + Grantlee::registerMetaType(); + + QVariantList years; + + int i = 0; + while (stats_yearly != NULL && stats_yearly[i].period) { + YearInfo year(stats_yearly[i]); + years.append(QVariant::fromValue(year)); + i++; + } + + Grantlee::Context c; + c.insert("years", years); + c.insert("template_options", QVariant::fromValue(*templateOptions)); + c.insert("print_options", QVariant::fromValue(*PrintOptions)); + + Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); + if (!t || t->error()) { + qDebug() << "Can't load template"; + return htmlContent; + } + + htmlContent = t->render(&c); + + if (t->error()) { + qDebug() << "Can't render template"; + return htmlContent; + } + + emit progressUpdated(100); + return htmlContent; +} + +QString TemplateLayout::readTemplate(QString template_name) +{ + QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); + if (qfile.open(QFile::ReadOnly | QFile::Text)) { + QTextStream in(&qfile); + return in.readAll(); + } + return ""; +} + +void TemplateLayout::writeTemplate(QString template_name, QString grantlee_template) +{ + QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); + if (qfile.open(QFile::ReadWrite | QFile::Text)) { + qfile.write(grantlee_template.toUtf8().data()); + qfile.resize(qfile.pos()); + qfile.close(); + } +} + +YearInfo::YearInfo() +{ + +} + +YearInfo::~YearInfo() +{ + +} diff --git a/desktop-widgets/templatelayout.h b/desktop-widgets/templatelayout.h new file mode 100644 index 000000000..a2868e7ff --- /dev/null +++ b/desktop-widgets/templatelayout.h @@ -0,0 +1,168 @@ +#ifndef TEMPLATELAYOUT_H +#define TEMPLATELAYOUT_H + +#include +#include "mainwindow.h" +#include "printoptions.h" +#include "statistics.h" +#include "qthelper.h" +#include "helpers.h" + +int getTotalWork(print_options *printOptions); +void find_all_templates(); + +extern QList grantlee_templates, grantlee_statistics_templates; + +class TemplateLayout : public QObject { + Q_OBJECT +public: + TemplateLayout(print_options *PrintOptions, template_options *templateOptions); + ~TemplateLayout(); + QString generate(); + QString generateStatistics(); + static QString readTemplate(QString template_name); + static void writeTemplate(QString template_name, QString grantlee_template); + +private: + Grantlee::Engine *m_engine; + print_options *PrintOptions; + template_options *templateOptions; + +signals: + void progressUpdated(int value); +}; + +class YearInfo { +public: + stats_t *year; + YearInfo(stats_t& year) + :year(&year) + { + + } + YearInfo(); + ~YearInfo(); +}; + +Q_DECLARE_METATYPE(Dive) +Q_DECLARE_METATYPE(template_options) +Q_DECLARE_METATYPE(print_options) +Q_DECLARE_METATYPE(YearInfo) + +GRANTLEE_BEGIN_LOOKUP(Dive) +if (property == "number") + return object.number(); +else if (property == "id") + return object.id(); +else if (property == "date") + return object.date(); +else if (property == "time") + return object.time(); +else if (property == "location") + return object.location(); +else if (property == "duration") + return object.duration(); +else if (property == "depth") + return object.depth(); +else if (property == "divemaster") + return object.divemaster(); +else if (property == "buddy") + return object.buddy(); +else if (property == "airTemp") + return object.airTemp(); +else if (property == "waterTemp") + return object.waterTemp(); +else if (property == "notes") + return object.notes(); +else if (property == "rating") + return object.rating(); +else if (property == "sac") + return object.sac(); +else if (property == "tags") + return object.tags(); +else if (property == "gas") + return object.gas(); +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(template_options) +if (property == "font") { + switch (object.font_index) { + case 0: + return "Arial, Helvetica, sans-serif"; + case 1: + return "Impact, Charcoal, sans-serif"; + case 2: + return "Georgia, serif"; + case 3: + return "Courier, monospace"; + case 4: + return "Verdana, Geneva, sans-serif"; + } +} else if (property == "borderwidth") { + return object.border_width; +} else if (property == "font_size") { + return object.font_size / 9.0; +} else if (property == "line_spacing") { + return object.line_spacing; +} else if (property == "color1") { + return object.color_palette.color1.name(); +} else if (property == "color2") { + return object.color_palette.color2.name(); +} else if (property == "color3") { + return object.color_palette.color3.name(); +} else if (property == "color4") { + return object.color_palette.color4.name(); +} else if (property == "color5") { + return object.color_palette.color5.name(); +} else if (property == "color6") { + return object.color_palette.color6.name(); +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(print_options) +if (property == "grayscale") { + if (object.color_selected) { + return ""; + } else { + return "-webkit-filter: grayscale(100%)"; + } +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(YearInfo) +if (property == "year") { + return object.year->period; +} else if (property == "dives") { + return object.year->selection_size; +} else if (property == "min_temp") { + const char *unit; + double temp = get_temp_units(object.year->min_temp, &unit); + return object.year->min_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; +} else if (property == "max_temp") { + const char *unit; + double temp = get_temp_units(object.year->max_temp, &unit); + return object.year->max_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; +} else if (property == "total_time") { + return get_time_string(object.year->total_time.seconds, 0); +} else if (property == "avg_time") { + return get_minutes(object.year->total_time.seconds / object.year->selection_size); +} else if (property == "shortest_time") { + return get_minutes(object.year->shortest_time.seconds); +} else if (property == "longest_time") { + return get_minutes(object.year->longest_time.seconds); +} else if (property == "avg_depth") { + return get_depth_string(object.year->avg_depth); +} else if (property == "min_depth") { + return get_depth_string(object.year->min_depth); +} else if (property == "max_depth") { + return get_depth_string(object.year->max_depth); +} else if (property == "avg_sac") { + return get_volume_string(object.year->avg_sac); +} else if (property == "min_sac") { + return get_volume_string(object.year->min_sac); +} else if (property == "max_sac") { + return get_volume_string(object.year->max_sac); +} +GRANTLEE_END_LOOKUP + +#endif diff --git a/desktop-widgets/undocommands.cpp b/desktop-widgets/undocommands.cpp new file mode 100644 index 000000000..0fd182cb3 --- /dev/null +++ b/desktop-widgets/undocommands.cpp @@ -0,0 +1,156 @@ +#include "undocommands.h" +#include "mainwindow.h" +#include "divelist.h" + +UndoDeleteDive::UndoDeleteDive(QList deletedDives) : diveList(deletedDives) +{ + setText("delete dive"); + if (diveList.count() > 1) + setText(QString("delete %1 dives").arg(QString::number(diveList.count()))); +} + +void UndoDeleteDive::undo() +{ + // first bring back the trip(s) + Q_FOREACH(struct dive_trip *trip, tripList) + insert_trip(&trip); + + // now walk the list of deleted dives + for (int i = 0; i < diveList.count(); i++) { + struct dive *d = diveList.at(i); + // we adjusted the divetrip to point to the "new" divetrip + if (d->divetrip) { + struct dive_trip *trip = d->divetrip; + tripflag_t tripflag = d->tripflag; // this gets overwritten in add_dive_to_trip() + d->divetrip = NULL; + d->next = NULL; + d->pprev = NULL; + add_dive_to_trip(d, trip); + d->tripflag = tripflag; + } + record_dive(diveList.at(i)); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoDeleteDive::redo() +{ + QList newList; + for (int i = 0; i < diveList.count(); i++) { + // make a copy of the dive before deleting it + struct dive* d = alloc_dive(); + copy_dive(diveList.at(i), d); + newList.append(d); + // check for trip - if this is the last dive in the trip + // the trip will get deleted, so we need to remember it as well + if (d->divetrip && d->divetrip->nrdives == 1) { + struct dive_trip *undo_trip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip)); + *undo_trip = *d->divetrip; + undo_trip->location = copy_string(d->divetrip->location); + undo_trip->notes = copy_string(d->divetrip->notes); + undo_trip->nrdives = 0; + undo_trip->next = NULL; + undo_trip->dives = NULL; + // update all the dives who were in this trip to point to the copy of the + // trip that we are about to delete implicitly when deleting its last dive below + Q_FOREACH(struct dive *inner_dive, newList) + if (inner_dive->divetrip == d->divetrip) + inner_dive->divetrip = undo_trip; + d->divetrip = undo_trip; + tripList.append(undo_trip); + } + //delete the dive + delete_single_dive(get_divenr(diveList.at(i))); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); + diveList.clear(); + diveList = newList; +} + + +UndoShiftTime::UndoShiftTime(QList changedDives, int amount) + : diveList(changedDives), timeChanged(amount) +{ + setText("shift time"); +} + +void UndoShiftTime::undo() +{ + for (int i = 0; i < diveList.count(); i++) { + struct dive* d = get_dive_by_uniq_id(diveList.at(i)); + d->when -= timeChanged; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoShiftTime::redo() +{ + for (int i = 0; i < diveList.count(); i++) { + struct dive* d = get_dive_by_uniq_id(diveList.at(i)); + d->when += timeChanged; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + + +UndoRenumberDives::UndoRenumberDives(QMap > originalNumbers) +{ + oldNumbers = originalNumbers; + if (oldNumbers.count() > 1) + setText(QString("renumber %1 dives").arg(QString::number(oldNumbers.count()))); + else + setText("renumber dive"); +} + +void UndoRenumberDives::undo() +{ + foreach (int key, oldNumbers.keys()) { + struct dive* d = get_dive_by_uniq_id(key); + d->number = oldNumbers.value(key).first; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoRenumberDives::redo() +{ + foreach (int key, oldNumbers.keys()) { + struct dive* d = get_dive_by_uniq_id(key); + d->number = oldNumbers.value(key).second; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + + +UndoRemoveDivesFromTrip::UndoRemoveDivesFromTrip(QMap removedDives) +{ + divesToUndo = removedDives; + setText("remove dive(s) from trip"); +} + +void UndoRemoveDivesFromTrip::undo() +{ + QMapIterator i(divesToUndo); + while (i.hasNext()) { + i.next(); + add_dive_to_trip(i.key (), i.value()); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoRemoveDivesFromTrip::redo() +{ + QMapIterator i(divesToUndo); + while (i.hasNext()) { + i.next(); + remove_dive_from_trip(i.key(), false); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} diff --git a/desktop-widgets/undocommands.h b/desktop-widgets/undocommands.h new file mode 100644 index 000000000..8e359db51 --- /dev/null +++ b/desktop-widgets/undocommands.h @@ -0,0 +1,50 @@ +#ifndef UNDOCOMMANDS_H +#define UNDOCOMMANDS_H + +#include +#include +#include "dive.h" + +class UndoDeleteDive : public QUndoCommand { +public: + UndoDeleteDive(QList deletedDives); + virtual void undo(); + virtual void redo(); + +private: + QList diveList; + QList tripList; +}; + +class UndoShiftTime : public QUndoCommand { +public: + UndoShiftTime(QList changedDives, int amount); + virtual void undo(); + virtual void redo(); + +private: + QList diveList; + int timeChanged; +}; + +class UndoRenumberDives : public QUndoCommand { +public: + UndoRenumberDives(QMap > originalNumbers); + virtual void undo(); + virtual void redo(); + +private: + QMap > oldNumbers; +}; + +class UndoRemoveDivesFromTrip : public QUndoCommand { +public: + UndoRemoveDivesFromTrip(QMap removedDives); + virtual void undo(); + virtual void redo(); + +private: + QMap divesToUndo; +}; + +#endif // UNDOCOMMANDS_H diff --git a/desktop-widgets/updatemanager.cpp b/desktop-widgets/updatemanager.cpp new file mode 100644 index 000000000..0760d6407 --- /dev/null +++ b/desktop-widgets/updatemanager.cpp @@ -0,0 +1,154 @@ +#include "updatemanager.h" +#include "helpers.h" +#include +#include +#include +#include "subsurfacewebservices.h" +#include "version.h" +#include "mainwindow.h" + +UpdateManager::UpdateManager(QObject *parent) : + QObject(parent), + isAutomaticCheck(false) +{ + // is this the first time this version was run? + QSettings settings; + settings.beginGroup("UpdateManager"); + if (settings.contains("DontCheckForUpdates") && settings.value("DontCheckForUpdates") == "TRUE") + return; + if (settings.contains("LastVersionUsed")) { + // we have checked at least once before + if (settings.value("LastVersionUsed").toString() != subsurface_git_version()) { + // we have just updated - wait two weeks before you check again + settings.setValue("LastVersionUsed", QString(subsurface_git_version())); + settings.setValue("NextCheck", QDateTime::currentDateTime().addDays(14).toString(Qt::ISODate)); + } else { + // is it time to check again? + QString nextCheckString = settings.value("NextCheck").toString(); + QDateTime nextCheck = QDateTime::fromString(nextCheckString, Qt::ISODate); + if (nextCheck > QDateTime::currentDateTime()) + return; + } + } + settings.setValue("LastVersionUsed", QString(subsurface_git_version())); + settings.setValue("NextCheck", QDateTime::currentDateTime().addDays(14).toString(Qt::ISODate)); + checkForUpdates(true); +} + +void UpdateManager::checkForUpdates(bool automatic) +{ + QString os; + +#if defined(Q_OS_WIN) + os = "win"; +#elif defined(Q_OS_MAC) + os = "osx"; +#elif defined(Q_OS_LINUX) + os = "linux"; +#else + os = "unknown"; +#endif + isAutomaticCheck = automatic; + QString version = subsurface_canonical_version(); + QString uuidString = getUUID(); + QString url = QString("http://subsurface-divelog.org/updatecheck.html?os=%1&version=%2&uuid=%3").arg(os, version, uuidString); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("Accept", "text/xml"); + QString userAgent = getUserAgent(); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + connect(SubsurfaceWebServices::manager()->get(request), SIGNAL(finished()), this, SLOT(requestReceived()), Qt::UniqueConnection); +} + +QString UpdateManager::getUUID() +{ + QString uuidString; + QSettings settings; + settings.beginGroup("UpdateManager"); + if (settings.contains("UUID")) { + uuidString = settings.value("UUID").toString(); + } else { + QUuid uuid = QUuid::createUuid(); + uuidString = uuid.toString(); + settings.setValue("UUID", uuidString); + } + uuidString.replace("{", "").replace("}", ""); + return uuidString; +} + +void UpdateManager::requestReceived() +{ + bool haveNewVersion = false; + QMessageBox msgbox; + QString msgTitle = tr("Check for updates."); + QString msgText = "

" + tr("Subsurface was unable to check for updates.") + "

"; + + QNetworkReply *reply = qobject_cast(sender()); + if (reply->error() != QNetworkReply::NoError) { + //Network Error + msgText = msgText + "
" + tr("The following error occurred:") + "
" + reply->errorString() + + "

" + tr("Please check your internet connection.") + ""; + } else { + //No network error + QString responseBody(reply->readAll()); + QString responseLink; + if (responseBody.contains('"')) + responseLink = responseBody.split("\"").at(1); + + msgbox.setIcon(QMessageBox::Information); + if (responseBody == "OK") { + msgText = tr("You are using the latest version of Subsurface."); + } else if (responseBody.startsWith("[\"http")) { + haveNewVersion = true; + msgText = tr("A new version of Subsurface is available.
Click on:
%1
to download it.") + .arg(responseLink); + } else if (responseBody.startsWith("Latest version")) { + // the webservice backend doesn't localize - but it's easy enough to just replace the + // strings that it is likely to send back + haveNewVersion = true; + msgText = QString("") + tr("A new version of Subsurface is available.") + QString("

") + + tr("Latest version is %1, please check %2 our download page %3 for information in how to update.") + .arg(responseLink).arg("").arg(""); + } else { + // the webservice backend doesn't localize - but it's easy enough to just replace the + // strings that it is likely to send back + if (!responseBody.contains("latest development") && + !responseBody.contains("newer") && + !responseBody.contains("beta", Qt::CaseInsensitive)) + haveNewVersion = true; + if (responseBody.contains("Newest release version is ")) + responseBody.replace("Newest release version is ", tr("Newest release version is ")); + msgText = tr("The server returned the following information:").append("

").append(responseBody); + msgbox.setIcon(QMessageBox::Warning); + } + } +#ifndef SUBSURFACE_MOBILE + if (haveNewVersion || !isAutomaticCheck) { + msgbox.setWindowTitle(msgTitle); + msgbox.setWindowIcon(QIcon(":/subsurface-icon")); + msgbox.setText(msgText); + msgbox.setTextFormat(Qt::RichText); + msgbox.exec(); + } + if (isAutomaticCheck) { + QSettings settings; + settings.beginGroup("UpdateManager"); + if (!settings.contains("DontCheckForUpdates")) { + // we allow an opt out of future checks + QMessageBox response(MainWindow::instance()); + QString message = tr("Subsurface is checking every two weeks if a new version is available. If you don't want Subsurface to continue checking, please click Decline."); + response.addButton(tr("Decline"), QMessageBox::RejectRole); + response.addButton(tr("Accept"), QMessageBox::AcceptRole); + response.setText(message); + response.setWindowTitle(tr("Automatic check for updates")); + response.setIcon(QMessageBox::Question); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + if (ret == QMessageBox::Accepted) + settings.setValue("DontCheckForUpdates", "FALSE"); + else + settings.setValue("DontCheckForUpdates", "TRUE"); + } + } +#endif +} diff --git a/desktop-widgets/updatemanager.h b/desktop-widgets/updatemanager.h new file mode 100644 index 000000000..f91c82dc8 --- /dev/null +++ b/desktop-widgets/updatemanager.h @@ -0,0 +1,24 @@ +#ifndef UPDATEMANAGER_H +#define UPDATEMANAGER_H + +#include + +class QNetworkAccessManager; +class QNetworkReply; + +class UpdateManager : public QObject { + Q_OBJECT +public: + explicit UpdateManager(QObject *parent = 0); + void checkForUpdates(bool automatic = false); + static QString getUUID(); + +public +slots: + void requestReceived(); + +private: + bool isAutomaticCheck; +}; + +#endif // UPDATEMANAGER_H diff --git a/desktop-widgets/urldialog.ui b/desktop-widgets/urldialog.ui new file mode 100644 index 000000000..397f90a64 --- /dev/null +++ b/desktop-widgets/urldialog.ui @@ -0,0 +1,91 @@ + + + URLDialog + + + + 0 + 0 + 397 + 103 + + + + Dialog + + + + + 40 + 60 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 10 + 30 + 371 + 21 + + + + + + + 10 + 10 + 151 + 16 + + + + Enter URL for images + + + + + + + buttonBox + accepted() + URLDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + URLDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/desktop-widgets/usermanual.cpp b/desktop-widgets/usermanual.cpp new file mode 100644 index 000000000..6b676f16b --- /dev/null +++ b/desktop-widgets/usermanual.cpp @@ -0,0 +1,151 @@ +#include +#include +#include + +#include "usermanual.h" +#include "mainwindow.h" +#include "helpers.h" + +SearchBar::SearchBar(QWidget *parent): QWidget(parent) +{ + ui.setupUi(this); + #if defined(Q_OS_MAC) || defined(Q_OS_WIN) + ui.findNext->setIcon(QIcon(":icons/subsurface/32x32/actions/go-down.png")); + ui.findPrev->setIcon(QIcon(":icons/subsurface/32x32/actions/go-up.png")); + ui.findClose->setIcon(QIcon(":icons/subsurface/32x32/actions/window-close.png")); + #endif + + connect(ui.findNext, SIGNAL(pressed()), this, SIGNAL(searchNext())); + connect(ui.findPrev, SIGNAL(pressed()), this, SIGNAL(searchPrev())); + connect(ui.searchEdit, SIGNAL(textChanged(QString)), this, SIGNAL(searchTextChanged(QString))); + connect(ui.searchEdit, SIGNAL(textChanged(QString)), this, SLOT(enableButtons(QString))); + connect(ui.findClose, SIGNAL(pressed()), this, SLOT(hide())); +} + +void SearchBar::setVisible(bool visible) +{ + QWidget::setVisible(visible); + ui.searchEdit->setFocus(); +} + +void SearchBar::enableButtons(const QString &s) +{ + ui.findPrev->setEnabled(s.length()); + ui.findNext->setEnabled(s.length()); +} + +UserManual::UserManual(QWidget *parent) : QWidget(parent) +{ + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(closeKey, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quitKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quitKey, SIGNAL(activated()), qApp, SLOT(quit())); + + QAction *actionShowSearch = new QAction(this); + actionShowSearch->setShortcut(Qt::CTRL + Qt::Key_F); + actionShowSearch->setShortcutContext(Qt::WindowShortcut); + addAction(actionShowSearch); + + QAction *actionHideSearch = new QAction(this); + actionHideSearch->setShortcut(Qt::Key_Escape); + actionHideSearch->setShortcutContext(Qt::WindowShortcut); + addAction(actionHideSearch); + + setWindowTitle(tr("User manual")); + setWindowIcon(QIcon(":/subsurface-icon")); + + userManual = new QWebView(this); + QString colorBack = palette().highlight().color().name(QColor::HexRgb); + QString colorText = palette().highlightedText().color().name(QColor::HexRgb); + userManual->setStyleSheet(QString("QWebView { selection-background-color: %1; selection-color: %2; }") + .arg(colorBack).arg(colorText)); + userManual->page()->setLinkDelegationPolicy(QWebPage::DelegateExternalLinks); + QString searchPath = getSubsurfaceDataPath("Documentation"); + if (searchPath.size()) { + // look for localized versions of the manual first + QString lang = uiLanguage(NULL); + QString prefix = searchPath.append("/user-manual"); + QFile manual(prefix + "_" + lang + ".html"); + if (!manual.exists()) + manual.setFileName(prefix + "_" + lang.left(2) + ".html"); + if (!manual.exists()) + manual.setFileName(prefix + ".html"); + if (!manual.exists()) { + userManual->setHtml(tr("Cannot find the Subsurface manual")); + } else { + QString urlString = QString("file:///") + manual.fileName(); + userManual->setUrl(QUrl(urlString, QUrl::TolerantMode)); + } + } else { + userManual->setHtml(tr("Cannot find the Subsurface manual")); + } + + searchBar = new SearchBar(this); + searchBar->hide(); + connect(actionShowSearch, SIGNAL(triggered(bool)), searchBar, SLOT(show())); + connect(actionHideSearch, SIGNAL(triggered(bool)), searchBar, SLOT(hide())); + connect(userManual, SIGNAL(linkClicked(QUrl)), this, SLOT(linkClickedSlot(QUrl))); + connect(searchBar, SIGNAL(searchTextChanged(QString)), this, SLOT(searchTextChanged(QString))); + connect(searchBar, SIGNAL(searchNext()), this, SLOT(searchNext())); + connect(searchBar, SIGNAL(searchPrev()), this, SLOT(searchPrev())); + + QVBoxLayout *vboxLayout = new QVBoxLayout(); + userManual->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + vboxLayout->addWidget(userManual); + vboxLayout->addWidget(searchBar); + setLayout(vboxLayout); +} + +void UserManual::search(QString text, QWebPage::FindFlags flags = 0) +{ + if (userManual->findText(text, QWebPage::FindWrapsAroundDocument | flags) || text.length() == 0) { + searchBar->setStyleSheet(""); + } else { + searchBar->setStyleSheet("QLineEdit{background: red;}"); + } +} + +void UserManual::searchTextChanged(const QString& text) +{ + mLastText = text; + search(text); +} + +void UserManual::searchNext() +{ + search(mLastText); +} + +void UserManual::searchPrev() +{ + search(mLastText, QWebPage::FindBackward); +} + +void UserManual::linkClickedSlot(const QUrl& url) +{ + QDesktopServices::openUrl(url); +} + +#ifdef Q_OS_MAC +void UserManual::showEvent(QShowEvent *e) { + filterAction = NULL; + closeAction = NULL; + MainWindow *m = MainWindow::instance(); + Q_FOREACH (QObject *o, m->children()) { + if (o->objectName() == "actionFilterTags") { + filterAction = qobject_cast(o); + filterAction->setShortcut(QKeySequence()); + } else if (o->objectName() == "actionClose") { + closeAction = qobject_cast(o); + closeAction->setShortcut(QKeySequence()); + } + } +} +void UserManual::hideEvent(QHideEvent *e) { + if (closeAction != NULL) + closeAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_W)); + if (filterAction != NULL) + filterAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F)); + closeAction = filterAction = NULL; +} +#endif diff --git a/desktop-widgets/usermanual.h b/desktop-widgets/usermanual.h new file mode 100644 index 000000000..5101a3c3b --- /dev/null +++ b/desktop-widgets/usermanual.h @@ -0,0 +1,50 @@ +#ifndef USERMANUAL_H +#define USERMANUAL_H + +#include + +#include "ui_searchbar.h" + +class SearchBar : public QWidget{ + Q_OBJECT +public: + SearchBar(QWidget *parent = 0); +signals: + void searchTextChanged(const QString& s); + void searchNext(); + void searchPrev(); +protected: + void setVisible(bool visible); +private slots: + void enableButtons(const QString& s); +private: + Ui::SearchBar ui; +}; + +class UserManual : public QWidget { + Q_OBJECT + +public: + explicit UserManual(QWidget *parent = 0); + +#ifdef Q_OS_MAC +protected: + void showEvent(QShowEvent *e); + void hideEvent(QHideEvent *e); + QAction *closeAction; + QAction *filterAction; +#endif + +private +slots: + void searchTextChanged(const QString& s); + void searchNext(); + void searchPrev(); + void linkClickedSlot(const QUrl& url); +private: + QWebView *userManual; + SearchBar *searchBar; + QString mLastText; + void search(QString, QWebPage::FindFlags); +}; +#endif // USERMANUAL_H diff --git a/desktop-widgets/usersurvey.cpp b/desktop-widgets/usersurvey.cpp new file mode 100644 index 000000000..05da582a1 --- /dev/null +++ b/desktop-widgets/usersurvey.cpp @@ -0,0 +1,133 @@ +#include +#include +#include + +#include "usersurvey.h" +#include "ui_usersurvey.h" +#include "version.h" +#include "subsurfacewebservices.h" +#include "updatemanager.h" + +#include "helpers.h" +#include "subsurfacesysinfo.h" + +UserSurvey::UserSurvey(QWidget *parent) : QDialog(parent), + ui(new Ui::UserSurvey) +{ + ui->setupUi(this); + ui->buttonBox->buttons().first()->setText(tr("Send")); + this->adjustSize(); + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(closeKey, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quitKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quitKey, SIGNAL(activated()), parent, SLOT(close())); + + os = QString("ssrfVers=%1").arg(subsurface_version()); + os.append(QString("&prettyOsName=%1").arg(SubsurfaceSysInfo::prettyOsName())); + QString arch = SubsurfaceSysInfo::buildCpuArchitecture(); + os.append(QString("&appCpuArch=%1").arg(arch)); + if (arch == "i386") { + QString osArch = SubsurfaceSysInfo::currentCpuArchitecture(); + os.append(QString("&osCpuArch=%1").arg(osArch)); + } + os.append(QString("&uiLang=%1").arg(uiLanguage(NULL))); + os.append(QString("&uuid=%1").arg(UpdateManager::getUUID())); + ui->system->setPlainText(getVersion()); +} + +QString UserSurvey::getVersion() +{ + QString arch; + // fill in the system data + QString sysInfo = QString("Subsurface %1").arg(subsurface_version()); + sysInfo.append(tr("\nOperating system: %1").arg(SubsurfaceSysInfo::prettyOsName())); + arch = SubsurfaceSysInfo::buildCpuArchitecture(); + sysInfo.append(tr("\nCPU architecture: %1").arg(arch)); + if (arch == "i386") + sysInfo.append(tr("\nOS CPU architecture: %1").arg(SubsurfaceSysInfo::currentCpuArchitecture())); + sysInfo.append(tr("\nLanguage: %1").arg(uiLanguage(NULL))); + return sysInfo; +} + +UserSurvey::~UserSurvey() +{ + delete ui; +} + +#define ADD_OPTION(_name) values.append(ui->_name->isChecked() ? "&" #_name "=1" : "&" #_name "=0") + +void UserSurvey::on_buttonBox_accepted() +{ + // now we need to collect the data and submit it + QString values = os; + ADD_OPTION(recreational); + ADD_OPTION(tech); + ADD_OPTION(planning); + ADD_OPTION(download); + ADD_OPTION(divecomputer); + ADD_OPTION(manual); + ADD_OPTION(companion); + values.append(QString("&suggestion=%1").arg(ui->suggestions->toPlainText())); + UserSurveyServices uss(this); + connect(uss.sendSurvey(values), SIGNAL(finished()), SLOT(requestReceived())); + hide(); +} + +void UserSurvey::on_buttonBox_rejected() +{ + QMessageBox response(this); + response.setText(tr("Should we ask you later?")); + response.addButton(tr("Don't ask me again"), QMessageBox::RejectRole); + response.addButton(tr("Ask later"), QMessageBox::AcceptRole); + response.setWindowTitle(tr("Ask again?")); // Not displayed on MacOSX as described in Qt API + response.setIcon(QMessageBox::Question); + response.setWindowModality(Qt::WindowModal); + switch (response.exec()) { + case QDialog::Accepted: + // nothing to do here, we'll just ask again the next time they start + break; + case QDialog::Rejected: + QSettings s; + s.beginGroup("UserSurvey"); + s.setValue("SurveyDone", "declined"); + break; + } + hide(); +} + +void UserSurvey::requestReceived() +{ + QMessageBox msgbox; + QString msgTitle = tr("Submit user survey."); + QString msgText = "

" + tr("Subsurface was unable to submit the user survey.") + "

"; + + QNetworkReply *reply = qobject_cast(sender()); + if (reply->error() != QNetworkReply::NoError) { + //Network Error + msgText = msgText + "
" + tr("The following error occurred:") + "
" + reply->errorString() + + "

" + tr("Please check your internet connection.") + ""; + } else { + //No network error + QString response(reply->readAll()); + QString responseBody = response.split("\"").at(1); + + msgbox.setIcon(QMessageBox::Information); + + if (responseBody == "OK") { + msgText = tr("Survey successfully submitted."); + QSettings s; + s.beginGroup("UserSurvey"); + s.setValue("SurveyDone", "submitted"); + } else { + msgText = tr("There was an error while trying to check for updates.

%1").arg(responseBody); + msgbox.setIcon(QMessageBox::Warning); + } + } + + msgbox.setWindowTitle(msgTitle); + msgbox.setWindowIcon(QIcon(":/subsurface-icon")); + msgbox.setText(msgText); + msgbox.setTextFormat(Qt::RichText); + msgbox.exec(); + reply->deleteLater(); +} diff --git a/desktop-widgets/usersurvey.h b/desktop-widgets/usersurvey.h new file mode 100644 index 000000000..1dd5aaab3 --- /dev/null +++ b/desktop-widgets/usersurvey.h @@ -0,0 +1,30 @@ +#ifndef USERSURVEY_H +#define USERSURVEY_H + +#include +class QNetworkAccessManager; +class QNetworkReply; + +namespace Ui { + class UserSurvey; +} + +class UserSurvey : public QDialog { + Q_OBJECT + +public: + explicit UserSurvey(QWidget *parent = 0); + ~UserSurvey(); + static QString getVersion(); + +private +slots: + void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + void requestReceived(); + +private: + Ui::UserSurvey *ui; + QString os; +}; +#endif // USERSURVEY_H diff --git a/desktop-widgets/usersurvey.ui b/desktop-widgets/usersurvey.ui new file mode 100644 index 000000000..c118fe89d --- /dev/null +++ b/desktop-widgets/usersurvey.ui @@ -0,0 +1,301 @@ + + + UserSurvey + + + + 0 + 0 + 538 + 714 + + + + User survey + + + + + 180 + 10 + 241 + 21 + + + + + 11 + + + + Subsurface user survey + + + + + + 9 + 36 + 521 + 91 + + + + <html><head/><body><p>We would love to learn more about our users, their preferences and their usage habits. Please spare a minute to fill out this form and submit it to the Subsurface team.</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + 10 + 190 + 161 + 26 + + + + Technical diver + + + + + + 180 + 190 + 181 + 26 + + + + Recreational diver + + + + + + 380 + 190 + 141 + 26 + + + + Dive planner + + + + + + 10 + 270 + 481 + 26 + + + + Supported dive computer + + + + + + 10 + 300 + 491 + 26 + + + + Other software/sources + + + + + + 10 + 330 + 501 + 26 + + + + Manually entering dives + + + + + + 10 + 360 + 491 + 26 + + + + Android/iPhone companion app + + + + + + 9 + 410 + 501 + 21 + + + + Any suggestions? (in English) + + + + + + 10 + 440 + 521 + 71 + + + + + + + 9 + 520 + 511 + 51 + + + + The following information about your system will also be submitted. + + + true + + + + + + 9 + 590 + 521 + 71 + + + + + 0 + 0 + + + + true + + + + + + 350 + 670 + 176 + 31 + + + + Qt::TabFocus + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + 10 + 160 + 451 + 21 + + + + What kind of diver are you? + + + + + + 10 + 140 + 511 + 20 + + + + Qt::Horizontal + + + + + + 10 + 220 + 511 + 20 + + + + Qt::Horizontal + + + + + + 10 + 240 + 491 + 21 + + + + Where are you importing data from? + + + + + + 10 + 390 + 511 + 20 + + + + Qt::Horizontal + + + + + tech + recreational + planning + download + divecomputer + manual + companion + suggestions + system + buttonBox + + + + diff --git a/desktop-widgets/webservices.ui b/desktop-widgets/webservices.ui new file mode 100644 index 000000000..bcca35f1e --- /dev/null +++ b/desktop-widgets/webservices.ui @@ -0,0 +1,156 @@ + + + WebServices + + + + 0 + 0 + 425 + 169 + + + + Web service connection + + + + :/subsurface-icon + + + + + + + Status: + + + + + + + Enter your ID here + + + + 420 + + + + + + + + Download + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help + + + + + + + 0 + + + + + + + User ID + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Save user ID locally? + + + + + + + Password + + + + + + + QLineEdit::Password + + + + + + + Upload + + + + + + + userID + password + download + upload + buttonBox + + + + + + + buttonBox + accepted() + WebServices + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + WebServices + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/main.cpp b/main.cpp index 171876abf..e7259c3f3 100644 --- a/main.cpp +++ b/main.cpp @@ -8,9 +8,9 @@ #include "dive.h" #include "qt-gui.h" #include "subsurfacestartup.h" -#include "qt-ui/mainwindow.h" -#include "qt-ui/diveplanner.h" -#include "qt-ui/graphicsview-common.h" +#include "desktop-widgets/mainwindow.h" +#include "desktop-widgets/diveplanner.h" +#include "subsurface-core/color.h" #include "qthelper.h" #include diff --git a/qt-gui.cpp b/qt-gui.cpp index a4997a6fe..5c7157fe6 100644 --- a/qt-gui.cpp +++ b/qt-gui.cpp @@ -2,7 +2,7 @@ /* Qt UI implementation */ #include "dive.h" #include "display.h" -#include "qt-ui/mainwindow.h" +#include "desktop-widgets/mainwindow.h" #include "helpers.h" #include diff --git a/qt-models/diveplotdatamodel.cpp b/qt-models/diveplotdatamodel.cpp index f219947ac..de156bfac 100644 --- a/qt-models/diveplotdatamodel.cpp +++ b/qt-models/diveplotdatamodel.cpp @@ -1,8 +1,8 @@ #include "diveplotdatamodel.h" #include "dive.h" #include "profile.h" -#include "graphicsview-common.h" #include "divelist.h" +#include "subsurface-core/color.h" DivePlotDataModel::DivePlotDataModel(QObject *parent) : QAbstractTableModel(parent), diff --git a/qt-models/filtermodels.cpp b/qt-models/filtermodels.cpp index 29853cb3d..f56f4be1c 100644 --- a/qt-models/filtermodels.cpp +++ b/qt-models/filtermodels.cpp @@ -1,7 +1,9 @@ #include "filtermodels.h" #include "models.h" -#include "divelistview.h" #include "display.h" +#include "divetripmodel.h" + +#include #define CREATE_INSTANCE_METHOD( CLASS ) \ CLASS *CLASS::instance() \ diff --git a/qt-ui/CMakeLists.txt b/qt-ui/CMakeLists.txt deleted file mode 100644 index 45447771a..000000000 --- a/qt-ui/CMakeLists.txt +++ /dev/null @@ -1,113 +0,0 @@ -# create the libraries -file(GLOB SUBSURFACE_UI *.ui) -qt5_wrap_ui(SUBSURFACE_UI_HDRS ${SUBSURFACE_UI}) -qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc) -source_group("Subsurface Interface Files" FILES ${SUBSURFACE_UI}) - -if(BTSUPPORT) - set(BT_SRC_FILES btdeviceselectiondialog.cpp) -endif() - -include_directories(. - ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_BINARY_DIR} -) - -# the interface, in C++ -set(SUBSURFACE_INTERFACE - updatemanager.cpp - about.cpp - divecomputermanagementdialog.cpp - divelistview.cpp - diveplanner.cpp - diveshareexportdialog.cpp - downloadfromdivecomputer.cpp - globe.cpp - graphicsview-common.cpp - kmessagewidget.cpp - maintab.cpp - mainwindow.cpp - modeldelegates.cpp - metrics.cpp - notificationwidget.cpp - preferences.cpp - simplewidgets.cpp - starwidget.cpp - subsurfacewebservices.cpp - tableview.cpp - divelogimportdialog.cpp - tagwidget.cpp - groupedlineedit.cpp - divelogexportdialog.cpp - divepicturewidget.cpp - usersurvey.cpp - configuredivecomputerdialog.cpp - undocommands.cpp - locationinformation.cpp - qtwaitingspinner.cpp -) - -if(NOT NO_USERMANUAL) - set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} - usermanual.cpp - ) -endif() - -if(NOT NO_PRINTING) - set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} - templateedit.cpp - printdialog.cpp - printoptions.cpp - printer.cpp - templatelayout.cpp - ) -endif() - -if (FBSUPPORT) - set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} - socialnetworks.cpp - ) -endif() - -if (BTSUPPORT) - set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} - btdeviceselectiondialog.cpp - ) -endif() - -source_group("Subsurface Interface" FILES ${SUBSURFACE_INTERFACE}) - -# the profile widget -set(SUBSURFACE_PROFILE_LIB_SRCS - profile/profilewidget2.cpp - profile/diverectitem.cpp - profile/divepixmapitem.cpp - profile/divelineitem.cpp - profile/divetextitem.cpp - profile/animationfunctions.cpp - profile/divecartesianaxis.cpp - profile/diveprofileitem.cpp - profile/diveeventitem.cpp - profile/divetooltipitem.cpp - profile/ruleritem.cpp - profile/tankitem.cpp -) -source_group("Subsurface Profile" FILES ${SUBSURFACE_PROFILE_LIB_SRCS}) - -# the yearly statistics widget. -set(SUBSURFACE_STATISTICS_LIB_SRCS - statistics/statisticswidget.cpp - statistics/yearstatistics.cpp - statistics/statisticsbar.cpp - statistics/monthstatistics.cpp -) -source_group("Subsurface Statistics" FILES ${SUBSURFACE_STATISTICS_LIB_SRCS}) - -add_library(subsurface_profile STATIC ${SUBSURFACE_PROFILE_LIB_SRCS}) -target_link_libraries(subsurface_profile ${QT_LIBRARIES}) -add_library(subsurface_statistics STATIC ${SUBSURFACE_STATISTICS_LIB_SRCS}) -target_link_libraries(subsurface_statistics ${QT_LIBRARIES}) -add_library(subsurface_generated_ui STATIC ${SUBSURFACE_UI_HDRS}) -target_link_libraries(subsurface_generated_ui ${QT_LIBRARIES}) -add_library(subsurface_interface STATIC ${SUBSURFACE_INTERFACE}) -target_link_libraries(subsurface_interface ${QT_LIBRARIES} ${MARBLE_LIBRARIES}) diff --git a/qt-ui/about.cpp b/qt-ui/about.cpp deleted file mode 100644 index e0df55980..000000000 --- a/qt-ui/about.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "about.h" -#include "version.h" -#include -#include -#include - -SubsurfaceAbout::SubsurfaceAbout(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) -{ - ui.setupUi(this); - - setWindowModality(Qt::ApplicationModal); - QString versionString(subsurface_git_version()); - QStringList readableVersions = QStringList() << "4.4.96" << "4.5 Beta 1" << - "4.4.97" << "4.5 Beta 2" << - "4.4.98" << "4.5 Beta 3"; - if (readableVersions.contains(versionString)) - versionString = readableVersions[readableVersions.indexOf(versionString) + 1]; - - ui.aboutLabel->setText(tr("" - "Subsurface %1

" - "Multi-platform divelog software
" - "" - "Linus Torvalds, Dirk Hohndel, Tomaz Canabrava, and others, 2011-2015" - "").arg(versionString)); - - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -void SubsurfaceAbout::on_licenseButton_clicked() -{ - QDesktopServices::openUrl(QUrl("http://www.gnu.org/licenses/gpl-2.0.txt")); -} - -void SubsurfaceAbout::on_websiteButton_clicked() -{ - QDesktopServices::openUrl(QUrl("http://subsurface-divelog.org")); -} diff --git a/qt-ui/about.h b/qt-ui/about.h deleted file mode 100644 index 47423aea2..000000000 --- a/qt-ui/about.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef ABOUT_H -#define ABOUT_H - -#include -#include "ui_about.h" - -class SubsurfaceAbout : public QDialog { - Q_OBJECT - -public: - explicit SubsurfaceAbout(QWidget *parent = 0, Qt::WindowFlags f = 0); -private -slots: - void on_licenseButton_clicked(); - void on_websiteButton_clicked(); - -private: - Ui::SubsurfaceAbout ui; -}; - -#endif // ABOUT_H diff --git a/qt-ui/about.ui b/qt-ui/about.ui deleted file mode 100644 index 0c1735e26..000000000 --- a/qt-ui/about.ui +++ /dev/null @@ -1,136 +0,0 @@ - - - SubsurfaceAbout - - - Qt::WindowModal - - - - 0 - 0 - 359 - 423 - - - - - 0 - 0 - - - - About Subsurface - - - - :/subsurface-icon:/subsurface-icon - - - true - - - - - - - - - :/subsurface-icon - - - Qt::AlignCenter - - - - - - - - - - Qt::RichText - - - Qt::AlignCenter - - - 10 - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - &License - - - - - - - &Website - - - - - - - &Close - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - closeButton - clicked() - SubsurfaceAbout - accept() - - - 290 - 411 - - - 340 - 409 - - - - - diff --git a/qt-ui/btdeviceselectiondialog.cpp b/qt-ui/btdeviceselectiondialog.cpp deleted file mode 100644 index fb2cbc1f3..000000000 --- a/qt-ui/btdeviceselectiondialog.cpp +++ /dev/null @@ -1,656 +0,0 @@ -#include -#include -#include -#include - -#include "ui_btdeviceselectiondialog.h" -#include "btdeviceselectiondialog.h" - -#if defined(Q_OS_WIN) -Q_DECLARE_METATYPE(QBluetoothDeviceDiscoveryAgent::Error) -#endif -#if QT_VERSION < 0x050500 -Q_DECLARE_METATYPE(QBluetoothDeviceInfo) -#endif - -BtDeviceSelectionDialog::BtDeviceSelectionDialog(QWidget *parent) : - QDialog(parent), - ui(new Ui::BtDeviceSelectionDialog), - remoteDeviceDiscoveryAgent(0) -{ - ui->setupUi(this); - - // Quit button callbacks - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), this, SLOT(reject())); - connect(ui->quit, SIGNAL(clicked()), this, SLOT(reject())); - - // Translate the UI labels - ui->localDeviceDetails->setTitle(tr("Local Bluetooth device details")); - ui->selectDeviceLabel->setText(tr("Select device:")); - ui->deviceAddressLabel->setText(tr("Address:")); - ui->deviceNameLabel->setText(tr("Name:")); - ui->deviceState->setText(tr("Bluetooth powered on")); - ui->changeDeviceState->setText(tr("Turn on/off")); - ui->discoveredDevicesLabel->setText(tr("Discovered devices")); - ui->scan->setText(tr("Scan")); - ui->clear->setText(tr("Clear")); - ui->save->setText(tr("Save")); - ui->quit->setText(tr("Quit")); - - // Disable the save button because there is no device selected - ui->save->setEnabled(false); - - // Add event for item selection - connect(ui->discoveredDevicesList, SIGNAL(itemClicked(QListWidgetItem*)), - this, SLOT(itemClicked(QListWidgetItem*))); - -#if defined(Q_OS_WIN) - ULONG ulRetCode = SUCCESS; - WSADATA WSAData = { 0 }; - - // Initialize WinSock and ask for version 2.2. - ulRetCode = WSAStartup(MAKEWORD(2, 2), &WSAData); - if (ulRetCode != SUCCESS) { - QMessageBox::StandardButton warningBox; - warningBox = QMessageBox::critical(this, "Bluetooth", - tr("Could not initialize Winsock version 2.2"), QMessageBox::Ok); - return; - } - - // Initialize the device discovery agent - initializeDeviceDiscoveryAgent(); - - // On Windows we cannot select a device or show information about the local device - ui->localDeviceDetails->hide(); -#else - // Initialize the local Bluetooth device - localDevice = new QBluetoothLocalDevice(); - - // Populate the list with local bluetooth devices - QList localAvailableDevices = localDevice->allDevices(); - int availableDevicesSize = localAvailableDevices.size(); - - if (availableDevicesSize > 1) { - int defaultDeviceIndex = -1; - - for (int it = 0; it < availableDevicesSize; it++) { - QBluetoothHostInfo localAvailableDevice = localAvailableDevices.at(it); - ui->localSelectedDevice->addItem(localAvailableDevice.name(), - QVariant::fromValue(localAvailableDevice.address())); - - if (localDevice->address() == localAvailableDevice.address()) - defaultDeviceIndex = it; - } - - // Positionate the current index to the default device and register to index changes events - ui->localSelectedDevice->setCurrentIndex(defaultDeviceIndex); - connect(ui->localSelectedDevice, SIGNAL(currentIndexChanged(int)), - this, SLOT(localDeviceChanged(int))); - } else { - // If there is only one local Bluetooth adapter hide the combobox and the label - ui->selectDeviceLabel->hide(); - ui->localSelectedDevice->hide(); - } - - // Update the UI information about the local device - updateLocalDeviceInformation(); - - // Initialize the device discovery agent - if (localDevice->isValid()) - initializeDeviceDiscoveryAgent(); -#endif -} - -BtDeviceSelectionDialog::~BtDeviceSelectionDialog() -{ - delete ui; - -#if defined(Q_OS_WIN) - // Terminate the use of Winsock 2 DLL - WSACleanup(); -#else - // Clean the local device - delete localDevice; -#endif - if (remoteDeviceDiscoveryAgent) { - // Clean the device discovery agent - if (remoteDeviceDiscoveryAgent->isActive()) { - remoteDeviceDiscoveryAgent->stop(); -#if defined(Q_OS_WIN) - remoteDeviceDiscoveryAgent->wait(); -#endif - } - - delete remoteDeviceDiscoveryAgent; - } -} - -void BtDeviceSelectionDialog::on_changeDeviceState_clicked() -{ -#if defined(Q_OS_WIN) - // TODO add implementation -#else - if (localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) { - ui->dialogStatus->setText(tr("Trying to turn on the local Bluetooth device...")); - localDevice->powerOn(); - } else { - ui->dialogStatus->setText(tr("Trying to turn off the local Bluetooth device...")); - localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); - } -#endif -} - -void BtDeviceSelectionDialog::on_save_clicked() -{ - // Get the selected device. There will be always a selected device if the save button is enabled. - QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); - QBluetoothDeviceInfo remoteDeviceInfo = currentItem->data(Qt::UserRole).value(); - - // Save the selected device - selectedRemoteDeviceInfo = QSharedPointer(new QBluetoothDeviceInfo(remoteDeviceInfo)); - - if (remoteDeviceDiscoveryAgent->isActive()) { - // Stop the SDP agent if the clear button is pressed and enable the Scan button - remoteDeviceDiscoveryAgent->stop(); -#if defined(Q_OS_WIN) - remoteDeviceDiscoveryAgent->wait(); -#endif - ui->scan->setEnabled(true); - } - - // Close the device selection dialog and set the result code to Accepted - accept(); -} - -void BtDeviceSelectionDialog::on_clear_clicked() -{ - ui->dialogStatus->setText(tr("Remote devices list was cleared.")); - ui->discoveredDevicesList->clear(); - ui->save->setEnabled(false); - - if (remoteDeviceDiscoveryAgent->isActive()) { - // Stop the SDP agent if the clear button is pressed and enable the Scan button - remoteDeviceDiscoveryAgent->stop(); -#if defined(Q_OS_WIN) - remoteDeviceDiscoveryAgent->wait(); -#endif - ui->scan->setEnabled(true); - } -} - -void BtDeviceSelectionDialog::on_scan_clicked() -{ - ui->dialogStatus->setText(tr("Scanning for remote devices...")); - ui->discoveredDevicesList->clear(); - remoteDeviceDiscoveryAgent->start(); - ui->scan->setEnabled(false); -} - -void BtDeviceSelectionDialog::remoteDeviceScanFinished() -{ - if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::NoError) { - ui->dialogStatus->setText(tr("Scanning finished successfully.")); - } else { - deviceDiscoveryError(remoteDeviceDiscoveryAgent->error()); - } - - ui->scan->setEnabled(true); -} - -void BtDeviceSelectionDialog::hostModeStateChanged(QBluetoothLocalDevice::HostMode mode) -{ -#if defined(Q_OS_WIN) - // TODO add implementation -#else - bool on = !(mode == QBluetoothLocalDevice::HostPoweredOff); - - //: %1 will be replaced with "turned on" or "turned off" - ui->dialogStatus->setText(tr("The local Bluetooth device was %1.") - .arg(on? tr("turned on") : tr("turned off"))); - ui->deviceState->setChecked(on); - ui->scan->setEnabled(on); -#endif -} - -void BtDeviceSelectionDialog::addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo) -{ -#if defined(Q_OS_WIN) - // On Windows we cannot obtain the pairing status so we set only the name and the address of the device - QString deviceLabel = QString("%1 (%2)").arg(remoteDeviceInfo.name(), - remoteDeviceInfo.address().toString()); - QColor pairingColor = QColor(Qt::white); -#else - // By default we use the status label and the color for the UNPAIRED state - QColor pairingColor = QColor(Qt::red); - QString pairingStatusLabel = tr("UNPAIRED"); - QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); - - if (pairingStatus == QBluetoothLocalDevice::Paired) { - pairingStatusLabel = tr("PAIRED"); - pairingColor = QColor(Qt::gray); - } else if (pairingStatus == QBluetoothLocalDevice::AuthorizedPaired) { - pairingStatusLabel = tr("AUTHORIZED_PAIRED"); - pairingColor = QColor(Qt::blue); - } - - QString deviceLabel = tr("%1 (%2) [State: %3]").arg(remoteDeviceInfo.name(), - remoteDeviceInfo.address().toString(), - pairingStatusLabel); -#endif - // Create the new item, set its information and add it to the list - QListWidgetItem *item = new QListWidgetItem(deviceLabel); - - item->setData(Qt::UserRole, QVariant::fromValue(remoteDeviceInfo)); - item->setBackgroundColor(pairingColor); - - ui->discoveredDevicesList->addItem(item); -} - -void BtDeviceSelectionDialog::itemClicked(QListWidgetItem *item) -{ - // By default we assume that the devices are paired - QBluetoothDeviceInfo remoteDeviceInfo = item->data(Qt::UserRole).value(); - QString statusMessage = tr("The device %1 can be used for connection. You can press the Save button.") - .arg(remoteDeviceInfo.address().toString()); - bool enableSaveButton = true; - -#if !defined(Q_OS_WIN) - // On other platforms than Windows we can obtain the pairing status so if the devices are not paired we disable the button - QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); - - if (pairingStatus == QBluetoothLocalDevice::Unpaired) { - statusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") - .arg(remoteDeviceInfo.address().toString()); - enableSaveButton = false; - } -#endif - // Update the status message and the save button - ui->dialogStatus->setText(statusMessage); - ui->save->setEnabled(enableSaveButton); -} - -void BtDeviceSelectionDialog::localDeviceChanged(int index) -{ -#if defined(Q_OS_WIN) - // TODO add implementation -#else - QBluetoothAddress localDeviceSelectedAddress = ui->localSelectedDevice->itemData(index, Qt::UserRole).value(); - - // Delete the old localDevice - if (localDevice) - delete localDevice; - - // Create a new local device using the selected address - localDevice = new QBluetoothLocalDevice(localDeviceSelectedAddress); - - ui->dialogStatus->setText(tr("The local device was changed.")); - - // Clear the discovered devices list - on_clear_clicked(); - - // Update the UI information about the local device - updateLocalDeviceInformation(); - - // Initialize the device discovery agent - if (localDevice->isValid()) - initializeDeviceDiscoveryAgent(); -#endif -} - -void BtDeviceSelectionDialog::displayPairingMenu(const QPoint &pos) -{ -#if defined(Q_OS_WIN) - // TODO add implementation -#else - QMenu menu(this); - QAction *pairAction = menu.addAction(tr("Pair")); - QAction *removePairAction = menu.addAction(tr("Remove pairing")); - QAction *chosenAction = menu.exec(ui->discoveredDevicesList->viewport()->mapToGlobal(pos)); - QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); - QBluetoothDeviceInfo currentRemoteDeviceInfo = currentItem->data(Qt::UserRole).value(); - QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(currentRemoteDeviceInfo.address()); - - //TODO: disable the actions - if (pairingStatus == QBluetoothLocalDevice::Unpaired) { - pairAction->setEnabled(true); - removePairAction->setEnabled(false); - } else { - pairAction->setEnabled(false); - removePairAction->setEnabled(true); - } - - if (chosenAction == pairAction) { - ui->dialogStatus->setText(tr("Trying to pair device %1") - .arg(currentRemoteDeviceInfo.address().toString())); - localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Paired); - } else if (chosenAction == removePairAction) { - ui->dialogStatus->setText(tr("Trying to unpair device %1") - .arg(currentRemoteDeviceInfo.address().toString())); - localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Unpaired); - } -#endif -} - -void BtDeviceSelectionDialog::pairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) -{ - // Determine the color, the new pairing status and the log message. By default we assume that the devices are UNPAIRED. - QString remoteDeviceStringAddress = address.toString(); - QColor pairingColor = QColor(Qt::red); - QString pairingStatusLabel = tr("UNPAIRED"); - QString dialogStatusMessage = tr("Device %1 was unpaired.").arg(remoteDeviceStringAddress); - bool enableSaveButton = false; - - if (pairing == QBluetoothLocalDevice::Paired) { - pairingStatusLabel = tr("PAIRED"); - pairingColor = QColor(Qt::gray); - enableSaveButton = true; - dialogStatusMessage = tr("Device %1 was paired.").arg(remoteDeviceStringAddress); - } else if (pairing == QBluetoothLocalDevice::AuthorizedPaired) { - pairingStatusLabel = tr("AUTHORIZED_PAIRED"); - pairingColor = QColor(Qt::blue); - enableSaveButton = true; - dialogStatusMessage = tr("Device %1 was paired and is authorized.").arg(remoteDeviceStringAddress); - } - - // Find the items which represent the BTH device and update their state - QList items = ui->discoveredDevicesList->findItems(remoteDeviceStringAddress, Qt::MatchContains); - QRegularExpression pairingExpression = QRegularExpression(QString("%1|%2|%3").arg(tr("PAIRED"), - tr("AUTHORIZED_PAIRED"), - tr("UNPAIRED"))); - - for (int i = 0; i < items.count(); ++i) { - QListWidgetItem *item = items.at(i); - QString updatedDeviceLabel = item->text().replace(QRegularExpression(pairingExpression), - pairingStatusLabel); - - item->setText(updatedDeviceLabel); - item->setBackgroundColor(pairingColor); - } - - // Check if the updated device is the selected one from the list and inform the user that it can/cannot start the download mode - QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); - - if (currentItem != NULL && currentItem->text().contains(remoteDeviceStringAddress, Qt::CaseInsensitive)) { - if (pairing == QBluetoothLocalDevice::Unpaired) { - dialogStatusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") - .arg(remoteDeviceStringAddress); - } else { - dialogStatusMessage = tr("The device %1 can now be used for connection. You can press the Save button.") - .arg(remoteDeviceStringAddress); - } - } - - // Update the save button and the dialog status message - ui->save->setEnabled(enableSaveButton); - ui->dialogStatus->setText(dialogStatusMessage); -} - -void BtDeviceSelectionDialog::error(QBluetoothLocalDevice::Error error) -{ - ui->dialogStatus->setText(tr("Local device error: %1.") - .arg((error == QBluetoothLocalDevice::PairingError)? tr("Pairing error. If the remote device requires a custom PIN code, " - "please try to pair the devices using your operating system. ") - : tr("Unknown error"))); -} - -void BtDeviceSelectionDialog::deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error) -{ - QString errorDescription; - - switch (error) { - case QBluetoothDeviceDiscoveryAgent::PoweredOffError: - errorDescription = tr("The Bluetooth adaptor is powered off, power it on before doing discovery."); - break; - case QBluetoothDeviceDiscoveryAgent::InputOutputError: - errorDescription = tr("Writing to or reading from the device resulted in an error."); - break; - default: -#if defined(Q_OS_WIN) - errorDescription = remoteDeviceDiscoveryAgent->errorToString(); -#else - errorDescription = tr("An unknown error has occurred."); -#endif - break; - } - - ui->dialogStatus->setText(tr("Device discovery error: %1.").arg(errorDescription)); -} - -QString BtDeviceSelectionDialog::getSelectedDeviceAddress() -{ - if (selectedRemoteDeviceInfo) { - return selectedRemoteDeviceInfo.data()->address().toString(); - } - - return QString(); -} - -QString BtDeviceSelectionDialog::getSelectedDeviceName() -{ - if (selectedRemoteDeviceInfo) { - return selectedRemoteDeviceInfo.data()->name(); - } - - return QString(); -} - -void BtDeviceSelectionDialog::updateLocalDeviceInformation() -{ -#if defined(Q_OS_WIN) - // TODO add implementation -#else - // Check if the selected Bluetooth device can be accessed - if (!localDevice->isValid()) { - QString na = tr("Not available"); - - // Update the UI information - ui->deviceAddress->setText(na); - ui->deviceName->setText(na); - - // Announce the user that there is a problem with the selected local Bluetooth adapter - ui->dialogStatus->setText(tr("The local Bluetooth adapter cannot be accessed.")); - - // Disable the buttons - ui->save->setEnabled(false); - ui->scan->setEnabled(false); - ui->clear->setEnabled(false); - ui->changeDeviceState->setEnabled(false); - - return; - } - - // Set UI information about the local device - ui->deviceAddress->setText(localDevice->address().toString()); - ui->deviceName->setText(localDevice->name()); - - connect(localDevice, SIGNAL(hostModeStateChanged(QBluetoothLocalDevice::HostMode)), - this, SLOT(hostModeStateChanged(QBluetoothLocalDevice::HostMode))); - - // Initialize the state of the local device and activate/deactive the scan button - hostModeStateChanged(localDevice->hostMode()); - - // Add context menu for devices to be able to pair them - ui->discoveredDevicesList->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->discoveredDevicesList, SIGNAL(customContextMenuRequested(QPoint)), - this, SLOT(displayPairingMenu(QPoint))); - connect(localDevice, SIGNAL(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing)), - this, SLOT(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing))); - - connect(localDevice, SIGNAL(error(QBluetoothLocalDevice::Error)), - this, SLOT(error(QBluetoothLocalDevice::Error))); -#endif -} - -void BtDeviceSelectionDialog::initializeDeviceDiscoveryAgent() -{ -#if defined(Q_OS_WIN) - // Register QBluetoothDeviceInfo metatype - qRegisterMetaType(); - - // Register QBluetoothDeviceDiscoveryAgent metatype (Needed for QBluetoothDeviceDiscoveryAgent::Error) - qRegisterMetaType(); - - // Intialize the discovery agent - remoteDeviceDiscoveryAgent = new WinBluetoothDeviceDiscoveryAgent(this); -#else - // Intialize the discovery agent - remoteDeviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(localDevice->address()); - - // Test if the discovery agent was successfully created - if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError) { - ui->dialogStatus->setText(tr("The device discovery agent was not created because the %1 address does not " - "match the physical adapter address of any local Bluetooth device.") - .arg(localDevice->address().toString())); - ui->scan->setEnabled(false); - ui->clear->setEnabled(false); - return; - } -#endif - connect(remoteDeviceDiscoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)), - this, SLOT(addRemoteDevice(QBluetoothDeviceInfo))); - connect(remoteDeviceDiscoveryAgent, SIGNAL(finished()), - this, SLOT(remoteDeviceScanFinished())); - connect(remoteDeviceDiscoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), - this, SLOT(deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error))); -} - -#if defined(Q_OS_WIN) -WinBluetoothDeviceDiscoveryAgent::WinBluetoothDeviceDiscoveryAgent(QObject *parent) : QThread(parent) -{ - // Initialize the internal flags by their default values - running = false; - stopped = false; - lastError = QBluetoothDeviceDiscoveryAgent::NoError; - lastErrorToString = tr("No error"); -} - -WinBluetoothDeviceDiscoveryAgent::~WinBluetoothDeviceDiscoveryAgent() -{ -} - -bool WinBluetoothDeviceDiscoveryAgent::isActive() const -{ - return running; -} - -QString WinBluetoothDeviceDiscoveryAgent::errorToString() const -{ - return lastErrorToString; -} - -QBluetoothDeviceDiscoveryAgent::Error WinBluetoothDeviceDiscoveryAgent::error() const -{ - return lastError; -} - -void WinBluetoothDeviceDiscoveryAgent::run() -{ - // Initialize query for device and start the lookup service - WSAQUERYSET queryset; - HANDLE hLookup; - int result = SUCCESS; - - running = true; - lastError = QBluetoothDeviceDiscoveryAgent::NoError; - lastErrorToString = tr("No error"); - - memset(&queryset, 0, sizeof(WSAQUERYSET)); - queryset.dwSize = sizeof(WSAQUERYSET); - queryset.dwNameSpace = NS_BTH; - - // The LUP_CONTAINERS flag is used to signal that we are doing a device inquiry - // while LUP_FLUSHCACHE flag is used to flush the device cache for all inquiries - // and to do a fresh lookup instead. - result = WSALookupServiceBegin(&queryset, LUP_CONTAINERS | LUP_FLUSHCACHE, &hLookup); - - if (result != SUCCESS) { - // Get the last error and emit a signal - lastErrorToString = qt_error_string(); - lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError; - emit error(lastError); - - // Announce that the inquiry finished and restore the stopped flag - running = false; - stopped = false; - - return; - } - - // Declare the necessary variables to collect the information - BYTE buffer[4096]; - DWORD bufferLength = sizeof(buffer); - WSAQUERYSET *pResults = (WSAQUERYSET*)&buffer; - - memset(buffer, 0, sizeof(buffer)); - - pResults->dwSize = sizeof(WSAQUERYSET); - pResults->dwNameSpace = NS_BTH; - pResults->lpBlob = NULL; - - //Start looking for devices - while (result == SUCCESS && !stopped){ - // LUP_RETURN_NAME and LUP_RETURN_ADDR flags are used to return the name and the address of the discovered device - result = WSALookupServiceNext(hLookup, LUP_RETURN_NAME | LUP_RETURN_ADDR, &bufferLength, pResults); - - if (result == SUCCESS) { - // Found a device - QString deviceAddress(BTH_ADDR_BUF_LEN, Qt::Uninitialized); - DWORD addressSize = BTH_ADDR_BUF_LEN; - - // Collect the address of the device from the WSAQUERYSET - SOCKADDR_BTH *socketBthAddress = (SOCKADDR_BTH *) pResults->lpcsaBuffer->RemoteAddr.lpSockaddr; - - // Convert the BTH_ADDR to string - if (WSAAddressToStringW((LPSOCKADDR) socketBthAddress, - sizeof (*socketBthAddress), - NULL, - reinterpret_cast(deviceAddress.data()), - &addressSize - ) != 0) { - // Get the last error and emit a signal - lastErrorToString = qt_error_string(); - lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; - emit(lastError); - - break; - } - - // Remove the round parentheses - deviceAddress.remove(')'); - deviceAddress.remove('('); - - // Save the name of the discovered device and truncate the address - QString deviceName = QString(pResults->lpszServiceInstanceName); - deviceAddress.truncate(BTH_ADDR_PRETTY_STRING_LEN); - - // Create an object with information about the discovered device - QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(QBluetoothAddress(deviceAddress), deviceName, 0); - - // Raise a signal with information about the found remote device - emit deviceDiscovered(deviceInfo); - } else { - // Get the last error and emit a signal - lastErrorToString = qt_error_string(); - lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; - emit(lastError); - } - } - - // Announce that the inquiry finished and restore the stopped flag - running = false; - stopped = false; - - // Restore the error status - lastError = QBluetoothDeviceDiscoveryAgent::NoError; - - // End the lookup service - WSALookupServiceEnd(hLookup); -} - -void WinBluetoothDeviceDiscoveryAgent::stop() -{ - // Stop the inqury - stopped = true; -} -#endif diff --git a/qt-ui/btdeviceselectiondialog.h b/qt-ui/btdeviceselectiondialog.h deleted file mode 100644 index 7651f164b..000000000 --- a/qt-ui/btdeviceselectiondialog.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef BTDEVICESELECTIONDIALOG_H -#define BTDEVICESELECTIONDIALOG_H - -#include -#include -#include -#include -#include -#include - -#if defined(Q_OS_WIN) - #include - #include - #include - - #define SUCCESS 0 - #define BTH_ADDR_BUF_LEN 40 - #define BTH_ADDR_PRETTY_STRING_LEN 17 // there are 6 two-digit hex values and 5 colons - - #undef ERROR // this is already declared in our headers - #undef IGNORE // this is already declared in our headers - #undef DC_VERSION // this is already declared in libdivecomputer header -#endif - -namespace Ui { - class BtDeviceSelectionDialog; -} - -#if defined(Q_OS_WIN) -class WinBluetoothDeviceDiscoveryAgent : public QThread { - Q_OBJECT -signals: - void deviceDiscovered(const QBluetoothDeviceInfo &info); - void error(QBluetoothDeviceDiscoveryAgent::Error error); - -public: - WinBluetoothDeviceDiscoveryAgent(QObject *parent); - ~WinBluetoothDeviceDiscoveryAgent(); - bool isActive() const; - QString errorToString() const; - QBluetoothDeviceDiscoveryAgent::Error error() const; - virtual void run(); - virtual void stop(); - -private: - bool running; - bool stopped; - QString lastErrorToString; - QBluetoothDeviceDiscoveryAgent::Error lastError; -}; -#endif - -class BtDeviceSelectionDialog : public QDialog { - Q_OBJECT - -public: - explicit BtDeviceSelectionDialog(QWidget *parent = 0); - ~BtDeviceSelectionDialog(); - QString getSelectedDeviceAddress(); - QString getSelectedDeviceName(); - -private slots: - void on_changeDeviceState_clicked(); - void on_save_clicked(); - void on_clear_clicked(); - void on_scan_clicked(); - void remoteDeviceScanFinished(); - void hostModeStateChanged(QBluetoothLocalDevice::HostMode mode); - void addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo); - void itemClicked(QListWidgetItem *item); - void displayPairingMenu(const QPoint &pos); - void pairingFinished(const QBluetoothAddress &address,QBluetoothLocalDevice::Pairing pairing); - void error(QBluetoothLocalDevice::Error error); - void deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error); - void localDeviceChanged(int); - -private: - Ui::BtDeviceSelectionDialog *ui; -#if defined(Q_OS_WIN) - WinBluetoothDeviceDiscoveryAgent *remoteDeviceDiscoveryAgent; -#else - QBluetoothLocalDevice *localDevice; - QBluetoothDeviceDiscoveryAgent *remoteDeviceDiscoveryAgent; -#endif - QSharedPointer selectedRemoteDeviceInfo; - - void updateLocalDeviceInformation(); - void initializeDeviceDiscoveryAgent(); -}; - -#endif // BTDEVICESELECTIONDIALOG_H diff --git a/qt-ui/btdeviceselectiondialog.ui b/qt-ui/btdeviceselectiondialog.ui deleted file mode 100644 index 4aa83cf1c..000000000 --- a/qt-ui/btdeviceselectiondialog.ui +++ /dev/null @@ -1,224 +0,0 @@ - - - BtDeviceSelectionDialog - - - - 0 - 0 - 735 - 460 - - - - Remote Bluetooth device selection - - - - - - - 0 - 0 - - - - - 75 - true - - - - Discovered devices - - - - - - - - - Save - - - - - - - - 0 - 0 - - - - Quit - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - 0 - 0 - - - - Scan - - - - - - - - 0 - 0 - - - - Clear - - - - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Local Bluetooth device details - - - false - - - - - - Name: - - - - - - - true - - - - - - - Address: - - - - - - - true - - - - - - - false - - - - 0 - 0 - - - - - 75 - true - - - - Bluetooth powered on - - - true - - - - - - - - 0 - 0 - - - - - 50 - false - - - - Turn on/off - - - - - - - - - - Select device: - - - - - - - - - - - - - true - - - - - - - - diff --git a/qt-ui/configuredivecomputerdialog.cpp b/qt-ui/configuredivecomputerdialog.cpp deleted file mode 100644 index ddb9450de..000000000 --- a/qt-ui/configuredivecomputerdialog.cpp +++ /dev/null @@ -1,1257 +0,0 @@ -#include "configuredivecomputerdialog.h" - -#include "helpers.h" -#include "mainwindow.h" -#include "display.h" - -#include -#include -#include -#include -#include - -struct product { - const char *product; - dc_descriptor_t *descriptor; - struct product *next; -}; - -struct vendor { - const char *vendor; - struct product *productlist; - struct vendor *next; -}; - -struct mydescriptor { - const char *vendor; - const char *product; - dc_family_t type; - unsigned int model; -}; - -GasSpinBoxItemDelegate::GasSpinBoxItemDelegate(QObject *parent, column_type type) : QStyledItemDelegate(parent), type(type) -{ -} -GasSpinBoxItemDelegate::~GasSpinBoxItemDelegate() -{ -} - -QWidget *GasSpinBoxItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - // Create the spinbox and give it it's settings - QSpinBox *sb = new QSpinBox(parent); - if (type == PERCENT) { - sb->setMinimum(0); - sb->setMaximum(100); - sb->setSuffix("%"); - } else if (type == DEPTH) { - sb->setMinimum(0); - sb->setMaximum(255); - sb->setSuffix(" m"); - } else if (type == SETPOINT) { - sb->setMinimum(0); - sb->setMaximum(255); - sb->setSuffix(" cbar"); - } - return sb; -} - -void GasSpinBoxItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - if (QSpinBox *sb = qobject_cast(editor)) - sb->setValue(index.data(Qt::EditRole).toInt()); - else - QStyledItemDelegate::setEditorData(editor, index); -} - - -void GasSpinBoxItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const -{ - if (QSpinBox *sb = qobject_cast(editor)) - model->setData(index, sb->value(), Qt::EditRole); - else - QStyledItemDelegate::setModelData(editor, model, index); -} - -GasTypeComboBoxItemDelegate::GasTypeComboBoxItemDelegate(QObject *parent, computer_type type) : QStyledItemDelegate(parent), type(type) -{ -} -GasTypeComboBoxItemDelegate::~GasTypeComboBoxItemDelegate() -{ -} - -QWidget *GasTypeComboBoxItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - // Create the combobox and populate it - QComboBox *cb = new QComboBox(parent); - cb->addItem(QString("Disabled")); - if (type == OSTC3) { - cb->addItem(QString("First")); - cb->addItem(QString("Travel")); - cb->addItem(QString("Deco")); - } else if (type == OSTC) { - cb->addItem(QString("Active")); - cb->addItem(QString("First")); - } - return cb; -} - -void GasTypeComboBoxItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - if (QComboBox *cb = qobject_cast(editor)) - cb->setCurrentIndex(index.data(Qt::EditRole).toInt()); - else - QStyledItemDelegate::setEditorData(editor, index); -} - - -void GasTypeComboBoxItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const -{ - if (QComboBox *cb = qobject_cast(editor)) - model->setData(index, cb->currentIndex(), Qt::EditRole); - else - QStyledItemDelegate::setModelData(editor, model, index); -} - -ConfigureDiveComputerDialog::ConfigureDiveComputerDialog(QWidget *parent) : QDialog(parent), - config(0), -#ifdef BT_SUPPORT - deviceDetails(0), - btDeviceSelectionDialog(0) -#else - deviceDetails(0) -#endif -{ - ui.setupUi(this); - - deviceDetails = new DeviceDetails(this); - config = new ConfigureDiveComputer(); - connect(config, SIGNAL(progress(int)), ui.progressBar, SLOT(setValue(int))); - connect(config, SIGNAL(error(QString)), this, SLOT(configError(QString))); - connect(config, SIGNAL(message(QString)), this, SLOT(configMessage(QString))); - connect(config, SIGNAL(deviceDetailsChanged(DeviceDetails *)), - this, SLOT(deviceDetailsReceived(DeviceDetails *))); - connect(ui.retrieveDetails, SIGNAL(clicked()), this, SLOT(readSettings())); - connect(ui.resetButton, SIGNAL(clicked()), this, SLOT(resetSettings())); - ui.chooseLogFile->setEnabled(ui.logToFile->isChecked()); - connect(ui.chooseLogFile, SIGNAL(clicked()), this, SLOT(pickLogFile())); - connect(ui.logToFile, SIGNAL(stateChanged(int)), this, SLOT(checkLogFile(int))); - connect(ui.connectButton, SIGNAL(clicked()), this, SLOT(dc_open())); - connect(ui.disconnectButton, SIGNAL(clicked()), this, SLOT(dc_close())); -#ifdef BT_SUPPORT - connect(ui.bluetoothMode, SIGNAL(clicked(bool)), this, SLOT(selectRemoteBluetoothDevice())); -#else - ui.bluetoothMode->setVisible(false); -#endif - - memset(&device_data, 0, sizeof(device_data)); - fill_computer_list(); - if (default_dive_computer_device) - ui.device->setEditText(default_dive_computer_device); - - ui.DiveComputerList->setCurrentRow(0); - on_DiveComputerList_currentRowChanged(0); - - ui.ostc3GasTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); - ui.ostc3GasTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); - ui.ostc3GasTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC3)); - ui.ostc3GasTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); - ui.ostc3DilTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC3)); - ui.ostc3DilTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); - ui.ostc3SetPointTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::SETPOINT)); - ui.ostc3SetPointTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); - ui.ostcGasTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); - ui.ostcGasTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); - ui.ostcGasTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC)); - ui.ostcGasTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); - ui.ostcDilTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC)); - ui.ostcDilTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); - ui.ostcSetPointTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::SETPOINT)); - ui.ostcSetPointTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); - - QSettings settings; - settings.beginGroup("ConfigureDiveComputerDialog"); - settings.beginGroup("ostc3GasTable"); - for (int i = 0; i < ui.ostc3GasTable->columnCount(); i++) { - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - ui.ostc3GasTable->setColumnWidth(i, width.toInt()); - } - settings.endGroup(); - settings.beginGroup("ostc3DilTable"); - for (int i = 0; i < ui.ostc3DilTable->columnCount(); i++) { - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - ui.ostc3DilTable->setColumnWidth(i, width.toInt()); - } - settings.endGroup(); - settings.beginGroup("ostc3SetPointTable"); - for (int i = 0; i < ui.ostc3SetPointTable->columnCount(); i++) { - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - ui.ostc3SetPointTable->setColumnWidth(i, width.toInt()); - } - settings.endGroup(); - - settings.beginGroup("ostcGasTable"); - for (int i = 0; i < ui.ostcGasTable->columnCount(); i++) { - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - ui.ostcGasTable->setColumnWidth(i, width.toInt()); - } - settings.endGroup(); - settings.beginGroup("ostcDilTable"); - for (int i = 0; i < ui.ostcDilTable->columnCount(); i++) { - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - ui.ostcDilTable->setColumnWidth(i, width.toInt()); - } - settings.endGroup(); - settings.beginGroup("ostcSetPointTable"); - for (int i = 0; i < ui.ostcSetPointTable->columnCount(); i++) { - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - ui.ostcSetPointTable->setColumnWidth(i, width.toInt()); - } - settings.endGroup(); - settings.endGroup(); -} - -OstcFirmwareCheck::OstcFirmwareCheck(QString product) : parent(0) -{ - QUrl url; - memset(&devData, 1, sizeof(devData)); - if (product == "OSTC 3") { - url = QUrl("http://www.heinrichsweikamp.net/autofirmware/ostc3_changelog.txt"); - latestFirmwareHexFile = QString("http://www.heinrichsweikamp.net/autofirmware/ostc3_firmware.hex"); - } else if (product == "OSTC Sport") { - url = QUrl("http://www.heinrichsweikamp.net/autofirmware/ostc_sport_changelog.txt"); - latestFirmwareHexFile = QString("http://www.heinrichsweikamp.net/autofirmware/ostc_sport_firmware.hex"); - } else { // not one of the known dive computers - return; - } - connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(parseOstcFwVersion(QNetworkReply *))); - QNetworkRequest download(url); - manager.get(download); -} - -void OstcFirmwareCheck::parseOstcFwVersion(QNetworkReply *reply) -{ - QString parse = reply->readAll(); - int firstOpenBracket = parse.indexOf('['); - int firstCloseBracket = parse.indexOf(']'); - latestFirmwareAvailable = parse.mid(firstOpenBracket + 1, firstCloseBracket - firstOpenBracket - 1); - disconnect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(parseOstcFwVersion(QNetworkReply *))); -} - -void OstcFirmwareCheck::checkLatest(QWidget *_parent, device_data_t *data) -{ - devData = *data; - parent = _parent; - // If we didn't find a current firmware version stop this hole thing here. - if (latestFirmwareAvailable.isEmpty()) - return; - - // for now libdivecomputer gives us the firmware on device undecoded as integer - // for the OSTC that means highbyte.lowbyte is the version number - int firmwareOnDevice = devData.libdc_firmware; - QString firmwareOnDeviceString = QString("%1.%2").arg(firmwareOnDevice / 256).arg(firmwareOnDevice % 256); - - // Convert the latestFirmwareAvailable to a integear we can compare with - QStringList fwParts = latestFirmwareAvailable.split("."); - int latestFirmwareAvailableNumber = fwParts[0].toInt() * 256 + fwParts[1].toInt(); - if (latestFirmwareAvailableNumber > firmwareOnDevice) { - QMessageBox response(parent); - QString message = tr("You should update the firmware on your dive computer: you have version %1 but the latest stable version is %2") - .arg(firmwareOnDeviceString) - .arg(latestFirmwareAvailable); - if (strcmp(data->product, "OSTC Sport") == 0) - message += tr("\n\nPlease start Bluetooth on your OSTC Sport and do the same preparations as for a logbook download before continuing with the update"); - response.addButton(tr("Not now"), QMessageBox::RejectRole); - response.addButton(tr("Update firmware"), QMessageBox::AcceptRole); - response.setText(message); - response.setWindowTitle(tr("Firmware upgrade notice")); - response.setIcon(QMessageBox::Question); - response.setWindowModality(Qt::WindowModal); - int ret = response.exec(); - if (ret == QMessageBox::Accepted) - upgradeFirmware(); - } -} - -void OstcFirmwareCheck::upgradeFirmware() -{ - // start download of latestFirmwareHexFile - QString saveFileName = latestFirmwareHexFile; - saveFileName.replace("http://www.heinrichsweikamp.net/autofirmware/", ""); - saveFileName.replace("firmware", latestFirmwareAvailable); - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath().append(QDir::separator()).append(saveFileName); - storeFirmware = QFileDialog::getSaveFileName(parent, tr("Save the downloaded firmware as"), - filename, tr("HEX files (*.hex)")); - if (storeFirmware.isEmpty()) - return; - - connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveOstcFirmware(QNetworkReply *))); - QNetworkRequest download(latestFirmwareHexFile); - manager.get(download); -} - -void OstcFirmwareCheck::saveOstcFirmware(QNetworkReply *reply) -{ - // firmware is downloaded - // call config->startFirmwareUpdate() with that file and the device data - - QByteArray firmwareData = reply->readAll(); - QFile file(storeFirmware); - file.open(QIODevice::WriteOnly); - file.write(firmwareData); - file.close(); - QProgressDialog *dialog = new QProgressDialog("Updating firmware", "", 0, 100); - dialog->setCancelButton(0); - dialog->setAutoClose(true); - ConfigureDiveComputer *config = new ConfigureDiveComputer(); - connect(config, SIGNAL(message(QString)), dialog, SLOT(setLabelText(QString))); - connect(config, SIGNAL(error(QString)), dialog, SLOT(setLabelText(QString))); - connect(config, SIGNAL(progress(int)), dialog, SLOT(setValue(int))); - connect(dialog, SIGNAL(finished(int)), config, SLOT(dc_close())); - config->dc_open(&devData); - config->startFirmwareUpdate(storeFirmware, &devData); -} - -ConfigureDiveComputerDialog::~ConfigureDiveComputerDialog() -{ - delete config; -} - -void ConfigureDiveComputerDialog::closeEvent(QCloseEvent *event) -{ - dc_close(); - - QSettings settings; - settings.beginGroup("ConfigureDiveComputerDialog"); - settings.beginGroup("ostc3GasTable"); - for (int i = 0; i < ui.ostc3GasTable->columnCount(); i++) - settings.setValue(QString("colwidth%1").arg(i), ui.ostc3GasTable->columnWidth(i)); - settings.endGroup(); - settings.beginGroup("ostc3DilTable"); - for (int i = 0; i < ui.ostc3DilTable->columnCount(); i++) - settings.setValue(QString("colwidth%1").arg(i), ui.ostc3DilTable->columnWidth(i)); - settings.endGroup(); - settings.beginGroup("ostc3SetPointTable"); - for (int i = 0; i < ui.ostc3SetPointTable->columnCount(); i++) - settings.setValue(QString("colwidth%1").arg(i), ui.ostc3SetPointTable->columnWidth(i)); - settings.endGroup(); - - settings.beginGroup("ostcGasTable"); - for (int i = 0; i < ui.ostcGasTable->columnCount(); i++) - settings.setValue(QString("colwidth%1").arg(i), ui.ostcGasTable->columnWidth(i)); - settings.endGroup(); - settings.beginGroup("ostcDilTable"); - for (int i = 0; i < ui.ostcDilTable->columnCount(); i++) - settings.setValue(QString("colwidth%1").arg(i), ui.ostcDilTable->columnWidth(i)); - settings.endGroup(); - settings.beginGroup("ostcSetPointTable"); - for (int i = 0; i < ui.ostcSetPointTable->columnCount(); i++) - settings.setValue(QString("colwidth%1").arg(i), ui.ostcSetPointTable->columnWidth(i)); - settings.endGroup(); - settings.endGroup(); -} - - -static void fillDeviceList(const char *name, void *data) -{ - QComboBox *comboBox = (QComboBox *)data; - comboBox->addItem(name); -} - -void ConfigureDiveComputerDialog::fill_device_list(int dc_type) -{ - int deviceIndex; - ui.device->clear(); - deviceIndex = enumerate_devices(fillDeviceList, ui.device, dc_type); - if (deviceIndex >= 0) - ui.device->setCurrentIndex(deviceIndex); -} - -void ConfigureDiveComputerDialog::fill_computer_list() -{ - dc_iterator_t *iterator = NULL; - dc_descriptor_t *descriptor = NULL; - - struct mydescriptor *mydescriptor; - - dc_descriptor_iterator(&iterator); - while (dc_iterator_next(iterator, &descriptor) == DC_STATUS_SUCCESS) { - const char *vendor = dc_descriptor_get_vendor(descriptor); - const char *product = dc_descriptor_get_product(descriptor); - - if (!vendorList.contains(vendor)) - vendorList.append(vendor); - - if (!productList[vendor].contains(product)) - productList[vendor].push_back(product); - - descriptorLookup[QString(vendor) + QString(product)] = descriptor; - } - dc_iterator_free(iterator); - - mydescriptor = (struct mydescriptor *)malloc(sizeof(struct mydescriptor)); - mydescriptor->vendor = "Uemis"; - mydescriptor->product = "Zurich"; - mydescriptor->type = DC_FAMILY_NULL; - mydescriptor->model = 0; - - if (!vendorList.contains("Uemis")) - vendorList.append("Uemis"); - - if (!productList["Uemis"].contains("Zurich")) - productList["Uemis"].push_back("Zurich"); - - descriptorLookup["UemisZurich"] = (dc_descriptor_t *)mydescriptor; - - qSort(vendorList); -} - -void ConfigureDiveComputerDialog::populateDeviceDetails() -{ - switch (ui.dcStackedWidget->currentIndex()) { - case 0: - populateDeviceDetailsOSTC3(); - break; - case 1: - populateDeviceDetailsSuuntoVyper(); - break; - case 2: - populateDeviceDetailsOSTC(); - break; - } -} - -#define GET_INT_FROM(_field, _default) ((_field) != NULL) ? (_field)->data(Qt::EditRole).toInt() : (_default) - -void ConfigureDiveComputerDialog::populateDeviceDetailsOSTC3() -{ - deviceDetails->customText = ui.customTextLlineEdit->text(); - deviceDetails->diveMode = ui.diveModeComboBox->currentIndex(); - deviceDetails->saturation = ui.saturationSpinBox->value(); - deviceDetails->desaturation = ui.desaturationSpinBox->value(); - deviceDetails->lastDeco = ui.lastDecoSpinBox->value(); - deviceDetails->brightness = ui.brightnessComboBox->currentIndex(); - deviceDetails->units = ui.unitsComboBox->currentIndex(); - deviceDetails->samplingRate = ui.samplingRateComboBox->currentIndex(); - deviceDetails->salinity = ui.salinitySpinBox->value(); - deviceDetails->diveModeColor = ui.diveModeColour->currentIndex(); - deviceDetails->language = ui.languageComboBox->currentIndex(); - deviceDetails->dateFormat = ui.dateFormatComboBox->currentIndex(); - deviceDetails->compassGain = ui.compassGainComboBox->currentIndex(); - deviceDetails->syncTime = ui.dateTimeSyncCheckBox->isChecked(); - deviceDetails->safetyStop = ui.safetyStopCheckBox->isChecked(); - deviceDetails->gfHigh = ui.gfHighSpinBox->value(); - deviceDetails->gfLow = ui.gfLowSpinBox->value(); - deviceDetails->pressureSensorOffset = ui.pressureSensorOffsetSpinBox->value(); - deviceDetails->ppO2Min = ui.ppO2MinSpinBox->value(); - deviceDetails->ppO2Max = ui.ppO2MaxSpinBox->value(); - deviceDetails->futureTTS = ui.futureTTSSpinBox->value(); - deviceDetails->ccrMode = ui.ccrModeComboBox->currentIndex(); - deviceDetails->decoType = ui.decoTypeComboBox->currentIndex(); - deviceDetails->aGFSelectable = ui.aGFSelectableCheckBox->isChecked(); - deviceDetails->aGFHigh = ui.aGFHighSpinBox->value(); - deviceDetails->aGFLow = ui.aGFLowSpinBox->value(); - deviceDetails->calibrationGas = ui.calibrationGasSpinBox->value(); - deviceDetails->flipScreen = ui.flipScreenCheckBox->isChecked(); - deviceDetails->setPointFallback = ui.setPointFallbackCheckBox->isChecked(); - deviceDetails->leftButtonSensitivity = ui.leftButtonSensitivity->value(); - deviceDetails->rightButtonSensitivity = ui.rightButtonSensitivity->value(); - deviceDetails->bottomGasConsumption = ui.bottomGasConsumption->value(); - deviceDetails->decoGasConsumption = ui.decoGasConsumption->value(); - deviceDetails->modWarning = ui.modWarning->isChecked(); - deviceDetails->dynamicAscendRate = ui.dynamicAscendRate->isChecked(); - deviceDetails->graphicalSpeedIndicator = ui.graphicalSpeedIndicator->isChecked(); - deviceDetails->alwaysShowppO2 = ui.alwaysShowppO2->isChecked(); - - //set gas values - gas gas1; - gas gas2; - gas gas3; - gas gas4; - gas gas5; - - gas1.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(0, 1), 21); - gas1.helium = GET_INT_FROM(ui.ostc3GasTable->item(0, 2), 0); - gas1.type = GET_INT_FROM(ui.ostc3GasTable->item(0, 3), 0); - gas1.depth = GET_INT_FROM(ui.ostc3GasTable->item(0, 4), 0); - - gas2.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(1, 1), 21); - gas2.helium = GET_INT_FROM(ui.ostc3GasTable->item(1, 2), 0); - gas2.type = GET_INT_FROM(ui.ostc3GasTable->item(1, 3), 0); - gas2.depth = GET_INT_FROM(ui.ostc3GasTable->item(1, 4), 0); - - gas3.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(2, 1), 21); - gas3.helium = GET_INT_FROM(ui.ostc3GasTable->item(2, 2), 0); - gas3.type = GET_INT_FROM(ui.ostc3GasTable->item(2, 3), 0); - gas3.depth = GET_INT_FROM(ui.ostc3GasTable->item(2, 4), 0); - - gas4.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(3, 1), 21); - gas4.helium = GET_INT_FROM(ui.ostc3GasTable->item(3, 2), 0); - gas4.type = GET_INT_FROM(ui.ostc3GasTable->item(3, 3), 0); - gas4.depth = GET_INT_FROM(ui.ostc3GasTable->item(3, 4), 0); - - gas5.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(4, 1), 21); - gas5.helium = GET_INT_FROM(ui.ostc3GasTable->item(4, 2), 0); - gas5.type = GET_INT_FROM(ui.ostc3GasTable->item(4, 3), 0); - gas5.depth = GET_INT_FROM(ui.ostc3GasTable->item(4, 4), 0); - - deviceDetails->gas1 = gas1; - deviceDetails->gas2 = gas2; - deviceDetails->gas3 = gas3; - deviceDetails->gas4 = gas4; - deviceDetails->gas5 = gas5; - - //set dil values - gas dil1; - gas dil2; - gas dil3; - gas dil4; - gas dil5; - - dil1.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(0, 1), 21); - dil1.helium = GET_INT_FROM(ui.ostc3DilTable->item(0, 2), 0); - dil1.type = GET_INT_FROM(ui.ostc3DilTable->item(0, 3), 0); - dil1.depth = GET_INT_FROM(ui.ostc3DilTable->item(0, 4), 0); - - dil2.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(1, 1), 21); - dil2.helium = GET_INT_FROM(ui.ostc3DilTable->item(1, 2), 0); - dil2.type = GET_INT_FROM(ui.ostc3DilTable->item(1, 3), 0); - dil2.depth = GET_INT_FROM(ui.ostc3DilTable->item(1, 4), 0); - - dil3.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(2, 1), 21); - dil3.helium = GET_INT_FROM(ui.ostc3DilTable->item(2, 2), 0); - dil3.type = GET_INT_FROM(ui.ostc3DilTable->item(2, 3), 0); - dil3.depth = GET_INT_FROM(ui.ostc3DilTable->item(2, 4), 0); - - dil4.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(3, 1), 21); - dil4.helium = GET_INT_FROM(ui.ostc3DilTable->item(3, 2), 0); - dil4.type = GET_INT_FROM(ui.ostc3DilTable->item(3, 3), 0); - dil4.depth = GET_INT_FROM(ui.ostc3DilTable->item(3, 4), 0); - - dil5.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(4, 1), 21); - dil5.helium = GET_INT_FROM(ui.ostc3DilTable->item(4, 2), 0); - dil5.type = GET_INT_FROM(ui.ostc3DilTable->item(4, 3), 0); - dil5.depth = GET_INT_FROM(ui.ostc3DilTable->item(4, 4), 0); - - deviceDetails->dil1 = dil1; - deviceDetails->dil2 = dil2; - deviceDetails->dil3 = dil3; - deviceDetails->dil4 = dil4; - deviceDetails->dil5 = dil5; - - //set set point details - setpoint sp1; - setpoint sp2; - setpoint sp3; - setpoint sp4; - setpoint sp5; - - sp1.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(0, 1), 70); - sp1.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(0, 2), 0); - - sp2.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(1, 1), 90); - sp2.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(1, 2), 20); - - sp3.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(2, 1), 100); - sp3.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(2, 2), 33); - - sp4.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(3, 1), 120); - sp4.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(3, 2), 50); - - sp5.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(4, 1), 140); - sp5.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(4, 2), 70); - - deviceDetails->sp1 = sp1; - deviceDetails->sp2 = sp2; - deviceDetails->sp3 = sp3; - deviceDetails->sp4 = sp4; - deviceDetails->sp5 = sp5; -} - -void ConfigureDiveComputerDialog::populateDeviceDetailsOSTC() -{ - deviceDetails->customText = ui.customTextLlineEdit_3->text(); - deviceDetails->saturation = ui.saturationSpinBox_3->value(); - deviceDetails->desaturation = ui.desaturationSpinBox_3->value(); - deviceDetails->lastDeco = ui.lastDecoSpinBox_3->value(); - deviceDetails->samplingRate = ui.samplingRateSpinBox_3->value(); - deviceDetails->salinity = ui.salinityDoubleSpinBox_3->value() * 100; - deviceDetails->dateFormat = ui.dateFormatComboBox_3->currentIndex(); - deviceDetails->syncTime = ui.dateTimeSyncCheckBox_3->isChecked(); - deviceDetails->safetyStop = ui.safetyStopCheckBox_3->isChecked(); - deviceDetails->gfHigh = ui.gfHighSpinBox_3->value(); - deviceDetails->gfLow = ui.gfLowSpinBox_3->value(); - deviceDetails->ppO2Min = ui.ppO2MinSpinBox_3->value(); - deviceDetails->ppO2Max = ui.ppO2MaxSpinBox_3->value(); - deviceDetails->futureTTS = ui.futureTTSSpinBox_3->value(); - deviceDetails->decoType = ui.decoTypeComboBox_3->currentIndex(); - deviceDetails->aGFSelectable = ui.aGFSelectableCheckBox_3->isChecked(); - deviceDetails->aGFHigh = ui.aGFHighSpinBox_3->value(); - deviceDetails->aGFLow = ui.aGFLowSpinBox_3->value(); - deviceDetails->bottomGasConsumption = ui.bottomGasConsumption_3->value(); - deviceDetails->decoGasConsumption = ui.decoGasConsumption_3->value(); - deviceDetails->graphicalSpeedIndicator = ui.graphicalSpeedIndicator_3->isChecked(); - - //set gas values - gas gas1; - gas gas2; - gas gas3; - gas gas4; - gas gas5; - - gas1.oxygen = GET_INT_FROM(ui.ostcGasTable->item(0, 1), 21); - gas1.helium = GET_INT_FROM(ui.ostcGasTable->item(0, 2), 0); - gas1.type = GET_INT_FROM(ui.ostcGasTable->item(0, 3), 0); - gas1.depth = GET_INT_FROM(ui.ostcGasTable->item(0, 4), 0); - - gas2.oxygen = GET_INT_FROM(ui.ostcGasTable->item(1, 1), 21); - gas2.helium = GET_INT_FROM(ui.ostcGasTable->item(1, 2), 0); - gas2.type = GET_INT_FROM(ui.ostcGasTable->item(1, 3), 0); - gas2.depth = GET_INT_FROM(ui.ostcGasTable->item(1, 4), 0); - - gas3.oxygen = GET_INT_FROM(ui.ostcGasTable->item(2, 1), 21); - gas3.helium = GET_INT_FROM(ui.ostcGasTable->item(2, 2), 0); - gas3.type = GET_INT_FROM(ui.ostcGasTable->item(2, 3), 0); - gas3.depth = GET_INT_FROM(ui.ostcGasTable->item(2, 4), 0); - - gas4.oxygen = GET_INT_FROM(ui.ostcGasTable->item(3, 1), 21); - gas4.helium = GET_INT_FROM(ui.ostcGasTable->item(3, 2), 0); - gas4.type = GET_INT_FROM(ui.ostcGasTable->item(3, 3), 0); - gas4.depth = GET_INT_FROM(ui.ostcGasTable->item(3, 4), 0); - - gas5.oxygen = GET_INT_FROM(ui.ostcGasTable->item(4, 1), 21); - gas5.helium = GET_INT_FROM(ui.ostcGasTable->item(4, 2), 0); - gas5.type = GET_INT_FROM(ui.ostcGasTable->item(4, 3), 0); - gas5.depth = GET_INT_FROM(ui.ostcGasTable->item(4, 4), 0); - - deviceDetails->gas1 = gas1; - deviceDetails->gas2 = gas2; - deviceDetails->gas3 = gas3; - deviceDetails->gas4 = gas4; - deviceDetails->gas5 = gas5; - - //set dil values - gas dil1; - gas dil2; - gas dil3; - gas dil4; - gas dil5; - - dil1.oxygen = GET_INT_FROM(ui.ostcDilTable->item(0, 1), 21); - dil1.helium = GET_INT_FROM(ui.ostcDilTable->item(0, 2), 0); - dil1.type = GET_INT_FROM(ui.ostcDilTable->item(0, 3), 0); - dil1.depth = GET_INT_FROM(ui.ostcDilTable->item(0, 4), 0); - - dil2.oxygen = GET_INT_FROM(ui.ostcDilTable->item(1, 1), 21); - dil2.helium = GET_INT_FROM(ui.ostcDilTable->item(1, 2), 0); - dil2.type = GET_INT_FROM(ui.ostcDilTable->item(1, 3), 0); - dil2.depth = GET_INT_FROM(ui.ostcDilTable->item(1, 4), 0); - - dil3.oxygen = GET_INT_FROM(ui.ostcDilTable->item(2, 1), 21); - dil3.helium = GET_INT_FROM(ui.ostcDilTable->item(2, 2), 0); - dil3.type = GET_INT_FROM(ui.ostcDilTable->item(2, 3), 0); - dil3.depth = GET_INT_FROM(ui.ostcDilTable->item(2, 4), 0); - - dil4.oxygen = GET_INT_FROM(ui.ostcDilTable->item(3, 1), 21); - dil4.helium = GET_INT_FROM(ui.ostcDilTable->item(3, 2), 0); - dil4.type = GET_INT_FROM(ui.ostcDilTable->item(3, 3), 0); - dil4.depth = GET_INT_FROM(ui.ostcDilTable->item(3, 4), 0); - - dil5.oxygen = GET_INT_FROM(ui.ostcDilTable->item(4, 1), 21); - dil5.helium = GET_INT_FROM(ui.ostcDilTable->item(4, 2), 0); - dil5.type = GET_INT_FROM(ui.ostcDilTable->item(4, 3), 0); - dil5.depth = GET_INT_FROM(ui.ostcDilTable->item(4, 4), 0); - - deviceDetails->dil1 = dil1; - deviceDetails->dil2 = dil2; - deviceDetails->dil3 = dil3; - deviceDetails->dil4 = dil4; - deviceDetails->dil5 = dil5; - - //set set point details - setpoint sp1; - setpoint sp2; - setpoint sp3; - setpoint sp4; - setpoint sp5; - - sp1.sp = GET_INT_FROM(ui.ostcSetPointTable->item(0, 1), 70); - sp1.depth = GET_INT_FROM(ui.ostcSetPointTable->item(0, 2), 0); - - sp2.sp = GET_INT_FROM(ui.ostcSetPointTable->item(1, 1), 90); - sp2.depth = GET_INT_FROM(ui.ostcSetPointTable->item(1, 2), 20); - - sp3.sp = GET_INT_FROM(ui.ostcSetPointTable->item(2, 1), 100); - sp3.depth = GET_INT_FROM(ui.ostcSetPointTable->item(2, 2), 33); - - sp4.sp = GET_INT_FROM(ui.ostcSetPointTable->item(3, 1), 120); - sp4.depth = GET_INT_FROM(ui.ostcSetPointTable->item(3, 2), 50); - - sp5.sp = GET_INT_FROM(ui.ostcSetPointTable->item(4, 1), 140); - sp5.depth = GET_INT_FROM(ui.ostcSetPointTable->item(4, 2), 70); - - deviceDetails->sp1 = sp1; - deviceDetails->sp2 = sp2; - deviceDetails->sp3 = sp3; - deviceDetails->sp4 = sp4; - deviceDetails->sp5 = sp5; -} - -void ConfigureDiveComputerDialog::populateDeviceDetailsSuuntoVyper() -{ - deviceDetails->customText = ui.customTextLlineEdit_1->text(); - deviceDetails->samplingRate = ui.samplingRateComboBox_1->currentIndex() == 3 ? 60 : (ui.samplingRateComboBox_1->currentIndex() + 1) * 10; - deviceDetails->altitude = ui.altitudeRangeComboBox->currentIndex(); - deviceDetails->personalSafety = ui.personalSafetyComboBox->currentIndex(); - deviceDetails->timeFormat = ui.timeFormatComboBox->currentIndex(); - deviceDetails->units = ui.unitsComboBox_1->currentIndex(); - deviceDetails->diveMode = ui.diveModeComboBox_1->currentIndex(); - deviceDetails->lightEnabled = ui.lightCheckBox->isChecked(); - deviceDetails->light = ui.lightSpinBox->value(); - deviceDetails->alarmDepthEnabled = ui.alarmDepthCheckBox->isChecked(); - deviceDetails->alarmDepth = units_to_depth(ui.alarmDepthDoubleSpinBox->value()); - deviceDetails->alarmTimeEnabled = ui.alarmTimeCheckBox->isChecked(); - deviceDetails->alarmTime = ui.alarmTimeSpinBox->value(); -} - -void ConfigureDiveComputerDialog::readSettings() -{ - ui.progressBar->setValue(0); - ui.progressBar->setFormat("%p%"); - ui.progressBar->setTextVisible(true); - // Fw update is no longer a option, needs to be done on a untouched device - ui.updateFirmwareButton->setEnabled(false); - - config->readSettings(&device_data); -} - -void ConfigureDiveComputerDialog::resetSettings() -{ - ui.progressBar->setValue(0); - ui.progressBar->setFormat("%p%"); - ui.progressBar->setTextVisible(true); - - config->resetSettings(&device_data); -} - -void ConfigureDiveComputerDialog::configMessage(QString msg) -{ - ui.progressBar->setFormat(msg); -} - -void ConfigureDiveComputerDialog::configError(QString err) -{ - ui.progressBar->setFormat("Error: " + err); -} - -void ConfigureDiveComputerDialog::getDeviceData() -{ - device_data.devname = strdup(ui.device->currentText().toUtf8().data()); - device_data.vendor = strdup(selected_vendor.toUtf8().data()); - device_data.product = strdup(selected_product.toUtf8().data()); - - device_data.descriptor = descriptorLookup[selected_vendor + selected_product]; - device_data.deviceid = device_data.diveid = 0; - - set_default_dive_computer_device(device_data.devname); -} - -void ConfigureDiveComputerDialog::on_cancel_clicked() -{ - this->close(); -} - -void ConfigureDiveComputerDialog::on_saveSettingsPushButton_clicked() -{ - ui.progressBar->setValue(0); - ui.progressBar->setFormat("%p%"); - ui.progressBar->setTextVisible(true); - - populateDeviceDetails(); - config->saveDeviceDetails(deviceDetails, &device_data); -} - -void ConfigureDiveComputerDialog::deviceDetailsReceived(DeviceDetails *newDeviceDetails) -{ - deviceDetails = newDeviceDetails; - reloadValues(); -} - -void ConfigureDiveComputerDialog::reloadValues() -{ - // Enable the buttons to do operations on this data - ui.saveSettingsPushButton->setEnabled(true); - ui.backupButton->setEnabled(true); - - switch (ui.dcStackedWidget->currentIndex()) { - case 0: - reloadValuesOSTC3(); - break; - case 1: - reloadValuesSuuntoVyper(); - break; - case 2: - reloadValuesOSTC(); - break; - } -} - -void ConfigureDiveComputerDialog::reloadValuesOSTC3() -{ - ui.serialNoLineEdit->setText(deviceDetails->serialNo); - ui.firmwareVersionLineEdit->setText(deviceDetails->firmwareVersion); - ui.customTextLlineEdit->setText(deviceDetails->customText); - ui.modelLineEdit->setText(deviceDetails->model); - ui.diveModeComboBox->setCurrentIndex(deviceDetails->diveMode); - ui.saturationSpinBox->setValue(deviceDetails->saturation); - ui.desaturationSpinBox->setValue(deviceDetails->desaturation); - ui.lastDecoSpinBox->setValue(deviceDetails->lastDeco); - ui.brightnessComboBox->setCurrentIndex(deviceDetails->brightness); - ui.unitsComboBox->setCurrentIndex(deviceDetails->units); - ui.samplingRateComboBox->setCurrentIndex(deviceDetails->samplingRate); - ui.salinitySpinBox->setValue(deviceDetails->salinity); - ui.diveModeColour->setCurrentIndex(deviceDetails->diveModeColor); - ui.languageComboBox->setCurrentIndex(deviceDetails->language); - ui.dateFormatComboBox->setCurrentIndex(deviceDetails->dateFormat); - ui.compassGainComboBox->setCurrentIndex(deviceDetails->compassGain); - ui.safetyStopCheckBox->setChecked(deviceDetails->safetyStop); - ui.gfHighSpinBox->setValue(deviceDetails->gfHigh); - ui.gfLowSpinBox->setValue(deviceDetails->gfLow); - ui.pressureSensorOffsetSpinBox->setValue(deviceDetails->pressureSensorOffset); - ui.ppO2MinSpinBox->setValue(deviceDetails->ppO2Min); - ui.ppO2MaxSpinBox->setValue(deviceDetails->ppO2Max); - ui.futureTTSSpinBox->setValue(deviceDetails->futureTTS); - ui.ccrModeComboBox->setCurrentIndex(deviceDetails->ccrMode); - ui.decoTypeComboBox->setCurrentIndex(deviceDetails->decoType); - ui.aGFSelectableCheckBox->setChecked(deviceDetails->aGFSelectable); - ui.aGFHighSpinBox->setValue(deviceDetails->aGFHigh); - ui.aGFLowSpinBox->setValue(deviceDetails->aGFLow); - ui.calibrationGasSpinBox->setValue(deviceDetails->calibrationGas); - ui.flipScreenCheckBox->setChecked(deviceDetails->flipScreen); - ui.setPointFallbackCheckBox->setChecked(deviceDetails->setPointFallback); - ui.leftButtonSensitivity->setValue(deviceDetails->leftButtonSensitivity); - ui.rightButtonSensitivity->setValue(deviceDetails->rightButtonSensitivity); - ui.bottomGasConsumption->setValue(deviceDetails->bottomGasConsumption); - ui.decoGasConsumption->setValue(deviceDetails->decoGasConsumption); - ui.modWarning->setChecked(deviceDetails->modWarning); - ui.dynamicAscendRate->setChecked(deviceDetails->dynamicAscendRate); - ui.graphicalSpeedIndicator->setChecked(deviceDetails->graphicalSpeedIndicator); - ui.alwaysShowppO2->setChecked(deviceDetails->alwaysShowppO2); - - //load gas 1 values - ui.ostc3GasTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->gas1.oxygen))); - ui.ostc3GasTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->gas1.helium))); - ui.ostc3GasTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->gas1.type))); - ui.ostc3GasTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->gas1.depth))); - - //load gas 2 values - ui.ostc3GasTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->gas2.oxygen))); - ui.ostc3GasTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->gas2.helium))); - ui.ostc3GasTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->gas2.type))); - ui.ostc3GasTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->gas2.depth))); - - //load gas 3 values - ui.ostc3GasTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->gas3.oxygen))); - ui.ostc3GasTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->gas3.helium))); - ui.ostc3GasTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->gas3.type))); - ui.ostc3GasTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->gas3.depth))); - - //load gas 4 values - ui.ostc3GasTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->gas4.oxygen))); - ui.ostc3GasTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->gas4.helium))); - ui.ostc3GasTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->gas4.type))); - ui.ostc3GasTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->gas4.depth))); - - //load gas 5 values - ui.ostc3GasTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->gas5.oxygen))); - ui.ostc3GasTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->gas5.helium))); - ui.ostc3GasTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->gas5.type))); - ui.ostc3GasTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->gas5.depth))); - - //load dil 1 values - ui.ostc3DilTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->dil1.oxygen))); - ui.ostc3DilTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->dil1.helium))); - ui.ostc3DilTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->dil1.type))); - ui.ostc3DilTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->dil1.depth))); - - //load dil 2 values - ui.ostc3DilTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->dil2.oxygen))); - ui.ostc3DilTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->dil2.helium))); - ui.ostc3DilTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->dil2.type))); - ui.ostc3DilTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->dil2.depth))); - - //load dil 3 values - ui.ostc3DilTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->dil3.oxygen))); - ui.ostc3DilTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->dil3.helium))); - ui.ostc3DilTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->dil3.type))); - ui.ostc3DilTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->dil3.depth))); - - //load dil 4 values - ui.ostc3DilTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->dil4.oxygen))); - ui.ostc3DilTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->dil4.helium))); - ui.ostc3DilTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->dil4.type))); - ui.ostc3DilTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->dil4.depth))); - - //load dil 5 values - ui.ostc3DilTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->dil5.oxygen))); - ui.ostc3DilTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->dil5.helium))); - ui.ostc3DilTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->dil5.type))); - ui.ostc3DilTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->dil5.depth))); - - //load set point 1 values - ui.ostc3SetPointTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->sp1.sp))); - ui.ostc3SetPointTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->sp1.depth))); - - //load set point 2 values - ui.ostc3SetPointTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->sp2.sp))); - ui.ostc3SetPointTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->sp2.depth))); - - //load set point 3 values - ui.ostc3SetPointTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->sp3.sp))); - ui.ostc3SetPointTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->sp3.depth))); - - //load set point 4 values - ui.ostc3SetPointTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->sp4.sp))); - ui.ostc3SetPointTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->sp4.depth))); - - //load set point 5 values - ui.ostc3SetPointTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->sp5.sp))); - ui.ostc3SetPointTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->sp5.depth))); -} - -void ConfigureDiveComputerDialog::reloadValuesOSTC() -{ - /* -# Not in OSTC -setBrightness -setCalibrationGas -setCompassGain -setDiveMode - Bult into setDecoType -setDiveModeColor - Lots of different colors -setFlipScreen -setLanguage -setPressureSensorOffset -setUnits -setSetPointFallback -setCcrMode -# Not in OSTC3 -setNumberOfDives -*/ - ui.serialNoLineEdit_3->setText(deviceDetails->serialNo); - ui.firmwareVersionLineEdit_3->setText(deviceDetails->firmwareVersion); - ui.customTextLlineEdit_3->setText(deviceDetails->customText); - ui.saturationSpinBox_3->setValue(deviceDetails->saturation); - ui.desaturationSpinBox_3->setValue(deviceDetails->desaturation); - ui.lastDecoSpinBox_3->setValue(deviceDetails->lastDeco); - ui.samplingRateSpinBox_3->setValue(deviceDetails->samplingRate); - ui.salinityDoubleSpinBox_3->setValue((double)deviceDetails->salinity / 100.0); - ui.dateFormatComboBox_3->setCurrentIndex(deviceDetails->dateFormat); - ui.safetyStopCheckBox_3->setChecked(deviceDetails->safetyStop); - ui.gfHighSpinBox_3->setValue(deviceDetails->gfHigh); - ui.gfLowSpinBox_3->setValue(deviceDetails->gfLow); - ui.ppO2MinSpinBox_3->setValue(deviceDetails->ppO2Min); - ui.ppO2MaxSpinBox_3->setValue(deviceDetails->ppO2Max); - ui.futureTTSSpinBox_3->setValue(deviceDetails->futureTTS); - ui.decoTypeComboBox_3->setCurrentIndex(deviceDetails->decoType); - ui.aGFSelectableCheckBox_3->setChecked(deviceDetails->aGFSelectable); - ui.aGFHighSpinBox_3->setValue(deviceDetails->aGFHigh); - ui.aGFLowSpinBox_3->setValue(deviceDetails->aGFLow); - ui.numberOfDivesSpinBox_3->setValue(deviceDetails->numberOfDives); - ui.bottomGasConsumption_3->setValue(deviceDetails->bottomGasConsumption); - ui.decoGasConsumption_3->setValue(deviceDetails->decoGasConsumption); - ui.graphicalSpeedIndicator_3->setChecked(deviceDetails->graphicalSpeedIndicator); - - //load gas 1 values - ui.ostcGasTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->gas1.oxygen))); - ui.ostcGasTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->gas1.helium))); - ui.ostcGasTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->gas1.type))); - ui.ostcGasTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->gas1.depth))); - - //load gas 2 values - ui.ostcGasTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->gas2.oxygen))); - ui.ostcGasTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->gas2.helium))); - ui.ostcGasTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->gas2.type))); - ui.ostcGasTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->gas2.depth))); - - //load gas 3 values - ui.ostcGasTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->gas3.oxygen))); - ui.ostcGasTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->gas3.helium))); - ui.ostcGasTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->gas3.type))); - ui.ostcGasTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->gas3.depth))); - - //load gas 4 values - ui.ostcGasTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->gas4.oxygen))); - ui.ostcGasTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->gas4.helium))); - ui.ostcGasTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->gas4.type))); - ui.ostcGasTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->gas4.depth))); - - //load gas 5 values - ui.ostcGasTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->gas5.oxygen))); - ui.ostcGasTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->gas5.helium))); - ui.ostcGasTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->gas5.type))); - ui.ostcGasTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->gas5.depth))); - - //load dil 1 values - ui.ostcDilTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->dil1.oxygen))); - ui.ostcDilTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->dil1.helium))); - ui.ostcDilTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->dil1.type))); - ui.ostcDilTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->dil1.depth))); - - //load dil 2 values - ui.ostcDilTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->dil2.oxygen))); - ui.ostcDilTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->dil2.helium))); - ui.ostcDilTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->dil2.type))); - ui.ostcDilTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->dil2.depth))); - - //load dil 3 values - ui.ostcDilTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->dil3.oxygen))); - ui.ostcDilTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->dil3.helium))); - ui.ostcDilTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->dil3.type))); - ui.ostcDilTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->dil3.depth))); - - //load dil 4 values - ui.ostcDilTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->dil4.oxygen))); - ui.ostcDilTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->dil4.helium))); - ui.ostcDilTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->dil4.type))); - ui.ostcDilTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->dil4.depth))); - - //load dil 5 values - ui.ostcDilTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->dil5.oxygen))); - ui.ostcDilTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->dil5.helium))); - ui.ostcDilTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->dil5.type))); - ui.ostcDilTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->dil5.depth))); - - //load set point 1 values - ui.ostcSetPointTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->sp1.sp))); - ui.ostcSetPointTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->sp1.depth))); - - //load set point 2 values - ui.ostcSetPointTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->sp2.sp))); - ui.ostcSetPointTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->sp2.depth))); - - //load set point 3 values - ui.ostcSetPointTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->sp3.sp))); - ui.ostcSetPointTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->sp3.depth))); - - //load set point 4 values - ui.ostcSetPointTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->sp4.sp))); - ui.ostcSetPointTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->sp4.depth))); - - //load set point 5 values - ui.ostcSetPointTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->sp5.sp))); - ui.ostcSetPointTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->sp5.depth))); -} - -void ConfigureDiveComputerDialog::reloadValuesSuuntoVyper() -{ - const char *depth_unit; - ui.maxDepthDoubleSpinBox->setValue(get_depth_units(deviceDetails->maxDepth, NULL, &depth_unit)); - ui.maxDepthDoubleSpinBox->setSuffix(depth_unit); - ui.totalTimeSpinBox->setValue(deviceDetails->totalTime); - ui.numberOfDivesSpinBox->setValue(deviceDetails->numberOfDives); - ui.modelLineEdit_1->setText(deviceDetails->model); - ui.firmwareVersionLineEdit_1->setText(deviceDetails->firmwareVersion); - ui.serialNoLineEdit_1->setText(deviceDetails->serialNo); - ui.customTextLlineEdit_1->setText(deviceDetails->customText); - ui.samplingRateComboBox_1->setCurrentIndex(deviceDetails->samplingRate == 60 ? 3 : (deviceDetails->samplingRate / 10) - 1); - ui.altitudeRangeComboBox->setCurrentIndex(deviceDetails->altitude); - ui.personalSafetyComboBox->setCurrentIndex(deviceDetails->personalSafety); - ui.timeFormatComboBox->setCurrentIndex(deviceDetails->timeFormat); - ui.unitsComboBox_1->setCurrentIndex(deviceDetails->units); - ui.diveModeComboBox_1->setCurrentIndex(deviceDetails->diveMode); - ui.lightCheckBox->setChecked(deviceDetails->lightEnabled); - ui.lightSpinBox->setValue(deviceDetails->light); - ui.alarmDepthCheckBox->setChecked(deviceDetails->alarmDepthEnabled); - ui.alarmDepthDoubleSpinBox->setValue(get_depth_units(deviceDetails->alarmDepth, NULL, &depth_unit)); - ui.alarmDepthDoubleSpinBox->setSuffix(depth_unit); - ui.alarmTimeCheckBox->setChecked(deviceDetails->alarmTimeEnabled); - ui.alarmTimeSpinBox->setValue(deviceDetails->alarmTime); -} - -void ConfigureDiveComputerDialog::on_backupButton_clicked() -{ - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath().append(QDir::separator()).append("Backup.xml"); - QString backupPath = QFileDialog::getSaveFileName(this, tr("Backup dive computer settings"), - filename, tr("Backup files (*.xml)")); - if (!backupPath.isEmpty()) { - populateDeviceDetails(); - if (!config->saveXMLBackup(backupPath, deviceDetails, &device_data)) { - QMessageBox::critical(this, tr("XML backup error"), - tr("An error occurred while saving the backup file.\n%1") - .arg(config->lastError)); - } else { - QMessageBox::information(this, tr("Backup succeeded"), - tr("Your settings have been saved to: %1") - .arg(backupPath)); - } - } -} - -void ConfigureDiveComputerDialog::on_restoreBackupButton_clicked() -{ - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath().append(QDir::separator()).append("Backup.xml"); - QString restorePath = QFileDialog::getOpenFileName(this, tr("Restore dive computer settings"), - filename, tr("Backup files (*.xml)")); - if (!restorePath.isEmpty()) { - // Fw update is no longer a option, needs to be done on a untouched device - ui.updateFirmwareButton->setEnabled(false); - if (!config->restoreXMLBackup(restorePath, deviceDetails)) { - QMessageBox::critical(this, tr("XML restore error"), - tr("An error occurred while restoring the backup file.\n%1") - .arg(config->lastError)); - } else { - reloadValues(); - QMessageBox::information(this, tr("Restore succeeded"), - tr("Your settings have been restored successfully.")); - } - } -} - -void ConfigureDiveComputerDialog::on_updateFirmwareButton_clicked() -{ - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath(); - QString firmwarePath = QFileDialog::getOpenFileName(this, tr("Select firmware file"), - filename, tr("All files (*.*)")); - if (!firmwarePath.isEmpty()) { - ui.progressBar->setValue(0); - ui.progressBar->setFormat("%p%"); - ui.progressBar->setTextVisible(true); - - config->startFirmwareUpdate(firmwarePath, &device_data); - } -} - - -void ConfigureDiveComputerDialog::on_DiveComputerList_currentRowChanged(int currentRow) -{ - switch (currentRow) { - case 0: - selected_vendor = "Heinrichs Weikamp"; - selected_product = "OSTC 3"; - fw_upgrade_possible = true; - break; - case 1: - selected_vendor = "Suunto"; - selected_product = "Vyper"; - fw_upgrade_possible = false; - break; - case 2: - selected_vendor = "Heinrichs Weikamp"; - selected_product = "OSTC 2N"; - fw_upgrade_possible = true; - break; - default: - /* Not Supported */ - return; - } - - int dcType = DC_TYPE_SERIAL; - - - if (selected_vendor == QString("Uemis")) - dcType = DC_TYPE_UEMIS; - fill_device_list(dcType); -} - -void ConfigureDiveComputerDialog::checkLogFile(int state) -{ - ui.chooseLogFile->setEnabled(state == Qt::Checked); - device_data.libdc_log = (state == Qt::Checked); - if (state == Qt::Checked && logFile.isEmpty()) { - pickLogFile(); - } -} - -void ConfigureDiveComputerDialog::pickLogFile() -{ - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath().append(QDir::separator()).append("subsurface.log"); - logFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer download logfile"), - filename, tr("Log files (*.log)")); - if (!logFile.isEmpty()) { - free(logfile_name); - logfile_name = strdup(logFile.toUtf8().data()); - } -} - -#ifdef BT_SUPPORT -void ConfigureDiveComputerDialog::selectRemoteBluetoothDevice() -{ - if (!btDeviceSelectionDialog) { - btDeviceSelectionDialog = new BtDeviceSelectionDialog(this); - connect(btDeviceSelectionDialog, SIGNAL(finished(int)), - this, SLOT(bluetoothSelectionDialogIsFinished(int))); - } - - btDeviceSelectionDialog->show(); -} - -void ConfigureDiveComputerDialog::bluetoothSelectionDialogIsFinished(int result) -{ - if (result == QDialog::Accepted) { - ui.device->setCurrentText(btDeviceSelectionDialog->getSelectedDeviceAddress()); - device_data.bluetooth_mode = true; - - ui.progressBar->setFormat("Connecting to device..."); - dc_open(); - } -} -#endif - -void ConfigureDiveComputerDialog::dc_open() -{ - getDeviceData(); - - QString err = config->dc_open(&device_data); - - if (err != "") - return ui.progressBar->setFormat(err); - - ui.retrieveDetails->setEnabled(true); - ui.resetButton->setEnabled(true); - ui.updateFirmwareButton->setEnabled(true); - ui.disconnectButton->setEnabled(true); - ui.restoreBackupButton->setEnabled(true); - ui.connectButton->setEnabled(false); - ui.bluetoothMode->setEnabled(false); - ui.DiveComputerList->setEnabled(false); - ui.logToFile->setEnabled(false); - if (fw_upgrade_possible) - ui.updateFirmwareButton->setEnabled(true); - ui.progressBar->setFormat("Connected to device"); -} - -void ConfigureDiveComputerDialog::dc_close() -{ - config->dc_close(&device_data); - - ui.retrieveDetails->setEnabled(false); - ui.resetButton->setEnabled(false); - ui.updateFirmwareButton->setEnabled(false); - ui.disconnectButton->setEnabled(false); - ui.connectButton->setEnabled(true); - ui.bluetoothMode->setEnabled(true); - ui.backupButton->setEnabled(false); - ui.saveSettingsPushButton->setEnabled(false); - ui.restoreBackupButton->setEnabled(false); - ui.DiveComputerList->setEnabled(true); - ui.logToFile->setEnabled(true); - ui.updateFirmwareButton->setEnabled(false); - ui.progressBar->setFormat("Disonnected from device"); - ui.progressBar->setValue(0); -} diff --git a/qt-ui/configuredivecomputerdialog.h b/qt-ui/configuredivecomputerdialog.h deleted file mode 100644 index 9ad30ac67..000000000 --- a/qt-ui/configuredivecomputerdialog.h +++ /dev/null @@ -1,149 +0,0 @@ -#ifndef CONFIGUREDIVECOMPUTERDIALOG_H -#define CONFIGUREDIVECOMPUTERDIALOG_H - -#include -#include -#include "ui_configuredivecomputerdialog.h" -#include "subsurface-core/libdivecomputer.h" -#include "configuredivecomputer.h" -#include -#include -#ifdef BT_SUPPORT -#include "btdeviceselectiondialog.h" -#endif - -class GasSpinBoxItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - enum column_type { - PERCENT, - DEPTH, - SETPOINT, - }; - - GasSpinBoxItemDelegate(QObject *parent = 0, column_type type = PERCENT); - ~GasSpinBoxItemDelegate(); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; - -private: - column_type type; -}; - -class GasTypeComboBoxItemDelegate : public QStyledItemDelegate { - Q_OBJECT - -public: - enum computer_type { - OSTC3, - OSTC, - }; - - GasTypeComboBoxItemDelegate(QObject *parent = 0, computer_type type = OSTC3); - ~GasTypeComboBoxItemDelegate(); - - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; - -private: - computer_type type; -}; - -class ConfigureDiveComputerDialog : public QDialog { - Q_OBJECT - -public: - explicit ConfigureDiveComputerDialog(QWidget *parent = 0); - ~ConfigureDiveComputerDialog(); - -protected: - void closeEvent(QCloseEvent *event); - -private -slots: - void checkLogFile(int state); - void pickLogFile(); - void readSettings(); - void resetSettings(); - void configMessage(QString msg); - void configError(QString err); - void on_cancel_clicked(); - void on_saveSettingsPushButton_clicked(); - void deviceDetailsReceived(DeviceDetails *newDeviceDetails); - void reloadValues(); - void on_backupButton_clicked(); - - void on_restoreBackupButton_clicked(); - - - void on_updateFirmwareButton_clicked(); - - void on_DiveComputerList_currentRowChanged(int currentRow); - - void dc_open(); - void dc_close(); - -#ifdef BT_SUPPORT - void bluetoothSelectionDialogIsFinished(int result); - void selectRemoteBluetoothDevice(); -#endif - -private: - Ui::ConfigureDiveComputerDialog ui; - - QString logFile; - - QStringList vendorList; - QHash productList; - - ConfigureDiveComputer *config; - device_data_t device_data; - void getDeviceData(); - - QHash descriptorLookup; - void fill_device_list(int dc_type); - void fill_computer_list(); - - DeviceDetails *deviceDetails; - void populateDeviceDetails(); - void populateDeviceDetailsOSTC3(); - void populateDeviceDetailsOSTC(); - void populateDeviceDetailsSuuntoVyper(); - void reloadValuesOSTC3(); - void reloadValuesOSTC(); - void reloadValuesSuuntoVyper(); - - QString selected_vendor; - QString selected_product; - bool fw_upgrade_possible; - -#ifdef BT_SUPPORT - BtDeviceSelectionDialog *btDeviceSelectionDialog; -#endif -}; - -class OstcFirmwareCheck : QObject { - Q_OBJECT -public: - explicit OstcFirmwareCheck(QString product); - void checkLatest(QWidget *parent, device_data_t *data); -public -slots: - void parseOstcFwVersion(QNetworkReply *reply); - void saveOstcFirmware(QNetworkReply *reply); - -private: - void upgradeFirmware(); - device_data_t devData; - QString latestFirmwareAvailable; - QString latestFirmwareHexFile; - QString storeFirmware; - QWidget *parent; - QNetworkAccessManager manager; -}; - -#endif // CONFIGUREDIVECOMPUTERDIALOG_H diff --git a/qt-ui/configuredivecomputerdialog.ui b/qt-ui/configuredivecomputerdialog.ui deleted file mode 100644 index 0986d71ca..000000000 --- a/qt-ui/configuredivecomputerdialog.ui +++ /dev/null @@ -1,2758 +0,0 @@ - - - ConfigureDiveComputerDialog - - - - 0 - 0 - 842 - 614 - - - - Configure dive computer - - - - - - - - Device or mount point - - - device - - - - - - - - - - 0 - 0 - - - - true - - - - - - - Connect via Bluetooth - - - - - - - Connect - - - - - - - false - - - Disconnect - - - - - - - - - - - - - false - - - Retrieve available details - - - - - - - false - - - Read settings from backup file or from device before writing to the device - - - Save changes to device - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - Read settings from backup file or from device before writing to a backup file - - - Backup - - - - - - - false - - - Restore backup - - - - - - - false - - - Update firmware - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Save libdivecomputer logfile - - - - - - - ... - - - - - - - Cancel - - - - - - - - - - 0 - 2 - - - - Qt::Horizontal - - - - - 240 - 16777215 - - - - - 12 - - - - - 64 - 64 - - - - - OSTC 3,Sport,Cr,2 - - - - :/icons/ostc3.png:/icons/ostc3.png - - - - - Suunto Vyper family - - - - :/icons/suunto_vyper.png:/icons/suunto_vyper.png - - - - - OSTC, Mk.2/2N/2C - - - - :/icons/ostc2n.png:/icons/ostc2n.png - - - - - - 0 - - - - - - - 0 - - - - Basic settings - - - - - - - English - - - - - German - - - - - French - - - - - Italian - - - - - - - - - m/°C - - - - - ft/°F - - - - - - - - Serial No. - - - serialNoLineEdit - - - - - - - - 1 - 0 - - - - true - - - - - - - Firmware version - - - firmwareVersionLineEdit - - - - - - - true - - - - - - - Language - - - languageComboBox - - - - - - - - MMDDYY - - - - - DDMMYY - - - - - YYMMDD - - - - - - - - - Eco - - - - - Medium - - - - - High - - - - - - - - Date format - - - dateFormatComboBox - - - - - - - Brightness - - - brightnessComboBox - - - - - - - Units - - - unitsComboBox - - - - - - - Salinity (0-5%) - - - salinitySpinBox - - - - - - - % - - - 5 - - - - - - - - 1 - 0 - - - - - 230LSB/Gauss - - - - - 330LSB/Gauss - - - - - 390LSB/Gauss - - - - - 440LSB/Gauss - - - - - 660LSB/Gauss - - - - - 820LSB/Gauss - - - - - 1090LSB/Gauss - - - - - 1370LSB/Gauss - - - - - - - - Qt::Vertical - - - - 20 - 177 - - - - - - - - false - - - Reset device to default settings - - - - - - - Compass gain - - - compassGainComboBox - - - - - - - Custom text - - - customTextLlineEdit - - - - - - - - 1 - 0 - - - - 60 - - - - - - - Computer model - - - - - - - true - - - - - - - Dive mode - - - diveModeComboBox - - - - - - - - OC - - - - - CC - - - - - Gauge - - - - - Apnea - - - - - - - - Sampling rate - - - samplingRateComboBox - - - - - - - - 2s - - - - - 10s - - - - - - - - Dive mode color - - - diveModeColour - - - - - - - - Standard - - - - - Red - - - - - Green - - - - - Blue - - - - - - - - Sync dive computer time with PC - - - - - - - Show safety stop - - - - - - - - Advanced settings - - - - - - Left button sensitivity - - - - - - - Always show ppO2 - - - - - - - Alt GF can be selected underwater - - - - - - - Future TTS - - - - - - - Pressure sensor offset - - - - - - - GFLow - - - - - - - % - - - 10 - - - 100 - - - 30 - - - - - - - GFHigh - - - - - - - % - - - 60 - - - 110 - - - 85 - - - - - - - Desaturation - - - desaturationSpinBox - - - - - - - % - - - 60 - - - 100 - - - 90 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - m - - - 3 - - - 6 - - - - - - - Decotype - - - - - - - false - - - % - - - 60 - - - 100 - - - 60 - - - - - - - mbar - - - -20 - - - 20 - - - - - - - 1 - - - - ZH-L16 - - - - - ZH-L16+GF - - - - - - - - min - - - 9 - - - - - - - Last deco - - - lastDecoSpinBox - - - - - - - % - - - 100 - - - 140 - - - 110 - - - - - - - Alt GFLow - - - - - - - false - - - % - - - 70 - - - 120 - - - 85 - - - - - - - Alt GFHigh - - - - - - - Saturation - - - saturationSpinBox - - - - - - - Flip screen - - - - - - - Right button sensitivity - - - - - - - MOD warning - - - - - - - Graphical speed indicator - - - - - - - Dynamic ascent rate - - - - - - - Bottom gas consumption - - - - - - - Deco gas consumption - - - - - - - % - - - 20 - - - 100 - - - 40 - - - - - - - % - - - 20 - - - 100 - - - 40 - - - - - - - â„“/min - - - 5 - - - 50 - - - 20 - - - - - - - â„“/min - - - 5 - - - 50 - - - 20 - - - - - - - - Gas settings - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %Oâ‚‚ - - - - - %He - - - - - Type - - - - - Change depth - - - - - Gas 1 - - - - - Gas 2 - - - - - Gas 3 - - - - - Gas 4 - - - - - Gas 5 - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %Oâ‚‚ - - - - - %He - - - - - Type - - - - - Change depth - - - - - Dil 1 - - - - - Dil 2 - - - - - Dil 3 - - - - - Dil 4 - - - - - Dil 5 - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Setpoint - - - - - Change depth - - - - - SP 1 - - - - - SP 2 - - - - - SP 3 - - - - - SP 4 - - - - - SP 5 - - - - - - - - Oâ‚‚ in calibration gas - - - - - - - % - - - 21 - - - 100 - - - 21 - - - - - - - - Fixed setpoint - - - - - Sensor - - - - - - - - Setpoint fallback - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - cbar - - - 120 - - - 160 - - - 160 - - - - - - - cbar - - - 16 - - - 19 - - - 19 - - - - - - - pOâ‚‚ max - - - - - - - pOâ‚‚ min - - - - - - - - - - - - - - - 0 - - - - Basic settings - - - - - - - 1 - 0 - - - - true - - - 200.000000000000000 - - - - - - - Safety level - - - - - - - - A0 (0m - 300m) - - - - - A1 (300m - 1500m) - - - - - A2 (1500m - 3000m) - - - - - - - - Altitude range - - - - - - - Model - - - - - - - - 1 - 0 - - - - 30 - - - - - - - Number of dives - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - Serial No. - - - serialNoLineEdit_1 - - - - - - - - 1 - 0 - - - - true - - - - - - - Firmware version - - - firmwareVersionLineEdit_1 - - - - - - - true - - - - - - - Max depth - - - - - - - true - - - 5000 - - - - - - - Custom text - - - customTextLlineEdit_1 - - - - - - - - Air - - - - - Nitrox - - - - - Gauge - - - - - - - - - P0 (none) - - - - - P1 (medium) - - - - - P2 (high) - - - - - - - - Sample rate - - - - - - - - 10s - - - - - 20s - - - - - 30s - - - - - 60s - - - - - - - - Total dive time - - - - - - - Computer model - - - - - - - true - - - - - - - true - - - min - - - 0 - - - 5000000 - - - - - - - - 24h - - - - - 12h - - - - - - - - Time format - - - - - - - Units - - - - - - - - Imperial - - - - - Metric - - - - - - - - false - - - s - - - - - - - Light - - - - - - - false - - - 200.000000000000000 - - - - - - - Depth alarm - - - - - - - false - - - min - - - 999 - - - - - - - Time alarm - - - - - - - - - - - - - - - 0 - - - - Basic settings - - - - - - Salinity - - - salinitySpinBox - - - - - - - Serial No. - - - serialNoLineEdit - - - - - - - - 1 - 0 - - - - true - - - - - - - Firmware version - - - firmwareVersionLineEdit_3 - - - - - - - true - - - - - - - Custom text - - - customTextLlineEdit_3 - - - - - - - - 1 - 0 - - - - 23 - - - - - - - kg/â„“ - - - 1.000000000000000 - - - 1.040000000000000 - - - 0.010000000000000 - - - - - - - Qt::Vertical - - - - 20 - 177 - - - - - - - - Sync dive computer time with PC - - - - - - - Show safety stop - - - - - - - - MM/DD/YY - - - - - DD/MM/YY - - - - - YY/MM/DD - - - - - - - - Number of dives - - - - - - - true - - - - - - - - 0 - 0 - - - - 1 - - - 120 - - - 10 - - - - - - - Sampling rate - - - samplingRateComboBox - - - - - - - Date format - - - dateFormatComboBox - - - - - - - - Advanced settings - - - - - - Alt GF can be selected underwater - - - - - - - Desaturation - - - desaturationSpinBox - - - - - - - Future TTS - - - - - - - % - - - 60 - - - 100 - - - 90 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - m - - - 3 - - - 6 - - - - - - - Decotype - - - - - - - 4 - - - - ZH-L16 - - - - - Gauge - - - - - ZH-L16 CC - - - - - Apnoea - - - - - L16-GF OC - - - - - L16-GF CC - - - - - PSCR-GF - - - - - - - - min - - - 9 - - - - - - - false - - - % - - - 5 - - - 255 - - - 30 - - - - - - - Last deco - - - lastDecoSpinBox - - - - - - - % - - - 100 - - - 140 - - - 110 - - - - - - - Alt GFLow - - - - - - - false - - - % - - - 5 - - - 255 - - - 90 - - - - - - - Alt GFHigh - - - - - - - Saturation - - - saturationSpinBox - - - - - - - GFHigh - - - - - - - % - - - 10 - - - 100 - - - 30 - - - - - - - GFLow - - - - - - - % - - - 60 - - - 110 - - - 85 - - - - - - - Graphical speed indicator - - - - - - - â„“/min - - - 5 - - - 50 - - - 20 - - - - - - - â„“/min - - - 5 - - - 50 - - - 20 - - - - - - - Bottom gas consumption - - - - - - - Deco gas consumption - - - - - - - - Gas settings - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %Oâ‚‚ - - - - - %He - - - - - Type - - - - - Change depth - - - - - Gas 1 - - - - - Gas 2 - - - - - Gas 3 - - - - - Gas 4 - - - - - Gas 5 - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %Oâ‚‚ - - - - - %He - - - - - Type - - - - - Change depth - - - - - Dil 1 - - - - - Dil 2 - - - - - Dil 3 - - - - - Dil 4 - - - - - Dil 5 - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - - - - - - Setpoint - - - - - Change depth - - - - - SP 1 - - - - - SP 2 - - - - - SP 3 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - cbar - - - 120 - - - 180 - - - 160 - - - - - - - cbar - - - 16 - - - 21 - - - 19 - - - - - - - pOâ‚‚ max - - - - - - - pOâ‚‚ min - - - - - - - - - - - - - - - - 0 - - - - - - - - - - device - retrieveDetails - saveSettingsPushButton - backupButton - restoreBackupButton - cancel - - - - - - - DiveComputerList - currentRowChanged(int) - dcStackedWidget - setCurrentIndex(int) - - - 20 - 20 - - - 20 - 20 - - - - - lightCheckBox - toggled(bool) - lightSpinBox - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - alarmDepthCheckBox - toggled(bool) - alarmDepthDoubleSpinBox - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - alarmTimeCheckBox - toggled(bool) - alarmTimeSpinBox - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - aGFSelectableCheckBox - toggled(bool) - aGFHighSpinBox - setEnabled(bool) - - - 340 - 229 - - - 686 - 265 - - - - - aGFSelectableCheckBox - toggled(bool) - aGFLowSpinBox - setEnabled(bool) - - - 340 - 229 - - - 686 - 229 - - - - - aGFSelectableCheckBox_3 - toggled(bool) - aGFHighSpinBox_3 - setEnabled(bool) - - - 340 - 229 - - - 686 - 265 - - - - - aGFSelectableCheckBox_3 - toggled(bool) - aGFLowSpinBox_3 - setEnabled(bool) - - - 340 - 229 - - - 686 - 229 - - - - - diff --git a/qt-ui/css/tableviews.css b/qt-ui/css/tableviews.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/divecomponentselection.ui b/qt-ui/divecomponentselection.ui deleted file mode 100644 index 8262a5c2a..000000000 --- a/qt-ui/divecomponentselection.ui +++ /dev/null @@ -1,190 +0,0 @@ - - - DiveComponentSelectionDialog - - - Qt::WindowModal - - - - 0 - 0 - 401 - 317 - - - - - 0 - 0 - - - - Component selection - - - - :/subsurface-icon - - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - 0 - 0 - - - - Which components would you like to copy - - - - 5 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Dive site - - - - - - - Suit - - - - - - - Visibility - - - - - - - Notes - - - - - - - Tags - - - - - - - Weights - - - - - - - Cylinders - - - - - - - Divemaster - - - - - - - Buddy - - - - - - - Rating - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - DiveComponentSelectionDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - DiveComponentSelectionDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/divecomputermanagementdialog.cpp b/qt-ui/divecomputermanagementdialog.cpp deleted file mode 100644 index fd9273ffb..000000000 --- a/qt-ui/divecomputermanagementdialog.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "divecomputermanagementdialog.h" -#include "mainwindow.h" -#include "helpers.h" -#include "divecomputermodel.h" -#include -#include - -DiveComputerManagementDialog::DiveComputerManagementDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), - model(0) -{ - ui.setupUi(this); - init(); - connect(ui.tableView, SIGNAL(clicked(QModelIndex)), this, SLOT(tryRemove(QModelIndex))); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -void DiveComputerManagementDialog::init() -{ - delete model; - model = new DiveComputerModel(dcList.dcMap); - ui.tableView->setModel(model); -} - -DiveComputerManagementDialog *DiveComputerManagementDialog::instance() -{ - static DiveComputerManagementDialog *self = new DiveComputerManagementDialog(MainWindow::instance()); - self->setAttribute(Qt::WA_QuitOnClose, false); - return self; -} - -void DiveComputerManagementDialog::update() -{ - model->update(); - ui.tableView->resizeColumnsToContents(); - ui.tableView->setColumnWidth(DiveComputerModel::REMOVE, 22); - layout()->activate(); -} - -void DiveComputerManagementDialog::tryRemove(const QModelIndex &index) -{ - if (index.column() != DiveComputerModel::REMOVE) - return; - - QMessageBox::StandardButton response = QMessageBox::question( - this, TITLE_OR_TEXT( - tr("Remove the selected dive computer?"), - tr("Are you sure that you want to \n remove the selected dive computer?")), - QMessageBox::Ok | QMessageBox::Cancel); - - if (response == QMessageBox::Ok) - model->remove(index); -} - -void DiveComputerManagementDialog::accept() -{ - model->keepWorkingList(); - hide(); - close(); -} - -void DiveComputerManagementDialog::reject() -{ - model->dropWorkingList(); - hide(); - close(); -} diff --git a/qt-ui/divecomputermanagementdialog.h b/qt-ui/divecomputermanagementdialog.h deleted file mode 100644 index d065a0208..000000000 --- a/qt-ui/divecomputermanagementdialog.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef DIVECOMPUTERMANAGEMENTDIALOG_H -#define DIVECOMPUTERMANAGEMENTDIALOG_H -#include -#include "ui_divecomputermanagementdialog.h" - -class QModelIndex; -class DiveComputerModel; - -class DiveComputerManagementDialog : public QDialog { - Q_OBJECT - -public: - static DiveComputerManagementDialog *instance(); - void update(); - void init(); - -public -slots: - void tryRemove(const QModelIndex &index); - void accept(); - void reject(); - -private: - explicit DiveComputerManagementDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); - Ui::DiveComputerManagementDialog ui; - DiveComputerModel *model; -}; - -#endif // DIVECOMPUTERMANAGEMENTDIALOG_H diff --git a/qt-ui/divecomputermanagementdialog.ui b/qt-ui/divecomputermanagementdialog.ui deleted file mode 100644 index 0e5db2c41..000000000 --- a/qt-ui/divecomputermanagementdialog.ui +++ /dev/null @@ -1,81 +0,0 @@ - - - DiveComputerManagementDialog - - - Qt::WindowModal - - - - 0 - 0 - 560 - 300 - - - - Edit dive computer nicknames - - - - :/subsurface-icon - - - - - - - true - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - accepted() - DiveComputerManagementDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - DiveComputerManagementDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/divelistview.cpp b/qt-ui/divelistview.cpp deleted file mode 100644 index d2386ecf1..000000000 --- a/qt-ui/divelistview.cpp +++ /dev/null @@ -1,1035 +0,0 @@ -/* - * divelistview.cpp - * - * classes for the divelist of Subsurface - * - */ -#include "filtermodels.h" -#include "modeldelegates.h" -#include "mainwindow.h" -#include "divepicturewidget.h" -#include "display.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include "qthelper.h" -#include "undocommands.h" -#include "divelistview.h" -#include "divepicturemodel.h" -#include "metrics.h" -#include "helpers.h" - -// # Date Rtg Dpth Dur Tmp Wght Suit Cyl Gas SAC OTU CNS Loc -static int defaultWidth[] = { 70, 140, 90, 50, 50, 50, 50, 70, 50, 50, 70, 50, 50, 500}; - -DiveListView::DiveListView(QWidget *parent) : QTreeView(parent), mouseClickSelection(false), sortColumn(0), - currentOrder(Qt::DescendingOrder), dontEmitDiveChangedSignal(false), selectionSaved(false) -{ - setItemDelegate(new DiveListDelegate(this)); - setUniformRowHeights(true); - setItemDelegateForColumn(DiveTripModel::RATING, new StarWidgetsDelegate(this)); - MultiFilterSortModel *model = MultiFilterSortModel::instance(); - model->setSortRole(DiveTripModel::SORT_ROLE); - model->setFilterKeyColumn(-1); // filter all columns - model->setFilterCaseSensitivity(Qt::CaseInsensitive); - setModel(model); - connect(model, SIGNAL(layoutChanged()), this, SLOT(fixMessyQtModelBehaviour())); - - setSortingEnabled(false); - setContextMenuPolicy(Qt::DefaultContextMenu); - setSelectionMode(ExtendedSelection); - header()->setContextMenuPolicy(Qt::ActionsContextMenu); - - const QFontMetrics metrics(defaultModelFont()); - int em = metrics.width('m'); - int zw = metrics.width('0'); - - // Fixes for the layout needed for mac -#ifdef Q_OS_MAC - int ht = metrics.height(); - header()->setMinimumHeight(ht + 4); -#endif - - // TODO FIXME we need this to get the header names - // can we find a smarter way? - DiveTripModel *tripModel = new DiveTripModel(this); - - // set the default width as a minimum between the hard-coded defaults, - // the header text width and the (assumed) content width, calculated - // based on type - for (int col = DiveTripModel::NR; col < DiveTripModel::COLUMNS; ++col) { - QString header_txt = tripModel->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString(); - int width = metrics.width(header_txt); - int sw = 0; - switch (col) { - case DiveTripModel::NR: - case DiveTripModel::DURATION: - sw = 8*zw; - break; - case DiveTripModel::DATE: - sw = 14*em; - break; - case DiveTripModel::RATING: - sw = static_cast(itemDelegateForColumn(col))->starSize().width(); - break; - case DiveTripModel::SUIT: - case DiveTripModel::SAC: - sw = 7*em; - break; - case DiveTripModel::LOCATION: - sw = 50*em; - break; - default: - sw = 5*em; - } - if (sw > width) - width = sw; - width += zw; // small padding - if (width > defaultWidth[col]) - defaultWidth[col] = width; - } - delete tripModel; - - - header()->setStretchLastSection(true); - - installEventFilter(this); -} - -DiveListView::~DiveListView() -{ - QSettings settings; - settings.beginGroup("ListWidget"); - // don't set a width for the last column - location is supposed to be "the rest" - for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS - 1; i++) { - if (isColumnHidden(i)) - continue; - // we used to hardcode them all to 100 - so that might still be in the settings - if (columnWidth(i) == 100 || columnWidth(i) == defaultWidth[i]) - settings.remove(QString("colwidth%1").arg(i)); - else - settings.setValue(QString("colwidth%1").arg(i), columnWidth(i)); - } - settings.remove(QString("colwidth%1").arg(DiveTripModel::COLUMNS - 1)); - settings.endGroup(); -} - -void DiveListView::setupUi() -{ - QSettings settings; - static bool firstRun = true; - if (firstRun) - backupExpandedRows(); - settings.beginGroup("ListWidget"); - /* if no width are set, use the calculated width for each column; - * for that to work we need to temporarily expand all rows */ - expandAll(); - for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS; i++) { - if (isColumnHidden(i)) - continue; - QVariant width = settings.value(QString("colwidth%1").arg(i)); - if (width.isValid()) - setColumnWidth(i, width.toInt()); - else - setColumnWidth(i, defaultWidth[i]); - } - settings.endGroup(); - if (firstRun) - restoreExpandedRows(); - else - collapseAll(); - firstRun = false; - setColumnWidth(lastVisibleColumn(), 10); -} - -int DiveListView::lastVisibleColumn() -{ - int lastColumn = -1; - for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS; i++) { - if (isColumnHidden(i)) - continue; - lastColumn = i; - } - return lastColumn; -} - -void DiveListView::backupExpandedRows() -{ - expandedRows.clear(); - for (int i = 0; i < model()->rowCount(); i++) - if (isExpanded(model()->index(i, 0))) - expandedRows.push_back(i); -} - -void DiveListView::restoreExpandedRows() -{ - setAnimated(false); - Q_FOREACH (const int &i, expandedRows) - setExpanded(model()->index(i, 0), true); - setAnimated(true); -} -void DiveListView::fixMessyQtModelBehaviour() -{ - QAbstractItemModel *m = model(); - for (int i = 0; i < model()->rowCount(); i++) - if (m->rowCount(m->index(i, 0)) != 0) - setFirstColumnSpanned(i, QModelIndex(), true); -} - -// this only remembers dives that were selected, not trips -void DiveListView::rememberSelection() -{ - selectedDives.clear(); - QItemSelection selection = selectionModel()->selection(); - Q_FOREACH (const QModelIndex &index, selection.indexes()) { - if (index.column() != 0) // We only care about the dives, so, let's stick to rows and discard columns. - continue; - struct dive *d = (struct dive *)index.data(DiveTripModel::DIVE_ROLE).value(); - if (d) { - selectedDives.insert(d->divetrip, get_divenr(d)); - } else { - struct dive_trip *t = (struct dive_trip *)index.data(DiveTripModel::TRIP_ROLE).value(); - if (t) - selectedDives.insert(t, -1); - } - } - selectionSaved = true; -} - -void DiveListView::restoreSelection() -{ - if (!selectionSaved) - return; - - selectionSaved = false; - dontEmitDiveChangedSignal = true; - unselectDives(); - dontEmitDiveChangedSignal = false; - Q_FOREACH (dive_trip_t *trip, selectedDives.keys()) { - QList divesOnTrip = getDivesInTrip(trip); - QList selectedDivesOnTrip = selectedDives.values(trip); - - // Only select trip if all of its dives were selected - if(selectedDivesOnTrip.contains(-1)) { - selectTrip(trip); - selectedDivesOnTrip.removeAll(-1); - } - selectDives(selectedDivesOnTrip); - } -} - -void DiveListView::selectTrip(dive_trip_t *trip) -{ - if (!trip) - return; - - QSortFilterProxyModel *m = qobject_cast(model()); - QModelIndexList match = m->match(m->index(0, 0), DiveTripModel::TRIP_ROLE, QVariant::fromValue(trip), 2, Qt::MatchRecursive); - QItemSelectionModel::SelectionFlags flags; - if (!match.count()) - return; - QModelIndex idx = match.first(); - flags = QItemSelectionModel::Select; - flags |= QItemSelectionModel::Rows; - selectionModel()->select(idx, flags); - expand(idx); -} - -// this is an odd one - when filtering the dive list the selection status of the trips -// is kept - but all other selections are lost. That's gets us into rather inconsistent state -// we call this function which clears the selection state of the trips as well, but does so -// without updating our internal "->selected" state. So once we called this function we can -// go back and select those dives that are still visible under the filter and everything -// works as expected -void DiveListView::clearTripSelection() -{ - // we want to make sure no trips are selected - disconnect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); - disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); - - Q_FOREACH (const QModelIndex &index, selectionModel()->selectedRows()) { - dive_trip_t *trip = static_cast(index.data(DiveTripModel::TRIP_ROLE).value()); - if (!trip) - continue; - selectionModel()->select(index, QItemSelectionModel::Deselect); - } - - connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); - connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); -} - -void DiveListView::unselectDives() -{ - // make sure we don't try to redraw the dives during the selection change - selected_dive = -1; - amount_selected = 0; - // clear the Qt selection - selectionModel()->clearSelection(); - // clearSelection should emit selectionChanged() but sometimes that - // appears not to happen - // since we are unselecting all dives there is no need to use deselect_dive() - that - // would only cause pointless churn - int i; - struct dive *dive; - for_each_dive (i, dive) { - dive->selected = false; - } -} - -QList DiveListView::selectedTrips() -{ - QList ret; - Q_FOREACH (const QModelIndex &index, selectionModel()->selectedRows()) { - dive_trip_t *trip = static_cast(index.data(DiveTripModel::TRIP_ROLE).value()); - if (!trip) - continue; - ret.push_back(trip); - } - return ret; -} - -void DiveListView::selectDive(int i, bool scrollto, bool toggle) -{ - if (i == -1) - return; - QSortFilterProxyModel *m = qobject_cast(model()); - QModelIndexList match = m->match(m->index(0, 0), DiveTripModel::DIVE_IDX, i, 2, Qt::MatchRecursive); - QItemSelectionModel::SelectionFlags flags; - if (match.isEmpty()) - return; - QModelIndex idx = match.first(); - flags = toggle ? QItemSelectionModel::Toggle : QItemSelectionModel::Select; - flags |= QItemSelectionModel::Rows; - selectionModel()->setCurrentIndex(idx, flags); - if (idx.parent().isValid()) { - setAnimated(false); - expand(idx.parent()); - if (scrollto) - scrollTo(idx.parent()); - setAnimated(true); - } - if (scrollto) - scrollTo(idx, PositionAtCenter); -} - -void DiveListView::selectDives(const QList &newDiveSelection) -{ - int firstInList, newSelection; - struct dive *d; - - if (!newDiveSelection.count()) - return; - - dontEmitDiveChangedSignal = true; - // select the dives, highest index first - this way the oldest of the dives - // becomes the selected_dive that we scroll to - QList sortedSelection = newDiveSelection; - qSort(sortedSelection.begin(), sortedSelection.end()); - newSelection = firstInList = sortedSelection.first(); - - while (!sortedSelection.isEmpty()) - selectDive(sortedSelection.takeLast()); - - while (selected_dive == -1) { - // that can happen if we restored a selection after edit - // and the only selected dive is no longer visible because of a filter - newSelection--; - if (newSelection < 0) - newSelection = dive_table.nr - 1; - if (newSelection == firstInList) - break; - if ((d = get_dive(newSelection)) != NULL && !d->hidden_by_filter) - selectDive(newSelection); - } - QSortFilterProxyModel *m = qobject_cast(model()); - QModelIndexList idxList = m->match(m->index(0, 0), DiveTripModel::DIVE_IDX, selected_dive, 2, Qt::MatchRecursive); - if (!idxList.isEmpty()) { - QModelIndex idx = idxList.first(); - if (idx.parent().isValid()) - scrollTo(idx.parent()); - scrollTo(idx); - } - // now that everything is up to date, update the widgets - Q_EMIT currentDiveChanged(selected_dive); - dontEmitDiveChangedSignal = false; - return; -} - -bool DiveListView::eventFilter(QObject *, QEvent *event) -{ - if (event->type() != QEvent::KeyPress) - return false; - QKeyEvent *keyEv = static_cast(event); - if (keyEv->key() == Qt::Key_Delete) { - contextMenuIndex = currentIndex(); - deleteDive(); - } - if (keyEv->key() != Qt::Key_Escape) - return false; - return true; -} - -// NOTE! This loses trip selection, because while we remember the -// dives, we don't remember the trips (see the "currentSelectedDives" -// list). I haven't figured out how to look up the trip from the -// index. TRIP_ROLE vs DIVE_ROLE? -void DiveListView::headerClicked(int i) -{ - DiveTripModel::Layout newLayout = i == (int)DiveTripModel::NR ? DiveTripModel::TREE : DiveTripModel::LIST; - rememberSelection(); - unselectDives(); - /* No layout change? Just re-sort, and scroll to first selection, making sure all selections are expanded */ - if (currentLayout == newLayout) { - currentOrder = (currentOrder == Qt::DescendingOrder) ? Qt::AscendingOrder : Qt::DescendingOrder; - sortByColumn(i, currentOrder); - } else { - // clear the model, repopulate with new indexes. - if (currentLayout == DiveTripModel::TREE) { - backupExpandedRows(); - } - reload(newLayout, false); - currentOrder = Qt::DescendingOrder; - sortByColumn(i, currentOrder); - if (newLayout == DiveTripModel::TREE) { - restoreExpandedRows(); - } - } - restoreSelection(); - // remember the new sort column - sortColumn = i; -} - -void DiveListView::reload(DiveTripModel::Layout layout, bool forceSort) -{ - // we want to run setupUi() once we actually are displaying something - // in the widget - static bool first = true; - if (first && dive_table.nr > 0) { - setupUi(); - first = false; - } - if (layout == DiveTripModel::CURRENT) - layout = currentLayout; - else - currentLayout = layout; - - header()->setSectionsClickable(true); - connect(header(), SIGNAL(sectionPressed(int)), this, SLOT(headerClicked(int)), Qt::UniqueConnection); - - QSortFilterProxyModel *m = qobject_cast(model()); - QAbstractItemModel *oldModel = m->sourceModel(); - if (oldModel) { - oldModel->deleteLater(); - } - DiveTripModel *tripModel = new DiveTripModel(this); - tripModel->setLayout(layout); - - m->setSourceModel(tripModel); - - if (!forceSort) - return; - - sortByColumn(sortColumn, currentOrder); - if (amount_selected && current_dive != NULL) { - selectDive(selected_dive, true); - } else { - QModelIndex firstDiveOrTrip = m->index(0, 0); - if (firstDiveOrTrip.isValid()) { - if (m->index(0, 0, firstDiveOrTrip).isValid()) - setCurrentIndex(m->index(0, 0, firstDiveOrTrip)); - else - setCurrentIndex(firstDiveOrTrip); - } - } - if (selectedIndexes().count()) { - QModelIndex curr = selectedIndexes().first(); - curr = curr.parent().isValid() ? curr.parent() : curr; - if (!isExpanded(curr)) { - setAnimated(false); - expand(curr); - scrollTo(curr); - setAnimated(true); - } - } - if (currentLayout == DiveTripModel::TREE) { - fixMessyQtModelBehaviour(); - } -} - -void DiveListView::reloadHeaderActions() -{ - // Populate the context menu of the headers that will show - // the menu to show / hide columns. - if (!header()->actions().size()) { - QSettings s; - s.beginGroup("DiveListColumnState"); - for (int i = 0; i < model()->columnCount(); i++) { - QString title = QString("%1").arg(model()->headerData(i, Qt::Horizontal).toString()); - QString settingName = QString("showColumn%1").arg(i); - QAction *a = new QAction(title, header()); - bool showHeaderFirstRun = !(i == DiveTripModel::MAXCNS || - i == DiveTripModel::GAS || - i == DiveTripModel::OTU || - i == DiveTripModel::TEMPERATURE || - i == DiveTripModel::TOTALWEIGHT || - i == DiveTripModel::SUIT || - i == DiveTripModel::CYLINDER || - i == DiveTripModel::SAC); - bool shown = s.value(settingName, showHeaderFirstRun).toBool(); - a->setCheckable(true); - a->setChecked(shown); - a->setProperty("index", i); - a->setProperty("settingName", settingName); - connect(a, SIGNAL(triggered(bool)), this, SLOT(toggleColumnVisibilityByIndex())); - header()->addAction(a); - setColumnHidden(i, !shown); - } - s.endGroup(); - } else { - for (int i = 0; i < model()->columnCount(); i++) { - QString title = QString("%1").arg(model()->headerData(i, Qt::Horizontal).toString()); - header()->actions()[i]->setText(title); - } - } -} - -void DiveListView::toggleColumnVisibilityByIndex() -{ - QAction *action = qobject_cast(sender()); - if (!action) - return; - - QSettings s; - s.beginGroup("DiveListColumnState"); - s.setValue(action->property("settingName").toString(), action->isChecked()); - s.endGroup(); - s.sync(); - setColumnHidden(action->property("index").toInt(), !action->isChecked()); - setColumnWidth(lastVisibleColumn(), 10); -} - -void DiveListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) -{ - if (!isVisible()) - return; - if (!current.isValid()) - return; - scrollTo(current); -} - -void DiveListView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) -{ - QItemSelection newSelected = selected.size() ? selected : selectionModel()->selection(); - QItemSelection newDeselected = deselected; - - disconnect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); - disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); - - Q_FOREACH (const QModelIndex &index, newDeselected.indexes()) { - if (index.column() != 0) - continue; - const QAbstractItemModel *model = index.model(); - struct dive *dive = (struct dive *)model->data(index, DiveTripModel::DIVE_ROLE).value(); - if (!dive) // it's a trip! - deselect_dives_in_trip((dive_trip_t *)model->data(index, DiveTripModel::TRIP_ROLE).value()); - else - deselect_dive(get_divenr(dive)); - } - Q_FOREACH (const QModelIndex &index, newSelected.indexes()) { - if (index.column() != 0) - continue; - - const QAbstractItemModel *model = index.model(); - struct dive *dive = (struct dive *)model->data(index, DiveTripModel::DIVE_ROLE).value(); - if (!dive) { // it's a trip! - if (model->rowCount(index)) { - QItemSelection selection; - select_dives_in_trip((dive_trip_t *)model->data(index, DiveTripModel::TRIP_ROLE).value()); - selection.select(index.child(0, 0), index.child(model->rowCount(index) - 1, 0)); - selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); - selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::NoUpdate); - if (!isExpanded(index)) - expand(index); - } - } else { - select_dive(get_divenr(dive)); - } - } - QTreeView::selectionChanged(selectionModel()->selection(), newDeselected); - connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); - connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); - if (!dontEmitDiveChangedSignal) - Q_EMIT currentDiveChanged(selected_dive); -} - -enum asked_user {NOTYET, MERGE, DONTMERGE}; - -static bool can_merge(const struct dive *a, const struct dive *b, enum asked_user *have_asked) -{ - if (!a || !b) - return false; - if (a->when > b->when) - return false; - /* Don't merge dives if there's more than half an hour between them */ - if (dive_endtime(a) + 30 * 60 < b->when) { - if (*have_asked == NOTYET) { - if (QMessageBox::warning(MainWindow::instance(), - MainWindow::instance()->tr("Warning"), - MainWindow::instance()->tr("Trying to merge dives with %1min interval in between").arg( - (b->when - dive_endtime(a)) / 60), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { - *have_asked = DONTMERGE; - return false; - } else { - *have_asked = MERGE; - return true; - } - } else { - return *have_asked == MERGE ? true : false; - } - } - return true; -} - -void DiveListView::mergeDives() -{ - int i; - struct dive *dive, *maindive = NULL; - enum asked_user have_asked = NOTYET; - - for_each_dive (i, dive) { - if (dive->selected) { - if (!can_merge(maindive, dive, &have_asked)) { - maindive = dive; - } else { - maindive = merge_two_dives(maindive, dive); - i--; // otherwise we skip a dive in the freshly changed list - } - } - } - MainWindow::instance()->refreshProfile(); - MainWindow::instance()->refreshDisplay(); -} - -void DiveListView::splitDives() -{ - int i; - struct dive *dive; - - for_each_dive (i, dive) { - if (dive->selected) - split_dive(dive); - } - MainWindow::instance()->refreshProfile(); - MainWindow::instance()->refreshDisplay(); -} - -void DiveListView::renumberDives() -{ - RenumberDialog::instance()->renumberOnlySelected(); - RenumberDialog::instance()->show(); -} - -void DiveListView::merge_trip(const QModelIndex &a, int offset) -{ - int i = a.row() + offset; - QModelIndex b = a.sibling(i, 0); - - dive_trip_t *trip_a = (dive_trip_t *)a.data(DiveTripModel::TRIP_ROLE).value(); - dive_trip_t *trip_b = (dive_trip_t *)b.data(DiveTripModel::TRIP_ROLE).value(); - if (trip_a == trip_b || !trip_a || !trip_b) - return; - combine_trips(trip_a, trip_b); - rememberSelection(); - reload(currentLayout, false); - fixMessyQtModelBehaviour(); - restoreSelection(); - mark_divelist_changed(true); - //TODO: emit a signal to signalize that the divelist changed? -} - -void DiveListView::mergeTripAbove() -{ - merge_trip(contextMenuIndex, -1); -} - -void DiveListView::mergeTripBelow() -{ - merge_trip(contextMenuIndex, +1); -} - -void DiveListView::removeFromTrip() -{ - //TODO: move this to C-code. - int i; - struct dive *d; - QMap divesToRemove; - for_each_dive (i, d) { - if (d->selected) - divesToRemove.insert(d, d->divetrip); - } - UndoRemoveDivesFromTrip *undoCommand = new UndoRemoveDivesFromTrip(divesToRemove); - MainWindow::instance()->undoStack->push(undoCommand); - - rememberSelection(); - reload(currentLayout, false); - fixMessyQtModelBehaviour(); - restoreSelection(); - mark_divelist_changed(true); -} - -void DiveListView::newTripAbove() -{ - struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); - if (!d) // shouldn't happen as we only are setting up this action if this is a dive - return; - //TODO: port to c-code. - dive_trip_t *trip; - int idx; - rememberSelection(); - trip = create_and_hookup_trip_from_dive(d); - for_each_dive (idx, d) { - if (d->selected) - add_dive_to_trip(d, trip); - } - trip->expanded = 1; - reload(currentLayout, false); - fixMessyQtModelBehaviour(); - mark_divelist_changed(true); - restoreSelection(); -} - -void DiveListView::addToTripBelow() -{ - addToTrip(1); -} - -void DiveListView::addToTripAbove() -{ - addToTrip(-1); -} - -void DiveListView::addToTrip(int delta) -{ - // if there is a trip above / below, then it's a sibling at the same - // level as this dive. So let's take a look - struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); - QModelIndex t = contextMenuIndex.sibling(contextMenuIndex.row() + delta, 0); - dive_trip_t *trip = (dive_trip_t *)t.data(DiveTripModel::TRIP_ROLE).value(); - - if (!trip || !d) - // no dive, no trip? get me out of here - return; - - rememberSelection(); - - add_dive_to_trip(d, trip); - if (d->selected) { // there are possibly other selected dives that we should add - int idx; - for_each_dive (idx, d) { - if (d->selected) - add_dive_to_trip(d, trip); - } - } - trip->expanded = 1; - mark_divelist_changed(true); - - reload(currentLayout, false); - restoreSelection(); - fixMessyQtModelBehaviour(); -} - -void DiveListView::markDiveInvalid() -{ - int i; - struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); - if (!d) - return; - for_each_dive (i, d) { - if (!d->selected) - continue; - //TODO: this should be done in the future - // now mark the dive invalid... how do we do THAT? - // d->invalid = true; - } - if (amount_selected == 0) { - MainWindow::instance()->cleanUpEmpty(); - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); - if (prefs.display_invalid_dives == false) { - clearSelection(); - // select top dive that isn't marked invalid - rememberSelection(); - } - fixMessyQtModelBehaviour(); -} - -void DiveListView::deleteDive() -{ - struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); - if (!d) - return; - - int i; - int lastDiveNr = -1; - QList deletedDives; //a list of all deleted dives to be stored in the undo command - for_each_dive (i, d) { - if (!d->selected) - continue; - deletedDives.append(d); - lastDiveNr = i; - } - // the actual dive deletion is happening in the redo command that is implicitly triggered - UndoDeleteDive *undoEntry = new UndoDeleteDive(deletedDives); - MainWindow::instance()->undoStack->push(undoEntry); - if (amount_selected == 0) { - MainWindow::instance()->cleanUpEmpty(); - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); - if (lastDiveNr != -1) { - clearSelection(); - selectDive(lastDiveNr); - rememberSelection(); - } - fixMessyQtModelBehaviour(); -} - -void DiveListView::testSlot() -{ - struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); - if (d) { - qDebug("testSlot called on dive #%d", d->number); - } else { - QModelIndex child = contextMenuIndex.child(0, 0); - d = (struct dive *)child.data(DiveTripModel::DIVE_ROLE).value(); - if (d) - qDebug("testSlot called on trip including dive #%d", d->number); - else - qDebug("testSlot called on trip with no dive"); - } -} - -void DiveListView::contextMenuEvent(QContextMenuEvent *event) -{ - QAction *collapseAction = NULL; - // let's remember where we are - contextMenuIndex = indexAt(event->pos()); - struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value(); - dive_trip_t *trip = (dive_trip_t *)contextMenuIndex.data(DiveTripModel::TRIP_ROLE).value(); - QMenu popup(this); - if (currentLayout == DiveTripModel::TREE) { - // verify if there is a node that`s not expanded. - bool needs_expand = false; - bool needs_collapse = false; - uint expanded_nodes = 0; - for(int i = 0, end = model()->rowCount(); i < end; i++) { - QModelIndex idx = model()->index(i, 0); - if (idx.data(DiveTripModel::DIVE_ROLE).value()) - continue; - - if (!isExpanded(idx)) { - needs_expand = true; - } else { - needs_collapse = true; - expanded_nodes ++; - } - } - if (needs_expand) - popup.addAction(tr("Expand all"), this, SLOT(expandAll())); - if (needs_collapse) - popup.addAction(tr("Collapse all"), this, SLOT(collapseAll())); - - // verify if there`s a need for collapse others - if (expanded_nodes > 1) - collapseAction = popup.addAction(tr("Collapse others"), this, SLOT(collapseAll())); - - - if (d) { - popup.addAction(tr("Remove dive(s) from trip"), this, SLOT(removeFromTrip())); - popup.addAction(tr("Create new trip above"), this, SLOT(newTripAbove())); - if (!d->divetrip) { - struct dive *top = d; - struct dive *bottom = d; - if (d->selected) { - if (currentOrder == Qt::AscendingOrder) { - top = first_selected_dive(); - bottom = last_selected_dive(); - } else { - top = last_selected_dive(); - bottom = first_selected_dive(); - } - } - if (is_trip_before_after(top, (currentOrder == Qt::AscendingOrder))) - popup.addAction(tr("Add dive(s) to trip immediately above"), this, SLOT(addToTripAbove())); - if (is_trip_before_after(bottom, (currentOrder == Qt::DescendingOrder))) - popup.addAction(tr("Add dive(s) to trip immediately below"), this, SLOT(addToTripBelow())); - } - } - if (trip) { - popup.addAction(tr("Merge trip with trip above"), this, SLOT(mergeTripAbove())); - popup.addAction(tr("Merge trip with trip below"), this, SLOT(mergeTripBelow())); - } - } - if (d) { - popup.addAction(tr("Delete dive(s)"), this, SLOT(deleteDive())); -#if 0 - popup.addAction(tr("Mark dive(s) invalid", this, SLOT(markDiveInvalid()))); -#endif - } - if (amount_selected > 1 && consecutive_selected()) - popup.addAction(tr("Merge selected dives"), this, SLOT(mergeDives())); - if (amount_selected >= 1) { - popup.addAction(tr("Renumber dive(s)"), this, SLOT(renumberDives())); - popup.addAction(tr("Shift dive times"), this, SLOT(shiftTimes())); - popup.addAction(tr("Split selected dives"), this, SLOT(splitDives())); - popup.addAction(tr("Load image(s) from file(s)"), this, SLOT(loadImages())); - popup.addAction(tr("Load image(s) from web"), this, SLOT(loadWebImages())); - } - - // "collapse all" really closes all trips, - // "collapse" keeps the trip with the selected dive open - QAction *actionTaken = popup.exec(event->globalPos()); - if (actionTaken == collapseAction && collapseAction) { - this->setAnimated(false); - selectDive(selected_dive, true); - scrollTo(selectedIndexes().first()); - this->setAnimated(true); - } - event->accept(); -} - - -void DiveListView::shiftTimes() -{ - ShiftTimesDialog::instance()->show(); -} - -void DiveListView::loadImages() -{ - QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open image files"), lastUsedImageDir(), tr("Image files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); - if (fileNames.isEmpty()) - return; - updateLastUsedImageDir(QFileInfo(fileNames[0]).dir().path()); - matchImagesToDives(fileNames); -} - -void DiveListView::matchImagesToDives(QStringList fileNames) -{ - ShiftImageTimesDialog shiftDialog(this, fileNames); - shiftDialog.setOffset(lastImageTimeOffset()); - if (!shiftDialog.exec()) - return; - updateLastImageTimeOffset(shiftDialog.amount()); - - Q_FOREACH (const QString &fileName, fileNames) { - int j = 0; - struct dive *dive; - for_each_dive (j, dive) { - if (!dive->selected) - continue; - dive_create_picture(dive, copy_string(fileName.toUtf8().data()), shiftDialog.amount(), shiftDialog.matchAll()); - } - } - - mark_divelist_changed(true); - copy_dive(current_dive, &displayed_dive); - DivePictureModel::instance()->updateDivePictures(); -} - -void DiveListView::loadWebImages() -{ - URLDialog urlDialog(this); - if (!urlDialog.exec()) - return; - loadImageFromURL(QUrl::fromUserInput(urlDialog.url())); - -} - -void DiveListView::loadImageFromURL(QUrl url) -{ - if (url.isValid()) { - QEventLoop loop; - QNetworkRequest request(url); - QNetworkReply *reply = manager.get(request); - while (reply->isRunning()) { - loop.processEvents(); - sleep(1); - } - QByteArray imageData = reply->readAll(); - - QImage image = QImage(); - image.loadFromData(imageData); - if (image.isNull()) - // If this is not an image, maybe it's an html file and Miika can provide some xslr magic to extract images. - // In this case we would call the function recursively on the list of image source urls; - return; - - // Since we already downloaded the image we can cache it as well. - QCryptographicHash hash(QCryptographicHash::Sha1); - hash.addData(imageData); - QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); - QDir dir(path); - if (!dir.exists()) - dir.mkpath(path); - QFile imageFile(path.append("/").append(hash.result().toHex())); - if (imageFile.open(QIODevice::WriteOnly)) { - QDataStream stream(&imageFile); - stream.writeRawData(imageData.data(), imageData.length()); - imageFile.waitForBytesWritten(-1); - imageFile.close(); - add_hash(imageFile.fileName(), hash.result()); - struct picture picture; - picture.hash = NULL; - picture.filename = strdup(url.toString().toUtf8().data()); - learnHash(&picture, hash.result()); - matchImagesToDives(QStringList(url.toString())); - } - } - - -} - - -QString DiveListView::lastUsedImageDir() -{ - QSettings settings; - QString lastImageDir = QDir::homePath(); - - settings.beginGroup("FileDialog"); - if (settings.contains("LastImageDir")) - if (QDir::setCurrent(settings.value("LastImageDir").toString())) - lastImageDir = settings.value("LastIamgeDir").toString(); - return lastImageDir; -} - -void DiveListView::updateLastUsedImageDir(const QString &dir) -{ - QSettings s; - s.beginGroup("FileDialog"); - s.setValue("LastImageDir", dir); -} - -int DiveListView::lastImageTimeOffset() -{ - QSettings settings; - int offset = 0; - - settings.beginGroup("MainWindow"); - if (settings.contains("LastImageTimeOffset")) - offset = settings.value("LastImageTimeOffset").toInt(); - return offset; -} - -void DiveListView::updateLastImageTimeOffset(const int offset) -{ - QSettings s; - s.beginGroup("MainWindow"); - s.setValue("LastImageTimeOffset", offset); -} diff --git a/qt-ui/divelistview.h b/qt-ui/divelistview.h deleted file mode 100644 index aaec37af5..000000000 --- a/qt-ui/divelistview.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * divelistview.h - * - * header file for the dive list of Subsurface - * - */ -#ifndef DIVELISTVIEW_H -#define DIVELISTVIEW_H - -/*! A view subclass for use with dives - Note: calling this a list view might be misleading? -*/ - -#include -#include -#include -#include "divetripmodel.h" - -class DiveListView : public QTreeView { - Q_OBJECT -public: - DiveListView(QWidget *parent = 0); - ~DiveListView(); - void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); - void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); - void reload(DiveTripModel::Layout layout, bool forceSort = true); - bool eventFilter(QObject *, QEvent *); - void unselectDives(); - void clearTripSelection(); - void selectDive(int dive_table_idx, bool scrollto = false, bool toggle = false); - void selectDives(const QList &newDiveSelection); - void rememberSelection(); - void restoreSelection(); - void contextMenuEvent(QContextMenuEvent *event); - QList selectedTrips(); -public -slots: - void toggleColumnVisibilityByIndex(); - void reloadHeaderActions(); - void headerClicked(int); - void removeFromTrip(); - void deleteDive(); - void markDiveInvalid(); - void testSlot(); - void fixMessyQtModelBehaviour(); - void mergeTripAbove(); - void mergeTripBelow(); - void newTripAbove(); - void addToTripAbove(); - void addToTripBelow(); - void mergeDives(); - void splitDives(); - void renumberDives(); - void shiftTimes(); - void loadImages(); - void loadWebImages(); - static QString lastUsedImageDir(); - -signals: - void currentDiveChanged(int divenr); - -private: - bool mouseClickSelection; - QList expandedRows; - int sortColumn; - Qt::SortOrder currentOrder; - DiveTripModel::Layout currentLayout; - QModelIndex contextMenuIndex; - bool dontEmitDiveChangedSignal; - bool selectionSaved; - - /* if dive_trip_t is null, there's no problem. */ - QMultiHash selectedDives; - void merge_trip(const QModelIndex &a, const int offset); - void setupUi(); - void backupExpandedRows(); - void restoreExpandedRows(); - int lastVisibleColumn(); - void selectTrip(dive_trip_t *trip); - void updateLastUsedImageDir(const QString &s); - void updateLastImageTimeOffset(int offset); - int lastImageTimeOffset(); - void addToTrip(int delta); - void matchImagesToDives(QStringList fileNames); - void loadImageFromURL(QUrl url); - QNetworkAccessManager manager; -}; - -#endif // DIVELISTVIEW_H diff --git a/qt-ui/divelogexportdialog.cpp b/qt-ui/divelogexportdialog.cpp deleted file mode 100644 index 7a406b982..000000000 --- a/qt-ui/divelogexportdialog.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include -#include -#include -#include - -#include "divelogexportdialog.h" -#include "divelogexportlogic.h" -#include "diveshareexportdialog.h" -#include "ui_divelogexportdialog.h" -#include "subsurfacewebservices.h" -#include "worldmap-save.h" -#include "save-html.h" -#include "mainwindow.h" - -#define GET_UNIT(name, field, f, t) \ - v = settings.value(QString(name)); \ - if (v.isValid()) \ - field = (v.toInt() == 0) ? (t) : (f); \ - else \ - field = default_prefs.units.field - -DiveLogExportDialog::DiveLogExportDialog(QWidget *parent) : QDialog(parent), - ui(new Ui::DiveLogExportDialog) -{ - ui->setupUi(this); - showExplanation(); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), MainWindow::instance(), SLOT(close())); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - - /* the names are not the actual values exported to the json files,The font-family property should hold several - font names as a "fallback" system, to ensure maximum compatibility between browsers/operating systems */ - ui->fontSelection->addItem("Arial", "Arial, Helvetica, sans-serif"); - ui->fontSelection->addItem("Impact", "Impact, Charcoal, sans-serif"); - ui->fontSelection->addItem("Georgia", "Georgia, serif"); - ui->fontSelection->addItem("Courier", "Courier, monospace"); - ui->fontSelection->addItem("Verdana", "Verdana, Geneva, sans-serif"); - - QSettings settings; - settings.beginGroup("HTML"); - if (settings.contains("fontSelection")) { - ui->fontSelection->setCurrentIndex(settings.value("fontSelection").toInt()); - } - if (settings.contains("fontSizeSelection")) { - ui->fontSizeSelection->setCurrentIndex(settings.value("fontSizeSelection").toInt()); - } - if (settings.contains("themeSelection")) { - ui->themeSelection->setCurrentIndex(settings.value("themeSelection").toInt()); - } - if (settings.contains("subsurfaceNumbers")) { - ui->exportSubsurfaceNumber->setChecked(settings.value("subsurfaceNumbers").toBool()); - } - if (settings.contains("yearlyStatistics")) { - ui->exportStatistics->setChecked(settings.value("yearlyStatistics").toBool()); - } - if (settings.contains("listOnly")) { - ui->exportListOnly->setChecked(settings.value("listOnly").toBool()); - } - if (settings.contains("exportPhotos")) { - ui->exportPhotos->setChecked(settings.value("exportPhotos").toBool()); - } - settings.endGroup(); -} - -DiveLogExportDialog::~DiveLogExportDialog() -{ - delete ui; -} - -void DiveLogExportDialog::showExplanation() -{ - if (ui->exportUDDF->isChecked()) { - ui->description->setText(tr("Generic format that is used for data exchange between a variety of diving related programs.")); - } else if (ui->exportCSV->isChecked()) { - ui->description->setText(tr("Comma separated values describing the dive profile.")); - } else if (ui->exportCSVDetails->isChecked()) { - ui->description->setText(tr("Comma separated values of the dive information. This includes most of the dive details but no profile information.")); - } else if (ui->exportDivelogs->isChecked()) { - ui->description->setText(tr("Send the dive data to divelogs.de website.")); - } else if (ui->exportDiveshare->isChecked()) { - ui->description->setText(tr("Send the dive data to dive-share.appspot.com website")); - } else if (ui->exportWorldMap->isChecked()) { - ui->description->setText(tr("HTML export of the dive locations, visualized on a world map.")); - } else if (ui->exportSubsurfaceXML->isChecked()) { - ui->description->setText(tr("Subsurface native XML format.")); - } else if (ui->exportImageDepths->isChecked()) { - ui->description->setText(tr("Write depths of images to file.")); - } -} - -void DiveLogExportDialog::exportHtmlInit(const QString &filename) -{ - struct htmlExportSetting hes; - hes.themeFile = (ui->themeSelection->currentText() == tr("Light")) ? "light.css" : "sand.css"; - hes.exportPhotos = ui->exportPhotos->isChecked(); - hes.selectedOnly = ui->exportSelectedDives->isChecked(); - hes.listOnly = ui->exportListOnly->isChecked(); - hes.fontFamily = ui->fontSelection->itemData(ui->fontSelection->currentIndex()).toString(); - hes.fontSize = ui->fontSizeSelection->currentText(); - hes.themeSelection = ui->themeSelection->currentIndex(); - hes.subsurfaceNumbers = ui->exportSubsurfaceNumber->isChecked(); - hes.yearlyStatistics = ui->exportStatistics->isChecked(); - - exportHtmlInitLogic(filename, hes); -} - -void DiveLogExportDialog::exportHTMLsettings(const QString &filename) -{ - QSettings settings; - settings.beginGroup("HTML"); - settings.setValue("fontSelection", ui->fontSelection->currentIndex()); - settings.setValue("fontSizeSelection", ui->fontSizeSelection->currentIndex()); - settings.setValue("themeSelection", ui->themeSelection->currentIndex()); - settings.setValue("subsurfaceNumbers", ui->exportSubsurfaceNumber->isChecked()); - settings.setValue("yearlyStatistics", ui->exportStatistics->isChecked()); - settings.setValue("listOnly", ui->exportListOnly->isChecked()); - settings.setValue("exportPhotos", ui->exportPhotos->isChecked()); - settings.endGroup(); - -} - - -void DiveLogExportDialog::on_exportGroup_buttonClicked(QAbstractButton *button) -{ - showExplanation(); -} - -void DiveLogExportDialog::on_buttonBox_accepted() -{ - QString filename; - QString stylesheet; - QSettings settings; - QString lastDir = QDir::homePath(); - - settings.beginGroup("FileDialog"); - if (settings.contains("LastDir")) { - if (QDir::setCurrent(settings.value("LastDir").toString())) { - lastDir = settings.value("LastDir").toString(); - } - } - settings.endGroup(); - - switch (ui->tabWidget->currentIndex()) { - case 0: - if (ui->exportUDDF->isChecked()) { - stylesheet = "uddf-export.xslt"; - filename = QFileDialog::getSaveFileName(this, tr("Export UDDF file as"), lastDir, - tr("UDDF files (*.uddf *.UDDF)")); - } else if (ui->exportCSV->isChecked()) { - stylesheet = "xml2csv.xslt"; - filename = QFileDialog::getSaveFileName(this, tr("Export CSV file as"), lastDir, - tr("CSV files (*.csv *.CSV)")); - } else if (ui->exportCSVDetails->isChecked()) { - stylesheet = "xml2manualcsv.xslt"; - filename = QFileDialog::getSaveFileName(this, tr("Export CSV file as"), lastDir, - tr("CSV files (*.csv *.CSV)")); - } else if (ui->exportDivelogs->isChecked()) { - DivelogsDeWebServices::instance()->prepareDivesForUpload(ui->exportSelected->isChecked()); - } else if (ui->exportDiveshare->isChecked()) { - DiveShareExportDialog::instance()->prepareDivesForUpload(ui->exportSelected->isChecked()); - } else if (ui->exportWorldMap->isChecked()) { - filename = QFileDialog::getSaveFileName(this, tr("Export world map"), lastDir, - tr("HTML files (*.html)")); - if (!filename.isNull() && !filename.isEmpty()) - export_worldmap_HTML(filename.toUtf8().data(), ui->exportSelected->isChecked()); - } else if (ui->exportSubsurfaceXML->isChecked()) { - filename = QFileDialog::getSaveFileName(this, tr("Export Subsurface XML"), lastDir, - tr("XML files (*.xml *.ssrf)")); - if (!filename.isNull() && !filename.isEmpty()) { - if (!filename.contains('.')) - filename.append(".ssrf"); - QByteArray bt = QFile::encodeName(filename); - save_dives_logic(bt.data(), ui->exportSelected->isChecked()); - } - } else if (ui->exportImageDepths->isChecked()) { - filename = QFileDialog::getSaveFileName(this, tr("Save image depths"), lastDir); - if (!filename.isNull() && !filename.isEmpty()) - export_depths(filename.toUtf8().data(), ui->exportSelected->isChecked()); - } - break; - case 1: - filename = QFileDialog::getSaveFileName(this, tr("Export HTML files as"), lastDir, - tr("HTML files (*.html)")); - if (!filename.isNull() && !filename.isEmpty()) - exportHtmlInit(filename); - break; - } - - if (!filename.isNull() && !filename.isEmpty()) { - // remember the last export path - QFileInfo fileInfo(filename); - settings.beginGroup("FileDialog"); - settings.setValue("LastDir", fileInfo.dir().path()); - settings.endGroup(); - // the non XSLT exports are called directly above, the XSLT based ons are called here - if (!stylesheet.isEmpty()) { - future = QtConcurrent::run(export_dives_xslt, filename.toUtf8(), ui->exportSelected->isChecked(), ui->CSVUnits_2->currentIndex(), stylesheet.toUtf8()); - MainWindow::instance()->getNotificationWidget()->showNotification(tr("Please wait, exporting..."), KMessageWidget::Information); - MainWindow::instance()->getNotificationWidget()->setFuture(future); - } - } -} - -void DiveLogExportDialog::export_depths(const char *filename, const bool selected_only) -{ - FILE *f; - struct dive *dive; - depth_t depth; - int i; - const char *unit = NULL; - - struct membuffer buf = { 0 }; - - for_each_dive (i, dive) { - if (selected_only && !dive->selected) - continue; - - FOR_EACH_PICTURE (dive) { - int n = dive->dc.samples; - struct sample *s = dive->dc.sample; - depth.mm = 0; - while (--n >= 0 && (int32_t)s->time.seconds <= picture->offset.seconds) { - depth.mm = s->depth.mm; - s++; - } - put_format(&buf, "%s\t%.1f", picture->filename, get_depth_units(depth.mm, NULL, &unit)); - put_format(&buf, "%s\n", unit); - } - } - - f = subsurface_fopen(filename, "w+"); - if (!f) { - report_error(tr("Can't open file %s").toUtf8().data(), filename); - } else { - flush_buffer(&buf, f); /*check for writing errors? */ - fclose(f); - } - free_buffer(&buf); -} diff --git a/qt-ui/divelogexportdialog.h b/qt-ui/divelogexportdialog.h deleted file mode 100644 index a5b5cc770..000000000 --- a/qt-ui/divelogexportdialog.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef DIVELOGEXPORTDIALOG_H -#define DIVELOGEXPORTDIALOG_H - -#include -#include -#include -#include "helpers.h" -#include "statistics.h" - -class QAbstractButton; - -namespace Ui { - class DiveLogExportDialog; -} - -void exportHTMLstatisticsTotal(QTextStream &out, stats_t *total_stats); - -class DiveLogExportDialog : public QDialog { - Q_OBJECT - -public: - explicit DiveLogExportDialog(QWidget *parent = 0); - ~DiveLogExportDialog(); - -private -slots: - void on_buttonBox_accepted(); - void on_exportGroup_buttonClicked(QAbstractButton *); - -private: - QFuture future; - Ui::DiveLogExportDialog *ui; - void showExplanation(); - void exportHtmlInit(const QString &filename); - void exportHTMLsettings(const QString &filename); - void export_depths(const char *filename, const bool selected_only); -}; - -#endif // DIVELOGEXPORTDIALOG_H diff --git a/qt-ui/divelogexportdialog.ui b/qt-ui/divelogexportdialog.ui deleted file mode 100644 index 02c8cf38b..000000000 --- a/qt-ui/divelogexportdialog.ui +++ /dev/null @@ -1,606 +0,0 @@ - - - DiveLogExportDialog - - - - 0 - 0 - 507 - 548 - - - - Export dive log files - - - - :/subsurface-icon - - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - 0 - - - true - - - - General export - - - - 5 - - - 5 - - - 5 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 100 - - - - - 16777215 - 100 - - - - - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Export format - - - - - - - 171 - 16777215 - - - - Subsurface &XML - - - true - - - exportGroup - - - - - - - - 110 - 16777215 - - - - UDDF - - - false - - - exportGroup - - - - - - - di&velogs.de - - - exportGroup - - - - - - - DiveShare - - - exportGroup - - - - - - - CSV dive profile - - - exportGroup - - - - - - - CSV dive details - - - exportGroup - - - - - - - Worldmap - - - exportGroup - - - - - - - I&mage depths - - - exportGroup - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 100 - - - - - 16777215 - 16777215 - - - - Selection - - - - - - true - - - Selected dives - - - true - - - - - - - All dives - - - - - - - - - - false - - - CSV units - - - - - 30 - 30 - 102 - 27 - - - - - Metric - - - - - Imperial - - - - - - - - - - - - - HTML - - - - 0 - - - 5 - - - 0 - - - 0 - - - - - General settings - - - - - - Subsurface numbers - - - true - - - - - - - - 117 - 0 - - - - Selected dives - - - true - - - buttonGroup - - - - - - - Export yearly statistics - - - true - - - - - - - - 117 - 0 - - - - All di&ves - - - buttonGroup - - - - - - - Export list only - - - - - - - Export photos - - - true - - - - - - - - - - true - - - Style options - - - true - - - false - - - - QFormLayout::AllNonFixedFieldsGrow - - - - - Font - - - - - - - - - - Font size - - - - - - - 3 - - - - 8 - - - - - 10 - - - - - 12 - - - - - 14 - - - - - 16 - - - - - 18 - - - - - 20 - - - - - - - - Theme - - - - - - - 0 - - - - Light - - - - - Sand - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - DiveLogExportDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - DiveLogExportDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - exportCSV - toggled(bool) - groupBox - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - exportCSVDetails - toggled(bool) - groupBox - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - - - - - diff --git a/qt-ui/divelogimportdialog.cpp b/qt-ui/divelogimportdialog.cpp deleted file mode 100644 index 025d181d1..000000000 --- a/qt-ui/divelogimportdialog.cpp +++ /dev/null @@ -1,861 +0,0 @@ -#include "divelogimportdialog.h" -#include "mainwindow.h" -#include "color.h" -#include "ui_divelogimportdialog.h" -#include -#include -#include - -static QString subsurface_mimedata = "subsurface/csvcolumns"; -static QString subsurface_index = "subsurface/csvindex"; - -const DiveLogImportDialog::CSVAppConfig DiveLogImportDialog::CSVApps[CSVAPPS] = { - // time, depth, temperature, po2, sensor1, sensor2, sensor3, cns, ndl, tts, stopdepth, pressure, setpoint - // indices are 0 based, -1 means the column doesn't exist - { "Manual import", }, - { "APD Log Viewer - DC1", 0, 1, 15, 6, 3, 4, 5, 17, -1, -1, 18, -1, 2, "Tab" }, - { "APD Log Viewer - DC2", 0, 1, 15, 6, 7, 8, 9, 17, -1, -1, 18, -1, 2, "Tab" }, - { "XP5", 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "Tab" }, - { "SensusCSV", 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "," }, - { "Seabear CSV", 0, 1, 5, -1, -1, -1, -1, -1, 2, 3, 4, 6, -1, ";" }, - { "SubsurfaceCSV", -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "Tab" }, - { NULL, } -}; - -static enum { - MANUAL, - APD, - APD2, - XP5, - SENSUS, - SEABEAR, - SUBSURFACE -} known; - -ColumnNameProvider::ColumnNameProvider(QObject *parent) : QAbstractListModel(parent) -{ - columnNames << tr("Dive #") << tr("Date") << tr("Time") << tr("Duration") << tr("Location") << tr("GPS") << tr("Weight") << tr("Cyl. size") << tr("Start pressure") << - tr("End pressure") << tr("Max. depth") << tr("Avg. depth") << tr("Divemaster") << tr("Buddy") << tr("Suit") << tr("Notes") << tr("Tags") << tr("Air temp.") << tr("Water temp.") << - tr("Oâ‚‚") << tr("He") << tr("Sample time") << tr("Sample depth") << tr("Sample temperature") << tr("Sample pOâ‚‚") << tr("Sample CNS") << tr("Sample NDL") << - tr("Sample TTS") << tr("Sample stopdepth") << tr("Sample pressure") << - tr("Sample sensor1 pOâ‚‚") << tr("Sample sensor2 pOâ‚‚") << tr("Sample sensor3 pOâ‚‚") << - tr("Sample setpoint"); -} - -bool ColumnNameProvider::insertRows(int row, int count, const QModelIndex &parent) -{ - beginInsertRows(QModelIndex(), row, row); - columnNames.append(QString()); - endInsertRows(); - return true; -} - -bool ColumnNameProvider::removeRows(int row, int count, const QModelIndex &parent) -{ - beginRemoveRows(QModelIndex(), row, row); - columnNames.removeAt(row); - endRemoveRows(); - return true; -} - -bool ColumnNameProvider::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (role == Qt::EditRole) { - columnNames[index.row()] = value.toString(); - } - dataChanged(index, index); - return true; -} - -QVariant ColumnNameProvider::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - if (role != Qt::DisplayRole) - return QVariant(); - - return QVariant(columnNames[index.row()]); -} - -int ColumnNameProvider::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - return columnNames.count(); -} - -int ColumnNameProvider::mymatch(QString value) const -{ - QString searchString = value.toLower(); - searchString.replace("\"", "").replace(" ", "").replace(".", "").replace("\n",""); - for (int i = 0; i < columnNames.count(); i++) { - QString name = columnNames.at(i).toLower(); - name.replace("\"", "").replace(" ", "").replace(".", "").replace("\n",""); - if (searchString == name.toLower()) - return i; - } - return -1; -} - - - -ColumnNameView::ColumnNameView(QWidget *parent) -{ - setAcceptDrops(true); - setDragEnabled(true); -} - -void ColumnNameView::mousePressEvent(QMouseEvent *press) -{ - QModelIndex atClick = indexAt(press->pos()); - if (!atClick.isValid()) - return; - - QRect indexRect = visualRect(atClick); - QPixmap pix(indexRect.width(), indexRect.height()); - pix.fill(QColor(0,0,0,0)); - render(&pix, QPoint(0, 0),QRegion(indexRect)); - - QDrag *drag = new QDrag(this); - QMimeData *mimeData = new QMimeData; - mimeData->setData(subsurface_mimedata, atClick.data().toByteArray()); - model()->removeRow(atClick.row()); - drag->setPixmap(pix); - drag->setMimeData(mimeData); - if (drag->exec() == Qt::IgnoreAction){ - model()->insertRow(model()->rowCount()); - QModelIndex idx = model()->index(model()->rowCount()-1, 0); - model()->setData(idx, mimeData->data(subsurface_mimedata)); - } -} - -void ColumnNameView::dragLeaveEvent(QDragLeaveEvent *leave) -{ - Q_UNUSED(leave); -} - -void ColumnNameView::dragEnterEvent(QDragEnterEvent *event) -{ - event->acceptProposedAction(); -} - -void ColumnNameView::dragMoveEvent(QDragMoveEvent *event) -{ - QModelIndex curr = indexAt(event->pos()); - if (!curr.isValid() || curr.row() != 0) - return; - event->acceptProposedAction(); -} - -void ColumnNameView::dropEvent(QDropEvent *event) -{ - const QMimeData *mimeData = event->mimeData(); - if (mimeData->data(subsurface_mimedata).count()) { - if (event->source() != this) { - event->acceptProposedAction(); - QVariant value = QString(mimeData->data(subsurface_mimedata)); - model()->insertRow(model()->rowCount()); - model()->setData(model()->index(model()->rowCount()-1, 0), value); - } - } -} - -ColumnDropCSVView::ColumnDropCSVView(QWidget *parent) -{ - setAcceptDrops(true); -} - -void ColumnDropCSVView::dragLeaveEvent(QDragLeaveEvent *leave) -{ - Q_UNUSED(leave); -} - -void ColumnDropCSVView::dragEnterEvent(QDragEnterEvent *event) -{ - event->acceptProposedAction(); -} - -void ColumnDropCSVView::dragMoveEvent(QDragMoveEvent *event) -{ - QModelIndex curr = indexAt(event->pos()); - if (!curr.isValid() || curr.row() != 0) - return; - event->acceptProposedAction(); -} - -void ColumnDropCSVView::dropEvent(QDropEvent *event) -{ - QModelIndex curr = indexAt(event->pos()); - if (!curr.isValid() || curr.row() != 0) - return; - - const QMimeData *mimeData = event->mimeData(); - if (!mimeData->data(subsurface_mimedata).count()) - return; - - if (event->source() == this ) { - int value_old = mimeData->data(subsurface_index).toInt(); - int value_new = curr.column(); - ColumnNameResult *m = qobject_cast(model()); - m->swapValues(value_old, value_new); - event->acceptProposedAction(); - return; - } - - if (curr.data().toString().isEmpty()) { - QVariant value = QString(mimeData->data(subsurface_mimedata)); - model()->setData(curr, value); - event->acceptProposedAction(); - } -} - -ColumnNameResult::ColumnNameResult(QObject *parent) : QAbstractTableModel(parent) -{ - -} - -void ColumnNameResult::swapValues(int firstIndex, int secondIndex) { - QString one = columnNames[firstIndex]; - QString two = columnNames[secondIndex]; - setData(index(0, firstIndex), QVariant(two), Qt::EditRole); - setData(index(0, secondIndex), QVariant(one), Qt::EditRole); -} - -bool ColumnNameResult::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (!index.isValid() || index.row() != 0) { - return false; - } - if (role == Qt::EditRole) { - columnNames[index.column()] = value.toString(); - dataChanged(index, index); - } - return true; -} - -QVariant ColumnNameResult::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - if (role == Qt::BackgroundColorRole) - if (index.row() == 0) - return QVariant(AIR_BLUE_TRANS); - - if (role != Qt::DisplayRole) - return QVariant(); - - if (index.row() == 0) { - return (columnNames[index.column()]); - } - // make sure the element exists before returning it - this might get called before the - // model is correctly set up again (e.g., when changing separators) - if (columnValues.count() > index.row() - 1 && columnValues[index.row() - 1].count() > index.column()) - return QVariant(columnValues[index.row() - 1][index.column()]); - else - return QVariant(); -} - -int ColumnNameResult::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return columnValues.count() + 1; // +1 == the header. -} - -int ColumnNameResult::columnCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return columnNames.count(); -} - -QStringList ColumnNameResult::result() const -{ - return columnNames; -} - -void ColumnNameResult::setColumnValues(QList columns) -{ - if (rowCount() != 1) { - beginRemoveRows(QModelIndex(), 1, rowCount()-1); - columnValues.clear(); - endRemoveRows(); - } - if (columnCount() != 0) { - beginRemoveColumns(QModelIndex(), 0, columnCount()-1); - columnNames.clear(); - endRemoveColumns(); - } - - QStringList first = columns.first(); - beginInsertColumns(QModelIndex(), 0, first.count()-1); - for(int i = 0; i < first.count(); i++) - columnNames.append(QString()); - - endInsertColumns(); - - beginInsertRows(QModelIndex(), 0, columns.count()-1); - columnValues = columns; - endInsertRows(); -} - -void ColumnDropCSVView::mousePressEvent(QMouseEvent *press) -{ - QModelIndex atClick = indexAt(press->pos()); - if (!atClick.isValid() || atClick.row()) - return; - - QRect indexRect = visualRect(atClick); - QPixmap pix(indexRect.width(), indexRect.height()); - pix.fill(QColor(0,0,0,0)); - render(&pix, QPoint(0, 0),QRegion(indexRect)); - - QDrag *drag = new QDrag(this); - QMimeData *mimeData = new QMimeData; - mimeData->setData(subsurface_mimedata, atClick.data().toByteArray()); - mimeData->setData(subsurface_index, QString::number(atClick.column()).toLocal8Bit()); - drag->setPixmap(pix); - drag->setMimeData(mimeData); - if (drag->exec() != Qt::IgnoreAction){ - QObject *target = drag->target(); - if (target->objectName() == "qt_scrollarea_viewport") - target = target->parent(); - if (target != drag->source()) - model()->setData(atClick, QString()); - } -} - -DiveLogImportDialog::DiveLogImportDialog(QStringList fn, QWidget *parent) : QDialog(parent), - selector(true), - ui(new Ui::DiveLogImportDialog) -{ - ui->setupUi(this); - fileNames = fn; - column = 0; - delta = "0"; - hw = ""; - - /* Add indexes of XSLTs requiring special handling to the list */ - specialCSV << SENSUS; - specialCSV << SUBSURFACE; - - for (int i = 0; !CSVApps[i].name.isNull(); ++i) - ui->knownImports->addItem(CSVApps[i].name); - - ui->CSVSeparator->addItems( QStringList() << tr("Tab") << "," << ";"); - - loadFileContents(-1, INITIAL); - - /* manually import CSV file */ - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); - - connect(ui->CSVSeparator, SIGNAL(currentIndexChanged(int)), this, SLOT(loadFileContentsSeperatorSelected(int))); - connect(ui->knownImports, SIGNAL(currentIndexChanged(int)), this, SLOT(loadFileContentsKnownTypesSelected(int))); -} - -DiveLogImportDialog::~DiveLogImportDialog() -{ - delete ui; -} - -void DiveLogImportDialog::loadFileContentsSeperatorSelected(int value) -{ - loadFileContents(value, SEPARATOR); -} - -void DiveLogImportDialog::loadFileContentsKnownTypesSelected(int value) -{ - loadFileContents(value, KNOWNTYPES); -} - -void DiveLogImportDialog::loadFileContents(int value, whatChanged triggeredBy) -{ - QFile f(fileNames.first()); - QList fileColumns; - QStringList currColumns; - QStringList headers; - bool matchedSome = false; - bool seabear = false; - bool xp5 = false; - bool apd = false; - - // reset everything - ColumnNameProvider *provider = new ColumnNameProvider(this); - ui->avaliableColumns->setModel(provider); - ui->avaliableColumns->setItemDelegate(new TagDragDelegate(ui->avaliableColumns)); - resultModel = new ColumnNameResult(this); - ui->tableView->setModel(resultModel); - - f.open(QFile::ReadOnly); - QString firstLine = f.readLine(); - if (firstLine.contains("SEABEAR")) { - seabear = true; - - /* - * Parse header - currently only interested in sample - * interval and hardware version. If we have old format - * the interval value is missing from the header. - */ - - while ((firstLine = f.readLine().trimmed()).length() > 0 && !f.atEnd()) { - if (firstLine.contains("//Hardware Version: ")) { - hw = firstLine.replace(QString::fromLatin1("//Hardware Version: "), QString::fromLatin1("\"Seabear ")).trimmed().append("\""); - break; - } - } - - /* - * Note that we scan over the "Log interval" on purpose - */ - - while ((firstLine = f.readLine().trimmed()).length() > 0 && !f.atEnd()) { - if (firstLine.contains("//Log interval: ")) - delta = firstLine.remove(QString::fromLatin1("//Log interval: ")).trimmed().remove(QString::fromLatin1(" s")); - } - - /* - * Parse CSV fields - */ - - firstLine = f.readLine().trimmed(); - - currColumns = firstLine.split(';'); - Q_FOREACH (QString columnText, currColumns) { - if (columnText == "Time") { - headers.append("Sample time"); - } else if (columnText == "Depth") { - headers.append("Sample depth"); - } else if (columnText == "Temperature") { - headers.append("Sample temperature"); - } else if (columnText == "NDT") { - headers.append("Sample NDL"); - } else if (columnText == "TTS") { - headers.append("Sample TTS"); - } else if (columnText == "pO2_1") { - headers.append("Sample sensor1 pOâ‚‚"); - } else if (columnText == "pO2_2") { - headers.append("Sample sensor2 pOâ‚‚"); - } else if (columnText == "pO2_3") { - headers.append("Sample sensor3 pOâ‚‚"); - } else if (columnText == "Ceiling") { - headers.append("Sample ceiling"); - } else if (columnText == "Tank pressure") { - headers.append("Sample pressure"); - } else { - // We do not know about this value - qDebug() << "Seabear import found an un-handled field: " << columnText; - headers.append(""); - } - } - - firstLine = headers.join(";"); - blockSignals(true); - ui->knownImports->setCurrentText("Seabear CSV"); - blockSignals(false); - } else if (firstLine.contains("Tauchgangs-Nr.:")) { - xp5 = true; - //"Abgelaufene Tauchzeit (Std:Min.)\tTiefe\tStickstoff Balkenanzeige\tSauerstoff Balkenanzeige\tAufstiegsgeschwindigkeit\tRestluftzeit\tRestliche Tauchzeit\tDekompressionszeit (Std:Min)\tDekostopp-Tiefe\tTemperatur\tPO2\tPressluftflasche\tLesen des Druckes\tStatus der Verbindung\tTauchstatus"; - firstLine = "Sample time\tSample depth\t\t\t\t\t\t\t\tSample temperature\t"; - blockSignals(true); - ui->knownImports->setCurrentText("XP5"); - blockSignals(false); - } - - // Special handling for APD Log Viewer - if ((triggeredBy == KNOWNTYPES && (value == APD || value == APD2)) || (triggeredBy == INITIAL && fileNames.first().endsWith(".apd", Qt::CaseInsensitive))) { - apd=true; - firstLine = "Sample time\tSample depth\tSample setpoint\tSample sensor1 pOâ‚‚\tSample sensor2 pOâ‚‚\tSample sensor3 pOâ‚‚\tSample pOâ‚‚\t\t\t\t\t\t\t\t\tSample temperature\t\tSample CNS\tSample stopdepth"; - blockSignals(true); - ui->CSVSeparator->setCurrentText(tr("Tab")); - if (triggeredBy == INITIAL && fileNames.first().contains(".apd", Qt::CaseInsensitive)) - ui->knownImports->setCurrentText("APD Log Viewer - DC1"); - blockSignals(false); - } - - QString separator = ui->CSVSeparator->currentText() == tr("Tab") ? "\t" : ui->CSVSeparator->currentText(); - currColumns = firstLine.split(separator); - if (triggeredBy == INITIAL) { - // guess the separator - int tabs = firstLine.count('\t'); - int commas = firstLine.count(','); - int semis = firstLine.count(';'); - if (tabs > commas && tabs > semis) - separator = "\t"; - else if (commas > tabs && commas > semis) - separator = ","; - else if (semis > tabs && semis > commas) - separator = ";"; - if (ui->CSVSeparator->currentText() != separator) { - blockSignals(true); - ui->CSVSeparator->setCurrentText(separator); - blockSignals(false); - currColumns = firstLine.split(separator); - } - } - if (triggeredBy == INITIAL || (triggeredBy == KNOWNTYPES && value == MANUAL) || triggeredBy == SEPARATOR) { - // now try and guess the columns - Q_FOREACH (QString columnText, currColumns) { - /* - * We have to skip the conversion of 2 to â‚‚ for APD Log - * viewer as that would mess up the sensor numbering. We - * also know that the column headers do not need this - * conversion. - */ - if (apd == false) { - columnText.replace("\"", ""); - columnText.replace("number", "#", Qt::CaseInsensitive); - columnText.replace("2", "â‚‚", Qt::CaseInsensitive); - columnText.replace("cylinder", "cyl.", Qt::CaseInsensitive); - } - int idx = provider->mymatch(columnText.trimmed()); - if (idx >= 0) { - QString foundHeading = provider->data(provider->index(idx, 0), Qt::DisplayRole).toString(); - provider->removeRow(idx); - headers.append(foundHeading); - matchedSome = true; - } else { - headers.append(""); - } - } - if (matchedSome) { - ui->dragInstructions->setText(tr("Some column headers were pre-populated; please drag and drop the headers so they match the column they are in.")); - if (triggeredBy != KNOWNTYPES && !seabear && !xp5 && !apd) { - blockSignals(true); - ui->knownImports->setCurrentIndex(0); // <- that's "Manual import" - blockSignals(false); - } - } - } - if (triggeredBy == KNOWNTYPES && value != MANUAL) { - // an actual known type - if (value == SUBSURFACE) { - /* - * Subsurface CSV file needs separator detection - * as we used to default to comma but switched - * to tab. - */ - int tabs = firstLine.count('\t'); - int commas = firstLine.count(','); - if (tabs > commas) - separator = "Tab"; - else - separator = ","; - } else { - separator = CSVApps[value].separator; - } - - if (ui->CSVSeparator->currentText() != separator || separator == "Tab") { - ui->CSVSeparator->blockSignals(true); - ui->CSVSeparator->setCurrentText(separator); - ui->CSVSeparator->blockSignals(false); - if (separator == "Tab") - separator = "\t"; - currColumns = firstLine.split(separator); - } - // now set up time, depth, temperature, po2, cns, ndl, tts, stopdepth, pressure, setpoint - for (int i = 0; i < currColumns.count(); i++) - headers.append(""); - if (CSVApps[value].time > -1 && CSVApps[value].time < currColumns.count()) - headers.replace(CSVApps[value].time, tr("Sample time")); - if (CSVApps[value].depth > -1 && CSVApps[value].depth < currColumns.count()) - headers.replace(CSVApps[value].depth, tr("Sample depth")); - if (CSVApps[value].temperature > -1 && CSVApps[value].temperature < currColumns.count()) - headers.replace(CSVApps[value].temperature, tr("Sample temperature")); - if (CSVApps[value].po2 > -1 && CSVApps[value].po2 < currColumns.count()) - headers.replace(CSVApps[value].po2, tr("Sample pOâ‚‚")); - if (CSVApps[value].sensor1 > -1 && CSVApps[value].sensor1 < currColumns.count()) - headers.replace(CSVApps[value].sensor1, tr("Sample sensor1 pOâ‚‚")); - if (CSVApps[value].sensor2 > -1 && CSVApps[value].sensor2 < currColumns.count()) - headers.replace(CSVApps[value].sensor2, tr("Sample sensor2 pOâ‚‚")); - if (CSVApps[value].sensor3 > -1 && CSVApps[value].sensor3 < currColumns.count()) - headers.replace(CSVApps[value].sensor3, tr("Sample sensor3 pOâ‚‚")); - if (CSVApps[value].cns > -1 && CSVApps[value].cns < currColumns.count()) - headers.replace(CSVApps[value].cns, tr("Sample CNS")); - if (CSVApps[value].ndl > -1 && CSVApps[value].ndl < currColumns.count()) - headers.replace(CSVApps[value].ndl, tr("Sample NDL")); - if (CSVApps[value].tts > -1 && CSVApps[value].tts < currColumns.count()) - headers.replace(CSVApps[value].tts, tr("Sample TTS")); - if (CSVApps[value].stopdepth > -1 && CSVApps[value].stopdepth < currColumns.count()) - headers.replace(CSVApps[value].stopdepth, tr("Sample stopdepth")); - if (CSVApps[value].pressure > -1 && CSVApps[value].pressure < currColumns.count()) - headers.replace(CSVApps[value].pressure, tr("Sample pressure")); - if (CSVApps[value].setpoint > -1 && CSVApps[value].setpoint < currColumns.count()) - headers.replace(CSVApps[value].setpoint, tr("Sample setpoint")); - - /* Show the Subsurface CSV column headers */ - if (value == SUBSURFACE && currColumns.count() >= 23) { - headers.replace(0, tr("Dive #")); - headers.replace(1, tr("Date")); - headers.replace(2, tr("Time")); - headers.replace(3, tr("Duration")); - headers.replace(4, tr("Max. depth")); - headers.replace(5, tr("Avg. depth")); - headers.replace(6, tr("Air temp.")); - headers.replace(7, tr("Water temp.")); - headers.replace(8, tr("Cyl. size")); - headers.replace(9, tr("Start pressure")); - headers.replace(10, tr("End pressure")); - headers.replace(11, tr("Oâ‚‚")); - headers.replace(12, tr("He")); - headers.replace(13, tr("Location")); - headers.replace(14, tr("GPS")); - headers.replace(15, tr("Divemaster")); - headers.replace(16, tr("Buddy")); - headers.replace(17, tr("Suit")); - headers.replace(18, tr("Rating")); - headers.replace(19, tr("Visibility")); - headers.replace(20, tr("Notes")); - headers.replace(21, tr("Weight")); - headers.replace(22, tr("Tags")); - - blockSignals(true); - ui->CSVSeparator->setCurrentText(separator); - ui->DateFormat->setCurrentText("yyyy-mm-dd"); - ui->DurationFormat->setCurrentText("Minutes:seconds"); - blockSignals(false); - } - } - - f.reset(); - int rows = 0; - - /* Skipping the header of Seabear and XP5 CSV files. */ - if (seabear || xp5) { - /* - * First set of data on Seabear CSV file is metadata - * that is separated by an empty line (windows line - * termination might be encountered. - */ - while (strlen(f.readLine()) > 3 && !f.atEnd()); - /* - * Next we have description of the fields and two dummy - * lines. Separated again with an empty line from the - * actual data. - */ - while (strlen(f.readLine()) > 3 && !f.atEnd()); - } - - while (rows < 10 && !f.atEnd()) { - QString currLine = f.readLine().trimmed(); - currColumns = currLine.split(separator); - fileColumns.append(currColumns); - rows += 1; - } - resultModel->setColumnValues(fileColumns); - for (int i = 0; i < headers.count(); i++) - if (!headers.at(i).isEmpty()) - resultModel->setData(resultModel->index(0, i),headers.at(i),Qt::EditRole); -} - -char *intdup(int index) -{ - char tmpbuf[21]; - - snprintf(tmpbuf, sizeof(tmpbuf) - 2, "%d", index); - tmpbuf[20] = 0; - return strdup(tmpbuf); -} - -int DiveLogImportDialog::setup_csv_params(QStringList r, char **params, int pnr) -{ - params[pnr++] = strdup("timeField"); - params[pnr++] = intdup(r.indexOf(tr("Sample time"))); - params[pnr++] = strdup("depthField"); - params[pnr++] = intdup(r.indexOf(tr("Sample depth"))); - params[pnr++] = strdup("tempField"); - params[pnr++] = intdup(r.indexOf(tr("Sample temperature"))); - params[pnr++] = strdup("po2Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample pOâ‚‚"))); - params[pnr++] = strdup("o2sensor1Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample sensor1 pOâ‚‚"))); - params[pnr++] = strdup("o2sensor2Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample sensor2 pOâ‚‚"))); - params[pnr++] = strdup("o2sensor3Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample sensor3 pOâ‚‚"))); - params[pnr++] = strdup("cnsField"); - params[pnr++] = intdup(r.indexOf(tr("Sample CNS"))); - params[pnr++] = strdup("ndlField"); - params[pnr++] = intdup(r.indexOf(tr("Sample NDL"))); - params[pnr++] = strdup("ttsField"); - params[pnr++] = intdup(r.indexOf(tr("Sample TTS"))); - params[pnr++] = strdup("stopdepthField"); - params[pnr++] = intdup(r.indexOf(tr("Sample stopdepth"))); - params[pnr++] = strdup("pressureField"); - params[pnr++] = intdup(r.indexOf(tr("Sample pressure"))); - params[pnr++] = strdup("setpointFiend"); - params[pnr++] = intdup(r.indexOf(tr("Sample setpoint"))); - params[pnr++] = strdup("separatorIndex"); - params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); - params[pnr++] = strdup("units"); - params[pnr++] = intdup(ui->CSVUnits->currentIndex()); - if (hw.length()) { - params[pnr++] = strdup("hw"); - params[pnr++] = strdup(hw.toUtf8().data()); - } else if (ui->knownImports->currentText().length() > 0) { - params[pnr++] = strdup("hw"); - params[pnr++] = strdup(ui->knownImports->currentText().prepend("\"").append("\"").toUtf8().data()); - } - params[pnr++] = NULL; - - return pnr; -} - -void DiveLogImportDialog::on_buttonBox_accepted() -{ - QStringList r = resultModel->result(); - if (ui->knownImports->currentText() != "Manual import") { - for (int i = 0; i < fileNames.size(); ++i) { - if (ui->knownImports->currentText() == "Seabear CSV") { - char *params[40]; - int pnr = 0; - - params[pnr++] = strdup("timeField"); - params[pnr++] = intdup(r.indexOf(tr("Sample time"))); - params[pnr++] = strdup("depthField"); - params[pnr++] = intdup(r.indexOf(tr("Sample depth"))); - params[pnr++] = strdup("tempField"); - params[pnr++] = intdup(r.indexOf(tr("Sample temperature"))); - params[pnr++] = strdup("po2Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample pOâ‚‚"))); - params[pnr++] = strdup("o2sensor1Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample sensor1 pOâ‚‚"))); - params[pnr++] = strdup("o2sensor2Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample sensor2 pOâ‚‚"))); - params[pnr++] = strdup("o2sensor3Field"); - params[pnr++] = intdup(r.indexOf(tr("Sample sensor3 pOâ‚‚"))); - params[pnr++] = strdup("cnsField"); - params[pnr++] = intdup(r.indexOf(tr("Sample CNS"))); - params[pnr++] = strdup("ndlField"); - params[pnr++] = intdup(r.indexOf(tr("Sample NDL"))); - params[pnr++] = strdup("ttsField"); - params[pnr++] = intdup(r.indexOf(tr("Sample TTS"))); - params[pnr++] = strdup("stopdepthField"); - params[pnr++] = intdup(r.indexOf(tr("Sample stopdepth"))); - params[pnr++] = strdup("pressureField"); - params[pnr++] = intdup(r.indexOf(tr("Sample pressure"))); - params[pnr++] = strdup("setpointFiend"); - params[pnr++] = intdup(-1); - params[pnr++] = strdup("separatorIndex"); - params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); - params[pnr++] = strdup("units"); - params[pnr++] = intdup(ui->CSVUnits->currentIndex()); - params[pnr++] = strdup("delta"); - params[pnr++] = strdup(delta.toUtf8().data()); - if (hw.length()) { - params[pnr++] = strdup("hw"); - params[pnr++] = strdup(hw.toUtf8().data()); - } - params[pnr++] = NULL; - - if (parse_seabear_csv_file(fileNames[i].toUtf8().data(), - params, pnr - 1, "csv") < 0) { - return; - } - // Seabear CSV stores NDL and TTS in Minutes, not seconds - struct dive *dive = dive_table.dives[dive_table.nr - 1]; - for(int s_nr = 0 ; s_nr <= dive->dc.samples ; s_nr++) { - struct sample *sample = dive->dc.sample + s_nr; - sample->ndl.seconds *= 60; - sample->tts.seconds *= 60; - } - } else { - char *params[37]; - int pnr = 0; - - pnr = setup_csv_params(r, params, pnr); - parse_csv_file(fileNames[i].toUtf8().data(), params, pnr - 1, - specialCSV.contains(ui->knownImports->currentIndex()) ? CSVApps[ui->knownImports->currentIndex()].name.toUtf8().data() : "csv"); - } - } - } else { - for (int i = 0; i < fileNames.size(); ++i) { - if (r.indexOf(tr("Sample time")) < 0) { - char *params[55]; - int pnr = 0; - params[pnr++] = strdup("numberField"); - params[pnr++] = intdup(r.indexOf(tr("Dive #"))); - params[pnr++] = strdup("dateField"); - params[pnr++] = intdup(r.indexOf(tr("Date"))); - params[pnr++] = strdup("timeField"); - params[pnr++] = intdup(r.indexOf(tr("Time"))); - params[pnr++] = strdup("durationField"); - params[pnr++] = intdup(r.indexOf(tr("Duration"))); - params[pnr++] = strdup("locationField"); - params[pnr++] = intdup(r.indexOf(tr("Location"))); - params[pnr++] = strdup("gpsField"); - params[pnr++] = intdup(r.indexOf(tr("GPS"))); - params[pnr++] = strdup("maxDepthField"); - params[pnr++] = intdup(r.indexOf(tr("Max. depth"))); - params[pnr++] = strdup("meanDepthField"); - params[pnr++] = intdup(r.indexOf(tr("Avg. depth"))); - params[pnr++] = strdup("divemasterField"); - params[pnr++] = intdup(r.indexOf(tr("Divemaster"))); - params[pnr++] = strdup("buddyField"); - params[pnr++] = intdup(r.indexOf(tr("Buddy"))); - params[pnr++] = strdup("suitField"); - params[pnr++] = intdup(r.indexOf(tr("Suit"))); - params[pnr++] = strdup("notesField"); - params[pnr++] = intdup(r.indexOf(tr("Notes"))); - params[pnr++] = strdup("weightField"); - params[pnr++] = intdup(r.indexOf(tr("Weight"))); - params[pnr++] = strdup("tagsField"); - params[pnr++] = intdup(r.indexOf(tr("Tags"))); - params[pnr++] = strdup("separatorIndex"); - params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); - params[pnr++] = strdup("units"); - params[pnr++] = intdup(ui->CSVUnits->currentIndex()); - params[pnr++] = strdup("datefmt"); - params[pnr++] = intdup(ui->DateFormat->currentIndex()); - params[pnr++] = strdup("durationfmt"); - params[pnr++] = intdup(ui->DurationFormat->currentIndex()); - params[pnr++] = strdup("cylindersizeField"); - params[pnr++] = intdup(r.indexOf(tr("Cyl. size"))); - params[pnr++] = strdup("startpressureField"); - params[pnr++] = intdup(r.indexOf(tr("Start pressure"))); - params[pnr++] = strdup("endpressureField"); - params[pnr++] = intdup(r.indexOf(tr("End pressure"))); - params[pnr++] = strdup("o2Field"); - params[pnr++] = intdup(r.indexOf(tr("Oâ‚‚"))); - params[pnr++] = strdup("heField"); - params[pnr++] = intdup(r.indexOf(tr("He"))); - params[pnr++] = strdup("airtempField"); - params[pnr++] = intdup(r.indexOf(tr("Air temp."))); - params[pnr++] = strdup("watertempField"); - params[pnr++] = intdup(r.indexOf(tr("Water temp."))); - params[pnr++] = NULL; - - parse_manual_file(fileNames[i].toUtf8().data(), params, pnr - 1); - } else { - char *params[37]; - int pnr = 0; - - pnr = setup_csv_params(r, params, pnr); - parse_csv_file(fileNames[i].toUtf8().data(), params, pnr - 1, - specialCSV.contains(ui->knownImports->currentIndex()) ? CSVApps[ui->knownImports->currentIndex()].name.toUtf8().data() : "csv"); - } - } - } - - process_dives(true, false); - MainWindow::instance()->refreshDisplay(); -} - -TagDragDelegate::TagDragDelegate(QObject *parent) : QStyledItemDelegate(parent) -{ -} - -QSize TagDragDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const -{ - QSize originalSize = QStyledItemDelegate::sizeHint(option, index); - return originalSize + QSize(5,5); -} - -void TagDragDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const -{ - painter->save(); - painter->setRenderHints(QPainter::Antialiasing); - painter->setBrush(QBrush(AIR_BLUE_TRANS)); - painter->drawRoundedRect(option.rect.adjusted(2,2,-2,-2), 5, 5); - painter->restore(); - QStyledItemDelegate::paint(painter, option, index); -} diff --git a/qt-ui/divelogimportdialog.h b/qt-ui/divelogimportdialog.h deleted file mode 100644 index 2d12c7cac..000000000 --- a/qt-ui/divelogimportdialog.h +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef DIVELOGIMPORTDIALOG_H -#define DIVELOGIMPORTDIALOG_H - -#include -#include -#include -#include -#include -#include -#include - -#include "subsurface-core/dive.h" -#include "subsurface-core/divelist.h" - -namespace Ui { - class DiveLogImportDialog; -} - -class ColumnNameProvider : public QAbstractListModel { - Q_OBJECT -public: - ColumnNameProvider(QObject *parent); - bool insertRows(int row, int count, const QModelIndex &parent); - bool removeRows(int row, int count, const QModelIndex &parent); - bool setData(const QModelIndex &index, const QVariant &value, int role); - QVariant data(const QModelIndex &index, int role) const; - int rowCount(const QModelIndex &parent) const; - int mymatch(QString value) const; -private: - QStringList columnNames; -}; - -class ColumnNameResult : public QAbstractTableModel { - Q_OBJECT -public: - ColumnNameResult(QObject *parent); - bool setData(const QModelIndex &index, const QVariant &value, int role); - QVariant data(const QModelIndex &index, int role) const; - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - void setColumnValues(QList columns); - QStringList result() const; - void swapValues(int firstIndex, int secondIndex); -private: - QList columnValues; - QStringList columnNames; -}; - -class ColumnNameView : public QListView { - Q_OBJECT -public: - ColumnNameView(QWidget *parent); -protected: - void mousePressEvent(QMouseEvent *press); - void dragLeaveEvent(QDragLeaveEvent *leave); - void dragEnterEvent(QDragEnterEvent *event); - void dragMoveEvent(QDragMoveEvent *event); - void dropEvent(QDropEvent *event); -private: -}; - -class ColumnDropCSVView : public QTableView { - Q_OBJECT -public: - ColumnDropCSVView(QWidget *parent); -protected: - void mousePressEvent(QMouseEvent *press); - void dragLeaveEvent(QDragLeaveEvent *leave); - void dragEnterEvent(QDragEnterEvent *event); - void dragMoveEvent(QDragMoveEvent *event); - void dropEvent(QDropEvent *event); -private: - QStringList columns; -}; - -class DiveLogImportDialog : public QDialog { - Q_OBJECT - -public: - explicit DiveLogImportDialog(QStringList fn, QWidget *parent = 0); - ~DiveLogImportDialog(); - enum whatChanged { INITIAL, SEPARATOR, KNOWNTYPES }; -private -slots: - void on_buttonBox_accepted(); - void loadFileContentsSeperatorSelected(int value); - void loadFileContentsKnownTypesSelected(int value); - void loadFileContents(int value, enum whatChanged triggeredBy); - int setup_csv_params(QStringList r, char **params, int pnr); - -private: - bool selector; - QStringList fileNames; - Ui::DiveLogImportDialog *ui; - QList specialCSV; - int column; - ColumnNameResult *resultModel; - QString delta; - QString hw; - - struct CSVAppConfig { - QString name; - int time; - int depth; - int temperature; - int po2; - int sensor1; - int sensor2; - int sensor3; - int cns; - int ndl; - int tts; - int stopdepth; - int pressure; - int setpoint; - QString separator; - }; - -#define CSVAPPS 8 - static const CSVAppConfig CSVApps[CSVAPPS]; -}; - -class TagDragDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - TagDragDelegate(QObject *parent); - QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const; - void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const; -}; - -#endif // DIVELOGIMPORTDIALOG_H diff --git a/qt-ui/divelogimportdialog.ui b/qt-ui/divelogimportdialog.ui deleted file mode 100644 index 6d154b7c6..000000000 --- a/qt-ui/divelogimportdialog.ui +++ /dev/null @@ -1,249 +0,0 @@ - - - DiveLogImportDialog - - - - 0 - 0 - 614 - 434 - - - - Import dive log file - - - - :/subsurface-icon - - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 5 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 2 - - - - - -1 - - - - - - - - - - - dd.mm.yyyy - - - - - mm/dd/yyyy - - - - - yyyy-mm-dd - - - - - - - - - Seconds - - - - - Minutes - - - - - Minutes:seconds - - - - - - - - - Metric - - - - - Imperial - - - - - - - - - - - 16777215 - 100 - - - - QListView::IconMode - - - - - - - Drag the tags above to each corresponding column below - - - - - - - - 16777215 - 16777215 - - - - false - - - false - - - false - - - false - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - - - - ColumnNameView - QListView -
divelogimportdialog.h
-
- - ColumnDropCSVView - QTableView -
divelogimportdialog.h
-
-
- - buttonBox - - - - - buttonBox - accepted() - DiveLogImportDialog - accept() - - - 334 - 467 - - - 215 - 164 - - - - - buttonBox - rejected() - DiveLogImportDialog - reject() - - - 334 - 467 - - - 215 - 164 - - - - -
diff --git a/qt-ui/divepicturewidget.cpp b/qt-ui/divepicturewidget.cpp deleted file mode 100644 index bed3d3bd1..000000000 --- a/qt-ui/divepicturewidget.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "divepicturewidget.h" -#include "divepicturemodel.h" -#include "metrics.h" -#include "dive.h" -#include "divelist.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -void loadPicture(struct picture *picture) -{ - ImageDownloader download(picture); - download.load(); -} - -SHashedImage::SHashedImage(struct picture *picture) : QImage() -{ - QUrl url = QUrl::fromUserInput(QString(picture->filename)); - if(url.isLocalFile()) - load(url.toLocalFile()); - if (isNull()) { - // Hash lookup. - load(fileFromHash(picture->hash)); - if (!isNull()) { - QtConcurrent::run(updateHash, picture); - } else { - QtConcurrent::run(loadPicture, picture); - } - } else { - QByteArray hash = hashFile(url.toLocalFile()); - free(picture->hash); - picture->hash = strdup(hash.toHex().data()); - } -} - -ImageDownloader::ImageDownloader(struct picture *pic) -{ - picture = pic; -} - -void ImageDownloader::load(){ - QUrl url = QUrl::fromUserInput(QString(picture->filename)); - if (url.isValid()) { - QEventLoop loop; - QNetworkRequest request(url); - connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveImage(QNetworkReply *))); - QNetworkReply *reply = manager.get(request); - while (reply->isRunning()) { - loop.processEvents(); - sleep(1); - } - } - -} - -void ImageDownloader::saveImage(QNetworkReply *reply) -{ - QByteArray imageData = reply->readAll(); - QImage image = QImage(); - image.loadFromData(imageData); - if (image.isNull()) - return; - QCryptographicHash hash(QCryptographicHash::Sha1); - hash.addData(imageData); - QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); - QDir dir(path); - if (!dir.exists()) - dir.mkpath(path); - QFile imageFile(path.append("/").append(hash.result().toHex())); - if (imageFile.open(QIODevice::WriteOnly)) { - QDataStream stream(&imageFile); - stream.writeRawData(imageData.data(), imageData.length()); - imageFile.waitForBytesWritten(-1); - imageFile.close(); - add_hash(imageFile.fileName(), hash.result()); - learnHash(picture, hash.result()); - DivePictureModel::instance()->updateDivePictures(); - } - reply->manager()->deleteLater(); - reply->deleteLater(); -} - -DivePictureWidget::DivePictureWidget(QWidget *parent) : QListView(parent) -{ - connect(this, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(doubleClicked(const QModelIndex &))); -} - -void DivePictureWidget::doubleClicked(const QModelIndex &index) -{ - QString filePath = model()->data(index, Qt::DisplayPropertyRole).toString(); - emit photoDoubleClicked(localFilePath(filePath)); -} diff --git a/qt-ui/divepicturewidget.h b/qt-ui/divepicturewidget.h deleted file mode 100644 index 54f5bb826..000000000 --- a/qt-ui/divepicturewidget.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef DIVEPICTUREWIDGET_H -#define DIVEPICTUREWIDGET_H - -#include -#include -#include -#include -#include - -class ImageDownloader : public QObject { - Q_OBJECT; -public: - ImageDownloader(struct picture *picture); - void load(); -private: - struct picture *picture; - QNetworkAccessManager manager; -private slots: - void saveImage(QNetworkReply *reply); -}; - -class DivePictureWidget : public QListView { - Q_OBJECT -public: - DivePictureWidget(QWidget *parent); -signals: - void photoDoubleClicked(const QString filePath); -private -slots: - void doubleClicked(const QModelIndex &index); -}; - -class DivePictureThumbnailThread : public QThread { -}; - -#endif diff --git a/qt-ui/diveplanner.cpp b/qt-ui/diveplanner.cpp deleted file mode 100644 index b4413d11a..000000000 --- a/qt-ui/diveplanner.cpp +++ /dev/null @@ -1,513 +0,0 @@ -#include "diveplanner.h" -#include "modeldelegates.h" -#include "mainwindow.h" -#include "planner.h" -#include "helpers.h" -#include "cylindermodel.h" -#include "models.h" -#include "profile/profilewidget2.h" -#include "diveplannermodel.h" - -#include -#include -#include -#include - -#define TIME_INITIAL_MAX 30 - -#define MAX_DEPTH M_OR_FT(150, 450) -#define MIN_DEPTH M_OR_FT(20, 60) - -#define UNIT_FACTOR ((prefs.units.length == units::METERS) ? 1000.0 / 60.0 : feet_to_mm(1.0) / 60.0) - -static DivePlannerPointsModel* plannerModel = DivePlannerPointsModel::instance(); - -DiveHandler::DiveHandler() : QGraphicsEllipseItem() -{ - setRect(-5, -5, 10, 10); - setFlags(ItemIgnoresTransformations | ItemIsSelectable | ItemIsMovable | ItemSendsGeometryChanges); - setBrush(Qt::white); - setZValue(2); - t.start(); -} - -int DiveHandler::parentIndex() -{ - ProfileWidget2 *view = qobject_cast(scene()->views().first()); - return view->handles.indexOf(this); -} - -void DiveHandler::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) -{ - QMenu m; - // Don't have a gas selection for the last point - QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS); - if (index.sibling(index.row() + 1, index.column()).isValid()) { - GasSelectionModel *model = GasSelectionModel::instance(); - model->repopulate(); - int rowCount = model->rowCount(); - for (int i = 0; i < rowCount; i++) { - QAction *action = new QAction(&m); - action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString()); - connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); - m.addAction(action); - } - } - // don't allow removing the last point - if (plannerModel->rowCount() > 1) { - m.addSeparator(); - m.addAction(QObject::tr("Remove this point"), this, SLOT(selfRemove())); - m.exec(event->screenPos()); - } -} - -void DiveHandler::selfRemove() -{ - setSelected(true); - ProfileWidget2 *view = qobject_cast(scene()->views().first()); - view->keyDeleteAction(); -} - -void DiveHandler::changeGas() -{ - QAction *action = qobject_cast(sender()); - QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS); - plannerModel->gaschange(index.sibling(index.row() + 1, index.column()), action->text()); -} - -void DiveHandler::mouseMoveEvent(QGraphicsSceneMouseEvent *event) -{ - if (t.elapsed() < 40) - return; - t.start(); - - ProfileWidget2 *view = qobject_cast(scene()->views().first()); - if(view->isPointOutOfBoundaries(event->scenePos())) - return; - - QGraphicsEllipseItem::mouseMoveEvent(event); - emit moved(); -} - -void DiveHandler::mousePressEvent(QGraphicsSceneMouseEvent *event) -{ - QGraphicsItem::mousePressEvent(event); - emit clicked(); -} - -void DiveHandler::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) -{ - QGraphicsItem::mouseReleaseEvent(event); - emit released(); -} - -DivePlannerWidget::DivePlannerWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) -{ - ui.setupUi(this); - ui.dateEdit->setDisplayFormat(getDateFormat()); - ui.tableWidget->setTitle(tr("Dive planner points")); - ui.tableWidget->setModel(plannerModel); - plannerModel->setRecalc(true); - ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::GAS, new AirTypesDelegate(this)); - ui.cylinderTableWidget->setTitle(tr("Available gases")); - ui.cylinderTableWidget->setModel(CylindersModel::instance()); - QTableView *view = ui.cylinderTableWidget->view(); - view->setColumnHidden(CylindersModel::START, true); - view->setColumnHidden(CylindersModel::END, true); - view->setColumnHidden(CylindersModel::DEPTH, false); - view->setItemDelegateForColumn(CylindersModel::TYPE, new TankInfoDelegate(this)); - connect(ui.cylinderTableWidget, SIGNAL(addButtonClicked()), plannerModel, SLOT(addCylinder_clicked())); - connect(ui.tableWidget, SIGNAL(addButtonClicked()), plannerModel, SLOT(addStop())); - - connect(CylindersModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), - GasSelectionModel::instance(), SLOT(repopulate())); - connect(CylindersModel::instance(), SIGNAL(rowsInserted(QModelIndex, int, int)), - GasSelectionModel::instance(), SLOT(repopulate())); - connect(CylindersModel::instance(), SIGNAL(rowsRemoved(QModelIndex, int, int)), - GasSelectionModel::instance(), SLOT(repopulate())); - connect(CylindersModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), - plannerModel, SIGNAL(cylinderModelEdited())); - connect(CylindersModel::instance(), SIGNAL(rowsInserted(QModelIndex, int, int)), - plannerModel, SIGNAL(cylinderModelEdited())); - connect(CylindersModel::instance(), SIGNAL(rowsRemoved(QModelIndex, int, int)), - plannerModel, SIGNAL(cylinderModelEdited())); - connect(plannerModel, SIGNAL(calculatedPlanNotes()), MainWindow::instance(), SLOT(setPlanNotes())); - - - ui.tableWidget->setBtnToolTip(tr("Add dive data point")); - connect(ui.startTime, SIGNAL(timeChanged(QTime)), plannerModel, SLOT(setStartTime(QTime))); - connect(ui.dateEdit, SIGNAL(dateChanged(QDate)), plannerModel, SLOT(setStartDate(QDate))); - connect(ui.ATMPressure, SIGNAL(valueChanged(int)), this, SLOT(atmPressureChanged(int))); - connect(ui.atmHeight, SIGNAL(valueChanged(int)), this, SLOT(heightChanged(int))); - connect(ui.salinity, SIGNAL(valueChanged(double)), this, SLOT(salinityChanged(double))); - connect(plannerModel, SIGNAL(startTimeChanged(QDateTime)), this, SLOT(setupStartTime(QDateTime))); - - // Creating (and canceling) the plan - replanButton = ui.buttonBox->addButton(tr("Save new"), QDialogButtonBox::ActionRole); - connect(replanButton, SIGNAL(clicked()), plannerModel, SLOT(saveDuplicatePlan())); - connect(ui.buttonBox, SIGNAL(accepted()), plannerModel, SLOT(savePlan())); - connect(ui.buttonBox, SIGNAL(rejected()), plannerModel, SLOT(cancelPlan())); - QShortcut *closeKey = new QShortcut(QKeySequence(Qt::Key_Escape), this); - connect(closeKey, SIGNAL(activated()), plannerModel, SLOT(cancelPlan())); - - // This makes shure the spinbox gets a setMinimum(0) on it so we can't have negative time or depth. - ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::DEPTH, new SpinBoxDelegate(0, INT_MAX, 1, this)); - ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::RUNTIME, new SpinBoxDelegate(0, INT_MAX, 1, this)); - ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::DURATION, new SpinBoxDelegate(0, INT_MAX, 1, this)); - ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::CCSETPOINT, new DoubleSpinBoxDelegate(0, 2, 0.1, this)); - - /* set defaults. */ - ui.ATMPressure->setValue(1013); - ui.atmHeight->setValue(0); - - setMinimumWidth(0); - setMinimumHeight(0); -} - -void DivePlannerWidget::setReplanButton(bool replan) -{ - replanButton->setVisible(replan); -} - -void DivePlannerWidget::setupStartTime(QDateTime startTime) -{ - ui.startTime->setTime(startTime.time()); - ui.dateEdit->setDate(startTime.date()); -} - -void DivePlannerWidget::settingsChanged() -{ - // Adopt units - if (get_units()->length == units::FEET) { - ui.atmHeight->setSuffix("ft"); - } else { - ui.atmHeight->setSuffix(("m")); - } - ui.atmHeight->blockSignals(true); - ui.atmHeight->setValue((int) get_depth_units((int) (log(1013.0 / plannerModel->getSurfacePressure()) * 7800000), NULL,NULL)); - ui.atmHeight->blockSignals(false); -} - -void DivePlannerWidget::atmPressureChanged(const int pressure) -{ - plannerModel->setSurfacePressure(pressure); - ui.atmHeight->blockSignals(true); - ui.atmHeight->setValue((int) get_depth_units((int) (log(1013.0 / pressure) * 7800000), NULL,NULL)); - ui.atmHeight->blockSignals(false); -} - -void DivePlannerWidget::heightChanged(const int height) -{ - int pressure = (int) (1013.0 * exp(- (double) units_to_depth((double) height) / 7800000.0)); - ui.ATMPressure->blockSignals(true); - ui.ATMPressure->setValue(pressure); - ui.ATMPressure->blockSignals(false); - plannerModel->setSurfacePressure(pressure); -} - -void DivePlannerWidget::salinityChanged(const double salinity) -{ - /* Salinity is expressed in weight in grams per 10l */ - plannerModel->setSalinity(10000 * salinity); -} - -void PlannerSettingsWidget::bottomSacChanged(const double bottomSac) -{ - plannerModel->setBottomSac(bottomSac); -} - -void PlannerSettingsWidget::decoSacChanged(const double decosac) -{ - plannerModel->setDecoSac(decosac); -} - -void PlannerSettingsWidget::disableDecoElements(int mode) -{ - if (mode == RECREATIONAL) { - ui.gflow->setDisabled(false); - ui.gfhigh->setDisabled(false); - ui.lastStop->setDisabled(true); - ui.backgasBreaks->setDisabled(true); - ui.bottompo2->setDisabled(true); - ui.decopo2->setDisabled(true); - ui.reserve_gas->setDisabled(false); - ui.conservatism_lvl->setDisabled(true); - ui.switch_at_req_stop->setDisabled(true); - ui.min_switch_duration->setDisabled(true); - } - else if (mode == VPMB) { - ui.gflow->setDisabled(true); - ui.gfhigh->setDisabled(true); - ui.lastStop->setDisabled(false); - ui.backgasBreaks->setDisabled(false); - ui.bottompo2->setDisabled(false); - ui.decopo2->setDisabled(false); - ui.reserve_gas->setDisabled(true); - ui.conservatism_lvl->setDisabled(false); - ui.switch_at_req_stop->setDisabled(false); - ui.min_switch_duration->setDisabled(false); - } - else if (mode == BUEHLMANN) { - ui.gflow->setDisabled(false); - ui.gfhigh->setDisabled(false); - ui.lastStop->setDisabled(false); - ui.backgasBreaks->setDisabled(false); - ui.bottompo2->setDisabled(false); - ui.decopo2->setDisabled(false); - ui.reserve_gas->setDisabled(true); - ui.conservatism_lvl->setDisabled(true); - ui.switch_at_req_stop->setDisabled(false); - ui.min_switch_duration->setDisabled(false); - } -} - -void DivePlannerWidget::printDecoPlan() -{ - MainWindow::instance()->printPlan(); -} - -PlannerSettingsWidget::PlannerSettingsWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) -{ - ui.setupUi(this); - - QSettings s; - QStringList rebreather_modes; - s.beginGroup("Planner"); - prefs.last_stop = s.value("last_stop", prefs.last_stop).toBool(); - prefs.verbatim_plan = s.value("verbatim_plan", prefs.verbatim_plan).toBool(); - prefs.display_duration = s.value("display_duration", prefs.display_duration).toBool(); - prefs.display_runtime = s.value("display_runtime", prefs.display_runtime).toBool(); - prefs.display_transitions = s.value("display_transitions", prefs.display_transitions).toBool(); - prefs.deco_mode = deco_mode(s.value("deco_mode", prefs.deco_mode).toInt()); - prefs.safetystop = s.value("safetystop", prefs.safetystop).toBool(); - prefs.reserve_gas = s.value("reserve_gas", prefs.reserve_gas).toInt(); - prefs.ascrate75 = s.value("ascrate75", prefs.ascrate75).toInt(); - prefs.ascrate50 = s.value("ascrate50", prefs.ascrate50).toInt(); - prefs.ascratestops = s.value("ascratestops", prefs.ascratestops).toInt(); - prefs.ascratelast6m = s.value("ascratelast6m", prefs.ascratelast6m).toInt(); - prefs.descrate = s.value("descrate", prefs.descrate).toInt(); - prefs.bottompo2 = s.value("bottompo2", prefs.bottompo2).toInt(); - prefs.decopo2 = s.value("decopo2", prefs.decopo2).toInt(); - prefs.doo2breaks = s.value("doo2breaks", prefs.doo2breaks).toBool(); - prefs.switch_at_req_stop = s.value("switch_at_req_stop", prefs.switch_at_req_stop).toBool(); - prefs.min_switch_duration = s.value("min_switch_duration", prefs.min_switch_duration).toInt(); - prefs.drop_stone_mode = s.value("drop_stone_mode", prefs.drop_stone_mode).toBool(); - prefs.bottomsac = s.value("bottomsac", prefs.bottomsac).toInt(); - prefs.decosac = s.value("decosac", prefs.decosac).toInt(); - prefs.conservatism_level = s.value("conservatism", prefs.conservatism_level).toInt(); - plannerModel->getDiveplan().bottomsac = prefs.bottomsac; - plannerModel->getDiveplan().decosac = prefs.decosac; - s.endGroup(); - - updateUnitsUI(); - ui.lastStop->setChecked(prefs.last_stop); - ui.verbatim_plan->setChecked(prefs.verbatim_plan); - ui.display_duration->setChecked(prefs.display_duration); - ui.display_runtime->setChecked(prefs.display_runtime); - ui.display_transitions->setChecked(prefs.display_transitions); - ui.safetystop->setChecked(prefs.safetystop); - ui.reserve_gas->setValue(prefs.reserve_gas / 1000); - ui.bottompo2->setValue(prefs.bottompo2 / 1000.0); - ui.decopo2->setValue(prefs.decopo2 / 1000.0); - ui.backgasBreaks->setChecked(prefs.doo2breaks); - ui.drop_stone_mode->setChecked(prefs.drop_stone_mode); - ui.switch_at_req_stop->setChecked(prefs.switch_at_req_stop); - ui.min_switch_duration->setValue(prefs.min_switch_duration / 60); - ui.recreational_deco->setChecked(prefs.deco_mode == RECREATIONAL); - ui.buehlmann_deco->setChecked(prefs.deco_mode == BUEHLMANN); - ui.vpmb_deco->setChecked(prefs.deco_mode == VPMB); - ui.conservatism_lvl->setValue(prefs.conservatism_level); - disableDecoElements((int) prefs.deco_mode); - - // should be the same order as in dive_comp_type! - rebreather_modes << tr("Open circuit") << tr("CCR") << tr("pSCR"); - ui.rebreathermode->insertItems(0, rebreather_modes); - - modeMapper = new QSignalMapper(this); - connect(modeMapper, SIGNAL(mapped(int)) , plannerModel, SLOT(setDecoMode(int))); - modeMapper->setMapping(ui.recreational_deco, int(RECREATIONAL)); - modeMapper->setMapping(ui.buehlmann_deco, int(BUEHLMANN)); - modeMapper->setMapping(ui.vpmb_deco, int(VPMB)); - - connect(ui.recreational_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); - connect(ui.buehlmann_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); - connect(ui.vpmb_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); - - connect(ui.lastStop, SIGNAL(toggled(bool)), plannerModel, SLOT(setLastStop6m(bool))); - connect(ui.verbatim_plan, SIGNAL(toggled(bool)), plannerModel, SLOT(setVerbatim(bool))); - connect(ui.display_duration, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayDuration(bool))); - connect(ui.display_runtime, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayRuntime(bool))); - connect(ui.display_transitions, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayTransitions(bool))); - connect(ui.safetystop, SIGNAL(toggled(bool)), plannerModel, SLOT(setSafetyStop(bool))); - connect(ui.reserve_gas, SIGNAL(valueChanged(int)), plannerModel, SLOT(setReserveGas(int))); - connect(ui.ascRate75, SIGNAL(valueChanged(int)), this, SLOT(setAscRate75(int))); - connect(ui.ascRate75, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); - connect(ui.ascRate50, SIGNAL(valueChanged(int)), this, SLOT(setAscRate50(int))); - connect(ui.ascRate50, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); - connect(ui.ascRateStops, SIGNAL(valueChanged(int)), this, SLOT(setAscRateStops(int))); - connect(ui.ascRateStops, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); - connect(ui.ascRateLast6m, SIGNAL(valueChanged(int)), this, SLOT(setAscRateLast6m(int))); - connect(ui.ascRateLast6m, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); - connect(ui.descRate, SIGNAL(valueChanged(int)), this, SLOT(setDescRate(int))); - connect(ui.descRate, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); - connect(ui.bottompo2, SIGNAL(valueChanged(double)), this, SLOT(setBottomPo2(double))); - connect(ui.decopo2, SIGNAL(valueChanged(double)), this, SLOT(setDecoPo2(double))); - connect(ui.drop_stone_mode, SIGNAL(toggled(bool)), plannerModel, SLOT(setDropStoneMode(bool))); - connect(ui.bottomSAC, SIGNAL(valueChanged(double)), this, SLOT(bottomSacChanged(double))); - connect(ui.decoStopSAC, SIGNAL(valueChanged(double)), this, SLOT(decoSacChanged(double))); - connect(ui.gfhigh, SIGNAL(valueChanged(int)), plannerModel, SLOT(setGFHigh(int))); - connect(ui.gflow, SIGNAL(valueChanged(int)), plannerModel, SLOT(setGFLow(int))); - connect(ui.gfhigh, SIGNAL(editingFinished()), plannerModel, SLOT(triggerGFHigh())); - connect(ui.gflow, SIGNAL(editingFinished()), plannerModel, SLOT(triggerGFLow())); - connect(ui.conservatism_lvl, SIGNAL(valueChanged(int)), plannerModel, SLOT(setConservatism(int))); - connect(ui.backgasBreaks, SIGNAL(toggled(bool)), this, SLOT(setBackgasBreaks(bool))); - connect(ui.switch_at_req_stop, SIGNAL(toggled(bool)), plannerModel, SLOT(setSwitchAtReqStop(bool))); - connect(ui.min_switch_duration, SIGNAL(valueChanged(int)), plannerModel, SLOT(setMinSwitchDuration(int))); - connect(ui.rebreathermode, SIGNAL(currentIndexChanged(int)), plannerModel, SLOT(setRebreatherMode(int))); - connect(modeMapper, SIGNAL(mapped(int)), this, SLOT(disableDecoElements(int))); - - settingsChanged(); - ui.gflow->setValue(prefs.gflow); - ui.gfhigh->setValue(prefs.gfhigh); - - setMinimumWidth(0); - setMinimumHeight(0); -} - -void PlannerSettingsWidget::updateUnitsUI() -{ - ui.ascRate75->setValue(rint(prefs.ascrate75 / UNIT_FACTOR)); - ui.ascRate50->setValue(rint(prefs.ascrate50 / UNIT_FACTOR)); - ui.ascRateStops->setValue(rint(prefs.ascratestops / UNIT_FACTOR)); - ui.ascRateLast6m->setValue(rint(prefs.ascratelast6m / UNIT_FACTOR)); - ui.descRate->setValue(rint(prefs.descrate / UNIT_FACTOR)); -} - -PlannerSettingsWidget::~PlannerSettingsWidget() -{ - QSettings s; - s.beginGroup("Planner"); - s.setValue("last_stop", prefs.last_stop); - s.setValue("verbatim_plan", prefs.verbatim_plan); - s.setValue("display_duration", prefs.display_duration); - s.setValue("display_runtime", prefs.display_runtime); - s.setValue("display_transitions", prefs.display_transitions); - s.setValue("safetystop", prefs.safetystop); - s.setValue("reserve_gas", prefs.reserve_gas); - s.setValue("ascrate75", prefs.ascrate75); - s.setValue("ascrate50", prefs.ascrate50); - s.setValue("ascratestops", prefs.ascratestops); - s.setValue("ascratelast6m", prefs.ascratelast6m); - s.setValue("descrate", prefs.descrate); - s.setValue("bottompo2", prefs.bottompo2); - s.setValue("decopo2", prefs.decopo2); - s.setValue("doo2breaks", prefs.doo2breaks); - s.setValue("drop_stone_mode", prefs.drop_stone_mode); - s.setValue("switch_at_req_stop", prefs.switch_at_req_stop); - s.setValue("min_switch_duration", prefs.min_switch_duration); - s.setValue("bottomsac", prefs.bottomsac); - s.setValue("decosac", prefs.decosac); - s.setValue("deco_mode", int(prefs.deco_mode)); - s.setValue("conservatism", prefs.conservatism_level); - s.endGroup(); -} - -void PlannerSettingsWidget::settingsChanged() -{ - QString vs; - // don't recurse into setting the value from the ui when setting the ui from the value - ui.bottomSAC->blockSignals(true); - ui.decoStopSAC->blockSignals(true); - if (get_units()->length == units::FEET) { - vs.append(tr("ft/min")); - ui.lastStop->setText(tr("Last stop at 20ft")); - ui.asc50to6->setText(tr("50% avg. depth to 20ft")); - ui.asc6toSurf->setText(tr("20ft to surface")); - } else { - vs.append(tr("m/min")); - ui.lastStop->setText(tr("Last stop at 6m")); - ui.asc50to6->setText(tr("50% avg. depth to 6m")); - ui.asc6toSurf->setText(tr("6m to surface")); - } - if(get_units()->volume == units::CUFT) { - ui.bottomSAC->setSuffix(tr("cuft/min")); - ui.decoStopSAC->setSuffix(tr("cuft/min")); - ui.bottomSAC->setDecimals(2); - ui.bottomSAC->setSingleStep(0.1); - ui.decoStopSAC->setDecimals(2); - ui.decoStopSAC->setSingleStep(0.1); - ui.bottomSAC->setValue(ml_to_cuft(prefs.bottomsac)); - ui.decoStopSAC->setValue(ml_to_cuft(prefs.decosac)); - } else { - ui.bottomSAC->setSuffix(tr("â„“/min")); - ui.decoStopSAC->setSuffix(tr("â„“/min")); - ui.bottomSAC->setDecimals(0); - ui.bottomSAC->setSingleStep(1); - ui.decoStopSAC->setDecimals(0); - ui.decoStopSAC->setSingleStep(1); - ui.bottomSAC->setValue((double) prefs.bottomsac / 1000.0); - ui.decoStopSAC->setValue((double) prefs.decosac / 1000.0); - } - ui.bottomSAC->blockSignals(false); - ui.decoStopSAC->blockSignals(false); - updateUnitsUI(); - ui.ascRate75->setSuffix(vs); - ui.ascRate50->setSuffix(vs); - ui.ascRateStops->setSuffix(vs); - ui.ascRateLast6m->setSuffix(vs); - ui.descRate->setSuffix(vs); -} - -void PlannerSettingsWidget::atmPressureChanged(const QString &pressure) -{ -} - -void PlannerSettingsWidget::printDecoPlan() -{ -} - -void PlannerSettingsWidget::setAscRate75(int rate) -{ - prefs.ascrate75 = rate * UNIT_FACTOR; -} - -void PlannerSettingsWidget::setAscRate50(int rate) -{ - prefs.ascrate50 = rate * UNIT_FACTOR; -} - -void PlannerSettingsWidget::setAscRateStops(int rate) -{ - prefs.ascratestops = rate * UNIT_FACTOR; -} - -void PlannerSettingsWidget::setAscRateLast6m(int rate) -{ - prefs.ascratelast6m = rate * UNIT_FACTOR; -} - -void PlannerSettingsWidget::setDescRate(int rate) -{ - prefs.descrate = rate * UNIT_FACTOR; -} - -void PlannerSettingsWidget::setBottomPo2(double po2) -{ - prefs.bottompo2 = (int) (po2 * 1000.0); -} - -void PlannerSettingsWidget::setDecoPo2(double po2) -{ - prefs.decopo2 = (int) (po2 * 1000.0); -} - -void PlannerSettingsWidget::setBackgasBreaks(bool dobreaks) -{ - prefs.doo2breaks = dobreaks; - plannerModel->emitDataChanged(); -} - -PlannerDetails::PlannerDetails(QWidget *parent) : QWidget(parent) -{ - ui.setupUi(this); -} diff --git a/qt-ui/diveplanner.h b/qt-ui/diveplanner.h deleted file mode 100644 index b2e03a97b..000000000 --- a/qt-ui/diveplanner.h +++ /dev/null @@ -1,104 +0,0 @@ -#ifndef DIVEPLANNER_H -#define DIVEPLANNER_H - -#include -#include -#include -#include -#include - -#include "dive.h" - -class QListView; -class QModelIndex; -class DivePlannerPointsModel; - -class DiveHandler : public QObject, public QGraphicsEllipseItem { - Q_OBJECT -public: - DiveHandler(); - -protected: - void contextMenuEvent(QGraphicsSceneContextMenuEvent *event); - void mouseMoveEvent(QGraphicsSceneMouseEvent *event); - void mousePressEvent(QGraphicsSceneMouseEvent *event); - void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); -signals: - void moved(); - void clicked(); - void released(); -private: - int parentIndex(); -public -slots: - void selfRemove(); - void changeGas(); -private: - QTime t; -}; - -#include "ui_diveplanner.h" - -class DivePlannerWidget : public QWidget { - Q_OBJECT -public: - explicit DivePlannerWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); - void setReplanButton(bool replan); -public -slots: - void setupStartTime(QDateTime startTime); - void settingsChanged(); - void atmPressureChanged(const int pressure); - void heightChanged(const int height); - void salinityChanged(const double salinity); - void printDecoPlan(); - -private: - Ui::DivePlanner ui; - QAbstractButton *replanButton; -}; - -#include "ui_plannerSettings.h" - -class PlannerSettingsWidget : public QWidget { - Q_OBJECT -public: - explicit PlannerSettingsWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); - virtual ~PlannerSettingsWidget(); -public -slots: - void settingsChanged(); - void atmPressureChanged(const QString &pressure); - void bottomSacChanged(const double bottomSac); - void decoSacChanged(const double decosac); - void printDecoPlan(); - void setAscRate75(int rate); - void setAscRate50(int rate); - void setAscRateStops(int rate); - void setAscRateLast6m(int rate); - void setDescRate(int rate); - void setBottomPo2(double po2); - void setDecoPo2(double po2); - void setBackgasBreaks(bool dobreaks); - void disableDecoElements(int mode); - -private: - Ui::plannerSettingsWidget ui; - void updateUnitsUI(); - QSignalMapper *modeMapper; -}; - -#include "ui_plannerDetails.h" - -class PlannerDetails : public QWidget { - Q_OBJECT -public: - explicit PlannerDetails(QWidget *parent = 0); - QPushButton *printPlan() const { return ui.printPlan; } - QTextEdit *divePlanOutput() const { return ui.divePlanOutput; } - -private: - Ui::plannerDetails ui; -}; - -#endif // DIVEPLANNER_H diff --git a/qt-ui/diveplanner.ui b/qt-ui/diveplanner.ui deleted file mode 100644 index adb44fad9..000000000 --- a/qt-ui/diveplanner.ui +++ /dev/null @@ -1,257 +0,0 @@ - - - DivePlanner - - - true - - - - 0 - 0 - 515 - 591 - - - - - 5 - - - 5 - - - 5 - - - 5 - - - 0 - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 505 - 581 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 2 - - - - - - 0 - 0 - - - - - 0 - 50 - - - - - - - - - 0 - 0 - - - - - 0 - 50 - - - - - - - - - 0 - 0 - - - - Planned dive time - - - - - - - - 0 - 0 - - - - true - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - - - - - - Altitude - - - - - - - ATM pressure - - - - - - - Salinity - - - - - - - - 0 - 0 - - - - mbar - - - 689 - - - 1100 - - - - - - - - 0 - 0 - - - - m - - - -100 - - - 3000 - - - 10 - - - - - - - - 0 - 0 - - - - kg/â„“ - - - 1.000000000000000 - - - 1.050000000000000 - - - 0.010000000000000 - - - 1.030000000000000 - - - - - - - - - - - - TableView - QWidget -
tableview.h
- 1 -
-
- - startTime - buttonBox - scrollArea - - - -
diff --git a/qt-ui/diveshareexportdialog.cpp b/qt-ui/diveshareexportdialog.cpp deleted file mode 100644 index 9fe6eefd6..000000000 --- a/qt-ui/diveshareexportdialog.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include "diveshareexportdialog.h" -#include "ui_diveshareexportdialog.h" -#include "mainwindow.h" -#include "save-html.h" -#include "subsurfacewebservices.h" -#include "helpers.h" - -#include -#include - -DiveShareExportDialog::DiveShareExportDialog(QWidget *parent) : - QDialog(parent), - ui(new Ui::DiveShareExportDialog), - reply(NULL), - exportSelected(false) -{ - ui->setupUi(this); -} - -DiveShareExportDialog::~DiveShareExportDialog() -{ - delete ui; -} - -void DiveShareExportDialog::UIDFromBrowser() -{ - QDesktopServices::openUrl(QUrl(DIVESHARE_BASE_URI "/secret")); -} - -DiveShareExportDialog *DiveShareExportDialog::instance() -{ - static DiveShareExportDialog *self = new DiveShareExportDialog(MainWindow::instance()); - self->setAttribute(Qt::WA_QuitOnClose, false); - self->ui->txtResult->setHtml(""); - self->ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); - return self; -} - -void DiveShareExportDialog::prepareDivesForUpload(bool selected) -{ - exportSelected = selected; - ui->frameConfigure->setVisible(true); - ui->frameResults->setVisible(false); - - QSettings settings; - if (settings.contains("diveshareExport/uid")) - ui->txtUID->setText(settings.value("diveshareExport/uid").toString()); - - if (settings.contains("diveshareExport/private")) - ui->chkPrivate->setChecked(settings.value("diveshareExport/private").toBool()); - - show(); -} - -static QByteArray generate_html_list(const QByteArray &data) -{ - QList dives = data.split('\n'); - QByteArray html; - html.append(""); - for (int i = 0; i < dives.length(); i++ ) { - html.append(""); - QList dive_details = dives[i].split(','); - if (dive_details.length() < 3) - continue; - - QByteArray dive_id = dive_details[0]; - QByteArray dive_delete = dive_details[1]; - - html.append(""); - html.append("" ); - - html.append(""); - } - - html.append("
"); - html.append(""); - - //Title gets separated too, this puts it back together - const char *sep = ""; - for (int t = 2; t < dive_details.length(); t++) { - html.append(sep); - html.append(dive_details[t]); - sep = ","; - } - - html.append(""); - html.append(""); - html.append("Delete dive"); - html.append("
"); - return html; -} - -void DiveShareExportDialog::finishedSlot() -{ - ui->progressBar->setVisible(false); - if (reply->error() != 0) { - ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); - ui->txtResult->setText(reply->errorString()); - } else { - ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok); - ui->txtResult->setHtml(generate_html_list(reply->readAll())); - } - - reply->deleteLater(); -} - -void DiveShareExportDialog::doUpload() -{ - //Store current settings - QSettings settings; - settings.setValue("diveshareExport/uid", ui->txtUID->text()); - settings.setValue("diveshareExport/private", ui->chkPrivate->isChecked()); - - //Change UI into results mode - ui->frameConfigure->setVisible(false); - ui->frameResults->setVisible(true); - ui->progressBar->setVisible(true); - ui->progressBar->setRange(0, 0); - - //generate json - struct membuffer buf = { 0 }; - export_list(&buf, NULL, exportSelected, false); - QByteArray json_data(buf.buffer, buf.len); - free_buffer(&buf); - - //Request to server - QNetworkRequest request; - - if (ui->chkPrivate->isChecked()) - request.setUrl(QUrl(DIVESHARE_BASE_URI "/upload?private=true")); - else - request.setUrl(QUrl(DIVESHARE_BASE_URI "/upload")); - - request.setRawHeader("User-Agent", getUserAgent().toUtf8()); - if (ui->txtUID->text().length() != 0) - request.setRawHeader("X-UID", ui->txtUID->text().toUtf8()); - - reply = WebServices::manager()->put(request, json_data); - - QObject::connect(reply, SIGNAL(finished()), this, SLOT(finishedSlot())); -} diff --git a/qt-ui/diveshareexportdialog.h b/qt-ui/diveshareexportdialog.h deleted file mode 100644 index 85dadf5f1..000000000 --- a/qt-ui/diveshareexportdialog.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef DIVESHAREEXPORTDIALOG_H -#define DIVESHAREEXPORTDIALOG_H - -#include -#include -#include - -#define DIVESHARE_WEBSITE "dive-share.appspot.com" -#define DIVESHARE_BASE_URI "http://" DIVESHARE_WEBSITE - -namespace Ui { -class DiveShareExportDialog; -} - -class DiveShareExportDialog : public QDialog -{ - Q_OBJECT -public: - explicit DiveShareExportDialog(QWidget *parent = 0); - ~DiveShareExportDialog(); - static DiveShareExportDialog *instance(); - void prepareDivesForUpload(bool); -private: - Ui::DiveShareExportDialog *ui; - bool exportSelected; - QNetworkReply *reply; -private -slots: - void UIDFromBrowser(); - void doUpload(); - void finishedSlot(); -}; - -#endif // DIVESHAREEXPORTDIALOG_H diff --git a/qt-ui/diveshareexportdialog.ui b/qt-ui/diveshareexportdialog.ui deleted file mode 100644 index 2235740c8..000000000 --- a/qt-ui/diveshareexportdialog.ui +++ /dev/null @@ -1,291 +0,0 @@ - - - DiveShareExportDialog - - - - 0 - 0 - 593 - 420 - - - - Dialog - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - QFrame::NoFrame - - - QFrame::Plain - - - 0 - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 2 - - - - - User ID - - - - - - - - - - - - - - ⌫ - - - - - - - Get user ID - - - - - - - - - <html><head/><body><p><span style=" font-size:20pt; font-weight:600; color:#ff8000;">âš </span> Not using a UserID means that you will need to manually keep bookmarks to your dives, to find them again.</p></body></html> - - - Qt::AutoText - - - true - - - - - - - Private dives will not appear in "related dives" lists, and will only be accessible if their URL is known. - - - Keep dives private - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Upload dive data - - - false - - - true - - - - - - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Oxygen-Sans'; font-size:7pt; font-weight:600; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - - - true - - - - - - - 24 - - - - - - - - - - Qt::Vertical - - - - 20 - 68 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel - - - - - - - - - buttonBox - rejected() - DiveShareExportDialog - reject() - - - 97 - 299 - - - 286 - 252 - - - - - getUIDbutton - clicked() - DiveShareExportDialog - UIDFromBrowser() - - - 223 - 29 - - - 159 - 215 - - - - - doUploadButton - clicked() - DiveShareExportDialog - doUpload() - - - 223 - 120 - - - 159 - 215 - - - - - buttonBox - accepted() - DiveShareExportDialog - accept() - - - 159 - 288 - - - 159 - 154 - - - - - - getUID() - doUpload() - - diff --git a/qt-ui/downloadfromdivecomputer.cpp b/qt-ui/downloadfromdivecomputer.cpp deleted file mode 100644 index 4c8fa6b4a..000000000 --- a/qt-ui/downloadfromdivecomputer.cpp +++ /dev/null @@ -1,727 +0,0 @@ -#include "downloadfromdivecomputer.h" -#include "helpers.h" -#include "mainwindow.h" -#include "divelistview.h" -#include "display.h" -#include "uemis.h" -#include "models.h" - -#include -#include -#include -#include - -struct product { - const char *product; - dc_descriptor_t *descriptor; - struct product *next; -}; - -struct vendor { - const char *vendor; - struct product *productlist; - struct vendor *next; -}; - -struct mydescriptor { - const char *vendor; - const char *product; - dc_family_t type; - unsigned int model; -}; - -namespace DownloadFromDcGlobal { - const char *err_string; -}; - -struct dive_table downloadTable; - -DownloadFromDCWidget::DownloadFromDCWidget(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), - thread(0), - downloading(false), - previousLast(0), - vendorModel(0), - productModel(0), - timer(new QTimer(this)), - dumpWarningShown(false), - ostcFirmwareCheck(0), - currentState(INITIAL) -{ - clear_table(&downloadTable); - ui.setupUi(this); - ui.progressBar->hide(); - ui.progressBar->setMinimum(0); - ui.progressBar->setMaximum(100); - diveImportedModel = new DiveImportedModel(this); - ui.downloadedView->setModel(diveImportedModel); - ui.downloadedView->setSelectionBehavior(QAbstractItemView::SelectRows); - ui.downloadedView->setSelectionMode(QAbstractItemView::SingleSelection); - int startingWidth = defaultModelFont().pointSize(); - ui.downloadedView->setColumnWidth(0, startingWidth * 20); - ui.downloadedView->setColumnWidth(1, startingWidth * 10); - ui.downloadedView->setColumnWidth(2, startingWidth * 10); - connect(ui.downloadedView, SIGNAL(clicked(QModelIndex)), diveImportedModel, SLOT(changeSelected(QModelIndex))); - - progress_bar_text = ""; - - fill_computer_list(); - - ui.chooseDumpFile->setEnabled(ui.dumpToFile->isChecked()); - connect(ui.chooseDumpFile, SIGNAL(clicked()), this, SLOT(pickDumpFile())); - connect(ui.dumpToFile, SIGNAL(stateChanged(int)), this, SLOT(checkDumpFile(int))); - ui.chooseLogFile->setEnabled(ui.logToFile->isChecked()); - connect(ui.chooseLogFile, SIGNAL(clicked()), this, SLOT(pickLogFile())); - connect(ui.logToFile, SIGNAL(stateChanged(int)), this, SLOT(checkLogFile(int))); - ui.selectAllButton->setEnabled(false); - ui.unselectAllButton->setEnabled(false); - connect(ui.selectAllButton, SIGNAL(clicked()), diveImportedModel, SLOT(selectAll())); - connect(ui.unselectAllButton, SIGNAL(clicked()), diveImportedModel, SLOT(selectNone())); - vendorModel = new QStringListModel(vendorList); - ui.vendor->setModel(vendorModel); - if (default_dive_computer_vendor) { - ui.vendor->setCurrentIndex(ui.vendor->findText(default_dive_computer_vendor)); - productModel = new QStringListModel(productList[default_dive_computer_vendor]); - ui.product->setModel(productModel); - if (default_dive_computer_product) - ui.product->setCurrentIndex(ui.product->findText(default_dive_computer_product)); - } - if (default_dive_computer_device) - ui.device->setEditText(default_dive_computer_device); - - timer->setInterval(200); - connect(timer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); - updateState(INITIAL); - memset(&data, 0, sizeof(data)); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); - ui.ok->setEnabled(false); - ui.downloadCancelRetryButton->setEnabled(true); - ui.downloadCancelRetryButton->setText(tr("Download")); - -#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) - ui.bluetoothMode->setText(tr("Choose Bluetooth download mode")); - ui.bluetoothMode->setChecked(default_dive_computer_download_mode == DC_TRANSPORT_BLUETOOTH); - btDeviceSelectionDialog = 0; - ui.chooseBluetoothDevice->setEnabled(ui.bluetoothMode->isChecked()); - connect(ui.bluetoothMode, SIGNAL(stateChanged(int)), this, SLOT(enableBluetoothMode(int))); - connect(ui.chooseBluetoothDevice, SIGNAL(clicked()), this, SLOT(selectRemoteBluetoothDevice())); -#else - ui.bluetoothMode->hide(); - ui.chooseBluetoothDevice->hide(); -#endif -} - -void DownloadFromDCWidget::updateProgressBar() -{ - if (*progress_bar_text != '\0') { - ui.progressBar->setFormat(progress_bar_text); - } else { - ui.progressBar->setFormat("%p%"); - } - ui.progressBar->setValue(progress_bar_fraction * 100); -} - -void DownloadFromDCWidget::updateState(states state) -{ - if (state == currentState) - return; - - if (state == INITIAL) { - fill_device_list(DC_TYPE_OTHER); - ui.progressBar->hide(); - markChildrenAsEnabled(); - timer->stop(); - } - - // tries to cancel an on going download - else if (currentState == DOWNLOADING && state == CANCELLING) { - import_thread_cancelled = true; - ui.downloadCancelRetryButton->setEnabled(false); - } - - // user pressed cancel but the application isn't doing anything. - // means close the window - else if ((currentState == INITIAL || currentState == DONE || currentState == ERROR) && state == CANCELLING) { - timer->stop(); - reject(); - } - - // the cancelation process is finished - else if (currentState == CANCELLING && state == DONE) { - timer->stop(); - ui.progressBar->setValue(0); - ui.progressBar->hide(); - markChildrenAsEnabled(); - } - - // DOWNLOAD is finally done, but we don't know if there was an error as libdivecomputer doesn't pass - // that information on to us. - // If we find an error, offer to retry, otherwise continue the interaction to pick the dives the user wants - else if (currentState == DOWNLOADING && state == DONE) { - timer->stop(); - if (QString(progress_bar_text).contains("error", Qt::CaseInsensitive)) { - updateProgressBar(); - markChildrenAsEnabled(); - progress_bar_text = ""; - } else { - ui.progressBar->setValue(100); - markChildrenAsEnabled(); - } - } - - // DOWNLOAD is started. - else if (state == DOWNLOADING) { - timer->start(); - ui.progressBar->setValue(0); - updateProgressBar(); - ui.progressBar->show(); - markChildrenAsDisabled(); - } - - // got an error - else if (state == ERROR) { - QMessageBox::critical(this, TITLE_OR_TEXT(tr("Error"), this->thread->error), QMessageBox::Ok); - - markChildrenAsEnabled(); - ui.progressBar->hide(); - } - - // properly updating the widget state - currentState = state; -} - -void DownloadFromDCWidget::on_vendor_currentIndexChanged(const QString &vendor) -{ - int dcType = DC_TYPE_SERIAL; - QAbstractItemModel *currentModel = ui.product->model(); - if (!currentModel) - return; - - productModel = new QStringListModel(productList[vendor]); - ui.product->setModel(productModel); - - if (vendor == QString("Uemis")) - dcType = DC_TYPE_UEMIS; - fill_device_list(dcType); - - // Memleak - but deleting gives me a crash. - //currentModel->deleteLater(); -} - -void DownloadFromDCWidget::on_product_currentIndexChanged(const QString &product) -{ - // Set up the DC descriptor - dc_descriptor_t *descriptor = NULL; - descriptor = descriptorLookup[ui.vendor->currentText() + product]; - - // call dc_descriptor_get_transport to see if the dc_transport_t is DC_TRANSPORT_SERIAL - if (dc_descriptor_get_transport(descriptor) == DC_TRANSPORT_SERIAL) { - // if the dc_transport_t is DC_TRANSPORT_SERIAL, then enable the device node box. - ui.device->setEnabled(true); - } else { - // otherwise disable the device node box - ui.device->setEnabled(false); - } -} - -void DownloadFromDCWidget::fill_computer_list() -{ - dc_iterator_t *iterator = NULL; - dc_descriptor_t *descriptor = NULL; - struct mydescriptor *mydescriptor; - - QStringList computer; - dc_descriptor_iterator(&iterator); - while (dc_iterator_next(iterator, &descriptor) == DC_STATUS_SUCCESS) { - const char *vendor = dc_descriptor_get_vendor(descriptor); - const char *product = dc_descriptor_get_product(descriptor); - - if (!vendorList.contains(vendor)) - vendorList.append(vendor); - - if (!productList[vendor].contains(product)) - productList[vendor].push_back(product); - - descriptorLookup[QString(vendor) + QString(product)] = descriptor; - } - dc_iterator_free(iterator); - Q_FOREACH (QString vendor, vendorList) - qSort(productList[vendor]); - - /* and add the Uemis Zurich which we are handling internally - THIS IS A HACK as we magically have a data structure here that - happens to match a data structure that is internal to libdivecomputer; - this WILL BREAK if libdivecomputer changes the dc_descriptor struct... - eventually the UEMIS code needs to move into libdivecomputer, I guess */ - - mydescriptor = (struct mydescriptor *)malloc(sizeof(struct mydescriptor)); - mydescriptor->vendor = "Uemis"; - mydescriptor->product = "Zurich"; - mydescriptor->type = DC_FAMILY_NULL; - mydescriptor->model = 0; - - if (!vendorList.contains("Uemis")) - vendorList.append("Uemis"); - - if (!productList["Uemis"].contains("Zurich")) - productList["Uemis"].push_back("Zurich"); - - descriptorLookup["UemisZurich"] = (dc_descriptor_t *)mydescriptor; - - qSort(vendorList); -} - -void DownloadFromDCWidget::on_search_clicked() -{ - if (ui.vendor->currentText() == "Uemis") { - QString dirName = QFileDialog::getExistingDirectory(this, - tr("Find Uemis dive computer"), - QDir::homePath(), - QFileDialog::ShowDirsOnly); - if (ui.device->findText(dirName) == -1) - ui.device->addItem(dirName); - ui.device->setEditText(dirName); - } -} - -void DownloadFromDCWidget::on_downloadCancelRetryButton_clicked() -{ - if (currentState == DOWNLOADING) { - updateState(CANCELLING); - return; - } - if (currentState == DONE) { - // this means we are retrying - so we better clean out the partial - // list of downloaded dives from the last attempt - diveImportedModel->clearTable(); - clear_table(&downloadTable); - } - updateState(DOWNLOADING); - - // you cannot cancel the dialog, just the download - ui.cancel->setEnabled(false); - ui.downloadCancelRetryButton->setText("Cancel download"); - - // I don't really think that create/destroy the thread - // is really necessary. - if (thread) { - thread->deleteLater(); - } - - data.vendor = strdup(ui.vendor->currentText().toUtf8().data()); - data.product = strdup(ui.product->currentText().toUtf8().data()); -#if defined(BT_SUPPORT) - data.bluetooth_mode = ui.bluetoothMode->isChecked(); - if (data.bluetooth_mode && btDeviceSelectionDialog != NULL) { - // Get the selected device address - data.devname = strdup(btDeviceSelectionDialog->getSelectedDeviceAddress().toUtf8().data()); - } else - // this breaks an "else if" across lines... not happy... -#endif - if (same_string(data.vendor, "Uemis")) { - char *colon; - char *devname = strdup(ui.device->currentText().toUtf8().data()); - - if ((colon = strstr(devname, ":\\ (UEMISSDA)")) != NULL) { - *(colon + 2) = '\0'; - fprintf(stderr, "shortened devname to \"%s\"", data.devname); - } - data.devname = devname; - } else { - data.devname = strdup(ui.device->currentText().toUtf8().data()); - } - data.descriptor = descriptorLookup[ui.vendor->currentText() + ui.product->currentText()]; - data.force_download = ui.forceDownload->isChecked(); - data.create_new_trip = ui.createNewTrip->isChecked(); - data.trip = NULL; - data.deviceid = data.diveid = 0; - set_default_dive_computer(data.vendor, data.product); - set_default_dive_computer_device(data.devname); -#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) - set_default_dive_computer_download_mode(ui.bluetoothMode->isChecked() ? DC_TRANSPORT_BLUETOOTH : DC_TRANSPORT_SERIAL); -#endif - thread = new DownloadThread(this, &data); - - connect(thread, SIGNAL(finished()), - this, SLOT(onDownloadThreadFinished()), Qt::QueuedConnection); - - MainWindow *w = MainWindow::instance(); - connect(thread, SIGNAL(finished()), w, SLOT(refreshDisplay())); - - // before we start, remember where the dive_table ended - previousLast = dive_table.nr; - - thread->start(); - - QString product(ui.product->currentText()); - if (product == "OSTC 3" || product == "OSTC Sport") - ostcFirmwareCheck = new OstcFirmwareCheck(product); -} - -bool DownloadFromDCWidget::preferDownloaded() -{ - return ui.preferDownloaded->isChecked(); -} - -void DownloadFromDCWidget::checkLogFile(int state) -{ - ui.chooseLogFile->setEnabled(state == Qt::Checked); - data.libdc_log = (state == Qt::Checked); - if (state == Qt::Checked && logFile.isEmpty()) { - pickLogFile(); - } -} - -void DownloadFromDCWidget::pickLogFile() -{ - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath().append(QDir::separator()).append("subsurface.log"); - logFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer download logfile"), - filename, tr("Log files (*.log)")); - if (!logFile.isEmpty()) { - free(logfile_name); - logfile_name = strdup(logFile.toUtf8().data()); - } -} - -void DownloadFromDCWidget::checkDumpFile(int state) -{ - ui.chooseDumpFile->setEnabled(state == Qt::Checked); - data.libdc_dump = (state == Qt::Checked); - if (state == Qt::Checked) { - if (dumpFile.isEmpty()) - pickDumpFile(); - if (!dumpWarningShown) { - QMessageBox::warning(this, tr("Warning"), - tr("Saving the libdivecomputer dump will NOT download dives to the dive list.")); - dumpWarningShown = true; - } - } -} - -void DownloadFromDCWidget::pickDumpFile() -{ - QString filename = existing_filename ?: prefs.default_filename; - QFileInfo fi(filename); - filename = fi.absolutePath().append(QDir::separator()).append("subsurface.bin"); - dumpFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer binary dump file"), - filename, tr("Dump files (*.bin)")); - if (!dumpFile.isEmpty()) { - free(dumpfile_name); - dumpfile_name = strdup(dumpFile.toUtf8().data()); - } -} - -void DownloadFromDCWidget::reject() -{ - // we don't want the download window being able to close - // while we're still downloading. - if (currentState != DOWNLOADING && currentState != CANCELLING) - QDialog::reject(); -} - -void DownloadFromDCWidget::onDownloadThreadFinished() -{ - if (currentState == DOWNLOADING) { - if (thread->error.isEmpty()) - updateState(DONE); - else - updateState(ERROR); - } else if (currentState == CANCELLING) { - updateState(DONE); - } - ui.downloadCancelRetryButton->setText(tr("Retry")); - ui.downloadCancelRetryButton->setEnabled(true); - // regardless, if we got dives, we should show them to the user - if (downloadTable.nr) { - diveImportedModel->setImportedDivesIndexes(0, downloadTable.nr - 1); - } - -} - -void DownloadFromDCWidget::on_cancel_clicked() -{ - if (currentState == DOWNLOADING || currentState == CANCELLING) - return; - - // now discard all the dives - clear_table(&downloadTable); - done(-1); -} - -void DownloadFromDCWidget::on_ok_clicked() -{ - struct dive *dive; - - if (currentState != DONE && currentState != ERROR) - return; - - // record all the dives in the 'real' dive_table - for (int i = 0; i < downloadTable.nr; i++) { - if (diveImportedModel->data(diveImportedModel->index(i, 0),Qt::CheckStateRole) == Qt::Checked) - record_dive(downloadTable.dives[i]); - downloadTable.dives[i] = NULL; - } - downloadTable.nr = 0; - - int uniqId, idx; - // remember the last downloaded dive (on most dive computers this will be the chronologically - // first new dive) and select it again after processing all the dives - MainWindow::instance()->dive_list()->unselectDives(); - - dive = get_dive(dive_table.nr - 1); - if (dive != NULL) { - uniqId = get_dive(dive_table.nr - 1)->id; - process_dives(true, preferDownloaded()); - // after process_dives does any merging or resorting needed, we need - // to recreate the model for the dive list so we can select the newest dive - MainWindow::instance()->recreateDiveList(); - idx = get_idx_by_uniq_id(uniqId); - // this shouldn't be necessary - but there are reports that somehow existing dives stay selected - // (but not visible as selected) - MainWindow::instance()->dive_list()->unselectDives(); - MainWindow::instance()->dive_list()->selectDive(idx, true); - } - - if (ostcFirmwareCheck && currentState == DONE) - ostcFirmwareCheck->checkLatest(this, &data); - accept(); -} - -void DownloadFromDCWidget::markChildrenAsDisabled() -{ - ui.device->setEnabled(false); - ui.vendor->setEnabled(false); - ui.product->setEnabled(false); - ui.forceDownload->setEnabled(false); - ui.createNewTrip->setEnabled(false); - ui.preferDownloaded->setEnabled(false); - ui.ok->setEnabled(false); - ui.search->setEnabled(false); - ui.logToFile->setEnabled(false); - ui.dumpToFile->setEnabled(false); - ui.chooseLogFile->setEnabled(false); - ui.chooseDumpFile->setEnabled(false); - ui.selectAllButton->setEnabled(false); - ui.unselectAllButton->setEnabled(false); - ui.bluetoothMode->setEnabled(false); - ui.chooseBluetoothDevice->setEnabled(false); -} - -void DownloadFromDCWidget::markChildrenAsEnabled() -{ - ui.device->setEnabled(true); - ui.vendor->setEnabled(true); - ui.product->setEnabled(true); - ui.forceDownload->setEnabled(true); - ui.createNewTrip->setEnabled(true); - ui.preferDownloaded->setEnabled(true); - ui.ok->setEnabled(true); - ui.cancel->setEnabled(true); - ui.search->setEnabled(true); - ui.logToFile->setEnabled(true); - ui.dumpToFile->setEnabled(true); - ui.chooseLogFile->setEnabled(true); - ui.chooseDumpFile->setEnabled(true); - ui.selectAllButton->setEnabled(true); - ui.unselectAllButton->setEnabled(true); -#if defined(BT_SUPPORT) - ui.bluetoothMode->setEnabled(true); - ui.chooseBluetoothDevice->setEnabled(true); -#endif -} - -#if defined(BT_SUPPORT) -void DownloadFromDCWidget::selectRemoteBluetoothDevice() -{ - if (!btDeviceSelectionDialog) { - btDeviceSelectionDialog = new BtDeviceSelectionDialog(this); - connect(btDeviceSelectionDialog, SIGNAL(finished(int)), - this, SLOT(bluetoothSelectionDialogIsFinished(int))); - } - - btDeviceSelectionDialog->show(); -} - -void DownloadFromDCWidget::bluetoothSelectionDialogIsFinished(int result) -{ - if (result == QDialog::Accepted) { - /* Make the selected Bluetooth device default */ - QString selectedDeviceName = btDeviceSelectionDialog->getSelectedDeviceName(); - - if (selectedDeviceName == NULL || selectedDeviceName.isEmpty()) { - ui.device->setCurrentText(btDeviceSelectionDialog->getSelectedDeviceAddress()); - } else { - ui.device->setCurrentText(selectedDeviceName); - } - } else if (result == QDialog::Rejected){ - /* Disable Bluetooth download mode */ - ui.bluetoothMode->setChecked(false); - } -} - -void DownloadFromDCWidget::enableBluetoothMode(int state) -{ - ui.chooseBluetoothDevice->setEnabled(state == Qt::Checked); - - if (state == Qt::Checked) - selectRemoteBluetoothDevice(); - else - ui.device->setCurrentIndex(-1); -} -#endif - -static void fillDeviceList(const char *name, void *data) -{ - QComboBox *comboBox = (QComboBox *)data; - comboBox->addItem(name); -} - -void DownloadFromDCWidget::fill_device_list(int dc_type) -{ - int deviceIndex; - ui.device->clear(); - deviceIndex = enumerate_devices(fillDeviceList, ui.device, dc_type); - if (deviceIndex >= 0) - ui.device->setCurrentIndex(deviceIndex); -} - -DownloadThread::DownloadThread(QObject *parent, device_data_t *data) : QThread(parent), - data(data) -{ -} - -static QString str_error(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - const QString str = QString().vsprintf(fmt, args); - va_end(args); - - return str; -} - -void DownloadThread::run() -{ - const char *errorText; - import_thread_cancelled = false; - data->download_table = &downloadTable; - if (!strcmp(data->vendor, "Uemis")) - errorText = do_uemis_import(data); - else - errorText = do_libdivecomputer_import(data); - if (errorText) - error = str_error(errorText, data->devname, data->vendor, data->product); -} - -DiveImportedModel::DiveImportedModel(QObject *o) : QAbstractTableModel(o), - firstIndex(0), - lastIndex(-1), - checkStates(0) -{ -} - -int DiveImportedModel::columnCount(const QModelIndex &model) const -{ - return 3; -} - -int DiveImportedModel::rowCount(const QModelIndex &model) const -{ - return lastIndex - firstIndex + 1; -} - -QVariant DiveImportedModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (orientation == Qt::Vertical) - return QVariant(); - if (role == Qt::DisplayRole) { - switch (section) { - case 0: - return QVariant(tr("Date/time")); - case 1: - return QVariant(tr("Duration")); - case 2: - return QVariant(tr("Depth")); - } - } - return QVariant(); -} - -QVariant DiveImportedModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - if (index.row() + firstIndex > lastIndex) - return QVariant(); - - struct dive *d = get_dive_from_table(index.row() + firstIndex, &downloadTable); - if (!d) - return QVariant(); - if (role == Qt::DisplayRole) { - switch (index.column()) { - case 0: - return QVariant(get_short_dive_date_string(d->when)); - case 1: - return QVariant(get_dive_duration_string(d->duration.seconds, tr("h:"), tr("min"))); - case 2: - return QVariant(get_depth_string(d->maxdepth.mm, true, false)); - } - } - if (role == Qt::CheckStateRole) { - if (index.column() == 0) - return checkStates[index.row()] ? Qt::Checked : Qt::Unchecked; - } - return QVariant(); -} - -void DiveImportedModel::changeSelected(QModelIndex clickedIndex) -{ - checkStates[clickedIndex.row()] = !checkStates[clickedIndex.row()]; - dataChanged(index(clickedIndex.row(), 0), index(clickedIndex.row(), 0), QVector() << Qt::CheckStateRole); -} - -void DiveImportedModel::selectAll() -{ - memset(checkStates, true, lastIndex - firstIndex + 1); - dataChanged(index(0, 0), index(lastIndex - firstIndex, 0), QVector() << Qt::CheckStateRole); -} - -void DiveImportedModel::selectNone() -{ - memset(checkStates, false, lastIndex - firstIndex + 1); - dataChanged(index(0, 0), index(lastIndex - firstIndex,0 ), QVector() << Qt::CheckStateRole); -} - -Qt::ItemFlags DiveImportedModel::flags(const QModelIndex &index) const -{ - if (index.column() != 0) - return QAbstractTableModel::flags(index); - return QAbstractTableModel::flags(index) | Qt::ItemIsUserCheckable; -} - -void DiveImportedModel::clearTable() -{ - beginRemoveRows(QModelIndex(), 0, lastIndex - firstIndex); - lastIndex = -1; - firstIndex = 0; - endRemoveRows(); -} - -void DiveImportedModel::setImportedDivesIndexes(int first, int last) -{ - Q_ASSERT(last >= first); - beginRemoveRows(QModelIndex(), 0, lastIndex - firstIndex); - endRemoveRows(); - beginInsertRows(QModelIndex(), 0, last - first); - lastIndex = last; - firstIndex = first; - delete[] checkStates; - checkStates = new bool[last - first + 1]; - memset(checkStates, true, last - first + 1); - endInsertRows(); -} diff --git a/qt-ui/downloadfromdivecomputer.h b/qt-ui/downloadfromdivecomputer.h deleted file mode 100644 index 7acd49e95..000000000 --- a/qt-ui/downloadfromdivecomputer.h +++ /dev/null @@ -1,125 +0,0 @@ -#ifndef DOWNLOADFROMDIVECOMPUTER_H -#define DOWNLOADFROMDIVECOMPUTER_H - -#include -#include -#include -#include -#include - -#include "libdivecomputer.h" -#include "configuredivecomputerdialog.h" -#include "ui_downloadfromdivecomputer.h" - -#if defined(BT_SUPPORT) -#include "btdeviceselectiondialog.h" -#endif - -class QStringListModel; - -class DownloadThread : public QThread { - Q_OBJECT -public: - DownloadThread(QObject *parent, device_data_t *data); - virtual void run(); - - QString error; - -private: - device_data_t *data; -}; - -class DiveImportedModel : public QAbstractTableModel -{ - Q_OBJECT -public: - DiveImportedModel(QObject *o); - int columnCount(const QModelIndex& index = QModelIndex()) const; - int rowCount(const QModelIndex& index = QModelIndex()) const; - QVariant data(const QModelIndex& index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role) const; - void setImportedDivesIndexes(int first, int last); - Qt::ItemFlags flags(const QModelIndex &index) const; - void clearTable(); - -public -slots: - void changeSelected(QModelIndex clickedIndex); - void selectAll(); - void selectNone(); - -private: - int firstIndex; - int lastIndex; - bool *checkStates; -}; - -class DownloadFromDCWidget : public QDialog { - Q_OBJECT -public: - explicit DownloadFromDCWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); - void reject(); - - enum states { - INITIAL, - DOWNLOADING, - CANCELLING, - ERROR, - DONE, - }; - -public -slots: - void on_downloadCancelRetryButton_clicked(); - void on_ok_clicked(); - void on_cancel_clicked(); - void on_search_clicked(); - void on_vendor_currentIndexChanged(const QString &vendor); - void on_product_currentIndexChanged(const QString &product); - - void onDownloadThreadFinished(); - void updateProgressBar(); - void checkLogFile(int state); - void checkDumpFile(int state); - void pickDumpFile(); - void pickLogFile(); -#if defined(BT_SUPPORT) - void enableBluetoothMode(int state); - void selectRemoteBluetoothDevice(); - void bluetoothSelectionDialogIsFinished(int result); -#endif -private: - void markChildrenAsDisabled(); - void markChildrenAsEnabled(); - - Ui::DownloadFromDiveComputer ui; - DownloadThread *thread; - bool downloading; - - QStringList vendorList; - QHash productList; - QMap descriptorLookup; - device_data_t data; - int previousLast; - - QStringListModel *vendorModel; - QStringListModel *productModel; - void fill_computer_list(); - void fill_device_list(int dc_type); - QString logFile; - QString dumpFile; - QTimer *timer; - bool dumpWarningShown; - OstcFirmwareCheck *ostcFirmwareCheck; - DiveImportedModel *diveImportedModel; -#if defined(BT_SUPPORT) - BtDeviceSelectionDialog *btDeviceSelectionDialog; -#endif - -public: - bool preferDownloaded(); - void updateState(states state); - states currentState; -}; - -#endif // DOWNLOADFROMDIVECOMPUTER_H diff --git a/qt-ui/downloadfromdivecomputer.ui b/qt-ui/downloadfromdivecomputer.ui deleted file mode 100644 index b1f152034..000000000 --- a/qt-ui/downloadfromdivecomputer.ui +++ /dev/null @@ -1,301 +0,0 @@ - - - DownloadFromDiveComputer - - - - 0 - 0 - 747 - 535 - - - - Download from dive computer - - - - :/subsurface-icon - - - - - 5 - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - - - - 0 - - - 5 - - - - - Device or mount point - - - - - - - true - - - - - - - ... - - - - - - - Force download of all dives - - - - - - - Always prefer downloaded dives - - - - - - - Download into new trip - - - - - - - Save libdivecomputer logfile - - - - - - - ... - - - - - - - Save libdivecomputer dumpfile - - - - - - - ... - - - - - - - Choose Bluetooth download mode - - - - - - - Select a remote Bluetooth device. - - - ... - - - - - - - Vendor - - - - - - - - - - Dive computer - - - - - - - - - - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Download - - - - - - - - - 0 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - 0 - - - - - 5 - - - - - - 1 - 0 - - - - Downloaded dives - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - Select all - - - - - - - Unselect all - - - - - - - - - - 1 - 0 - - - - - - - - - - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - OK - - - - - - - Cancel - - - - - - - - - - diff --git a/qt-ui/filterwidget.ui b/qt-ui/filterwidget.ui deleted file mode 100644 index 1450d81b2..000000000 --- a/qt-ui/filterwidget.ui +++ /dev/null @@ -1,140 +0,0 @@ - - - FilterWidget2 - - - - 0 - 0 - 594 - 362 - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Reset filters - - - - :/filter-reset:/filter-reset - - - true - - - - - - - Show/hide filters - - - - :/filter-hide:/filter-hide - - - true - - - - - - - Close and reset filters - - - - :/filter-close:/filter-close - - - true - - - - - - - - - QFrame::NoFrame - - - true - - - - - 0 - 0 - 594 - 337 - - - - - - - - - - - - diff --git a/qt-ui/globe.cpp b/qt-ui/globe.cpp deleted file mode 100644 index 135f195a1..000000000 --- a/qt-ui/globe.cpp +++ /dev/null @@ -1,431 +0,0 @@ -#include "globe.h" -#ifndef NO_MARBLE -#include "mainwindow.h" -#include "helpers.h" -#include "divelistview.h" -#include "maintab.h" -#include "display.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef MARBLE_SUBSURFACE_BRANCH -#include -#endif - -GlobeGPS *GlobeGPS::instance() -{ - static GlobeGPS *self = new GlobeGPS(); - return self; -} - -GlobeGPS::GlobeGPS(QWidget *parent) : MarbleWidget(parent), - loadedDives(0), - messageWidget(new KMessageWidget(this)), - fixZoomTimer(new QTimer(this)), - needResetZoom(false), - editingDiveLocation(false), - doubleClick(false) -{ -#ifdef MARBLE_SUBSURFACE_BRANCH - // we need to make sure this gets called after the command line arguments have - // been processed but before we initialize the rest of Marble - Marble::MarbleDebug::setEnabled(verbose); -#endif - currentZoomLevel = -1; - // check if Google Sat Maps are installed - // if not, check if they are in a known location - MapThemeManager mtm; - QStringList list = mtm.mapThemeIds(); - QString subsurfaceDataPath; - QDir marble; - if (!list.contains("earth/googlesat/googlesat.dgml")) { - subsurfaceDataPath = getSubsurfaceDataPath("marbledata"); - if (subsurfaceDataPath.size()) { - MarbleDirs::setMarbleDataPath(subsurfaceDataPath); - } else { - subsurfaceDataPath = getSubsurfaceDataPath("data"); - if (subsurfaceDataPath.size()) - MarbleDirs::setMarbleDataPath(subsurfaceDataPath); - } - } - messageWidget->setCloseButtonVisible(false); - messageWidget->setHidden(true); - - setMapThemeId("earth/googlesat/googlesat.dgml"); - //setMapThemeId("earth/openstreetmap/openstreetmap.dgml"); - setProjection(Marble::Spherical); - - setAnimationsEnabled(true); - Q_FOREACH (AbstractFloatItem *i, floatItems()) { - i->setVisible(false); - } - - setShowClouds(false); - setShowBorders(false); - setShowPlaces(true); - setShowCrosshairs(false); - setShowGrid(false); - setShowOverviewMap(false); - setShowScaleBar(true); - setShowCompass(false); - connect(this, SIGNAL(mouseClickGeoPosition(qreal, qreal, GeoDataCoordinates::Unit)), - this, SLOT(mouseClicked(qreal, qreal, GeoDataCoordinates::Unit))); - - setMinimumHeight(0); - setMinimumWidth(0); - connect(fixZoomTimer, SIGNAL(timeout()), this, SLOT(fixZoom())); - fixZoomTimer->setSingleShot(true); - installEventFilter(this); -} - -bool GlobeGPS::eventFilter(QObject *obj, QEvent *ev) -{ - // sometimes Marble seems not to notice double clicks and consequently not call - // the right callback - so let's remember here if the last 'click' is a 'double' or not - enum QEvent::Type type = ev->type(); - if (type == QEvent::MouseButtonDblClick) - doubleClick = true; - else if (type == QEvent::MouseButtonPress) - doubleClick = false; - - // This disables Zooming when a double click occours on the scene. - if (type == QEvent::MouseButtonDblClick && !editingDiveLocation) - return true; - // This disables the Marble's Context Menu - // we need to move this to our 'contextMenuEvent' - // if we plan to do a different one in the future. - if (type == QEvent::ContextMenu) { - contextMenuEvent(static_cast(ev)); - return true; - } - if (type == QEvent::MouseButtonPress) { - QMouseEvent *e = static_cast(ev); - if (e->button() == Qt::RightButton) - return true; - } - return QObject::eventFilter(obj, ev); -} - -void GlobeGPS::contextMenuEvent(QContextMenuEvent *ev) -{ - QMenu m; - QAction *a = m.addAction(tr("Edit selected dive locations"), this, SLOT(prepareForGetDiveCoordinates())); - a->setData(QVariant::fromValue(&m)); - a->setEnabled(current_dive); - m.exec(ev->globalPos()); -} - -void GlobeGPS::mouseClicked(qreal lon, qreal lat, GeoDataCoordinates::Unit unit) -{ - if (doubleClick) { - // strangely sometimes we don't get the changeDiveGeoPosition callback - // and end up here instead - changeDiveGeoPosition(lon, lat, unit); - return; - } - // don't mess with the selection while the user is editing a dive - if (MainWindow::instance()->information()->isEditing() || messageWidget->isVisible()) - return; - - GeoDataCoordinates here(lon, lat, unit); - long lon_udeg = rint(1000000 * here.longitude(GeoDataCoordinates::Degree)); - long lat_udeg = rint(1000000 * here.latitude(GeoDataCoordinates::Degree)); - - // distance() is in km above the map. - // We're going to use that to decide how - // approximate the dives have to be. - // - // Totally arbitrarily I say that 1km - // distance means that we can resolve - // to about 100m. Which in turn is about - // 1000 udeg. - // - // Trigonometry is hard, but sin x == x - // for small x, so let's just do this as - // a linear thing. - long resolve = rint(distance() * 1000); - - int idx; - struct dive *dive; - bool clear = !(QApplication::keyboardModifiers() & Qt::ControlModifier); - QList selectedDiveIds; - for_each_dive (idx, dive) { - long lat_diff, lon_diff; - struct dive_site *ds = get_dive_site_for_dive(dive); - if (!dive_site_has_gps_location(ds)) - continue; - lat_diff = labs(ds->latitude.udeg - lat_udeg); - lon_diff = labs(ds->longitude.udeg - lon_udeg); - if (lat_diff > 180000000) - lat_diff = 360000000 - lat_diff; - if (lon_diff > 180000000) - lon_diff = 180000000 - lon_diff; - if (lat_diff > resolve || lon_diff > resolve) - continue; - - selectedDiveIds.push_back(idx); - } - if (selectedDiveIds.empty()) - return; - if (clear) - MainWindow::instance()->dive_list()->unselectDives(); - MainWindow::instance()->dive_list()->selectDives(selectedDiveIds); -} - -void GlobeGPS::repopulateLabels() -{ - static GeoDataStyle otherSite, currentSite; - static GeoDataIconStyle darkFlag(QImage(":flagDark")), lightFlag(QImage(":flagLight")); - struct dive_site *ds; - int idx; - QMap locationMap; - if (loadedDives) { - model()->treeModel()->removeDocument(loadedDives); - delete loadedDives; - } - loadedDives = new GeoDataDocument; - otherSite.setIconStyle(darkFlag); - currentSite.setIconStyle(lightFlag); - - if (displayed_dive_site.uuid && dive_site_has_gps_location(&displayed_dive_site)) { - GeoDataPlacemark *place = new GeoDataPlacemark(displayed_dive_site.name); - place->setStyle(¤tSite); - place->setCoordinate(displayed_dive_site.longitude.udeg / 1000000.0, - displayed_dive_site.latitude.udeg / 1000000.0, 0, GeoDataCoordinates::Degree); - locationMap[QString(displayed_dive_site.name)] = place; - loadedDives->append(place); - } - for_each_dive_site(idx, ds) { - if (ds->uuid == displayed_dive_site.uuid) - continue; - if (dive_site_has_gps_location(ds)) { - GeoDataPlacemark *place = new GeoDataPlacemark(ds->name); - place->setStyle(&otherSite); - place->setCoordinate(ds->longitude.udeg / 1000000.0, ds->latitude.udeg / 1000000.0, 0, GeoDataCoordinates::Degree); - - // don't add dive locations twice, unless they are at least 50m apart - if (locationMap[QString(ds->name)]) { - GeoDataCoordinates existingLocation = locationMap[QString(ds->name)]->coordinate(); - GeoDataLineString segment = GeoDataLineString(); - segment.append(existingLocation); - GeoDataCoordinates newLocation = place->coordinate(); - segment.append(newLocation); - double dist = segment.length(6371); - // the dist is scaled to the radius given - so with 6371km as radius - // 50m turns into 0.05 as threashold - if (dist < 0.05) - continue; - } - locationMap[QString(ds->name)] = place; - loadedDives->append(place); - } - } - model()->treeModel()->addDocument(loadedDives); - - struct dive_site *center = displayed_dive_site.uuid != 0 ? - &displayed_dive_site : current_dive ? - get_dive_site_by_uuid(current_dive->dive_site_uuid) : NULL; - if(dive_site_has_gps_location(&displayed_dive_site) && center) - centerOn(displayed_dive_site.longitude.udeg / 1000000.0, displayed_dive_site.latitude.udeg / 1000000.0, true); -} - -void GlobeGPS::reload() -{ - editingDiveLocation = false; - messageWidget->hide(); - repopulateLabels(); -} - -void GlobeGPS::centerOnDiveSite(struct dive_site *ds) -{ - if (!dive_site_has_gps_location(ds)) { - // this is not intuitive and at times causes trouble - let's comment it out for now - // zoomOutForNoGPS(); - return; - } - qreal longitude = ds->longitude.udeg / 1000000.0; - qreal latitude = ds->latitude.udeg / 1000000.0; - - if(IS_FP_SAME(longitude, centerLongitude()) && IS_FP_SAME(latitude,centerLatitude())) { - return; - } - - // if no zoom is set up, set the zoom as seen from 3km above - // if we come back from a dive without GPS data, reset to the last zoom value - // otherwise check to make sure we aren't still running an animation and then remember - // the current zoom level - if (currentZoomLevel == -1) { - currentZoomLevel = zoomFromDistance(3.0); - centerOn(longitude, latitude); - fixZoom(true); - return; - } - if (!fixZoomTimer->isActive()) { - if (needResetZoom) { - needResetZoom = false; - fixZoom(); - } else if (zoom() >= 1200) { - currentZoomLevel = zoom(); - } - } - // From the marble source code, the maximum time of - // 'spin and fit' is 2000 miliseconds so wait a bit them zoom again. - fixZoomTimer->stop(); - if (zoom() < 1200 && IS_FP_SAME(centerLatitude(), latitude) && IS_FP_SAME(centerLongitude(), longitude)) { - // create a tiny movement - centerOn(longitude + 0.00001, latitude + 0.00001); - fixZoomTimer->start(300); - } else { - fixZoomTimer->start(2100); - } - centerOn(longitude, latitude, true); -} - -void GlobeGPS::fixZoom(bool now) -{ - setZoom(currentZoomLevel, now ? Marble::Instant : Marble::Linear); -} - -void GlobeGPS::zoomOutForNoGPS() -{ - // this is called if the dive has no GPS location. - // zoom out quite a bit to show the globe and remember that the next time - // we show a dive with GPS location we need to zoom in again - if (!needResetZoom) { - needResetZoom = true; - if (!fixZoomTimer->isActive() && zoom() >= 1500) { - currentZoomLevel = zoom(); - } - } - if (fixZoomTimer->isActive()) - fixZoomTimer->stop(); - // 1000 is supposed to make sure you see the whole globe - setZoom(1000, Marble::Linear); -} - -void GlobeGPS::endGetDiveCoordinates() -{ - messageWidget->animatedHide(); - editingDiveLocation = false; -} - -void GlobeGPS::prepareForGetDiveCoordinates() -{ - messageWidget->setMessageType(KMessageWidget::Warning); - messageWidget->setText(QObject::tr("Move the map and double-click to set the dive location")); - messageWidget->setWordWrap(true); - messageWidget->setCloseButtonVisible(false); - messageWidget->animatedShow(); - editingDiveLocation = true; - // this is not intuitive and at times causes trouble - let's comment it out for now - // if (!dive_has_gps_location(current_dive)) - // zoomOutForNoGPS(); -} - -void GlobeGPS::changeDiveGeoPosition(qreal lon, qreal lat, GeoDataCoordinates::Unit unit) -{ - if (!editingDiveLocation) - return; - - // convert to degrees if in radian. - if (unit == GeoDataCoordinates::Radian) { - lon = lon * 180 / M_PI; - lat = lat * 180 / M_PI; - } - centerOn(lon, lat, true); - - // change the location of the displayed_dive and put the UI in edit mode - displayed_dive_site.latitude.udeg = lrint(lat * 1000000.0); - displayed_dive_site.longitude.udeg = lrint(lon * 1000000.0); - emit coordinatesChanged(); - repopulateLabels(); -} - -void GlobeGPS::mousePressEvent(QMouseEvent *event) -{ - if (event->type() != QEvent::MouseButtonDblClick) - return; - - qreal lat, lon; - bool clickOnGlobe = geoCoordinates(event->pos().x(), event->pos().y(), lon, lat, GeoDataCoordinates::Degree); - - // there could be two scenarios that got us here; let's check if we are editing a dive - if (MainWindow::instance()->information()->isEditing() && clickOnGlobe) { - // - // FIXME - // TODO - // - // this needs to do this on the dive site screen - // MainWindow::instance()->information()->updateCoordinatesText(lat, lon); - repopulateLabels(); - } else if (clickOnGlobe) { - changeDiveGeoPosition(lon, lat, GeoDataCoordinates::Degree); - } -} - -void GlobeGPS::resizeEvent(QResizeEvent *event) -{ - int size = event->size().width(); - MarbleWidget::resizeEvent(event); - if (size > 600) - messageWidget->setGeometry((size - 600) / 2, 5, 600, 0); - else - messageWidget->setGeometry(5, 5, size - 10, 0); - messageWidget->setMaximumHeight(500); -} - -void GlobeGPS::centerOnIndex(const QModelIndex& idx) -{ - struct dive_site *ds = get_dive_site_by_uuid(idx.model()->index(idx.row(), 0).data().toInt()); - if (!ds || !dive_site_has_gps_location(ds)) - centerOnDiveSite(&displayed_dive_site); - else - centerOnDiveSite(ds); -} -#else - -GlobeGPS *GlobeGPS::instance() -{ - static GlobeGPS *self = new GlobeGPS(); - return self; -} - -GlobeGPS::GlobeGPS(QWidget *parent) -{ - setText("MARBLE DISABLED AT BUILD TIME"); -} -void GlobeGPS::repopulateLabels() -{ -} -void GlobeGPS::centerOnCurrentDive() -{ -} -bool GlobeGPS::eventFilter(QObject *obj, QEvent *ev) -{ - return QObject::eventFilter(obj, ev); -} -void GlobeGPS::prepareForGetDiveCoordinates() -{ -} -void GlobeGPS::endGetDiveCoordinates() -{ -} -void GlobeGPS::reload() -{ -} -void GlobeGPS::centerOnIndex(const QModelIndex& idx) -{ -} -#endif diff --git a/qt-ui/globe.h b/qt-ui/globe.h deleted file mode 100644 index 8cc1265e4..000000000 --- a/qt-ui/globe.h +++ /dev/null @@ -1,84 +0,0 @@ -#ifndef GLOBE_H -#define GLOBE_H - -#include - -#ifndef NO_MARBLE -#include -#include - -#include - -namespace Marble{ - class GeoDataDocument; -} - -class KMessageWidget; -using namespace Marble; -struct dive; - -class GlobeGPS : public MarbleWidget { - Q_OBJECT -public: - using MarbleWidget::centerOn; - static GlobeGPS *instance(); - void reload(); - bool eventFilter(QObject *, QEvent *); - -protected: - /* reimp */ void resizeEvent(QResizeEvent *event); - /* reimp */ void mousePressEvent(QMouseEvent *event); - /* reimp */ void contextMenuEvent(QContextMenuEvent *); - -private: - GeoDataDocument *loadedDives; - KMessageWidget *messageWidget; - QTimer *fixZoomTimer; - int currentZoomLevel; - bool needResetZoom; - bool editingDiveLocation; - bool doubleClick; - GlobeGPS(QWidget *parent = 0); - -signals: - void coordinatesChanged(); - -public -slots: - void repopulateLabels(); - void changeDiveGeoPosition(qreal lon, qreal lat, GeoDataCoordinates::Unit); - void mouseClicked(qreal lon, qreal lat, GeoDataCoordinates::Unit); - void fixZoom(bool now = false); - void zoomOutForNoGPS(); - void prepareForGetDiveCoordinates(); - void endGetDiveCoordinates(); - void centerOnDiveSite(struct dive_site *ds); - void centerOnIndex(const QModelIndex& idx); -}; - -#else // NO_MARBLE -/* Dummy widget for when we don't have MarbleWidget */ -#include - -class GlobeGPS : public QLabel { - Q_OBJECT -public: - GlobeGPS(QWidget *parent = 0); - static GlobeGPS *instance(); - void reload(); - void repopulateLabels(); - void centerOnDiveSite(uint32_t uuid); - void centerOnIndex(const QModelIndex& idx); - void centerOnCurrentDive(); - bool eventFilter(QObject *, QEvent *); -public -slots: - void prepareForGetDiveCoordinates(); - void endGetDiveCoordinates(); -}; - -#endif // NO_MARBLE - -extern "C" double getDistance(int lat1, int lon1, int lat2, int lon2); - -#endif // GLOBE_H diff --git a/qt-ui/graphicsview-common.cpp b/qt-ui/graphicsview-common.cpp deleted file mode 100644 index a1813ce2d..000000000 --- a/qt-ui/graphicsview-common.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "graphicsview-common.h" - -QMap > profile_color; - -void fill_profile_color() -{ -#define COLOR(x, y, z) QVector() << x << y << z; - profile_color[SAC_1] = COLOR(FUNGREEN1, BLACK1_LOW_TRANS, FUNGREEN1); - profile_color[SAC_2] = COLOR(APPLE1, BLACK1_LOW_TRANS, APPLE1); - profile_color[SAC_3] = COLOR(ATLANTIS1, BLACK1_LOW_TRANS, ATLANTIS1); - profile_color[SAC_4] = COLOR(ATLANTIS2, BLACK1_LOW_TRANS, ATLANTIS2); - profile_color[SAC_5] = COLOR(EARLSGREEN1, BLACK1_LOW_TRANS, EARLSGREEN1); - profile_color[SAC_6] = COLOR(HOKEYPOKEY1, BLACK1_LOW_TRANS, HOKEYPOKEY1); - profile_color[SAC_7] = COLOR(TUSCANY1, BLACK1_LOW_TRANS, TUSCANY1); - profile_color[SAC_8] = COLOR(CINNABAR1, BLACK1_LOW_TRANS, CINNABAR1); - profile_color[SAC_9] = COLOR(REDORANGE1, BLACK1_LOW_TRANS, REDORANGE1); - - profile_color[VELO_STABLE] = COLOR(CAMARONE1, BLACK1_LOW_TRANS, CAMARONE1); - profile_color[VELO_SLOW] = COLOR(LIMENADE1, BLACK1_LOW_TRANS, LIMENADE1); - profile_color[VELO_MODERATE] = COLOR(RIOGRANDE1, BLACK1_LOW_TRANS, RIOGRANDE1); - profile_color[VELO_FAST] = COLOR(PIRATEGOLD1, BLACK1_LOW_TRANS, PIRATEGOLD1); - profile_color[VELO_CRAZY] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); - - profile_color[PO2] = COLOR(APPLE1, BLACK1_LOW_TRANS, APPLE1); - profile_color[PO2_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); - profile_color[PN2] = COLOR(BLACK1_LOW_TRANS, BLACK1_LOW_TRANS, BLACK1_LOW_TRANS); - profile_color[PN2_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); - profile_color[PHE] = COLOR(PEANUT, BLACK1_LOW_TRANS, PEANUT); - profile_color[PHE_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); - profile_color[O2SETPOINT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); - profile_color[CCRSENSOR1] = COLOR(TUNDORA1_MED_TRANS, BLACK1_LOW_TRANS, TUNDORA1_MED_TRANS); - profile_color[CCRSENSOR2] = COLOR(ROYALBLUE2_LOW_TRANS, BLACK1_LOW_TRANS, ROYALBLUE2_LOW_TRANS); - profile_color[CCRSENSOR3] = COLOR(PEANUT, BLACK1_LOW_TRANS, PEANUT); - profile_color[PP_LINES] = COLOR(BLACK1_HIGH_TRANS, BLACK1_LOW_TRANS, BLACK1_HIGH_TRANS); - - profile_color[TEXT_BACKGROUND] = COLOR(CONCRETE1_LOWER_TRANS, WHITE1, CONCRETE1_LOWER_TRANS); - profile_color[ALERT_BG] = COLOR(BROOM1_LOWER_TRANS, BLACK1_LOW_TRANS, BROOM1_LOWER_TRANS); - profile_color[ALERT_FG] = COLOR(BLACK1_LOW_TRANS, WHITE1, BLACK1_LOW_TRANS); - profile_color[EVENTS] = COLOR(REDORANGE1, BLACK1_LOW_TRANS, REDORANGE1); - profile_color[SAMPLE_DEEP] = COLOR(QColor(Qt::red).darker(), BLACK1, PERSIANRED1); - profile_color[SAMPLE_SHALLOW] = COLOR(QColor(Qt::red).lighter(), BLACK1_LOW_TRANS, PERSIANRED1); - profile_color[SMOOTHED] = COLOR(REDORANGE1_HIGH_TRANS, BLACK1_LOW_TRANS, REDORANGE1_HIGH_TRANS); - profile_color[MINUTE] = COLOR(MEDIUMREDVIOLET1_HIGHER_TRANS, BLACK1_LOW_TRANS, MEDIUMREDVIOLET1_HIGHER_TRANS); - profile_color[TIME_GRID] = COLOR(WHITE1, BLACK1_HIGH_TRANS, TUNDORA1_MED_TRANS); - profile_color[TIME_TEXT] = COLOR(FORESTGREEN1, BLACK1, FORESTGREEN1); - profile_color[DEPTH_GRID] = COLOR(WHITE1, BLACK1_HIGH_TRANS, TUNDORA1_MED_TRANS); - profile_color[MEAN_DEPTH] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); - profile_color[HR_PLOT] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); - profile_color[HR_TEXT] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); - profile_color[HR_AXIS] = COLOR(MED_GRAY_HIGH_TRANS, MED_GRAY_HIGH_TRANS, MED_GRAY_HIGH_TRANS); - profile_color[DEPTH_BOTTOM] = COLOR(GOVERNORBAY1_MED_TRANS, BLACK1_HIGH_TRANS, GOVERNORBAY1_MED_TRANS); - profile_color[DEPTH_TOP] = COLOR(MERCURY1_MED_TRANS, WHITE1_MED_TRANS, MERCURY1_MED_TRANS); - profile_color[TEMP_TEXT] = COLOR(GOVERNORBAY2, BLACK1_LOW_TRANS, GOVERNORBAY2); - profile_color[TEMP_PLOT] = COLOR(ROYALBLUE2_LOW_TRANS, BLACK1_LOW_TRANS, ROYALBLUE2_LOW_TRANS); - profile_color[SAC_DEFAULT] = COLOR(WHITE1, BLACK1_LOW_TRANS, FORESTGREEN1); - profile_color[BOUNDING_BOX] = COLOR(WHITE1, BLACK1_LOW_TRANS, TUNDORA1_MED_TRANS); - profile_color[PRESSURE_TEXT] = COLOR(KILLARNEY1, BLACK1_LOW_TRANS, KILLARNEY1); - profile_color[BACKGROUND] = COLOR(SPRINGWOOD1, WHITE1, SPRINGWOOD1); - profile_color[BACKGROUND_TRANS] = COLOR(SPRINGWOOD1_MED_TRANS, WHITE1_MED_TRANS, SPRINGWOOD1_MED_TRANS); - profile_color[CEILING_SHALLOW] = COLOR(REDORANGE1_HIGH_TRANS, BLACK1_HIGH_TRANS, REDORANGE1_HIGH_TRANS); - profile_color[CEILING_DEEP] = COLOR(RED1_MED_TRANS, BLACK1_HIGH_TRANS, RED1_MED_TRANS); - profile_color[CALC_CEILING_SHALLOW] = COLOR(FUNGREEN1_HIGH_TRANS, BLACK1_HIGH_TRANS, FUNGREEN1_HIGH_TRANS); - profile_color[CALC_CEILING_DEEP] = COLOR(APPLE1_HIGH_TRANS, BLACK1_HIGH_TRANS, APPLE1_HIGH_TRANS); - profile_color[TISSUE_PERCENTAGE] = COLOR(GOVERNORBAY2, BLACK1_LOW_TRANS, GOVERNORBAY2); - profile_color[GF_LINE] = COLOR(BLACK1, BLACK1_LOW_TRANS, BLACK1); - profile_color[AMB_PRESSURE_LINE] = COLOR(TUNDORA1_MED_TRANS, BLACK1_LOW_TRANS, ATLANTIS1); -#undef COLOR -} - -QColor getColor(const color_indice_t i, bool isGrayscale) -{ - if (profile_color.count() > i && i >= 0) - return profile_color[i].at((isGrayscale) ? 1 : 0); - return QColor(Qt::black); -} - -QColor getSacColor(int sac, int avg_sac) -{ - int sac_index = 0; - int delta = sac - avg_sac + 7000; - - sac_index = delta / 2000; - if (sac_index < 0) - sac_index = 0; - if (sac_index > SAC_COLORS - 1) - sac_index = SAC_COLORS - 1; - return getColor((color_indice_t)(SAC_COLORS_START_IDX + sac_index), false); -} diff --git a/qt-ui/graphicsview-common.h b/qt-ui/graphicsview-common.h deleted file mode 100644 index 2a757b2ae..000000000 --- a/qt-ui/graphicsview-common.h +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef GRAPHICSVIEW_COMMON_H -#define GRAPHICSVIEW_COMMON_H - -#include "subsurface-core/color.h" -#include -#include -#include - -#define SAC_COLORS_START_IDX SAC_1 -#define SAC_COLORS 9 -#define VELOCITY_COLORS_START_IDX VELO_STABLE -#define VELOCITY_COLORS 5 - -typedef enum { - /* SAC colors. Order is important, the SAC_COLORS_START_IDX define above. */ - SAC_1, - SAC_2, - SAC_3, - SAC_4, - SAC_5, - SAC_6, - SAC_7, - SAC_8, - SAC_9, - - /* Velocity colors. Order is still important, ref VELOCITY_COLORS_START_IDX. */ - VELO_STABLE, - VELO_SLOW, - VELO_MODERATE, - VELO_FAST, - VELO_CRAZY, - - /* gas colors */ - PO2, - PO2_ALERT, - PN2, - PN2_ALERT, - PHE, - PHE_ALERT, - O2SETPOINT, - CCRSENSOR1, - CCRSENSOR2, - CCRSENSOR3, - PP_LINES, - - /* Other colors */ - TEXT_BACKGROUND, - ALERT_BG, - ALERT_FG, - EVENTS, - SAMPLE_DEEP, - SAMPLE_SHALLOW, - SMOOTHED, - MINUTE, - TIME_GRID, - TIME_TEXT, - DEPTH_GRID, - MEAN_DEPTH, - HR_TEXT, - HR_PLOT, - HR_AXIS, - DEPTH_TOP, - DEPTH_BOTTOM, - TEMP_TEXT, - TEMP_PLOT, - SAC_DEFAULT, - BOUNDING_BOX, - PRESSURE_TEXT, - BACKGROUND, - BACKGROUND_TRANS, - CEILING_SHALLOW, - CEILING_DEEP, - CALC_CEILING_SHALLOW, - CALC_CEILING_DEEP, - TISSUE_PERCENTAGE, - GF_LINE, - AMB_PRESSURE_LINE -} color_indice_t; - - -/* profile_color[color indice] = COLOR(screen color, b/w printer color, color printer}} printer & screen colours could be different */ - -extern QMap > profile_color; -void fill_profile_color(); -QColor getColor(const color_indice_t i, bool isGrayscale = false); -QColor getSacColor(int sac, int diveSac); -struct text_render_options { - double size; - color_indice_t color; - double hpos, vpos; -}; - -typedef text_render_options text_render_options_t; -#endif // GRAPHICSVIEW_COMMON_H diff --git a/qt-ui/groupedlineedit.cpp b/qt-ui/groupedlineedit.cpp deleted file mode 100644 index 9ce5e175c..000000000 --- a/qt-ui/groupedlineedit.cpp +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2013 Maximilian Güntner - * - * This file is subject to the terms and conditions of version 2 of - * the GNU General Public License. See the file gpl-2.0.txt in the main - * directory of this archive for more details. - * - * Original License: - * - * This file is part of the Nepomuk widgets collection - * Copyright (c) 2013 Denis Steckelmacher - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License version 2.1 as published by the Free Software Foundation, - * or any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public License - * along with this library; see the file COPYING.LIB. If not, write to - * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. -*/ - -#include "groupedlineedit.h" - -#include -#include -#include -#include -#include -#include -#include - -struct GroupedLineEdit::Private { - struct Block { - int start; - int end; - QString text; - }; - QVector blocks; - QVector colors; -}; - -GroupedLineEdit::GroupedLineEdit(QWidget *parent) : QPlainTextEdit(parent), - d(new Private) -{ - setWordWrapMode(QTextOption::NoWrap); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - document()->setMaximumBlockCount(1); -} - -GroupedLineEdit::~GroupedLineEdit() -{ - delete d; -} - -QString GroupedLineEdit::text() const -{ - // Remove the block crosses from the text - return toPlainText(); -} - -int GroupedLineEdit::cursorPosition() const -{ - return textCursor().positionInBlock(); -} - -void GroupedLineEdit::addBlock(int start, int end) -{ - Private::Block block; - block.start = start; - block.end = end; - block.text = text().mid(start, end - start + 1).remove(',').trimmed(); - if (block.text.isEmpty()) - return; - d->blocks.append(block); - viewport()->update(); -} - -void GroupedLineEdit::addColor(QColor color) -{ - d->colors.append(color); -} - -void GroupedLineEdit::removeAllColors() -{ - d->colors.clear(); -} - -QStringList GroupedLineEdit::getBlockStringList() -{ - QStringList retList; - foreach (const Private::Block &block, d->blocks) - retList.append(block.text); - return retList; -} - -void GroupedLineEdit::setCursorPosition(int position) -{ - QTextCursor c = textCursor(); - c.setPosition(position, QTextCursor::MoveAnchor); - setTextCursor(c); -} - -void GroupedLineEdit::setText(const QString &text) -{ - setPlainText(text); -} - -void GroupedLineEdit::clear() -{ - QPlainTextEdit::clear(); - removeAllBlocks(); -} - -void GroupedLineEdit::selectAll() -{ - QTextCursor c = textCursor(); - c.select(QTextCursor::LineUnderCursor); - setTextCursor(c); -} - -void GroupedLineEdit::removeAllBlocks() -{ - d->blocks.clear(); - viewport()->update(); -} - -QSize GroupedLineEdit::sizeHint() const -{ - QSize rs( - 40, - document()->findBlock(0).layout()->lineAt(0).height() + - document()->documentMargin() * 2 + - frameWidth() * 2); - return rs; -} - -QSize GroupedLineEdit::minimumSizeHint() const -{ - return sizeHint(); -} - -void GroupedLineEdit::keyPressEvent(QKeyEvent *e) -{ - switch (e->key()) { - case Qt::Key_Return: - case Qt::Key_Enter: - emit editingFinished(); - return; - } - QPlainTextEdit::keyPressEvent(e); -} - -void GroupedLineEdit::paintEvent(QPaintEvent *e) -{ - QTextLine line = document()->findBlock(0).layout()->lineAt(0); - QPainter painter(viewport()); - - painter.setRenderHint(QPainter::Antialiasing, true); - painter.fillRect(0, 0, viewport()->width(), viewport()->height(), palette().base()); - - QVectorIterator i(d->colors); - i.toFront(); - foreach (const Private::Block &block, d->blocks) { - qreal start_x = line.cursorToX(block.start, QTextLine::Leading); - qreal end_x = line.cursorToX(block.end-1, QTextLine::Trailing); - - QPainterPath path; - QRectF rectangle( - start_x - 1.0 - double(horizontalScrollBar()->value()), - 1.0, - end_x - start_x + 2.0, - double(viewport()->height() - 2)); - if (!i.hasNext()) - i.toFront(); - path.addRoundedRect(rectangle, 5.0, 5.0); - painter.setPen(i.peekNext()); - if (palette().color(QPalette::Text).lightnessF() <= 0.3) - painter.setBrush(i.next().lighter()); - else if (palette().color(QPalette::Text).lightnessF() <= 0.6) - painter.setBrush(i.next()); - else - painter.setBrush(i.next().darker()); - painter.drawPath(path); - } - QPlainTextEdit::paintEvent(e); -} diff --git a/qt-ui/groupedlineedit.h b/qt-ui/groupedlineedit.h deleted file mode 100644 index c9cd1a0e0..000000000 --- a/qt-ui/groupedlineedit.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2013 Maximilian Güntner - * - * This file is subject to the terms and conditions of version 2 of - * the GNU General Public License. See the file gpl-2.0.txt in the main - * directory of this archive for more details. - * - * Original License: - * - * This file is part of the Nepomuk widgets collection - * Copyright (c) 2013 Denis Steckelmacher - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License version 2.1 as published by the Free Software Foundation, - * or any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Library General Public License for more details. - * - * You should have received a copy of the GNU Library General Public License - * along with this library; see the file COPYING.LIB. If not, write to - * - * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef GROUPEDLINEEDIT_H -#define GROUPEDLINEEDIT_H - -#include -#include - -class GroupedLineEdit : public QPlainTextEdit { - Q_OBJECT - -public: - explicit GroupedLineEdit(QWidget *parent = 0); - virtual ~GroupedLineEdit(); - - QString text() const; - - int cursorPosition() const; - void setCursorPosition(int position); - void setText(const QString &text); - void clear(); - void selectAll(); - - void removeAllBlocks(); - void addBlock(int start, int end); - QStringList getBlockStringList(); - - void addColor(QColor color); - void removeAllColors(); - - virtual QSize sizeHint() const; - virtual QSize minimumSizeHint() const; - -signals: - void editingFinished(); - -protected: - virtual void paintEvent(QPaintEvent *e); - virtual void keyPressEvent(QKeyEvent *e); - -private: - struct Private; - Private *d; -}; -#endif // GROUPEDLINEEDIT_H diff --git a/qt-ui/kmessagewidget.cpp b/qt-ui/kmessagewidget.cpp deleted file mode 100644 index 2e506af2d..000000000 --- a/qt-ui/kmessagewidget.cpp +++ /dev/null @@ -1,480 +0,0 @@ -/* This file is part of the KDE libraries - * - * Copyright (c) 2011 Aurélien Gâteau - * Copyright (c) 2014 Dominik Haumann - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301 USA - */ -#include "kmessagewidget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -//--------------------------------------------------------------------- -// KMessageWidgetPrivate -//--------------------------------------------------------------------- -class KMessageWidgetPrivate -{ -public: - void init(KMessageWidget *); - - KMessageWidget *q; - QFrame *content; - QLabel *iconLabel; - QLabel *textLabel; - QToolButton *closeButton; - QTimeLine *timeLine; - QIcon icon; - - KMessageWidget::MessageType messageType; - bool wordWrap; - QList buttons; - QPixmap contentSnapShot; - - void createLayout(); - void updateSnapShot(); - void updateLayout(); - void slotTimeLineChanged(qreal); - void slotTimeLineFinished(); - - int bestContentHeight() const; -}; - -void KMessageWidgetPrivate::init(KMessageWidget *q_ptr) -{ - q = q_ptr; - - q->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); - - timeLine = new QTimeLine(500, q); - QObject::connect(timeLine, SIGNAL(valueChanged(qreal)), q, SLOT(slotTimeLineChanged(qreal))); - QObject::connect(timeLine, SIGNAL(finished()), q, SLOT(slotTimeLineFinished())); - - content = new QFrame(q); - content->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - wordWrap = false; - - iconLabel = new QLabel(content); - iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - iconLabel->hide(); - - textLabel = new QLabel(content); - textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); - QObject::connect(textLabel, SIGNAL(linkActivated(QString)), q, SIGNAL(linkActivated(QString))); - QObject::connect(textLabel, SIGNAL(linkHovered(QString)), q, SIGNAL(linkHovered(QString))); - - QAction *closeAction = new QAction(q); - closeAction->setText(KMessageWidget::tr("&Close")); - closeAction->setToolTip(KMessageWidget::tr("Close message")); - closeAction->setIcon(q->style()->standardIcon(QStyle::SP_DialogCloseButton)); - - QObject::connect(closeAction, SIGNAL(triggered(bool)), q, SLOT(animatedHide())); - - closeButton = new QToolButton(content); - closeButton->setAutoRaise(true); - closeButton->setDefaultAction(closeAction); - - q->setMessageType(KMessageWidget::Information); -} - -void KMessageWidgetPrivate::createLayout() -{ - delete content->layout(); - - content->resize(q->size()); - - qDeleteAll(buttons); - buttons.clear(); - - Q_FOREACH (QAction *action, q->actions()) { - QToolButton *button = new QToolButton(content); - button->setDefaultAction(action); - button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - buttons.append(button); - } - - // AutoRaise reduces visual clutter, but we don't want to turn it on if - // there are other buttons, otherwise the close button will look different - // from the others. - closeButton->setAutoRaise(buttons.isEmpty()); - - if (wordWrap) { - QGridLayout *layout = new QGridLayout(content); - // Set alignment to make sure icon does not move down if text wraps - layout->addWidget(iconLabel, 0, 0, 1, 1, Qt::AlignHCenter | Qt::AlignTop); - layout->addWidget(textLabel, 0, 1); - - QHBoxLayout *buttonLayout = new QHBoxLayout; - buttonLayout->addStretch(); - Q_FOREACH (QToolButton *button, buttons) { - // For some reason, calling show() is necessary if wordwrap is true, - // otherwise the buttons do not show up. It is not needed if - // wordwrap is false. - button->show(); - buttonLayout->addWidget(button); - } - buttonLayout->addWidget(closeButton); - layout->addItem(buttonLayout, 1, 0, 1, 2); - } else { - QHBoxLayout *layout = new QHBoxLayout(content); - layout->addWidget(iconLabel); - layout->addWidget(textLabel); - - Q_FOREACH (QToolButton *button, buttons) { - layout->addWidget(button); - } - - layout->addWidget(closeButton); - }; - - if (q->isVisible()) { - q->setFixedHeight(content->sizeHint().height()); - } - q->updateGeometry(); -} - -void KMessageWidgetPrivate::updateLayout() -{ - if (content->layout()) { - createLayout(); - } -} - -void KMessageWidgetPrivate::updateSnapShot() -{ - // Attention: updateSnapShot calls QWidget::render(), which causes the whole - // window layouts to be activated. Calling this method from resizeEvent() - // can lead to infinite recursion, see: - // https://bugs.kde.org/show_bug.cgi?id=311336 - contentSnapShot = QPixmap(content->size() * q->devicePixelRatio()); - contentSnapShot.setDevicePixelRatio(q->devicePixelRatio()); - contentSnapShot.fill(Qt::transparent); - content->render(&contentSnapShot, QPoint(), QRegion(), QWidget::DrawChildren); -} - -void KMessageWidgetPrivate::slotTimeLineChanged(qreal value) -{ - q->setFixedHeight(qMin(value * 2, qreal(1.0)) * content->height()); - q->update(); -} - -void KMessageWidgetPrivate::slotTimeLineFinished() -{ - if (timeLine->direction() == QTimeLine::Forward) { - // Show - // We set the whole geometry here, because it may be wrong if a - // KMessageWidget is shown right when the toplevel window is created. - content->setGeometry(0, 0, q->width(), bestContentHeight()); - - // notify about finished animation - emit q->showAnimationFinished(); - } else { - // hide and notify about finished animation - q->hide(); - emit q->hideAnimationFinished(); - } -} - -int KMessageWidgetPrivate::bestContentHeight() const -{ - int height = content->heightForWidth(q->width()); - if (height == -1) { - height = content->sizeHint().height(); - } - return height; -} - -//--------------------------------------------------------------------- -// KMessageWidget -//--------------------------------------------------------------------- -KMessageWidget::KMessageWidget(QWidget *parent) - : QFrame(parent) - , d(new KMessageWidgetPrivate) -{ - d->init(this); -} - -KMessageWidget::KMessageWidget(const QString &text, QWidget *parent) - : QFrame(parent) - , d(new KMessageWidgetPrivate) -{ - d->init(this); - setText(text); -} - -KMessageWidget::~KMessageWidget() -{ - delete d; -} - -QString KMessageWidget::text() const -{ - return d->textLabel->text(); -} - -void KMessageWidget::setText(const QString &text) -{ - d->textLabel->setText(text); - updateGeometry(); -} - -KMessageWidget::MessageType KMessageWidget::messageType() const -{ - return d->messageType; -} - -static QColor darkShade(QColor c) -{ - qreal contrast = 0.7; // taken from kcolorscheme for the dark shade - - qreal darkAmount; - if (c.lightnessF() < 0.006) { /* too dark */ - darkAmount = 0.02 + 0.40 * contrast; - } else if (c.lightnessF() > 0.93) { /* too bright */ - darkAmount = -0.06 - 0.60 * contrast; - } else { - darkAmount = (-c.lightnessF()) * (0.55 + contrast * 0.35); - } - - qreal v = c.lightnessF() + darkAmount; - v = v > 0.0 ? (v < 1.0 ? v : 1.0) : 0.0; - c.setHsvF(c.hslHueF(), c.hslSaturationF(), v); - return c; -} - -void KMessageWidget::setMessageType(KMessageWidget::MessageType type) -{ - d->messageType = type; - QColor bg0, bg1, bg2, border, fg; - switch (type) { - case Positive: - bg1.setRgb(0, 110, 40); // values taken from kcolorscheme.cpp (Positive) - break; - case Information: - bg1 = palette().highlight().color(); - break; - case Warning: - bg1.setRgb(176, 128, 0); // values taken from kcolorscheme.cpp (Neutral) - break; - case Error: - bg1.setRgb(191, 3, 3); // values taken from kcolorscheme.cpp (Negative) - break; - } - - // Colors - fg = palette().highlightedText().color(); - bg0 = bg1.lighter(110); - bg2 = bg1.darker(110); - border = darkShade(bg1); - - d->content->setStyleSheet( - QString(QLatin1String(".QFrame {" - "background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," - " stop: 0 %1," - " stop: 0.1 %2," - " stop: 1.0 %3);" - "border-radius: 5px;" - "border: 1px solid %4;" - "margin: %5px;" - "}" - ".QLabel { color: %6; }" - )) - .arg(bg0.name()) - .arg(bg1.name()) - .arg(bg2.name()) - .arg(border.name()) - // DefaultFrameWidth returns the size of the external margin + border width. We know our border is 1px, so we subtract this from the frame normal QStyle FrameWidth to get our margin - .arg(style()->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, this) - 1) - .arg(fg.name()) - ); -} - -QSize KMessageWidget::sizeHint() const -{ - ensurePolished(); - return d->content->sizeHint(); -} - -QSize KMessageWidget::minimumSizeHint() const -{ - ensurePolished(); - return d->content->minimumSizeHint(); -} - -bool KMessageWidget::event(QEvent *event) -{ - if (event->type() == QEvent::Polish && !d->content->layout()) { - d->createLayout(); - } - return QFrame::event(event); -} - -void KMessageWidget::resizeEvent(QResizeEvent *event) -{ - QFrame::resizeEvent(event); - - if (d->timeLine->state() == QTimeLine::NotRunning) { - d->content->resize(width(), d->bestContentHeight()); - } -} - -int KMessageWidget::heightForWidth(int width) const -{ - ensurePolished(); - return d->content->heightForWidth(width); -} - -void KMessageWidget::paintEvent(QPaintEvent *event) -{ - QFrame::paintEvent(event); - if (d->timeLine->state() == QTimeLine::Running) { - QPainter painter(this); - painter.setOpacity(d->timeLine->currentValue() * d->timeLine->currentValue()); - painter.drawPixmap(0, 0, d->contentSnapShot); - } -} - -bool KMessageWidget::wordWrap() const -{ - return d->wordWrap; -} - -void KMessageWidget::setWordWrap(bool wordWrap) -{ - d->wordWrap = wordWrap; - d->textLabel->setWordWrap(wordWrap); - QSizePolicy policy = sizePolicy(); - policy.setHeightForWidth(wordWrap); - setSizePolicy(policy); - d->updateLayout(); - // Without this, when user does wordWrap -> !wordWrap -> wordWrap, a minimum - // height is set, causing the widget to be too high. - // Mostly visible in test programs. - if (wordWrap) { - setMinimumHeight(0); - } -} - -bool KMessageWidget::isCloseButtonVisible() const -{ - return d->closeButton->isVisible(); -} - -void KMessageWidget::setCloseButtonVisible(bool show) -{ - d->closeButton->setVisible(show); - updateGeometry(); -} - -void KMessageWidget::addAction(QAction *action) -{ - QFrame::addAction(action); - d->updateLayout(); -} - -void KMessageWidget::removeAction(QAction *action) -{ - QFrame::removeAction(action); - d->updateLayout(); -} - -void KMessageWidget::animatedShow() -{ - if (!style()->styleHint(QStyle::SH_Widget_Animate, 0, this)) { - show(); - emit showAnimationFinished(); - return; - } - - if (isVisible()) { - return; - } - - QFrame::show(); - setFixedHeight(0); - int wantedHeight = d->bestContentHeight(); - d->content->setGeometry(0, -wantedHeight, width(), wantedHeight); - - d->updateSnapShot(); - - d->timeLine->setDirection(QTimeLine::Forward); - if (d->timeLine->state() == QTimeLine::NotRunning) { - d->timeLine->start(); - } -} - -void KMessageWidget::animatedHide() -{ - if (!style()->styleHint(QStyle::SH_Widget_Animate, 0, this)) { - hide(); - emit hideAnimationFinished(); - return; - } - - if (!isVisible()) { - hide(); - return; - } - - d->content->move(0, -d->content->height()); - d->updateSnapShot(); - - d->timeLine->setDirection(QTimeLine::Backward); - if (d->timeLine->state() == QTimeLine::NotRunning) { - d->timeLine->start(); - } -} - -bool KMessageWidget::isHideAnimationRunning() const -{ - return (d->timeLine->direction() == QTimeLine::Backward) - && (d->timeLine->state() == QTimeLine::Running); -} - -bool KMessageWidget::isShowAnimationRunning() const -{ - return (d->timeLine->direction() == QTimeLine::Forward) - && (d->timeLine->state() == QTimeLine::Running); -} - -QIcon KMessageWidget::icon() const -{ - return d->icon; -} - -void KMessageWidget::setIcon(const QIcon &icon) -{ - d->icon = icon; - if (d->icon.isNull()) { - d->iconLabel->hide(); - } else { - const int size = style()->pixelMetric(QStyle::PM_ToolBarIconSize); - d->iconLabel->setPixmap(d->icon.pixmap(size)); - d->iconLabel->show(); - } -} - -#include "moc_kmessagewidget.cpp" diff --git a/qt-ui/kmessagewidget.h b/qt-ui/kmessagewidget.h deleted file mode 100644 index 885d2a78f..000000000 --- a/qt-ui/kmessagewidget.h +++ /dev/null @@ -1,342 +0,0 @@ -/* This file is part of the KDE libraries - * - * Copyright (c) 2011 Aurélien Gâteau - * Copyright (c) 2014 Dominik Haumann - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301 USA - */ -#ifndef KMESSAGEWIDGET_H -#define KMESSAGEWIDGET_H - -#include - -class KMessageWidgetPrivate; - -/** - * @short A widget to provide feedback or propose opportunistic interactions. - * - * KMessageWidget can be used to provide inline positive or negative - * feedback, or to implement opportunistic interactions. - * - * As a feedback widget, KMessageWidget provides a less intrusive alternative - * to "OK Only" message boxes. If you want to avoid a modal KMessageBox, - * consider using KMessageWidget instead. - * - * Examples of KMessageWidget look as follows, all of them having an icon set - * with setIcon(), and the first three show a close button: - * - * \image html kmessagewidget.png "KMessageWidget with different message types" - * - * Negative feedback - * - * The KMessageWidget can be used as a secondary indicator of failure: the - * first indicator is usually the fact the action the user expected to happen - * did not happen. - * - * Example: User fills a form, clicks "Submit". - * - * @li Expected feedback: form closes - * @li First indicator of failure: form stays there - * @li Second indicator of failure: a KMessageWidget appears on top of the - * form, explaining the error condition - * - * When used to provide negative feedback, KMessageWidget should be placed - * close to its context. In the case of a form, it should appear on top of the - * form entries. - * - * KMessageWidget should get inserted in the existing layout. Space should not - * be reserved for it, otherwise it becomes "dead space", ignored by the user. - * KMessageWidget should also not appear as an overlay to prevent blocking - * access to elements the user needs to interact with to fix the failure. - * - * Positive feedback - * - * KMessageWidget can be used for positive feedback but it shouldn't be - * overused. It is often enough to provide feedback by simply showing the - * results of an action. - * - * Examples of acceptable uses: - * - * @li Confirm success of "critical" transactions - * @li Indicate completion of background tasks - * - * Example of unadapted uses: - * - * @li Indicate successful saving of a file - * @li Indicate a file has been successfully removed - * - * Opportunistic interaction - * - * Opportunistic interaction is the situation where the application suggests to - * the user an action he could be interested in perform, either based on an - * action the user just triggered or an event which the application noticed. - * - * Example of acceptable uses: - * - * @li A browser can propose remembering a recently entered password - * @li A music collection can propose ripping a CD which just got inserted - * @li A chat application may notify the user a "special friend" just connected - * - * @author Aurélien Gâteau - * @since 4.7 - */ -class KMessageWidget : public QFrame -{ - Q_OBJECT - Q_ENUMS(MessageType) - - Q_PROPERTY(QString text READ text WRITE setText) - Q_PROPERTY(bool wordWrap READ wordWrap WRITE setWordWrap) - Q_PROPERTY(bool closeButtonVisible READ isCloseButtonVisible WRITE setCloseButtonVisible) - Q_PROPERTY(MessageType messageType READ messageType WRITE setMessageType) - Q_PROPERTY(QIcon icon READ icon WRITE setIcon) -public: - - /** - * Available message types. - * The background colors are chosen depending on the message type. - */ - enum MessageType { - Positive, - Information, - Warning, - Error - }; - - /** - * Constructs a KMessageWidget with the specified @p parent. - */ - explicit KMessageWidget(QWidget *parent = 0); - - /** - * Constructs a KMessageWidget with the specified @p parent and - * contents @p text. - */ - explicit KMessageWidget(const QString &text, QWidget *parent = 0); - - /** - * Destructor. - */ - ~KMessageWidget(); - - /** - * Get the text of this message widget. - * @see setText() - */ - QString text() const; - - /** - * Check whether word wrap is enabled. - * - * If word wrap is enabled, the message widget wraps the displayed text - * as required to the available width of the widget. This is useful to - * avoid breaking widget layouts. - * - * @see setWordWrap() - */ - bool wordWrap() const; - - /** - * Check whether the close button is visible. - * - * @see setCloseButtonVisible() - */ - bool isCloseButtonVisible() const; - - /** - * Get the type of this message. - * By default, the type is set to KMessageWidget::Information. - * - * @see KMessageWidget::MessageType, setMessageType() - */ - MessageType messageType() const; - - /** - * Add @p action to the message widget. - * For each action a button is added to the message widget in the - * order the actions were added. - * - * @param action the action to add - * @see removeAction(), QWidget::actions() - */ - void addAction(QAction *action); - - /** - * Remove @p action from the message widget. - * - * @param action the action to remove - * @see KMessageWidget::MessageType, addAction(), setMessageType() - */ - void removeAction(QAction *action); - - /** - * Returns the preferred size of the message widget. - */ - QSize sizeHint() const Q_DECL_OVERRIDE; - - /** - * Returns the minimum size of the message widget. - */ - QSize minimumSizeHint() const Q_DECL_OVERRIDE; - - /** - * Returns the required height for @p width. - * @param width the width in pixels - */ - int heightForWidth(int width) const Q_DECL_OVERRIDE; - - /** - * The icon shown on the left of the text. By default, no icon is shown. - * @since 4.11 - */ - QIcon icon() const; - - /** - * Check whether the hide animation started by calling animatedHide() - * is still running. If animations are disabled, this function always - * returns @e false. - * - * @see animatedHide(), hideAnimationFinished() - * @since 5.0 - */ - bool isHideAnimationRunning() const; - - /** - * Check whether the show animation started by calling animatedShow() - * is still running. If animations are disabled, this function always - * returns @e false. - * - * @see animatedShow(), showAnimationFinished() - * @since 5.0 - */ - bool isShowAnimationRunning() const; - -public Q_SLOTS: - /** - * Set the text of the message widget to @p text. - * If the message widget is already visible, the text changes on the fly. - * - * @param text the text to display, rich text is allowed - * @see text() - */ - void setText(const QString &text); - - /** - * Set word wrap to @p wordWrap. If word wrap is enabled, the text() - * of the message widget is wrapped to fit the available width. - * If word wrap is disabled, the message widget's minimum size is - * such that the entire text fits. - * - * @param wordWrap disable/enable word wrap - * @see wordWrap() - */ - void setWordWrap(bool wordWrap); - - /** - * Set the visibility of the close button. If @p visible is @e true, - * a close button is shown that calls animatedHide() if clicked. - * - * @see closeButtonVisible(), animatedHide() - */ - void setCloseButtonVisible(bool visible); - - /** - * Set the message type to @p type. - * By default, the message type is set to KMessageWidget::Information. - * - * @see messageType(), KMessageWidget::MessageType - */ - void setMessageType(KMessageWidget::MessageType type); - - /** - * Show the widget using an animation. - */ - void animatedShow(); - - /** - * Hide the widget using an animation. - */ - void animatedHide(); - - /** - * Define an icon to be shown on the left of the text - * @since 4.11 - */ - void setIcon(const QIcon &icon); - -Q_SIGNALS: - /** - * This signal is emitted when the user clicks a link in the text label. - * The URL referred to by the href anchor is passed in contents. - * @param contents text of the href anchor - * @see QLabel::linkActivated() - * @since 4.10 - */ - void linkActivated(const QString &contents); - - /** - * This signal is emitted when the user hovers over a link in the text label. - * The URL referred to by the href anchor is passed in contents. - * @param contents text of the href anchor - * @see QLabel::linkHovered() - * @since 4.11 - */ - void linkHovered(const QString &contents); - - /** - * This signal is emitted when the hide animation is finished, started by - * calling animatedHide(). If animations are disabled, this signal is - * emitted immediately after the message widget got hidden. - * - * @note This signal is @e not emitted if the widget was hidden by - * calling hide(), so this signal is only useful in conjunction - * with animatedHide(). - * - * @see animatedHide() - * @since 5.0 - */ - void hideAnimationFinished(); - - /** - * This signal is emitted when the show animation is finished, started by - * calling animatedShow(). If animations are disabled, this signal is - * emitted immediately after the message widget got shown. - * - * @note This signal is @e not emitted if the widget was shown by - * calling show(), so this signal is only useful in conjunction - * with animatedShow(). - * - * @see animatedShow() - * @since 5.0 - */ - void showAnimationFinished(); - -protected: - void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; - - bool event(QEvent *event) Q_DECL_OVERRIDE; - - void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; - -private: - KMessageWidgetPrivate *const d; - friend class KMessageWidgetPrivate; - - Q_PRIVATE_SLOT(d, void slotTimeLineChanged(qreal)) - Q_PRIVATE_SLOT(d, void slotTimeLineFinished()) -}; - -#endif /* KMESSAGEWIDGET_H */ diff --git a/qt-ui/listfilter.ui b/qt-ui/listfilter.ui deleted file mode 100644 index 48d813d21..000000000 --- a/qt-ui/listfilter.ui +++ /dev/null @@ -1,63 +0,0 @@ - - - FilterWidget - - - - 0 - 0 - 400 - 166 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 5 - - - - - Text label - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - Filter this list - - - - - - - - - - - - - diff --git a/qt-ui/locationInformation.ui b/qt-ui/locationInformation.ui deleted file mode 100644 index 58d065648..000000000 --- a/qt-ui/locationInformation.ui +++ /dev/null @@ -1,156 +0,0 @@ - - - LocationInformation - - - - 0 - 0 - 556 - 707 - - - - GroupBox - - - - - - - 6 - - - 4 - - - - - Name - - - - - - - Description - - - - - - - Notes - - - - - - - - - - Coordinates - - - - - - - - - - - - - - - - Reverse geo lookup - - - ... - - - - :/satellite:/satellite - - - - - - - - 0 - 0 - - - - - - - - Dive sites on same coordinates - - - - - - - 0 - 0 - - - - QAbstractItemView::MultiSelection - - - 0 - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Tags - - - - - - - - - - - - - - - KMessageWidget - QFrame -
kmessagewidget.h
- 1 -
-
- - - - -
diff --git a/qt-ui/locationinformation.cpp b/qt-ui/locationinformation.cpp deleted file mode 100644 index aee0b7328..000000000 --- a/qt-ui/locationinformation.cpp +++ /dev/null @@ -1,618 +0,0 @@ -#include "locationinformation.h" -#include "dive.h" -#include "mainwindow.h" -#include "divelistview.h" -#include "qthelper.h" -#include "globe.h" -#include "filtermodels.h" -#include "divelocationmodel.h" -#include "divesitehelpers.h" -#include "modeldelegates.h" - -#include -#include -#include -#include -#include -#include -#include - -LocationInformationWidget::LocationInformationWidget(QWidget *parent) : QGroupBox(parent), modified(false) -{ - ui.setupUi(this); - ui.diveSiteMessage->setCloseButtonVisible(false); - - acceptAction = new QAction(tr("Apply changes"), this); - connect(acceptAction, SIGNAL(triggered(bool)), this, SLOT(acceptChanges())); - - rejectAction = new QAction(tr("Discard changes"), this); - connect(rejectAction, SIGNAL(triggered(bool)), this, SLOT(rejectChanges())); - - ui.diveSiteMessage->setText(tr("Dive site management")); - ui.diveSiteMessage->addAction(acceptAction); - ui.diveSiteMessage->addAction(rejectAction); - - connect(this, SIGNAL(startFilterDiveSite(uint32_t)), MultiFilterSortModel::instance(), SLOT(startFilterDiveSite(uint32_t))); - connect(this, SIGNAL(stopFilterDiveSite()), MultiFilterSortModel::instance(), SLOT(stopFilterDiveSite())); - connect(ui.geoCodeButton, SIGNAL(clicked()), this, SLOT(reverseGeocode())); - - SsrfSortFilterProxyModel *filter_model = new SsrfSortFilterProxyModel(this); - filter_model->setSourceModel(LocationInformationModel::instance()); - filter_model->setFilterRow(filter_same_gps_cb); - ui.diveSiteListView->setModel(filter_model); - ui.diveSiteListView->setModelColumn(LocationInformationModel::NAME); - ui.diveSiteListView->installEventFilter(this); -#ifndef NO_MARBLE - // Globe Management Code. - connect(this, &LocationInformationWidget::requestCoordinates, - GlobeGPS::instance(), &GlobeGPS::prepareForGetDiveCoordinates); - connect(this, &LocationInformationWidget::endRequestCoordinates, - GlobeGPS::instance(), &GlobeGPS::endGetDiveCoordinates); - connect(GlobeGPS::instance(), &GlobeGPS::coordinatesChanged, - this, &LocationInformationWidget::updateGpsCoordinates); - connect(this, &LocationInformationWidget::endEditDiveSite, - GlobeGPS::instance(), &GlobeGPS::repopulateLabels); -#endif -} - -bool LocationInformationWidget::eventFilter(QObject *, QEvent *ev) -{ - if (ev->type() == QEvent::ContextMenu) { - QContextMenuEvent *ctx = (QContextMenuEvent *)ev; - QMenu contextMenu; - contextMenu.addAction(tr("Merge into current site"), this, SLOT(mergeSelectedDiveSites())); - contextMenu.exec(ctx->globalPos()); - return true; - } - return false; -} - -void LocationInformationWidget::mergeSelectedDiveSites() -{ - if (QMessageBox::warning(MainWindow::instance(), tr("Merging dive sites"), - tr("You are about to merge dive sites, you can't undo that action \n Are you sure you want to continue?"), - QMessageBox::Ok, QMessageBox::Cancel) != QMessageBox::Ok) - return; - - QModelIndexList selection = ui.diveSiteListView->selectionModel()->selectedIndexes(); - uint32_t *selected_dive_sites = (uint32_t *)malloc(sizeof(uint32_t) * selection.count()); - int i = 0; - Q_FOREACH (const QModelIndex &idx, selection) { - selected_dive_sites[i] = (uint32_t)idx.data(LocationInformationModel::UUID_ROLE).toInt(); - i++; - } - merge_dive_sites(displayed_dive_site.uuid, selected_dive_sites, i); - LocationInformationModel::instance()->update(); - QSortFilterProxyModel *m = (QSortFilterProxyModel *)ui.diveSiteListView->model(); - m->invalidate(); - free(selected_dive_sites); -} - -void LocationInformationWidget::updateLabels() -{ - if (displayed_dive_site.name) - ui.diveSiteName->setText(displayed_dive_site.name); - else - ui.diveSiteName->clear(); - if (displayed_dive_site.description) - ui.diveSiteDescription->setText(displayed_dive_site.description); - else - ui.diveSiteDescription->clear(); - if (displayed_dive_site.notes) - ui.diveSiteNotes->setPlainText(displayed_dive_site.notes); - else - ui.diveSiteNotes->clear(); - if (displayed_dive_site.latitude.udeg || displayed_dive_site.longitude.udeg) { - const char *coords = printGPSCoords(displayed_dive_site.latitude.udeg, displayed_dive_site.longitude.udeg); - ui.diveSiteCoordinates->setText(coords); - free((void *)coords); - } else { - ui.diveSiteCoordinates->clear(); - } - - ui.locationTags->setText(constructLocationTags(displayed_dive_site.uuid)); - - emit startFilterDiveSite(displayed_dive_site.uuid); - emit startEditDiveSite(displayed_dive_site.uuid); -} - -void LocationInformationWidget::updateGpsCoordinates() -{ - QString oldText = ui.diveSiteCoordinates->text(); - const char *coords = printGPSCoords(displayed_dive_site.latitude.udeg, displayed_dive_site.longitude.udeg); - ui.diveSiteCoordinates->setText(coords); - free((void *)coords); - if (oldText != ui.diveSiteCoordinates->text()) - markChangedWidget(ui.diveSiteCoordinates); -} - -void LocationInformationWidget::acceptChanges() -{ - char *uiString; - struct dive_site *currentDs; - uiString = ui.diveSiteName->text().toUtf8().data(); - - if (get_dive_site_by_uuid(displayed_dive_site.uuid) != NULL) - currentDs = get_dive_site_by_uuid(displayed_dive_site.uuid); - else - currentDs = get_dive_site_by_uuid(create_dive_site_from_current_dive(uiString)); - - currentDs->latitude = displayed_dive_site.latitude; - currentDs->longitude = displayed_dive_site.longitude; - if (!same_string(uiString, currentDs->name)) { - free(currentDs->name); - currentDs->name = copy_string(uiString); - } - uiString = ui.diveSiteDescription->text().toUtf8().data(); - if (!same_string(uiString, currentDs->description)) { - free(currentDs->description); - currentDs->description = copy_string(uiString); - } - uiString = ui.diveSiteNotes->document()->toPlainText().toUtf8().data(); - if (!same_string(uiString, currentDs->notes)) { - free(currentDs->notes); - currentDs->notes = copy_string(uiString); - } - if (!ui.diveSiteCoordinates->text().isEmpty()) { - double lat, lon; - parseGpsText(ui.diveSiteCoordinates->text(), &lat, &lon); - currentDs->latitude.udeg = lat * 1000000.0; - currentDs->longitude.udeg = lon * 1000000.0; - } - if (dive_site_is_empty(currentDs)) { - LocationInformationModel::instance()->removeRow(get_divesite_idx(currentDs)); - displayed_dive.dive_site_uuid = 0; - } - copy_dive_site(currentDs, &displayed_dive_site); - mark_divelist_changed(true); - resetState(); - emit endRequestCoordinates(); - emit endEditDiveSite(); - emit stopFilterDiveSite(); - emit coordinatesChanged(); -} - -void LocationInformationWidget::rejectChanges() -{ - resetState(); - emit endRequestCoordinates(); - emit stopFilterDiveSite(); - emit endEditDiveSite(); - emit coordinatesChanged(); -} - -void LocationInformationWidget::showEvent(QShowEvent *ev) -{ - if (displayed_dive_site.uuid) { - updateLabels(); - ui.geoCodeButton->setEnabled(dive_site_has_gps_location(&displayed_dive_site)); - QSortFilterProxyModel *m = qobject_cast(ui.diveSiteListView->model()); - emit startFilterDiveSite(displayed_dive_site.uuid); - if (m) - m->invalidate(); - } - emit requestCoordinates(); - - QGroupBox::showEvent(ev); -} - -void LocationInformationWidget::markChangedWidget(QWidget *w) -{ - QPalette p; - qreal h, s, l, a; - if (!modified) - enableEdition(); - qApp->palette().color(QPalette::Text).getHslF(&h, &s, &l, &a); - p.setBrush(QPalette::Base, (l <= 0.3) ? QColor(Qt::yellow).lighter() : (l <= 0.6) ? QColor(Qt::yellow).light() : /* else */ QColor(Qt::yellow).darker(300)); - w->setPalette(p); - modified = true; -} - -void LocationInformationWidget::resetState() -{ - modified = false; - resetPallete(); - MainWindow::instance()->dive_list()->setEnabled(true); - MainWindow::instance()->setEnabledToolbar(true); - ui.diveSiteMessage->setText(tr("Dive site management")); -} - -void LocationInformationWidget::enableEdition() -{ - MainWindow::instance()->dive_list()->setEnabled(false); - MainWindow::instance()->setEnabledToolbar(false); - ui.diveSiteMessage->setText(tr("You are editing a dive site")); -} - -void LocationInformationWidget::on_diveSiteCoordinates_textChanged(const QString &text) -{ - uint lat = displayed_dive_site.latitude.udeg; - uint lon = displayed_dive_site.longitude.udeg; - const char *coords = printGPSCoords(lat, lon); - if (!same_string(qPrintable(text), coords)) { - double latitude, longitude; - if (parseGpsText(text, &latitude, &longitude)) { - displayed_dive_site.latitude.udeg = latitude * 1000000; - displayed_dive_site.longitude.udeg = longitude * 1000000; - markChangedWidget(ui.diveSiteCoordinates); - emit coordinatesChanged(); - ui.geoCodeButton->setEnabled(latitude != 0 && longitude != 0); - } else { - ui.geoCodeButton->setEnabled(false); - } - } - free((void *)coords); -} - -void LocationInformationWidget::on_diveSiteDescription_textChanged(const QString &text) -{ - if (!same_string(qPrintable(text), displayed_dive_site.description)) - markChangedWidget(ui.diveSiteDescription); -} - -void LocationInformationWidget::on_diveSiteName_textChanged(const QString &text) -{ - if (!same_string(qPrintable(text), displayed_dive_site.name)) - markChangedWidget(ui.diveSiteName); -} - -void LocationInformationWidget::on_diveSiteNotes_textChanged() -{ - if (!same_string(qPrintable(ui.diveSiteNotes->toPlainText()), displayed_dive_site.notes)) - markChangedWidget(ui.diveSiteNotes); -} - -void LocationInformationWidget::resetPallete() -{ - QPalette p; - ui.diveSiteCoordinates->setPalette(p); - ui.diveSiteDescription->setPalette(p); - ui.diveSiteName->setPalette(p); - ui.diveSiteNotes->setPalette(p); -} - -void LocationInformationWidget::reverseGeocode() -{ - ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); - geoLookup->lookup(&displayed_dive_site); - updateLabels(); -} - -DiveLocationFilterProxyModel::DiveLocationFilterProxyModel(QObject *parent) -{ -} - -DiveLocationLineEdit *location_line_edit = 0; - -bool DiveLocationFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const -{ - if (source_row == 0) - return true; - - QString sourceString = sourceModel()->index(source_row, DiveLocationModel::NAME).data(Qt::DisplayRole).toString(); - return sourceString.toLower().startsWith(location_line_edit->text().toLower()); -} - -bool DiveLocationFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const -{ - return source_left.data().toString() <= source_right.data().toString(); -} - - -DiveLocationModel::DiveLocationModel(QObject *o) -{ - resetModel(); -} - -void DiveLocationModel::resetModel() -{ - beginResetModel(); - endResetModel(); -} - -QVariant DiveLocationModel::data(const QModelIndex &index, int role) const -{ - static const QIcon plusIcon(":plus"); - static const QIcon geoCode(":geocode"); - - if (index.row() <= 1) { // two special cases. - if (index.column() == UUID) { - return RECENTLY_ADDED_DIVESITE; - } - switch (role) { - case Qt::DisplayRole: - return new_ds_value[index.row()]; - case Qt::ToolTipRole: - return displayed_dive_site.uuid ? - tr("Create a new dive site, copying relevant information from the current dive.") : - tr("Create a new dive site with this name"); - case Qt::DecorationRole: - return plusIcon; - } - } - - // The dive sites are -2 because of the first two items. - struct dive_site *ds = get_dive_site(index.row() - 2); - switch (role) { - case Qt::EditRole: - case Qt::DisplayRole: - switch (index.column()) { - case UUID: - return ds->uuid; - case NAME: - return ds->name; - case LATITUDE: - return ds->latitude.udeg; - case LONGITUDE: - return ds->longitude.udeg; - case DESCRIPTION: - return ds->description; - case NOTES: - return ds->name; - } - break; - case Qt::DecorationRole: { - if (dive_site_has_gps_location(ds)) - return geoCode; - } - } - return QVariant(); -} - -int DiveLocationModel::columnCount(const QModelIndex &parent) const -{ - return COLUMNS; -} - -int DiveLocationModel::rowCount(const QModelIndex &parent) const -{ - return dive_site_table.nr + 2; -} - -bool DiveLocationModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (!index.isValid()) - return false; - if (index.row() > 1) - return false; - - new_ds_value[index.row()] = value.toString(); - - dataChanged(index, index); - return true; -} - -DiveLocationLineEdit::DiveLocationLineEdit(QWidget *parent) : QLineEdit(parent), - proxy(new DiveLocationFilterProxyModel()), - model(new DiveLocationModel()), - view(new DiveLocationListView()), - currType(NO_DIVE_SITE) -{ - currUuid = 0; - location_line_edit = this; - - proxy->setSourceModel(model); - proxy->setFilterKeyColumn(DiveLocationModel::NAME); - - view->setModel(proxy); - view->setModelColumn(DiveLocationModel::NAME); - view->setItemDelegate(new LocationFilterDelegate()); - view->setEditTriggers(QAbstractItemView::NoEditTriggers); - view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - view->setSelectionBehavior(QAbstractItemView::SelectRows); - view->setSelectionMode(QAbstractItemView::SingleSelection); - view->setParent(0, Qt::Popup); - view->installEventFilter(this); - view->setFocusPolicy(Qt::NoFocus); - view->setFocusProxy(this); - view->setMouseTracking(true); - - connect(this, &QLineEdit::textEdited, this, &DiveLocationLineEdit::setTemporaryDiveSiteName); - connect(view, &QAbstractItemView::activated, this, &DiveLocationLineEdit::itemActivated); - connect(view, &QAbstractItemView::entered, this, &DiveLocationLineEdit::entered); - connect(view, &DiveLocationListView::currentIndexChanged, this, &DiveLocationLineEdit::currentChanged); -} - -bool DiveLocationLineEdit::eventFilter(QObject *o, QEvent *e) -{ - if (e->type() == QEvent::KeyPress) { - QKeyEvent *keyEv = (QKeyEvent *)e; - - if (keyEv->key() == Qt::Key_Escape) { - view->hide(); - return true; - } - - if (keyEv->key() == Qt::Key_Return || - keyEv->key() == Qt::Key_Enter) { -#if __APPLE__ - // for some reason it seems like on a Mac hitting return/enter - // doesn't call 'activated' for that index. so let's do it manually - if (view->currentIndex().isValid()) - itemActivated(view->currentIndex()); -#endif - view->hide(); - return false; - } - - if (keyEv->key() == Qt::Key_Tab) { - itemActivated(view->currentIndex()); - view->hide(); - return false; - } - event(e); - } else if (e->type() == QEvent::MouseButtonPress) { - if (!view->underMouse()) { - view->hide(); - return true; - } - } - - return false; -} - -void DiveLocationLineEdit::focusOutEvent(QFocusEvent *ev) -{ - if (!view->isVisible()) { - QLineEdit::focusOutEvent(ev); - } -} - -void DiveLocationLineEdit::itemActivated(const QModelIndex &index) -{ - QModelIndex idx = index; - if (index.column() == DiveLocationModel::UUID) - idx = index.model()->index(index.row(), DiveLocationModel::NAME); - - QModelIndex uuidIndex = index.model()->index(index.row(), DiveLocationModel::UUID); - uint32_t uuid = uuidIndex.data().toInt(); - currType = uuid == 1 ? NEW_DIVE_SITE : EXISTING_DIVE_SITE; - currUuid = uuid; - setText(idx.data().toString()); - if (currUuid == NEW_DIVE_SITE) - qDebug() << "Setting a New dive site"; - else - qDebug() << "Setting a Existing dive site"; - if (view->isVisible()) - view->hide(); - emit diveSiteSelected(currUuid); -} - -void DiveLocationLineEdit::refreshDiveSiteCache() -{ - model->resetModel(); -} - -static struct dive_site *get_dive_site_name_start_which_str(const QString &str) -{ - struct dive_site *ds; - int i; - for_each_dive_site (i, ds) { - QString dsName(ds->name); - if (dsName.toLower().startsWith(str.toLower())) { - return ds; - } - } - return NULL; -} - -void DiveLocationLineEdit::setTemporaryDiveSiteName(const QString &s) -{ - QModelIndex i0 = model->index(0, DiveLocationModel::NAME); - QModelIndex i1 = model->index(1, DiveLocationModel::NAME); - model->setData(i0, text()); - - QString i1_name = INVALID_DIVE_SITE_NAME; - if (struct dive_site *ds = get_dive_site_name_start_which_str(text())) { - const QString orig_name = QString(ds->name).toLower(); - const QString new_name = text().toLower(); - if (new_name != orig_name) - i1_name = QString(ds->name); - } - - model->setData(i1, i1_name); - proxy->invalidate(); - fixPopupPosition(); - if (!view->isVisible()) - view->show(); -} - -void DiveLocationLineEdit::keyPressEvent(QKeyEvent *ev) -{ - QLineEdit::keyPressEvent(ev); - if (ev->key() != Qt::Key_Left && - ev->key() != Qt::Key_Right && - ev->key() != Qt::Key_Escape && - ev->key() != Qt::Key_Return) { - - if (ev->key() != Qt::Key_Up && ev->key() != Qt::Key_Down) { - currType = NEW_DIVE_SITE; - currUuid = RECENTLY_ADDED_DIVESITE; - } else { - showPopup(); - } - } else if (ev->key() == Qt::Key_Escape) { - view->hide(); - } -} - -void DiveLocationLineEdit::fixPopupPosition() -{ - const QRect screen = QApplication::desktop()->availableGeometry(this); - const int maxVisibleItems = 5; - Qt::LayoutDirection dir = layoutDirection(); - QPoint pos; - int rh, w; - int h = (view->sizeHintForRow(0) * qMin(maxVisibleItems, view->model()->rowCount()) + 3) + 3; - QScrollBar *hsb = view->horizontalScrollBar(); - if (hsb && hsb->isVisible()) - h += view->horizontalScrollBar()->sizeHint().height(); - - rh = height(); - pos = mapToGlobal(QPoint(0, height() - 2)); - w = width(); - - if (w > screen.width()) - w = screen.width(); - if ((pos.x() + w) > (screen.x() + screen.width())) - pos.setX(screen.x() + screen.width() - w); - if (pos.x() < screen.x()) - pos.setX(screen.x()); - - int top = pos.y() - rh - screen.top() + 2; - int bottom = screen.bottom() - pos.y(); - h = qMax(h, view->minimumHeight()); - if (h > bottom) { - h = qMin(qMax(top, bottom), h); - if (top > bottom) - pos.setY(pos.y() - h - rh + 2); - } - - view->setGeometry(pos.x(), pos.y(), w, h); - if (!view->currentIndex().isValid() && view->model()->rowCount()) { - view->setCurrentIndex(view->model()->index(0, 0)); - } -} - -void DiveLocationLineEdit::setCurrentDiveSiteUuid(uint32_t uuid) -{ - currUuid = uuid; - if (uuid == 0) { - currType = NO_DIVE_SITE; - } - struct dive_site *ds = get_dive_site_by_uuid(uuid); - if (!ds) - clear(); - else - setText(ds->name); -} - -void DiveLocationLineEdit::showPopup() -{ - fixPopupPosition(); - if (!view->isVisible()) { - setTemporaryDiveSiteName(text()); - proxy->invalidate(); - view->show(); - } -} - -DiveLocationLineEdit::DiveSiteType DiveLocationLineEdit::currDiveSiteType() const -{ - return currType; -} - -uint32_t DiveLocationLineEdit::currDiveSiteUuid() const -{ - return currUuid; -} - -DiveLocationListView::DiveLocationListView(QWidget *parent) -{ -} - -void DiveLocationListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) -{ - QListView::currentChanged(current, previous); - emit currentIndexChanged(current); -} diff --git a/qt-ui/locationinformation.h b/qt-ui/locationinformation.h deleted file mode 100644 index 243df939b..000000000 --- a/qt-ui/locationinformation.h +++ /dev/null @@ -1,114 +0,0 @@ -#ifndef LOCATIONINFORMATION_H -#define LOCATIONINFORMATION_H - -#include "ui_locationInformation.h" -#include -#include -#include - -class LocationInformationWidget : public QGroupBox { -Q_OBJECT -public: - LocationInformationWidget(QWidget *parent = 0); - virtual bool eventFilter(QObject*, QEvent*); - -protected: - void showEvent(QShowEvent *); - -public slots: - void acceptChanges(); - void rejectChanges(); - void updateGpsCoordinates(); - void markChangedWidget(QWidget *w); - void enableEdition(); - void resetState(); - void resetPallete(); - void on_diveSiteCoordinates_textChanged(const QString& text); - void on_diveSiteDescription_textChanged(const QString& text); - void on_diveSiteName_textChanged(const QString& text); - void on_diveSiteNotes_textChanged(); - void reverseGeocode(); - void mergeSelectedDiveSites(); -private slots: - void updateLabels(); -signals: - void startEditDiveSite(uint32_t uuid); - void endEditDiveSite(); - void coordinatesChanged(); - void startFilterDiveSite(uint32_t uuid); - void stopFilterDiveSite(); - void requestCoordinates(); - void endRequestCoordinates(); - -private: - Ui::LocationInformation ui; - bool modified; - QAction *acceptAction, *rejectAction; -}; - -class DiveLocationFilterProxyModel : public QSortFilterProxyModel { - Q_OBJECT -public: - DiveLocationFilterProxyModel(QObject *parent = 0); - virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; - virtual bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const; -}; - -class DiveLocationModel : public QAbstractTableModel { - Q_OBJECT -public: - enum columns{UUID, NAME, LATITUDE, LONGITUDE, DESCRIPTION, NOTES, COLUMNS}; - DiveLocationModel(QObject *o = 0); - void resetModel(); - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; - int rowCount(const QModelIndex& parent = QModelIndex()) const; - int columnCount(const QModelIndex& parent = QModelIndex()) const; - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); -private: - QString new_ds_value[2]; -}; - -class DiveLocationListView : public QListView { - Q_OBJECT -public: - DiveLocationListView(QWidget *parent = 0); -protected: - virtual void currentChanged(const QModelIndex& current, const QModelIndex& previous); -signals: - void currentIndexChanged(const QModelIndex& current); -}; - -class DiveLocationLineEdit : public QLineEdit { - Q_OBJECT -public: - enum DiveSiteType { NO_DIVE_SITE, NEW_DIVE_SITE, EXISTING_DIVE_SITE }; - DiveLocationLineEdit(QWidget *parent =0 ); - void refreshDiveSiteCache(); - void setTemporaryDiveSiteName(const QString& s); - bool eventFilter(QObject*, QEvent*); - void itemActivated(const QModelIndex& index); - DiveSiteType currDiveSiteType() const; - uint32_t currDiveSiteUuid() const; - void fixPopupPosition(); - void setCurrentDiveSiteUuid(uint32_t uuid); - -signals: - void diveSiteSelected(uint32_t uuid); - void entered(const QModelIndex& index); - void currentChanged(const QModelIndex& index); - -protected: - void keyPressEvent(QKeyEvent *ev); - void focusOutEvent(QFocusEvent *ev); - void showPopup(); - -private: - using QLineEdit::setText; - DiveLocationFilterProxyModel *proxy; - DiveLocationModel *model; - DiveLocationListView *view; - DiveSiteType currType; - uint32_t currUuid; -}; - -#endif diff --git a/qt-ui/maintab.cpp b/qt-ui/maintab.cpp deleted file mode 100644 index 0afb7b4c0..000000000 --- a/qt-ui/maintab.cpp +++ /dev/null @@ -1,1612 +0,0 @@ -/* - * maintab.cpp - * - * classes for the "notebook" area of the main window of Subsurface - * - */ -#include "maintab.h" -#include "mainwindow.h" -#include "globe.h" -#include "helpers.h" -#include "statistics.h" -#include "modeldelegates.h" -#include "diveplannermodel.h" -#include "divelistview.h" -#include "display.h" -#include "profile/profilewidget2.h" -#include "diveplanner.h" -#include "divesitehelpers.h" -#include "cylindermodel.h" -#include "weightmodel.h" -#include "divepicturemodel.h" -#include "divecomputerextradatamodel.h" -#include "divelocationmodel.h" -#include "divesite.h" -#include "locationinformation.h" -#include "divesite.h" - -#include -#include -#include -#include -#include -#include -#include - -MainTab::MainTab(QWidget *parent) : QTabWidget(parent), - weightModel(new WeightModel(this)), - cylindersModel(CylindersModel::instance()), - extraDataModel(new ExtraDataModel(this)), - editMode(NONE), - divePictureModel(DivePictureModel::instance()), - copyPaste(false), - currentTrip(0) -{ - ui.setupUi(this); - ui.dateEdit->setDisplayFormat(getDateFormat()); - - memset(&displayed_dive, 0, sizeof(displayed_dive)); - memset(&displayedTrip, 0, sizeof(displayedTrip)); - - ui.cylinders->setModel(cylindersModel); - ui.weights->setModel(weightModel); - ui.photosView->setModel(divePictureModel); - connect(ui.photosView, SIGNAL(photoDoubleClicked(QString)), this, SLOT(photoDoubleClicked(QString))); - ui.extraData->setModel(extraDataModel); - closeMessage(); - - connect(ui.editDiveSiteButton, SIGNAL(clicked()), MainWindow::instance(), SIGNAL(startDiveSiteEdit())); -#ifndef NO_MARBLE - connect(ui.location, &DiveLocationLineEdit::entered, GlobeGPS::instance(), &GlobeGPS::centerOnIndex); - connect(ui.location, &DiveLocationLineEdit::currentChanged, GlobeGPS::instance(), &GlobeGPS::centerOnIndex); -#endif - - QAction *action = new QAction(tr("Apply changes"), this); - connect(action, SIGNAL(triggered(bool)), this, SLOT(acceptChanges())); - addMessageAction(action); - - action = new QAction(tr("Discard changes"), this); - connect(action, SIGNAL(triggered(bool)), this, SLOT(rejectChanges())); - addMessageAction(action); - - QShortcut *closeKey = new QShortcut(QKeySequence(Qt::Key_Escape), this); - connect(closeKey, SIGNAL(activated()), this, SLOT(escDetected())); - - if (qApp->style()->objectName() == "oxygen") - setDocumentMode(true); - else - setDocumentMode(false); - - // we start out with the fields read-only; once things are - // filled from a dive, they are made writeable - setEnabled(false); - - Q_FOREACH (QObject *obj, ui.statisticsTab->children()) { - QLabel *label = qobject_cast(obj); - if (label) - label->setAlignment(Qt::AlignHCenter); - } - ui.cylinders->setTitle(tr("Cylinders")); - ui.cylinders->setBtnToolTip(tr("Add cylinder")); - connect(ui.cylinders, SIGNAL(addButtonClicked()), this, SLOT(addCylinder_clicked())); - - ui.weights->setTitle(tr("Weights")); - ui.weights->setBtnToolTip(tr("Add weight system")); - connect(ui.weights, SIGNAL(addButtonClicked()), this, SLOT(addWeight_clicked())); - - // This needs to be the same order as enum dive_comp_type in dive.h! - ui.DiveType->insertItems(0, QStringList() << tr("OC") << tr("CCR") << tr("pSCR") << tr("Freedive")); - connect(ui.DiveType, SIGNAL(currentIndexChanged(int)), this, SLOT(divetype_Changed(int))); - - connect(ui.cylinders->view(), SIGNAL(clicked(QModelIndex)), this, SLOT(editCylinderWidget(QModelIndex))); - connect(ui.weights->view(), SIGNAL(clicked(QModelIndex)), this, SLOT(editWeightWidget(QModelIndex))); - - ui.cylinders->view()->setItemDelegateForColumn(CylindersModel::TYPE, new TankInfoDelegate(this)); - ui.cylinders->view()->setItemDelegateForColumn(CylindersModel::USE, new TankUseDelegate(this)); - ui.weights->view()->setItemDelegateForColumn(WeightModel::TYPE, new WSInfoDelegate(this)); - ui.cylinders->view()->setColumnHidden(CylindersModel::DEPTH, true); - completers.buddy = new QCompleter(&buddyModel, ui.buddy); - completers.divemaster = new QCompleter(&diveMasterModel, ui.divemaster); - completers.suit = new QCompleter(&suitModel, ui.suit); - completers.tags = new QCompleter(&tagModel, ui.tagWidget); - completers.buddy->setCaseSensitivity(Qt::CaseInsensitive); - completers.divemaster->setCaseSensitivity(Qt::CaseInsensitive); - completers.suit->setCaseSensitivity(Qt::CaseInsensitive); - completers.tags->setCaseSensitivity(Qt::CaseInsensitive); - ui.buddy->setCompleter(completers.buddy); - ui.divemaster->setCompleter(completers.divemaster); - ui.suit->setCompleter(completers.suit); - ui.tagWidget->setCompleter(completers.tags); - ui.diveNotesMessage->hide(); - ui.diveEquipmentMessage->hide(); - ui.diveInfoMessage->hide(); - ui.diveStatisticsMessage->hide(); - setMinimumHeight(0); - setMinimumWidth(0); - - // Current display of things on Gnome3 looks like shit, so - // let`s fix that. - if (isGnome3Session()) { - QPalette p; - p.setColor(QPalette::Window, QColor(Qt::white)); - ui.scrollArea->viewport()->setPalette(p); - ui.scrollArea_2->viewport()->setPalette(p); - ui.scrollArea_3->viewport()->setPalette(p); - ui.scrollArea_4->viewport()->setPalette(p); - - // GroupBoxes in Gnome3 looks like I'v drawn them... - static const QString gnomeCss( - "QGroupBox {" - " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," - " stop: 0 #E0E0E0, stop: 1 #FFFFFF);" - " border: 2px solid gray;" - " border-radius: 5px;" - " margin-top: 1ex;" - "}" - "QGroupBox::title {" - " subcontrol-origin: margin;" - " subcontrol-position: top center;" - " padding: 0 3px;" - " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," - " stop: 0 #E0E0E0, stop: 1 #FFFFFF);" - "}"); - Q_FOREACH (QGroupBox *box, findChildren()) { - box->setStyleSheet(gnomeCss); - } - } - // QLineEdit and QLabels should have minimal margin on the left and right but not waste vertical space - QMargins margins(3, 2, 1, 0); - Q_FOREACH (QLabel *label, findChildren()) { - label->setContentsMargins(margins); - } - ui.cylinders->view()->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu); - ui.weights->view()->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu); - - QSettings s; - s.beginGroup("cylinders_dialog"); - for (int i = 0; i < CylindersModel::COLUMNS; i++) { - if ((i == CylindersModel::REMOVE) || (i == CylindersModel::TYPE)) - continue; - bool checked = s.value(QString("column%1_hidden").arg(i)).toBool(); - action = new QAction(cylindersModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(), ui.cylinders->view()); - action->setCheckable(true); - action->setData(i); - action->setChecked(!checked); - connect(action, SIGNAL(triggered(bool)), this, SLOT(toggleTriggeredColumn())); - ui.cylinders->view()->setColumnHidden(i, checked); - ui.cylinders->view()->horizontalHeader()->addAction(action); - } - - QAction *deletePhoto = new QAction(this); - deletePhoto->setShortcut(Qt::Key_Delete); - deletePhoto->setShortcutContext(Qt::WidgetShortcut); - ui.photosView->addAction(deletePhoto); - ui.photosView->setSelectionMode(QAbstractItemView::SingleSelection); - connect(deletePhoto, SIGNAL(triggered(bool)), this, SLOT(removeSelectedPhotos())); - - ui.waitingSpinner->setRoundness(70.0); - ui.waitingSpinner->setMinimumTrailOpacity(15.0); - ui.waitingSpinner->setTrailFadePercentage(70.0); - ui.waitingSpinner->setNumberOfLines(8); - ui.waitingSpinner->setLineLength(5); - ui.waitingSpinner->setLineWidth(3); - ui.waitingSpinner->setInnerRadius(5); - ui.waitingSpinner->setRevolutionsPerSecond(1); - - connect(ReverseGeoLookupThread::instance(), SIGNAL(finished()), - LocationInformationModel::instance(), SLOT(update())); - - connect(ReverseGeoLookupThread::instance(), &QThread::finished, - this, &MainTab::setCurrentLocationIndex); - - connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished, - ui.location, &DiveLocationLineEdit::fixPopupPosition); - - acceptingEdit = false; - - ui.diveTripLocation->hide(); -} - -MainTab::~MainTab() -{ - QSettings s; - s.beginGroup("cylinders_dialog"); - for (int i = 0; i < CylindersModel::COLUMNS; i++) { - if ((i == CylindersModel::REMOVE) || (i == CylindersModel::TYPE)) - continue; - s.setValue(QString("column%1_hidden").arg(i), ui.cylinders->view()->isColumnHidden(i)); - } -} - -void MainTab::setCurrentLocationIndex() -{ - if (current_dive) { - struct dive_site *ds = get_dive_site_by_uuid(current_dive->dive_site_uuid); - if (ds) - ui.location->setCurrentDiveSiteUuid(ds->uuid); - else - ui.location->clear(); - } -} - -void MainTab::enableGeoLookupEdition() -{ - ui.waitingSpinner->stop(); -} - -void MainTab::disableGeoLookupEdition() -{ - ui.waitingSpinner->start(); -} - -void MainTab::toggleTriggeredColumn() -{ - QAction *action = qobject_cast(sender()); - int col = action->data().toInt(); - QTableView *view = ui.cylinders->view(); - - if (action->isChecked()) { - view->showColumn(col); - if (view->columnWidth(col) <= 15) - view->setColumnWidth(col, 80); - } else - view->hideColumn(col); -} - -void MainTab::addDiveStarted() -{ - enableEdition(ADD); -} - -void MainTab::addMessageAction(QAction *action) -{ - ui.diveEquipmentMessage->addAction(action); - ui.diveNotesMessage->addAction(action); - ui.diveInfoMessage->addAction(action); - ui.diveStatisticsMessage->addAction(action); -} - -void MainTab::hideMessage() -{ - ui.diveNotesMessage->animatedHide(); - ui.diveEquipmentMessage->animatedHide(); - ui.diveInfoMessage->animatedHide(); - ui.diveStatisticsMessage->animatedHide(); - updateTextLabels(false); -} - -void MainTab::closeMessage() -{ - hideMessage(); - ui.diveNotesMessage->setCloseButtonVisible(false); - ui.diveEquipmentMessage->setCloseButtonVisible(false); - ui.diveInfoMessage->setCloseButtonVisible(false); - ui.diveStatisticsMessage->setCloseButtonVisible(false); -} - -void MainTab::displayMessage(QString str) -{ - ui.diveNotesMessage->setCloseButtonVisible(false); - ui.diveEquipmentMessage->setCloseButtonVisible(false); - ui.diveInfoMessage->setCloseButtonVisible(false); - ui.diveStatisticsMessage->setCloseButtonVisible(false); - ui.diveNotesMessage->setText(str); - ui.diveNotesMessage->animatedShow(); - ui.diveEquipmentMessage->setText(str); - ui.diveEquipmentMessage->animatedShow(); - ui.diveInfoMessage->setText(str); - ui.diveInfoMessage->animatedShow(); - ui.diveStatisticsMessage->setText(str); - ui.diveStatisticsMessage->animatedShow(); - updateTextLabels(); -} - -void MainTab::updateTextLabels(bool showUnits) -{ - if (showUnits) { - ui.airTempLabel->setText(tr("Air temp. [%1]").arg(get_temp_unit())); - ui.waterTempLabel->setText(tr("Water temp. [%1]").arg(get_temp_unit())); - } else { - ui.airTempLabel->setText(tr("Air temp.")); - ui.waterTempLabel->setText(tr("Water temp.")); - } -} - -void MainTab::enableEdition(EditMode newEditMode) -{ - const bool isTripEdit = MainWindow::instance() && - MainWindow::instance()->dive_list()->selectedTrips().count() == 1; - - if (((newEditMode == DIVE || newEditMode == NONE) && current_dive == NULL) || editMode != NONE) - return; - modified = false; - copyPaste = false; - if ((newEditMode == DIVE || newEditMode == NONE) && - !isTripEdit && - current_dive->dc.model && - strcmp(current_dive->dc.model, "manually added dive") == 0) { - // editCurrentDive will call enableEdition with newEditMode == MANUALLY_ADDED_DIVE - // so exit this function here after editCurrentDive() returns - - - - // FIXME : can we get rid of this recursive crap? - - - - MainWindow::instance()->editCurrentDive(); - return; - } - - ui.editDiveSiteButton->setEnabled(false); - MainWindow::instance()->dive_list()->setEnabled(false); - MainWindow::instance()->setEnabledToolbar(false); - - if (isTripEdit) { - // we are editing trip location and notes - displayMessage(tr("This trip is being edited.")); - currentTrip = current_dive->divetrip; - ui.dateEdit->setEnabled(false); - editMode = TRIP; - } else { - ui.dateEdit->setEnabled(true); - if (amount_selected > 1) { - displayMessage(tr("Multiple dives are being edited.")); - } else { - displayMessage(tr("This dive is being edited.")); - } - editMode = newEditMode != NONE ? newEditMode : DIVE; - } -} - -void MainTab::clearEquipment() -{ - cylindersModel->clear(); - weightModel->clear(); -} - -void MainTab::nextInputField(QKeyEvent *event) -{ - keyPressEvent(event); -} - -void MainTab::clearInfo() -{ - ui.sacText->clear(); - ui.otuText->clear(); - ui.maxcnsText->clear(); - ui.oxygenHeliumText->clear(); - ui.gasUsedText->clear(); - ui.dateText->clear(); - ui.diveTimeText->clear(); - ui.surfaceIntervalText->clear(); - ui.maximumDepthText->clear(); - ui.averageDepthText->clear(); - ui.waterTemperatureText->clear(); - ui.airTemperatureText->clear(); - ui.airPressureText->clear(); - ui.salinityText->clear(); - ui.tagWidget->clear(); -} - -void MainTab::clearStats() -{ - ui.depthLimits->clear(); - ui.sacLimits->clear(); - ui.divesAllText->clear(); - ui.tempLimits->clear(); - ui.totalTimeAllText->clear(); - ui.timeLimits->clear(); -} - -#define UPDATE_TEXT(d, field) \ - if (clear || !d.field) \ - ui.field->setText(QString()); \ - else \ - ui.field->setText(d.field) - -#define UPDATE_TEMP(d, field) \ - if (clear || d.field.mkelvin == 0) \ - ui.field->setText(""); \ - else \ - ui.field->setText(get_temperature_string(d.field, true)) - -bool MainTab::isEditing() -{ - return editMode != NONE; -} - -void MainTab::showLocation() -{ - if (get_dive_site_by_uuid(displayed_dive.dive_site_uuid)) - ui.location->setCurrentDiveSiteUuid(displayed_dive.dive_site_uuid); - else - ui.location->clear(); -} - -// Seems wrong, since we can also call updateDiveInfo(), but since the updateDiveInfo -// has a parameter on it's definition it didn't worked on the signal slot connection. -void MainTab::refreshDiveInfo() -{ - updateDiveInfo(); -} - -void MainTab::updateDiveInfo(bool clear) -{ - ui.location->refreshDiveSiteCache(); - EditMode rememberEM = editMode; - // don't execute this while adding / planning a dive - if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE || MainWindow::instance()->graphics()->isPlanner()) - return; - if (!isEnabled() && !clear ) - setEnabled(true); - if (isEnabled() && clear) - setEnabled(false); - editMode = IGNORE; // don't trigger on changes to the widgets - - // This method updates ALL tabs whenever a new dive or trip is - // selected. - // If exactly one trip has been selected, we show the location / notes - // for the trip in the Info tab, otherwise we show the info of the - // selected_dive - temperature_t temp; - struct dive *prevd; - char buf[1024]; - - process_selected_dives(); - process_all_dives(&displayed_dive, &prevd); - - divePictureModel->updateDivePictures(); - - ui.notes->setText(QString()); - if (!clear) { - QString tmp(displayed_dive.notes); - if (tmp.indexOf("setHtml(tmp); - else - ui.notes->setPlainText(tmp); - } - UPDATE_TEXT(displayed_dive, notes); - UPDATE_TEXT(displayed_dive, suit); - UPDATE_TEXT(displayed_dive, divemaster); - UPDATE_TEXT(displayed_dive, buddy); - UPDATE_TEMP(displayed_dive, airtemp); - UPDATE_TEMP(displayed_dive, watertemp); - ui.DiveType->setCurrentIndex(get_dive_dc(&displayed_dive, dc_number)->divemode); - - if (!clear) { - struct dive_site *ds = NULL; - // if we are showing a dive and editing it, let's refer to the displayed_dive_site as that - // already may contain changes, otherwise start with the dive site referred to by the displayed - // dive - if (rememberEM == DIVE) { - ds = &displayed_dive_site; - } else { - ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); - if (ds) - copy_dive_site(ds, &displayed_dive_site); - } - - if (ds) { - ui.location->setCurrentDiveSiteUuid(ds->uuid); - ui.locationTags->setText(constructLocationTags(ds->uuid)); - } else { - ui.location->clear(); - clear_dive_site(&displayed_dive_site); - } - - // Subsurface always uses "local time" as in "whatever was the local time at the location" - // so all time stamps have no time zone information and are in UTC - QDateTime localTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); - localTime.setTimeSpec(Qt::UTC); - ui.dateEdit->setDate(localTime.date()); - ui.timeEdit->setTime(localTime.time()); - if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { - setTabText(0, tr("Trip notes")); - currentTrip = *MainWindow::instance()->dive_list()->selectedTrips().begin(); - // only use trip relevant fields - ui.divemaster->setVisible(false); - ui.DivemasterLabel->setVisible(false); - ui.buddy->setVisible(false); - ui.BuddyLabel->setVisible(false); - ui.suit->setVisible(false); - ui.SuitLabel->setVisible(false); - ui.rating->setVisible(false); - ui.RatingLabel->setVisible(false); - ui.visibility->setVisible(false); - ui.visibilityLabel->setVisible(false); - ui.tagWidget->setVisible(false); - ui.TagLabel->setVisible(false); - ui.airTempLabel->setVisible(false); - ui.airtemp->setVisible(false); - ui.DiveType->setVisible(false); - ui.TypeLabel->setVisible(false); - ui.waterTempLabel->setVisible(false); - ui.watertemp->setVisible(false); - ui.diveTripLocation->show(); - ui.location->hide(); - ui.editDiveSiteButton->hide(); - // rename the remaining fields and fill data from selected trip - ui.LocationLabel->setText(tr("Trip location")); - ui.diveTripLocation->setText(currentTrip->location); - ui.locationTags->clear(); - //TODO: Fix this. - //ui.location->setText(currentTrip->location); - ui.NotesLabel->setText(tr("Trip notes")); - ui.notes->setText(currentTrip->notes); - clearEquipment(); - ui.equipmentTab->setEnabled(false); - } else { - setTabText(0, tr("Notes")); - currentTrip = NULL; - // make all the fields visible writeable - ui.diveTripLocation->hide(); - ui.location->show(); - ui.editDiveSiteButton->show(); - ui.divemaster->setVisible(true); - ui.buddy->setVisible(true); - ui.suit->setVisible(true); - ui.SuitLabel->setVisible(true); - ui.rating->setVisible(true); - ui.RatingLabel->setVisible(true); - ui.visibility->setVisible(true); - ui.visibilityLabel->setVisible(true); - ui.BuddyLabel->setVisible(true); - ui.DivemasterLabel->setVisible(true); - ui.TagLabel->setVisible(true); - ui.tagWidget->setVisible(true); - ui.airTempLabel->setVisible(true); - ui.airtemp->setVisible(true); - ui.TypeLabel->setVisible(true); - ui.DiveType->setVisible(true); - ui.waterTempLabel->setVisible(true); - ui.watertemp->setVisible(true); - /* and fill them from the dive */ - ui.rating->setCurrentStars(displayed_dive.rating); - ui.visibility->setCurrentStars(displayed_dive.visibility); - // reset labels in case we last displayed trip notes - ui.LocationLabel->setText(tr("Location")); - ui.NotesLabel->setText(tr("Notes")); - ui.equipmentTab->setEnabled(true); - cylindersModel->updateDive(); - weightModel->updateDive(); - extraDataModel->updateDive(); - taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); - ui.tagWidget->setText(QString(buf)); - } - ui.maximumDepthText->setText(get_depth_string(displayed_dive.maxdepth, true)); - ui.averageDepthText->setText(get_depth_string(displayed_dive.meandepth, true)); - ui.maxcnsText->setText(QString("%1\%").arg(displayed_dive.maxcns)); - ui.otuText->setText(QString("%1").arg(displayed_dive.otu)); - ui.waterTemperatureText->setText(get_temperature_string(displayed_dive.watertemp, true)); - ui.airTemperatureText->setText(get_temperature_string(displayed_dive.airtemp, true)); - ui.DiveType->setCurrentIndex(get_dive_dc(&displayed_dive, dc_number)->divemode); - - volume_t gases[MAX_CYLINDERS] = {}; - get_gas_used(&displayed_dive, gases); - QString volumes; - int mean[MAX_CYLINDERS], duration[MAX_CYLINDERS]; - per_cylinder_mean_depth(&displayed_dive, select_dc(&displayed_dive), mean, duration); - volume_t sac; - QString gaslist, SACs, separator; - - gaslist = ""; SACs = ""; volumes = ""; separator = ""; - for (int i = 0; i < MAX_CYLINDERS; i++) { - if (!is_cylinder_used(&displayed_dive, i)) - continue; - gaslist.append(separator); volumes.append(separator); SACs.append(separator); - separator = "\n"; - - gaslist.append(gasname(&displayed_dive.cylinder[i].gasmix)); - if (!gases[i].mliter) - continue; - volumes.append(get_volume_string(gases[i], true)); - if (duration[i]) { - sac.mliter = gases[i].mliter / (depth_to_atm(mean[i], &displayed_dive) * duration[i] / 60); - SACs.append(get_volume_string(sac, true).append(tr("/min"))); - } - } - ui.gasUsedText->setText(volumes); - ui.oxygenHeliumText->setText(gaslist); - ui.dateText->setText(get_short_dive_date_string(displayed_dive.when)); - if (displayed_dive.dc.divemode != FREEDIVE) - ui.diveTimeText->setText(get_time_string_s(displayed_dive.duration.seconds + 30, 0, false)); - else - ui.diveTimeText->setText(get_time_string_s(displayed_dive.duration.seconds, 0, true)); - if (prevd) - ui.surfaceIntervalText->setText(get_time_string_s(displayed_dive.when - (prevd->when + prevd->duration.seconds), 4, - (displayed_dive.dc.divemode == FREEDIVE))); - else - ui.surfaceIntervalText->clear(); - if (mean[0]) - ui.sacText->setText(SACs); - else - ui.sacText->clear(); - if (displayed_dive.surface_pressure.mbar) - /* this is ALWAYS displayed in mbar */ - ui.airPressureText->setText(QString("%1mbar").arg(displayed_dive.surface_pressure.mbar)); - else - ui.airPressureText->clear(); - if (displayed_dive.salinity) - ui.salinityText->setText(QString("%1g/l").arg(displayed_dive.salinity / 10.0)); - else - ui.salinityText->clear(); - ui.depthLimits->setMaximum(get_depth_string(stats_selection.max_depth, true)); - ui.depthLimits->setMinimum(get_depth_string(stats_selection.min_depth, true)); - // the overall average depth is really confusing when listed between the - // deepest and shallowest dive - let's just not set it - // ui.depthLimits->setAverage(get_depth_string(stats_selection.avg_depth, true)); - ui.depthLimits->overrideMaxToolTipText(tr("Deepest dive")); - ui.depthLimits->overrideMinToolTipText(tr("Shallowest dive")); - if (amount_selected > 1 && stats_selection.max_sac.mliter) - ui.sacLimits->setMaximum(get_volume_string(stats_selection.max_sac, true).append(tr("/min"))); - else - ui.sacLimits->setMaximum(""); - if (amount_selected > 1 && stats_selection.min_sac.mliter) - ui.sacLimits->setMinimum(get_volume_string(stats_selection.min_sac, true).append(tr("/min"))); - else - ui.sacLimits->setMinimum(""); - if (stats_selection.avg_sac.mliter) - ui.sacLimits->setAverage(get_volume_string(stats_selection.avg_sac, true).append(tr("/min"))); - else - ui.sacLimits->setAverage(""); - ui.sacLimits->overrideMaxToolTipText(tr("Highest total SAC of a dive")); - ui.sacLimits->overrideMinToolTipText(tr("Lowest total SAC of a dive")); - ui.sacLimits->overrideAvgToolTipText(tr("Average total SAC of all selected dives")); - ui.divesAllText->setText(QString::number(stats_selection.selection_size)); - temp.mkelvin = stats_selection.max_temp; - ui.tempLimits->setMaximum(get_temperature_string(temp, true)); - temp.mkelvin = stats_selection.min_temp; - ui.tempLimits->setMinimum(get_temperature_string(temp, true)); - if (stats_selection.combined_temp && stats_selection.combined_count) { - const char *unit; - get_temp_units(0, &unit); - ui.tempLimits->setAverage(QString("%1%2").arg(stats_selection.combined_temp / stats_selection.combined_count, 0, 'f', 1).arg(unit)); - } - ui.tempLimits->overrideMaxToolTipText(tr("Highest temperature")); - ui.tempLimits->overrideMinToolTipText(tr("Lowest temperature")); - ui.tempLimits->overrideAvgToolTipText(tr("Average temperature of all selected dives")); - ui.totalTimeAllText->setText(get_time_string_s(stats_selection.total_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); - int seconds = stats_selection.total_time.seconds; - if (stats_selection.selection_size) - seconds /= stats_selection.selection_size; - ui.timeLimits->setAverage(get_time_string_s(seconds, 0,(displayed_dive.dc.divemode == FREEDIVE))); - if (amount_selected > 1) { - ui.timeLimits->setMaximum(get_time_string_s(stats_selection.longest_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); - ui.timeLimits->setMinimum(get_time_string_s(stats_selection.shortest_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); - } - ui.timeLimits->overrideMaxToolTipText(tr("Longest dive")); - ui.timeLimits->overrideMinToolTipText(tr("Shortest dive")); - ui.timeLimits->overrideAvgToolTipText(tr("Average length of all selected dives")); - // now let's get some gas use statistics - QVector > gasUsed; - QString gasUsedString; - volume_t vol; - selectedDivesGasUsed(gasUsed); - for (int j = 0; j < 20; j++) { - if (gasUsed.isEmpty()) - break; - QPair gasPair = gasUsed.last(); - gasUsed.pop_back(); - vol.mliter = gasPair.second; - gasUsedString.append(gasPair.first).append(": ").append(get_volume_string(vol, true)).append("\n"); - } - if (!gasUsed.isEmpty()) - gasUsedString.append("..."); - volume_t o2_tot = {}, he_tot = {}; - selected_dives_gas_parts(&o2_tot, &he_tot); - - /* No need to show the gas mixing information if diving - * with pure air, and only display the he / O2 part when - * it is used. - */ - if (he_tot.mliter || o2_tot.mliter) { - gasUsedString.append(tr("These gases could be\nmixed from Air and using:\n")); - if (he_tot.mliter) - gasUsedString.append(QString("He: %1").arg(get_volume_string(he_tot, true))); - if (he_tot.mliter && o2_tot.mliter) - gasUsedString.append(tr(" and ")); - if (o2_tot.mliter) - gasUsedString.append(QString("O2: %2\n").arg(get_volume_string(o2_tot, true))); - } - ui.gasConsumption->setText(gasUsedString); - if(ui.locationTags->text().isEmpty()) - ui.locationTags->hide(); - else - ui.locationTags->show(); - /* unset the special value text for date and time, just in case someone dove at midnight */ - ui.dateEdit->setSpecialValueText(QString("")); - ui.timeEdit->setSpecialValueText(QString("")); - - } else { - /* clear the fields */ - clearInfo(); - clearStats(); - clearEquipment(); - ui.rating->setCurrentStars(0); - ui.visibility->setCurrentStars(0); - ui.location->clear(); - /* set date and time to minimums which triggers showing the special value text */ - ui.dateEdit->setSpecialValueText(QString("-")); - ui.dateEdit->setMinimumDate(QDate(1, 1, 1)); - ui.dateEdit->setDate(QDate(1, 1, 1)); - ui.timeEdit->setSpecialValueText(QString("-")); - ui.timeEdit->setMinimumTime(QTime(0, 0, 0, 0)); - ui.timeEdit->setTime(QTime(0, 0, 0, 0)); - } - editMode = rememberEM; - ui.cylinders->view()->hideColumn(CylindersModel::DEPTH); - if (get_dive_dc(&displayed_dive, dc_number)->divemode == CCR) - ui.cylinders->view()->showColumn(CylindersModel::USE); - else - ui.cylinders->view()->hideColumn(CylindersModel::USE); - - if (verbose) - qDebug() << "Set the current dive site:" << displayed_dive.dive_site_uuid; - emit diveSiteChanged(get_dive_site_by_uuid(displayed_dive.dive_site_uuid)); -} - -void MainTab::addCylinder_clicked() -{ - if (editMode == NONE) - enableEdition(); - cylindersModel->add(); -} - -void MainTab::addWeight_clicked() -{ - if (editMode == NONE) - enableEdition(); - weightModel->add(); -} - -void MainTab::reload() -{ - suitModel.updateModel(); - buddyModel.updateModel(); - diveMasterModel.updateModel(); - tagModel.updateModel(); - LocationInformationModel::instance()->update(); -} - -// tricky little macro to edit all the selected dives -// loop over all dives, for each selected dive do WHAT, but do it -// last for the current dive; this is required in case the invocation -// wants to compare things to the original value in current_dive like it should -#define MODIFY_SELECTED_DIVES(WHAT) \ - do { \ - struct dive *mydive = NULL; \ - int _i; \ - for_each_dive (_i, mydive) { \ - if (!mydive->selected || mydive == cd) \ - continue; \ - \ - WHAT; \ - } \ - mydive = cd; \ - WHAT; \ - mark_divelist_changed(true); \ - } while (0) - -#define EDIT_TEXT(what) \ - if (same_string(mydive->what, cd->what) || copyPaste) { \ - free(mydive->what); \ - mydive->what = copy_string(displayed_dive.what); \ - } - -MainTab::EditMode MainTab::getEditMode() const -{ - return editMode; -} - -#define EDIT_VALUE(what) \ - if (mydive->what == cd->what || copyPaste) { \ - mydive->what = displayed_dive.what; \ - } - -void MainTab::refreshDisplayedDiveSite() -{ - if (displayed_dive_site.uuid) { - copy_dive_site(get_dive_site_by_uuid(displayed_dive_site.uuid), &displayed_dive_site); - ui.location->setCurrentDiveSiteUuid(displayed_dive_site.uuid); - } -} - -// when this is called we already have updated the current_dive and know that it exists -// there is no point in calling this function if there is no current dive -uint32_t MainTab::updateDiveSite(uint32_t pickedUuid, int divenr) -{ - struct dive *cd = get_dive(divenr); - if (!cd) - return 0; - - if (ui.location->text().isEmpty()) - return 0; - - if (pickedUuid == 0) - return 0; - - const uint32_t origUuid = cd->dive_site_uuid; - struct dive_site *origDs = get_dive_site_by_uuid(origUuid); - struct dive_site *newDs = NULL; - bool createdNewDive = false; - - if (pickedUuid == origUuid) - return origUuid; - - if (pickedUuid == RECENTLY_ADDED_DIVESITE) { - pickedUuid = create_dive_site(ui.location->text().isEmpty() ? qPrintable(tr("New dive site")) : qPrintable(ui.location->text()), displayed_dive.when); - createdNewDive = true; - } - - newDs = get_dive_site_by_uuid(pickedUuid); - - // Copy everything from the displayed_dive_site, so we have the latitude, longitude, notes, etc. - // The user *might* be using wrongly the 'choose dive site' just to edit the name of it, sigh. - if (origDs) { - if(createdNewDive) { - copy_dive_site(origDs, newDs); - free(newDs->name); - newDs->name = copy_string(qPrintable(ui.location->text().constData())); - newDs->uuid = pickedUuid; - qDebug() << "Creating and copying dive site"; - } else if (newDs->latitude.udeg == 0 && newDs->longitude.udeg == 0) { - newDs->latitude.udeg = origDs->latitude.udeg; - newDs->longitude.udeg = origDs->longitude.udeg; - qDebug() << "Copying GPS information"; - } - } - - if (origDs && pickedUuid != origDs->uuid && same_string(origDs->notes, "SubsurfaceWebservice")) { - if (!is_dive_site_used(origDs->uuid, false)) { - if (verbose) - qDebug() << "delete the autogenerated dive site" << origDs->name; - delete_dive_site(origDs->uuid); - } - } - - cd->dive_site_uuid = pickedUuid; - qDebug() << "Setting the dive site id on the dive:" << pickedUuid; - return pickedUuid; -} - -void MainTab::acceptChanges() -{ - int i, addedId = -1; - struct dive *d; - bool do_replot = false; - - if(ui.location->hasFocus()) { - this->setFocus(); - } - - acceptingEdit = true; - tabBar()->setTabIcon(0, QIcon()); // Notes - tabBar()->setTabIcon(1, QIcon()); // Equipment - ui.dateEdit->setEnabled(true); - hideMessage(); - ui.equipmentTab->setEnabled(true); - if (editMode == ADD) { - // We need to add the dive we just created to the dive list and select it. - // Easy, right? - struct dive *added_dive = clone_dive(&displayed_dive); - record_dive(added_dive); - addedId = added_dive->id; - // make sure that the dive site is handled as well - updateDiveSite(ui.location->currDiveSiteUuid(), get_idx_by_uniq_id(added_dive->id)); - - // unselect everything as far as the UI is concerned and select the new - // dive - we'll have to undo/redo this later after we resort the dive_table - // but we need the dive selected for the middle part of this function - this - // way we can reuse the code used for editing dives - MainWindow::instance()->dive_list()->unselectDives(); - selected_dive = get_divenr(added_dive); - amount_selected = 1; - } else if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { - /* now figure out if things have changed */ - if (displayedTrip.notes && !same_string(displayedTrip.notes, currentTrip->notes)) { - currentTrip->notes = copy_string(displayedTrip.notes); - mark_divelist_changed(true); - } - if (displayedTrip.location && !same_string(displayedTrip.location, currentTrip->location)) { - currentTrip->location = copy_string(displayedTrip.location); - mark_divelist_changed(true); - } - currentTrip = NULL; - ui.dateEdit->setEnabled(true); - } else { - if (editMode == MANUALLY_ADDED_DIVE) { - // preserve any changes to the profile - free(current_dive->dc.sample); - copy_samples(&displayed_dive.dc, ¤t_dive->dc); - addedId = displayed_dive.id; - } - struct dive *cd = current_dive; - struct divecomputer *displayed_dc = get_dive_dc(&displayed_dive, dc_number); - // now check if something has changed and if yes, edit the selected dives that - // were identical with the master dive shown (and mark the divelist as changed) - if (!same_string(displayed_dive.suit, cd->suit)) - MODIFY_SELECTED_DIVES(EDIT_TEXT(suit)); - if (!same_string(displayed_dive.notes, cd->notes)) - MODIFY_SELECTED_DIVES(EDIT_TEXT(notes)); - if (displayed_dive.rating != cd->rating) - MODIFY_SELECTED_DIVES(EDIT_VALUE(rating)); - if (displayed_dive.visibility != cd->visibility) - MODIFY_SELECTED_DIVES(EDIT_VALUE(visibility)); - if (displayed_dive.airtemp.mkelvin != cd->airtemp.mkelvin) - MODIFY_SELECTED_DIVES(EDIT_VALUE(airtemp.mkelvin)); - if (displayed_dc->divemode != current_dc->divemode) { - MODIFY_SELECTED_DIVES( - if (get_dive_dc(mydive, dc_number)->divemode == current_dc->divemode || copyPaste) { - get_dive_dc(mydive, dc_number)->divemode = displayed_dc->divemode; - } - ); - MODIFY_SELECTED_DIVES(update_setpoint_events(get_dive_dc(mydive, dc_number))); - do_replot = true; - } - if (displayed_dive.watertemp.mkelvin != cd->watertemp.mkelvin) - MODIFY_SELECTED_DIVES(EDIT_VALUE(watertemp.mkelvin)); - if (displayed_dive.when != cd->when) { - time_t offset = cd->when - displayed_dive.when; - MODIFY_SELECTED_DIVES(mydive->when -= offset;); - } - - if (displayed_dive.dive_site_uuid != cd->dive_site_uuid) - MODIFY_SELECTED_DIVES(EDIT_VALUE(dive_site_uuid)); - - // three text fields are somewhat special and are represented as tags - // in the UI - they need somewhat smarter handling - saveTaggedStrings(); - saveTags(); - - if (editMode != ADD && cylindersModel->changed) { - mark_divelist_changed(true); - MODIFY_SELECTED_DIVES( - for (int i = 0; i < MAX_CYLINDERS; i++) { - if (mydive != cd) { - if (same_string(mydive->cylinder[i].type.description, cd->cylinder[i].type.description) || copyPaste) { - // if we started out with the same cylinder description (for multi-edit) or if we do copt & paste - // make sure that we have the same cylinder type and copy the gasmix, but DON'T copy the start - // and end pressures (those are per dive after all) - if (!same_string(mydive->cylinder[i].type.description, displayed_dive.cylinder[i].type.description)) { - free((void*)mydive->cylinder[i].type.description); - mydive->cylinder[i].type.description = copy_string(displayed_dive.cylinder[i].type.description); - } - mydive->cylinder[i].type.size = displayed_dive.cylinder[i].type.size; - mydive->cylinder[i].type.workingpressure = displayed_dive.cylinder[i].type.workingpressure; - mydive->cylinder[i].gasmix = displayed_dive.cylinder[i].gasmix; - mydive->cylinder[i].cylinder_use = displayed_dive.cylinder[i].cylinder_use; - mydive->cylinder[i].depth = displayed_dive.cylinder[i].depth; - } - } - } - ); - for (int i = 0; i < MAX_CYLINDERS; i++) { - // copy the cylinder but make sure we have our own copy of the strings - free((void*)cd->cylinder[i].type.description); - cd->cylinder[i] = displayed_dive.cylinder[i]; - cd->cylinder[i].type.description = copy_string(displayed_dive.cylinder[i].type.description); - } - /* if cylinders changed we may have changed gas change events - * - so far this is ONLY supported for a single selected dive */ - struct divecomputer *tdc = ¤t_dive->dc; - struct divecomputer *sdc = &displayed_dive.dc; - while(tdc && sdc) { - free_events(tdc->events); - copy_events(sdc, tdc); - tdc = tdc->next; - sdc = sdc->next; - } - do_replot = true; - } - - if (weightModel->changed) { - mark_divelist_changed(true); - MODIFY_SELECTED_DIVES( - for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { - if (mydive != cd && (copyPaste || same_string(mydive->weightsystem[i].description, cd->weightsystem[i].description))) { - mydive->weightsystem[i] = displayed_dive.weightsystem[i]; - mydive->weightsystem[i].description = copy_string(displayed_dive.weightsystem[i].description); - } - } - ); - for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { - cd->weightsystem[i] = displayed_dive.weightsystem[i]; - cd->weightsystem[i].description = copy_string(displayed_dive.weightsystem[i].description); - } - } - - // update the dive site for the selected dives that had the same dive site as the current dive - uint32_t oldUuid = cd->dive_site_uuid; - uint32_t newUuid = 0; - MODIFY_SELECTED_DIVES( - if (mydive->dive_site_uuid == current_dive->dive_site_uuid) { - newUuid = updateDiveSite(newUuid == 0 ? ui.location->currDiveSiteUuid() : newUuid, get_idx_by_uniq_id(mydive->id)); - } - ); - if (!is_dive_site_used(oldUuid, false)) { - if (verbose) { - struct dive_site *ds = get_dive_site_by_uuid(oldUuid); - qDebug() << "delete now unused dive site" << ((ds && ds->name) ? ds->name : "without name"); - } - delete_dive_site(oldUuid); - GlobeGPS::instance()->reload(); - } - // the code above can change the correct uuid for the displayed dive site - and the - // code below triggers an update of the display without re-initializing displayed_dive - // so let's make sure here that our data is consistent now that we have handled the - // dive sites - displayed_dive.dive_site_uuid = current_dive->dive_site_uuid; - struct dive_site *ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); - if (ds) - copy_dive_site(ds, &displayed_dive_site); - - // each dive that was selected might have had the temperatures in its active divecomputer changed - // so re-populate the temperatures - easiest way to do this is by calling fixup_dive - for_each_dive (i, d) { - if (d->selected) - fixup_dive(d); - } - } - if (editMode != TRIP && current_dive->divetrip) { - current_dive->divetrip->when = current_dive->when; - find_new_trip_start_time(current_dive->divetrip); - } - if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { - // we just added or edited the dive, let fixup_dive() make - // sure we get the max depth right - current_dive->maxdepth.mm = current_dc->maxdepth.mm = 0; - fixup_dive(current_dive); - set_dive_nr_for_current_dive(); - MainWindow::instance()->showProfile(); - mark_divelist_changed(true); - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); - } - int scrolledBy = MainWindow::instance()->dive_list()->verticalScrollBar()->sliderPosition(); - resetPallete(); - if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { - // since a newly added dive could be in the middle of the dive_table we need - // to resort the dive list and make sure the newly added dive gets selected again - sort_table(&dive_table); - MainWindow::instance()->dive_list()->reload(DiveTripModel::CURRENT, true); - int newDiveNr = get_divenr(get_dive_by_uniq_id(addedId)); - MainWindow::instance()->dive_list()->unselectDives(); - MainWindow::instance()->dive_list()->selectDive(newDiveNr, true); - editMode = NONE; - MainWindow::instance()->refreshDisplay(); - MainWindow::instance()->graphics()->replot(); - emit addDiveFinished(); - } else { - editMode = NONE; - if (do_replot) - MainWindow::instance()->graphics()->replot(); - MainWindow::instance()->dive_list()->rememberSelection(); - sort_table(&dive_table); - MainWindow::instance()->refreshDisplay(); - MainWindow::instance()->dive_list()->restoreSelection(); - } - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); - MainWindow::instance()->dive_list()->verticalScrollBar()->setSliderPosition(scrolledBy); - MainWindow::instance()->dive_list()->setFocus(); - cylindersModel->changed = false; - weightModel->changed = false; - MainWindow::instance()->setEnabledToolbar(true); - acceptingEdit = false; - ui.editDiveSiteButton->setEnabled(true); -} - -void MainTab::resetPallete() -{ - QPalette p; - ui.buddy->setPalette(p); - ui.notes->setPalette(p); - ui.location->setPalette(p); - ui.divemaster->setPalette(p); - ui.suit->setPalette(p); - ui.airtemp->setPalette(p); - ui.DiveType->setPalette(p); - ui.watertemp->setPalette(p); - ui.dateEdit->setPalette(p); - ui.timeEdit->setPalette(p); - ui.tagWidget->setPalette(p); - ui.diveTripLocation->setPalette(p); -} - -#define EDIT_TEXT2(what, text) \ - textByteArray = text.toUtf8(); \ - free(what); \ - what = strdup(textByteArray.data()); - -#define FREE_IF_DIFFERENT(what) \ - if (displayed_dive.what != cd->what) \ - free(displayed_dive.what) - -void MainTab::rejectChanges() -{ - EditMode lastMode = editMode; - - if (lastMode != NONE && current_dive && - (modified || - memcmp(¤t_dive->cylinder[0], &displayed_dive.cylinder[0], sizeof(cylinder_t) * MAX_CYLINDERS) || - memcmp(¤t_dive->cylinder[0], &displayed_dive.weightsystem[0], sizeof(weightsystem_t) * MAX_WEIGHTSYSTEMS))) { - if (QMessageBox::warning(MainWindow::instance(), TITLE_OR_TEXT(tr("Discard the changes?"), - tr("You are about to discard your changes.")), - QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Discard) != QMessageBox::Discard) { - return; - } - } - ui.dateEdit->setEnabled(true); - editMode = NONE; - tabBar()->setTabIcon(0, QIcon()); // Notes - tabBar()->setTabIcon(1, QIcon()); // Equipment - hideMessage(); - resetPallete(); - // no harm done to call cancelPlan even if we were not in ADD or PLAN mode... - DivePlannerPointsModel::instance()->cancelPlan(); - if(lastMode == ADD) - MainWindow::instance()->dive_list()->restoreSelection(); - - // now make sure that the correct dive is displayed - if (selected_dive >= 0) - copy_dive(current_dive, &displayed_dive); - else - clear_dive(&displayed_dive); - updateDiveInfo(selected_dive < 0); - DivePictureModel::instance()->updateDivePictures(); - // the user could have edited the location and then canceled the edit - // let's get the correct location back in view -#ifndef NO_MARBLE - GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(displayed_dive.dive_site_uuid)); -#endif - // show the profile and dive info - MainWindow::instance()->graphics()->replot(); - MainWindow::instance()->setEnabledToolbar(true); - cylindersModel->changed = false; - weightModel->changed = false; - cylindersModel->updateDive(); - weightModel->updateDive(); - extraDataModel->updateDive(); - ui.editDiveSiteButton->setEnabled(true); -} -#undef EDIT_TEXT2 - -void MainTab::markChangedWidget(QWidget *w) -{ - QPalette p; - qreal h, s, l, a; - enableEdition(); - qApp->palette().color(QPalette::Text).getHslF(&h, &s, &l, &a); - p.setBrush(QPalette::Base, (l <= 0.3) ? QColor(Qt::yellow).lighter() : (l <= 0.6) ? QColor(Qt::yellow).light() : /* else */ QColor(Qt::yellow).darker(300)); - w->setPalette(p); - modified = true; -} - -void MainTab::on_buddy_textChanged() -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - - if (same_string(displayed_dive.buddy, ui.buddy->toPlainText().toUtf8().data())) - return; - - QStringList text_list = ui.buddy->toPlainText().split(",", QString::SkipEmptyParts); - for (int i = 0; i < text_list.size(); i++) - text_list[i] = text_list[i].trimmed(); - QString text = text_list.join(", "); - free(displayed_dive.buddy); - displayed_dive.buddy = strdup(text.toUtf8().data()); - markChangedWidget(ui.buddy); -} - -void MainTab::on_divemaster_textChanged() -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - - if (same_string(displayed_dive.divemaster, ui.divemaster->toPlainText().toUtf8().data())) - return; - - QStringList text_list = ui.divemaster->toPlainText().split(",", QString::SkipEmptyParts); - for (int i = 0; i < text_list.size(); i++) - text_list[i] = text_list[i].trimmed(); - QString text = text_list.join(", "); - free(displayed_dive.divemaster); - displayed_dive.divemaster = strdup(text.toUtf8().data()); - markChangedWidget(ui.divemaster); -} - -void MainTab::on_airtemp_textChanged(const QString &text) -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - displayed_dive.airtemp.mkelvin = parseTemperatureToMkelvin(text); - markChangedWidget(ui.airtemp); - validate_temp_field(ui.airtemp, text); -} - -void MainTab::divetype_Changed(int index) -{ - if (editMode == IGNORE) - return; - struct divecomputer *displayed_dc = get_dive_dc(&displayed_dive, dc_number); - displayed_dc->divemode = (enum dive_comp_type) index; - update_setpoint_events(displayed_dc); - markChangedWidget(ui.DiveType); - MainWindow::instance()->graphics()->recalcCeiling(); -} - -void MainTab::on_watertemp_textChanged(const QString &text) -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - displayed_dive.watertemp.mkelvin = parseTemperatureToMkelvin(text); - markChangedWidget(ui.watertemp); - validate_temp_field(ui.watertemp, text); -} - -void MainTab::validate_temp_field(QLineEdit *tempField, const QString &text) -{ - static bool missing_unit = false; - static bool missing_precision = false; - if (!text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.][0-9]+){0,1}(°[CF]){0,1}$")) && - !text.isEmpty() && - !text.contains(QRegExp("^[-+]$"))) { - if (text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.][0-9]+){0,1}(°)$")) && !missing_unit) { - if (!missing_unit) { - missing_unit = true; - return; - } - } - if (text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.]){0,1}(°[CF]){0,1}$")) && !missing_precision) { - if (!missing_precision) { - missing_precision = true; - return; - } - } - QPalette p; - p.setBrush(QPalette::Base, QColor(Qt::red).lighter()); - tempField->setPalette(p); - } else { - missing_unit = false; - missing_precision = false; - } -} - -void MainTab::on_dateEdit_dateChanged(const QDate &date) -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - markChangedWidget(ui.dateEdit); - QDateTime dateTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); - dateTime.setTimeSpec(Qt::UTC); - dateTime.setDate(date); - DivePlannerPointsModel::instance()->getDiveplan().when = displayed_dive.when = dateTime.toTime_t(); - emit dateTimeChanged(); -} - -void MainTab::on_timeEdit_timeChanged(const QTime &time) -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - markChangedWidget(ui.timeEdit); - QDateTime dateTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); - dateTime.setTimeSpec(Qt::UTC); - dateTime.setTime(time); - DivePlannerPointsModel::instance()->getDiveplan().when = displayed_dive.when = dateTime.toTime_t(); - emit dateTimeChanged(); -} - -// changing the tags on multiple dives is semantically strange - what's the right thing to do? -// here's what I think... add the tags that were added to the displayed dive and remove the tags -// that were removed from it -void MainTab::saveTags() -{ - struct dive *cd = current_dive; - struct tag_entry *added_list = NULL; - struct tag_entry *removed_list = NULL; - struct tag_entry *tl; - - taglist_free(displayed_dive.tag_list); - displayed_dive.tag_list = NULL; - Q_FOREACH (const QString& tag, ui.tagWidget->getBlockStringList()) - taglist_add_tag(&displayed_dive.tag_list, tag.toUtf8().data()); - taglist_cleanup(&displayed_dive.tag_list); - - // figure out which tags were added and which tags were removed - added_list = taglist_added(cd->tag_list, displayed_dive.tag_list); - removed_list = taglist_added(displayed_dive.tag_list, cd->tag_list); - // dump_taglist("added tags:", added_list); - // dump_taglist("removed tags:", removed_list); - - // we need to check if the tags were changed before just overwriting them - if (added_list == NULL && removed_list == NULL) - return; - - MODIFY_SELECTED_DIVES( - // create a new tag list and all the existing tags that were not - // removed and then all the added tags - struct tag_entry *new_tag_list; - new_tag_list = NULL; - tl = mydive->tag_list; - while (tl) { - if (!taglist_contains(removed_list, tl->tag->name)) - taglist_add_tag(&new_tag_list, tl->tag->name); - tl = tl->next; - } - tl = added_list; - while (tl) { - taglist_add_tag(&new_tag_list, tl->tag->name); - tl = tl->next; - } - taglist_free(mydive->tag_list); - mydive->tag_list = new_tag_list; - ); - taglist_free(added_list); - taglist_free(removed_list); -} - -// buddy and divemaster are represented in the UI just like the tags, but the internal -// representation is just a string (with commas as delimiters). So we need to do the same -// thing we did for tags, just differently -void MainTab::saveTaggedStrings() -{ - QStringList addedList, removedList; - struct dive *cd = current_dive; - - diffTaggedStrings(cd->buddy, displayed_dive.buddy, addedList, removedList); - MODIFY_SELECTED_DIVES( - QStringList oldList = QString(mydive->buddy).split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); - QString newString; - QString comma; - Q_FOREACH (const QString tag, oldList) { - if (!removedList.contains(tag, Qt::CaseInsensitive)) { - newString += comma + tag; - comma = ", "; - } - } - Q_FOREACH (const QString tag, addedList) { - if (!oldList.contains(tag, Qt::CaseInsensitive)) { - newString += comma + tag; - comma = ", "; - } - } - free(mydive->buddy); - mydive->buddy = copy_string(qPrintable(newString)); - ); - addedList.clear(); - removedList.clear(); - diffTaggedStrings(cd->divemaster, displayed_dive.divemaster, addedList, removedList); - MODIFY_SELECTED_DIVES( - QStringList oldList = QString(mydive->divemaster).split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); - QString newString; - QString comma; - Q_FOREACH (const QString tag, oldList) { - if (!removedList.contains(tag, Qt::CaseInsensitive)) { - newString += comma + tag; - comma = ", "; - } - } - Q_FOREACH (const QString tag, addedList) { - if (!oldList.contains(tag, Qt::CaseInsensitive)) { - newString += comma + tag; - comma = ", "; - } - } - free(mydive->divemaster); - mydive->divemaster = copy_string(qPrintable(newString)); - ); -} - -void MainTab::diffTaggedStrings(QString currentString, QString displayedString, QStringList &addedList, QStringList &removedList) -{ - QStringList displayedList, currentList; - currentList = currentString.split(',', QString::SkipEmptyParts); - displayedList = displayedString.split(',', QString::SkipEmptyParts); - Q_FOREACH ( const QString tag, currentList) { - if (!displayedList.contains(tag, Qt::CaseInsensitive)) - removedList << tag.trimmed(); - } - Q_FOREACH (const QString tag, displayedList) { - if (!currentList.contains(tag, Qt::CaseInsensitive)) - addedList << tag.trimmed(); - } -} - -void MainTab::on_tagWidget_textChanged() -{ - char buf[1024]; - - if (editMode == IGNORE || acceptingEdit == true) - return; - - taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); - if (same_string(buf, ui.tagWidget->toPlainText().toUtf8().data())) - return; - - markChangedWidget(ui.tagWidget); -} - -void MainTab::on_location_textChanged() -{ - if (editMode == IGNORE) - return; - - // we don't want to act on the edit until editing is finished, - // but we want to mark the field so it's obvious it is being edited - QString currentLocation; - struct dive_site *ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); - if (ds) - currentLocation = ds->name; - if (ui.location->text() != currentLocation) - markChangedWidget(ui.location); -} - -void MainTab::on_location_diveSiteSelected() -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - - if (ui.location->text().isEmpty()) { - displayed_dive.dive_site_uuid = 0; - markChangedWidget(ui.location); - emit diveSiteChanged(0); - return; - } else { - if (ui.location->currDiveSiteUuid() != displayed_dive.dive_site_uuid) { - markChangedWidget(ui.location); - } else { - QPalette p; - ui.location->setPalette(p); - } - } -} - -void MainTab::on_diveTripLocation_textEdited(const QString& text) -{ - if (currentTrip) { - free(displayedTrip.location); - displayedTrip.location = strdup(qPrintable(text)); - markChangedWidget(ui.diveTripLocation); - } -} - -void MainTab::on_suit_textChanged(const QString &text) -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - free(displayed_dive.suit); - displayed_dive.suit = strdup(text.toUtf8().data()); - markChangedWidget(ui.suit); -} - -void MainTab::on_notes_textChanged() -{ - if (editMode == IGNORE || acceptingEdit == true) - return; - if (currentTrip) { - if (same_string(displayedTrip.notes, ui.notes->toPlainText().toUtf8().data())) - return; - free(displayedTrip.notes); - displayedTrip.notes = strdup(ui.notes->toPlainText().toUtf8().data()); - } else { - if (same_string(displayed_dive.notes, ui.notes->toPlainText().toUtf8().data())) - return; - free(displayed_dive.notes); - if (ui.notes->toHtml().indexOf("toHtml().toUtf8().data()); - else - displayed_dive.notes = strdup(ui.notes->toPlainText().toUtf8().data()); - } - markChangedWidget(ui.notes); -} - -void MainTab::on_rating_valueChanged(int value) -{ - if (acceptingEdit == true) - return; - if (displayed_dive.rating != value) { - displayed_dive.rating = value; - modified = true; - enableEdition(); - } -} - -void MainTab::on_visibility_valueChanged(int value) -{ - if (acceptingEdit == true) - return; - if (displayed_dive.visibility != value) { - displayed_dive.visibility = value; - modified = true; - enableEdition(); - } -} - -#undef MODIFY_SELECTED_DIVES -#undef EDIT_TEXT -#undef EDIT_VALUE - -void MainTab::editCylinderWidget(const QModelIndex &index) -{ - // we need a local copy or bad things happen when enableEdition() is called - QModelIndex editIndex = index; - if (cylindersModel->changed && editMode == NONE) { - enableEdition(); - return; - } - if (editIndex.isValid() && editIndex.column() != CylindersModel::REMOVE) { - if (editMode == NONE) - enableEdition(); - ui.cylinders->edit(editIndex); - } -} - -void MainTab::editWeightWidget(const QModelIndex &index) -{ - if (editMode == NONE) - enableEdition(); - - if (index.isValid() && index.column() != WeightModel::REMOVE) - ui.weights->edit(index); -} - -void MainTab::escDetected() -{ - if (editMode != NONE) - rejectChanges(); -} - -void MainTab::photoDoubleClicked(const QString filePath) -{ - QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); -} - -void MainTab::removeSelectedPhotos() -{ - if (!ui.photosView->selectionModel()->hasSelection()) - return; - - QModelIndex photoIndex = ui.photosView->selectionModel()->selectedIndexes().first(); - QString fileUrl = photoIndex.data(Qt::DisplayPropertyRole).toString(); - DivePictureModel::instance()->removePicture(fileUrl); -} - -#define SHOW_SELECTIVE(_component) \ - if (what._component) \ - ui._component->setText(displayed_dive._component); - -void MainTab::showAndTriggerEditSelective(struct dive_components what) -{ - // take the data in our copyPasteDive and apply it to selected dives - enableEdition(); - copyPaste = true; - SHOW_SELECTIVE(buddy); - SHOW_SELECTIVE(divemaster); - SHOW_SELECTIVE(suit); - if (what.notes) { - QString tmp(displayed_dive.notes); - if (tmp.contains("setHtml(tmp); - else - ui.notes->setPlainText(tmp); - } - if (what.rating) - ui.rating->setCurrentStars(displayed_dive.rating); - if (what.visibility) - ui.visibility->setCurrentStars(displayed_dive.visibility); - if (what.divesite) - ui.location->setCurrentDiveSiteUuid(displayed_dive.dive_site_uuid); - if (what.tags) { - char buf[1024]; - taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); - ui.tagWidget->setText(QString(buf)); - } - if (what.cylinders) { - cylindersModel->updateDive(); - cylindersModel->changed = true; - } - if (what.weights) { - weightModel->updateDive(); - weightModel->changed = true; - } -} diff --git a/qt-ui/maintab.h b/qt-ui/maintab.h deleted file mode 100644 index 20b4da690..000000000 --- a/qt-ui/maintab.h +++ /dev/null @@ -1,129 +0,0 @@ -/* - * maintab.h - * - * header file for the main tab of Subsurface - * - */ -#ifndef MAINTAB_H -#define MAINTAB_H - -#include -#include -#include -#include - -#include "ui_maintab.h" -#include "completionmodels.h" -#include "divelocationmodel.h" -#include "dive.h" - -class WeightModel; -class CylindersModel; -class ExtraDataModel; -class DivePictureModel; -class QCompleter; - -struct Completers { - QCompleter *divemaster; - QCompleter *buddy; - QCompleter *suit; - QCompleter *tags; -}; - -class MainTab : public QTabWidget { - Q_OBJECT -public: - enum EditMode { - NONE, - DIVE, - TRIP, - ADD, - MANUALLY_ADDED_DIVE, - IGNORE - }; - - MainTab(QWidget *parent = 0); - ~MainTab(); - void clearStats(); - void clearInfo(); - void clearEquipment(); - void reload(); - void initialUiSetup(); - bool isEditing(); - void updateCoordinatesText(qreal lat, qreal lon); - void refreshDisplayedDiveSite(); - void nextInputField(QKeyEvent *event); - void showAndTriggerEditSelective(struct dive_components what); - -signals: - void addDiveFinished(); - void dateTimeChanged(); - void diveSiteChanged(struct dive_site * ds); -public -slots: - void addCylinder_clicked(); - void addWeight_clicked(); - void refreshDiveInfo(); - void updateDiveInfo(bool clear = false); - void acceptChanges(); - void rejectChanges(); - void on_location_diveSiteSelected(); - void on_location_textChanged(); - void on_divemaster_textChanged(); - void on_buddy_textChanged(); - void on_suit_textChanged(const QString &text); - void on_diveTripLocation_textEdited(const QString& text); - void on_notes_textChanged(); - void on_airtemp_textChanged(const QString &text); - void divetype_Changed(int); - void on_watertemp_textChanged(const QString &text); - void validate_temp_field(QLineEdit *tempField, const QString &text); - void on_dateEdit_dateChanged(const QDate &date); - void on_timeEdit_timeChanged(const QTime & time); - void on_rating_valueChanged(int value); - void on_visibility_valueChanged(int value); - void on_tagWidget_textChanged(); - void editCylinderWidget(const QModelIndex &index); - void editWeightWidget(const QModelIndex &index); - void addDiveStarted(); - void addMessageAction(QAction *action); - void hideMessage(); - void closeMessage(); - void displayMessage(QString str); - void enableEdition(EditMode newEditMode = NONE); - void toggleTriggeredColumn(); - void updateTextLabels(bool showUnits = true); - void escDetected(void); - void photoDoubleClicked(const QString filePath); - void removeSelectedPhotos(); - void showLocation(); - void enableGeoLookupEdition(); - void disableGeoLookupEdition(); - void setCurrentLocationIndex(); - EditMode getEditMode() const; -private: - Ui::MainTab ui; - WeightModel *weightModel; - CylindersModel *cylindersModel; - ExtraDataModel *extraDataModel; - EditMode editMode; - BuddyCompletionModel buddyModel; - DiveMasterCompletionModel diveMasterModel; - SuitCompletionModel suitModel; - TagCompletionModel tagModel; - DivePictureModel *divePictureModel; - Completers completers; - bool modified; - bool copyPaste; - void resetPallete(); - void saveTags(); - void saveTaggedStrings(); - void diffTaggedStrings(QString currentString, QString displayedString, QStringList &addedList, QStringList &removedList); - void markChangedWidget(QWidget *w); - dive_trip_t *currentTrip; - dive_trip_t displayedTrip; - bool acceptingEdit; - uint32_t updateDiveSite(uint32_t pickedUuid, int divenr); -}; - -#endif // MAINTAB_H diff --git a/qt-ui/maintab.ui b/qt-ui/maintab.ui deleted file mode 100644 index 2c515a225..000000000 --- a/qt-ui/maintab.ui +++ /dev/null @@ -1,1267 +0,0 @@ - - - MainTab - - - - 0 - 0 - 463 - 815 - - - - 0 - - - - Notes - - - General notes about the current selection - - - - 5 - - - 5 - - - 5 - - - 5 - - - 0 - - - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 445 - 726 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 5 - - - 5 - - - 8 - - - 0 - - - - - Date - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - Time - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - Air temp. - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - Water temp. - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - true - - - Qt::UTC - - - - - - - - 0 - 0 - - - - Qt::UTC - - - - - - - false - - - - - - - false - - - - - - - - - 0 - - - 5 - - - 5 - - - - - - - Location - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - - - - Qt::RichText - - - - - - - - - 2 - - - - - - - - Edit dive site - - - ... - - - - :/geocode:/geocode - - - - - - - - - - - - - - - - - 5 - - - 5 - - - 5 - - - 0 - - - - - Divemaster - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - Buddy - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - false - - - - - - - false - - - - - - - - - 5 - - - 5 - - - 5 - - - 0 - - - - - - 0 - 0 - - - - Rating - - - - - - - - 0 - 0 - - - - Visibility - - - - - - - Suit - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - - 0 - 0 - - - - false - - - - - - - - - 5 - - - 0 - - - - - - - - Tags - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - Dive mode - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - QPlainTextEdit::NoWrap - - - - - - - - - 0 - - - - - Notes - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - 0 - - - - - false - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - - - - - - Equipment - - - Used equipment in the current selection - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 70 - 16 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 2 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Vertical - - - - - - - - - - - - - - - - - Info - - - Dive information - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 287 - 320 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 2 - - - - - Date - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Interval - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Gases used - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Gas consumed - - - - - - - - - Qt::AlignCenter - - - - - - - - - - SAC - - - - - - - - - Qt::AlignCenter - - - - - - - - - - CNS - - - - - - - - - Qt::AlignCenter - - - - - - - - - - OTU - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Max. depth - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Avg. depth - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Air pressure - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Air temp. - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Water temp. - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Dive time - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Salinity - - - - - - Qt::AlignCenter - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 20 - - - - - - - - - - - - - Stats - - - Simple statistics about the selection - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 297 - 187 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - Depth - - - - - - - - - - - - Duration - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Temperature - - - - - - - - - - - - Total time - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Dives - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - SAC - - - - - - - - - - - - Gas consumption - - - - - - - - - Qt::AlignCenter - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - - - Photos - - - All photos from the current selection - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - QListView::IconMode - - - - - - - - Extra data - - - Adittional data from the dive computer - - - - 0 - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - - - - - KMessageWidget - QFrame -
kmessagewidget.h
- 1 -
- - StarWidget - QWidget -
starwidget.h
- 1 -
- - MinMaxAvgWidget - QWidget -
simplewidgets.h
- 1 -
- - TableView - QWidget -
tableview.h
- 1 -
- - TagWidget - QPlainTextEdit -
qt-ui/tagwidget.h
-
- - DivePictureWidget - QListView -
divepicturewidget.h
-
- - QtWaitingSpinner - QWidget -
qtwaitingspinner.h
- 1 -
- - DiveLocationLineEdit - QLineEdit -
locationinformation.h
-
-
- - dateEdit - timeEdit - airtemp - watertemp - divemaster - buddy - rating - visibility - suit - notes - - - - - -
diff --git a/qt-ui/mainwindow.cpp b/qt-ui/mainwindow.cpp deleted file mode 100644 index e0476381f..000000000 --- a/qt-ui/mainwindow.cpp +++ /dev/null @@ -1,1922 +0,0 @@ -/* - * mainwindow.cpp - * - * classes for the main UI window in Subsurface - */ -#include "mainwindow.h" - -#include -#include -#include -#include -#include -#include -#include "version.h" -#include "divelistview.h" -#include "downloadfromdivecomputer.h" -#include "preferences.h" -#include "subsurfacewebservices.h" -#include "divecomputermanagementdialog.h" -#include "about.h" -#include "updatemanager.h" -#include "planner.h" -#include "filtermodels.h" -#include "profile/profilewidget2.h" -#include "globe.h" -#include "divecomputer.h" -#include "maintab.h" -#include "diveplanner.h" -#ifndef NO_PRINTING -#include -#include "printdialog.h" -#endif -#include "tankinfomodel.h" -#include "weigthsysteminfomodel.h" -#include "yearlystatisticsmodel.h" -#include "diveplannermodel.h" -#include "divelogimportdialog.h" -#include "divelogexportdialog.h" -#include "usersurvey.h" -#include "divesitehelpers.h" -#include "windowtitleupdate.h" -#include "locationinformation.h" - -#ifndef NO_USERMANUAL -#include "usermanual.h" -#endif -#include "divepicturemodel.h" -#include "git-access.h" -#include -#include -#include -#include - -#if defined(FBSUPPORT) -#include "socialnetworks.h" -#endif - -QProgressDialog *progressDialog = NULL; -bool progressDialogCanceled = false; - -extern "C" int updateProgress(int percent) -{ - if (progressDialog) - progressDialog->setValue(percent); - return progressDialogCanceled; -} - -MainWindow *MainWindow::m_Instance = NULL; - -MainWindow::MainWindow() : QMainWindow(), - actionNextDive(0), - actionPreviousDive(0), - helpView(0), - state(VIEWALL), - survey(0) -{ - Q_ASSERT_X(m_Instance == NULL, "MainWindow", "MainWindow recreated!"); - m_Instance = this; - ui.setupUi(this); - read_hashes(); - // Define the States of the Application Here, Currently the states are situations where the different - // widgets will change on the mainwindow. - - // for the "default" mode - MainTab *mainTab = new MainTab(); - DiveListView *diveListView = new DiveListView(); - ProfileWidget2 *profileWidget = new ProfileWidget2(); - -#ifndef NO_MARBLE - GlobeGPS *globeGps = GlobeGPS::instance(); -#else - QWidget *globeGps = NULL; -#endif - - PlannerSettingsWidget *plannerSettings = new PlannerSettingsWidget(); - DivePlannerWidget *plannerWidget = new DivePlannerWidget(); - PlannerDetails *plannerDetails = new PlannerDetails(); - - // what is a sane order for those icons? we should have the ones the user is - // most likely to want towards the top so they are always visible - // and the ones that someone likely sets and then never touches again towards the bottom - profileToolbarActions << ui.profCalcCeiling << ui.profCalcAllTissues << // start with various ceilings - ui.profIncrement3m << ui.profDcCeiling << - ui.profPhe << ui.profPn2 << ui.profPO2 << // partial pressure graphs - ui.profRuler << ui.profScaled << // measuring and scaling - ui.profTogglePicture << ui.profTankbar << - ui.profMod << ui.profNdl_tts << // various values that a user is either interested in or not - ui.profEad << ui.profSAC << - ui.profHR << // very few dive computers support this - ui.profTissues; // maybe less frequently used - - QToolBar *toolBar = new QToolBar(); - Q_FOREACH (QAction *a, profileToolbarActions) - toolBar->addAction(a); - toolBar->setOrientation(Qt::Vertical); - toolBar->setIconSize(QSize(24,24)); - QWidget *profileContainer = new QWidget(); - QHBoxLayout *profLayout = new QHBoxLayout(); - profLayout->setSpacing(0); - profLayout->setMargin(0); - profLayout->setContentsMargins(0,0,0,0); - profLayout->addWidget(toolBar); - profLayout->addWidget(profileWidget); - profileContainer->setLayout(profLayout); - - LocationInformationWidget * diveSiteEdit = new LocationInformationWidget(); - connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, - this, &MainWindow::setDefaultState); - - connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, - mainTab, &MainTab::refreshDiveInfo); - - connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, - mainTab, &MainTab::refreshDisplayedDiveSite); - - QWidget *diveSitePictures = new QWidget(); // Placeholder - - std::pair enabled = std::make_pair("enabled", QVariant(true)); - std::pair disabled = std::make_pair("enabled", QVariant(false)); - PropertyList enabledList; - PropertyList disabledList; - enabledList.push_back(enabled); - disabledList.push_back(disabled); - - registerApplicationState("Default", mainTab, profileContainer, diveListView, globeGps ); - registerApplicationState("AddDive", mainTab, profileContainer, diveListView, globeGps ); - registerApplicationState("EditDive", mainTab, profileContainer, diveListView, globeGps ); - registerApplicationState("PlanDive", plannerWidget, profileContainer, plannerSettings, plannerDetails ); - registerApplicationState("EditPlannedDive", plannerWidget, profileContainer, diveListView, globeGps ); - registerApplicationState("EditDiveSite", diveSiteEdit, profileContainer, diveListView, globeGps); - - setStateProperties("Default", enabledList, enabledList, enabledList,enabledList); - setStateProperties("AddDive", enabledList, enabledList, enabledList,enabledList); - setStateProperties("EditDive", enabledList, enabledList, enabledList,enabledList); - setStateProperties("PlanDive", enabledList, enabledList, enabledList,enabledList); - setStateProperties("EditPlannedDive", enabledList, enabledList, enabledList,enabledList); - setStateProperties("EditDiveSite", enabledList, disabledList, disabledList, enabledList); - - setApplicationState("Default"); - - ui.multiFilter->hide(); - - setWindowIcon(QIcon(":subsurface-icon")); - if (!QIcon::hasThemeIcon("window-close")) { - QIcon::setThemeName("subsurface"); - } - connect(dive_list(), SIGNAL(currentDiveChanged(int)), this, SLOT(current_dive_changed(int))); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(readSettings())); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(update())); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(reloadHeaderActions())); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), information(), SLOT(updateDiveInfo())); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerWidget(), SLOT(settingsChanged())); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerSettingsWidget(), SLOT(settingsChanged())); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), TankInfoModel::instance(), SLOT(update())); - connect(ui.actionRecent1, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); - connect(ui.actionRecent2, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); - connect(ui.actionRecent3, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); - connect(ui.actionRecent4, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); - connect(information(), SIGNAL(addDiveFinished()), graphics(), SLOT(setProfileState())); - connect(DivePlannerPointsModel::instance(), SIGNAL(planCreated()), this, SLOT(planCreated())); - connect(DivePlannerPointsModel::instance(), SIGNAL(planCanceled()), this, SLOT(planCanceled())); - connect(plannerDetails->printPlan(), SIGNAL(pressed()), divePlannerWidget(), SLOT(printDecoPlan())); - connect(this, SIGNAL(startDiveSiteEdit()), this, SLOT(on_actionDiveSiteEdit_triggered())); - -#ifndef NO_MARBLE - connect(information(), SIGNAL(diveSiteChanged(struct dive_site *)), globeGps, SLOT(centerOnDiveSite(struct dive_site *))); -#endif - wtu = new WindowTitleUpdate(); - connect(WindowTitleUpdate::instance(), SIGNAL(updateTitle()), this, SLOT(setAutomaticTitle())); -#ifdef NO_PRINTING - plannerDetails->printPlan()->hide(); - ui.menuFile->removeAction(ui.actionPrint); -#endif -#ifndef USE_LIBGIT23_API - ui.menuFile->removeAction(ui.actionCloudstorageopen); - ui.menuFile->removeAction(ui.actionCloudstoragesave); - qDebug() << "disabled / made invisible the cloud storage stuff"; -#else - enableDisableCloudActions(); -#endif - - ui.mainErrorMessage->hide(); - graphics()->setEmptyState(); - initialUiSetup(); - readSettings(); - diveListView->reload(DiveTripModel::TREE); - diveListView->reloadHeaderActions(); - diveListView->setFocus(); - GlobeGPS::instance()->reload(); - diveListView->expand(dive_list()->model()->index(0, 0)); - diveListView->scrollTo(dive_list()->model()->index(0, 0), QAbstractItemView::PositionAtCenter); - divePlannerWidget()->settingsChanged(); - divePlannerSettingsWidget()->settingsChanged(); -#ifdef NO_MARBLE - ui.menuView->removeAction(ui.actionViewGlobe); -#endif -#ifdef NO_USERMANUAL - ui.menuHelp->removeAction(ui.actionUserManual); -#endif - memset(©PasteDive, 0, sizeof(copyPasteDive)); - memset(&what, 0, sizeof(what)); - - updateManager = new UpdateManager(this); - undoStack = new QUndoStack(this); - QAction *undoAction = undoStack->createUndoAction(this, tr("&Undo")); - QAction *redoAction = undoStack->createRedoAction(this, tr("&Redo")); - undoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Z)); - redoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z)); - QListundoRedoActions; - undoRedoActions.append(undoAction); - undoRedoActions.append(redoAction); - ui.menu_Edit->addActions(undoRedoActions); - - ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); - connect(geoLookup, SIGNAL(started()),information(), SLOT(disableGeoLookupEdition())); - connect(geoLookup, SIGNAL(finished()), information(), SLOT(enableGeoLookupEdition())); -#ifndef NO_PRINTING - // copy the bundled print templates to the user path; no overwriting occurs! - copyPath(getPrintingTemplatePathBundle(), getPrintingTemplatePathUser()); - find_all_templates(); -#endif - -#if defined(FBSUPPORT) - FacebookManager *fb = FacebookManager::instance(); - connect(fb, SIGNAL(justLoggedIn(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); - connect(fb, SIGNAL(justLoggedOut(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); - connect(ui.actionFacebook, SIGNAL(triggered(bool)), fb, SLOT(sendDive())); - ui.actionFacebook->setEnabled(fb->loggedIn()); -#else - ui.actionFacebook->setEnabled(false); -#endif - - - ui.menubar->show(); - set_git_update_cb(&updateProgress); -} - -MainWindow::~MainWindow() -{ - write_hashes(); - m_Instance = NULL; -} - -void MainWindow::setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl, const PropertyList& br) -{ - stateProperties[state] = PropertiesForQuadrant(tl, tr, bl, br); -} - -void MainWindow::on_actionDiveSiteEdit_triggered() { - setApplicationState("EditDiveSite"); -} - -void MainWindow::enableDisableCloudActions() -{ -#ifdef USE_LIBGIT23_API - ui.actionCloudstorageopen->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); - ui.actionCloudstoragesave->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); -#endif -} - -PlannerDetails *MainWindow::plannerDetails() const { - return qobject_cast(applicationState["PlanDive"].bottomRight); -} - -PlannerSettingsWidget *MainWindow::divePlannerSettingsWidget() { - return qobject_cast(applicationState["PlanDive"].bottomLeft); -} - -void MainWindow::setDefaultState() { - setApplicationState("Default"); - if (information()->getEditMode() != MainTab::NONE) { - ui.bottomLeft->currentWidget()->setEnabled(false); - } -} - -void MainWindow::setLoadedWithFiles(bool f) -{ - filesAsArguments = f; -} - -bool MainWindow::filesFromCommandLine() const -{ - return filesAsArguments; -} - -MainWindow *MainWindow::instance() -{ - return m_Instance; -} - -// this gets called after we download dives from a divecomputer -void MainWindow::refreshDisplay(bool doRecreateDiveList) -{ - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - information()->reload(); - TankInfoModel::instance()->update(); - GlobeGPS::instance()->reload(); - if (doRecreateDiveList) - recreateDiveList(); - - setApplicationState("Default"); - dive_list()->setEnabled(true); - dive_list()->setFocus(); - WSInfoModel::instance()->updateInfo(); - if (amount_selected == 0) - cleanUpEmpty(); -} - -void MainWindow::recreateDiveList() -{ - dive_list()->reload(DiveTripModel::CURRENT); - TagFilterModel::instance()->repopulate(); - BuddyFilterModel::instance()->repopulate(); - LocationFilterModel::instance()->repopulate(); - SuitsFilterModel::instance()->repopulate(); -} - -void MainWindow::configureToolbar() { - if (selected_dive>0) { - if (current_dive->dc.divemode == FREEDIVE) { - ui.profCalcCeiling->setDisabled(true); - ui.profCalcAllTissues ->setDisabled(true); - ui.profIncrement3m->setDisabled(true); - ui.profDcCeiling->setDisabled(true); - ui.profPhe->setDisabled(true); - ui.profPn2->setDisabled(true); //TODO is the same as scuba? - ui.profPO2->setDisabled(true); //TODO is the same as scuba? - ui.profRuler->setDisabled(false); - ui.profScaled->setDisabled(false); // measuring and scaling - ui.profTogglePicture->setDisabled(false); - ui.profTankbar->setDisabled(true); - ui.profMod->setDisabled(true); - ui.profNdl_tts->setDisabled(true); - ui.profEad->setDisabled(true); - ui.profSAC->setDisabled(true); - ui.profHR->setDisabled(false); - ui.profTissues->setDisabled(true); - } else { - ui.profCalcCeiling->setDisabled(false); - ui.profCalcAllTissues ->setDisabled(false); - ui.profIncrement3m->setDisabled(false); - ui.profDcCeiling->setDisabled(false); - ui.profPhe->setDisabled(false); - ui.profPn2->setDisabled(false); - ui.profPO2->setDisabled(false); // partial pressure graphs - ui.profRuler->setDisabled(false); - ui.profScaled->setDisabled(false); // measuring and scaling - ui.profTogglePicture->setDisabled(false); - ui.profTankbar->setDisabled(false); - ui.profMod->setDisabled(false); - ui.profNdl_tts->setDisabled(false); // various values that a user is either interested in or not - ui.profEad->setDisabled(false); - ui.profSAC->setDisabled(false); - ui.profHR->setDisabled(false); // very few dive computers support this - ui.profTissues->setDisabled(false);; // maybe less frequently used - } - } -} - -void MainWindow::current_dive_changed(int divenr) -{ - if (divenr >= 0) { - select_dive(divenr); - } - graphics()->plotDive(); - information()->updateDiveInfo(); - configureToolbar(); - GlobeGPS::instance()->reload(); -} - -void MainWindow::on_actionNew_triggered() -{ - on_actionClose_triggered(); -} - -void MainWindow::on_actionOpen_triggered() -{ - if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) - return; - - // yes, this look wrong to use getSaveFileName() for the open dialog, but we need to be able - // to enter file names that don't exist in order to use our git syntax /path/to/dir[branch] - // with is a potentially valid input, but of course won't exist. So getOpenFileName() wouldn't work - QFileDialog dialog(this, tr("Open file"), lastUsedDir(), filter()); - dialog.setFileMode(QFileDialog::AnyFile); - dialog.setViewMode(QFileDialog::Detail); - dialog.setLabelText(QFileDialog::Accept, tr("Open")); - dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); - dialog.setAcceptMode(QFileDialog::AcceptOpen); - QStringList filenames; - if (dialog.exec()) - filenames = dialog.selectedFiles(); - if (filenames.isEmpty()) - return; - updateLastUsedDir(QFileInfo(filenames.first()).dir().path()); - closeCurrentFile(); - // some file dialogs decide to add the default extension to a filename without extension - // so we would get dir[branch].ssrf when trying to select dir[branch]. - // let's detect that and remove the incorrect extension - QStringList cleanFilenames; - QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); - - Q_FOREACH (QString filename, filenames) { - if (reg.match(filename).hasMatch()) - filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); - cleanFilenames << filename; - } - loadFiles(cleanFilenames); -} - -void MainWindow::on_actionSave_triggered() -{ - file_save(); -} - -void MainWindow::on_actionSaveAs_triggered() -{ - file_save_as(); -} - -void MainWindow::on_actionCloudstorageopen_triggered() -{ - if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) - return; - - QString filename; - if (getCloudURL(filename)) { - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - return; - } - qDebug() << filename; - - closeCurrentFile(); - - int error; - - showProgressBar(); - QByteArray fileNamePtr = QFile::encodeName(filename); - error = parse_file(fileNamePtr.data()); - if (!error) { - set_filename(fileNamePtr.data(), true); - setTitle(MWTF_FILENAME); - } - getNotificationWidget()->hideNotification(); - process_dives(false, false); - hideProgressBar(); - refreshDisplay(); - ui.actionAutoGroup->setChecked(autogroup); -} - -void MainWindow::on_actionCloudstoragesave_triggered() -{ - QString filename; - if (getCloudURL(filename)) { - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - return; - } - qDebug() << filename; - if (information()->isEditing()) - information()->acceptChanges(); - - showProgressBar(); - - if (save_dives(filename.toUtf8().data())) { - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - return; - } - - hideProgressBar(); - - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - set_filename(filename.toUtf8().data(), true); - setTitle(MWTF_FILENAME); - mark_divelist_changed(false); -} - -void learnImageDirs(QStringList dirnames) -{ - QList > futures; - foreach (QString dir, dirnames) { - futures << QtConcurrent::run(learnImages, QDir(dir), 10, false); - } - DivePictureModel::instance()->updateDivePicturesWhenDone(futures); -} - -void MainWindow::on_actionHash_images_triggered() -{ - QFuture future; - QFileDialog dialog(this, tr("Traverse image directories"), lastUsedDir(), filter()); - dialog.setFileMode(QFileDialog::Directory); - dialog.setViewMode(QFileDialog::Detail); - dialog.setLabelText(QFileDialog::Accept, tr("Scan")); - dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); - QStringList dirnames; - if (dialog.exec()) - dirnames = dialog.selectedFiles(); - if (dirnames.isEmpty()) - return; - future = QtConcurrent::run(learnImageDirs,dirnames); - MainWindow::instance()->getNotificationWidget()->showNotification(tr("Scanning images...(this can take a while)"), KMessageWidget::Information); - MainWindow::instance()->getNotificationWidget()->setFuture(future); - -} - -ProfileWidget2 *MainWindow::graphics() const -{ - return qobject_cast(applicationState["Default"].topRight->layout()->itemAt(1)->widget()); -} - -void MainWindow::cleanUpEmpty() -{ - information()->clearStats(); - information()->clearInfo(); - information()->clearEquipment(); - information()->updateDiveInfo(true); - graphics()->setEmptyState(); - dive_list()->reload(DiveTripModel::TREE); - GlobeGPS::instance()->reload(); - if (!existing_filename) - setTitle(MWTF_DEFAULT); - disableShortcuts(); -} - -bool MainWindow::okToClose(QString message) -{ - if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || - information()->isEditing() ) { - QMessageBox::warning(this, tr("Warning"), message); - return false; - } - if (unsaved_changes() && askSaveChanges() == false) - return false; - - return true; -} - -void MainWindow::closeCurrentFile() -{ - graphics()->setEmptyState(); - /* free the dives and trips */ - clear_git_id(); - clear_dive_file_data(); - cleanUpEmpty(); - mark_divelist_changed(false); - - clear_events(); - - dcList.dcMap.clear(); -} - -void MainWindow::on_actionClose_triggered() -{ - if (okToClose(tr("Please save or cancel the current dive edit before closing the file."))) { - closeCurrentFile(); - // hide any pictures and the filter - DivePictureModel::instance()->updateDivePictures(); - ui.multiFilter->closeFilter(); - recreateDiveList(); - } -} - -QString MainWindow::lastUsedDir() -{ - QSettings settings; - QString lastDir = QDir::homePath(); - - settings.beginGroup("FileDialog"); - if (settings.contains("LastDir")) - if (QDir::setCurrent(settings.value("LastDir").toString())) - lastDir = settings.value("LastDir").toString(); - return lastDir; -} - -void MainWindow::updateLastUsedDir(const QString &dir) -{ - QSettings s; - s.beginGroup("FileDialog"); - s.setValue("LastDir", dir); -} - -void MainWindow::on_actionPrint_triggered() -{ -#ifndef NO_PRINTING - PrintDialog dlg(this); - - dlg.exec(); -#endif -} - -void MainWindow::disableShortcuts(bool disablePaste) -{ - ui.actionPreviousDC->setShortcut(QKeySequence()); - ui.actionNextDC->setShortcut(QKeySequence()); - ui.copy->setShortcut(QKeySequence()); - if (disablePaste) - ui.paste->setShortcut(QKeySequence()); -} - -void MainWindow::enableShortcuts() -{ - ui.actionPreviousDC->setShortcut(Qt::Key_Left); - ui.actionNextDC->setShortcut(Qt::Key_Right); - ui.copy->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C)); - ui.paste->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V)); -} - -void MainWindow::showProfile() -{ - enableShortcuts(); - graphics()->setProfileState(); - setApplicationState("Default"); -} - -void MainWindow::on_actionPreferences_triggered() -{ - PreferencesDialog::instance()->show(); - PreferencesDialog::instance()->raise(); -} - -void MainWindow::on_actionQuit_triggered() -{ - if (information()->isEditing()) { - information()->rejectChanges(); - if (information()->isEditing()) - // didn't discard the edits - return; - } - if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { - DivePlannerPointsModel::instance()->cancelPlan(); - if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) - // The planned dive was not discarded - return; - } - - if (unsaved_changes() && (askSaveChanges() == false)) - return; - writeSettings(); - QApplication::quit(); -} - -void MainWindow::on_actionDownloadDC_triggered() -{ - DownloadFromDCWidget dlg(this); - - dlg.exec(); -} - -void MainWindow::on_actionDownloadWeb_triggered() -{ - SubsurfaceWebServices dlg(this); - - dlg.exec(); -} - -void MainWindow::on_actionDivelogs_de_triggered() -{ - DivelogsDeWebServices::instance()->downloadDives(); -} - -void MainWindow::on_actionEditDeviceNames_triggered() -{ - DiveComputerManagementDialog::instance()->init(); - DiveComputerManagementDialog::instance()->update(); - DiveComputerManagementDialog::instance()->show(); -} - -bool MainWindow::plannerStateClean() -{ - if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || - information()->isEditing()) { - QMessageBox::warning(this, tr("Warning"), tr("Please save or cancel the current dive edit before trying to add a dive.")); - return false; - } - return true; -} - -void MainWindow::refreshProfile() -{ - showProfile(); - configureToolbar(); - graphics()->replot(get_dive(selected_dive)); - DivePictureModel::instance()->updateDivePictures(); -} - -void MainWindow::planCanceled() -{ - // while planning we might have modified the displayed_dive - // let's refresh what's shown on the profile - refreshProfile(); - refreshDisplay(false); -} - -void MainWindow::planCreated() -{ - // get the new dive selected and assign a number if reasonable - graphics()->setProfileState(); - if (displayed_dive.id == 0) { - // we might have added a new dive (so displayed_dive was cleared out by clone_dive() - dive_list()->unselectDives(); - select_dive(dive_table.nr - 1); - dive_list()->selectDive(selected_dive); - set_dive_nr_for_current_dive(); - } - // make sure our UI is in a consistent state - information()->updateDiveInfo(); - showProfile(); - refreshDisplay(); -} - -void MainWindow::setPlanNotes() -{ - plannerDetails()->divePlanOutput()->setHtml(displayed_dive.notes); -} - -void MainWindow::printPlan() -{ -#ifndef NO_PRINTING - QString diveplan = plannerDetails()->divePlanOutput()->toHtml(); - QString withDisclaimer = QString(" ") + diveplan + QString(disclaimer); - - QPrinter printer; - QPrintDialog *dialog = new QPrintDialog(&printer, this); - dialog->setWindowTitle(tr("Print runtime table")); - if (dialog->exec() != QDialog::Accepted) - return; - - plannerDetails()->divePlanOutput()->setHtml(withDisclaimer); - plannerDetails()->divePlanOutput()->print(&printer); - plannerDetails()->divePlanOutput()->setHtml(diveplan); -#endif -} - -void MainWindow::setupForAddAndPlan(const char *model) -{ - // clean out the dive and give it an id and the correct dc model - clear_dive(&displayed_dive); - clear_dive_site(&displayed_dive_site); - displayed_dive.id = dive_getUniqID(&displayed_dive); - displayed_dive.when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset() + 3600; - displayed_dive.dc.model = model; // don't translate! this is stored in the XML file - // setup the dive cylinders - DivePlannerPointsModel::instance()->clear(); - DivePlannerPointsModel::instance()->setupCylinders(); -} - -void MainWindow::on_actionReplanDive_triggered() -{ - if (!plannerStateClean() || !current_dive || !current_dive->dc.model) - return; - else if (strcmp(current_dive->dc.model, "planned dive")) { - if (QMessageBox::warning(this, tr("Warning"), tr("Trying to replan a dive that's not a planned dive."), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) - return; - } - // put us in PLAN mode - DivePlannerPointsModel::instance()->clear(); - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); - - graphics()->setPlanState(); - graphics()->clearHandlers(); - setApplicationState("PlanDive"); - divePlannerWidget()->setReplanButton(true); - DivePlannerPointsModel::instance()->loadFromDive(current_dive); - reset_cylinders(&displayed_dive, true); -} - -void MainWindow::on_actionDivePlanner_triggered() -{ - if (!plannerStateClean()) - return; - - // put us in PLAN mode - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); - setApplicationState("PlanDive"); - - graphics()->setPlanState(); - - // create a simple starting dive, using the first gas from the just copied cylinders - setupForAddAndPlan("planned dive"); // don't translate, stored in XML file - DivePlannerPointsModel::instance()->setupStartTime(); - DivePlannerPointsModel::instance()->createSimpleDive(); - DivePictureModel::instance()->updateDivePictures(); - divePlannerWidget()->setReplanButton(false); -} - -DivePlannerWidget* MainWindow::divePlannerWidget() { - return qobject_cast(applicationState["PlanDive"].topLeft); -} - -void MainWindow::on_actionAddDive_triggered() -{ - if (!plannerStateClean()) - return; - - if (dive_list()->selectedTrips().count() >= 1) { - dive_list()->rememberSelection(); - dive_list()->clearSelection(); - } - - setApplicationState("AddDive"); - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); - - // setup things so we can later create our starting dive - setupForAddAndPlan("manually added dive"); // don't translate, stored in the XML file - - // now show the mostly empty main tab - information()->updateDiveInfo(); - - // show main tab - information()->setCurrentIndex(0); - - information()->addDiveStarted(); - - graphics()->setAddState(); - DivePlannerPointsModel::instance()->createSimpleDive(); - configureToolbar(); - graphics()->plotDive(); -} - -void MainWindow::on_actionEditDive_triggered() -{ - if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { - QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); - return; - } - - const bool isTripEdit = dive_list()->selectedTrips().count() >= 1; - if (!current_dive || isTripEdit || (current_dive->dc.model && strcmp(current_dive->dc.model, "manually added dive"))) { - QMessageBox::warning(this, tr("Warning"), tr("Trying to edit a dive that's not a manually added dive.")); - return; - } - - DivePlannerPointsModel::instance()->clear(); - disableShortcuts(); - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); - graphics()->setAddState(); - GlobeGPS::instance()->endGetDiveCoordinates(); - setApplicationState("EditDive"); - DivePlannerPointsModel::instance()->loadFromDive(current_dive); - information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); -} - -void MainWindow::on_actionRenumber_triggered() -{ - RenumberDialog::instance()->renumberOnlySelected(false); - RenumberDialog::instance()->show(); -} - -void MainWindow::on_actionAutoGroup_triggered() -{ - autogroup = ui.actionAutoGroup->isChecked(); - if (autogroup) - autogroup_dives(); - else - remove_autogen_trips(); - refreshDisplay(); - mark_divelist_changed(true); -} - -void MainWindow::on_actionYearlyStatistics_triggered() -{ - QDialog d; - QVBoxLayout *l = new QVBoxLayout(&d); - YearlyStatisticsModel *m = new YearlyStatisticsModel(); - QTreeView *view = new QTreeView(); - view->setModel(m); - l->addWidget(view); - d.resize(width() * .8, height() / 2); - d.move(width() * .1, height() / 4); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), &d); - connect(close, SIGNAL(activated()), &d, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), &d); - connect(quit, SIGNAL(activated()), this, SLOT(close())); - d.setWindowFlags(Qt::Window | Qt::CustomizeWindowHint - | Qt::WindowCloseButtonHint | Qt::WindowTitleHint); - d.setWindowTitle(tr("Yearly statistics")); - d.setWindowIcon(QIcon(":/subsurface-icon")); - d.exec(); -} - -#define BEHAVIOR QList() - -#define TOGGLE_COLLAPSABLE( X ) \ - ui.mainSplitter->setCollapsible(0, X); \ - ui.mainSplitter->setCollapsible(1, X); \ - ui.topSplitter->setCollapsible(0, X); \ - ui.topSplitter->setCollapsible(1, X); \ - ui.bottomSplitter->setCollapsible(0, X); \ - ui.bottomSplitter->setCollapsible(1, X); - -void MainWindow::on_actionViewList_triggered() -{ - TOGGLE_COLLAPSABLE( true ); - beginChangeState(LIST_MAXIMIZED); - ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); - ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); -} - -void MainWindow::on_actionViewProfile_triggered() -{ - TOGGLE_COLLAPSABLE( true ); - beginChangeState(PROFILE_MAXIMIZED); - ui.topSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); - ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); -} - -void MainWindow::on_actionViewInfo_triggered() -{ - TOGGLE_COLLAPSABLE( true ); - beginChangeState(INFO_MAXIMIZED); - ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); - ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); -} - -void MainWindow::on_actionViewGlobe_triggered() -{ - TOGGLE_COLLAPSABLE( true ); - beginChangeState(GLOBE_MAXIMIZED); - ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); - ui.bottomSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); -} -#undef BEHAVIOR - -void MainWindow::on_actionViewAll_triggered() -{ - TOGGLE_COLLAPSABLE( false ); - beginChangeState(VIEWALL); - static QList mainSizes; - const int appH = qApp->desktop()->size().height(); - const int appW = qApp->desktop()->size().width(); - if (mainSizes.empty()) { - mainSizes.append(appH * 0.7); - mainSizes.append(appH * 0.3); - } - static QList infoProfileSizes; - if (infoProfileSizes.empty()) { - infoProfileSizes.append(appW * 0.3); - infoProfileSizes.append(appW * 0.7); - } - - static QList listGlobeSizes; - if (listGlobeSizes.empty()) { - listGlobeSizes.append(appW * 0.7); - listGlobeSizes.append(appW * 0.3); - } - - QSettings settings; - settings.beginGroup("MainWindow"); - if (settings.value("mainSplitter").isValid()) { - ui.mainSplitter->restoreState(settings.value("mainSplitter").toByteArray()); - ui.topSplitter->restoreState(settings.value("topSplitter").toByteArray()); - ui.bottomSplitter->restoreState(settings.value("bottomSplitter").toByteArray()); - if (ui.mainSplitter->sizes().first() == 0 || ui.mainSplitter->sizes().last() == 0) - ui.mainSplitter->setSizes(mainSizes); - if (ui.topSplitter->sizes().first() == 0 || ui.topSplitter->sizes().last() == 0) - ui.topSplitter->setSizes(infoProfileSizes); - if (ui.bottomSplitter->sizes().first() == 0 || ui.bottomSplitter->sizes().last() == 0) - ui.bottomSplitter->setSizes(listGlobeSizes); - - } else { - ui.mainSplitter->setSizes(mainSizes); - ui.topSplitter->setSizes(infoProfileSizes); - ui.bottomSplitter->setSizes(listGlobeSizes); - } - ui.mainSplitter->setCollapsible(0, false); - ui.mainSplitter->setCollapsible(1, false); - ui.topSplitter->setCollapsible(0, false); - ui.topSplitter->setCollapsible(1, false); - ui.bottomSplitter->setCollapsible(0,false); - ui.bottomSplitter->setCollapsible(1,false); -} - -#undef TOGGLE_COLLAPSABLE - -void MainWindow::beginChangeState(CurrentState s) -{ - if (state == VIEWALL && state != s) { - saveSplitterSizes(); - } - state = s; -} - -void MainWindow::saveSplitterSizes() -{ - QSettings settings; - settings.beginGroup("MainWindow"); - settings.setValue("mainSplitter", ui.mainSplitter->saveState()); - settings.setValue("topSplitter", ui.topSplitter->saveState()); - settings.setValue("bottomSplitter", ui.bottomSplitter->saveState()); -} - -void MainWindow::on_actionPreviousDC_triggered() -{ - unsigned nrdc = number_of_computers(current_dive); - dc_number = (dc_number + nrdc - 1) % nrdc; - configureToolbar(); - graphics()->plotDive(); - information()->updateDiveInfo(); -} - -void MainWindow::on_actionNextDC_triggered() -{ - unsigned nrdc = number_of_computers(current_dive); - dc_number = (dc_number + 1) % nrdc; - configureToolbar(); - graphics()->plotDive(); - information()->updateDiveInfo(); -} - -void MainWindow::on_actionFullScreen_triggered(bool checked) -{ - if (checked) { - setWindowState(windowState() | Qt::WindowFullScreen); - } else { - setWindowState(windowState() & ~Qt::WindowFullScreen); - } -} - -void MainWindow::on_actionAboutSubsurface_triggered() -{ - SubsurfaceAbout dlg(this); - - dlg.exec(); -} - -void MainWindow::on_action_Check_for_Updates_triggered() -{ - if (!updateManager) - updateManager = new UpdateManager(this); - - updateManager->checkForUpdates(); -} - -void MainWindow::on_actionUserManual_triggered() -{ -#ifndef NO_USERMANUAL - if (!helpView) { - helpView = new UserManual(); - } - helpView->show(); -#endif -} - -void MainWindow::on_actionUserSurvey_triggered() -{ - if(!survey) { - survey = new UserSurvey(this); - } - survey->show(); -} - -QString MainWindow::filter() -{ - QString f; - f += "Dive log files ( *.ssrf "; - f += "*.can *.CAN "; - f += "*.db *.DB " ; - f += "*.sql *.SQL " ; - f += "*.dld *.DLD "; - f += "*.jlb *.JLB "; - f += "*.lvd *.LVD "; - f += "*.sde *.SDE "; - f += "*.udcf *.UDCF "; - f += "*.uddf *.UDDF "; - f += "*.xml *.XML "; - f += "*.dlf *.DLF "; - f += ");;"; - - f += "Subsurface (*.ssrf);;"; - f += "Cochran (*.can *.CAN);;"; - f += "DiveLogs.de (*.dld *.DLD);;"; - f += "JDiveLog (*.jlb *.JLB);;"; - f += "Liquivision (*.lvd *.LVD);;"; - f += "Suunto (*.sde *.SDE *.db *.DB);;"; - f += "UDCF (*.udcf *.UDCF);;"; - f += "UDDF (*.uddf *.UDDF);;"; - f += "XML (*.xml *.XML)"; - f += "Divesoft (*.dlf *.DLF)"; - f += "Datatrak/WLog Files (*.log *.LOG)"; - - return f; -} - -bool MainWindow::askSaveChanges() -{ - QString message; - QMessageBox response(this); - - if (existing_filename) - message = tr("Do you want to save the changes that you made in the file %1?") - .arg(displayedFilename(existing_filename)); - else - message = tr("Do you want to save the changes that you made in the data file?"); - - response.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); - response.setDefaultButton(QMessageBox::Save); - response.setText(message); - response.setWindowTitle(tr("Save changes?")); // Not displayed on MacOSX as described in Qt API - response.setInformativeText(tr("Changes will be lost if you don't save them.")); - response.setIcon(QMessageBox::Warning); - response.setWindowModality(Qt::WindowModal); - int ret = response.exec(); - - switch (ret) { - case QMessageBox::Save: - file_save(); - return true; - case QMessageBox::Discard: - return true; - } - return false; -} - -void MainWindow::initialUiSetup() -{ - QSettings settings; - settings.beginGroup("MainWindow"); - if (settings.value("maximized", isMaximized()).value()) { - showMaximized(); - } else { - restoreGeometry(settings.value("geometry").toByteArray()); - restoreState(settings.value("windowState", 0).toByteArray()); - } - - state = (CurrentState)settings.value("lastState", 0).toInt(); - switch (state) { - case VIEWALL: - on_actionViewAll_triggered(); - break; - case GLOBE_MAXIMIZED: - on_actionViewGlobe_triggered(); - break; - case INFO_MAXIMIZED: - on_actionViewInfo_triggered(); - break; - case LIST_MAXIMIZED: - on_actionViewList_triggered(); - break; - case PROFILE_MAXIMIZED: - on_actionViewProfile_triggered(); - break; - } - settings.endGroup(); - show(); -} - -const char *getSetting(QSettings &s, QString name) -{ - QVariant v; - v = s.value(name); - if (v.isValid()) { - return strdup(v.toString().toUtf8().data()); - } - return NULL; -} - -#define TOOLBOX_PREF_BUTTON(pref, setting, button) \ - prefs.pref = s.value(#setting).toBool(); \ - ui.button->setChecked(prefs.pref); - -void MainWindow::readSettings() -{ - static bool firstRun = true; - QSettings s; - // the static object for preferences already reads in the settings - // and sets up the font, so just get what we need for the toolbox and other widgets here - - s.beginGroup("TecDetails"); - TOOLBOX_PREF_BUTTON(calcalltissues, calcalltissues, profCalcAllTissues); - TOOLBOX_PREF_BUTTON(calcceiling, calcceiling, profCalcCeiling); - TOOLBOX_PREF_BUTTON(dcceiling, dcceiling, profDcCeiling); - TOOLBOX_PREF_BUTTON(ead, ead, profEad); - TOOLBOX_PREF_BUTTON(calcceiling3m, calcceiling3m, profIncrement3m); - TOOLBOX_PREF_BUTTON(mod, mod, profMod); - TOOLBOX_PREF_BUTTON(calcndltts, calcndltts, profNdl_tts); - TOOLBOX_PREF_BUTTON(pp_graphs.phe, phegraph, profPhe); - TOOLBOX_PREF_BUTTON(pp_graphs.pn2, pn2graph, profPn2); - TOOLBOX_PREF_BUTTON(pp_graphs.po2, po2graph, profPO2); - TOOLBOX_PREF_BUTTON(hrgraph, hrgraph, profHR); - TOOLBOX_PREF_BUTTON(rulergraph, rulergraph, profRuler); - TOOLBOX_PREF_BUTTON(show_sac, show_sac, profSAC); - TOOLBOX_PREF_BUTTON(show_pictures_in_profile, show_pictures_in_profile, profTogglePicture); - TOOLBOX_PREF_BUTTON(tankbar, tankbar, profTankbar); - TOOLBOX_PREF_BUTTON(percentagegraph, percentagegraph, profTissues); - TOOLBOX_PREF_BUTTON(zoomed_plot, zoomed_plot, profScaled); - s.endGroup(); // note: why doesn't the list of 17 buttons match the order in the gui? - s.beginGroup("DiveComputer"); - default_dive_computer_vendor = getSetting(s, "dive_computer_vendor"); - default_dive_computer_product = getSetting(s, "dive_computer_product"); - default_dive_computer_device = getSetting(s, "dive_computer_device"); - default_dive_computer_download_mode = s.value("dive_computer_download_mode").toInt(); - s.endGroup(); - QNetworkProxy proxy; - proxy.setType(QNetworkProxy::ProxyType(prefs.proxy_type)); - proxy.setHostName(prefs.proxy_host); - proxy.setPort(prefs.proxy_port); - if (prefs.proxy_auth) { - proxy.setUser(prefs.proxy_user); - proxy.setPassword(prefs.proxy_pass); - } - QNetworkProxy::setApplicationProxy(proxy); - -#if !defined(SUBSURFACE_MOBILE) - loadRecentFiles(&s); - if (firstRun) { - checkSurvey(&s); - firstRun = false; - } -#endif -} - -#undef TOOLBOX_PREF_BUTTON - -void MainWindow::checkSurvey(QSettings *s) -{ - s->beginGroup("UserSurvey"); - if (!s->contains("FirstUse42")) { - QVariant value = QDate().currentDate(); - s->setValue("FirstUse42", value); - } - // wait a week for production versions, but not at all for non-tagged builds - QString ver(subsurface_version()); - int waitTime = 7; - QDate firstUse42 = s->value("FirstUse42").toDate(); - if (run_survey || (firstUse42.daysTo(QDate().currentDate()) > waitTime && !s->contains("SurveyDone"))) { - if (!survey) - survey = new UserSurvey(this); - survey->show(); - } - s->endGroup(); -} - -void MainWindow::writeSettings() -{ - QSettings settings; - - settings.beginGroup("MainWindow"); - settings.setValue("geometry", saveGeometry()); - settings.setValue("windowState", saveState()); - settings.setValue("maximized", isMaximized()); - settings.setValue("lastState", (int)state); - if (state == VIEWALL) - saveSplitterSizes(); - settings.endGroup(); -} - -void MainWindow::closeEvent(QCloseEvent *event) -{ - if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || - information()->isEditing()) { - on_actionQuit_triggered(); - event->ignore(); - return; - } - -#ifndef NO_USERMANUAL - if (helpView && helpView->isVisible()) { - helpView->close(); - helpView->deleteLater(); - } -#endif - - if (survey && survey->isVisible()) { - survey->close(); - survey->deleteLater(); - } - - if (unsaved_changes() && (askSaveChanges() == false)) { - event->ignore(); - return; - } - event->accept(); - writeSettings(); - QApplication::closeAllWindows(); -} - -DiveListView *MainWindow::dive_list() -{ - return qobject_cast(applicationState["Default"].bottomLeft); -} - -MainTab *MainWindow::information() -{ - return qobject_cast(applicationState["Default"].topLeft); -} - -void MainWindow::loadRecentFiles(QSettings *s) -{ - QStringList files; - bool modified = false; - - s->beginGroup("Recent_Files"); - for (int c = 1; c <= 4; c++) { - QString key = QString("File_%1").arg(c); - if (s->contains(key)) { - QString file = s->value(key).toString(); - - if (QFile::exists(file)) { - files.append(file); - } else { - modified = true; - } - } else { - break; - } - } - - if (modified) { - for (int c = 0; c < 4; c++) { - QString key = QString("File_%1").arg(c + 1); - - if (files.count() > c) { - s->setValue(key, files.at(c)); - } else { - if (s->contains(key)) { - s->remove(key); - } - } - } - - s->sync(); - } - s->endGroup(); - - for (int c = 0; c < 4; c++) { - QAction *action = this->findChild(QString("actionRecent%1").arg(c + 1)); - - if (files.count() > c) { - QFileInfo fi(files.at(c)); - action->setText(fi.fileName()); - action->setToolTip(fi.absoluteFilePath()); - action->setVisible(true); - } else { - action->setVisible(false); - } - } -} - -void MainWindow::addRecentFile(const QStringList &newFiles) -{ - QStringList files; - QSettings s; - - if (newFiles.isEmpty()) - return; - - s.beginGroup("Recent_Files"); - - for (int c = 1; c <= 4; c++) { - QString key = QString("File_%1").arg(c); - if (s.contains(key)) { - QString file = s.value(key).toString(); - - files.append(file); - } else { - break; - } - } - - foreach (const QString &file, newFiles) { - int index = files.indexOf(QDir::toNativeSeparators(file)); - - if (index >= 0) { - files.removeAt(index); - } - } - - foreach (const QString &file, newFiles) { - if (QFile::exists(file)) { - files.prepend(QDir::toNativeSeparators(file)); - } - } - - while (files.count() > 4) { - files.removeLast(); - } - - for (int c = 1; c <= 4; c++) { - QString key = QString("File_%1").arg(c); - - if (files.count() >= c) { - s.setValue(key, files.at(c - 1)); - } else { - if (s.contains(key)) { - s.remove(key); - } - } - } - s.endGroup(); - s.sync(); - - loadRecentFiles(&s); -} - -void MainWindow::removeRecentFile(QStringList failedFiles) -{ - QStringList files; - QSettings s; - - if (failedFiles.isEmpty()) - return; - - s.beginGroup("Recent_Files"); - - for (int c = 1; c <= 4; c++) { - QString key = QString("File_%1").arg(c); - - if (s.contains(key)) { - QString file = s.value(key).toString(); - files.append(file); - } else { - break; - } - } - - foreach (const QString &file, failedFiles) - files.removeAll(file); - - for (int c = 1; c <= 4; c++) { - QString key = QString("File_%1").arg(c); - - if (files.count() >= c) - s.setValue(key, files.at(c - 1)); - else if (s.contains(key)) - s.remove(key); - } - - s.endGroup(); - s.sync(); - - loadRecentFiles(&s); -} - -void MainWindow::recentFileTriggered(bool checked) -{ - Q_UNUSED(checked); - - if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) - return; - - QAction *actionRecent = (QAction *)sender(); - - const QString &filename = actionRecent->toolTip(); - - updateLastUsedDir(QFileInfo(filename).dir().path()); - closeCurrentFile(); - loadFiles(QStringList() << filename); -} - -int MainWindow::file_save_as(void) -{ - QString filename; - const char *default_filename = existing_filename; - - // if the default is to save to cloud storage, pick something that will work as local file: - // simply extract the branch name which should be the users email address - if (default_filename && strstr(default_filename, prefs.cloud_git_url)) { - QString filename(default_filename); - filename.remove(prefs.cloud_git_url); - filename.remove(0, filename.indexOf("[") + 1); - filename.replace("]", ".ssrf"); - default_filename = strdup(qPrintable(filename)); - } - // create a file dialog that allows us to save to a new file - QFileDialog selection_dialog(this, tr("Save file as"), default_filename, - tr("Subsurface XML files (*.ssrf *.xml *.XML)")); - selection_dialog.setAcceptMode(QFileDialog::AcceptSave); - selection_dialog.setFileMode(QFileDialog::AnyFile); - selection_dialog.setDefaultSuffix(""); - if (same_string(default_filename, "")) { - QFileInfo defaultFile(system_default_filename()); - selection_dialog.setDirectory(qPrintable(defaultFile.absolutePath())); - } - /* if the exit/cancel button is pressed return */ - if (!selection_dialog.exec()) - return 0; - - /* get the first selected file */ - filename = selection_dialog.selectedFiles().at(0); - - /* now for reasons I don't understand we appear to add a .ssrf to - * git style filenames /directory[branch] - * so let's remove that */ - QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); - if (reg.match(filename).hasMatch()) - filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); - if (filename.isNull() || filename.isEmpty()) - return report_error("No filename to save into"); - - if (information()->isEditing()) - information()->acceptChanges(); - - if (save_dives(filename.toUtf8().data())) { - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - return -1; - } - - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - set_filename(filename.toUtf8().data(), true); - setTitle(MWTF_FILENAME); - mark_divelist_changed(false); - addRecentFile(QStringList() << filename); - return 0; -} - -int MainWindow::file_save(void) -{ - const char *current_default; - bool is_cloud = false; - - if (!existing_filename) - return file_save_as(); - - is_cloud = (strncmp(existing_filename, "http", 4) == 0); - - if (information()->isEditing()) - information()->acceptChanges(); - - current_default = prefs.default_filename; - if (strcmp(existing_filename, current_default) == 0) { - /* if we are using the default filename the directory - * that we are creating the file in may not exist */ - QDir current_def_dir = QFileInfo(current_default).absoluteDir(); - if (!current_def_dir.exists()) - current_def_dir.mkpath(current_def_dir.absolutePath()); - } - if (is_cloud) - showProgressBar(); - if (save_dives(existing_filename)) { - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - if (is_cloud) - hideProgressBar(); - return -1; - } - if (is_cloud) - hideProgressBar(); - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - mark_divelist_changed(false); - addRecentFile(QStringList() << QString(existing_filename)); - return 0; -} - -NotificationWidget *MainWindow::getNotificationWidget() -{ - return ui.mainErrorMessage; -} - -void MainWindow::showError() -{ - getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); -} - -QString MainWindow::displayedFilename(QString fullFilename) -{ - QFile f(fullFilename); - QFileInfo fileInfo(f); - QString fileName(fileInfo.fileName()); - - if (fullFilename.contains(prefs.cloud_git_url)) - return tr("[cloud storage for] %1").arg(fileName.left(fileName.indexOf('['))); - else - return fileName; -} - -void MainWindow::setAutomaticTitle() -{ - setTitle(); -} - -void MainWindow::setTitle(enum MainWindowTitleFormat format) -{ - switch (format) { - case MWTF_DEFAULT: - setWindowTitle("Subsurface"); - break; - case MWTF_FILENAME: - if (!existing_filename) { - setTitle(MWTF_DEFAULT); - return; - } - QString unsaved = (unsaved_changes() ? " *" : ""); - setWindowTitle("Subsurface: " + displayedFilename(existing_filename) + unsaved); - break; - } -} - -void MainWindow::importFiles(const QStringList fileNames) -{ - if (fileNames.isEmpty()) - return; - - QByteArray fileNamePtr; - - for (int i = 0; i < fileNames.size(); ++i) { - fileNamePtr = QFile::encodeName(fileNames.at(i)); - parse_file(fileNamePtr.data()); - } - process_dives(true, false); - refreshDisplay(); -} - -void MainWindow::importTxtFiles(const QStringList fileNames) -{ - if (fileNames.isEmpty()) - return; - - QByteArray fileNamePtr, csv; - - for (int i = 0; i < fileNames.size(); ++i) { - fileNamePtr = QFile::encodeName(fileNames.at(i)); - csv = fileNamePtr.data(); - csv.replace(strlen(csv.data()) - 3, 3, "csv"); - parse_txt_file(fileNamePtr.data(), csv); - } - process_dives(true, false); - refreshDisplay(); -} - -void MainWindow::loadFiles(const QStringList fileNames) -{ - bool showWarning = false; - if (fileNames.isEmpty()) { - refreshDisplay(); - return; - } - QByteArray fileNamePtr; - QStringList failedParses; - - showProgressBar(); - for (int i = 0; i < fileNames.size(); ++i) { - int error; - - fileNamePtr = QFile::encodeName(fileNames.at(i)); - error = parse_file(fileNamePtr.data()); - if (!error) { - set_filename(fileNamePtr.data(), true); - setTitle(MWTF_FILENAME); - // if there were any messages, show them - QString warning = get_error_string(); - if (!warning.isEmpty()) { - showWarning = true; - getNotificationWidget()->showNotification(warning , KMessageWidget::Information); - } - } else { - failedParses.append(fileNames.at(i)); - } - } - hideProgressBar(); - if (!showWarning) - getNotificationWidget()->hideNotification(); - process_dives(false, false); - addRecentFile(fileNames); - removeRecentFile(failedParses); - - refreshDisplay(); - ui.actionAutoGroup->setChecked(autogroup); - - int min_datafile_version = get_min_datafile_version(); - if (min_datafile_version >0 && min_datafile_version < DATAFORMAT_VERSION) { - QMessageBox::warning(this, tr("Opening datafile from older version"), - tr("You opened a data file from an older version of Subsurface. We recommend " - "you read the manual to learn about the changes in the new version, especially " - "about dive site management which has changed significantly.\n" - "Subsurface has already tried to pre-populate the data but it might be worth " - "while taking a look at the new dive site management system and to make " - "sure that everything looks correct.")); - } -} - -void MainWindow::on_actionImportDiveLog_triggered() -{ - QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open dive log file"), lastUsedDir(), - tr("Dive log files (*.ssrf *.can *.csv *.db *.sql *.dld *.jlb *.lvd *.sde *.udcf *.uddf *.xml *.txt *.dlf *.apd" - "*.SSRF *.CAN *.CSV *.DB *.SQL *.DLD *.JLB *.LVD *.SDE *.UDCF *.UDDF *.xml *.TXT *.DLF *.APD);;" - "Cochran files (*.can *.CAN);;" - "CSV files (*.csv *.CSV);;" - "DiveLog.de files (*.dld *.DLD);;" - "JDiveLog files (*.jlb *.JLB);;" - "Liquivision files (*.lvd *.LVD);;" - "MkVI files (*.txt *.TXT);;" - "Suunto files (*.sde *.db *.SDE *.DB);;" - "Divesoft files (*.dlf *.DLF);;" - "UDDF/UDCF files (*.uddf *.udcf *.UDDF *.UDCF);;" - "XML files (*.xml *.XML);;" - "APD log viewer (*.apd *.APD);;" - "Datatrak/WLog Files (*.log *.LOG);;" - "OSTCtools Files (*.dive *.DIVE);;" - "All files (*)")); - - if (fileNames.isEmpty()) - return; - updateLastUsedDir(QFileInfo(fileNames[0]).dir().path()); - - QStringList logFiles = fileNames.filter(QRegExp("^.*\\.(?!(csv|txt|apd))", Qt::CaseInsensitive)); - QStringList csvFiles = fileNames.filter(".csv", Qt::CaseInsensitive); - csvFiles += fileNames.filter(".apd", Qt::CaseInsensitive); - QStringList txtFiles = fileNames.filter(".txt", Qt::CaseInsensitive); - - if (logFiles.size()) { - importFiles(logFiles); - } - - if (csvFiles.size()) { - DiveLogImportDialog *diveLogImport = new DiveLogImportDialog(csvFiles, this); - diveLogImport->show(); - process_dives(true, false); - refreshDisplay(); - } - - if (txtFiles.size()) { - importTxtFiles(txtFiles); - } -} - -void MainWindow::editCurrentDive() -{ - if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { - QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); - return; - } - - struct dive *d = current_dive; - QString defaultDC(d->dc.model); - DivePlannerPointsModel::instance()->clear(); - if (defaultDC == "manually added dive") { - disableShortcuts(); - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); - graphics()->setAddState(); - setApplicationState("EditDive"); - DivePlannerPointsModel::instance()->loadFromDive(d); - information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); - } else if (defaultDC == "planned dive") { - disableShortcuts(); - DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); - setApplicationState("EditPlannedDive"); - DivePlannerPointsModel::instance()->loadFromDive(d); - information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); - } -} - -#define PREF_PROFILE(QT_PREFS) \ - QSettings s; \ - s.beginGroup("TecDetails"); \ - s.setValue(#QT_PREFS, triggered); \ - PreferencesDialog::instance()->emitSettingsChanged(); - -#define TOOLBOX_PREF_PROFILE(METHOD, INTERNAL_PREFS, QT_PREFS) \ - void MainWindow::on_##METHOD##_triggered(bool triggered) \ - { \ - prefs.INTERNAL_PREFS = triggered; \ - PREF_PROFILE(QT_PREFS); \ - } - -// note: why doesn't the list of 17 buttons match the order in the gui? or the order above? (line 1136) -TOOLBOX_PREF_PROFILE(profCalcAllTissues, calcalltissues, calcalltissues); -TOOLBOX_PREF_PROFILE(profCalcCeiling, calcceiling, calcceiling); -TOOLBOX_PREF_PROFILE(profDcCeiling, dcceiling, dcceiling); -TOOLBOX_PREF_PROFILE(profEad, ead, ead); -TOOLBOX_PREF_PROFILE(profIncrement3m, calcceiling3m, calcceiling3m); -TOOLBOX_PREF_PROFILE(profMod, mod, mod); -TOOLBOX_PREF_PROFILE(profNdl_tts, calcndltts, calcndltts); -TOOLBOX_PREF_PROFILE(profPhe, pp_graphs.phe, phegraph); -TOOLBOX_PREF_PROFILE(profPn2, pp_graphs.pn2, pn2graph); -TOOLBOX_PREF_PROFILE(profPO2, pp_graphs.po2, po2graph); -TOOLBOX_PREF_PROFILE(profHR, hrgraph, hrgraph); -TOOLBOX_PREF_PROFILE(profRuler, rulergraph, rulergraph); -TOOLBOX_PREF_PROFILE(profSAC, show_sac, show_sac); -TOOLBOX_PREF_PROFILE(profScaled, zoomed_plot, zoomed_plot); -TOOLBOX_PREF_PROFILE(profTogglePicture, show_pictures_in_profile, show_pictures_in_profile); -TOOLBOX_PREF_PROFILE(profTankbar, tankbar, tankbar); -TOOLBOX_PREF_PROFILE(profTissues, percentagegraph, percentagegraph); -// couldn't the args to TOOLBOX_PREF_PROFILE be made to go in the same sequence as TOOLBOX_PREF_BUTTON? - -void MainWindow::turnOffNdlTts() -{ - const bool triggered = false; - prefs.calcndltts = triggered; - PREF_PROFILE(calcndltts); -} - -#undef TOOLBOX_PREF_PROFILE -#undef PERF_PROFILE - -void MainWindow::on_actionExport_triggered() -{ - DiveLogExportDialog diveLogExport; - diveLogExport.exec(); -} - -void MainWindow::on_actionConfigure_Dive_Computer_triggered() -{ - ConfigureDiveComputerDialog *dcConfig = new ConfigureDiveComputerDialog(this); - dcConfig->show(); -} - -void MainWindow::setEnabledToolbar(bool arg1) -{ - Q_FOREACH (QAction *b, profileToolbarActions) - b->setEnabled(arg1); -} - -void MainWindow::on_copy_triggered() -{ - // open dialog to select what gets copied - // copy the displayed dive - DiveComponentSelection dialog(this, ©PasteDive, &what); - dialog.exec(); -} - -void MainWindow::on_paste_triggered() -{ - // take the data in our copyPasteDive and apply it to selected dives - selective_copy_dive(©PasteDive, &displayed_dive, what, false); - information()->showAndTriggerEditSelective(what); -} - -void MainWindow::on_actionFilterTags_triggered() -{ - if (ui.multiFilter->isVisible()) - ui.multiFilter->closeFilter(); - else - ui.multiFilter->setVisible(true); -} - -void MainWindow::registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight) -{ - applicationState[state] = WidgetForQuadrant(topLeft, topRight, bottomLeft, bottomRight); - if (ui.topLeft->indexOf(topLeft) == -1 && topLeft) { - ui.topLeft->addWidget(topLeft); - } - if (ui.topRight->indexOf(topRight) == -1 && topRight) { - ui.topRight->addWidget(topRight); - } - if (ui.bottomLeft->indexOf(bottomLeft) == -1 && bottomLeft) { - ui.bottomLeft->addWidget(bottomLeft); - } - if(ui.bottomRight->indexOf(bottomRight) == -1 && bottomRight) { - ui.bottomRight->addWidget(bottomRight); - } -} - -void MainWindow::setApplicationState(const QByteArray& state) { - if (!applicationState.keys().contains(state)) - return; - - if (getCurrentAppState() == state) - return; - - setCurrentAppState(state); - -#define SET_CURRENT_INDEX( X ) \ - if (applicationState[state].X) { \ - ui.X->setCurrentWidget( applicationState[state].X); \ - ui.X->show(); \ - } else { \ - ui.X->hide(); \ - } - - SET_CURRENT_INDEX( topLeft ) - Q_FOREACH(const WidgetProperty& p, stateProperties[state].topLeft) { - ui.topLeft->currentWidget()->setProperty( p.first.data(), p.second); - } - SET_CURRENT_INDEX( topRight ) - Q_FOREACH(const WidgetProperty& p, stateProperties[state].topRight) { - ui.topRight->currentWidget()->setProperty( p.first.data(), p.second); - } - SET_CURRENT_INDEX( bottomLeft ) - Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomLeft) { - ui.bottomLeft->currentWidget()->setProperty( p.first.data(), p.second); - } - SET_CURRENT_INDEX( bottomRight ) - Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomRight) { - ui.bottomRight->currentWidget()->setProperty( p.first.data(), p.second); - } -#undef SET_CURRENT_INDEX -} - -void MainWindow::showProgressBar() -{ - if (progressDialog) - delete progressDialog; - - progressDialog = new QProgressDialog(tr("Contacting cloud service..."), tr("Cancel"), 0, 100, this); - progressDialog->setWindowModality(Qt::WindowModal); - progressDialog->setMinimumDuration(200); - progressDialogCanceled = false; - connect(progressDialog, SIGNAL(canceled()), this, SLOT(cancelCloudStorageOperation())); -} - -void MainWindow::cancelCloudStorageOperation() -{ - progressDialogCanceled = true; -} - -void MainWindow::hideProgressBar() -{ - if (progressDialog) { - progressDialog->setValue(100); - progressDialog->deleteLater(); - progressDialog = NULL; - } -} diff --git a/qt-ui/mainwindow.h b/qt-ui/mainwindow.h deleted file mode 100644 index 02ec2478c..000000000 --- a/qt-ui/mainwindow.h +++ /dev/null @@ -1,258 +0,0 @@ -/* - * mainwindow.h - * - * header file for the main window of Subsurface - * - */ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H - -#include -#include -#include -#include -#include - -#include "ui_mainwindow.h" -#include "notificationwidget.h" -#include "windowtitleupdate.h" - -struct DiveList; -class QSortFilterProxyModel; -class DiveTripModel; - -class DiveInfo; -class DiveNotes; -class Stats; -class Equipment; -class QItemSelection; -class DiveListView; -class MainTab; -class ProfileGraphicsView; -class QWebView; -class QSettings; -class UpdateManager; -class UserManual; -class DivePlannerWidget; -class ProfileWidget2; -class PlannerDetails; -class PlannerSettingsWidget; -class QUndoStack; -class LocationInformationWidget; - -typedef std::pair WidgetProperty; -typedef QVector PropertyList; - -enum MainWindowTitleFormat { - MWTF_DEFAULT, - MWTF_FILENAME -}; - -class MainWindow : public QMainWindow { - Q_OBJECT -public: - enum { - COLLAPSED, - EXPANDED - }; - - enum CurrentState { - VIEWALL, - GLOBE_MAXIMIZED, - INFO_MAXIMIZED, - PROFILE_MAXIMIZED, - LIST_MAXIMIZED - }; - - MainWindow(); - virtual ~MainWindow(); - static MainWindow *instance(); - MainTab *information(); - void loadRecentFiles(QSettings *s); - void addRecentFile(const QStringList &newFiles); - void removeRecentFile(QStringList failedFiles); - DiveListView *dive_list(); - DivePlannerWidget *divePlannerWidget(); - PlannerSettingsWidget *divePlannerSettingsWidget(); - LocationInformationWidget *locationInformationWidget(); - void setTitle(enum MainWindowTitleFormat format = MWTF_FILENAME); - - // Some shortcuts like "change DC" or "copy/paste dive components" - // should only be enabled when the profile's visible. - void disableShortcuts(bool disablePaste = true); - void enableShortcuts(); - void loadFiles(const QStringList files); - void importFiles(const QStringList importFiles); - void importTxtFiles(const QStringList fileNames); - void cleanUpEmpty(); - void setToolButtonsEnabled(bool enabled); - ProfileWidget2 *graphics() const; - PlannerDetails *plannerDetails() const; - void setLoadedWithFiles(bool filesFromCommandLine); - bool filesFromCommandLine() const; - void printPlan(); - void checkSurvey(QSettings *s); - void setApplicationState(const QByteArray& state); - void setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl,const PropertyList& br); - bool inPlanner(); - QUndoStack *undoStack; - NotificationWidget *getNotificationWidget(); - void enableDisableCloudActions(); - void showError(); -private -slots: - /* file menu action */ - void recentFileTriggered(bool checked); - void on_actionNew_triggered(); - void on_actionOpen_triggered(); - void on_actionSave_triggered(); - void on_actionSaveAs_triggered(); - void on_actionClose_triggered(); - void on_actionCloudstorageopen_triggered(); - void on_actionCloudstoragesave_triggered(); - void on_actionPrint_triggered(); - void on_actionPreferences_triggered(); - void on_actionQuit_triggered(); - void on_actionHash_images_triggered(); - - /* log menu actions */ - void on_actionDownloadDC_triggered(); - void on_actionDownloadWeb_triggered(); - void on_actionDivelogs_de_triggered(); - void on_actionEditDeviceNames_triggered(); - void on_actionAddDive_triggered(); - void on_actionEditDive_triggered(); - void on_actionRenumber_triggered(); - void on_actionAutoGroup_triggered(); - void on_actionYearlyStatistics_triggered(); - - /* view menu actions */ - void on_actionViewList_triggered(); - void on_actionViewProfile_triggered(); - void on_actionViewInfo_triggered(); - void on_actionViewGlobe_triggered(); - void on_actionViewAll_triggered(); - void on_actionPreviousDC_triggered(); - void on_actionNextDC_triggered(); - void on_actionFullScreen_triggered(bool checked); - - /* other menu actions */ - void on_actionAboutSubsurface_triggered(); - void on_actionUserManual_triggered(); - void on_actionUserSurvey_triggered(); - void on_actionDivePlanner_triggered(); - void on_actionReplanDive_triggered(); - void on_action_Check_for_Updates_triggered(); - - void on_actionDiveSiteEdit_triggered(); - void current_dive_changed(int divenr); - void initialUiSetup(); - - void on_actionImportDiveLog_triggered(); - - /* TODO: Move those slots below to it's own class */ - void on_profCalcAllTissues_triggered(bool triggered); - void on_profCalcCeiling_triggered(bool triggered); - void on_profDcCeiling_triggered(bool triggered); - void on_profEad_triggered(bool triggered); - void on_profIncrement3m_triggered(bool triggered); - void on_profMod_triggered(bool triggered); - void on_profNdl_tts_triggered(bool triggered); - void on_profPO2_triggered(bool triggered); - void on_profPhe_triggered(bool triggered); - void on_profPn2_triggered(bool triggered); - void on_profHR_triggered(bool triggered); - void on_profRuler_triggered(bool triggered); - void on_profSAC_triggered(bool triggered); - void on_profScaled_triggered(bool triggered); - void on_profTogglePicture_triggered(bool triggered); - void on_profTankbar_triggered(bool triggered); - void on_profTissues_triggered(bool triggered); - void on_actionExport_triggered(); - void on_copy_triggered(); - void on_paste_triggered(); - void on_actionFilterTags_triggered(); - void on_actionConfigure_Dive_Computer_triggered(); - void setDefaultState(); - void setAutomaticTitle(); - void cancelCloudStorageOperation(); - -protected: - void closeEvent(QCloseEvent *); - -signals: - void startDiveSiteEdit(); - -public -slots: - void turnOffNdlTts(); - void readSettings(); - void refreshDisplay(bool doRecreateDiveList = true); - void recreateDiveList(); - void showProfile(); - void refreshProfile(); - void editCurrentDive(); - void planCanceled(); - void planCreated(); - void setEnabledToolbar(bool arg1); - void setPlanNotes(); - -private: - Ui::MainWindow ui; - QAction *actionNextDive; - QAction *actionPreviousDive; - UserManual *helpView; - CurrentState state; - QString filter(); - static MainWindow *m_Instance; - QString displayedFilename(QString fullFilename); - bool askSaveChanges(); - bool okToClose(QString message); - void closeCurrentFile(); - void showProgressBar(); - void hideProgressBar(); - void writeSettings(); - int file_save(); - int file_save_as(); - void beginChangeState(CurrentState s); - void saveSplitterSizes(); - QString lastUsedDir(); - void updateLastUsedDir(const QString &s); - void registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight); - bool filesAsArguments; - UpdateManager *updateManager; - - bool plannerStateClean(); - void setupForAddAndPlan(const char *model); - void configureToolbar(); - QDialog *survey; - struct dive copyPasteDive; - struct dive_components what; - QList profileToolbarActions; - - struct WidgetForQuadrant { - WidgetForQuadrant(QWidget *tl = 0, QWidget *tr = 0, QWidget *bl = 0, QWidget *br = 0) : - topLeft(tl), topRight(tr), bottomLeft(bl), bottomRight(br) {} - QWidget *topLeft; - QWidget *topRight; - QWidget *bottomLeft; - QWidget *bottomRight; - }; - - struct PropertiesForQuadrant { - PropertiesForQuadrant(){} - PropertiesForQuadrant(const PropertyList& tl, const PropertyList& tr,const PropertyList& bl,const PropertyList& br) : - topLeft(tl), topRight(tr), bottomLeft(bl), bottomRight(br) {} - PropertyList topLeft; - PropertyList topRight; - PropertyList bottomLeft; - PropertyList bottomRight; - }; - - QHash applicationState; - QHash stateProperties; - - WindowTitleUpdate *wtu; -}; - -#endif // MAINWINDOW_H diff --git a/qt-ui/mainwindow.ui b/qt-ui/mainwindow.ui deleted file mode 100644 index 5e3200cfc..000000000 --- a/qt-ui/mainwindow.ui +++ /dev/null @@ -1,761 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 861 - 800 - - - - - - 0 - - - 0 - - - - - - - - Qt::Vertical - - - - Qt::Horizontal - - - - - - - Qt::Horizontal - - - - - - - - - - - - - - - 0 - 0 - 861 - 23 - - - - - &File - - - - - - - - - - - - - - - - - - - - - - - - - &Log - - - - - - - - - - - - - - - - - &View - - - - - - - - - - - - - - - - &Help - - - - - - - - - &Import - - - - - - - - - &Edit - - - - - Share on - - - - - - - - - - - - - - - &New logbook - - - New - - - Ctrl+N - - - - - &Open logbook - - - Open - - - Ctrl+O - - - - - &Save - - - Save - - - Ctrl+S - - - - - Sa&ve as - - - Save as - - - Ctrl+Shift+S - - - - - &Close - - - Close - - - Ctrl+W - - - - - &Print - - - Ctrl+P - - - - - P&references - - - Ctrl+, - - - QAction::PreferencesRole - - - - - &Quit - - - Ctrl+Q - - - QAction::QuitRole - - - - - Import from &dive computer - - - Ctrl+D - - - - - Import &GPS data from Subsurface web service - - - Ctrl+G - - - - - Edit device &names - - - - - &Add dive - - - Ctrl++ - - - - - &Edit dive - - - - - &Copy dive components - - - Ctrl+C - - - - - &Paste dive components - - - Ctrl+V - - - - - &Renumber - - - Ctrl+R - - - - - true - - - Auto &group - - - - - &Yearly statistics - - - Ctrl+Y - - - - - &Dive list - - - Ctrl+2 - - - - - &Profile - - - Ctrl+3 - - - - - &Info - - - Ctrl+4 - - - - - &All - - - Ctrl+1 - - - - - P&revious DC - - - Left - - - - - &Next DC - - - Right - - - - - &About Subsurface - - - QAction::AboutRole - - - - - User &manual - - - F1 - - - - - &Globe - - - Ctrl+5 - - - - - P&lan dive - - - Ctrl+L - - - - - &Import log files - - - Import divelog files from other applications - - - Ctrl+I - - - - - Import &from divelogs.de - - - - - true - - - &Full screen - - - Toggle full screen - - - F11 - - - - - false - - - - - false - - - - - false - - - - - false - - - - - &Check for updates - - - - - &Export - - - Export dive logs - - - Ctrl+E - - - - - Configure &dive computer - - - Ctrl+Shift+C - - - QAction::NoRole - - - - - Edit &dive in planner - - - - - true - - - - :/icon_o2:/icon_o2 - - - Toggle pOâ‚‚ graph - - - - - true - - - - :/icon_n2:/icon_n2 - - - Toggle pNâ‚‚ graph - - - - - true - - - - :/icon_he:/icon_he - - - Toggle pHe graph - - - - - true - - - - :/icon_ceiling_dc:/icon_ceiling_dc - - - Toggle DC reported ceiling - - - - - true - - - - :/icon_ceiling_calculated:/icon_ceiling_calculated - - - Toggle calculated ceiling - - - - - true - - - - :/icon_ceiling_alltissues:/icon_ceiling_alltissues - - - Toggle calculating all tissues - - - - - true - - - - :/icon_ceiling_3m:/icon_ceiling_3m - - - Toggle calculated ceiling with 3m increments - - - - - true - - - - :/icon_HR:/icon_HR - - - Toggle heart rate - - - - - true - - - - :/icon_mod:/icon_mod - - - Toggle MOD - - - - - true - - - - :/icon_ead:/icon_ead - - - Toggle EAD, END, EADD - - - - - true - - - - :/icon_NDLTTS:/icon_NDLTTS - - - Toggle NDL, TTS - - - - - true - - - - :/icon_lung:/icon_lung - - - Toggle SAC rate - - - - - true - - - - :/ruler:/ruler - - - Toggle ruler - - - - - true - - - - :/scale:/scale - - - Scale graph - - - - - true - - - - :/pictures:/pictures - - - Toggle pictures - - - - - true - - - - :/gaschange:/gaschange - - - Toggle tank bar - - - - - true - - - &Filter divelist - - - Ctrl+F - - - - - true - - - - :/icon_tissue:/icon_tissue - - - Toggle tissue graph - - - - - User &survey - - - - - &Undo - - - Ctrl+Z - - - - - &Redo - - - Ctrl+Shift+Z - - - - - &Find moved images - - - - - Open c&loud storage - - - - - Save to clo&ud storage - - - - - &Manage dive sites - - - - - Dive Site &Edit - - - - - Facebook - - - - - - NotificationWidget - QWidget -
notificationwidget.h
- 1 -
- - MultiFilter - QWidget -
simplewidgets.h
- 1 -
-
- - - - -
diff --git a/qt-ui/marble/GeoDataTreeModel.h b/qt-ui/marble/GeoDataTreeModel.h deleted file mode 100644 index 39eff8388..000000000 --- a/qt-ui/marble/GeoDataTreeModel.h +++ /dev/null @@ -1,118 +0,0 @@ -// -// This file is part of the Marble Virtual Globe. -// -// This program is free software licensed under the GNU LGPL. You can -// find a copy of this license in LICENSE.txt in the top directory of -// the source code. -// -// Copyright 2010 Thibaut Gridel -// - -#ifndef MARBLE_GEODATATREEMODEL_H -#define MARBLE_GEODATATREEMODEL_H - -// -> does not appear to be needed #include "marble_export.h" - -#include - -class QItemSelectionModel; - -namespace Marble -{ -class GeoDataObject; -class GeoDataDocument; -class GeoDataFeature; -class GeoDataContainer; - - -/** - * @short The representation of GeoData in a model - * This class represents all available data given by kml-data files. - */ -class MARBLE_EXPORT GeoDataTreeModel : public QAbstractItemModel -{ - Q_OBJECT - - public: - - /** - * Creates a new GeoDataTreeModel. - * - * @param parent The parent object. - */ - explicit GeoDataTreeModel( QObject *parent = 0 ); - - /** - * Destroys the GeoDataModel. - */ - ~GeoDataTreeModel(); - - virtual bool hasChildren( const QModelIndex &parent ) const; - - /** - * Return the number of Items in the Model. - */ - int rowCount( const QModelIndex &parent = QModelIndex() ) const; - - QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const; - - QVariant data( const QModelIndex &index, int role ) const; - - QModelIndex index( int row, int column, - const QModelIndex &parent = QModelIndex() ) const; - - QModelIndex index( GeoDataObject* object ); - - QModelIndex parent( const QModelIndex &index ) const; - - int columnCount( const QModelIndex &parent = QModelIndex() ) const; - - Qt::ItemFlags flags ( const QModelIndex & index ) const; - - bool setData ( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole ); - - void reset(); - - QItemSelectionModel *selectionModel(); - -public Q_SLOTS: - - /** - * Sets the root document to use. This replaces previously loaded data, if any. - * @param document The new root document. Ownership retains with the caller, - * i.e. GeoDataTreeModel will not delete the passed document at its destruction. - */ - void setRootDocument( GeoDataDocument *document ); - GeoDataDocument* rootDocument(); - - int addFeature( GeoDataContainer *parent, GeoDataFeature *feature, int row = -1 ); - - bool removeFeature( GeoDataContainer *parent, int index ); - - int removeFeature( const GeoDataFeature *feature ); - - void updateFeature( GeoDataFeature *feature ); - - int addDocument( GeoDataDocument *document ); - - void removeDocument( int index ); - - void removeDocument( GeoDataDocument* document ); - - void update(); - -Q_SIGNALS: - /// insert and remove row don't trigger any signal that proxies forward - /// this signal will refresh geometry layer and placemark layout - void removed( GeoDataObject *object ); - void added( GeoDataObject *object ); - private: - Q_DISABLE_COPY( GeoDataTreeModel ) - class Private; - Private* const d; -}; - -} - -#endif // MARBLE_GEODATATREEMODEL_H diff --git a/qt-ui/metrics.cpp b/qt-ui/metrics.cpp deleted file mode 100644 index 203c2e5e2..000000000 --- a/qt-ui/metrics.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/* - * metrics.cpp - * - * methods to find/compute essential UI metrics - * (font properties, icon sizes, etc) - * - */ - -#include "metrics.h" - -static IconMetrics dfltIconMetrics = { -1 }; - -QFont defaultModelFont() -{ - QFont font; -// font.setPointSizeF(font.pointSizeF() * 0.8); - return font; -} - -QFontMetrics defaultModelFontMetrics() -{ - return QFontMetrics(defaultModelFont()); -} - -// return the default icon size, computed as the multiple of 16 closest to -// the given height -static int defaultIconSize(int height) -{ - int ret = (height + 8)/16; - ret *= 16; - if (ret < 16) - ret = 16; - return ret; -} - -const IconMetrics & defaultIconMetrics() -{ - if (dfltIconMetrics.sz_small == -1) { - int small = defaultIconSize(defaultModelFontMetrics().height()); - dfltIconMetrics.sz_small = small; - dfltIconMetrics.sz_med = small + small/2; - dfltIconMetrics.sz_big = 2*small; - - dfltIconMetrics.sz_pic = 8*small; - - dfltIconMetrics.spacing = small/8; - } - - return dfltIconMetrics; -} diff --git a/qt-ui/metrics.h b/qt-ui/metrics.h deleted file mode 100644 index 30295a3d8..000000000 --- a/qt-ui/metrics.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * metrics.h - * - * header file for common function to find/compute essential UI metrics - * (font properties, icon sizes, etc) - * - */ -#ifndef METRICS_H -#define METRICS_H - -#include -#include -#include - -QFont defaultModelFont(); -QFontMetrics defaultModelFontMetrics(); - -// Collection of icon/picture sizes and other metrics, resolution independent -struct IconMetrics { - // icon sizes - int sz_small; // ex 16px - int sz_med; // ex 24px - int sz_big; // ex 32px - // picture size - int sz_pic; // ex 128px - // icon spacing - int spacing; // ex 2px -}; - -const IconMetrics & defaultIconMetrics(); - -#endif // METRICS_H diff --git a/qt-ui/modeldelegates.cpp b/qt-ui/modeldelegates.cpp deleted file mode 100644 index 881037a83..000000000 --- a/qt-ui/modeldelegates.cpp +++ /dev/null @@ -1,617 +0,0 @@ -#include "modeldelegates.h" -#include "dive.h" -#include "gettextfromc.h" -#include "mainwindow.h" -#include "cylindermodel.h" -#include "models.h" -#include "starwidget.h" -#include "profile/profilewidget2.h" -#include "tankinfomodel.h" -#include "weigthsysteminfomodel.h" -#include "weightmodel.h" -#include "divetripmodel.h" -#include "qthelper.h" -#ifndef NO_MARBLE -#include "globe.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -QSize DiveListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - return QSize(50, 22); -} - -// Gets the index of the model in the currentRow and column. -// currCombo is defined below. -#define IDX(_XX) mymodel->index(currCombo.currRow, (_XX)) -static bool keyboardFinished = false; - -StarWidgetsDelegate::StarWidgetsDelegate(QWidget *parent) : QStyledItemDelegate(parent), - parentWidget(parent) -{ - const IconMetrics& metrics = defaultIconMetrics(); - minStarSize = QSize(metrics.sz_small * TOTALSTARS + metrics.spacing * (TOTALSTARS - 1), metrics.sz_small); -} - -void StarWidgetsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - QStyledItemDelegate::paint(painter, option, index); - if (!index.isValid()) - return; - - QVariant value = index.model()->data(index, DiveTripModel::STAR_ROLE); - if (!value.isValid()) - return; - - int rating = value.toInt(); - int deltaY = option.rect.height() / 2 - StarWidget::starActive().height() / 2; - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - const QPixmap active = QPixmap::fromImage(StarWidget::starActive()); - const QPixmap inactive = QPixmap::fromImage(StarWidget::starInactive()); - const IconMetrics& metrics = defaultIconMetrics(); - - for (int i = 0; i < rating; i++) - painter->drawPixmap(option.rect.x() + i * metrics.sz_small + metrics.spacing, option.rect.y() + deltaY, active); - for (int i = rating; i < TOTALSTARS; i++) - painter->drawPixmap(option.rect.x() + i * metrics.sz_small + metrics.spacing, option.rect.y() + deltaY, inactive); - painter->restore(); -} - -QSize StarWidgetsDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - return minStarSize; -} - -const QSize& StarWidgetsDelegate::starSize() const -{ - return minStarSize; -} - -ComboBoxDelegate::ComboBoxDelegate(QAbstractItemModel *model, QObject *parent) : QStyledItemDelegate(parent), model(model) -{ - connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), - this, SLOT(revertModelData(QWidget *, QAbstractItemDelegate::EndEditHint))); - connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), - this, SLOT(fixTabBehavior())); -} - -void ComboBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const -{ - QComboBox *c = qobject_cast(editor); - QString data = index.model()->data(index, Qt::DisplayRole).toString(); - int i = c->findText(data); - if (i != -1) - c->setCurrentIndex(i); - else - c->setEditText(data); - c->lineEdit()->setSelection(0, c->lineEdit()->text().length()); -} - -struct CurrSelected { - QComboBox *comboEditor; - int currRow; - QString activeText; - QAbstractItemModel *model; - bool ignoreSelection; -} currCombo; - -QWidget *ComboBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - MainWindow *m = MainWindow::instance(); - QComboBox *comboDelegate = new QComboBox(parent); - comboDelegate->setModel(model); - comboDelegate->setEditable(true); - comboDelegate->setAutoCompletion(true); - comboDelegate->setAutoCompletionCaseSensitivity(Qt::CaseInsensitive); - comboDelegate->completer()->setCompletionMode(QCompleter::PopupCompletion); - comboDelegate->view()->setEditTriggers(QAbstractItemView::AllEditTriggers); - comboDelegate->lineEdit()->installEventFilter(const_cast(qobject_cast(this))); - if ((m->graphics()->currentState != ProfileWidget2::PROFILE)) - comboDelegate->lineEdit()->setEnabled(false); - comboDelegate->view()->installEventFilter(const_cast(qobject_cast(this))); - QAbstractItemView *comboPopup = comboDelegate->lineEdit()->completer()->popup(); - comboPopup->setMouseTracking(true); - connect(comboDelegate, SIGNAL(highlighted(QString)), this, SLOT(testActivation(QString))); - connect(comboDelegate, SIGNAL(activated(QString)), this, SLOT(fakeActivation())); - connect(comboPopup, SIGNAL(entered(QModelIndex)), this, SLOT(testActivation(QModelIndex))); - connect(comboPopup, SIGNAL(activated(QModelIndex)), this, SLOT(fakeActivation())); - currCombo.comboEditor = comboDelegate; - currCombo.currRow = index.row(); - currCombo.model = const_cast(index.model()); - keyboardFinished = false; - - // Current display of things on Gnome3 looks like shit, so - // let`s fix that. - if (isGnome3Session()) { - QPalette p; - p.setColor(QPalette::Window, QColor(Qt::white)); - p.setColor(QPalette::Base, QColor(Qt::white)); - comboDelegate->lineEdit()->setPalette(p); - comboDelegate->setPalette(p); - } - return comboDelegate; -} - -/* This Method is being called when the user *writes* something and press enter or tab, - * and it`s also called when the mouse walks over the list of choices from the ComboBox, - * One thing is important, if the user writes a *new* cylinder or weight type, it will - * be ADDED to the list, and the user will need to fill the other data. - */ -void ComboBoxDelegate::testActivation(const QString &currText) -{ - currCombo.activeText = currText.isEmpty() ? currCombo.comboEditor->currentText() : currText; - setModelData(currCombo.comboEditor, currCombo.model, QModelIndex()); -} - -void ComboBoxDelegate::testActivation(const QModelIndex &currIndex) -{ - testActivation(currIndex.data().toString()); -} - -// HACK, send a fake event so Qt thinks we hit 'enter' on the line edit. -void ComboBoxDelegate::fakeActivation() -{ - /* this test is needed because as soon as I show the selector, - * the first item gots selected, this sending an activated signal, - * calling this fakeActivation code and setting as the current, - * thig that we don't want. so, let's just set the ignoreSelection - * to false and be happy, because the next activation ( by click - * or keypress) is real. - */ - if (currCombo.ignoreSelection) { - currCombo.ignoreSelection = false; - return; - } - QKeyEvent ev(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); - QStyledItemDelegate::eventFilter(currCombo.comboEditor, &ev); -} - -// This 'reverts' the model data to what we actually choosed, -// becaus e a TAB is being understood by Qt as 'cancel' while -// we are on a QComboBox ( but not on a QLineEdit. -void ComboBoxDelegate::fixTabBehavior() -{ - if (keyboardFinished) { - setModelData(0, 0, QModelIndex()); - } -} - -bool ComboBoxDelegate::eventFilter(QObject *object, QEvent *event) -{ - // Reacts on Key_UP and Key_DOWN to show the QComboBox - list of choices. - if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { - if (object == currCombo.comboEditor) { // the 'LineEdit' part - QKeyEvent *ev = static_cast(event); - if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down) { - currCombo.ignoreSelection = true; - if (!currCombo.comboEditor->completer()->popup()->isVisible()) { - currCombo.comboEditor->showPopup(); - return true; - } - } - if (ev->key() == Qt::Key_Tab || ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { - currCombo.activeText = currCombo.comboEditor->currentText(); - keyboardFinished = true; - } - } else { // the 'Drop Down Menu' part. - QKeyEvent *ev = static_cast(event); - if (ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return || - ev->key() == Qt::Key_Tab || ev->key() == Qt::Key_Backtab || - ev->key() == Qt::Key_Escape) { - // treat Qt as a silly little boy - pretending that the key_return nwas pressed on the combo, - // instead of the list of choices. this can be extended later for - // other imputs, like tab navigation and esc. - QStyledItemDelegate::eventFilter(currCombo.comboEditor, event); - } - } - } - - return QStyledItemDelegate::eventFilter(object, event); -} - -void ComboBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - QRect defaultRect = option.rect; - defaultRect.setX(defaultRect.x() - 1); - defaultRect.setY(defaultRect.y() - 1); - defaultRect.setWidth(defaultRect.width() + 2); - defaultRect.setHeight(defaultRect.height() + 2); - editor->setGeometry(defaultRect); -} - -struct RevertCylinderData { - QString type; - int pressure; - int size; -} currCylinderData; - -void TankInfoDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &thisindex) const -{ - CylindersModel *mymodel = qobject_cast(currCombo.model); - TankInfoModel *tanks = TankInfoModel::instance(); - QModelIndexList matches = tanks->match(tanks->index(0, 0), Qt::DisplayRole, currCombo.activeText); - int row; - QString cylinderName = currCombo.activeText; - if (matches.isEmpty()) { - tanks->insertRows(tanks->rowCount(), 1); - tanks->setData(tanks->index(tanks->rowCount() - 1, 0), currCombo.activeText); - row = tanks->rowCount() - 1; - } else { - row = matches.first().row(); - cylinderName = matches.first().data().toString(); - } - int tankSize = tanks->data(tanks->index(row, TankInfoModel::ML)).toInt(); - int tankPressure = tanks->data(tanks->index(row, TankInfoModel::BAR)).toInt(); - - mymodel->setData(IDX(CylindersModel::TYPE), cylinderName, Qt::EditRole); - mymodel->passInData(IDX(CylindersModel::WORKINGPRESS), tankPressure); - mymodel->passInData(IDX(CylindersModel::SIZE), tankSize); -} - -TankInfoDelegate::TankInfoDelegate(QObject *parent) : ComboBoxDelegate(TankInfoModel::instance(), parent) -{ - connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), - this, SLOT(reenableReplot(QWidget *, QAbstractItemDelegate::EndEditHint))); -} - -void TankInfoDelegate::reenableReplot(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) -{ - MainWindow::instance()->graphics()->setReplot(true); - // FIXME: We need to replot after a cylinder is selected but the replot below overwrites - // the newly selected cylinder. - // MainWindow::instance()->graphics()->replot(); -} - -void TankInfoDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) -{ - if (hint == QAbstractItemDelegate::NoHint || - hint == QAbstractItemDelegate::RevertModelCache) { - CylindersModel *mymodel = qobject_cast(currCombo.model); - mymodel->setData(IDX(CylindersModel::TYPE), currCylinderData.type, Qt::EditRole); - mymodel->passInData(IDX(CylindersModel::WORKINGPRESS), currCylinderData.pressure); - mymodel->passInData(IDX(CylindersModel::SIZE), currCylinderData.size); - } -} - -QWidget *TankInfoDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - // ncreate editor needs to be called before because it will populate a few - // things in the currCombo global var. - QWidget *delegate = ComboBoxDelegate::createEditor(parent, option, index); - CylindersModel *mymodel = qobject_cast(currCombo.model); - cylinder_t *cyl = mymodel->cylinderAt(index); - currCylinderData.type = copy_string(cyl->type.description); - currCylinderData.pressure = cyl->type.workingpressure.mbar; - currCylinderData.size = cyl->type.size.mliter; - MainWindow::instance()->graphics()->setReplot(false); - return delegate; -} - -TankUseDelegate::TankUseDelegate(QObject *parent) : QStyledItemDelegate(parent) -{ -} - -QWidget *TankUseDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const -{ - QComboBox *comboBox = new QComboBox(parent); - for (int i = 0; i < NUM_GAS_USE; i++) - comboBox->addItem(gettextFromC::instance()->trGettext(cylinderuse_text[i])); - return comboBox; -} - -void TankUseDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const -{ - QComboBox *comboBox = qobject_cast(editor); - QString indexString = index.data().toString(); - comboBox->setCurrentIndex(cylinderuse_from_text(indexString.toUtf8().data())); -} - -void TankUseDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const -{ - QComboBox *comboBox = qobject_cast(editor); - model->setData(index, comboBox->currentIndex()); -} - -struct RevertWeightData { - QString type; - int weight; -} currWeight; - -void WSInfoDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) -{ - if (hint == QAbstractItemDelegate::NoHint || - hint == QAbstractItemDelegate::RevertModelCache) { - WeightModel *mymodel = qobject_cast(currCombo.model); - mymodel->setData(IDX(WeightModel::TYPE), currWeight.type, Qt::EditRole); - mymodel->passInData(IDX(WeightModel::WEIGHT), currWeight.weight); - } -} - -void WSInfoDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &thisindex) const -{ - WeightModel *mymodel = qobject_cast(currCombo.model); - WSInfoModel *wsim = WSInfoModel::instance(); - QModelIndexList matches = wsim->match(wsim->index(0, 0), Qt::DisplayRole, currCombo.activeText); - int row; - if (matches.isEmpty()) { - // we need to add this puppy - wsim->insertRows(wsim->rowCount(), 1); - wsim->setData(wsim->index(wsim->rowCount() - 1, 0), currCombo.activeText); - row = wsim->rowCount() - 1; - } else { - row = matches.first().row(); - } - int grams = wsim->data(wsim->index(row, WSInfoModel::GR)).toInt(); - QVariant v = QString(currCombo.activeText); - - mymodel->setData(IDX(WeightModel::TYPE), v, Qt::EditRole); - mymodel->passInData(IDX(WeightModel::WEIGHT), grams); -} - -WSInfoDelegate::WSInfoDelegate(QObject *parent) : ComboBoxDelegate(WSInfoModel::instance(), parent) -{ -} - -QWidget *WSInfoDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - /* First, call the combobox-create editor, it will setup our globals. */ - QWidget *editor = ComboBoxDelegate::createEditor(parent, option, index); - WeightModel *mymodel = qobject_cast(currCombo.model); - weightsystem_t *ws = mymodel->weightSystemAt(index); - currWeight.type = copy_string(ws->description); - currWeight.weight = ws->weight.grams; - return editor; -} - -void AirTypesDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) -{ -} - -void AirTypesDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const -{ - if (!index.isValid()) - return; - QComboBox *combo = qobject_cast(editor); - model->setData(index, QVariant(combo->currentText())); -} - -AirTypesDelegate::AirTypesDelegate(QObject *parent) : ComboBoxDelegate(GasSelectionModel::instance(), parent) -{ -} - -ProfilePrintDelegate::ProfilePrintDelegate(QObject *parent) : QStyledItemDelegate(parent) -{ -} - -static void paintRect(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) -{ - const QRect rect(option.rect); - const int row = index.row(); - const int col = index.column(); - - painter->save(); - // grid color - painter->setPen(QPen(QColor(0xff999999))); - // horizontal lines - if (row == 2 || row == 4 || row == 6) - painter->drawLine(rect.topLeft(), rect.topRight()); - if (row == 7) - painter->drawLine(rect.bottomLeft(), rect.bottomRight()); - // vertical lines - if (row > 1) { - painter->drawLine(rect.topLeft(), rect.bottomLeft()); - if (col == 4 || (col == 0 && row > 5)) - painter->drawLine(rect.topRight(), rect.bottomRight()); - } - painter->restore(); -} - -/* this method overrides the default table drawing method and places grid lines only at certain rows and columns */ -void ProfilePrintDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - paintRect(painter, option, index); - QStyledItemDelegate::paint(painter, option, index); -} - -SpinBoxDelegate::SpinBoxDelegate(int min, int max, int step, QObject *parent): - QStyledItemDelegate(parent), - min(min), - max(max), - step(step) -{ -} - -QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - QSpinBox *w = qobject_cast(QStyledItemDelegate::createEditor(parent, option, index)); - w->setRange(min,max); - w->setSingleStep(step); - return w; -} - -DoubleSpinBoxDelegate::DoubleSpinBoxDelegate(double min, double max, double step, QObject *parent): - QStyledItemDelegate(parent), - min(min), - max(max), - step(step) -{ -} - -QWidget *DoubleSpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - QDoubleSpinBox *w = qobject_cast(QStyledItemDelegate::createEditor(parent, option, index)); - w->setRange(min,max); - w->setSingleStep(step); - return w; -} - -HTMLDelegate::HTMLDelegate(QObject *parent) : ProfilePrintDelegate(parent) -{ -} - -void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const -{ - paintRect(painter, option, index); - QStyleOptionViewItemV4 options = option; - initStyleOption(&options, index); - painter->save(); - QTextDocument doc; - doc.setHtml(options.text); - doc.setTextWidth(options.rect.width()); - doc.setDefaultFont(options.font); - options.text.clear(); - options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter); - painter->translate(options.rect.left(), options.rect.top()); - QRect clip(0, 0, options.rect.width(), options.rect.height()); - doc.drawContents(painter, clip); - painter->restore(); -} - -QSize HTMLDelegate::sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const -{ - QStyleOptionViewItemV4 options = option; - initStyleOption(&options, index); - QTextDocument doc; - doc.setHtml(options.text); - doc.setTextWidth(options.rect.width()); - return QSize(doc.idealWidth(), doc.size().height()); -} - -LocationFilterDelegate::LocationFilterDelegate(QObject *parent) -{ -} - -void LocationFilterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &origIdx) const -{ - QFont fontBigger = qApp->font(); - QFont fontSmaller = qApp->font(); - QFontMetrics fmBigger(fontBigger); - QStyleOptionViewItemV4 opt = option; - const QAbstractProxyModel *proxyModel = dynamic_cast(origIdx.model()); - QModelIndex index = proxyModel->mapToSource(origIdx); - QStyledItemDelegate::initStyleOption(&opt, index); - QString diveSiteName = index.data().toString(); - QString bottomText; - QIcon icon = index.data(Qt::DecorationRole).value(); - struct dive_site *ds = get_dive_site_by_uuid( - index.model()->data(index.model()->index(index.row(),0)).toInt() - ); - //Special case: do not show name, but instead, show - if (index.row() < 2) { - diveSiteName = index.data().toString(); - bottomText = index.data(Qt::ToolTipRole).toString(); - goto print_part; - } - - if (!ds) - return; - - for (int i = 0; i < 3; i++) { - if (prefs.geocoding.category[i] == TC_NONE) - continue; - int idx = taxonomy_index_for_category(&ds->taxonomy, prefs.geocoding.category[i]); - if (idx == -1) - continue; - if(!bottomText.isEmpty()) - bottomText += " / "; - bottomText += QString(ds->taxonomy.category[idx].value); - } - - if (bottomText.isEmpty()) { - const char *gpsCoords = printGPSCoords(ds->latitude.udeg, ds->longitude.udeg); - bottomText = QString(gpsCoords); - free( (void*) gpsCoords); - } - - if (dive_site_has_gps_location(ds) && dive_site_has_gps_location(&displayed_dive_site)) { - // so we are showing a completion and both the current dive site and the completion - // have a GPS fix... so let's show the distance - if (ds->latitude.udeg == displayed_dive_site.latitude.udeg && - ds->longitude.udeg == displayed_dive_site.longitude.udeg) { - bottomText += tr(" (same GPS fix)"); - } else { - int distanceMeters = get_distance(ds->latitude, ds->longitude, displayed_dive_site.latitude, displayed_dive_site.longitude); - QString distance = distance_string(distanceMeters); - int nr = nr_of_dives_at_dive_site(ds->uuid, false); - bottomText += tr(" (~%1 away").arg(distance); - bottomText += tr(", %n dive(s) here)", "", nr); - } - } - if (bottomText.isEmpty()) { - if (dive_site_has_gps_location(&displayed_dive_site)) - bottomText = tr("(no existing GPS data, add GPS fix from this dive)"); - else - bottomText = tr("(no GPS data)"); - } - bottomText = tr("Pick site: ") + bottomText; - -print_part: - - fontBigger.setPointSize(fontBigger.pointSize() + 1); - fontBigger.setBold(true); - QPen textPen = QPen(option.state & QStyle::State_Selected ? option.palette.highlightedText().color() : option.palette.text().color(), 1); - - initStyleOption(&opt, index); - opt.text = QString(); - opt.icon = QIcon(); - painter->setClipRect(option.rect); - - painter->save(); - if (option.state & QStyle::State_Selected) { - painter->setPen(QPen(opt.palette.highlight().color().darker())); - painter->setBrush(opt.palette.highlight()); - const qreal pad = 1.0; - const qreal pad2 = pad * 2.0; - const qreal rounding = 5.0; - painter->drawRoundedRect(option.rect.x() + pad, - option.rect.y() + pad, - option.rect.width() - pad2, - option.rect.height() - pad2, - rounding, rounding); - } - painter->setPen(textPen); - painter->setFont(fontBigger); - const qreal textPad = 5.0; - painter->drawText(option.rect.x() + textPad, option.rect.y() + fmBigger.boundingRect("YH").height(), diveSiteName); - double pointSize = fontSmaller.pointSizeF(); - fontSmaller.setPointSizeF(0.9 * pointSize); - painter->setFont(fontSmaller); - painter->drawText(option.rect.x() + textPad, option.rect.y() + fmBigger.boundingRect("YH").height() * 2, bottomText); - painter->restore(); - - if (!icon.isNull()) { - painter->save(); - painter->drawPixmap( - option.rect.x() + option.rect.width() - 24, - option.rect.y() + option.rect.height() - 24, icon.pixmap(20,20)); - painter->restore(); - } -} - -QSize LocationFilterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - QFont fontBigger = qApp->font(); - fontBigger.setPointSize(fontBigger.pointSize()); - fontBigger.setBold(true); - - QFontMetrics fmBigger(fontBigger); - - QFont fontSmaller = qApp->font(); - QFontMetrics fmSmaller(fontSmaller); - - QSize retSize = QStyledItemDelegate::sizeHint(option, index); - retSize.setHeight( - fmBigger.boundingRect("Yellow House").height() + 5 /*spacing*/ + - fmSmaller.boundingRect("Yellow House").height()); - - return retSize; -} diff --git a/qt-ui/modeldelegates.h b/qt-ui/modeldelegates.h deleted file mode 100644 index 95701775a..000000000 --- a/qt-ui/modeldelegates.h +++ /dev/null @@ -1,141 +0,0 @@ -#ifndef MODELDELEGATES_H -#define MODELDELEGATES_H - -#include -#include -class QPainter; - -class DiveListDelegate : public QStyledItemDelegate { -public: - explicit DiveListDelegate(QObject *parent = 0) - : QStyledItemDelegate(parent) - { - } - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; -}; - -class StarWidgetsDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - explicit StarWidgetsDelegate(QWidget *parent = 0); - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; - const QSize& starSize() const; - -private: - QWidget *parentWidget; - QSize minStarSize; -}; - -class ComboBoxDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - explicit ComboBoxDelegate(QAbstractItemModel *model, QObject *parent = 0); - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; - virtual void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual bool eventFilter(QObject *object, QEvent *event); -public -slots: - void testActivation(const QString &currString = QString()); - void testActivation(const QModelIndex &currIndex); - //HACK: try to get rid of this in the future. - void fakeActivation(); - void fixTabBehavior(); - virtual void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) = 0; - -protected: - QAbstractItemModel *model; -}; - -class TankInfoDelegate : public ComboBoxDelegate { - Q_OBJECT -public: - explicit TankInfoDelegate(QObject *parent = 0); - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; -public -slots: - void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); - void reenableReplot(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); -}; - -class TankUseDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - explicit TankUseDelegate(QObject *parent = 0); - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual void setEditorData(QWidget * editor, const QModelIndex & index) const; -}; - -class WSInfoDelegate : public ComboBoxDelegate { - Q_OBJECT -public: - explicit WSInfoDelegate(QObject *parent = 0); - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; -public -slots: - void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); -}; - -class AirTypesDelegate : public ComboBoxDelegate { - Q_OBJECT -public: - explicit AirTypesDelegate(QObject *parent = 0); - virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; -public -slots: - void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); -}; - -/* ProfilePrintDelagate: - * this delegate is used to modify the look of the table that is printed - * bellow profiles. - */ -class ProfilePrintDelegate : public QStyledItemDelegate { -public: - explicit ProfilePrintDelegate(QObject *parent = 0); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; -}; - -class SpinBoxDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - SpinBoxDelegate(int min, int max, int step, QObject *parent = 0); - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; -private: - int min; - int max; - int step; -}; - -class DoubleSpinBoxDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - DoubleSpinBoxDelegate(double min, double max, double step, QObject *parent = 0); - virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; -private: - double min; - double max; - double step; -}; - -class HTMLDelegate : public ProfilePrintDelegate { - Q_OBJECT -public: - explicit HTMLDelegate(QObject *parent = 0); - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; - virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; -}; - -class LocationFilterDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - LocationFilterDelegate(QObject *parent = 0); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; -}; - -#endif // MODELDELEGATES_H diff --git a/qt-ui/notificationwidget.cpp b/qt-ui/notificationwidget.cpp deleted file mode 100644 index 103c0d068..000000000 --- a/qt-ui/notificationwidget.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "notificationwidget.h" - -NotificationWidget::NotificationWidget(QWidget *parent) : KMessageWidget(parent) -{ - future_watcher = new QFutureWatcher(); - connect(future_watcher, SIGNAL(finished()), this, SLOT(finish())); -} - -void NotificationWidget::showNotification(QString message, KMessageWidget::MessageType type) -{ - if (message.isEmpty()) - return; - setText(message); - setCloseButtonVisible(true); - setMessageType(type); - animatedShow(); -} - -void NotificationWidget::hideNotification() -{ - animatedHide(); -} - -QString NotificationWidget::getNotificationText() -{ - return text(); -} - -void NotificationWidget::setFuture(const QFuture &future) -{ - future_watcher->setFuture(future); -} - -void NotificationWidget::finish() -{ - hideNotification(); -} - -NotificationWidget::~NotificationWidget() -{ - delete future_watcher; -} diff --git a/qt-ui/notificationwidget.h b/qt-ui/notificationwidget.h deleted file mode 100644 index 8a551a0b3..000000000 --- a/qt-ui/notificationwidget.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef NOTIFICATIONWIDGET_H -#define NOTIFICATIONWIDGET_H - -#include -#include - -#include - -namespace Ui { - class NotificationWidget; -} - -class NotificationWidget : public KMessageWidget { - Q_OBJECT - -public: - explicit NotificationWidget(QWidget *parent = 0); - void setFuture(const QFuture &future); - void showNotification(QString message, KMessageWidget::MessageType type); - void hideNotification(); - QString getNotificationText(); - ~NotificationWidget(); - -private: - QFutureWatcher *future_watcher; - -private -slots: - void finish(); -}; - -#endif // NOTIFICATIONWIDGET_H diff --git a/qt-ui/plannerDetails.ui b/qt-ui/plannerDetails.ui deleted file mode 100644 index 1f2790d85..000000000 --- a/qt-ui/plannerDetails.ui +++ /dev/null @@ -1,104 +0,0 @@ - - - plannerDetails - - - - 0 - 0 - 400 - 300 - - - - Form - - - - 5 - - - 5 - - - 5 - - - 5 - - - 5 - - - - - - - - 16777215 - 20 - - - - <html><head/><body><p><span style=" font-weight:600;">Dive plan details</span></p></body></html> - - - Qt::RichText - - - - - - - - 0 - 0 - - - - Print - - - false - - - false - - - false - - - - - - - - - true - - - - 0 - 0 - - - - font: 13pt "Courier"; - - - true - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Courier'; font-size:13pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.Curier New';"><br /></p></body></html> - - - - - - - - diff --git a/qt-ui/plannerSettings.ui b/qt-ui/plannerSettings.ui deleted file mode 100644 index 4db69f883..000000000 --- a/qt-ui/plannerSettings.ui +++ /dev/null @@ -1,749 +0,0 @@ - - - plannerSettingsWidget - - - - 0 - 0 - 1102 - 442 - - - - Form - - - - 5 - - - 5 - - - 5 - - - 5 - - - - - QFrame::NoFrame - - - QFrame::Plain - - - true - - - - - 0 - 0 - 1092 - 432 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - Rates - - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - Ascent - - - - 12 - - - - - below 75% avg. depth - - - - - - - m/min - - - 1 - - - - - - - 75% to 50% avg. depth - - - - - - - m/min - - - 1 - - - - - - - 50% avg. depth to 6m - - - - - - - m/min - - - 1 - - - - - - - 6m to surface - - - - - - - m/min - - - 1 - - - - - - - - - - Descent - - - - - - - 0 - 0 - - - - surface to the bottom - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - m/min - - - 1 - - - 18 - - - - - descRate - descent - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - Planning - - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - VPM-B deco - - - - - - - Bühlmann deco - - - true - - - - - - - Reserve gas - - - 26 - - - - - - - bar - - - - - - 10 - - - 99 - - - 40 - - - - - - - % - - - 1 - - - 150 - - - - - - - Postpone gas change if a stop is not required - - - Only switch at required stops - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - GF low - - - 26 - - - - - - - Plan backgas breaks - - - - - - - GF high - - - 25 - - - - - - - min - - - - - - 0 - - - 9 - - - 1 - - - - - - - Last stop at 6m - - - - - - - Maximize bottom time allowed by gas and no decompression limits - - - Recreational mode - - - - - - - - - - 6 - - - - - - - % - - - 1 - - - 150 - - - - - - - Drop to first depth - - - - - - - Min. switch duration - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::LeftToRight - - - Safety stop - - - false - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - Conservatism level - - - 25 - - - - - - - 4 - - - - - - - - - - Gas options - - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - bar - - - 2.000000000000000 - - - 0.100000000000000 - - - 1.400000000000000 - - - - - - - â„“/min - - - 0 - - - 99.000000000000000 - - - - - - - bar - - - 2.000000000000000 - - - 0.100000000000000 - - - 1.600000000000000 - - - - - - - Bottom SAC - - - - - - - Bottom pOâ‚‚ - - - - - - - Notes - - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - In dive plan, show runtime (absolute time) of stops - - - Display runtime - - - - - - - true - - - In dive plan, show duration (relative time) of stops - - - Display segment duration - - - - - - - In diveplan, list transitions or treat them as implicit - - - Display transitions in deco - - - - - - - Verbatim dive plan - - - - - - - - - - â„“/min - - - 0 - - - 99.000000000000000 - - - - - - - Deco pOâ‚‚ - - - - - - - Deco SAC - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - - - - - - - - scrollArea - ascRate75 - ascRate50 - ascRateStops - ascRateLast6m - descRate - recreational_deco - reserve_gas - safetystop - buehlmann_deco - gflow - gfhigh - vpmb_deco - drop_stone_mode - lastStop - backgasBreaks - switch_at_req_stop - min_switch_duration - rebreathermode - bottomSAC - decoStopSAC - bottompo2 - decopo2 - display_runtime - display_duration - display_transitions - verbatim_plan - - - - diff --git a/qt-ui/preferences.cpp b/qt-ui/preferences.cpp deleted file mode 100644 index 6450c41cb..000000000 --- a/qt-ui/preferences.cpp +++ /dev/null @@ -1,559 +0,0 @@ -#include "preferences.h" -#include "mainwindow.h" -#include "models.h" -#include "divelocationmodel.h" -#include "prefs-macros.h" -#include "qthelper.h" -#include "subsurfacestartup.h" - -#include -#include -#include -#include -#include -#include - -#include "subsurfacewebservices.h" - -#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) -#include "socialnetworks.h" -#include -#endif - -PreferencesDialog *PreferencesDialog::instance() -{ - static PreferencesDialog *dialog = new PreferencesDialog(MainWindow::instance()); - return dialog; -} - -PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) -{ - ui.setupUi(this); - setAttribute(Qt::WA_QuitOnClose, false); - -#if defined(Q_OS_ANDROID) || !defined(FBSUPPORT) - for (int i = 0; i < ui.listWidget->count(); i++) { - if (ui.listWidget->item(i)->text() == "Facebook") { - delete ui.listWidget->item(i); - QWidget *fbpage = ui.stackedWidget->widget(i); - ui.stackedWidget->removeWidget(fbpage); - } - } -#endif - - ui.proxyType->clear(); - ui.proxyType->addItem(tr("No proxy"), QNetworkProxy::NoProxy); - ui.proxyType->addItem(tr("System proxy"), QNetworkProxy::DefaultProxy); - ui.proxyType->addItem(tr("HTTP proxy"), QNetworkProxy::HttpProxy); - ui.proxyType->addItem(tr("SOCKS proxy"), QNetworkProxy::Socks5Proxy); - ui.proxyType->setCurrentIndex(-1); - - ui.first_item->setModel(GeoReferencingOptionsModel::instance()); - ui.second_item->setModel(GeoReferencingOptionsModel::instance()); - ui.third_item->setModel(GeoReferencingOptionsModel::instance()); - // Facebook stuff: -#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) - FacebookManager *fb = FacebookManager::instance(); - facebookWebView = new QWebView(this); - ui.fbWebviewContainer->layout()->addWidget(facebookWebView); - if (fb->loggedIn()) { - facebookLoggedIn(); - } else { - facebookDisconnect(); - } - connect(facebookWebView, &QWebView::urlChanged, fb, &FacebookManager::tryLogin); - connect(fb, &FacebookManager::justLoggedIn, this, &PreferencesDialog::facebookLoggedIn); - connect(ui.fbDisconnect, &QPushButton::clicked, fb, &FacebookManager::logout); - connect(fb, &FacebookManager::justLoggedOut, this, &PreferencesDialog::facebookDisconnect); -#endif - connect(ui.proxyType, SIGNAL(currentIndexChanged(int)), this, SLOT(proxyType_changed(int))); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - connect(ui.gflow, SIGNAL(valueChanged(int)), this, SLOT(gflowChanged(int))); - connect(ui.gfhigh, SIGNAL(valueChanged(int)), this, SLOT(gfhighChanged(int))); - // connect(ui.defaultSetpoint, SIGNAL(valueChanged(double)), this, SLOT(defaultSetpointChanged(double))); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); - loadSettings(); - setUiFromPrefs(); - rememberPrefs(); -} - -void PreferencesDialog::facebookLoggedIn() -{ -#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) - ui.fbDisconnect->show(); - ui.fbWebviewContainer->hide(); - ui.fbWebviewContainer->setEnabled(false); - ui.FBLabel->setText(tr("To disconnect Subsurface from your Facebook account, use the button below")); -#endif -} - -void PreferencesDialog::facebookDisconnect() -{ -#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) - // remove the connect/disconnect button - // and instead add the login view - ui.fbDisconnect->hide(); - ui.fbWebviewContainer->show(); - ui.fbWebviewContainer->setEnabled(true); - ui.FBLabel->setText(tr("To connect to Facebook, please log in. This enables Subsurface to publish dives to your timeline")); - if (facebookWebView) { - facebookWebView->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar()); - facebookWebView->setUrl(FacebookManager::instance()->connectUrl()); - } -#endif -} - -void PreferencesDialog::cloudPinNeeded() -{ - ui.cloud_storage_pin->setEnabled(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); - ui.cloud_storage_pin->setVisible(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); - ui.cloud_storage_pin_label->setEnabled(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); - ui.cloud_storage_pin_label->setVisible(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); - ui.cloud_storage_new_passwd->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); - ui.cloud_storage_new_passwd->setVisible(prefs.cloud_verification_status == CS_VERIFIED); - ui.cloud_storage_new_passwd_label->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); - ui.cloud_storage_new_passwd_label->setVisible(prefs.cloud_verification_status == CS_VERIFIED); - if (prefs.cloud_verification_status == CS_VERIFIED) { - ui.cloudStorageGroupBox->setTitle(tr("Subsurface cloud storage (credentials verified)")); - ui.cloudDefaultFile->setEnabled(true); - } else { - ui.cloudStorageGroupBox->setTitle(tr("Subsurface cloud storage")); - if (ui.cloudDefaultFile->isChecked()) - ui.noDefaultFile->setChecked(true); - ui.cloudDefaultFile->setEnabled(false); - } - MainWindow::instance()->enableDisableCloudActions(); -} - -#define DANGER_GF (gf > 100) ? "* { color: red; }" : "" -void PreferencesDialog::gflowChanged(int gf) -{ - ui.gflow->setStyleSheet(DANGER_GF); -} -void PreferencesDialog::gfhighChanged(int gf) -{ - ui.gfhigh->setStyleSheet(DANGER_GF); -} -#undef DANGER_GF - -void PreferencesDialog::showEvent(QShowEvent *event) -{ - setUiFromPrefs(); - rememberPrefs(); - QDialog::showEvent(event); -} - -void PreferencesDialog::setUiFromPrefs() -{ - // graphs - ui.pheThreshold->setValue(prefs.pp_graphs.phe_threshold); - ui.po2Threshold->setValue(prefs.pp_graphs.po2_threshold); - ui.pn2Threshold->setValue(prefs.pp_graphs.pn2_threshold); - ui.maxpo2->setValue(prefs.modpO2); - ui.red_ceiling->setChecked(prefs.redceiling); - ui.units_group->setEnabled(ui.personalize->isChecked()); - - ui.gflow->setValue(prefs.gflow); - ui.gfhigh->setValue(prefs.gfhigh); - ui.gf_low_at_maxdepth->setChecked(prefs.gf_low_at_maxdepth); - ui.show_ccr_setpoint->setChecked(prefs.show_ccr_setpoint); - ui.show_ccr_sensors->setChecked(prefs.show_ccr_sensors); - ui.defaultSetpoint->setValue((double)prefs.defaultsetpoint / 1000.0); - ui.psro2rate->setValue(prefs.o2consumption / 1000.0); - ui.pscrfactor->setValue(rint(1000.0 / prefs.pscr_ratio)); - - // units - if (prefs.unit_system == METRIC) - ui.metric->setChecked(true); - else if (prefs.unit_system == IMPERIAL) - ui.imperial->setChecked(true); - else - ui.personalize->setChecked(true); - ui.gpsTraditional->setChecked(prefs.coordinates_traditional); - ui.gpsDecimal->setChecked(!prefs.coordinates_traditional); - - ui.celsius->setChecked(prefs.units.temperature == units::CELSIUS); - ui.fahrenheit->setChecked(prefs.units.temperature == units::FAHRENHEIT); - ui.meter->setChecked(prefs.units.length == units::METERS); - ui.feet->setChecked(prefs.units.length == units::FEET); - ui.bar->setChecked(prefs.units.pressure == units::BAR); - ui.psi->setChecked(prefs.units.pressure == units::PSI); - ui.liter->setChecked(prefs.units.volume == units::LITER); - ui.cuft->setChecked(prefs.units.volume == units::CUFT); - ui.kg->setChecked(prefs.units.weight == units::KG); - ui.lbs->setChecked(prefs.units.weight == units::LBS); - - ui.font->setCurrentFont(QString(prefs.divelist_font)); - ui.fontsize->setValue(prefs.font_size); - ui.defaultfilename->setText(prefs.default_filename); - ui.noDefaultFile->setChecked(prefs.default_file_behavior == NO_DEFAULT_FILE); - ui.cloudDefaultFile->setChecked(prefs.default_file_behavior == CLOUD_DEFAULT_FILE); - ui.localDefaultFile->setChecked(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); - ui.default_cylinder->clear(); - for (int i = 0; tank_info[i].name != NULL; i++) { - ui.default_cylinder->addItem(tank_info[i].name); - if (prefs.default_cylinder && strcmp(tank_info[i].name, prefs.default_cylinder) == 0) - ui.default_cylinder->setCurrentIndex(i); - } - ui.displayinvalid->setChecked(prefs.display_invalid_dives); - ui.display_unused_tanks->setChecked(prefs.display_unused_tanks); - ui.show_average_depth->setChecked(prefs.show_average_depth); - ui.vertical_speed_minutes->setChecked(prefs.units.vertical_speed_time == units::MINUTES); - ui.vertical_speed_seconds->setChecked(prefs.units.vertical_speed_time == units::SECONDS); - ui.velocitySlider->setValue(prefs.animation_speed); - - QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(); - filterModel->setSourceModel(LanguageModel::instance()); - filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - ui.languageView->setModel(filterModel); - filterModel->sort(0); - connect(ui.languageFilter, SIGNAL(textChanged(QString)), filterModel, SLOT(setFilterFixedString(QString))); - - QSettings s; - - ui.save_uid_local->setChecked(s.value("save_uid_local").toBool()); - ui.default_uid->setText(s.value("subsurface_webservice_uid").toString().toUpper()); - - s.beginGroup("Language"); - ui.languageSystemDefault->setChecked(s.value("UseSystemLanguage", true).toBool()); - QAbstractItemModel *m = ui.languageView->model(); - QModelIndexList languages = m->match(m->index(0, 0), Qt::UserRole, s.value("UiLanguage").toString()); - if (languages.count()) - ui.languageView->setCurrentIndex(languages.first()); - - s.endGroup(); - - ui.proxyHost->setText(prefs.proxy_host); - ui.proxyPort->setValue(prefs.proxy_port); - ui.proxyAuthRequired->setChecked(prefs.proxy_auth); - ui.proxyUsername->setText(prefs.proxy_user); - ui.proxyPassword->setText(prefs.proxy_pass); - ui.proxyType->setCurrentIndex(ui.proxyType->findData(prefs.proxy_type)); - ui.btnUseDefaultFile->setChecked(prefs.use_default_file); - - ui.cloud_storage_email->setText(prefs.cloud_storage_email); - ui.cloud_storage_password->setText(prefs.cloud_storage_password); - ui.save_password_local->setChecked(prefs.save_password_local); - cloudPinNeeded(); - ui.cloud_background_sync->setChecked(prefs.cloud_background_sync); - ui.default_uid->setText(prefs.userid); - - // GeoManagement -#ifdef DISABLED - ui.enable_geocoding->setChecked( prefs.geocoding.enable_geocoding ); - ui.parse_without_gps->setChecked(prefs.geocoding.parse_dive_without_gps); - ui.tag_existing_dives->setChecked(prefs.geocoding.tag_existing_dives); -#endif - ui.first_item->setCurrentIndex(prefs.geocoding.category[0]); - ui.second_item->setCurrentIndex(prefs.geocoding.category[1]); - ui.third_item->setCurrentIndex(prefs.geocoding.category[2]); -} - -void PreferencesDialog::restorePrefs() -{ - prefs = oldPrefs; - setUiFromPrefs(); -} - -void PreferencesDialog::rememberPrefs() -{ - oldPrefs = prefs; -} - -void PreferencesDialog::syncSettings() -{ - QSettings s; - - s.setValue("subsurface_webservice_uid", ui.default_uid->text().toUpper()); - set_save_userid_local(ui.save_uid_local->checkState()); - - // Graph - s.beginGroup("TecDetails"); - SAVE_OR_REMOVE("phethreshold", default_prefs.pp_graphs.phe_threshold, ui.pheThreshold->value()); - SAVE_OR_REMOVE("po2threshold", default_prefs.pp_graphs.po2_threshold, ui.po2Threshold->value()); - SAVE_OR_REMOVE("pn2threshold", default_prefs.pp_graphs.pn2_threshold, ui.pn2Threshold->value()); - SAVE_OR_REMOVE("modpO2", default_prefs.modpO2, ui.maxpo2->value()); - SAVE_OR_REMOVE("redceiling", default_prefs.redceiling, ui.red_ceiling->isChecked()); - SAVE_OR_REMOVE("gflow", default_prefs.gflow, ui.gflow->value()); - SAVE_OR_REMOVE("gfhigh", default_prefs.gfhigh, ui.gfhigh->value()); - SAVE_OR_REMOVE("gf_low_at_maxdepth", default_prefs.gf_low_at_maxdepth, ui.gf_low_at_maxdepth->isChecked()); - SAVE_OR_REMOVE("show_ccr_setpoint", default_prefs.show_ccr_setpoint, ui.show_ccr_setpoint->isChecked()); - SAVE_OR_REMOVE("show_ccr_sensors", default_prefs.show_ccr_sensors, ui.show_ccr_sensors->isChecked()); - SAVE_OR_REMOVE("display_unused_tanks", default_prefs.display_unused_tanks, ui.display_unused_tanks->isChecked()); - SAVE_OR_REMOVE("show_average_depth", default_prefs.show_average_depth, ui.show_average_depth->isChecked()); - s.endGroup(); - - // Units - s.beginGroup("Units"); - QString unitSystem[] = {"metric", "imperial", "personal"}; - short unitValue = ui.metric->isChecked() ? METRIC : (ui.imperial->isChecked() ? IMPERIAL : PERSONALIZE); - SAVE_OR_REMOVE_SPECIAL("unit_system", default_prefs.unit_system, unitValue, unitSystem[unitValue]); - s.setValue("temperature", ui.fahrenheit->isChecked() ? units::FAHRENHEIT : units::CELSIUS); - s.setValue("length", ui.feet->isChecked() ? units::FEET : units::METERS); - s.setValue("pressure", ui.psi->isChecked() ? units::PSI : units::BAR); - s.setValue("volume", ui.cuft->isChecked() ? units::CUFT : units::LITER); - s.setValue("weight", ui.lbs->isChecked() ? units::LBS : units::KG); - s.setValue("vertical_speed_time", ui.vertical_speed_minutes->isChecked() ? units::MINUTES : units::SECONDS); - s.setValue("coordinates", ui.gpsTraditional->isChecked()); - s.endGroup(); - - // Defaults - s.beginGroup("GeneralSettings"); - s.setValue("default_filename", ui.defaultfilename->text()); - s.setValue("default_cylinder", ui.default_cylinder->currentText()); - s.setValue("use_default_file", ui.btnUseDefaultFile->isChecked()); - if (ui.noDefaultFile->isChecked()) - s.setValue("default_file_behavior", NO_DEFAULT_FILE); - else if (ui.localDefaultFile->isChecked()) - s.setValue("default_file_behavior", LOCAL_DEFAULT_FILE); - else if (ui.cloudDefaultFile->isChecked()) - s.setValue("default_file_behavior", CLOUD_DEFAULT_FILE); - s.setValue("defaultsetpoint", rint(ui.defaultSetpoint->value() * 1000.0)); - s.setValue("o2consumption", rint(ui.psro2rate->value() *1000.0)); - s.setValue("pscr_ratio", rint(1000.0 / ui.pscrfactor->value())); - s.endGroup(); - - s.beginGroup("Display"); - SAVE_OR_REMOVE_SPECIAL("divelist_font", system_divelist_default_font, ui.font->currentFont().toString(), ui.font->currentFont()); - SAVE_OR_REMOVE("font_size", system_divelist_default_font_size, ui.fontsize->value()); - s.setValue("displayinvalid", ui.displayinvalid->isChecked()); - s.endGroup(); - s.sync(); - - // Locale - QLocale loc; - s.beginGroup("Language"); - bool useSystemLang = s.value("UseSystemLanguage", true).toBool(); - if (useSystemLang != ui.languageSystemDefault->isChecked() || - (!useSystemLang && s.value("UiLanguage").toString() != ui.languageView->currentIndex().data(Qt::UserRole))) { - QMessageBox::warning(MainWindow::instance(), tr("Restart required"), - tr("To correctly load a new language you must restart Subsurface.")); - } - s.setValue("UseSystemLanguage", ui.languageSystemDefault->isChecked()); - s.setValue("UiLanguage", ui.languageView->currentIndex().data(Qt::UserRole)); - s.endGroup(); - - // Animation - s.beginGroup("Animations"); - s.setValue("animation_speed", ui.velocitySlider->value()); - s.endGroup(); - - s.beginGroup("Network"); - s.setValue("proxy_type", ui.proxyType->itemData(ui.proxyType->currentIndex()).toInt()); - s.setValue("proxy_host", ui.proxyHost->text()); - s.setValue("proxy_port", ui.proxyPort->value()); - SB("proxy_auth", ui.proxyAuthRequired); - s.setValue("proxy_user", ui.proxyUsername->text()); - s.setValue("proxy_pass", ui.proxyPassword->text()); - s.endGroup(); - - s.beginGroup("CloudStorage"); - QString email = ui.cloud_storage_email->text(); - QString password = ui.cloud_storage_password->text(); - QString newpassword = ui.cloud_storage_new_passwd->text(); - if (prefs.cloud_verification_status == CS_VERIFIED && !newpassword.isEmpty()) { - // deal with password change - if (!email.isEmpty() && !password.isEmpty()) { - // connect to backend server to check / create credentials - QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); - if (!reg.match(email).hasMatch() || (!password.isEmpty() && !reg.match(password).hasMatch())) { - report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); - } else { - CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); - connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); - connect(cloudAuth, SIGNAL(passwordChangeSuccessful()), this, SLOT(passwordUpdateSuccessfull())); - QNetworkReply *reply = cloudAuth->backend(email, password, "", newpassword); - ui.cloud_storage_new_passwd->setText(""); - free(prefs.cloud_storage_newpassword); - prefs.cloud_storage_newpassword = strdup(qPrintable(newpassword)); - } - } - } else if (prefs.cloud_verification_status == CS_UNKNOWN || - prefs.cloud_verification_status == CS_INCORRECT_USER_PASSWD || - email != prefs.cloud_storage_email || - password != prefs.cloud_storage_password) { - - // different credentials - reset verification status - prefs.cloud_verification_status = CS_UNKNOWN; - if (!email.isEmpty() && !password.isEmpty()) { - // connect to backend server to check / create credentials - QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); - if (!reg.match(email).hasMatch() || (!password.isEmpty() && !reg.match(password).hasMatch())) { - report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); - } else { - CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); - connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); - QNetworkReply *reply = cloudAuth->backend(email, password); - } - } - } else if (prefs.cloud_verification_status == CS_NEED_TO_VERIFY) { - QString pin = ui.cloud_storage_pin->text(); - if (!pin.isEmpty()) { - // connect to backend server to check / create credentials - QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); - if (!reg.match(email).hasMatch() || !reg.match(password).hasMatch()) { - report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); - } - CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); - connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); - QNetworkReply *reply = cloudAuth->backend(email, password, pin); - } - } - SAVE_OR_REMOVE("email", default_prefs.cloud_storage_email, email); - SAVE_OR_REMOVE("save_password_local", default_prefs.save_password_local, ui.save_password_local->isChecked()); - if (ui.save_password_local->isChecked()) { - SAVE_OR_REMOVE("password", default_prefs.cloud_storage_password, password); - } else { - s.remove("password"); - free(prefs.cloud_storage_password); - prefs.cloud_storage_password = strdup(qPrintable(password)); - } - SAVE_OR_REMOVE("cloud_verification_status", default_prefs.cloud_verification_status, prefs.cloud_verification_status); - SAVE_OR_REMOVE("cloud_background_sync", default_prefs.cloud_background_sync, ui.cloud_background_sync->isChecked()); - - // at this point we intentionally do not have a UI for changing this - // it could go into some sort of "advanced setup" or something - SAVE_OR_REMOVE("cloud_base_url", default_prefs.cloud_base_url, prefs.cloud_base_url); - s.endGroup(); - - s.beginGroup("geocoding"); -#ifdef DISABLED - s.setValue("enable_geocoding", ui.enable_geocoding->isChecked()); - s.setValue("parse_dive_without_gps", ui.parse_without_gps->isChecked()); - s.setValue("tag_existing_dives", ui.tag_existing_dives->isChecked()); -#endif - s.setValue("cat0", ui.first_item->currentIndex()); - s.setValue("cat1", ui.second_item->currentIndex()); - s.setValue("cat2", ui.third_item->currentIndex()); - s.endGroup(); - - loadSettings(); - emit settingsChanged(); -} - -void PreferencesDialog::loadSettings() -{ - // This code was on the mainwindow, it should belong nowhere, but since we didn't - // correctly fixed this code yet ( too much stuff on the code calling preferences ) - // force this here. - loadPreferences(); - QSettings s; - QVariant v; - - ui.save_uid_local->setChecked(s.value("save_uid_local").toBool()); - ui.default_uid->setText(s.value("subsurface_webservice_uid").toString().toUpper()); - - ui.defaultfilename->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); - ui.btnUseDefaultFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); - ui.chooseFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); -} - -void PreferencesDialog::buttonClicked(QAbstractButton *button) -{ - switch (ui.buttonBox->standardButton(button)) { - case QDialogButtonBox::Discard: - restorePrefs(); - syncSettings(); - close(); - break; - case QDialogButtonBox::Apply: - syncSettings(); - break; - case QDialogButtonBox::FirstButton: - syncSettings(); - close(); - break; - default: - break; // ignore warnings. - } -} -#undef SB - -void PreferencesDialog::on_chooseFile_clicked() -{ - QFileInfo fi(system_default_filename()); - QString choosenFileName = QFileDialog::getOpenFileName(this, tr("Open default log file"), fi.absolutePath(), tr("Subsurface XML files (*.ssrf *.xml *.XML)")); - - if (!choosenFileName.isEmpty()) - ui.defaultfilename->setText(choosenFileName); -} - -void PreferencesDialog::on_resetSettings_clicked() -{ - QSettings s; - - QMessageBox response(this); - response.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - response.setDefaultButton(QMessageBox::Cancel); - response.setWindowTitle(tr("Warning")); - response.setText(tr("If you click OK, all settings of Subsurface will be reset to their default values. This will be applied immediately.")); - response.setWindowModality(Qt::WindowModal); - - int result = response.exec(); - if (result == QMessageBox::Ok) { - copy_prefs(&default_prefs, &prefs); - setUiFromPrefs(); - Q_FOREACH (QString key, s.allKeys()) { - s.remove(key); - } - syncSettings(); - close(); - } -} - -void PreferencesDialog::passwordUpdateSuccessfull() -{ - ui.cloud_storage_password->setText(prefs.cloud_storage_password); -} - -void PreferencesDialog::emitSettingsChanged() -{ - emit settingsChanged(); -} - -void PreferencesDialog::proxyType_changed(int idx) -{ - if (idx == -1) { - return; - } - - int proxyType = ui.proxyType->itemData(idx).toInt(); - bool hpEnabled = (proxyType == QNetworkProxy::Socks5Proxy || proxyType == QNetworkProxy::HttpProxy); - ui.proxyHost->setEnabled(hpEnabled); - ui.proxyPort->setEnabled(hpEnabled); - ui.proxyAuthRequired->setEnabled(hpEnabled); - ui.proxyUsername->setEnabled(hpEnabled & ui.proxyAuthRequired->isChecked()); - ui.proxyPassword->setEnabled(hpEnabled & ui.proxyAuthRequired->isChecked()); - ui.proxyAuthRequired->setChecked(ui.proxyAuthRequired->isChecked()); -} - -void PreferencesDialog::on_btnUseDefaultFile_toggled(bool toggle) -{ - if (toggle) { - ui.defaultfilename->setText(system_default_filename()); - ui.defaultfilename->setEnabled(false); - } else { - ui.defaultfilename->setEnabled(true); - } -} - -void PreferencesDialog::on_noDefaultFile_toggled(bool toggle) -{ - prefs.default_file_behavior = NO_DEFAULT_FILE; -} - -void PreferencesDialog::on_localDefaultFile_toggled(bool toggle) -{ - ui.defaultfilename->setEnabled(toggle); - ui.btnUseDefaultFile->setEnabled(toggle); - ui.chooseFile->setEnabled(toggle); - prefs.default_file_behavior = LOCAL_DEFAULT_FILE; -} - -void PreferencesDialog::on_cloudDefaultFile_toggled(bool toggle) -{ - prefs.default_file_behavior = CLOUD_DEFAULT_FILE; -} diff --git a/qt-ui/preferences.h b/qt-ui/preferences.h deleted file mode 100644 index 326b1f964..000000000 --- a/qt-ui/preferences.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef PREFERENCES_H -#define PREFERENCES_H - -#include -#include "pref.h" - -#include "ui_preferences.h" - -#ifndef Q_OS_ANDROID - class QWebView; -#endif - -class QAbstractButton; - -class PreferencesDialog : public QDialog { - Q_OBJECT -public: - static PreferencesDialog *instance(); - void showEvent(QShowEvent *); - void emitSettingsChanged(); - -signals: - void settingsChanged(); -public -slots: - void buttonClicked(QAbstractButton *button); - void on_chooseFile_clicked(); - void on_resetSettings_clicked(); - void syncSettings(); - void loadSettings(); - void restorePrefs(); - void rememberPrefs(); - void gflowChanged(int gf); - void gfhighChanged(int gf); - void proxyType_changed(int idx); - void on_btnUseDefaultFile_toggled(bool toggle); - void on_noDefaultFile_toggled(bool toggle); - void on_localDefaultFile_toggled(bool toggle); - void on_cloudDefaultFile_toggled(bool toggle); - void facebookLoggedIn(); - void facebookDisconnect(); - void cloudPinNeeded(); - void passwordUpdateSuccessfull(); -private: - explicit PreferencesDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); - void setUiFromPrefs(); - Ui::PreferencesDialog ui; - struct preferences oldPrefs; - #ifndef Q_OS_ANDROID - QWebView *facebookWebView; - #endif -}; - -#endif // PREFERENCES_H diff --git a/qt-ui/preferences.ui b/qt-ui/preferences.ui deleted file mode 100644 index de2d79b91..000000000 --- a/qt-ui/preferences.ui +++ /dev/null @@ -1,1883 +0,0 @@ - - - PreferencesDialog - - - - 0 - 0 - 711 - 662 - - - - Preferences - - - - :/subsurface-icon - - - - - 5 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 24 - 24 - - - - Qt::ElideNone - - - QListView::Static - - - true - - - QListView::Batched - - - 0 - - - - 110 - 40 - - - - QListView::ListMode - - - true - - - true - - - -1 - - - - Defaults - - - - :/subsurface-icon - - - - - - Units - - - - :/units - - - - - - Graph - - - - :/graph - - - - - - Language - - - - :/advanced - - - - - - Network - - - - :/network - - - - - - Facebook - - - - :/facebook - - - - - - Georeference - - - - :/georeference - - - - - - - - - - 0 - 0 - - - - 4 - - - - - 0 - 0 - - - - - 5 - - - 5 - - - - - Lists and tables - - - - 5 - - - - - Font - - - - - - - - - - Font size - - - - - - - - - - - - - Dives - - - - 5 - - - 5 - - - 5 - - - - - Default dive log file - - - - - - - - - No default file - - - defaultFileGroup - - - - - - - &Local default file - - - defaultFileGroup - - - - - - - Clo&ud storage default file - - - defaultFileGroup - - - - - - - - - Local dive log file - - - - - - - - - - - - Use default - - - true - - - - - - - ... - - - - - - - - - Display invalid - - - - - - - - - - - - - - - - - Default cylinder - - - - 5 - - - 5 - - - 5 - - - - - Use default cylinder - - - - - - - - - - - - - - - - - Animations - - - - 5 - - - - - Speed - - - - - - - 500 - - - Qt::Horizontal - - - - - - - 500 - - - - - - - - - - Clear all settings - - - - 5 - - - 5 - - - - - Reset all settings to their default value - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - 0 - 0 - - - - - 5 - - - 5 - - - - - Unit system - - - - - - System - - - - - - - &Metric - - - buttonGroup_6 - - - - - - - Imperial - - - buttonGroup_6 - - - - - - - Personali&ze - - - buttonGroup_6 - - - - - - - - - - Individual settings - - - false - - - false - - - - - - Depth - - - - - - - meter - - - buttonGroup - - - - - - - feet - - - buttonGroup - - - - - - - Pressure - - - - - - - bar - - - buttonGroup_2 - - - - - - - psi - - - buttonGroup_2 - - - - - - - Volume - - - - - - - &liter - - - buttonGroup_3 - - - - - - - cu ft - - - buttonGroup_3 - - - - - - - Temperature - - - - - - - celsius - - - buttonGroup_4 - - - - - - - fahrenheit - - - buttonGroup_4 - - - - - - - Weight - - - - - - - kg - - - buttonGroup_5 - - - - - - - lbs - - - buttonGroup_5 - - - - - - - - - - - - Time units - - - - - - Ascent/descent speed denominator - - - - - - - Minutes - - - verticalSpeed - - - - - - - Seconds - - - verticalSpeed - - - - - - - - - - - - GPS coordinates - - - - - - Location Display - - - - - - - traditional (dms) - - - buttonGroup_7 - - - - - - - decimal - - - buttonGroup_7 - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - 0 - 0 - - - - - 5 - - - 5 - - - - - Show - - - - - - - - true - - - Threshold when showing pOâ‚‚ - - - - - - - true - - - 0.100000000000000 - - - - - - - - - - - true - - - Threshold when showing pNâ‚‚ - - - - - - - true - - - 0.100000000000000 - - - - - - - - - - - true - - - Threshold when showing pHe - - - - - - - true - - - 0.100000000000000 - - - - - - - - - - - true - - - Max pOâ‚‚ when showing MOD - - - - - - - true - - - 0.100000000000000 - - - - - - - - - - - true - - - Draw dive computer reported ceiling red - - - - - - - - - - - Show unused cylinders in Equipment tab - - - - - - - - - - - Show average depth - - - - - - - - - - - - Misc - - - - - - GFLow - - - - - - - 1 - - - 150 - - - - - - - GFHigh - - - - - - - 1 - - - 150 - - - - - - - GFLow at max depth - - - - - - - CCR: show setpoints when viewing pOâ‚‚ - - - - - - - CCR: show individual Oâ‚‚ sensor values when viewing pOâ‚‚ - - - - - - - Default CCR set-point for dive planning - - - - - - - bar - - - 2 - - - 10.000000000000000 - - - 0.100000000000000 - - - - - - - pSCR Oâ‚‚ metabolism rate - - - - - - - pSCR ratio - - - - - - - â„“/min - - - 3 - - - - - - - - - - 1: - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - 0 - 0 - - - - - 5 - - - QLayout::SetNoConstraint - - - 5 - - - - - - 0 - 0 - - - - UI language - - - - - - System default - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Filter - - - - - - - - - - - - - - 0 - 0 - - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - - - 0 - 0 - - - - - 5 - - - 5 - - - - - Proxy - - - - - - - 0 - 0 - - - - Port - - - - - - - - - - Host - - - - - - - Qt::LeftToRight - - - Requires authentication - - - - - - - Proxy type - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - Username - - - - - - - - 1 - 0 - - - - 65535 - - - 80 - - - - - - - - 0 - 0 - - - - 32 - - - - - - - - 2 - 0 - - - - 64 - - - - - - - Password - - - - - - - - 0 - 0 - - - - 32 - - - QLineEdit::Password - - - - - - - - - - - 0 - 0 - - - - - 0 - 129 - - - - Subsurface cloud storage - - - - - - - - - Email address - - - - - - - Password - - - - - - - Verification PIN - - - - - - - New password - - - - - - - - - - - - - - QLineEdit::Password - - - - - - - - - - - - - - QLineEdit::Password - - - - - - - Sync to cloud in the background? - - - - - - - Save Password locally? - - - - - - - - - - Subsurface web service - - - - 5 - - - 5 - - - - - Default user ID - - - - - - - - - - Save user ID locally? - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - 0 - 0 - - - - - 5 - - - 5 - - - - - - - - - 0 - 0 - - - - Connect to facebook text placeholder - - - - - - - - - - - - Disconnect - - - - - - - - - - - - 0 - 0 - - - - - 5 - - - 5 - - - - - Dive Site Layout - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - / - - - - - - - - 0 - 0 - - - - - - - - / - - - - - - - - 0 - 0 - - - - - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Apply|QDialogButtonBox::Discard|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - PreferencesDialog - accept() - - - 264 - 720 - - - 157 - 274 - - - - - buttonBox - rejected() - PreferencesDialog - reject() - - - 332 - 720 - - - 286 - 274 - - - - - listWidget - currentRowChanged(int) - stackedWidget - setCurrentIndex(int) - - - 37 - 97 - - - 282 - 18 - - - - - personalize - toggled(bool) - units_group - setEnabled(bool) - - - 185 - 19 - - - 186 - 23 - - - - - languageSystemDefault - toggled(bool) - languageView - setDisabled(bool) - - - 231 - 26 - - - 186 - 30 - - - - - languageSystemDefault - toggled(bool) - languageFilter - setDisabled(bool) - - - 231 - 26 - - - 185 - 20 - - - - - imperial - toggled(bool) - feet - setChecked(bool) - - - 164 - 19 - - - 175 - 34 - - - - - metric - toggled(bool) - meter - setChecked(bool) - - - 142 - 19 - - - 153 - 34 - - - - - imperial - toggled(bool) - psi - setChecked(bool) - - - 164 - 19 - - - 175 - 33 - - - - - metric - toggled(bool) - bar - setChecked(bool) - - - 142 - 19 - - - 153 - 33 - - - - - imperial - toggled(bool) - cuft - setChecked(bool) - - - 164 - 19 - - - 175 - 31 - - - - - metric - toggled(bool) - liter - setChecked(bool) - - - 142 - 19 - - - 153 - 31 - - - - - imperial - toggled(bool) - fahrenheit - setChecked(bool) - - - 164 - 19 - - - 175 - 29 - - - - - metric - toggled(bool) - celsius - setChecked(bool) - - - 142 - 19 - - - 153 - 29 - - - - - imperial - toggled(bool) - lbs - setChecked(bool) - - - 164 - 19 - - - 175 - 28 - - - - - metric - toggled(bool) - kg - setChecked(bool) - - - 142 - 19 - - - 153 - 28 - - - - - velocitySlider - valueChanged(int) - velocitySpinBox - setValue(int) - - - 236 - 52 - - - 236 - 52 - - - - - velocitySpinBox - valueChanged(int) - velocitySlider - setValue(int) - - - 236 - 52 - - - 236 - 52 - - - - - proxyAuthRequired - toggled(bool) - proxyUsername - setEnabled(bool) - - - 409 - 123 - - - 409 - 153 - - - - - proxyAuthRequired - toggled(bool) - proxyPassword - setEnabled(bool) - - - 409 - 123 - - - 409 - 183 - - - - - btnUseDefaultFile - toggled(bool) - chooseFile - setHidden(bool) - - - 236 - 44 - - - 236 - 44 - - - - - - - - - - - - - - - - diff --git a/qt-ui/printdialog.cpp b/qt-ui/printdialog.cpp deleted file mode 100644 index cf08062d2..000000000 --- a/qt-ui/printdialog.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "printdialog.h" -#include "printoptions.h" -#include "mainwindow.h" - -#ifndef NO_PRINTING -#include -#include -#include -#include -#include -#include - -#define SETTINGS_GROUP "PrintDialog" - -template_options::color_palette_struct ssrf_colors, almond_colors, blueshades_colors, custom_colors; - -PrintDialog::PrintDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) -{ - // initialize const colors - ssrf_colors.color1 = QColor::fromRgb(0xff, 0xff, 0xff); - ssrf_colors.color2 = QColor::fromRgb(0xa6, 0xbc, 0xd7); - ssrf_colors.color3 = QColor::fromRgb(0xef, 0xf7, 0xff); - ssrf_colors.color4 = QColor::fromRgb(0x34, 0x65, 0xa4); - ssrf_colors.color5 = QColor::fromRgb(0x20, 0x4a, 0x87); - ssrf_colors.color6 = QColor::fromRgb(0x17, 0x37, 0x64); - almond_colors.color1 = QColor::fromRgb(255, 255, 255); - almond_colors.color2 = QColor::fromRgb(253, 204, 156); - almond_colors.color3 = QColor::fromRgb(243, 234, 207); - almond_colors.color4 = QColor::fromRgb(136, 160, 150); - almond_colors.color5 = QColor::fromRgb(187, 171, 139); - almond_colors.color6 = QColor::fromRgb(0, 0, 0); - blueshades_colors.color1 = QColor::fromRgb(255, 255, 255); - blueshades_colors.color2 = QColor::fromRgb(142, 152, 166); - blueshades_colors.color3 = QColor::fromRgb(182, 192, 206); - blueshades_colors.color4 = QColor::fromRgb(31, 49, 75); - blueshades_colors.color5 = QColor::fromRgb(21, 45, 84); - blueshades_colors.color6 = QColor::fromRgb(0, 0, 0); - - // check if the options were previously stored in the settings; if not use some defaults. - QSettings s; - bool stored = s.childGroups().contains(SETTINGS_GROUP); - if (!stored) { - printOptions.print_selected = true; - printOptions.color_selected = true; - printOptions.landscape = false; - printOptions.p_template = "one_dive.html"; - printOptions.type = print_options::DIVELIST; - templateOptions.font_index = 0; - templateOptions.font_size = 9; - templateOptions.color_palette_index = SSRF_COLORS; - templateOptions.line_spacing = 1; - custom_colors = ssrf_colors; - } else { - s.beginGroup(SETTINGS_GROUP); - printOptions.type = (print_options::print_type)s.value("type").toInt(); - printOptions.print_selected = s.value("print_selected").toBool(); - printOptions.color_selected = s.value("color_selected").toBool(); - printOptions.landscape = s.value("landscape").toBool(); - printOptions.p_template = s.value("template_selected").toString(); - qprinter.setOrientation((QPrinter::Orientation)printOptions.landscape); - templateOptions.font_index = s.value("font").toInt(); - templateOptions.font_size = s.value("font_size").toDouble(); - templateOptions.color_palette_index = s.value("color_palette").toInt(); - templateOptions.line_spacing = s.value("line_spacing").toDouble(); - custom_colors.color1 = QColor(s.value("custom_color_1").toString()); - custom_colors.color2 = QColor(s.value("custom_color_2").toString()); - custom_colors.color3 = QColor(s.value("custom_color_3").toString()); - custom_colors.color4 = QColor(s.value("custom_color_4").toString()); - custom_colors.color5 = QColor(s.value("custom_color_5").toString()); - } - - // handle cases from old QSettings group - if (templateOptions.font_size < 9) { - templateOptions.font_size = 9; - } - if (templateOptions.line_spacing < 1) { - templateOptions.line_spacing = 1; - } - - switch (templateOptions.color_palette_index) { - case SSRF_COLORS: // default Subsurface derived colors - templateOptions.color_palette = ssrf_colors; - break; - case ALMOND: // almond - templateOptions.color_palette = almond_colors; - break; - case BLUESHADES: // blueshades - templateOptions.color_palette = blueshades_colors; - break; - case CUSTOM: // custom - templateOptions.color_palette = custom_colors; - break; - } - - // create a print options object and pass our options struct - optionsWidget = new PrintOptions(this, &printOptions, &templateOptions); - - // create a new printer object - printer = new Printer(&qprinter, &printOptions, &templateOptions, Printer::PRINT); - - QVBoxLayout *layout = new QVBoxLayout(this); - setLayout(layout); - - layout->addWidget(optionsWidget); - - progressBar = new QProgressBar(); - progressBar->setMinimum(0); - progressBar->setMaximum(100); - progressBar->setValue(0); - progressBar->setTextVisible(false); - layout->addWidget(progressBar); - - QHBoxLayout *hLayout = new QHBoxLayout(); - layout->addLayout(hLayout); - - QPushButton *printButton = new QPushButton(tr("P&rint")); - connect(printButton, SIGNAL(clicked(bool)), this, SLOT(printClicked())); - - QPushButton *previewButton = new QPushButton(tr("&Preview")); - connect(previewButton, SIGNAL(clicked(bool)), this, SLOT(previewClicked())); - - QDialogButtonBox *buttonBox = new QDialogButtonBox; - buttonBox->addButton(QDialogButtonBox::Cancel); - buttonBox->addButton(printButton, QDialogButtonBox::AcceptRole); - buttonBox->addButton(previewButton, QDialogButtonBox::ActionRole); - - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - - hLayout->addWidget(buttonBox); - - setWindowTitle(tr("Print")); - setWindowIcon(QIcon(":subsurface-icon")); - - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); - - // seems to be the most reliable way to track for all sorts of dialog disposal. - connect(this, SIGNAL(finished(int)), this, SLOT(onFinished())); -} - -void PrintDialog::onFinished() -{ - QSettings s; - s.beginGroup(SETTINGS_GROUP); - - // save print paper settings - s.setValue("type", printOptions.type); - s.setValue("print_selected", printOptions.print_selected); - s.setValue("color_selected", printOptions.color_selected); - s.setValue("template_selected", printOptions.p_template); - - // save template settings - s.setValue("font", templateOptions.font_index); - s.setValue("font_size", templateOptions.font_size); - s.setValue("color_palette", templateOptions.color_palette_index); - s.setValue("line_spacing", templateOptions.line_spacing); - - // save custom colors - s.setValue("custom_color_1", custom_colors.color1.name()); - s.setValue("custom_color_2", custom_colors.color2.name()); - s.setValue("custom_color_3", custom_colors.color3.name()); - s.setValue("custom_color_4", custom_colors.color4.name()); - s.setValue("custom_color_5", custom_colors.color5.name()); -} - -void PrintDialog::previewClicked(void) -{ - QPrintPreviewDialog previewDialog(&qprinter, this, Qt::Window - | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint - | Qt::WindowTitleHint); - connect(&previewDialog, SIGNAL(paintRequested(QPrinter *)), this, SLOT(onPaintRequested(QPrinter *))); - previewDialog.exec(); -} - -void PrintDialog::printClicked(void) -{ - QPrintDialog printDialog(&qprinter, this); - if (printDialog.exec() == QDialog::Accepted) { - connect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); - printer->print(); - close(); - } -} - -void PrintDialog::onPaintRequested(QPrinter *printerPtr) -{ - connect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); - printer->print(); - progressBar->setValue(0); - disconnect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); -} -#endif diff --git a/qt-ui/printdialog.h b/qt-ui/printdialog.h deleted file mode 100644 index a00c4c5d9..000000000 --- a/qt-ui/printdialog.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef PRINTDIALOG_H -#define PRINTDIALOG_H - -#ifndef NO_PRINTING -#include -#include -#include "printoptions.h" -#include "printer.h" -#include "templateedit.h" - -class QProgressBar; -class PrintOptions; -class PrintLayout; - -// should be based on a custom QPrintDialog class -class PrintDialog : public QDialog { - Q_OBJECT - -public: - explicit PrintDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); - -private: - PrintOptions *optionsWidget; - QProgressBar *progressBar; - Printer *printer; - QPrinter qprinter; - struct print_options printOptions; - struct template_options templateOptions; - -private -slots: - void onFinished(); - void previewClicked(); - void printClicked(); - void onPaintRequested(QPrinter *); -}; -#endif -#endif // PRINTDIALOG_H diff --git a/qt-ui/printer.cpp b/qt-ui/printer.cpp deleted file mode 100644 index f0197d446..000000000 --- a/qt-ui/printer.cpp +++ /dev/null @@ -1,273 +0,0 @@ -#include "printer.h" -#include "templatelayout.h" -#include "statistics.h" -#include "helpers.h" - -#include -#include -#include -#include -#include - -Printer::Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode) -{ - this->paintDevice = paintDevice; - this->printOptions = printOptions; - this->templateOptions = templateOptions; - this->printMode = printMode; - dpi = 0; - done = 0; - webView = new QWebView(); -} - -Printer::~Printer() -{ - delete webView; -} - -void Printer::putProfileImage(QRect profilePlaceholder, QRect viewPort, QPainter *painter, struct dive *dive, QPointer profile) -{ - int x = profilePlaceholder.x() - viewPort.x(); - int y = profilePlaceholder.y() - viewPort.y(); - // use the placeHolder and the viewPort position to calculate the relative position of the dive profile. - QRect pos(x, y, profilePlaceholder.width(), profilePlaceholder.height()); - profile->plotDive(dive, true); - - if (!printOptions->color_selected) { - QImage image(pos.width(), pos.height(), QImage::Format_ARGB32); - QPainter imgPainter(&image); - imgPainter.setRenderHint(QPainter::Antialiasing); - imgPainter.setRenderHint(QPainter::SmoothPixmapTransform); - profile->render(&imgPainter, QRect(0, 0, pos.width(), pos.height())); - imgPainter.end(); - - // convert QImage to grayscale before rendering - for (int i = 0; i < image.height(); i++) { - QRgb *pixel = reinterpret_cast(image.scanLine(i)); - QRgb *end = pixel + image.width(); - for (; pixel != end; pixel++) { - int gray_val = qGray(*pixel); - *pixel = QColor(gray_val, gray_val, gray_val).rgb(); - } - } - - painter->drawImage(pos, image); - } else { - profile->render(painter, pos); - } -} - -void Printer::flowRender() -{ - // add extra padding at the bottom to pages with height not divisible by view port - int paddingBottom = pageSize.height() - (webView->page()->mainFrame()->contentsSize().height() % pageSize.height()); - QString styleString = QString::fromUtf8("padding-bottom: ") + QString::number(paddingBottom) + "px;"; - webView->page()->mainFrame()->findFirstElement("body").setAttribute("style", styleString); - - // render the Qwebview - QPainter painter; - QRect viewPort(0, 0, 0, 0); - painter.begin(paintDevice); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - // get all references to dontbreak divs - int start = 0, end = 0; - int fullPageResolution = webView->page()->mainFrame()->contentsSize().height(); - QWebElementCollection dontbreak = webView->page()->mainFrame()->findAllElements(".dontbreak"); - foreach (QWebElement dontbreakElement, dontbreak) { - if ((dontbreakElement.geometry().y() + dontbreakElement.geometry().height()) - start < pageSize.height()) { - // One more element can be placed - end = dontbreakElement.geometry().y() + dontbreakElement.geometry().height(); - } else { - // fill the page with background color - QRect fullPage(0, 0, pageSize.width(), pageSize.height()); - QBrush fillBrush(templateOptions->color_palette.color1); - painter.fillRect(fullPage, fillBrush); - QRegion reigon(0, 0, pageSize.width(), end - start); - viewPort.setRect(0, start, pageSize.width(), end - start); - - // render the base Html template - webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); - - // scroll the webview to the next page - webView->page()->mainFrame()->scroll(0, dontbreakElement.geometry().y() - start); - - // rendering progress is 4/5 of total work - emit(progessUpdated((end * 80.0 / fullPageResolution) + done)); - - // add new pages only in print mode, while previewing we don't add new pages - if (printMode == Printer::PRINT) - static_cast(paintDevice)->newPage(); - else { - painter.end(); - return; - } - start = dontbreakElement.geometry().y(); - } - } - // render the remianing page - QRect fullPage(0, 0, pageSize.width(), pageSize.height()); - QBrush fillBrush(templateOptions->color_palette.color1); - painter.fillRect(fullPage, fillBrush); - QRegion reigon(0, 0, pageSize.width(), end - start); - webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); - - painter.end(); -} - -void Printer::render(int Pages = 0) -{ - // keep original preferences - QPointer profile = MainWindow::instance()->graphics(); - int profileFrameStyle = profile->frameStyle(); - int animationOriginal = prefs.animation_speed; - double fontScale = profile->getFontPrintScale(); - double printFontScale = 1.0; - - // apply printing settings to profile - profile->setFrameStyle(QFrame::NoFrame); - profile->setPrintMode(true, !printOptions->color_selected); - profile->setToolTipVisibile(false); - prefs.animation_speed = 0; - - // render the Qwebview - QPainter painter; - QRect viewPort(0, 0, pageSize.width(), pageSize.height()); - painter.begin(paintDevice); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - // get all refereces to diveprofile class in the Html template - QWebElementCollection collection = webView->page()->mainFrame()->findAllElements(".diveprofile"); - - QSize originalSize = profile->size(); - if (collection.count() > 0) { - printFontScale = (double)collection.at(0).geometry().size().height() / (double)profile->size().height(); - profile->resize(collection.at(0).geometry().size()); - } - profile->setFontPrintScale(printFontScale); - - int elemNo = 0; - for (int i = 0; i < Pages; i++) { - // render the base Html template - webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer); - - // render all the dive profiles in the current page - while (elemNo < collection.count() && collection.at(elemNo).geometry().y() < viewPort.y() + viewPort.height()) { - // dive id field should be dive_{{dive_no}} se we remove the first 5 characters - QString diveIdString = collection.at(elemNo).attribute("id"); - int diveId = diveIdString.remove(0, 5).toInt(0, 10); - putProfileImage(collection.at(elemNo).geometry(), viewPort, &painter, get_dive_by_uniq_id(diveId), profile); - elemNo++; - } - - // scroll the webview to the next page - webView->page()->mainFrame()->scroll(0, pageSize.height()); - viewPort.adjust(0, pageSize.height(), 0, pageSize.height()); - - // rendering progress is 4/5 of total work - emit(progessUpdated((i * 80.0 / Pages) + done)); - if (i < Pages - 1 && printMode == Printer::PRINT) - static_cast(paintDevice)->newPage(); - } - painter.end(); - - // return profle settings - profile->setFrameStyle(profileFrameStyle); - profile->setPrintMode(false); - profile->setFontPrintScale(fontScale); - profile->setToolTipVisibile(true); - profile->resize(originalSize); - prefs.animation_speed = animationOriginal; - - //replot the dive after returning the settings - profile->plotDive(0, true); -} - -//value: ranges from 0 : 100 and shows the progress of the templating engine -void Printer::templateProgessUpdated(int value) -{ - done = value / 5; //template progess if 1/5 of total work - emit progessUpdated(done); -} - -void Printer::print() -{ - // we can only print if "PRINT" mode is selected - if (printMode != Printer::PRINT) { - return; - } - - - QPrinter *printerPtr; - printerPtr = static_cast(paintDevice); - - TemplateLayout t(printOptions, templateOptions); - connect(&t, SIGNAL(progressUpdated(int)), this, SLOT(templateProgessUpdated(int))); - dpi = printerPtr->resolution(); - //rendering resolution = selected paper size in inchs * printer dpi - pageSize.setHeight(qCeil(printerPtr->pageRect(QPrinter::Inch).height() * dpi)); - pageSize.setWidth(qCeil(printerPtr->pageRect(QPrinter::Inch).width() * dpi)); - webView->page()->setViewportSize(pageSize); - webView->page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff); - // export border width with at least 1 pixel - templateOptions->border_width = std::max(1, pageSize.width() / 1000); - if (printOptions->type == print_options::DIVELIST) { - webView->setHtml(t.generate()); - } else if (printOptions->type == print_options::STATISTICS ) { - webView->setHtml(t.generateStatistics()); - } - if (printOptions->color_selected && printerPtr->colorMode()) { - printerPtr->setColorMode(QPrinter::Color); - } else { - printerPtr->setColorMode(QPrinter::GrayScale); - } - // apply user settings - int divesPerPage; - - // get number of dives per page from data-numberofdives attribute in the body of the selected template - bool ok; - divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); - if (!ok) { - divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed - //TODO: show warning - } - int Pages; - if (divesPerPage == 0) { - flowRender(); - } else { - Pages = qCeil(getTotalWork(printOptions) / (float)divesPerPage); - render(Pages); - } -} - -void Printer::previewOnePage() -{ - if (printMode == PREVIEW) { - TemplateLayout t(printOptions, templateOptions); - - pageSize.setHeight(paintDevice->height()); - pageSize.setWidth(paintDevice->width()); - webView->page()->setViewportSize(pageSize); - // initialize the border settings - templateOptions->border_width = std::max(1, pageSize.width() / 1000); - if (printOptions->type == print_options::DIVELIST) { - webView->setHtml(t.generate()); - } else if (printOptions->type == print_options::STATISTICS ) { - webView->setHtml(t.generateStatistics()); - } - - bool ok; - int divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); - if (!ok) { - divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed - //TODO: show warning - } - if (divesPerPage == 0) { - flowRender(); - } else { - render(1); - } - } -} diff --git a/qt-ui/printer.h b/qt-ui/printer.h deleted file mode 100644 index 979cacd6a..000000000 --- a/qt-ui/printer.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef PRINTER_H -#define PRINTER_H - -#include -#include -#include -#include - -#include "profile/profilewidget2.h" -#include "printoptions.h" -#include "templateedit.h" - -class Printer : public QObject { - Q_OBJECT - -public: - enum PrintMode { - PRINT, - PREVIEW - }; - -private: - QPaintDevice *paintDevice; - QWebView *webView; - print_options *printOptions; - template_options *templateOptions; - QSize pageSize; - PrintMode printMode; - int done; - int dpi; - void render(int Pages); - void flowRender(); - void putProfileImage(QRect box, QRect viewPort, QPainter *painter, struct dive *dive, QPointer profile); - -private slots: - void templateProgessUpdated(int value); - -public: - Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode); - ~Printer(); - void print(); - void previewOnePage(); - -signals: - void progessUpdated(int value); -}; - -#endif //PRINTER_H diff --git a/qt-ui/printoptions.cpp b/qt-ui/printoptions.cpp deleted file mode 100644 index 769c89ff4..000000000 --- a/qt-ui/printoptions.cpp +++ /dev/null @@ -1,189 +0,0 @@ -#include "printoptions.h" -#include "templateedit.h" -#include "helpers.h" - -#include -#include -#include - -PrintOptions::PrintOptions(QWidget *parent, struct print_options *printOpt, struct template_options *templateOpt) -{ - hasSetupSlots = false; - ui.setupUi(this); - if (parent) - setParent(parent); - if (!printOpt || !templateOpt) - return; - templateOptions = templateOpt; - printOptions = printOpt; - setup(); -} - -void PrintOptions::setup() -{ - // print type radio buttons - switch (printOptions->type) { - case print_options::DIVELIST: - ui.radioDiveListPrint->setChecked(true); - break; - case print_options::STATISTICS: - ui.radioStatisticsPrint->setChecked(true); - break; - } - - setupTemplates(); - - // general print option checkboxes - if (printOptions->color_selected) - ui.printInColor->setChecked(true); - if (printOptions->print_selected) - ui.printSelected->setChecked(true); - - // connect slots only once - if (hasSetupSlots) - return; - - connect(ui.printInColor, SIGNAL(clicked(bool)), this, SLOT(printInColorClicked(bool))); - connect(ui.printSelected, SIGNAL(clicked(bool)), this, SLOT(printSelectedClicked(bool))); - - hasSetupSlots = true; -} - -void PrintOptions::setupTemplates() -{ - if (printOptions->type == print_options::DIVELIST) { - // insert dive list templates in the UI and select the current template - qSort(grantlee_templates); - int current_index = 0, index = 0; - for (QList::iterator i = grantlee_templates.begin(); i != grantlee_templates.end(); ++i) { - if ((*i).compare(printOptions->p_template) == 0) { - current_index = index; - break; - } - index++; - } - ui.printTemplate->clear(); - for (QList::iterator i = grantlee_templates.begin(); i != grantlee_templates.end(); ++i) { - ui.printTemplate->addItem((*i).split('.')[0], QVariant::fromValue(*i)); - } - ui.printTemplate->setCurrentIndex(current_index); - } else if (printOptions->type == print_options::STATISTICS) { - // insert statistics templates in the UI and select the current template - qSort(grantlee_statistics_templates); - int current_index = 0, index = 0; - for (QList::iterator i = grantlee_statistics_templates.begin(); i != grantlee_statistics_templates.end(); ++i) { - if ((*i).compare(printOptions->p_template) == 0) { - current_index = index; - break; - } - index++; - } - ui.printTemplate->clear(); - for (QList::iterator i = grantlee_statistics_templates.begin(); i != grantlee_statistics_templates.end(); ++i) { - ui.printTemplate->addItem((*i).split('.')[0], QVariant::fromValue(*i)); - } - ui.printTemplate->setCurrentIndex(current_index); - } -} - -// print type radio buttons -void PrintOptions::on_radioDiveListPrint_toggled(bool check) -{ - if (check) { - printOptions->type = print_options::DIVELIST; - - // print options - ui.printSelected->setEnabled(true); - - // print template - ui.deleteButton->setEnabled(true); - ui.exportButton->setEnabled(true); - ui.importButton->setEnabled(true); - - setupTemplates(); - } -} - -void PrintOptions::on_radioStatisticsPrint_toggled(bool check) -{ - if (check) { - printOptions->type = print_options::STATISTICS; - - // print options - ui.printSelected->setEnabled(false); - - // print template - ui.deleteButton->setEnabled(false); - ui.exportButton->setEnabled(false); - ui.importButton->setEnabled(false); - - setupTemplates(); - } -} - -// general print option checkboxes -void PrintOptions::printInColorClicked(bool check) -{ - printOptions->color_selected = check; -} - -void PrintOptions::printSelectedClicked(bool check) -{ - printOptions->print_selected = check; -} - - -void PrintOptions::on_printTemplate_currentIndexChanged(int index) -{ - printOptions->p_template = ui.printTemplate->itemData(index).toString(); -} - -void PrintOptions::on_editButton_clicked() -{ - TemplateEdit te(this, printOptions, templateOptions); - te.exec(); - setup(); -} - -void PrintOptions::on_importButton_clicked() -{ - QString filename = QFileDialog::getOpenFileName(this, tr("Import template file"), "", - tr("HTML files (*.html)")); - if (filename.isEmpty()) - return; - QFileInfo fileInfo(filename); - QFile::copy(filename, getPrintingTemplatePathUser() + QDir::separator() + fileInfo.fileName()); - printOptions->p_template = fileInfo.fileName(); - find_all_templates(); - setup(); -} - -void PrintOptions::on_exportButton_clicked() -{ - QString filename = QFileDialog::getSaveFileName(this, tr("Export template files as"), "", - tr("HTML files (*.html)")); - if (filename.isEmpty()) - return; - QFile::copy(getPrintingTemplatePathUser() + QDir::separator() + getSelectedTemplate(), filename); -} - -void PrintOptions::on_deleteButton_clicked() -{ - QString templateName = getSelectedTemplate(); - QMessageBox msgBox; - msgBox.setText(tr("This action cannot be undone!")); - msgBox.setInformativeText(tr("Delete template: %1?").arg(templateName)); - msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - msgBox.setDefaultButton(QMessageBox::Cancel); - if (msgBox.exec() == QMessageBox::Ok) { - QFile f(getPrintingTemplatePathUser() + QDir::separator() + templateName); - f.remove(); - find_all_templates(); - setup(); - } -} - -QString PrintOptions::getSelectedTemplate() -{ - return ui.printTemplate->currentData().toString(); -} diff --git a/qt-ui/printoptions.h b/qt-ui/printoptions.h deleted file mode 100644 index 9c50b10f3..000000000 --- a/qt-ui/printoptions.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef PRINTOPTIONS_H -#define PRINTOPTIONS_H - -#include - -#include "ui_printoptions.h" - -struct print_options { - enum print_type { - DIVELIST, - STATISTICS - } type; - QString p_template; - bool print_selected; - bool color_selected; - bool landscape; -}; - -struct template_options { - int font_index; - int color_palette_index; - int border_width; - double font_size; - double line_spacing; - struct color_palette_struct { - QColor color1; - QColor color2; - QColor color3; - QColor color4; - QColor color5; - QColor color6; - bool operator!=(const color_palette_struct &other) const { - return other.color1 != color1 - || other.color2 != color2 - || other.color3 != color3 - || other.color4 != color4 - || other.color5 != color5 - || other.color6 != color6; - } - } color_palette; - bool operator!=(const template_options &other) const { - return other.font_index != font_index - || other.color_palette_index != color_palette_index - || other.font_size != font_size - || other.line_spacing != line_spacing - || other.color_palette != color_palette; - } - }; - -extern template_options::color_palette_struct ssrf_colors, almond_colors, blueshades_colors, custom_colors; - -enum color_palette { - SSRF_COLORS, - ALMOND, - BLUESHADES, - CUSTOM -}; - -// should be based on a custom QPrintDialog class -class PrintOptions : public QWidget { - Q_OBJECT - -public: - explicit PrintOptions(QWidget *parent, struct print_options *printOpt, struct template_options *templateOpt); - void setup(); - QString getSelectedTemplate(); - -private: - Ui::PrintOptions ui; - struct print_options *printOptions; - struct template_options *templateOptions; - bool hasSetupSlots; - void setupTemplates(); - -private -slots: - void printInColorClicked(bool check); - void printSelectedClicked(bool check); - void on_radioStatisticsPrint_toggled(bool check); - void on_radioDiveListPrint_toggled(bool check); - void on_printTemplate_currentIndexChanged(int index); - void on_editButton_clicked(); - void on_importButton_clicked(); - void on_exportButton_clicked(); - void on_deleteButton_clicked(); -}; - -#endif // PRINTOPTIONS_H diff --git a/qt-ui/printoptions.ui b/qt-ui/printoptions.ui deleted file mode 100644 index 1c2523d39..000000000 --- a/qt-ui/printoptions.ui +++ /dev/null @@ -1,168 +0,0 @@ - - - PrintOptions - - - - 0 - 0 - 367 - 433 - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Print type - - - - - - - 0 - 0 - - - - &Dive list print - - - true - - - - - - - - 0 - 0 - - - - &Statistics print - - - - - - - - - - Print options - - - - - - - 0 - 0 - - - - Print only selected dives - - - - - - - - 0 - 0 - - - - Print in color - - - - - - - - - - Template - - - - - - - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Edit - - - - - - - Delete - - - - - - - Export - - - - - - - Import - - - - - - - - - - - - - - radioDiveListPrint - printSelected - printInColor - - - - diff --git a/qt-ui/profile/animationfunctions.cpp b/qt-ui/profile/animationfunctions.cpp deleted file mode 100644 index a19d50c9d..000000000 --- a/qt-ui/profile/animationfunctions.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "animationfunctions.h" -#include "pref.h" -#include - -namespace Animations { - - void hide(QObject *obj) - { - if (prefs.animation_speed != 0) { - QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); - animation->setStartValue(1); - animation->setEndValue(0); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - obj->setProperty("opacity", 0); - } - } - - void show(QObject *obj) - { - if (prefs.animation_speed != 0) { - QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); - animation->setStartValue(0); - animation->setEndValue(1); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - obj->setProperty("opacity", 1); - } - } - - void animDelete(QObject *obj) - { - if (prefs.animation_speed != 0) { - QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); - obj->connect(animation, SIGNAL(finished()), SLOT(deleteLater())); - animation->setStartValue(1); - animation->setEndValue(0); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - obj->setProperty("opacity", 0); - } - } - - void moveTo(QObject *obj, qreal x, qreal y) - { - if (prefs.animation_speed != 0) { - QPropertyAnimation *animation = new QPropertyAnimation(obj, "pos"); - animation->setDuration(prefs.animation_speed); - animation->setStartValue(obj->property("pos").toPointF()); - animation->setEndValue(QPointF(x, y)); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - obj->setProperty("pos", QPointF(x, y)); - } - } - - void scaleTo(QObject *obj, qreal scale) - { - if (prefs.animation_speed != 0) { - QPropertyAnimation *animation = new QPropertyAnimation(obj, "scale"); - animation->setDuration(prefs.animation_speed); - animation->setStartValue(obj->property("scale").toReal()); - animation->setEndValue(QVariant::fromValue(scale)); - animation->setEasingCurve(QEasingCurve::InCubic); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - obj->setProperty("scale", QVariant::fromValue(scale)); - } - } - - void moveTo(QObject *obj, const QPointF &pos) - { - moveTo(obj, pos.x(), pos.y()); - } -} diff --git a/qt-ui/profile/animationfunctions.h b/qt-ui/profile/animationfunctions.h deleted file mode 100644 index 3cfcff563..000000000 --- a/qt-ui/profile/animationfunctions.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef ANIMATIONFUNCTIONS_H -#define ANIMATIONFUNCTIONS_H - -#include -#include - -class QObject; - -namespace Animations { - void hide(QObject *obj); - void show(QObject *obj); - void moveTo(QObject *obj, qreal x, qreal y); - void moveTo(QObject *obj, const QPointF &pos); - void animDelete(QObject *obj); - void scaleTo(QObject *obj, qreal scale); -} - -#endif // ANIMATIONFUNCTIONS_H diff --git a/qt-ui/profile/divecartesianaxis.cpp b/qt-ui/profile/divecartesianaxis.cpp deleted file mode 100644 index bf5a5380c..000000000 --- a/qt-ui/profile/divecartesianaxis.cpp +++ /dev/null @@ -1,459 +0,0 @@ -#include "divecartesianaxis.h" -#include "divetextitem.h" -#include "helpers.h" -#include "preferences.h" -#include "diveplotdatamodel.h" -#include "animationfunctions.h" -#include "mainwindow.h" -#include "divelineitem.h" -#include "profilewidget2.h" - -QPen DiveCartesianAxis::gridPen() -{ - QPen pen; - pen.setColor(getColor(TIME_GRID)); - /* cosmetic width() == 0 for lines in printMode - * having setCosmetic(true) and width() > 0 does not work when - * printing on OSX and Linux */ - pen.setWidth(DiveCartesianAxis::printMode ? 0 : 2); - pen.setCosmetic(true); - return pen; -} - -double DiveCartesianAxis::tickInterval() const -{ - return interval; -} - -double DiveCartesianAxis::tickSize() const -{ - return tick_size; -} - -void DiveCartesianAxis::setFontLabelScale(qreal scale) -{ - labelScale = scale; - changed = true; -} - -void DiveCartesianAxis::setPrintMode(bool mode) -{ - printMode = mode; - // update the QPen of all lines depending on printMode - QPen newPen = gridPen(); - QColor oldColor = pen().brush().color(); - newPen.setBrush(oldColor); - setPen(newPen); - Q_FOREACH (DiveLineItem *item, lines) - item->setPen(pen()); -} - -void DiveCartesianAxis::setMaximum(double maximum) -{ - if (IS_FP_SAME(max, maximum)) - return; - max = maximum; - changed = true; - emit maxChanged(); -} - -void DiveCartesianAxis::setMinimum(double minimum) -{ - if (IS_FP_SAME(min, minimum)) - return; - min = minimum; - changed = true; -} - -void DiveCartesianAxis::setTextColor(const QColor &color) -{ - textColor = color; -} - -DiveCartesianAxis::DiveCartesianAxis() : QObject(), - QGraphicsLineItem(), - printMode(false), - unitSystem(0), - orientation(LeftToRight), - min(0), - max(0), - interval(1), - tick_size(0), - textVisibility(true), - lineVisibility(true), - labelScale(1.0), - line_size(1), - changed(true) -{ - setPen(gridPen()); -} - -DiveCartesianAxis::~DiveCartesianAxis() -{ -} - -void DiveCartesianAxis::setLineSize(qreal lineSize) -{ - line_size = lineSize; - changed = true; -} - -void DiveCartesianAxis::setOrientation(Orientation o) -{ - orientation = o; - changed = true; -} - -QColor DiveCartesianAxis::colorForValue(double value) -{ - return QColor(Qt::black); -} - -void DiveCartesianAxis::setTextVisible(bool arg1) -{ - if (textVisibility == arg1) { - return; - } - textVisibility = arg1; - Q_FOREACH (DiveTextItem *item, labels) { - item->setVisible(textVisibility); - } -} - -void DiveCartesianAxis::setLinesVisible(bool arg1) -{ - if (lineVisibility == arg1) { - return; - } - lineVisibility = arg1; - Q_FOREACH (DiveLineItem *item, lines) { - item->setVisible(lineVisibility); - } -} - -template -void emptyList(QList &list, double steps) -{ - if (!list.isEmpty() && list.size() > steps) { - while (list.size() > steps) { - T *removedItem = list.takeLast(); - Animations::animDelete(removedItem); - } - } -} - -void DiveCartesianAxis::updateTicks(color_indice_t color) -{ - if (!scene() || (!changed && !MainWindow::instance()->graphics()->getPrintMode())) - return; - QLineF m = line(); - // unused so far: - // QGraphicsView *view = scene()->views().first(); - double steps = (max - min) / interval; - double currValueText = min; - double currValueLine = min; - - if (steps < 1) - return; - - emptyList(labels, steps); - emptyList(lines, steps); - - // Move the remaining Ticks / Text to it's corerct position - // Regartind the possibly new values for the Axis - qreal begin, stepSize; - if (orientation == TopToBottom) { - begin = m.y1(); - stepSize = (m.y2() - m.y1()); - } else if (orientation == BottomToTop) { - begin = m.y2(); - stepSize = (m.y2() - m.y1()); - } else if (orientation == LeftToRight) { - begin = m.x1(); - stepSize = (m.x2() - m.x1()); - } else /* if (orientation == RightToLeft) */ { - begin = m.x2(); - stepSize = (m.x2() - m.x1()); - } - stepSize = stepSize / steps; - - for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) { - qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? - begin + i * stepSize : - begin - i * stepSize; - - labels[i]->setText(textForValue(currValueText)); - if (orientation == LeftToRight || orientation == RightToLeft) { - Animations::moveTo(labels[i],childPos, m.y1() + tick_size); - } else { - Animations::moveTo(labels[i],m.x1() - tick_size, childPos); - } - } - - for (int i = 0, count = lines.size(); i < count; i++, currValueLine += interval) { - qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? - begin + i * stepSize : - begin - i * stepSize; - - if (orientation == LeftToRight || orientation == RightToLeft) { - Animations::moveTo(lines[i],childPos, m.y1()); - } else { - Animations::moveTo(lines[i],m.x1(), childPos); - } - } - - // Add's the rest of the needed Ticks / Text. - for (int i = labels.size(); i < steps; i++, currValueText += interval) { - qreal childPos; - if (orientation == TopToBottom || orientation == LeftToRight) { - childPos = begin + i * stepSize; - } else { - childPos = begin - i * stepSize; - } - DiveTextItem *label = new DiveTextItem(this); - label->setText(textForValue(currValueText)); - label->setBrush(colorForValue(currValueText)); - label->setScale(fontLabelScale()); - label->setZValue(1); - labels.push_back(label); - if (orientation == RightToLeft || orientation == LeftToRight) { - label->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); - label->setPos(scene()->sceneRect().width() + 10, m.y1() + tick_size); // position it outside of the scene); - Animations::moveTo(label,childPos, m.y1() + tick_size); - } else { - label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); - label->setPos(m.x1() - tick_size, scene()->sceneRect().height() + 10); - Animations::moveTo(label,m.x1() - tick_size, childPos); - } - } - - // Add's the rest of the needed Ticks / Text. - for (int i = lines.size(); i < steps; i++, currValueText += interval) { - qreal childPos; - if (orientation == TopToBottom || orientation == LeftToRight) { - childPos = begin + i * stepSize; - } else { - childPos = begin - i * stepSize; - } - DiveLineItem *line = new DiveLineItem(this); - QPen pen = gridPen(); - pen.setBrush(getColor(color)); - line->setPen(pen); - line->setZValue(0); - lines.push_back(line); - if (orientation == RightToLeft || orientation == LeftToRight) { - line->setLine(0, -line_size, 0, 0); - line->setPos(scene()->sceneRect().width() + 10, m.y1()); // position it outside of the scene); - Animations::moveTo(line,childPos, m.y1()); - } else { - QPointF p1 = mapFromScene(3, 0); - QPointF p2 = mapFromScene(line_size, 0); - line->setLine(p1.x(), 0, p2.x(), 0); - line->setPos(m.x1(), scene()->sceneRect().height() + 10); - Animations::moveTo(line,m.x1(), childPos); - } - } - - Q_FOREACH (DiveTextItem *item, labels) - item->setVisible(textVisibility); - Q_FOREACH (DiveLineItem *item, lines) - item->setVisible(lineVisibility); - changed = false; -} - -void DiveCartesianAxis::setLine(const QLineF &line) -{ - QGraphicsLineItem::setLine(line); - changed = true; -} - -void DiveCartesianAxis::animateChangeLine(const QLineF &newLine) -{ - setLine(newLine); - updateTicks(); - sizeChanged(); -} - -QString DiveCartesianAxis::textForValue(double value) -{ - return QString::number(value); -} - -void DiveCartesianAxis::setTickSize(qreal size) -{ - tick_size = size; -} - -void DiveCartesianAxis::setTickInterval(double i) -{ - interval = i; -} - -qreal DiveCartesianAxis::valueAt(const QPointF &p) const -{ - QLineF m = line(); - QPointF relativePosition = p; - relativePosition -= pos(); // normalize p based on the axis' offset on screen - - double retValue = (orientation == LeftToRight || orientation == RightToLeft) ? - max * (relativePosition.x() - m.x1()) / (m.x2() - m.x1()) : - max * (relativePosition.y() - m.y1()) / (m.y2() - m.y1()); - return retValue; -} - -qreal DiveCartesianAxis::posAtValue(qreal value) -{ - QLineF m = line(); - QPointF p = pos(); - - double size = max - min; - // unused for now: - // double distanceFromOrigin = value - min; - double percent = IS_FP_SAME(min, max) ? 0.0 : (value - min) / size; - - - double realSize = orientation == LeftToRight || orientation == RightToLeft ? - m.x2() - m.x1() : - m.y2() - m.y1(); - - // Inverted axis, just invert the percentage. - if (orientation == RightToLeft || orientation == BottomToTop) - percent = 1 - percent; - - double retValue = realSize * percent; - double adjusted = - orientation == LeftToRight ? retValue + m.x1() + p.x() : - orientation == RightToLeft ? retValue + m.x1() + p.x() : - orientation == TopToBottom ? retValue + m.y1() + p.y() : - /* entation == BottomToTop */ retValue + m.y1() + p.y(); - return adjusted; -} - -qreal DiveCartesianAxis::percentAt(const QPointF &p) -{ - qreal value = valueAt(p); - double size = max - min; - double percent = value / size; - return percent; -} - -double DiveCartesianAxis::maximum() const -{ - return max; -} - -double DiveCartesianAxis::minimum() const -{ - return min; -} - -double DiveCartesianAxis::fontLabelScale() const -{ - return labelScale; -} - -void DiveCartesianAxis::setColor(const QColor &color) -{ - QPen defaultPen = gridPen(); - defaultPen.setColor(color); - defaultPen.setJoinStyle(Qt::RoundJoin); - defaultPen.setCapStyle(Qt::RoundCap); - setPen(defaultPen); -} - -QString DepthAxis::textForValue(double value) -{ - if (value == 0) - return QString(); - return get_depth_string(value, false, false); -} - -QColor DepthAxis::colorForValue(double value) -{ - Q_UNUSED(value); - return QColor(Qt::red); -} - -DepthAxis::DepthAxis() -{ - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); - changed = true; - settingsChanged(); -} - -void DepthAxis::settingsChanged() -{ - static int unitSystem = prefs.units.length; - if ( unitSystem == prefs.units.length ) - return; - changed = true; - updateTicks(); - unitSystem = prefs.units.length; -} - -QColor TimeAxis::colorForValue(double value) -{ - Q_UNUSED(value); - return QColor(Qt::blue); -} - -QString TimeAxis::textForValue(double value) -{ - int nr = value / 60; - if (maximum() < 600) - return QString("%1:%2").arg(nr).arg((int)value % 60, 2, 10, QChar('0')); - return QString::number(nr); -} - -void TimeAxis::updateTicks() -{ - DiveCartesianAxis::updateTicks(); - if (maximum() > 600) { - for (int i = 0; i < labels.count(); i++) { - labels[i]->setVisible(i % 2); - } - } -} - -QString TemperatureAxis::textForValue(double value) -{ - return QString::number(mkelvin_to_C((int)value)); -} - -PartialGasPressureAxis::PartialGasPressureAxis() : - DiveCartesianAxis(), - model(NULL) -{ - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); -} - -void PartialGasPressureAxis::setModel(DivePlotDataModel *m) -{ - model = m; - connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(settingsChanged())); - settingsChanged(); -} - -void PartialGasPressureAxis::settingsChanged() -{ - bool showPhe = prefs.pp_graphs.phe; - bool showPn2 = prefs.pp_graphs.pn2; - bool showPo2 = prefs.pp_graphs.po2; - setVisible(showPhe || showPn2 || showPo2); - if (!model->rowCount()) - return; - - double max = showPhe ? model->pheMax() : -1; - if (showPn2 && model->pn2Max() > max) - max = model->pn2Max(); - if (showPo2 && model->po2Max() > max) - max = model->po2Max(); - - qreal pp = floor(max * 10.0) / 10.0 + 0.2; - if (IS_FP_SAME(maximum(), pp)) - return; - - setMaximum(pp); - setTickInterval(pp > 4 ? 0.5 : 0.25); - updateTicks(); -} diff --git a/qt-ui/profile/divecartesianaxis.h b/qt-ui/profile/divecartesianaxis.h deleted file mode 100644 index 27cfa62d8..000000000 --- a/qt-ui/profile/divecartesianaxis.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef DIVECARTESIANAXIS_H -#define DIVECARTESIANAXIS_H - -#include -#include -#include - -class QPropertyAnimation; -class DiveTextItem; -class DiveLineItem; -class DivePlotDataModel; - -class DiveCartesianAxis : public QObject, public QGraphicsLineItem { - Q_OBJECT - Q_PROPERTY(QLineF line WRITE setLine READ line) - Q_PROPERTY(QPointF pos WRITE setPos READ pos) - Q_PROPERTY(qreal x WRITE setX READ x) - Q_PROPERTY(qreal y WRITE setY READ y) -private: - bool printMode; - QPen gridPen(); -public: - enum Orientation { - TopToBottom, - BottomToTop, - LeftToRight, - RightToLeft - }; - DiveCartesianAxis(); - virtual ~DiveCartesianAxis(); - void setPrintMode(bool mode); - void setMinimum(double minimum); - void setMaximum(double maximum); - void setTickInterval(double interval); - void setOrientation(Orientation orientation); - void setTickSize(qreal size); - void setFontLabelScale(qreal scale); - double minimum() const; - double maximum() const; - double tickInterval() const; - double tickSize() const; - double fontLabelScale() const; - qreal valueAt(const QPointF &p) const; - qreal percentAt(const QPointF &p); - qreal posAtValue(qreal value); - void setColor(const QColor &color); - void setTextColor(const QColor &color); - void animateChangeLine(const QLineF &newLine); - void setTextVisible(bool arg1); - void setLinesVisible(bool arg1); - void setLineSize(qreal lineSize); - void setLine(const QLineF& line); - int unitSystem; -public -slots: - virtual void updateTicks(color_indice_t color = TIME_GRID); - -signals: - void sizeChanged(); - void maxChanged(); - -protected: - virtual QString textForValue(double value); - virtual QColor colorForValue(double value); - Orientation orientation; - QList labels; - QList lines; - double min; - double max; - double interval; - double tick_size; - QColor textColor; - bool textVisibility; - bool lineVisibility; - double labelScale; - qreal line_size; - bool changed; -}; - -class DepthAxis : public DiveCartesianAxis { - Q_OBJECT -public: - DepthAxis(); - -protected: - QString textForValue(double value); - QColor colorForValue(double value); -private -slots: - void settingsChanged(); -}; - -class TimeAxis : public DiveCartesianAxis { - Q_OBJECT -public: - virtual void updateTicks(); - -protected: - QString textForValue(double value); - QColor colorForValue(double value); -}; - -class TemperatureAxis : public DiveCartesianAxis { - Q_OBJECT -protected: - QString textForValue(double value); -}; - -class PartialGasPressureAxis : public DiveCartesianAxis { - Q_OBJECT -public: - PartialGasPressureAxis(); - void setModel(DivePlotDataModel *model); -public -slots: - void settingsChanged(); - -private: - DivePlotDataModel *model; -}; - -#endif // DIVECARTESIANAXIS_H diff --git a/qt-ui/profile/diveeventitem.cpp b/qt-ui/profile/diveeventitem.cpp deleted file mode 100644 index 0bbc84267..000000000 --- a/qt-ui/profile/diveeventitem.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include "diveeventitem.h" -#include "diveplotdatamodel.h" -#include "divecartesianaxis.h" -#include "animationfunctions.h" -#include "libdivecomputer.h" -#include "profile.h" -#include "gettextfromc.h" -#include "metrics.h" - -extern struct ev_select *ev_namelist; -extern int evn_used; - -DiveEventItem::DiveEventItem(QObject *parent) : DivePixmapItem(parent), - vAxis(NULL), - hAxis(NULL), - dataModel(NULL), - internalEvent(NULL) -{ - setFlag(ItemIgnoresTransformations); -} - - -void DiveEventItem::setHorizontalAxis(DiveCartesianAxis *axis) -{ - hAxis = axis; - recalculatePos(true); -} - -void DiveEventItem::setModel(DivePlotDataModel *model) -{ - dataModel = model; - recalculatePos(true); -} - -void DiveEventItem::setVerticalAxis(DiveCartesianAxis *axis) -{ - vAxis = axis; - recalculatePos(true); - connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(recalculatePos())); -} - -struct event *DiveEventItem::getEvent() -{ - return internalEvent; -} - -void DiveEventItem::setEvent(struct event *ev) -{ - if (!ev) - return; - internalEvent = ev; - setupPixmap(); - setupToolTipString(); - recalculatePos(true); -} - -void DiveEventItem::setupPixmap() -{ - const IconMetrics& metrics = defaultIconMetrics(); - int sz_bigger = metrics.sz_med + metrics.sz_small; // ex 40px - int sz_pix = sz_bigger/2; // ex 20px - -#define EVENT_PIXMAP(PIX) QPixmap(QString(PIX)).scaled(sz_pix, sz_pix, Qt::KeepAspectRatio, Qt::SmoothTransformation) -#define EVENT_PIXMAP_BIGGER(PIX) QPixmap(QString(PIX)).scaled(sz_bigger, sz_bigger, Qt::KeepAspectRatio, Qt::SmoothTransformation) - if (same_string(internalEvent->name, "")) { - setPixmap(EVENT_PIXMAP(":warning")); - } else if (internalEvent->type == SAMPLE_EVENT_BOOKMARK) { - setPixmap(EVENT_PIXMAP(":flag")); - } else if (strcmp(internalEvent->name, "heading") == 0 || - (same_string(internalEvent->name, "SP change") && internalEvent->time.seconds == 0)) { - // 2 cases: - // a) some dive computers have heading in every sample - // b) at t=0 we might have an "SP change" to indicate dive type - // in both cases we want to get the right data into the tooltip but don't want the visual clutter - // so set an "almost invisible" pixmap (a narrow but somewhat tall, basically transparent pixmap) - // that allows tooltips to work when we don't want to show a specific - // pixmap for an event, but want to show the event value in the tooltip - QPixmap transparentPixmap(4, 20); - transparentPixmap.fill(QColor::fromRgbF(1.0, 1.0, 1.0, 0.01)); - setPixmap(transparentPixmap); - } else if (event_is_gaschange(internalEvent)) { - if (internalEvent->gas.mix.he.permille) - setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeTrimix")); - else if (gasmix_is_air(&internalEvent->gas.mix)) - setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeAir")); - else - setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeNitrox")); - } else { - setPixmap(EVENT_PIXMAP(":warning")); - } -#undef EVENT_PIXMAP -} - -void DiveEventItem::setupToolTipString() -{ - // we display the event on screen - so translate - QString name = gettextFromC::instance()->tr(internalEvent->name); - int value = internalEvent->value; - int type = internalEvent->type; - if (value) { - if (event_is_gaschange(internalEvent)) { - name += ": "; - name += gasname(&internalEvent->gas.mix); - - /* Do we have an explicit cylinder index? Show it. */ - if (internalEvent->gas.index >= 0) - name += QString(" (cyl %1)").arg(internalEvent->gas.index+1); - } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { - name += QString(":%1").arg((double)value / 1000); - } else { - name += QString(":%1").arg(value); - } - } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { - // this is a bad idea - we are abusing an existing event type that is supposed to - // warn of high or low pOâ‚‚ and are turning it into a set point change event - name += "\n" + tr("Manual switch to OC"); - } else { - name += internalEvent->flags == SAMPLE_FLAGS_BEGIN ? tr(" begin", "Starts with space!") : - internalEvent->flags == SAMPLE_FLAGS_END ? tr(" end", "Starts with space!") : ""; - } - // qDebug() << name; - setToolTip(name); -} - -void DiveEventItem::eventVisibilityChanged(const QString &eventName, bool visible) -{ -} - -bool DiveEventItem::shouldBeHidden() -{ - struct event *event = internalEvent; - - /* - * Some gas change events are special. Some dive computers just tell us the initial gas this way. - * Don't bother showing those - */ - struct sample *first_sample = &get_dive_dc(&displayed_dive, dc_number)->sample[0]; - if (!strcmp(event->name, "gaschange") && - (event->time.seconds == 0 || - (first_sample && event->time.seconds == first_sample->time.seconds))) - return true; - - for (int i = 0; i < evn_used; i++) { - if (!strcmp(event->name, ev_namelist[i].ev_name) && ev_namelist[i].plot_ev == false) - return true; - } - return false; -} - -void DiveEventItem::recalculatePos(bool instant) -{ - if (!vAxis || !hAxis || !internalEvent || !dataModel) - return; - - QModelIndexList result = dataModel->match(dataModel->index(0, DivePlotDataModel::TIME), Qt::DisplayRole, internalEvent->time.seconds); - if (result.isEmpty()) { - Q_ASSERT("can't find a spot in the dataModel"); - hide(); - return; - } - if (!isVisible() && !shouldBeHidden()) - show(); - int depth = dataModel->data(dataModel->index(result.first().row(), DivePlotDataModel::DEPTH)).toInt(); - qreal x = hAxis->posAtValue(internalEvent->time.seconds); - qreal y = vAxis->posAtValue(depth); - if (!instant) - Animations::moveTo(this, x, y); - else - setPos(x, y); - if (isVisible() && shouldBeHidden()) - hide(); -} diff --git a/qt-ui/profile/diveeventitem.h b/qt-ui/profile/diveeventitem.h deleted file mode 100644 index f358fee6d..000000000 --- a/qt-ui/profile/diveeventitem.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef DIVEEVENTITEM_H -#define DIVEEVENTITEM_H - -#include "divepixmapitem.h" - -class DiveCartesianAxis; -class DivePlotDataModel; -struct event; - -class DiveEventItem : public DivePixmapItem { - Q_OBJECT -public: - DiveEventItem(QObject *parent = 0); - void setEvent(struct event *ev); - struct event *getEvent(); - void eventVisibilityChanged(const QString &eventName, bool visible); - void setVerticalAxis(DiveCartesianAxis *axis); - void setHorizontalAxis(DiveCartesianAxis *axis); - void setModel(DivePlotDataModel *model); - bool shouldBeHidden(); -public -slots: - void recalculatePos(bool instant = false); - -private: - void setupToolTipString(); - void setupPixmap(); - DiveCartesianAxis *vAxis; - DiveCartesianAxis *hAxis; - DivePlotDataModel *dataModel; - struct event *internalEvent; -}; - -#endif // DIVEEVENTITEM_H diff --git a/qt-ui/profile/divelineitem.cpp b/qt-ui/profile/divelineitem.cpp deleted file mode 100644 index f9e288a44..000000000 --- a/qt-ui/profile/divelineitem.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "divelineitem.h" - -DiveLineItem::DiveLineItem(QGraphicsItem *parent) : QGraphicsLineItem(parent) -{ -} diff --git a/qt-ui/profile/divelineitem.h b/qt-ui/profile/divelineitem.h deleted file mode 100644 index ec88e9da5..000000000 --- a/qt-ui/profile/divelineitem.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef DIVELINEITEM_H -#define DIVELINEITEM_H - -#include -#include - -class DiveLineItem : public QObject, public QGraphicsLineItem { - Q_OBJECT - Q_PROPERTY(QPointF pos READ pos WRITE setPos) - Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) -public: - DiveLineItem(QGraphicsItem *parent = 0); -}; - -#endif // DIVELINEITEM_H diff --git a/qt-ui/profile/divepixmapitem.cpp b/qt-ui/profile/divepixmapitem.cpp deleted file mode 100644 index 581f6f9b4..000000000 --- a/qt-ui/profile/divepixmapitem.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "divepixmapitem.h" -#include "animationfunctions.h" -#include "divepicturemodel.h" -#include - -#include -#include -#include - -DivePixmapItem::DivePixmapItem(QObject *parent) : QObject(parent), QGraphicsPixmapItem() -{ -} - -DiveButtonItem::DiveButtonItem(QObject *parent): DivePixmapItem(parent) -{ -} - -void DiveButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *event) -{ - QGraphicsItem::mousePressEvent(event); - emit clicked(); -} - -// If we have many many pictures on screen, maybe a shared-pixmap would be better to -// paint on screen, but for now, this. -CloseButtonItem::CloseButtonItem(QObject *parent): DiveButtonItem(parent) -{ - static QPixmap p = QPixmap(":trash"); - setPixmap(p); - setFlag(ItemIgnoresTransformations); -} - -void CloseButtonItem::hide() -{ - DiveButtonItem::hide(); -} - -void CloseButtonItem::show() -{ - DiveButtonItem::show(); -} - -DivePictureItem::DivePictureItem(QObject *parent): DivePixmapItem(parent), - canvas(new QGraphicsRectItem(this)), - shadow(new QGraphicsRectItem(this)) -{ - setFlag(ItemIgnoresTransformations); - setAcceptHoverEvents(true); - setScale(0.2); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); - setVisible(prefs.show_pictures_in_profile); - - canvas->setPen(Qt::NoPen); - canvas->setBrush(QColor(Qt::white)); - canvas->setFlag(ItemStacksBehindParent); - canvas->setZValue(-1); - - shadow->setPos(5,5); - shadow->setPen(Qt::NoPen); - shadow->setBrush(QColor(Qt::lightGray)); - shadow->setFlag(ItemStacksBehindParent); - shadow->setZValue(-2); -} - -void DivePictureItem::settingsChanged() -{ - setVisible(prefs.show_pictures_in_profile); -} - -void DivePictureItem::setPixmap(const QPixmap &pix) -{ - DivePixmapItem::setPixmap(pix); - QRectF r = boundingRect(); - canvas->setRect(0 - 10, 0 -10, r.width() + 20, r.height() + 20); - shadow->setRect(canvas->rect()); -} - -CloseButtonItem *button = NULL; -void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) -{ - Animations::scaleTo(this, 1.0); - setZValue(5); - - if(!button) { - button = new CloseButtonItem(); - button->setScale(0.2); - button->setZValue(7); - scene()->addItem(button); - } - button->setParentItem(this); - button->setPos(boundingRect().width() - button->boundingRect().width() * 0.2, - boundingRect().height() - button->boundingRect().height() * 0.2); - button->setOpacity(0); - button->show(); - Animations::show(button); - button->disconnect(); - connect(button, SIGNAL(clicked()), this, SLOT(removePicture())); -} - -void DivePictureItem::setFileUrl(const QString &s) -{ - fileUrl = s; -} - -void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) -{ - Animations::scaleTo(this, 0.2); - setZValue(0); - if(button){ - button->setParentItem(NULL); - Animations::hide(button); - } -} - -DivePictureItem::~DivePictureItem(){ - if(button){ - button->setParentItem(NULL); - Animations::hide(button); - } -} - -void DivePictureItem::mousePressEvent(QGraphicsSceneMouseEvent *event) -{ - QDesktopServices::openUrl(QUrl::fromLocalFile(fileUrl)); -} - -void DivePictureItem::removePicture() -{ - DivePictureModel::instance()->removePicture(fileUrl); -} diff --git a/qt-ui/profile/divepixmapitem.h b/qt-ui/profile/divepixmapitem.h deleted file mode 100644 index 02c1523f7..000000000 --- a/qt-ui/profile/divepixmapitem.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef DIVEPIXMAPITEM_H -#define DIVEPIXMAPITEM_H - -#include -#include - -class DivePixmapItem : public QObject, public QGraphicsPixmapItem { - Q_OBJECT - Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) - Q_PROPERTY(QPointF pos WRITE setPos READ pos) - Q_PROPERTY(qreal x WRITE setX READ x) - Q_PROPERTY(qreal y WRITE setY READ y) -public: - DivePixmapItem(QObject *parent = 0); -}; - -class DivePictureItem : public DivePixmapItem { - Q_OBJECT - Q_PROPERTY(qreal scale WRITE setScale READ scale) -public: - DivePictureItem(QObject *parent = 0); - virtual ~DivePictureItem(); - void setPixmap(const QPixmap& pix); -public slots: - void settingsChanged(); - void removePicture(); - void setFileUrl(const QString& s); -protected: - void hoverEnterEvent(QGraphicsSceneHoverEvent *event); - void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); - void mousePressEvent(QGraphicsSceneMouseEvent *event); -private: - QString fileUrl; - QGraphicsRectItem *canvas; - QGraphicsRectItem *shadow; -}; - -class DiveButtonItem : public DivePixmapItem { - Q_OBJECT -public: - DiveButtonItem(QObject *parent = 0); -protected: - virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); -signals: - void clicked(); -}; - -class CloseButtonItem : public DiveButtonItem { - Q_OBJECT -public: - CloseButtonItem(QObject *parent = 0); -public slots: - void hide(); - void show(); -}; - -#endif // DIVEPIXMAPITEM_H diff --git a/qt-ui/profile/diveprofileitem.cpp b/qt-ui/profile/diveprofileitem.cpp deleted file mode 100644 index 2c814678a..000000000 --- a/qt-ui/profile/diveprofileitem.cpp +++ /dev/null @@ -1,979 +0,0 @@ -#include "diveprofileitem.h" -#include "diveplotdatamodel.h" -#include "divecartesianaxis.h" -#include "divetextitem.h" -#include "animationfunctions.h" -#include "dive.h" -#include "profile.h" -#include "preferences.h" -#include "diveplannermodel.h" -#include "helpers.h" -#include "libdivecomputer/parser.h" -#include "mainwindow.h" -#include "maintab.h" -#include "profile/profilewidget2.h" -#include "diveplanner.h" - -#include - -AbstractProfilePolygonItem::AbstractProfilePolygonItem() : QObject(), QGraphicsPolygonItem(), hAxis(NULL), vAxis(NULL), dataModel(NULL), hDataColumn(-1), vDataColumn(-1) -{ - setCacheMode(DeviceCoordinateCache); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); -} - -void AbstractProfilePolygonItem::settingsChanged() -{ -} - -void AbstractProfilePolygonItem::setHorizontalAxis(DiveCartesianAxis *horizontal) -{ - hAxis = horizontal; - connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); - modelDataChanged(); -} - -void AbstractProfilePolygonItem::setHorizontalDataColumn(int column) -{ - hDataColumn = column; - modelDataChanged(); -} - -void AbstractProfilePolygonItem::setModel(DivePlotDataModel *model) -{ - dataModel = model; - connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex))); - connect(dataModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(modelDataRemoved(QModelIndex, int, int))); - modelDataChanged(); -} - -void AbstractProfilePolygonItem::modelDataRemoved(const QModelIndex &parent, int from, int to) -{ - setPolygon(QPolygonF()); - qDeleteAll(texts); - texts.clear(); -} - -void AbstractProfilePolygonItem::setVerticalAxis(DiveCartesianAxis *vertical) -{ - vAxis = vertical; - connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); - connect(vAxis, SIGNAL(maxChanged()), this, SLOT(modelDataChanged())); - modelDataChanged(); -} - -void AbstractProfilePolygonItem::setVerticalDataColumn(int column) -{ - vDataColumn = column; - modelDataChanged(); -} - -bool AbstractProfilePolygonItem::shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - if (!hAxis || !vAxis) - return false; - if (!dataModel || dataModel->rowCount() == 0) - return false; - if (hDataColumn == -1 || vDataColumn == -1) - return false; - if (topLeft.isValid() && bottomRight.isValid()) { - if ((topLeft.column() >= vDataColumn || topLeft.column() >= hDataColumn) && - (bottomRight.column() <= vDataColumn || topLeft.column() <= hDataColumn)) { - return true; - } - } - return true; -} - -void AbstractProfilePolygonItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - // We don't have enougth data to calculate things, quit. - - // 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 = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { - qreal horizontalValue = dataModel->index(i, hDataColumn).data().toReal(); - qreal verticalValue = dataModel->index(i, vDataColumn).data().toReal(); - QPointF point(hAxis->posAtValue(horizontalValue), vAxis->posAtValue(verticalValue)); - poly.append(point); - } - setPolygon(poly); - - qDeleteAll(texts); - texts.clear(); -} - -DiveProfileItem::DiveProfileItem() : show_reported_ceiling(0), reported_ceiling_in_red(0) -{ -} - -void DiveProfileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - Q_UNUSED(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 = 1, count = dataModel->rowCount(); i < count; i++) { - QModelIndex colorIndex = dataModel->index(i, DivePlotDataModel::COLOR); - pen.setBrush(QBrush(colorIndex.data(Qt::BackgroundRole).value())); - painter->setPen(pen); - painter->drawLine(poly[i - 1], poly[i]); - } - painter->restore(); -} - -int DiveProfileItem::maxCeiling(int row) -{ - int max = -1; - plot_data *entry = dataModel->data().entry + row; - for (int tissue = 0; tissue < 16; tissue++) { - if (max < entry->ceilings[tissue]) - max = entry->ceilings[tissue]; - } - return max; -} - -void DiveProfileItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - bool eventAdded = false; - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); - if (polygon().isEmpty()) - return; - - show_reported_ceiling = prefs.dcceiling; - reported_ceiling_in_red = prefs.redceiling; - profileColor = getColor(DEPTH_BOTTOM); - - int currState = qobject_cast(scene()->views().first())->currentState; - if (currState == ProfileWidget2::PLAN) { - plot_data *entry = dataModel->data().entry; - for (int i = 0; i < dataModel->rowCount(); i++, entry++) { - int max = maxCeiling(i); - // Don't scream if we violate the ceiling by a few cm - if (entry->depth < max - 100 && entry->sec > 0) { - profileColor = QColor(Qt::red); - if (!eventAdded) { - add_event(&displayed_dive.dc, entry->sec, SAMPLE_EVENT_CEILING, -1, max / 1000, "planned waypoint above ceiling"); - eventAdded = true; - } - } - } - } - - /* Show any ceiling we may have encountered */ - if (prefs.dcceiling && !prefs.redceiling) { - QPolygonF p = polygon(); - plot_data *entry = dataModel->data().entry + dataModel->rowCount() - 1; - for (int i = dataModel->rowCount() - 1; i >= 0; 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 = 0, count = dataModel->rowCount(); i < count; i++) { - - struct plot_data *entry = dataModel->data().entry + i; - if (entry->depth < 2000) - continue; - - if ((entry == entry->max[2]) && entry->depth / 100 != last) { - plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_DEEP)); - last = entry->depth / 100; - } - - if ((entry == entry->min[2]) && 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::settingsChanged() -{ - //TODO: Only modelDataChanged() here if we need to rebuild the graph ( for instance, - // if the prefs.dcceiling are enabled, but prefs.redceiling is disabled - // and only if it changed something. let's not waste cpu cycles repoloting something we don't need to. - modelDataChanged(); -} - -void DiveProfileItem::plot_depth_sample(struct plot_data *entry, QFlags flags, const QColor &color) -{ - int decimals; - double d = get_depth_units(entry->depth, &decimals, NULL); - DiveTextItem *item = new DiveTextItem(this); - item->setPos(hAxis->posAtValue(entry->sec), vAxis->posAtValue(entry->depth)); - item->setText(QString("%1").arg(d, 0, 'f', 1)); - item->setAlignment(flags); - item->setBrush(color); - texts.append(item); -} - -DiveHeartrateItem::DiveHeartrateItem() -{ - QPen pen; - pen.setBrush(QBrush(getColor(::HR_PLOT))); - pen.setCosmetic(true); - pen.setWidth(1); - setPen(pen); - settingsChanged(); -} - -void DiveHeartrateItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - int last = -300, last_printed_hr = 0, sec = 0; - struct { - int sec; - int hr; - } hist[3] = {}; - - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - qDeleteAll(texts); - texts.clear(); - // Ignore empty values. a heartrate of 0 would be a bad sign. - QPolygonF poly; - for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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; - createTextItem(sec, hr); - last_printed_hr = hr; - } - setPolygon(poly); - - if (texts.count()) - texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); -} - -void DiveHeartrateItem::createTextItem(int sec, int hr) -{ - DiveTextItem *text = new DiveTextItem(this); - text->setAlignment(Qt::AlignRight | Qt::AlignBottom); - text->setBrush(getColor(HR_TEXT)); - text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(hr))); - text->setScale(0.7); // need to call this BEFORE setText() - text->setText(QString("%1").arg(hr)); - texts.append(text); -} - -void DiveHeartrateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - painter->save(); - painter->setPen(pen()); - painter->drawPolyline(polygon()); - painter->restore(); -} - -void DiveHeartrateItem::settingsChanged() -{ - setVisible(prefs.hrgraph); -} - -DivePercentageItem::DivePercentageItem(int i) -{ - QPen pen; - QColor color; - color.setHsl(100 + 10 * i, 200, 100); - pen.setBrush(QBrush(color)); - pen.setCosmetic(true); - pen.setWidth(1); - setPen(pen); - settingsChanged(); -} - -void DivePercentageItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - int sec = 0; - - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - // Ignore empty values. a heartrate of 0 would be a bad sign. - QPolygonF poly; - for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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); - } - setPolygon(poly); - - if (texts.count()) - texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); -} - -void DivePercentageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - painter->save(); - painter->setPen(pen()); - painter->drawPolyline(polygon()); - painter->restore(); -} - -void DivePercentageItem::settingsChanged() -{ - setVisible(prefs.percentagegraph); -} - -DiveAmbPressureItem::DiveAmbPressureItem() -{ - QPen pen; - pen.setBrush(QBrush(getColor(::AMB_PRESSURE_LINE))); - pen.setCosmetic(true); - pen.setWidth(2); - setPen(pen); - settingsChanged(); -} - -void DiveAmbPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - int sec = 0; - - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - // Ignore empty values. a heartrate of 0 would be a bad sign. - QPolygonF poly; - for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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); - } - setPolygon(poly); - - if (texts.count()) - texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); -} - -void DiveAmbPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - painter->save(); - painter->setPen(pen()); - painter->drawPolyline(polygon()); - painter->restore(); -} - -void DiveAmbPressureItem::settingsChanged() -{ - setVisible(prefs.percentagegraph); -} - -DiveGFLineItem::DiveGFLineItem() -{ - QPen pen; - pen.setBrush(QBrush(getColor(::GF_LINE))); - pen.setCosmetic(true); - pen.setWidth(2); - setPen(pen); - settingsChanged(); -} - -void DiveGFLineItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - int sec = 0; - - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - // Ignore empty values. a heartrate of 0 would be a bad sign. - QPolygonF poly; - for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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); - } - setPolygon(poly); - - if (texts.count()) - texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); -} - -void DiveGFLineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - painter->save(); - painter->setPen(pen()); - painter->drawPolyline(polygon()); - painter->restore(); -} - -void DiveGFLineItem::settingsChanged() -{ - setVisible(prefs.percentagegraph); -} - -DiveTemperatureItem::DiveTemperatureItem() -{ - QPen pen; - pen.setBrush(QBrush(getColor(::TEMP_PLOT))); - pen.setCosmetic(true); - pen.setWidth(2); - setPen(pen); -} - -void DiveTemperatureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - int last = -300, last_printed_temp = 0, sec = 0, last_valid_temp = 0; - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - qDeleteAll(texts); - texts.clear(); - // Ignore empty values. things do not look good with '0' as temperature in kelvin... - QPolygonF poly; - for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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) - createTextItem(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))) { - createTextItem(sec, last_valid_temp); - } - if (texts.count()) - texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); -} - -void DiveTemperatureItem::createTextItem(int sec, int mkelvin) -{ - double deg; - const char *unit; - deg = get_temp_units(mkelvin, &unit); - - DiveTextItem *text = new DiveTextItem(this); - text->setAlignment(Qt::AlignRight | Qt::AlignBottom); - text->setBrush(getColor(TEMP_TEXT)); - text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(mkelvin))); - text->setScale(0.8); // need to call this BEFORE setText() - text->setText(QString("%1%2").arg(deg, 0, 'f', 1).arg(unit)); - texts.append(text); -} - -void DiveTemperatureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - painter->save(); - painter->setPen(pen()); - painter->drawPolyline(polygon()); - painter->restore(); -} - -DiveMeanDepthItem::DiveMeanDepthItem() -{ - QPen pen; - pen.setBrush(QBrush(getColor(::HR_AXIS))); - pen.setCosmetic(true); - pen.setWidth(2); - setPen(pen); - settingsChanged(); -} - -void DiveMeanDepthItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - double meandepthvalue = 0.0; - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - QPolygonF poly; - plot_data *entry = dataModel->data().entry; - for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; 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 *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - painter->save(); - painter->setPen(pen()); - painter->drawPolyline(polygon()); - painter->restore(); -} - -void DiveMeanDepthItem::settingsChanged() -{ - setVisible(prefs.show_average_depth); -} - -void DiveMeanDepthItem::createTextItem() { - plot_data *entry = dataModel->data().entry; - int sec = entry[dataModel->rowCount()-1].sec; - qDeleteAll(texts); - texts.clear(); - int decimals; - const char *unitText; - double d = get_depth_units(lastRunningSum, &decimals, &unitText); - DiveTextItem *text = new DiveTextItem(this); - text->setAlignment(Qt::AlignRight | Qt::AlignTop); - text->setBrush(getColor(TEMP_TEXT)); - text->setPos(QPointF(hAxis->posAtValue(sec) + 1, vAxis->posAtValue(lastRunningSum))); - text->setScale(0.8); // need to call this BEFORE setText() - text->setText(QString("%1%2").arg(d, 0, 'f', 1).arg(unitText)); - texts.append(text); -} - -void DiveGasPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - int last_index = -1; - int o2mbar; - QPolygonF boundingPoly, o2Poly; // This is the "Whole Item", but a pressure can be divided in N Polygons. - polygons.clear(); - if (displayed_dive.dc.divemode == CCR) - polygons.append(o2Poly); - - for (int i = 0, count = dataModel->rowCount(); i < count; i++) { - o2mbar = 0; - plot_data *entry = dataModel->data().entry + i; - int mbar = GET_PRESSURE(entry); - if (displayed_dive.dc.divemode == CCR) - o2mbar = GET_O2CYLINDER_PRESSURE(entry); - - if (entry->cylinderindex != last_index) { - polygons.append(QPolygonF()); // this is the polygon that will be actually drawn on screen. - last_index = entry->cylinderindex; - } - if (!mbar) { - continue; - } - if (o2mbar) { - QPointF o2point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(o2mbar)); - boundingPoly.push_back(o2point); - polygons.first().push_back(o2point); - } - - QPointF point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(mbar)); - boundingPoly.push_back(point); // The BoundingRect - polygons.last().push_back(point); // The polygon thta will be plotted. - } - setPolygon(boundingPoly); - qDeleteAll(texts); - texts.clear(); - int mbar, cyl; - int seen_cyl[MAX_CYLINDERS] = { false, }; - int last_pressure[MAX_CYLINDERS] = { 0, }; - int last_time[MAX_CYLINDERS] = { 0, }; - struct plot_data *entry; - - cyl = -1; - o2mbar = 0; - - double print_y_offset[8][2] = { { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } ,{ 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } }; - // CCR dives: These are offset values used to print the gas lables and pressures on a CCR dive profile at - // appropriate Y-coordinates: One doublet of values for each of 8 cylinders. - // Order of offsets within a doublet: gas lable offset; gas pressure offset. - // The array is initialised with default values that apply to non-CCR dives. - - bool offsets_initialised = false; - int o2cyl = -1, dilcyl = -1; - QFlags alignVar= Qt::AlignTop, align_dil = Qt::AlignBottom, align_o2 = Qt::AlignTop; - double axisRange = (vAxis->maximum() - vAxis->minimum())/1000; // Convert axis pressure range to bar - double axisLog = log10(log10(axisRange)); - for (int i = 0, count = dataModel->rowCount(); i < count; i++) { - entry = dataModel->data().entry + i; - mbar = GET_PRESSURE(entry); - if (displayed_dive.dc.divemode == CCR && displayed_dive.oxygen_cylinder_index >= 0) - o2mbar = GET_O2CYLINDER_PRESSURE(entry); - - if (o2mbar) { // If there is an o2mbar value then this is a CCR dive. Then do: - // The first time an o2 value is detected, see if the oxygen cyl pressure graph starts above or below the dil graph - if (!offsets_initialised) { // Initialise the parameters for placing the text correctly near the graph line: - o2cyl = displayed_dive.oxygen_cylinder_index; - dilcyl = displayed_dive.diluent_cylinder_index; - if ((o2mbar > mbar)) { // If above, write o2 start cyl pressure above graph and diluent pressure below graph: - print_y_offset[o2cyl][0] = -7 * axisLog; // y offset for oxygen gas lable (above); pressure offsets=-0.5, already initialised - print_y_offset[dilcyl][0] = 5 * axisLog; // y offset for diluent gas lable (below) - } else { // ... else write o2 start cyl pressure below graph: - print_y_offset[o2cyl][0] = 5 * axisLog; // o2 lable & pressure below graph; pressure offsets=-0.5, already initialised - print_y_offset[dilcyl][0] = -7.8 * axisLog; // and diluent lable above graph. - align_dil = Qt::AlignTop; - align_o2 = Qt::AlignBottom; - } - offsets_initialised = true; - } - - if (!seen_cyl[displayed_dive.oxygen_cylinder_index]) { //For o2, on the left of profile, write lable and pressure - plotPressureValue(o2mbar, entry->sec, align_o2, print_y_offset[o2cyl][1]); - plotGasValue(o2mbar, entry->sec, displayed_dive.cylinder[displayed_dive.oxygen_cylinder_index].gasmix, align_o2, print_y_offset[o2cyl][0]); - seen_cyl[displayed_dive.oxygen_cylinder_index] = true; - } - last_pressure[displayed_dive.oxygen_cylinder_index] = o2mbar; - last_time[displayed_dive.oxygen_cylinder_index] = entry->sec; - alignVar = align_dil; - } - - if (!mbar) - continue; - - if (cyl != entry->cylinderindex) { // Pressure value near the left hand edge of the profile - other cylinders: - cyl = entry->cylinderindex; // For each other cylinder, write the gas lable and pressure - if (!seen_cyl[cyl]) { - plotPressureValue(mbar, entry->sec, alignVar, print_y_offset[cyl][1]); - plotGasValue(mbar, entry->sec, displayed_dive.cylinder[cyl].gasmix, align_dil, print_y_offset[cyl][0]); - seen_cyl[cyl] = true; - } - } - last_pressure[cyl] = mbar; - last_time[cyl] = entry->sec; - } - - for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { // For each cylinder, on right hand side of profile, write cylinder pressure - alignVar = ((o2cyl >= 0) && (cyl == displayed_dive.oxygen_cylinder_index)) ? align_o2 : align_dil; - if (last_time[cyl]) { - plotPressureValue(last_pressure[cyl], last_time[cyl], (alignVar | Qt::AlignLeft), print_y_offset[cyl][1]); - } - } -} - -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(this); - text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + pressure_offset ); - text->setText(QString("%1 %2").arg(pressure).arg(unit)); - text->setAlignment(align); - text->setBrush(getColor(PRESSURE_TEXT)); - 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(this); - text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + gasname_offset ); - text->setText(gas); - text->setAlignment(align); - text->setBrush(getColor(PRESSURE_TEXT)); - texts.push_back(text); -} - -void DiveGasPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - QPen pen; - pen.setCosmetic(true); - pen.setWidth(2); - painter->save(); - struct plot_data *entry; - Q_FOREACH (const QPolygonF &poly, polygons) { - entry = dataModel->data().entry; - for (int i = 1, count = poly.count(); i < count; i++, entry++) { - if (entry->sac) - pen.setBrush(getSacColor(entry->sac, displayed_dive.sac)); - else - pen.setBrush(MED_GRAY_HIGH_TRANS); - painter->setPen(pen); - painter->drawLine(poly[i - 1], poly[i]); - } - } - painter->restore(); -} - -DiveCalculatedCeiling::DiveCalculatedCeiling() : is3mIncrement(false) -{ - settingsChanged(); -} - -void DiveCalculatedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - if (MainWindow::instance()->information()) - connect(MainWindow::instance()->information(), SIGNAL(dateTimeChanged()), this, SLOT(recalc()), Qt::UniqueConnection); - - // We don't have enougth data to calculate things, quit. - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); - // Add 2 points to close the polygon. - QPolygonF poly = polygon(); - if (poly.isEmpty()) - return; - QPointF p1 = poly.first(); - QPointF p2 = poly.last(); - - poly.prepend(QPointF(p1.x(), vAxis->posAtValue(0))); - poly.append(QPointF(p2.x(), vAxis->posAtValue(0))); - setPolygon(poly); - - 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() -{ - settingsChanged(); -} - -void DiveCalculatedTissue::settingsChanged() -{ - setVisible(prefs.calcalltissues && prefs.calcceiling); -} - -void DiveReportedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - QPolygonF p; - p.append(QPointF(hAxis->posAtValue(0), vAxis->posAtValue(0))); - plot_data *entry = dataModel->data().entry; - for (int i = 0, count = dataModel->rowCount(); i < count; i++, entry++) { - if (entry->in_deco && entry->stopdepth) { - p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(qMin(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 DiveCalculatedCeiling::recalc() -{ - dataModel->calculateDecompression(); -} - -void DiveCalculatedCeiling::settingsChanged() -{ - if (dataModel && is3mIncrement != prefs.calcceiling3m) { - // recalculate that part. - recalc(); - } - is3mIncrement = prefs.calcceiling3m; - setVisible(prefs.calcceiling); -} - -void DiveReportedCeiling::settingsChanged() -{ - setVisible(prefs.dcceiling); -} - -void DiveReportedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (polygon().isEmpty()) - return; - QGraphicsPolygonItem::paint(painter, option, widget); -} - -void PartialPressureGasItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - //AbstractProfilePolygonItem::modelDataChanged(); - if (!shouldCalculateStuff(topLeft, bottomRight)) - return; - - plot_data *entry = dataModel->data().entry; - QPolygonF poly; - QPolygonF alertpoly; - alertPolygons.clear(); - QSettings s; - s.beginGroup("TecDetails"); - double threshold = 0.0; - if (thresholdPtr) - threshold = *thresholdPtr; - bool inAlertFragment = false; - for (int i = 0; i < dataModel->rowCount(); 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 (value >= threshold) { - 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" UTF8_SUBSCRIPT_2),getColor(PN2), legendPos); - */ -} - -void PartialPressureGasItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - 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::setThreshouldSettingsKey(double *prefPointer) -{ - thresholdPtr = prefPointer; -} - -PartialPressureGasItem::PartialPressureGasItem() : - thresholdPtr(NULL) -{ -} - -void PartialPressureGasItem::settingsChanged() -{ - QSettings s; - s.beginGroup("TecDetails"); - setVisible(s.value(visibilityKey).toBool()); -} - -void PartialPressureGasItem::setVisibilitySettingsKey(const QString &key) -{ - visibilityKey = key; -} - -void PartialPressureGasItem::setColors(const QColor &normal, const QColor &alert) -{ - normalColor = normal; - alertColor = alert; -} diff --git a/qt-ui/profile/diveprofileitem.h b/qt-ui/profile/diveprofileitem.h deleted file mode 100644 index 2160782f7..000000000 --- a/qt-ui/profile/diveprofileitem.h +++ /dev/null @@ -1,226 +0,0 @@ -#ifndef DIVEPROFILEITEM_H -#define DIVEPROFILEITEM_H - -#include -#include -#include - -#include "graphicsview-common.h" -#include "divelineitem.h" - -/* This is the Profile Item, it should be used for quite a lot of things - on the profile view. The usage should be pretty simple: - - DiveProfileItem *profile = new DiveProfileItem(); - profile->setVerticalAxis( profileYAxis ); - profile->setHorizontalAxis( timeAxis ); - profile->setModel( DiveDataModel ); - profile->setHorizontalDataColumn( DiveDataModel::TIME ); - profile->setVerticalDataColumn( DiveDataModel::DEPTH ); - scene()->addItem(profile); - - This is a generically item and should be used as a base for others, I think... -*/ - -class DivePlotDataModel; -class DiveTextItem; -class DiveCartesianAxis; -class QAbstractTableModel; -struct plot_data; - -class AbstractProfilePolygonItem : public QObject, public QGraphicsPolygonItem { - Q_OBJECT - Q_PROPERTY(QPointF pos WRITE setPos READ pos) - Q_PROPERTY(qreal x WRITE setX READ x) - Q_PROPERTY(qreal y WRITE setY READ y) -public: - AbstractProfilePolygonItem(); - void setVerticalAxis(DiveCartesianAxis *vertical); - void setHorizontalAxis(DiveCartesianAxis *horizontal); - void setModel(DivePlotDataModel *model); - void setHorizontalDataColumn(int column); - void setVerticalDataColumn(int column); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) = 0; - virtual void clear() - { - } -public -slots: - virtual void settingsChanged(); - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void modelDataRemoved(const QModelIndex &parent, int from, int to); - -protected: - /* when the model emits a 'datachanged' signal, this method below should be used to check if the - * modified data affects this particular item ( for example, when setting the '3m increment' - * the data for Ceiling and tissues will be changed, and only those. so, the topLeft will be the CEILING - * column and the bottomRight will have the TISSUE_16 column. this method takes the vDataColumn and hDataColumn - * into consideration when returning 'true' for "yes, continue the calculation', and 'false' for - * 'do not recalculate, we already have the right data. - */ - bool shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight); - - DiveCartesianAxis *hAxis; - DiveCartesianAxis *vAxis; - DivePlotDataModel *dataModel; - int hDataColumn; - int vDataColumn; - QList texts; -}; - -class DiveProfileItem : public AbstractProfilePolygonItem { - Q_OBJECT - -public: - DiveProfileItem(); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void settingsChanged(); - void plot_depth_sample(struct plot_data *entry, QFlags flags, const QColor &color); - int maxCeiling(int row); - -private: - unsigned int show_reported_ceiling; - unsigned int reported_ceiling_in_red; - QColor profileColor; -}; - -class DiveMeanDepthItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DiveMeanDepthItem(); - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - virtual void settingsChanged(); - -private: - void createTextItem(); - double lastRunningSum; - QString visibilityKey; -}; - -class DiveTemperatureItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DiveTemperatureItem(); - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - -private: - void createTextItem(int seconds, int mkelvin); -}; - -class DiveHeartrateItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DiveHeartrateItem(); - virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); - virtual void settingsChanged(); - -private: - void createTextItem(int seconds, int hr); - QString visibilityKey; -}; - -class DivePercentageItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DivePercentageItem(int i); - virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); - virtual void settingsChanged(); - -private: - QString visibilityKey; -}; - -class DiveAmbPressureItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DiveAmbPressureItem(); - virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); - virtual void settingsChanged(); - -private: - QString visibilityKey; -}; - -class DiveGFLineItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - DiveGFLineItem(); - virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); - virtual void settingsChanged(); - -private: - QString visibilityKey; -}; - -class DiveGasPressureItem : public AbstractProfilePolygonItem { - Q_OBJECT - -public: - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - -private: - void plotPressureValue(int mbar, int sec, QFlags align, double offset); - void plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags align, double offset); - QVector polygons; -}; - -class DiveCalculatedCeiling : public AbstractProfilePolygonItem { - Q_OBJECT - -public: - DiveCalculatedCeiling(); - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - virtual void settingsChanged(); - -public -slots: - void recalc(); - -private: - bool is3mIncrement; -}; - -class DiveReportedCeiling : public AbstractProfilePolygonItem { - Q_OBJECT - -public: - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - virtual void settingsChanged(); -}; - -class DiveCalculatedTissue : public DiveCalculatedCeiling { - Q_OBJECT -public: - DiveCalculatedTissue(); - virtual void settingsChanged(); -}; - -class PartialPressureGasItem : public AbstractProfilePolygonItem { - Q_OBJECT -public: - PartialPressureGasItem(); - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - virtual void settingsChanged(); - void setThreshouldSettingsKey(double *prefPointer); - void setVisibilitySettingsKey(const QString &setVisibilitySettingsKey); - void setColors(const QColor &normalColor, const QColor &alertColor); - -private: - QVector alertPolygons; - double *thresholdPtr; - QString visibilityKey; - QColor normalColor; - QColor alertColor; -}; -#endif // DIVEPROFILEITEM_H diff --git a/qt-ui/profile/diverectitem.cpp b/qt-ui/profile/diverectitem.cpp deleted file mode 100644 index 8cb60c3f5..000000000 --- a/qt-ui/profile/diverectitem.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "diverectitem.h" - -DiveRectItem::DiveRectItem(QObject *parent, QGraphicsItem *parentItem) : QObject(parent), QGraphicsRectItem(parentItem) -{ -} diff --git a/qt-ui/profile/diverectitem.h b/qt-ui/profile/diverectitem.h deleted file mode 100644 index e616cf591..000000000 --- a/qt-ui/profile/diverectitem.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef DIVERECTITEM_H -#define DIVERECTITEM_H - -#include -#include - -class DiveRectItem : public QObject, public QGraphicsRectItem { - Q_OBJECT - Q_PROPERTY(QRectF rect WRITE setRect READ rect) - Q_PROPERTY(QPointF pos WRITE setPos READ pos) - Q_PROPERTY(qreal x WRITE setX READ x) - Q_PROPERTY(qreal y WRITE setY READ y) -public: - DiveRectItem(QObject *parent = 0, QGraphicsItem *parentItem = 0); -}; - -#endif // DIVERECTITEM_H diff --git a/qt-ui/profile/divetextitem.cpp b/qt-ui/profile/divetextitem.cpp deleted file mode 100644 index 9c7848cd4..000000000 --- a/qt-ui/profile/divetextitem.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "divetextitem.h" -#include "mainwindow.h" -#include "profilewidget2.h" - -DiveTextItem::DiveTextItem(QGraphicsItem *parent) : QGraphicsItemGroup(parent), - internalAlignFlags(Qt::AlignHCenter | Qt::AlignVCenter), - textBackgroundItem(new QGraphicsPathItem(this)), - textItem(new QGraphicsPathItem(this)), - printScale(1.0), - scale(1.0), - connected(false) -{ - setFlag(ItemIgnoresTransformations); - textBackgroundItem->setBrush(QBrush(getColor(TEXT_BACKGROUND))); - textBackgroundItem->setPen(Qt::NoPen); - textItem->setPen(Qt::NoPen); -} - -void DiveTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - updateText(); - QGraphicsItemGroup::paint(painter, option, widget); -} - -void DiveTextItem::fontPrintScaleUpdate(double scale) -{ - printScale = scale; -} - -void DiveTextItem::setAlignment(int alignFlags) -{ - if (alignFlags != internalAlignFlags) { - internalAlignFlags = alignFlags; - } -} - -void DiveTextItem::setBrush(const QBrush &b) -{ - textItem->setBrush(b); -} - -void DiveTextItem::setScale(double newscale) -{ - if (scale != newscale) { - scale = newscale; - } -} - -void DiveTextItem::setText(const QString &t) -{ - if (internalText != t) { - if (!connected) { - if (scene()) { - // by now we should be on a scene. grab the profile widget from it and setup our printScale - // and connect to the signal that makes sure we keep track if that changes - ProfileWidget2 *profile = qobject_cast(scene()->views().first()); - connect(profile, SIGNAL(fontPrintScaleChanged(double)), this, SLOT(fontPrintScaleUpdate(double)), Qt::UniqueConnection); - fontPrintScaleUpdate(profile->getFontPrintScale()); - connected = true; - } else { - qDebug() << "called before scene was set up" << t; - } - } - internalText = t; - updateText(); - } -} - -const QString &DiveTextItem::text() -{ - return internalText; -} - -void DiveTextItem::updateText() -{ - double size; - if (internalText.isEmpty()) { - return; - } - - QFont fnt(qApp->font()); - if ((size = fnt.pixelSize()) > 0) { - // set in pixels - so the scale factor may not make a difference if it's too close to 1 - size *= scale * printScale; - fnt.setPixelSize(size); - } else { - size = fnt.pointSizeF(); - size *= scale * printScale; - fnt.setPointSizeF(size); - } - QFontMetrics fm(fnt); - - QPainterPath textPath; - qreal xPos = 0, yPos = 0; - - QRectF rect = fm.boundingRect(internalText); - yPos = (internalAlignFlags & Qt::AlignTop) ? 0 : - (internalAlignFlags & Qt::AlignBottom) ? +rect.height() : - /*(internalAlignFlags & Qt::AlignVCenter ? */ +rect.height() / 4; - - xPos = (internalAlignFlags & Qt::AlignLeft) ? -rect.width() : - (internalAlignFlags & Qt::AlignHCenter) ? -rect.width() / 2 : - /* (internalAlignFlags & Qt::AlignRight) */ 0; - - textPath.addText(xPos, yPos, fnt, internalText); - QPainterPathStroker stroker; - stroker.setWidth(3); - textBackgroundItem->setPath(stroker.createStroke(textPath)); - textItem->setPath(textPath); -} diff --git a/qt-ui/profile/divetextitem.h b/qt-ui/profile/divetextitem.h deleted file mode 100644 index 3991fe7f3..000000000 --- a/qt-ui/profile/divetextitem.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef DIVETEXTITEM_H -#define DIVETEXTITEM_H - -#include -#include -#include "graphicsview-common.h" -#include - -/* A Line Item that has animated-properties. */ -class DiveTextItem : public QObject, public QGraphicsItemGroup { - Q_OBJECT - Q_PROPERTY(QPointF pos READ pos WRITE setPos) - Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) -public: - DiveTextItem(QGraphicsItem *parent = 0); - void setText(const QString &text); - void setAlignment(int alignFlags); - void setScale(double newscale); - void setBrush(const QBrush &brush); - const QString &text(); - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); - -private -slots: - void fontPrintScaleUpdate(double scale); - -private: - void updateText(); - int internalAlignFlags; - QGraphicsPathItem *textBackgroundItem; - QGraphicsPathItem *textItem; - QString internalText; - double printScale; - double scale; - bool connected; -}; - -#endif // DIVETEXTITEM_H diff --git a/qt-ui/profile/divetooltipitem.cpp b/qt-ui/profile/divetooltipitem.cpp deleted file mode 100644 index d4818422b..000000000 --- a/qt-ui/profile/divetooltipitem.cpp +++ /dev/null @@ -1,285 +0,0 @@ -#include "divetooltipitem.h" -#include "divecartesianaxis.h" -#include "dive.h" -#include "profile.h" -#include "membuffer.h" -#include "metrics.h" -#include -#include -#include -#include - -#define PORT_IN_PROGRESS 1 -#ifdef PORT_IN_PROGRESS -#include "display.h" -#endif - -void ToolTipItem::addToolTip(const QString &toolTip, const QIcon &icon, const QPixmap& pixmap) -{ - const IconMetrics& iconMetrics = defaultIconMetrics(); - - QGraphicsPixmapItem *iconItem = 0; - double yValue = title->boundingRect().height() + iconMetrics.spacing; - Q_FOREACH (ToolTip t, toolTips) { - yValue += t.second->boundingRect().height(); - } - if (entryToolTip.second) { - yValue += entryToolTip.second->boundingRect().height(); - } - iconItem = new QGraphicsPixmapItem(this); - if (!icon.isNull()) { - iconItem->setPixmap(icon.pixmap(iconMetrics.sz_small, iconMetrics.sz_small)); - } else if (!pixmap.isNull()) { - iconItem->setPixmap(pixmap); - } - iconItem->setPos(iconMetrics.spacing, yValue); - - QGraphicsSimpleTextItem *textItem = new QGraphicsSimpleTextItem(toolTip, this); - textItem->setPos(iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing, yValue); - textItem->setBrush(QBrush(Qt::white)); - textItem->setFlag(ItemIgnoresTransformations); - toolTips.push_back(qMakePair(iconItem, textItem)); -} - -void ToolTipItem::clear() -{ - Q_FOREACH (ToolTip t, toolTips) { - delete t.first; - delete t.second; - } - toolTips.clear(); -} - -void ToolTipItem::setRect(const QRectF &r) -{ - if( r == rect() ) { - return; - } - - QGraphicsRectItem::setRect(r); - updateTitlePosition(); -} - -void ToolTipItem::collapse() -{ - int dim = defaultIconMetrics().sz_small; - - if (prefs.animation_speed) { - QPropertyAnimation *animation = new QPropertyAnimation(this, "rect"); - animation->setDuration(100); - animation->setStartValue(nextRectangle); - animation->setEndValue(QRect(0, 0, dim, dim)); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - setRect(nextRectangle); - } - clear(); - - status = COLLAPSED; -} - -void ToolTipItem::expand() -{ - if (!title) - return; - - const IconMetrics& iconMetrics = defaultIconMetrics(); - - double width = 0, height = title->boundingRect().height() + iconMetrics.spacing; - Q_FOREACH (const ToolTip& t, toolTips) { - QRectF sRect = t.second->boundingRect(); - if (sRect.width() > width) - width = sRect.width(); - height += sRect.height(); - } - - if (entryToolTip.first) { - QRectF sRect = entryToolTip.second->boundingRect(); - if (sRect.width() > width) - width = sRect.width(); - height += sRect.height(); - } - - /* Left padding, Icon Size, space, right padding */ - width += iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing + iconMetrics.spacing; - - if (width < title->boundingRect().width() + iconMetrics.spacing * 2) - width = title->boundingRect().width() + iconMetrics.spacing * 2; - - if (height < iconMetrics.sz_small) - height = iconMetrics.sz_small; - - nextRectangle.setWidth(width); - nextRectangle.setHeight(height); - - if (nextRectangle != rect()) { - if (prefs.animation_speed) { - QPropertyAnimation *animation = new QPropertyAnimation(this, "rect", this); - animation->setDuration(prefs.animation_speed); - animation->setStartValue(rect()); - animation->setEndValue(nextRectangle); - animation->start(QAbstractAnimation::DeleteWhenStopped); - } else { - setRect(nextRectangle); - } - } - - status = EXPANDED; -} - -ToolTipItem::ToolTipItem(QGraphicsItem *parent) : QGraphicsRectItem(parent), - title(new QGraphicsSimpleTextItem(tr("Information"), this)), - status(COLLAPSED), - timeAxis(0), - lastTime(-1) -{ - memset(&pInfo, 0, sizeof(pInfo)); - entryToolTip.first = NULL; - entryToolTip.second = NULL; - setFlags(ItemIgnoresTransformations | ItemIsMovable | ItemClipsChildrenToShape); - - QColor c = QColor(Qt::black); - c.setAlpha(155); - setBrush(c); - - setZValue(99); - - addToolTip(QString(), QIcon(), QPixmap(16,60)); - entryToolTip = toolTips.first(); - toolTips.clear(); - - title->setFlag(ItemIgnoresTransformations); - title->setPen(QPen(Qt::white, 1)); - title->setBrush(Qt::white); - - setPen(QPen(Qt::white, 2)); - refreshTime.start(); -} - -ToolTipItem::~ToolTipItem() -{ - clear(); -} - -void ToolTipItem::updateTitlePosition() -{ - const IconMetrics& iconMetrics = defaultIconMetrics(); - if (rect().width() < title->boundingRect().width() + iconMetrics.spacing * 4) { - QRectF newRect = rect(); - newRect.setWidth(title->boundingRect().width() + iconMetrics.spacing * 4); - newRect.setHeight((newRect.height() && isExpanded()) ? newRect.height() : iconMetrics.sz_small); - setRect(newRect); - } - - title->setPos(rect().width() / 2 - title->boundingRect().width() / 2 - 1, 0); -} - -bool ToolTipItem::isExpanded() const -{ - return status == EXPANDED; -} - -void ToolTipItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) -{ - persistPos(); - QGraphicsRectItem::mouseReleaseEvent(event); - Q_FOREACH (QGraphicsItem *item, oldSelection) { - item->setSelected(true); - } -} - -void ToolTipItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - Q_UNUSED(widget); - painter->save(); - painter->setClipRect(option->rect); - painter->setPen(pen()); - painter->setBrush(brush()); - painter->drawRoundedRect(rect(), 10, 10, Qt::AbsoluteSize); - painter->restore(); -} - -void ToolTipItem::persistPos() -{ - QSettings s; - s.beginGroup("ProfileMap"); - s.setValue("tooltip_position", pos()); - s.endGroup(); -} - -void ToolTipItem::readPos() -{ - QSettings s; - s.beginGroup("ProfileMap"); - QPointF value = s.value("tooltip_position").toPoint(); - if (!scene()->sceneRect().contains(value)) { - value = QPointF(0, 0); - } - setPos(value); -} - -void ToolTipItem::setPlotInfo(const plot_info &plot) -{ - pInfo = plot; -} - -void ToolTipItem::setTimeAxis(DiveCartesianAxis *axis) -{ - timeAxis = axis; -} - -void ToolTipItem::refresh(const QPointF &pos) -{ - struct plot_data *entry; - static QPixmap tissues(16,60); - static QPainter painter(&tissues); - static struct membuffer mb = { 0 }; - - if(refreshTime.elapsed() < 40) - return; - refreshTime.start(); - - int time = timeAxis->valueAt(pos); - if (time == lastTime) - return; - - lastTime = time; - clear(); - - mb.len = 0; - entry = get_plot_details_new(&pInfo, time, &mb); - if (entry) { - tissues.fill(); - painter.setPen(QColor(0, 0, 0, 0)); - painter.setBrush(QColor(LIMENADE1)); - painter.drawRect(0, 10 + (100 - AMB_PERCENTAGE) / 2, 16, AMB_PERCENTAGE / 2); - painter.setBrush(QColor(SPRINGWOOD1)); - painter.drawRect(0, 10, 16, (100 - AMB_PERCENTAGE) / 2); - painter.setBrush(QColor(Qt::red)); - painter.drawRect(0,0,16,10); - painter.setPen(QColor(0, 0, 0, 255)); - painter.drawLine(0, 60 - entry->gfline / 2, 16, 60 - entry->gfline / 2); - painter.drawLine(0, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure / 2, - 16, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure /2); - painter.setPen(QColor(0, 0, 0, 127)); - for (int i=0; i<16; i++) { - painter.drawLine(i, 60, i, 60 - entry->percentages[i] / 2); - } - entryToolTip.first->setPixmap(tissues); - entryToolTip.second->setText(QString::fromUtf8(mb.buffer, mb.len)); - } - - Q_FOREACH (QGraphicsItem *item, scene()->items(pos, Qt::IntersectsItemBoundingRect - ,Qt::DescendingOrder, scene()->views().first()->transform())) { - if (!item->toolTip().isEmpty()) - addToolTip(item->toolTip()); - } - expand(); -} - -void ToolTipItem::mousePressEvent(QGraphicsSceneMouseEvent *event) -{ - oldSelection = scene()->selectedItems(); - scene()->clearSelection(); - QGraphicsItem::mousePressEvent(event); -} diff --git a/qt-ui/profile/divetooltipitem.h b/qt-ui/profile/divetooltipitem.h deleted file mode 100644 index 4fa7ec2d7..000000000 --- a/qt-ui/profile/divetooltipitem.h +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef DIVETOOLTIPITEM_H -#define DIVETOOLTIPITEM_H - -#include -#include -#include -#include -#include -#include -#include "display.h" - -class DiveCartesianAxis; -class QGraphicsLineItem; -class QGraphicsSimpleTextItem; -class QGraphicsPixmapItem; -struct graphics_context; - -/* To use a tooltip, simply ->setToolTip on the QGraphicsItem that you want - * or, if it's a "global" tooltip, set it on the mouseMoveEvent of the ProfileGraphicsView. - */ -class ToolTipItem : public QObject, public QGraphicsRectItem { - Q_OBJECT - void updateTitlePosition(); - Q_PROPERTY(QRectF rect READ rect WRITE setRect) - -public: - enum Status { - COLLAPSED, - EXPANDED - }; - - explicit ToolTipItem(QGraphicsItem *parent = 0); - virtual ~ToolTipItem(); - - void collapse(); - void expand(); - void clear(); - void addToolTip(const QString &toolTip, const QIcon &icon = QIcon(), const QPixmap &pixmap = QPixmap()); - void refresh(const QPointF &pos); - bool isExpanded() const; - void persistPos(); - void readPos(); - void mousePressEvent(QGraphicsSceneMouseEvent *event); - void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); - void setTimeAxis(DiveCartesianAxis *axis); - void setPlotInfo(const plot_info &plot); - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); -public -slots: - void setRect(const QRectF &rect); - -private: - typedef QPair ToolTip; - QVector toolTips; - ToolTip entryToolTip; - QGraphicsSimpleTextItem *title; - Status status; - QRectF rectangle; - QRectF nextRectangle; - DiveCartesianAxis *timeAxis; - plot_info pInfo; - int lastTime; - QTime refreshTime; - QList oldSelection; -}; - -#endif // DIVETOOLTIPITEM_H diff --git a/qt-ui/profile/profilewidget2.cpp b/qt-ui/profile/profilewidget2.cpp deleted file mode 100644 index 3ccd1bb6d..000000000 --- a/qt-ui/profile/profilewidget2.cpp +++ /dev/null @@ -1,1836 +0,0 @@ -#include "profilewidget2.h" -#include "diveplotdatamodel.h" -#include "helpers.h" -#include "profile.h" -#include "diveeventitem.h" -#include "divetextitem.h" -#include "divetooltipitem.h" -#include "planner.h" -#include "device.h" -#include "ruleritem.h" -#include "tankitem.h" -#include "pref.h" -#include "divepicturewidget.h" -#include "diveplannermodel.h" -#include "models.h" -#include "divepicturemodel.h" -#include "maintab.h" -#include "diveplanner.h" - -#include -#include -#include -#include -#include -#include -#include - -#ifndef QT_NO_DEBUG -#include -#endif -#include "mainwindow.h" -#include - -/* This is the global 'Item position' variable. - * it should tell you where to position things up - * on the canvas. - * - * please, please, please, use this instead of - * hard coding the item on the scene with a random - * value. - */ -static struct _ItemPos { - struct _Pos { - QPointF on; - QPointF off; - }; - struct _Axis { - _Pos pos; - QLineF shrinked; - QLineF expanded; - QLineF intermediate; - }; - _Pos background; - _Pos dcLabel; - _Pos tankBar; - _Axis depth; - _Axis partialPressure; - _Axis partialPressureTissue; - _Axis partialPressureWithTankBar; - _Axis percentage; - _Axis percentageWithTankBar; - _Axis time; - _Axis cylinder; - _Axis temperature; - _Axis temperatureAll; - _Axis heartBeat; - _Axis heartBeatWithTankBar; -} itemPos; - -ProfileWidget2::ProfileWidget2(QWidget *parent) : QGraphicsView(parent), - currentState(INVALID), - dataModel(new DivePlotDataModel(this)), - zoomLevel(0), - zoomFactor(1.15), - background(new DivePixmapItem()), - backgroundFile(":poster"), - toolTipItem(new ToolTipItem()), - isPlotZoomed(prefs.zoomed_plot),// no! bad use of prefs. 'PreferencesDialog::loadSettings' NOT CALLED yet. - profileYAxis(new DepthAxis()), - gasYAxis(new PartialGasPressureAxis()), - temperatureAxis(new TemperatureAxis()), - timeAxis(new TimeAxis()), - diveProfileItem(new DiveProfileItem()), - temperatureItem(new DiveTemperatureItem()), - meanDepthItem(new DiveMeanDepthItem()), - cylinderPressureAxis(new DiveCartesianAxis()), - gasPressureItem(new DiveGasPressureItem()), - diveComputerText(new DiveTextItem()), - diveCeiling(new DiveCalculatedCeiling()), - gradientFactor(new DiveTextItem()), - reportedCeiling(new DiveReportedCeiling()), - pn2GasItem(new PartialPressureGasItem()), - pheGasItem(new PartialPressureGasItem()), - po2GasItem(new PartialPressureGasItem()), - o2SetpointGasItem(new PartialPressureGasItem()), - ccrsensor1GasItem(new PartialPressureGasItem()), - ccrsensor2GasItem(new PartialPressureGasItem()), - ccrsensor3GasItem(new PartialPressureGasItem()), - heartBeatAxis(new DiveCartesianAxis()), - heartBeatItem(new DiveHeartrateItem()), - percentageAxis(new DiveCartesianAxis()), - ambPressureItem(new DiveAmbPressureItem()), - gflineItem(new DiveGFLineItem()), - mouseFollowerVertical(new DiveLineItem()), - mouseFollowerHorizontal(new DiveLineItem()), - rulerItem(new RulerItem2()), - tankItem(new TankItem()), - isGrayscale(false), - printMode(false), - shouldCalculateMaxTime(true), - shouldCalculateMaxDepth(true), - fontPrintScale(1.0) -{ - // would like to be able to ASSERT here that PreferencesDialog::loadSettings has been called. - isPlotZoomed = prefs.zoomed_plot; // now it seems that 'prefs' has loaded our preferences - - memset(&plotInfo, 0, sizeof(plotInfo)); - - setupSceneAndFlags(); - setupItemSizes(); - setupItemOnScene(); - addItemsToScene(); - scene()->installEventFilter(this); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); - QAction *action = NULL; -#define ADD_ACTION(SHORTCUT, Slot) \ - action = new QAction(this); \ - action->setShortcut(SHORTCUT); \ - action->setShortcutContext(Qt::WindowShortcut); \ - addAction(action); \ - connect(action, SIGNAL(triggered(bool)), this, SLOT(Slot)); \ - actionsForKeys[SHORTCUT] = action; - - ADD_ACTION(Qt::Key_Escape, keyEscAction()); - ADD_ACTION(Qt::Key_Delete, keyDeleteAction()); - ADD_ACTION(Qt::Key_Up, keyUpAction()); - ADD_ACTION(Qt::Key_Down, keyDownAction()); - ADD_ACTION(Qt::Key_Left, keyLeftAction()); - ADD_ACTION(Qt::Key_Right, keyRightAction()); -#undef ADD_ACTION - -#if !defined(QT_NO_DEBUG) && defined(SHOW_PLOT_INFO_TABLE) - QTableView *diveDepthTableView = new QTableView(); - diveDepthTableView->setModel(dataModel); - diveDepthTableView->show(); -#endif -} - - -ProfileWidget2::~ProfileWidget2() -{ - delete background; - delete toolTipItem; - delete profileYAxis; - delete gasYAxis; - delete temperatureAxis; - delete timeAxis; - delete diveProfileItem; - delete temperatureItem; - delete meanDepthItem; - delete cylinderPressureAxis; - delete gasPressureItem; - delete diveComputerText; - delete diveCeiling; - delete reportedCeiling; - delete pn2GasItem; - delete pheGasItem; - delete po2GasItem; - delete o2SetpointGasItem; - delete ccrsensor1GasItem; - delete ccrsensor2GasItem; - delete ccrsensor3GasItem; - delete heartBeatAxis; - delete heartBeatItem; - delete percentageAxis; - delete ambPressureItem; - delete gflineItem; - delete mouseFollowerVertical; - delete mouseFollowerHorizontal; - delete rulerItem; - delete tankItem; -} - -#define SUBSURFACE_OBJ_DATA 1 -#define SUBSURFACE_OBJ_DC_TEXT 0x42 - -void ProfileWidget2::addItemsToScene() -{ - scene()->addItem(background); - scene()->addItem(toolTipItem); - scene()->addItem(profileYAxis); - scene()->addItem(gasYAxis); - scene()->addItem(temperatureAxis); - scene()->addItem(timeAxis); - scene()->addItem(diveProfileItem); - scene()->addItem(cylinderPressureAxis); - scene()->addItem(temperatureItem); - scene()->addItem(meanDepthItem); - scene()->addItem(gasPressureItem); - // I cannot seem to figure out if an object that I find with itemAt() on the scene - // is the object I am looking for - my guess is there's a simple way in Qt to do that - // but nothing I tried worked. - // so instead this adds a special magic key/value pair to the object to mark it - diveComputerText->setData(SUBSURFACE_OBJ_DATA, SUBSURFACE_OBJ_DC_TEXT); - scene()->addItem(diveComputerText); - scene()->addItem(diveCeiling); - scene()->addItem(gradientFactor); - scene()->addItem(reportedCeiling); - scene()->addItem(pn2GasItem); - scene()->addItem(pheGasItem); - scene()->addItem(po2GasItem); - scene()->addItem(o2SetpointGasItem); - scene()->addItem(ccrsensor1GasItem); - scene()->addItem(ccrsensor2GasItem); - scene()->addItem(ccrsensor3GasItem); - scene()->addItem(percentageAxis); - scene()->addItem(heartBeatAxis); - scene()->addItem(heartBeatItem); - scene()->addItem(rulerItem); - scene()->addItem(rulerItem->sourceNode()); - scene()->addItem(rulerItem->destNode()); - scene()->addItem(tankItem); - scene()->addItem(mouseFollowerHorizontal); - scene()->addItem(mouseFollowerVertical); - QPen pen(QColor(Qt::red).lighter()); - pen.setWidth(0); - mouseFollowerHorizontal->setPen(pen); - mouseFollowerVertical->setPen(pen); - Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { - scene()->addItem(tissue); - } - Q_FOREACH (DivePercentageItem *percentage, allPercentages) { - scene()->addItem(percentage); - } - scene()->addItem(ambPressureItem); - scene()->addItem(gflineItem); -} - -void ProfileWidget2::setupItemOnScene() -{ - background->setZValue(9999); - toolTipItem->setZValue(9998); - toolTipItem->setTimeAxis(timeAxis); - rulerItem->setZValue(9997); - tankItem->setZValue(100); - - profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom); - profileYAxis->setMinimum(0); - profileYAxis->setTickInterval(M_OR_FT(10, 30)); - profileYAxis->setTickSize(0.5); - profileYAxis->setLineSize(96); - - timeAxis->setLineSize(92); - timeAxis->setTickSize(-0.5); - - gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop); - gasYAxis->setTickInterval(1); - gasYAxis->setTickSize(1); - gasYAxis->setMinimum(0); - gasYAxis->setModel(dataModel); - gasYAxis->setFontLabelScale(0.7); - gasYAxis->setLineSize(96); - - heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop); - heartBeatAxis->setTickSize(0.2); - heartBeatAxis->setTickInterval(10); - heartBeatAxis->setFontLabelScale(0.7); - heartBeatAxis->setLineSize(96); - - percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop); - percentageAxis->setTickSize(0.2); - percentageAxis->setTickInterval(10); - percentageAxis->setFontLabelScale(0.7); - percentageAxis->setLineSize(96); - - temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop); - temperatureAxis->setTickSize(2); - temperatureAxis->setTickInterval(300); - - cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop); - cylinderPressureAxis->setTickSize(2); - cylinderPressureAxis->setTickInterval(30000); - - - diveComputerText->setAlignment(Qt::AlignRight | Qt::AlignTop); - diveComputerText->setBrush(getColor(TIME_TEXT, isGrayscale)); - - rulerItem->setAxis(timeAxis, profileYAxis); - tankItem->setHorizontalAxis(timeAxis); - - // show the gradient factor at the top in the center - gradientFactor->setY(0); - gradientFactor->setX(50); - gradientFactor->setBrush(getColor(PRESSURE_TEXT)); - gradientFactor->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); - - setupItem(reportedCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); - setupItem(diveCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); - for (int i = 0; i < 16; i++) { - DiveCalculatedTissue *tissueItem = new DiveCalculatedTissue(); - setupItem(tissueItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::TISSUE_1 + i, DivePlotDataModel::TIME, 1 + i); - allTissues.append(tissueItem); - DivePercentageItem *percentageItem = new DivePercentageItem(i); - setupItem(percentageItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::PERCENTAGE_1 + i, DivePlotDataModel::TIME, 1 + i); - allPercentages.append(percentageItem); - } - setupItem(gasPressureItem, timeAxis, cylinderPressureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); - setupItem(temperatureItem, timeAxis, temperatureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); - setupItem(heartBeatItem, timeAxis, heartBeatAxis, dataModel, DivePlotDataModel::HEARTBEAT, DivePlotDataModel::TIME, 1); - setupItem(ambPressureItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::AMBPRESSURE, DivePlotDataModel::TIME, 1); - setupItem(gflineItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::GFLINE, DivePlotDataModel::TIME, 1); - setupItem(diveProfileItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::DEPTH, DivePlotDataModel::TIME, 0); - setupItem(meanDepthItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::INSTANT_MEANDEPTH, DivePlotDataModel::TIME, 1); - - -#define CREATE_PP_GAS(ITEM, VERTICAL_COLUMN, COLOR, COLOR_ALERT, THRESHOULD_SETTINGS, VISIBILITY_SETTINGS) \ - setupItem(ITEM, timeAxis, gasYAxis, dataModel, DivePlotDataModel::VERTICAL_COLUMN, DivePlotDataModel::TIME, 0); \ - ITEM->setThreshouldSettingsKey(THRESHOULD_SETTINGS); \ - ITEM->setVisibilitySettingsKey(VISIBILITY_SETTINGS); \ - ITEM->setColors(getColor(COLOR, isGrayscale), getColor(COLOR_ALERT, isGrayscale)); \ - ITEM->settingsChanged(); \ - ITEM->setZValue(99); - - CREATE_PP_GAS(pn2GasItem, PN2, PN2, PN2_ALERT, &prefs.pp_graphs.pn2_threshold, "pn2graph"); - CREATE_PP_GAS(pheGasItem, PHE, PHE, PHE_ALERT, &prefs.pp_graphs.phe_threshold, "phegraph"); - CREATE_PP_GAS(po2GasItem, PO2, PO2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); - CREATE_PP_GAS(o2SetpointGasItem, O2SETPOINT, PO2_ALERT, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); - CREATE_PP_GAS(ccrsensor1GasItem, CCRSENSOR1, CCRSENSOR1, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); - CREATE_PP_GAS(ccrsensor2GasItem, CCRSENSOR2, CCRSENSOR2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); - CREATE_PP_GAS(ccrsensor3GasItem, CCRSENSOR3, CCRSENSOR3, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); -#undef CREATE_PP_GAS - - temperatureAxis->setTextVisible(false); - temperatureAxis->setLinesVisible(false); - cylinderPressureAxis->setTextVisible(false); - cylinderPressureAxis->setLinesVisible(false); - timeAxis->setLinesVisible(true); - profileYAxis->setLinesVisible(true); - gasYAxis->setZValue(timeAxis->zValue() + 1); - heartBeatAxis->setTextVisible(true); - heartBeatAxis->setLinesVisible(true); - percentageAxis->setTextVisible(true); - percentageAxis->setLinesVisible(true); - - replotEnabled = true; -} - -void ProfileWidget2::replot(struct dive *d) -{ - if (!replotEnabled) - return; - dataModel->clear(); - plotDive(d, true); -} - -void ProfileWidget2::setupItemSizes() -{ - // Scene is *always* (double) 100 / 100. - // Background Config - /* Much probably a better math is needed here. - * good thing is that we only need to change the - * Axis and everything else is auto-adjusted.* - */ - - itemPos.background.on.setX(0); - itemPos.background.on.setY(0); - itemPos.background.off.setX(0); - itemPos.background.off.setY(110); - - //Depth Axis Config - itemPos.depth.pos.on.setX(3); - itemPos.depth.pos.on.setY(3); - itemPos.depth.pos.off.setX(-2); - itemPos.depth.pos.off.setY(3); - itemPos.depth.expanded.setP1(QPointF(0, 0)); - itemPos.depth.expanded.setP2(QPointF(0, 85)); - itemPos.depth.shrinked.setP1(QPointF(0, 0)); - itemPos.depth.shrinked.setP2(QPointF(0, 55)); - itemPos.depth.intermediate.setP1(QPointF(0, 0)); - itemPos.depth.intermediate.setP2(QPointF(0, 65)); - - // Time Axis Config - itemPos.time.pos.on.setX(3); - itemPos.time.pos.on.setY(95); - itemPos.time.pos.off.setX(3); - itemPos.time.pos.off.setY(110); - itemPos.time.expanded.setP1(QPointF(0, 0)); - itemPos.time.expanded.setP2(QPointF(94, 0)); - - // Partial Gas Axis Config - itemPos.partialPressure.pos.on.setX(97); - itemPos.partialPressure.pos.on.setY(75); - itemPos.partialPressure.pos.off.setX(110); - itemPos.partialPressure.pos.off.setY(63); - itemPos.partialPressure.expanded.setP1(QPointF(0, 0)); - itemPos.partialPressure.expanded.setP2(QPointF(0, 19)); - itemPos.partialPressureWithTankBar = itemPos.partialPressure; - itemPos.partialPressureWithTankBar.expanded.setP2(QPointF(0, 17)); - itemPos.partialPressureTissue = itemPos.partialPressure; - itemPos.partialPressureTissue.pos.on.setX(97); - itemPos.partialPressureTissue.pos.on.setY(65); - itemPos.partialPressureTissue.expanded.setP2(QPointF(0, 16)); - - // cylinder axis config - itemPos.cylinder.pos.on.setX(3); - itemPos.cylinder.pos.on.setY(20); - itemPos.cylinder.pos.off.setX(-10); - itemPos.cylinder.pos.off.setY(20); - itemPos.cylinder.expanded.setP1(QPointF(0, 15)); - itemPos.cylinder.expanded.setP2(QPointF(0, 50)); - itemPos.cylinder.shrinked.setP1(QPointF(0, 0)); - itemPos.cylinder.shrinked.setP2(QPointF(0, 20)); - itemPos.cylinder.intermediate.setP1(QPointF(0, 0)); - itemPos.cylinder.intermediate.setP2(QPointF(0, 20)); - - // Temperature axis config - itemPos.temperature.pos.on.setX(3); - itemPos.temperature.pos.on.setY(60); - itemPos.temperatureAll.pos.on.setY(51); - itemPos.temperature.pos.off.setX(-10); - itemPos.temperature.pos.off.setY(40); - itemPos.temperature.expanded.setP1(QPointF(0, 20)); - itemPos.temperature.expanded.setP2(QPointF(0, 33)); - itemPos.temperature.shrinked.setP1(QPointF(0, 2)); - itemPos.temperature.shrinked.setP2(QPointF(0, 12)); - itemPos.temperature.intermediate.setP1(QPointF(0, 2)); - itemPos.temperature.intermediate.setP2(QPointF(0, 12)); - - // Heartbeat axis config - itemPos.heartBeat.pos.on.setX(3); - itemPos.heartBeat.pos.on.setY(82); - itemPos.heartBeat.expanded.setP1(QPointF(0, 0)); - itemPos.heartBeat.expanded.setP2(QPointF(0, 10)); - itemPos.heartBeatWithTankBar = itemPos.heartBeat; - itemPos.heartBeatWithTankBar.expanded.setP2(QPointF(0, 7)); - - // Percentage axis config - itemPos.percentage.pos.on.setX(3); - itemPos.percentage.pos.on.setY(80); - itemPos.percentage.expanded.setP1(QPointF(0, 0)); - itemPos.percentage.expanded.setP2(QPointF(0, 15)); - itemPos.percentageWithTankBar = itemPos.percentage; - itemPos.percentageWithTankBar.expanded.setP2(QPointF(0, 12)); - - itemPos.dcLabel.on.setX(3); - itemPos.dcLabel.on.setY(100); - itemPos.dcLabel.off.setX(-10); - itemPos.dcLabel.off.setY(100); - - itemPos.tankBar.on.setX(0); - itemPos.tankBar.on.setY(91.5); -} - -void ProfileWidget2::setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, - DiveCartesianAxis *vAxis, DivePlotDataModel *model, - int vData, int hData, int zValue) -{ - item->setHorizontalAxis(hAxis); - item->setVerticalAxis(vAxis); - item->setModel(model); - item->setVerticalDataColumn(vData); - item->setHorizontalDataColumn(hData); - item->setZValue(zValue); -} - -void ProfileWidget2::setupSceneAndFlags() -{ - setScene(new QGraphicsScene(this)); - scene()->setSceneRect(0, 0, 100, 100); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scene()->setItemIndexMethod(QGraphicsScene::NoIndex); - setOptimizationFlags(QGraphicsView::DontSavePainterState); - setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); - setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); - setMouseTracking(true); - background->setFlag(QGraphicsItem::ItemIgnoresTransformations); -} - -void ProfileWidget2::resetZoom() -{ - if (!zoomLevel) - return; - const qreal defScale = 1.0 / qPow(zoomFactor, (qreal)zoomLevel); - scale(defScale, defScale); - zoomLevel = 0; -} - -// Currently just one dive, but the plan is to enable All of the selected dives. -void ProfileWidget2::plotDive(struct dive *d, bool force) -{ - static bool firstCall = true; - QTime measureDuration; // let's measure how long this takes us (maybe we'll turn of TTL calculation later - measureDuration.start(); - - if (currentState != ADD && currentState != PLAN) { - if (!d) { - if (selected_dive == -1) - return; - d = current_dive; // display the current dive - } - - // No need to do this again if we are already showing the same dive - // computer of the same dive, so we check the unique id of the dive - // and the selected dive computer number against the ones we are - // showing (can't compare the dive pointers as those might change). - if (d->id == displayed_dive.id && dc_number == dataModel->dcShown() && !force) - return; - - // this copies the dive and makes copies of all the relevant additional data - copy_dive(d, &displayed_dive); - gradientFactor->setText(QString("GF %1/%2").arg(prefs.gflow).arg(prefs.gfhigh)); - } else { - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - plannerModel->createTemporaryPlan(); - struct diveplan &diveplan = plannerModel->getDiveplan(); - if (!diveplan.dp) { - plannerModel->deleteTemporaryPlan(); - return; - } - gradientFactor->setText(QString("GF %1/%2").arg(diveplan.gflow).arg(diveplan.gfhigh)); - } - - // special handling for the first time we display things - int animSpeedBackup = 0; - if (firstCall && MainWindow::instance()->filesFromCommandLine()) { - animSpeedBackup = prefs.animation_speed; - prefs.animation_speed = 0; - firstCall = false; - } - - // restore default zoom level - resetZoom(); - - // reset some item visibility on printMode changes - toolTipItem->setVisible(!printMode); - rulerItem->setVisible(prefs.rulergraph && !printMode && currentState != PLAN && currentState != ADD); - - if (currentState == EMPTY) - setProfileState(); - - // next get the dive computer structure - if there are no samples - // let's create a fake profile that's somewhat reasonable for the - // data that we have - struct divecomputer *currentdc = select_dc(&displayed_dive); - Q_ASSERT(currentdc); - if (!currentdc || !currentdc->samples) { - currentdc = fake_dc(currentdc); - } - - bool setpointflag = (currentdc->divemode == CCR) && prefs.pp_graphs.po2 && current_dive; - bool sensorflag = setpointflag && prefs.show_ccr_sensors; - o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); - ccrsensor1GasItem->setVisible(sensorflag); - ccrsensor2GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 1)); - ccrsensor3GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 2)); - - /* This struct holds all the data that's about to be plotted. - * I'm not sure this is the best approach ( but since we are - * interpolating some points of the Dive, maybe it is... ) - * The Calculation of the points should be done per graph, - * so I'll *not* calculate everything if something is not being - * shown. - */ - plotInfo = calculate_max_limits_new(&displayed_dive, currentdc); - create_plot_info_new(&displayed_dive, currentdc, &plotInfo, !shouldCalculateMaxDepth); - if (shouldCalculateMaxTime) - maxtime = get_maxtime(&plotInfo); - - /* Only update the max depth if it's bigger than the current ones - * when we are dragging the handler to plan / add dive. - * otherwhise, update normally. - */ - int newMaxDepth = get_maxdepth(&plotInfo); - if (!shouldCalculateMaxDepth) { - if (maxdepth < newMaxDepth) { - maxdepth = newMaxDepth; - } - } else { - maxdepth = newMaxDepth; - } - - dataModel->setDive(&displayed_dive, plotInfo); - toolTipItem->setPlotInfo(plotInfo); - - // It seems that I'll have a lot of boilerplate setting the model / axis for - // each item, I'll mostly like to fix this in the future, but I'll keep at this for now. - profileYAxis->setMaximum(maxdepth); - profileYAxis->updateTicks(); - - temperatureAxis->setMinimum(plotInfo.mintemp); - temperatureAxis->setMaximum(plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000); - - if (plotInfo.maxhr) { - heartBeatAxis->setMinimum(plotInfo.minhr); - heartBeatAxis->setMaximum(plotInfo.maxhr); - heartBeatAxis->updateTicks(HR_AXIS); // this shows the ticks - } - heartBeatAxis->setVisible(prefs.hrgraph && plotInfo.maxhr); - - percentageAxis->setMinimum(0); - percentageAxis->setMaximum(100); - percentageAxis->setVisible(false); - percentageAxis->updateTicks(HR_AXIS); - - timeAxis->setMaximum(maxtime); - int i, incr; - static int increments[8] = { 10, 20, 30, 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60 }; - /* Time markers: at most every 10 seconds, but no more than 12 markers. - * We start out with 10 seconds and increment up to 30 minutes, - * depending on the dive time. - * This allows for 6h dives - enough (I hope) for even the craziest - * divers - but just in case, for those 8h depth-record-breaking dives, - * we double the interval if this still doesn't get us to 12 or fewer - * time markers */ - i = 0; - while (i < 7 && maxtime / increments[i] > 12) - i++; - incr = increments[i]; - while (maxtime / incr > 12) - incr *= 2; - timeAxis->setTickInterval(incr); - timeAxis->updateTicks(); - cylinderPressureAxis->setMinimum(plotInfo.minpressure); - cylinderPressureAxis->setMaximum(plotInfo.maxpressure); - - rulerItem->setPlotInfo(plotInfo); - tankItem->setData(dataModel, &plotInfo, &displayed_dive); - - dataModel->emitDataChanged(); - // The event items are a bit special since we don't know how many events are going to - // exist on a dive, so I cant create cache items for that. that's why they are here - // while all other items are up there on the constructor. - qDeleteAll(eventItems); - eventItems.clear(); - struct event *event = currentdc->events; - while (event) { - // if print mode is selected only draw headings, SP change, gas events or bookmark event - if (printMode) { - if (same_string(event->name, "") || - !(strcmp(event->name, "heading") == 0 || - (same_string(event->name, "SP change") && event->time.seconds == 0) || - event_is_gaschange(event) || - event->type == SAMPLE_EVENT_BOOKMARK)) { - event = event->next; - continue; - } - } - DiveEventItem *item = new DiveEventItem(); - item->setHorizontalAxis(timeAxis); - item->setVerticalAxis(profileYAxis); - item->setModel(dataModel); - item->setEvent(event); - item->setZValue(2); - scene()->addItem(item); - eventItems.push_back(item); - event = event->next; - } - // Only set visible the events that should be visible - Q_FOREACH (DiveEventItem *event, eventItems) { - event->setVisible(!event->shouldBeHidden()); - } - QString dcText = get_dc_nickname(currentdc->model, currentdc->deviceid); - int nr; - if ((nr = number_of_computers(&displayed_dive)) > 1) - dcText += tr(" (#%1 of %2)").arg(dc_number + 1).arg(nr); - if (dcText.isEmpty()) - dcText = tr("Unknown dive computer"); - diveComputerText->setText(dcText); - if (MainWindow::instance()->filesFromCommandLine() && animSpeedBackup != 0) { - prefs.animation_speed = animSpeedBackup; - } - - if (currentState == ADD || currentState == PLAN) { // TODO: figure a way to move this from here. - repositionDiveHandlers(); - DivePlannerPointsModel *model = DivePlannerPointsModel::instance(); - model->deleteTemporaryPlan(); - } - plotPictures(); - - // 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. - if (measureDuration.elapsed() > 1000 && prefs.calcndltts) { - MainWindow::instance()->turnOffNdlTts(); - MainWindow::instance()->getNotificationWidget()->showNotification(tr("Show NDL / TTS was disabled because of excessive processing time"), KMessageWidget::Error); - } - MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - -} - -void ProfileWidget2::recalcCeiling() -{ - diveCeiling->recalc(); -} - -void ProfileWidget2::settingsChanged() -{ - // if we are showing calculated ceilings then we have to replot() - // because the GF could have changed; otherwise we try to avoid replot() - bool needReplot = prefs.calcceiling; - if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { - profileYAxis->animateChangeLine(itemPos.depth.shrinked); - temperatureAxis->setPos(itemPos.temperatureAll.pos.on); - temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); - cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); - - if (prefs.tankbar) { - percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); - percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); - heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); - }else { - percentageAxis->setPos(itemPos.percentage.pos.on); - percentageAxis->animateChangeLine(itemPos.percentage.expanded); - heartBeatAxis->setPos(itemPos.heartBeat.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); - } - gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); - gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); - - } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { - profileYAxis->animateChangeLine(itemPos.depth.intermediate); - temperatureAxis->setPos(itemPos.temperature.pos.on); - temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); - cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); - if (prefs.tankbar) { - percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); - percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); - gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); - gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); - heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); - } else { - gasYAxis->setPos(itemPos.partialPressure.pos.on); - gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); - percentageAxis->setPos(itemPos.percentage.pos.on); - percentageAxis->setLine(itemPos.percentage.expanded); - heartBeatAxis->setPos(itemPos.heartBeat.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); - } - } else { - profileYAxis->animateChangeLine(itemPos.depth.expanded); - if (prefs.tankbar) { - temperatureAxis->setPos(itemPos.temperatureAll.pos.on); - } else { - temperatureAxis->setPos(itemPos.temperature.pos.on); - } - temperatureAxis->animateChangeLine(itemPos.temperature.expanded); - cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); - } - - tankItem->setVisible(prefs.tankbar); - if (prefs.zoomed_plot != isPlotZoomed) { - isPlotZoomed = prefs.zoomed_plot; - needReplot = true; - } - if (needReplot) - replot(); -} - -void ProfileWidget2::resizeEvent(QResizeEvent *event) -{ - QGraphicsView::resizeEvent(event); - fitInView(sceneRect(), Qt::IgnoreAspectRatio); - fixBackgroundPos(); -} - -void ProfileWidget2::mousePressEvent(QMouseEvent *event) -{ - if (zoomLevel) - return; - QGraphicsView::mousePressEvent(event); - if (currentState == PLAN) - shouldCalculateMaxTime = false; -} - -void ProfileWidget2::divePlannerHandlerClicked() -{ - if (zoomLevel) - return; - shouldCalculateMaxDepth = false; - replot(); -} - -void ProfileWidget2::divePlannerHandlerReleased() -{ - if (zoomLevel) - return; - shouldCalculateMaxDepth = true; - replot(); -} - -void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event) -{ - if (zoomLevel) - return; - QGraphicsView::mouseReleaseEvent(event); - if (currentState == PLAN) { - shouldCalculateMaxTime = true; - replot(); - } -} - -void ProfileWidget2::fixBackgroundPos() -{ - static QPixmap toBeScaled(backgroundFile); - if (currentState != EMPTY) - return; - QPixmap p = toBeScaled.scaledToHeight(viewport()->height() - 40, Qt::SmoothTransformation); - int x = viewport()->width() / 2 - p.width() / 2; - int y = viewport()->height() / 2 - p.height() / 2; - background->setPixmap(p); - background->setX(mapToScene(x, 0).x()); - background->setY(mapToScene(y, 20).y()); -} - -void ProfileWidget2::wheelEvent(QWheelEvent *event) -{ - if (currentState == EMPTY) - return; - QPoint toolTipPos = mapFromScene(toolTipItem->pos()); - if (event->buttons() == Qt::LeftButton) - return; - if (event->delta() > 0 && zoomLevel < 20) { - scale(zoomFactor, zoomFactor); - zoomLevel++; - } else if (event->delta() < 0 && zoomLevel > 0) { - // Zooming out - scale(1.0 / zoomFactor, 1.0 / zoomFactor); - zoomLevel--; - } - scrollViewTo(event->pos()); - toolTipItem->setPos(mapToScene(toolTipPos)); -} - -void ProfileWidget2::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (currentState == PLAN || currentState == ADD) { - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - QPointF mappedPos = mapToScene(event->pos()); - if (isPointOutOfBoundaries(mappedPos)) - return; - - int minutes = rint(timeAxis->valueAt(mappedPos) / 60); - int milimeters = rint(profileYAxis->valueAt(mappedPos) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); - plannerModel->addStop(milimeters, minutes * 60, 0, 0, true); - } -} - -bool ProfileWidget2::isPointOutOfBoundaries(const QPointF &point) const -{ - double xpos = timeAxis->valueAt(point); - double ypos = profileYAxis->valueAt(point); - return (xpos > timeAxis->maximum() || - xpos < timeAxis->minimum() || - ypos > profileYAxis->maximum() || - ypos < profileYAxis->minimum()); -} - -void ProfileWidget2::scrollViewTo(const QPoint &pos) -{ - /* since we cannot use translate() directly on the scene we hack on - * the scroll bars (hidden) functionality */ - if (!zoomLevel || currentState == EMPTY) - return; - QScrollBar *vs = verticalScrollBar(); - QScrollBar *hs = horizontalScrollBar(); - const qreal yRat = (qreal)pos.y() / viewport()->height(); - const qreal xRat = (qreal)pos.x() / viewport()->width(); - vs->setValue(yRat * vs->maximum()); - hs->setValue(xRat * hs->maximum()); -} - -void ProfileWidget2::mouseMoveEvent(QMouseEvent *event) -{ - QPointF pos = mapToScene(event->pos()); - toolTipItem->refresh(pos); - if (zoomLevel == 0) { - QGraphicsView::mouseMoveEvent(event); - } else { - QPoint toolTipPos = mapFromScene(toolTipItem->pos()); - scrollViewTo(event->pos()); - toolTipItem->setPos(mapToScene(toolTipPos)); - } - - qreal vValue = profileYAxis->valueAt(pos); - qreal hValue = timeAxis->valueAt(pos); - if (profileYAxis->maximum() >= vValue && profileYAxis->minimum() <= vValue) { - mouseFollowerHorizontal->setPos(timeAxis->pos().x(), pos.y()); - } - if (timeAxis->maximum() >= hValue && timeAxis->minimum() <= hValue) { - mouseFollowerVertical->setPos(pos.x(), profileYAxis->line().y1()); - } -} - -bool ProfileWidget2::eventFilter(QObject *object, QEvent *event) -{ - QGraphicsScene *s = qobject_cast(object); - if (s && event->type() == QEvent::GraphicsSceneHelp) { - event->ignore(); - return true; - } - return QGraphicsView::eventFilter(object, event); -} - -void ProfileWidget2::setEmptyState() -{ - // Then starting Empty State, move the background up. - if (currentState == EMPTY) - return; - - disconnectTemporaryConnections(); - setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); - dataModel->clear(); - currentState = EMPTY; - MainWindow::instance()->setEnabledToolbar(false); - - fixBackgroundPos(); - background->setVisible(true); - - profileYAxis->setVisible(false); - gasYAxis->setVisible(false); - timeAxis->setVisible(false); - temperatureAxis->setVisible(false); - cylinderPressureAxis->setVisible(false); - toolTipItem->setVisible(false); - diveComputerText->setVisible(false); - diveCeiling->setVisible(false); - gradientFactor->setVisible(false); - reportedCeiling->setVisible(false); - rulerItem->setVisible(false); - tankItem->setVisible(false); - pn2GasItem->setVisible(false); - po2GasItem->setVisible(false); - o2SetpointGasItem->setVisible(false); - ccrsensor1GasItem->setVisible(false); - ccrsensor2GasItem->setVisible(false); - ccrsensor3GasItem->setVisible(false); - pheGasItem->setVisible(false); - ambPressureItem->setVisible(false); - gflineItem->setVisible(false); - mouseFollowerHorizontal->setVisible(false); - mouseFollowerVertical->setVisible(false); - -#define HIDE_ALL(TYPE, CONTAINER) \ - Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); - HIDE_ALL(DiveCalculatedTissue, allTissues); - HIDE_ALL(DivePercentageItem, allPercentages); - HIDE_ALL(DiveEventItem, eventItems); - HIDE_ALL(DiveHandler, handles); - HIDE_ALL(QGraphicsSimpleTextItem, gases); -#undef HIDE_ALL -} - -void ProfileWidget2::setProfileState() -{ - // Then starting Empty State, move the background up. - if (currentState == PROFILE) - return; - - disconnectTemporaryConnections(); - connect(DivePictureModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(plotPictures())); - connect(DivePictureModel::instance(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(plotPictures())); - connect(DivePictureModel::instance(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(plotPictures())); - /* show the same stuff that the profile shows. */ - - //TODO: Move the DC handling to another method. - MainWindow::instance()->enableShortcuts(); - - currentState = PROFILE; - MainWindow::instance()->setEnabledToolbar(true); - toolTipItem->readPos(); - setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); - - background->setVisible(false); - toolTipItem->setVisible(true); - profileYAxis->setVisible(true); - gasYAxis->setVisible(true); - timeAxis->setVisible(true); - temperatureAxis->setVisible(true); - cylinderPressureAxis->setVisible(true); - - profileYAxis->setPos(itemPos.depth.pos.on); - if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { - profileYAxis->animateChangeLine(itemPos.depth.shrinked); - temperatureAxis->setPos(itemPos.temperatureAll.pos.on); - temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); - cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); - - if (prefs.tankbar) { - percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); - percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); - heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); - }else { - percentageAxis->setPos(itemPos.percentage.pos.on); - percentageAxis->animateChangeLine(itemPos.percentage.expanded); - heartBeatAxis->setPos(itemPos.heartBeat.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); - } - gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); - gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); - - } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { - profileYAxis->animateChangeLine(itemPos.depth.intermediate); - temperatureAxis->setPos(itemPos.temperature.pos.on); - temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); - cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); - if (prefs.tankbar) { - percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); - percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); - gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); - gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); - heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); - } else { - gasYAxis->setPos(itemPos.partialPressure.pos.on); - gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); - percentageAxis->setPos(itemPos.percentage.pos.on); - percentageAxis->setLine(itemPos.percentage.expanded); - heartBeatAxis->setPos(itemPos.heartBeat.pos.on); - heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); - } - } else { - profileYAxis->animateChangeLine(itemPos.depth.expanded); - if (prefs.tankbar) { - temperatureAxis->setPos(itemPos.temperatureAll.pos.on); - } else { - temperatureAxis->setPos(itemPos.temperature.pos.on); - } - temperatureAxis->animateChangeLine(itemPos.temperature.expanded); - cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); - } - pn2GasItem->setVisible(prefs.pp_graphs.pn2); - po2GasItem->setVisible(prefs.pp_graphs.po2); - pheGasItem->setVisible(prefs.pp_graphs.phe); - - bool setpointflag = current_dive && (current_dc->divemode == CCR) && prefs.pp_graphs.po2; - bool sensorflag = setpointflag && prefs.show_ccr_sensors; - o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); - ccrsensor1GasItem->setVisible(sensorflag); - ccrsensor2GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 1)); - ccrsensor3GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 2)); - - timeAxis->setPos(itemPos.time.pos.on); - timeAxis->setLine(itemPos.time.expanded); - - cylinderPressureAxis->setPos(itemPos.cylinder.pos.on); - heartBeatItem->setVisible(prefs.hrgraph); - meanDepthItem->setVisible(prefs.show_average_depth); - - diveComputerText->setVisible(true); - diveComputerText->setPos(itemPos.dcLabel.on); - - diveCeiling->setVisible(prefs.calcceiling); - gradientFactor->setVisible(prefs.calcceiling); - reportedCeiling->setVisible(prefs.dcceiling); - - if (prefs.calcalltissues) { - Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { - tissue->setVisible(true); - } - } - - if (prefs.percentagegraph) { - Q_FOREACH (DivePercentageItem *percentage, allPercentages) { - percentage->setVisible(true); - } - - ambPressureItem->setVisible(true); - gflineItem->setVisible(true); - } - - rulerItem->setVisible(prefs.rulergraph); - tankItem->setVisible(prefs.tankbar); - tankItem->setPos(itemPos.tankBar.on); - -#define HIDE_ALL(TYPE, CONTAINER) \ - Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); - HIDE_ALL(DiveHandler, handles); - HIDE_ALL(QGraphicsSimpleTextItem, gases); -#undef HIDE_ALL - mouseFollowerHorizontal->setVisible(false); - mouseFollowerVertical->setVisible(false); -} - -void ProfileWidget2::clearHandlers() -{ - if (handles.count()) { - foreach (DiveHandler *handle, handles) { - scene()->removeItem(handle); - delete handle; - } - handles.clear(); - } -} - -void ProfileWidget2::setToolTipVisibile(bool visible) -{ - toolTipItem->setVisible(visible); -} - -void ProfileWidget2::setAddState() -{ - if (currentState == ADD) - return; - - clearHandlers(); - setProfileState(); - mouseFollowerHorizontal->setVisible(true); - mouseFollowerVertical->setVisible(true); - mouseFollowerHorizontal->setLine(timeAxis->line()); - mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); - disconnectTemporaryConnections(); - //TODO: Move this method to another place, shouldn't be on mainwindow. - MainWindow::instance()->disableShortcuts(false); - actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); - actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); - actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); - actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); - actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); - actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); - connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); - connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), - this, SLOT(pointInserted(const QModelIndex &, int, int))); - connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), - this, SLOT(pointsRemoved(const QModelIndex &, int, int))); - /* show the same stuff that the profile shows. */ - currentState = ADD; /* enable the add state. */ - diveCeiling->setVisible(true); - gradientFactor->setVisible(true); - setBackgroundBrush(QColor("#A7DCFF")); -} - -void ProfileWidget2::setPlanState() -{ - if (currentState == PLAN) - return; - - setProfileState(); - mouseFollowerHorizontal->setVisible(true); - mouseFollowerVertical->setVisible(true); - mouseFollowerHorizontal->setLine(timeAxis->line()); - mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); - disconnectTemporaryConnections(); - //TODO: Move this method to another place, shouldn't be on mainwindow. - MainWindow::instance()->disableShortcuts(); - actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); - actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); - actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); - actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); - actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); - actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); - connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); - connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), - this, SLOT(pointInserted(const QModelIndex &, int, int))); - connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), - this, SLOT(pointsRemoved(const QModelIndex &, int, int))); - /* show the same stuff that the profile shows. */ - currentState = PLAN; /* enable the add state. */ - diveCeiling->setVisible(true); - gradientFactor->setVisible(true); - setBackgroundBrush(QColor("#D7E3EF")); -} - -extern struct ev_select *ev_namelist; -extern int evn_allocated; -extern int evn_used; - -bool ProfileWidget2::isPlanner() -{ - return currentState == PLAN; -} - -bool ProfileWidget2::isAddOrPlanner() -{ - return currentState == PLAN || currentState == ADD; -} - -struct plot_data *ProfileWidget2::getEntryFromPos(QPointF pos) -{ - // find the time stamp corresponding to the mouse position - int seconds = timeAxis->valueAt(pos); - struct plot_data *entry = NULL; - - for (int i = 0; i < plotInfo.nr; i++) { - entry = plotInfo.entry + i; - if (entry->sec >= seconds) - break; - } - return entry; -} - -void ProfileWidget2::setReplot(bool state) -{ - replotEnabled = state; -} - -void ProfileWidget2::contextMenuEvent(QContextMenuEvent *event) -{ - if (currentState == ADD || currentState == PLAN) { - QGraphicsView::contextMenuEvent(event); - return; - } - QMenu m; - bool isDCName = false; - if (selected_dive == -1) - return; - // figure out if we are ontop of the dive computer name in the profile - QGraphicsItem *sceneItem = itemAt(mapFromGlobal(event->globalPos())); - if (sceneItem) { - QGraphicsItem *parentItem = sceneItem; - while (parentItem) { - if (parentItem->data(SUBSURFACE_OBJ_DATA) == SUBSURFACE_OBJ_DC_TEXT) { - isDCName = true; - break; - } - parentItem = parentItem->parentItem(); - } - if (isDCName) { - if (dc_number == 0 && count_divecomputers() == 1) - // nothing to do, can't delete or reorder - return; - // create menu to show when right clicking on dive computer name - if (dc_number > 0) - m.addAction(tr("Make first divecomputer"), this, SLOT(makeFirstDC())); - if (count_divecomputers() > 1) - m.addAction(tr("Delete this divecomputer"), this, SLOT(deleteCurrentDC())); - m.exec(event->globalPos()); - // don't show the regular profile context menu - return; - } - } - // create the profile context menu - QPointF scenePos = mapToScene(event->pos()); - struct plot_data *entry = getEntryFromPos(scenePos); - GasSelectionModel *model = GasSelectionModel::instance(); - model->repopulate(); - int rowCount = model->rowCount(); - if (rowCount > 1) { - // if we have more than one gas, offer to switch to another one - QMenu *gasChange = m.addMenu(tr("Add gas change")); - for (int i = 0; i < rowCount; i++) { - QAction *action = new QAction(&m); - action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString() + QString(tr(" (Tank %1)")).arg(i + 1)); - connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); - action->setData(event->globalPos()); - if (i == entry->cylinderindex) - action->setDisabled(true); - gasChange->addAction(action); - } - } - QAction *setpointAction = m.addAction(tr("Add set-point change"), this, SLOT(addSetpointChange())); - setpointAction->setData(event->globalPos()); - QAction *action = m.addAction(tr("Add bookmark"), this, SLOT(addBookmark())); - action->setData(event->globalPos()); - - if (same_string(current_dc->model, "manually added dive")) - QAction *editProfileAction = m.addAction(tr("Edit the profile"), MainWindow::instance(), SLOT(editCurrentDive())); - - if (DiveEventItem *item = dynamic_cast(sceneItem)) { - action = new QAction(&m); - action->setText(tr("Remove event")); - action->setData(QVariant::fromValue(item)); // so we know what to remove. - connect(action, SIGNAL(triggered(bool)), this, SLOT(removeEvent())); - m.addAction(action); - action = new QAction(&m); - action->setText(tr("Hide similar events")); - action->setData(QVariant::fromValue(item)); - connect(action, SIGNAL(triggered(bool)), this, SLOT(hideEvents())); - m.addAction(action); - struct event *dcEvent = item->getEvent(); - if (dcEvent->type == SAMPLE_EVENT_BOOKMARK) { - action = new QAction(&m); - action->setText(tr("Edit name")); - action->setData(QVariant::fromValue(item)); - connect(action, SIGNAL(triggered(bool)), this, SLOT(editName())); - m.addAction(action); - } -#if 0 // FIXME::: FINISH OR DISABLE - // 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 (dcEvent->type == SAMPLE_EVENT_GASCHANGE || dcEvent->type == SAMPLE_EVENT_GASCHANGE2) { - qDebug() << "figure out if there are interpolated pressures"; - struct plot_data *gasChangeEntry = entry; - struct plot_data *newGasEntry; - while (gasChangeEntry > plotInfo.entry) { - --gasChangeEntry; - if (gasChangeEntry->sec <= dcEvent->time.seconds) - break; - } - qDebug() << "at gas change at" << gasChangeEntry->sec << ": sensor pressure" << gasChangeEntry->pressure[0] << "interpolated" << gasChangeEntry->pressure[1]; - // 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 (gasChangeEntry + 1 <= plotInfo.entry + plotInfo.nr) { - newGasEntry = gasChangeEntry + 1; - qDebug() << "after gas change at " << newGasEntry->sec << ": sensor pressure" << newGasEntry->pressure[0] << "interpolated" << newGasEntry->pressure[1]; - if (SENSOR_PRESSURE(gasChangeEntry) == 0 || displayed_dive.cylinder[gasChangeEntry->cylinderindex].sample_start.mbar == 0) { - // 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 = INTERPOLATED_PRESSURE(gasChangeEntry) ? : SENSOR_PRESSURE(gasChangeEntry); - QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") - .arg(gasChangeEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); - } - if (SENSOR_PRESSURE(newGasEntry) == 0 || displayed_dive.cylinder[newGasEntry->cylinderindex].sample_start.mbar == 0) { - // we only have interpolated press -- see commend above - pressure_t pressure; - pressure.mbar = INTERPOLATED_PRESSURE(newGasEntry) ? : SENSOR_PRESSURE(newGasEntry); - QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") - .arg(newGasEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); - } - } - } -#endif - } - bool some_hidden = false; - for (int i = 0; i < evn_used; i++) { - if (ev_namelist[i].plot_ev == false) { - some_hidden = true; - break; - } - } - if (some_hidden) { - action = m.addAction(tr("Unhide all events"), this, SLOT(unhideEvents())); - action->setData(event->globalPos()); - } - m.exec(event->globalPos()); -} - -void ProfileWidget2::deleteCurrentDC() -{ - delete_current_divecomputer(); - mark_divelist_changed(true); - // we need to force it since it's likely the same dive and same dc_number - but that's a different dive computer now - MainWindow::instance()->graphics()->plotDive(0, true); - MainWindow::instance()->refreshDisplay(); -} - -void ProfileWidget2::makeFirstDC() -{ - make_first_dc(); - mark_divelist_changed(true); - // this is now the first DC, so we need to redraw the profile and refresh the dive list - // (and no, it's not just enough to rewrite the text - the first DC is special so values in the - // dive list may change). - // As a side benefit, this returns focus to the dive list. - dc_number = 0; - MainWindow::instance()->refreshDisplay(); -} - -void ProfileWidget2::hideEvents() -{ - QAction *action = qobject_cast(sender()); - DiveEventItem *item = static_cast(action->data().value()); - struct event *event = item->getEvent(); - - if (QMessageBox::question(MainWindow::instance(), - TITLE_OR_TEXT(tr("Hide events"), tr("Hide all %1 events?").arg(event->name)), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { - if (!same_string(event->name, "")) { - for (int i = 0; i < evn_used; i++) { - if (same_string(event->name, ev_namelist[i].ev_name)) { - ev_namelist[i].plot_ev = false; - break; - } - } - Q_FOREACH (DiveEventItem *evItem, eventItems) { - if (same_string(evItem->getEvent()->name, event->name)) - evItem->hide(); - } - } else { - item->hide(); - } - } -} - -void ProfileWidget2::unhideEvents() -{ - for (int i = 0; i < evn_used; i++) { - ev_namelist[i].plot_ev = true; - } - Q_FOREACH (DiveEventItem *item, eventItems) - item->show(); -} - -void ProfileWidget2::removeEvent() -{ - QAction *action = qobject_cast(sender()); - DiveEventItem *item = static_cast(action->data().value()); - struct event *event = item->getEvent(); - - if (QMessageBox::question(MainWindow::instance(), TITLE_OR_TEXT( - tr("Remove the selected event?"), - tr("%1 @ %2:%3").arg(event->name).arg(event->time.seconds / 60).arg(event->time.seconds % 60, 2, 10, QChar('0'))), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { - remove_event(event); - mark_divelist_changed(true); - replot(); - } -} - -void ProfileWidget2::addBookmark() -{ - QAction *action = qobject_cast(sender()); - QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); - add_event(current_dc, timeAxis->valueAt(scenePos), SAMPLE_EVENT_BOOKMARK, 0, 0, "bookmark"); - mark_divelist_changed(true); - replot(); -} - -void ProfileWidget2::addSetpointChange() -{ - QAction *action = qobject_cast(sender()); - QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); - SetpointDialog::instance()->setpointData(current_dc, timeAxis->valueAt(scenePos)); - SetpointDialog::instance()->show(); -} - -void ProfileWidget2::changeGas() -{ - QAction *action = qobject_cast(sender()); - QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); - QString gas = action->text(); - gas.remove(QRegExp(" \\(.*\\)")); - - // backup the things on the dataModel, since we will clear that out. - struct gasmix gasmix; - qreal sec_val = timeAxis->valueAt(scenePos); - - // no gas changes before the dive starts - unsigned int seconds = (sec_val < 0.0) ? 0 : (unsigned int)sec_val; - - // if there is a gas change at this time stamp, remove it before adding the new one - struct event *gasChangeEvent = current_dc->events; - while ((gasChangeEvent = get_next_event(gasChangeEvent, "gaschange")) != NULL) { - if (gasChangeEvent->time.seconds == seconds) { - remove_event(gasChangeEvent); - gasChangeEvent = current_dc->events; - } else { - gasChangeEvent = gasChangeEvent->next; - } - } - validate_gas(gas.toUtf8().constData(), &gasmix); - QRegExp rx("\\(\\D*(\\d+)"); - int tank; - if (rx.indexIn(action->text()) > -1) { - tank = rx.cap(1).toInt() - 1; // we display the tank 1 based - } else { - qDebug() << "failed to parse tank number"; - tank = get_gasidx(&displayed_dive, &gasmix); - } - // add this both to the displayed dive and the current dive - add_gas_switch_event(current_dive, current_dc, seconds, tank); - add_gas_switch_event(&displayed_dive, get_dive_dc(&displayed_dive, dc_number), seconds, tank); - // this means we potentially have a new tank that is being used and needs to be shown - fixup_dive(&displayed_dive); - - // FIXME - this no longer gets written to the dive list - so we need to enableEdition() here - - MainWindow::instance()->information()->updateDiveInfo(); - mark_divelist_changed(true); - replot(); -} - -bool ProfileWidget2::getPrintMode() -{ - return printMode; -} - -void ProfileWidget2::setPrintMode(bool mode, bool grayscale) -{ - printMode = mode; - resetZoom(); - - // set printMode for axes - profileYAxis->setPrintMode(mode); - gasYAxis->setPrintMode(mode); - temperatureAxis->setPrintMode(mode); - timeAxis->setPrintMode(mode); - cylinderPressureAxis->setPrintMode(mode); - heartBeatAxis->setPrintMode(mode); - percentageAxis->setPrintMode(mode); - - isGrayscale = mode ? grayscale : false; - mouseFollowerHorizontal->setVisible(!mode); - mouseFollowerVertical->setVisible(!mode); -} - -void ProfileWidget2::setFontPrintScale(double scale) -{ - fontPrintScale = scale; - emit fontPrintScaleChanged(scale); -} - -double ProfileWidget2::getFontPrintScale() -{ - if (printMode) - return fontPrintScale; - else - return 1.0; -} - -void ProfileWidget2::editName() -{ - QAction *action = qobject_cast(sender()); - DiveEventItem *item = static_cast(action->data().value()); - struct event *event = item->getEvent(); - bool ok; - QString newName = QInputDialog::getText(MainWindow::instance(), tr("Edit name of bookmark"), - tr("Custom name:"), QLineEdit::Normal, - event->name, &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; - } - // order is important! first update the current dive (by matching the unchanged event), - // then update the displayed dive (as event is part of the events on displayed dive - // and will be freed as part of changing the name! - update_event_name(current_dive, event, newName.toUtf8().data()); - update_event_name(&displayed_dive, event, newName.toUtf8().data()); - mark_divelist_changed(true); - replot(); - } -} - -void ProfileWidget2::disconnectTemporaryConnections() -{ - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - disconnect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); - disconnect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); - - disconnect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), - this, SLOT(pointInserted(const QModelIndex &, int, int))); - disconnect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), - this, SLOT(pointsRemoved(const QModelIndex &, int, int))); - - Q_FOREACH (QAction *action, actionsForKeys.values()) { - action->setShortcut(QKeySequence()); - action->setShortcutContext(Qt::WidgetShortcut); - } -} - -void ProfileWidget2::pointInserted(const QModelIndex &parent, int start, int end) -{ - DiveHandler *item = new DiveHandler(); - scene()->addItem(item); - handles << item; - - connect(item, SIGNAL(moved()), this, SLOT(recreatePlannedDive())); - connect(item, SIGNAL(clicked()), this, SLOT(divePlannerHandlerClicked())); - connect(item, SIGNAL(released()), this, SLOT(divePlannerHandlerReleased())); - QGraphicsSimpleTextItem *gasChooseBtn = new QGraphicsSimpleTextItem(); - scene()->addItem(gasChooseBtn); - gasChooseBtn->setZValue(10); - gasChooseBtn->setFlag(QGraphicsItem::ItemIgnoresTransformations); - gases << gasChooseBtn; - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - if (plannerModel->recalcQ()) - replot(); -} - -void ProfileWidget2::pointsRemoved(const QModelIndex &, int start, int end) -{ // start and end are inclusive. - int num = (end - start) + 1; - for (int i = num; i != 0; i--) { - delete handles.back(); - handles.pop_back(); - delete gases.back(); - gases.pop_back(); - } - scene()->clearSelection(); - replot(); -} - -void ProfileWidget2::repositionDiveHandlers() -{ - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - // Re-position the user generated dive handlers - struct gasmix mix, lastmix; - 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.at(i); - h->setVisible(datapoint.entered); - h->setPos(timeAxis->posAtValue(datapoint.time), profileYAxis->posAtValue(datapoint.depth)); - 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(timeAxis->posAtValue(datapoint.depth / prefs.descrate), - profileYAxis->posAtValue(datapoint.depth)); - else - // place the text on the straight line from the origin to the first position - p1 = QPointF(timeAxis->posAtValue(0), 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); - gases[i]->setText(get_divepoint_gas_string(datapoint)); - gases[i]->setVisible(datapoint.entered && - (i == 0 || gases[i]->text() != gases[i-1]->text())); - } -} - -int ProfileWidget2::fixHandlerIndex(DiveHandler *activeHandler) -{ - int index = handles.indexOf(activeHandler); - if (index > 0 && index < handles.count() - 1) { - DiveHandler *before = handles[index - 1]; - if (before->pos().x() > activeHandler->pos().x()) { - handles.swap(index, index - 1); - return index - 1; - } - DiveHandler *after = handles[index + 1]; - if (after->pos().x() < activeHandler->pos().x()) { - handles.swap(index, index + 1); - return index + 1; - } - } - return index; -} - -void ProfileWidget2::recreatePlannedDive() -{ - DiveHandler *activeHandler = qobject_cast(sender()); - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - int index = fixHandlerIndex(activeHandler); - int mintime = 0, maxtime = (timeAxis->maximum() + 10) * 60; - if (index > 0) - mintime = plannerModel->at(index - 1).time; - if (index < plannerModel->size() - 1) - maxtime = plannerModel->at(index + 1).time; - - int minutes = rint(timeAxis->valueAt(activeHandler->pos()) / 60); - if (minutes * 60 <= mintime || minutes * 60 >= maxtime) - return; - - divedatapoint data = plannerModel->at(index); - data.depth = rint(profileYAxis->valueAt(activeHandler->pos()) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); - data.time = rint(timeAxis->valueAt(activeHandler->pos())); - - plannerModel->editStop(index, data); -} - -void ProfileWidget2::keyDownAction() -{ - if (currentState != ADD && currentState != PLAN) - return; - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handles.indexOf(handler); - divedatapoint dp = plannerModel->at(row); - if (dp.depth >= profileYAxis->maximum()) - continue; - - dp.depth += M_OR_FT(1, 5); - plannerModel->editStop(row, dp); - } - } -} - -void ProfileWidget2::keyUpAction() -{ - if (currentState != ADD && currentState != PLAN) - return; - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handles.indexOf(handler); - divedatapoint dp = plannerModel->at(row); - - if (dp.depth <= 0) - continue; - - dp.depth -= M_OR_FT(1, 5); - plannerModel->editStop(row, dp); - } - } -} - -void ProfileWidget2::keyLeftAction() -{ - if (currentState != ADD && currentState != PLAN) - return; - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handles.indexOf(handler); - divedatapoint dp = plannerModel->at(row); - - if (dp.time / 60 <= 0) - continue; - - // don't overlap positions. - // maybe this is a good place for a 'goto'? - double xpos = timeAxis->posAtValue((dp.time - 60) / 60); - bool nextStep = false; - Q_FOREACH (DiveHandler *h, handles) { - if (IS_FP_SAME(h->pos().x(), xpos)) { - nextStep = true; - break; - } - } - if (nextStep) - continue; - - dp.time -= 60; - plannerModel->editStop(row, dp); - } - } -} - -void ProfileWidget2::keyRightAction() -{ - if (currentState != ADD && currentState != PLAN) - return; - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - int row = handles.indexOf(handler); - divedatapoint dp = plannerModel->at(row); - if (dp.time / 60.0 >= timeAxis->maximum()) - continue; - - // don't overlap positions. - // maybe this is a good place for a 'goto'? - double xpos = timeAxis->posAtValue((dp.time + 60) / 60); - bool nextStep = false; - Q_FOREACH (DiveHandler *h, handles) { - if (IS_FP_SAME(h->pos().x(), xpos)) { - nextStep = true; - break; - } - } - if (nextStep) - continue; - - dp.time += 60; - plannerModel->editStop(row, dp); - } - } -} - -void ProfileWidget2::keyDeleteAction() -{ - if (currentState != ADD && currentState != PLAN) - return; - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - int selCount = scene()->selectedItems().count(); - if (selCount) { - QVector selectedIndexes; - Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { - if (DiveHandler *handler = qgraphicsitem_cast(i)) { - selectedIndexes.push_back(handles.indexOf(handler)); - handler->hide(); - } - } - plannerModel->removeSelectedPoints(selectedIndexes); - } -} - -void ProfileWidget2::keyEscAction() -{ - if (currentState != ADD && currentState != PLAN) - return; - - if (scene()->selectedItems().count()) { - scene()->clearSelection(); - return; - } - - DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); - if (plannerModel->isPlanner()) - plannerModel->cancelPlan(); -} - -void ProfileWidget2::plotPictures() -{ - Q_FOREACH (DivePictureItem *item, pictures) { - item->hide(); - item->deleteLater(); - } - pictures.clear(); - - if (printMode) - return; - - double x, y, lastX = -1.0, lastY = -1.0; - DivePictureModel *m = DivePictureModel::instance(); - for (int i = 0; i < m->rowCount(); i++) { - int offsetSeconds = m->index(i, 1).data(Qt::UserRole).value(); - // it's a correct picture, but doesn't have a timestamp: only show on the widget near the - // information area. - if (!offsetSeconds) - continue; - DivePictureItem *item = new DivePictureItem(); - item->setPixmap(m->index(i, 0).data(Qt::DecorationRole).value()); - item->setFileUrl(m->index(i, 1).data().toString()); - // 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. - x = timeAxis->posAtValue(offsetSeconds); - if (i == 0) - y = 10; - else if (fabs(x - lastX) < 4) - y = lastY + 3; - else - y = 10; - lastX = x; - lastY = y; - item->setPos(x, y); - scene()->addItem(item); - pictures.push_back(item); - } -} diff --git a/qt-ui/profile/profilewidget2.h b/qt-ui/profile/profilewidget2.h deleted file mode 100644 index 2d1a7bfb4..000000000 --- a/qt-ui/profile/profilewidget2.h +++ /dev/null @@ -1,212 +0,0 @@ -#ifndef PROFILEWIDGET2_H -#define PROFILEWIDGET2_H - -#include - -// /* The idea of this widget is to display and edit the profile. -// * It has: -// * 1 - ToolTip / Legend item, displays every information of the current mouse position on it, plus the legends of the maps. -// * 2 - ToolBox, displays the QActions that are used to do special stuff on the profile ( like activating the plugins. ) -// * 3 - Cartesian Axis for depth ( y ) -// * 4 - Cartesian Axis for Gases ( y ) -// * 5 - Cartesian Axis for Time ( x ) -// * -// * It needs to be dynamic, things should *flow* on it, not just appear / disappear. -// */ -#include "graphicsview-common.h" -#include "divelineitem.h" -#include "diveprofileitem.h" -#include "display.h" - -class RulerItem2; -struct dive; -struct plot_info; -class ToolTipItem; -class DiveMeanDepth; -class DiveReportedCeiling; -class DiveTextItem; -class TemperatureAxis; -class DiveEventItem; -class DivePlotDataModel; -class DivePixmapItem; -class DiveRectItem; -class DepthAxis; -class DiveCartesianAxis; -class DiveProfileItem; -class TimeAxis; -class DiveTemperatureItem; -class DiveHeartrateItem; -class PercentageItem; -class DiveGasPressureItem; -class DiveCalculatedCeiling; -class DiveCalculatedTissue; -class PartialPressureGasItem; -class PartialGasPressureAxis; -class AbstractProfilePolygonItem; -class TankItem; -class DiveHandler; -class QGraphicsSimpleTextItem; -class QModelIndex; -class DivePictureItem; - -class ProfileWidget2 : public QGraphicsView { - Q_OBJECT -public: - enum State { - EMPTY, - PROFILE, - EDIT, - ADD, - PLAN, - INVALID - }; - enum Items { - BACKGROUND, - PROFILE_Y_AXIS, - GAS_Y_AXIS, - TIME_AXIS, - DEPTH_CONTROLLER, - TIME_CONTROLLER, - COLUMNS - }; - - ProfileWidget2(QWidget *parent = 0); - void resetZoom(); - void plotDive(struct dive *d = 0, bool force = false); - virtual bool eventFilter(QObject *, QEvent *); - void setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis, DivePlotDataModel *model, int vData, int hData, int zValue); - void setPrintMode(bool mode, bool grayscale = false); - bool getPrintMode(); - bool isPointOutOfBoundaries(const QPointF &point) const; - bool isPlanner(); - bool isAddOrPlanner(); - double getFontPrintScale(); - void setFontPrintScale(double scale); - void clearHandlers(); - void recalcCeiling(); - void setToolTipVisibile(bool visible); - State currentState; - -signals: - void fontPrintScaleChanged(double scale); - -public -slots: // Necessary to call from QAction's signals. - void settingsChanged(); - void setEmptyState(); - void setProfileState(); - void setPlanState(); - void setAddState(); - void changeGas(); - void addSetpointChange(); - void addBookmark(); - void hideEvents(); - void unhideEvents(); - void removeEvent(); - void editName(); - void makeFirstDC(); - void deleteCurrentDC(); - void pointInserted(const QModelIndex &parent, int start, int end); - void pointsRemoved(const QModelIndex &, int start, int end); - void plotPictures(); - void setReplot(bool state); - void replot(dive *d = 0); - - /* this is called for every move on the handlers. maybe we can speed up this a bit? */ - void recreatePlannedDive(); - - /* key press handlers */ - void keyEscAction(); - void keyDeleteAction(); - void keyUpAction(); - void keyDownAction(); - void keyLeftAction(); - void keyRightAction(); - - void divePlannerHandlerClicked(); - void divePlannerHandlerReleased(); - -protected: - virtual ~ProfileWidget2(); - virtual void resizeEvent(QResizeEvent *event); - virtual void wheelEvent(QWheelEvent *event); - virtual void mouseMoveEvent(QMouseEvent *event); - virtual void contextMenuEvent(QContextMenuEvent *event); - virtual void mouseDoubleClickEvent(QMouseEvent *event); - virtual void mousePressEvent(QMouseEvent *event); - virtual void mouseReleaseEvent(QMouseEvent *event); - -private: /*methods*/ - void fixBackgroundPos(); - void scrollViewTo(const QPoint &pos); - void setupSceneAndFlags(); - void setupItemSizes(); - void addItemsToScene(); - void setupItemOnScene(); - void disconnectTemporaryConnections(); - struct plot_data *getEntryFromPos(QPointF pos); - -private: - DivePlotDataModel *dataModel; - int zoomLevel; - qreal zoomFactor; - DivePixmapItem *background; - QString backgroundFile; - ToolTipItem *toolTipItem; - bool isPlotZoomed; - bool replotEnabled; - // All those here should probably be merged into one structure, - // So it's esyer to replicate for more dives later. - // In the meantime, keep it here. - struct plot_info plotInfo; - DepthAxis *profileYAxis; - PartialGasPressureAxis *gasYAxis; - TemperatureAxis *temperatureAxis; - TimeAxis *timeAxis; - DiveProfileItem *diveProfileItem; - DiveTemperatureItem *temperatureItem; - DiveMeanDepthItem *meanDepthItem; - DiveCartesianAxis *cylinderPressureAxis; - DiveGasPressureItem *gasPressureItem; - QList eventItems; - DiveTextItem *diveComputerText; - DiveCalculatedCeiling *diveCeiling; - DiveTextItem *gradientFactor; - QList allTissues; - DiveReportedCeiling *reportedCeiling; - PartialPressureGasItem *pn2GasItem; - PartialPressureGasItem *pheGasItem; - PartialPressureGasItem *po2GasItem; - PartialPressureGasItem *o2SetpointGasItem; - PartialPressureGasItem *ccrsensor1GasItem; - PartialPressureGasItem *ccrsensor2GasItem; - PartialPressureGasItem *ccrsensor3GasItem; - DiveCartesianAxis *heartBeatAxis; - DiveHeartrateItem *heartBeatItem; - DiveCartesianAxis *percentageAxis; - QList allPercentages; - DiveAmbPressureItem *ambPressureItem; - DiveGFLineItem *gflineItem; - DiveLineItem *mouseFollowerVertical; - DiveLineItem *mouseFollowerHorizontal; - RulerItem2 *rulerItem; - TankItem *tankItem; - bool isGrayscale; - bool printMode; - - //specifics for ADD and PLAN - QList handles; - QList gases; - QList pictures; - void repositionDiveHandlers(); - int fixHandlerIndex(DiveHandler *activeHandler); - friend class DiveHandler; - QHash actionsForKeys; - bool shouldCalculateMaxTime; - bool shouldCalculateMaxDepth; - int maxtime; - int maxdepth; - double fontPrintScale; -}; - -#endif // PROFILEWIDGET2_H diff --git a/qt-ui/profile/ruleritem.cpp b/qt-ui/profile/ruleritem.cpp deleted file mode 100644 index 830985552..000000000 --- a/qt-ui/profile/ruleritem.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#include "ruleritem.h" -#include "preferences.h" -#include "mainwindow.h" -#include "profilewidget2.h" -#include "display.h" - -#include - -#include "profile.h" - -RulerNodeItem2::RulerNodeItem2() : - entry(NULL), - ruler(NULL), - timeAxis(NULL), - depthAxis(NULL) -{ - memset(&pInfo, 0, sizeof(pInfo)); - setRect(-8, -8, 16, 16); - setBrush(QColor(0xff, 0, 0, 127)); - setPen(QColor(Qt::red)); - setFlag(ItemIsMovable); - setFlag(ItemSendsGeometryChanges); - setFlag(ItemIgnoresTransformations); -} - -void RulerNodeItem2::setPlotInfo(plot_info &info) -{ - pInfo = info; - entry = pInfo.entry; -} - -void RulerNodeItem2::setRuler(RulerItem2 *r) -{ - ruler = r; -} - -void RulerNodeItem2::recalculate() -{ - struct plot_data *data = pInfo.entry + (pInfo.nr - 1); - uint16_t count = 0; - if (x() < 0) { - setPos(0, y()); - } else if (x() > timeAxis->posAtValue(data->sec)) { - setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); - } else { - data = pInfo.entry; - count = 0; - while (timeAxis->posAtValue(data->sec) < x() && count < pInfo.nr) { - data = pInfo.entry + count; - count++; - } - setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); - entry = data; - } -} - -void RulerNodeItem2::mouseMoveEvent(QGraphicsSceneMouseEvent *event) -{ - qreal x = event->scenePos().x(); - if (x < 0.0) - x = 0.0; - setPos(x, event->scenePos().y()); - recalculate(); - ruler->recalculate(); -} - -RulerItem2::RulerItem2() : source(new RulerNodeItem2()), - dest(new RulerNodeItem2()), - timeAxis(NULL), - depthAxis(NULL), - textItemBack(new QGraphicsRectItem(this)), - textItem(new QGraphicsSimpleTextItem(this)) -{ - memset(&pInfo, 0, sizeof(pInfo)); - source->setRuler(this); - dest->setRuler(this); - textItem->setFlag(QGraphicsItem::ItemIgnoresTransformations); - textItemBack->setBrush(QColor(0xff, 0xff, 0xff, 190)); - textItemBack->setPen(QColor(Qt::white)); - textItemBack->setFlag(QGraphicsItem::ItemIgnoresTransformations); - setPen(QPen(QColor(Qt::black), 0.0)); - connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); -} - -void RulerItem2::settingsChanged() -{ - ProfileWidget2 *profWidget = NULL; - if (scene() && scene()->views().count()) - profWidget = qobject_cast(scene()->views().first()); - - if (profWidget && profWidget->currentState == ProfileWidget2::PROFILE) - setVisible(prefs.rulergraph); - else - setVisible(false); -} - -void RulerItem2::recalculate() -{ - char buffer[500]; - QPointF tmp; - QFont font; - QFontMetrics fm(font); - - if (timeAxis == NULL || depthAxis == NULL || pInfo.nr == 0) - return; - - prepareGeometryChange(); - startPoint = mapFromItem(source, 0, 0); - endPoint = mapFromItem(dest, 0, 0); - - if (startPoint.x() > endPoint.x()) { - tmp = endPoint; - endPoint = startPoint; - startPoint = tmp; - } - QLineF line(startPoint, endPoint); - setLine(line); - compare_samples(source->entry, dest->entry, buffer, 500, 1); - text = QString(buffer); - - // draw text - QGraphicsView *view = scene()->views().first(); - QPoint begin = view->mapFromScene(mapToScene(startPoint)); - textItem->setText(text); - qreal tgtX = startPoint.x(); - const qreal diff = begin.x() + textItem->boundingRect().width(); - // clamp so that the text doesn't go out of the screen to the right - if (diff > view->width()) { - begin.setX(begin.x() - (diff - view->width())); - tgtX = mapFromScene(view->mapToScene(begin)).x(); - } - // always show the text bellow the lowest of the start and end points - qreal tgtY = (startPoint.y() >= endPoint.y()) ? startPoint.y() : endPoint.y(); - // this isn't exactly optimal, since we want to scale the 1.0, 4.0 distances as well - textItem->setPos(tgtX - 1.0, tgtY + 4.0); - - // setup the text background - textItemBack->setVisible(startPoint.x() != endPoint.x()); - textItemBack->setPos(textItem->x(), textItem->y()); - textItemBack->setRect(0, 0, textItem->boundingRect().width(), textItem->boundingRect().height()); -} - -RulerNodeItem2 *RulerItem2::sourceNode() const -{ - return source; -} - -RulerNodeItem2 *RulerItem2::destNode() const -{ - return dest; -} - -void RulerItem2::setPlotInfo(plot_info info) -{ - pInfo = info; - dest->setPlotInfo(info); - source->setPlotInfo(info); - dest->recalculate(); - source->recalculate(); - recalculate(); -} - -void RulerItem2::setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth) -{ - timeAxis = time; - depthAxis = depth; - dest->depthAxis = depth; - dest->timeAxis = time; - source->depthAxis = depth; - source->timeAxis = time; - recalculate(); -} - -void RulerItem2::setVisible(bool visible) -{ - QGraphicsLineItem::setVisible(visible); - source->setVisible(visible); - dest->setVisible(visible); -} diff --git a/qt-ui/profile/ruleritem.h b/qt-ui/profile/ruleritem.h deleted file mode 100644 index 4fad0451c..000000000 --- a/qt-ui/profile/ruleritem.h +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef RULERITEM_H -#define RULERITEM_H - -#include -#include -#include -#include "divecartesianaxis.h" -#include "display.h" - -struct plot_data; -class RulerItem2; - -class RulerNodeItem2 : public QObject, public QGraphicsEllipseItem { - Q_OBJECT - friend class RulerItem2; - -public: - explicit RulerNodeItem2(); - void setRuler(RulerItem2 *r); - void setPlotInfo(struct plot_info &info); - void recalculate(); - -protected: - virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); -private: - struct plot_info pInfo; - struct plot_data *entry; - RulerItem2 *ruler; - DiveCartesianAxis *timeAxis; - DiveCartesianAxis *depthAxis; -}; - -class RulerItem2 : public QObject, public QGraphicsLineItem { - Q_OBJECT -public: - explicit RulerItem2(); - void recalculate(); - - void setPlotInfo(struct plot_info pInfo); - RulerNodeItem2 *sourceNode() const; - RulerNodeItem2 *destNode() const; - void setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth); - void setVisible(bool visible); - -public -slots: - void settingsChanged(); - -private: - struct plot_info pInfo; - QPointF startPoint, endPoint; - RulerNodeItem2 *source, *dest; - QString text; - DiveCartesianAxis *timeAxis; - DiveCartesianAxis *depthAxis; - QGraphicsRectItem *textItemBack; - QGraphicsSimpleTextItem *textItem; -}; -#endif diff --git a/qt-ui/profile/tankitem.cpp b/qt-ui/profile/tankitem.cpp deleted file mode 100644 index c0e75a371..000000000 --- a/qt-ui/profile/tankitem.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "tankitem.h" -#include "diveplotdatamodel.h" -#include "divetextitem.h" -#include "profile.h" -#include - -TankItem::TankItem(QObject *parent) : - QGraphicsRectItem(), - dataModel(0), - pInfoEntry(0), - pInfoNr(0) -{ - height = 3; - QColor red(PERSIANRED1); - QColor blue(AIR_BLUE); - QColor yellow(NITROX_YELLOW); - QColor green(NITROX_GREEN); - QLinearGradient nitroxGradient(QPointF(0, 0), QPointF(0, height)); - nitroxGradient.setColorAt(0.0, green); - nitroxGradient.setColorAt(0.49, green); - nitroxGradient.setColorAt(0.5, yellow); - nitroxGradient.setColorAt(1.0, yellow); - nitrox = nitroxGradient; - oxygen = green; - QLinearGradient trimixGradient(QPointF(0, 0), QPointF(0, height)); - trimixGradient.setColorAt(0.0, green); - trimixGradient.setColorAt(0.49, green); - trimixGradient.setColorAt(0.5, red); - trimixGradient.setColorAt(1.0, red); - trimix = trimixGradient; - air = blue; - memset(&diveCylinderStore, 0, sizeof(diveCylinderStore)); -} - -TankItem::~TankItem() -{ - // Should this be clear_dive(diveCylinderStore)? - for (int i = 0; i < MAX_CYLINDERS; i++) - free((void *)diveCylinderStore.cylinder[i].type.description); -} - -void TankItem::setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d) -{ - free(pInfoEntry); - // the plotInfo and dive structures passed in could become invalid before we stop using them, - // so copy the data that we need - int size = plotInfo->nr * sizeof(plotInfo->entry[0]); - pInfoEntry = (struct plot_data *)malloc(size); - pInfoNr = plotInfo->nr; - memcpy(pInfoEntry, plotInfo->entry, size); - copy_cylinders(d, &diveCylinderStore, false); - dataModel = model; - connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex)), Qt::UniqueConnection); - modelDataChanged(); -} - -void TankItem::createBar(qreal x, qreal w, struct gasmix *gas) -{ - // pick the right gradient, size, position and text - QGraphicsRectItem *rect = new QGraphicsRectItem(x, 0, w, height, this); - if (gasmix_is_air(gas)) - rect->setBrush(air); - else if (gas->he.permille) - rect->setBrush(trimix); - else if (gas->o2.permille == 1000) - rect->setBrush(oxygen); - else - rect->setBrush(nitrox); - rect->setPen(QPen(QBrush(), 0.0)); // get rid of the thick line around the rectangle - rects.push_back(rect); - DiveTextItem *label = new DiveTextItem(rect); - label->setText(gasname(gas)); - label->setBrush(Qt::black); - label->setPos(x + 1, 0); - label->setAlignment(Qt::AlignBottom | Qt::AlignRight); - label->setZValue(101); -} - -void TankItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - // We don't have enougth data to calculate things, quit. - - if (!dataModel || !pInfoEntry || !pInfoNr) - return; - - // remove the old rectangles - foreach (QGraphicsRectItem *r, rects) { - delete(r); - } - rects.clear(); - - // walk the list and figure out which tanks go where - struct plot_data *entry = pInfoEntry; - int cylIdx = entry->cylinderindex; - int i = -1; - int startTime = 0; - struct gasmix *gas = &diveCylinderStore.cylinder[cylIdx].gasmix; - qreal width, left; - while (++i < pInfoNr) { - entry = &pInfoEntry[i]; - if (entry->cylinderindex == cylIdx) - continue; - width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); - left = hAxis->posAtValue(startTime); - createBar(left, width, gas); - cylIdx = entry->cylinderindex; - gas = &diveCylinderStore.cylinder[cylIdx].gasmix; - startTime = entry->sec; - } - width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); - left = hAxis->posAtValue(startTime); - createBar(left, width, gas); -} - -void TankItem::setHorizontalAxis(DiveCartesianAxis *horizontal) -{ - hAxis = horizontal; - connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); - modelDataChanged(); -} diff --git a/qt-ui/profile/tankitem.h b/qt-ui/profile/tankitem.h deleted file mode 100644 index fd685fc82..000000000 --- a/qt-ui/profile/tankitem.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef TANKITEM_H -#define TANKITEM_H - -#include -#include -#include -#include "divelineitem.h" -#include "divecartesianaxis.h" -#include "dive.h" - -class TankItem : public QObject, public QGraphicsRectItem -{ - Q_OBJECT - -public: - explicit TankItem(QObject *parent = 0); - ~TankItem(); - void setHorizontalAxis(DiveCartesianAxis *horizontal); - void setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d); - -signals: - -public slots: - virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); - -private: - void createBar(qreal x, qreal w, struct gasmix *gas); - DivePlotDataModel *dataModel; - DiveCartesianAxis *hAxis; - int hDataColumn; - struct dive diveCylinderStore; - struct plot_data *pInfoEntry; - int pInfoNr; - qreal height; - QBrush air, nitrox, oxygen, trimix; - QList rects; -}; - -#endif // TANKITEM_H diff --git a/qt-ui/qtwaitingspinner.cpp b/qt-ui/qtwaitingspinner.cpp deleted file mode 100644 index 14e8669b0..000000000 --- a/qt-ui/qtwaitingspinner.cpp +++ /dev/null @@ -1,288 +0,0 @@ - -/* Original Work Copyright (c) 2012-2014 Alexander Turkin - Modified 2014 by William Hallatt - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -#include -#include - -#include -#include - -#include "qtwaitingspinner.h" - -/*----------------------------------------------------------------------------*/ - -// Defaults -const QColor c_color(Qt::black); -const qreal c_roundness(70.0); -const qreal c_minTrailOpacity(15.0); -const qreal c_trailFadePercentage(70.0); -const int c_lines(12); -const int c_lineLength(10); -const int c_lineWidth(5); -const int c_innerRadius(10); -const int c_revPerSec(1); - -/*----------------------------------------------------------------------------*/ - -QtWaitingSpinner::QtWaitingSpinner(QWidget *parent) - : QWidget(parent), - - // Configurable settings. - m_color(c_color), m_roundness(c_roundness), - m_minTrailOpacity(c_minTrailOpacity), - m_trailFadePercentage(c_trailFadePercentage), m_revPerSec(c_revPerSec), - m_numberOfLines(c_lines), m_lineLength(c_lineLength + c_lineWidth), - m_lineWidth(c_lineWidth), m_innerRadius(c_innerRadius), - - // Other - m_timer(NULL), m_parent(parent), m_centreOnParent(false), - m_currentCounter(0), m_isSpinning(false) { - initialise(); -} - -/*----------------------------------------------------------------------------*/ - -QtWaitingSpinner::QtWaitingSpinner(Qt::WindowModality modality, QWidget *parent, - bool centreOnParent) - : QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint), - - // Configurable settings. - m_color(c_color), m_roundness(c_roundness), - m_minTrailOpacity(c_minTrailOpacity), - m_trailFadePercentage(c_trailFadePercentage), m_revPerSec(c_revPerSec), - m_numberOfLines(c_lines), m_lineLength(c_lineLength + c_lineWidth), - m_lineWidth(c_lineWidth), m_innerRadius(c_innerRadius), - - // Other - m_timer(NULL), m_parent(parent), m_centreOnParent(centreOnParent), - m_currentCounter(0) { - initialise(); - - // We need to set the window modality AFTER we've hidden the - // widget for the first time since changing this property while - // the widget is visible has no effect. - this->setWindowModality(modality); - this->setAttribute(Qt::WA_TranslucentBackground); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::initialise() { - m_timer = new QTimer(this); - connect(m_timer, SIGNAL(timeout()), this, SLOT(rotate())); - updateSize(); - updateTimer(); - this->hide(); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::paintEvent(QPaintEvent * /*ev*/) { - QPainter painter(this); - painter.fillRect(this->rect(), Qt::transparent); - painter.setRenderHint(QPainter::Antialiasing, true); - - if (m_currentCounter >= m_numberOfLines) { - m_currentCounter = 0; - } - painter.setPen(Qt::NoPen); - for (int i = 0; i < m_numberOfLines; ++i) { - painter.save(); - painter.translate(m_innerRadius + m_lineLength, - m_innerRadius + m_lineLength); - qreal rotateAngle = - static_cast(360 * i) / static_cast(m_numberOfLines); - painter.rotate(rotateAngle); - painter.translate(m_innerRadius, 0); - int distance = - lineCountDistanceFromPrimary(i, m_currentCounter, m_numberOfLines); - QColor color = - currentLineColor(distance, m_numberOfLines, m_trailFadePercentage, - m_minTrailOpacity, m_color); - painter.setBrush(color); - // TODO improve the way rounded rect is painted - painter.drawRoundedRect( - QRect(0, -m_lineWidth / 2, m_lineLength, m_lineWidth), m_roundness, - m_roundness, Qt::RelativeSize); - painter.restore(); - } -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::start() { - updatePosition(); - m_isSpinning = true; - this->show(); - if (!m_timer->isActive()) { - m_timer->start(); - m_currentCounter = 0; - } -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::stop() { - m_isSpinning = false; - this->hide(); - if (m_timer->isActive()) { - m_timer->stop(); - m_currentCounter = 0; - } -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setNumberOfLines(int lines) { - m_numberOfLines = lines; - m_currentCounter = 0; - updateTimer(); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setLineLength(int length) { - m_lineLength = length; - updateSize(); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setLineWidth(int width) { - m_lineWidth = width; - updateSize(); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setInnerRadius(int radius) { - m_innerRadius = radius; - updateSize(); -} - -/*----------------------------------------------------------------------------*/ - -bool QtWaitingSpinner::isSpinning() const { return m_isSpinning; } - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setRoundness(qreal roundness) { - m_roundness = std::max(0.0, std::min(100.0, roundness)); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setColor(QColor color) { m_color = color; } - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setRevolutionsPerSecond(int rps) { - m_revPerSec = rps; - updateTimer(); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setTrailFadePercentage(qreal trail) { - m_trailFadePercentage = trail; -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::setMinimumTrailOpacity(qreal minOpacity) { - m_minTrailOpacity = minOpacity; -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::rotate() { - ++m_currentCounter; - if (m_currentCounter >= m_numberOfLines) { - m_currentCounter = 0; - } - update(); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::updateSize() { - int size = (m_innerRadius + m_lineLength) * 2; - setFixedSize(size, size); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::updateTimer() { - m_timer->setInterval(calculateTimerInterval(m_numberOfLines, m_revPerSec)); -} - -/*----------------------------------------------------------------------------*/ - -void QtWaitingSpinner::updatePosition() { - if (m_parent && m_centreOnParent) { - this->move(m_parent->frameGeometry().topLeft() + m_parent->rect().center() - - this->rect().center()); - } -} - -/*----------------------------------------------------------------------------*/ - -int QtWaitingSpinner::calculateTimerInterval(int lines, int speed) { - return 1000 / (lines * speed); -} - -/*----------------------------------------------------------------------------*/ - -int QtWaitingSpinner::lineCountDistanceFromPrimary(int current, int primary, - int totalNrOfLines) { - int distance = primary - current; - if (distance < 0) { - distance += totalNrOfLines; - } - return distance; -} - -/*----------------------------------------------------------------------------*/ - -QColor QtWaitingSpinner::currentLineColor(int countDistance, int totalNrOfLines, - qreal trailFadePerc, qreal minOpacity, - QColor color) { - if (countDistance == 0) { - return color; - } - const qreal minAlphaF = minOpacity / 100.0; - int distanceThreshold = - static_cast(ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)); - if (countDistance > distanceThreshold) { - color.setAlphaF(minAlphaF); - } else { - qreal alphaDiff = color.alphaF() - minAlphaF; - qreal gradient = alphaDiff / static_cast(distanceThreshold + 1); - qreal resultAlpha = color.alphaF() - gradient * countDistance; - - // If alpha is out of bounds, clip it. - resultAlpha = std::min(1.0, std::max(0.0, resultAlpha)); - color.setAlphaF(resultAlpha); - } - return color; -} - -/*----------------------------------------------------------------------------*/ diff --git a/qt-ui/qtwaitingspinner.h b/qt-ui/qtwaitingspinner.h deleted file mode 100644 index 254b52ec7..000000000 --- a/qt-ui/qtwaitingspinner.h +++ /dev/null @@ -1,103 +0,0 @@ -/* Original Work Copyright (c) 2012-2014 Alexander Turkin - Modified 2014 by William Hallatt - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -#ifndef QTWAITINGSPINNER_H -#define QTWAITINGSPINNER_H - -#include - -#include -#include - -class QtWaitingSpinner : public QWidget { - Q_OBJECT -public: - /*! Constructor for "standard" widget behaviour - use this - * constructor if you wish to, e.g. embed your widget in another. */ - QtWaitingSpinner(QWidget *parent = 0); - - /*! Constructor - use this constructor to automatically create a modal - * ("blocking") spinner on top of the calling widget/window. If a valid - * parent widget is provided, "centreOnParent" will ensure that - * QtWaitingSpinner automatically centres itself on it, if not, - * "centreOnParent" is ignored. */ - QtWaitingSpinner(Qt::WindowModality modality, QWidget *parent = 0, - bool centreOnParent = true); - -public Q_SLOTS: - void start(); - void stop(); - -public: - void setColor(QColor color); - void setRoundness(qreal roundness); - void setMinimumTrailOpacity(qreal minOpacity); - void setTrailFadePercentage(qreal trail); - void setRevolutionsPerSecond(int rps); - void setNumberOfLines(int lines); - void setLineLength(int length); - void setLineWidth(int width); - void setInnerRadius(int radius); - - bool isSpinning() const; - -private Q_SLOTS: - void rotate(); - -protected: - void paintEvent(QPaintEvent *ev); - -private: - static int calculateTimerInterval(int lines, int speed); - static int lineCountDistanceFromPrimary(int current, int primary, - int totalNrOfLines); - static QColor currentLineColor(int distance, int totalNrOfLines, - qreal trailFadePerc, qreal minOpacity, - QColor color); - - void initialise(); - void updateSize(); - void updateTimer(); - void updatePosition(); - -private: - // Configurable settings. - QColor m_color; - qreal m_roundness; // 0..100 - qreal m_minTrailOpacity; - qreal m_trailFadePercentage; - int m_revPerSec; // revolutions per second - int m_numberOfLines; - int m_lineLength; - int m_lineWidth; - int m_innerRadius; - -private: - QtWaitingSpinner(const QtWaitingSpinner&); - QtWaitingSpinner& operator=(const QtWaitingSpinner&); - - QTimer *m_timer; - QWidget *m_parent; - bool m_centreOnParent; - int m_currentCounter; - bool m_isSpinning; -}; - -#endif // QTWAITINGSPINNER_H diff --git a/qt-ui/renumber.ui b/qt-ui/renumber.ui deleted file mode 100644 index 00e7b84bb..000000000 --- a/qt-ui/renumber.ui +++ /dev/null @@ -1,120 +0,0 @@ - - - RenumberDialog - - - Qt::WindowModal - - - - 0 - 0 - 211 - 125 - - - - Renumber - - - - :/subsurface-icon - - - - - 1 - - - 3 - - - 3 - - - 3 - - - 3 - - - - - New starting number - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 99999 - - - 1 - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - accepted() - RenumberDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - RenumberDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/searchbar.ui b/qt-ui/searchbar.ui deleted file mode 100644 index 22bce39c6..000000000 --- a/qt-ui/searchbar.ui +++ /dev/null @@ -1,134 +0,0 @@ - - - SearchBar - - - - 0 - 0 - 400 - 34 - - - - Form - - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - - - - false - - - - 0 - 0 - - - - - - - - - - - - true - - - - - - - false - - - - 0 - 0 - - - - - - - - - - - - true - - - - - - - - 0 - 0 - - - - - - - - - - - - true - - - - - - - - diff --git a/qt-ui/setpoint.ui b/qt-ui/setpoint.ui deleted file mode 100644 index d96488a31..000000000 --- a/qt-ui/setpoint.ui +++ /dev/null @@ -1,130 +0,0 @@ - - - SetpointDialog - - - Qt::WindowModal - - - - 0 - 0 - 211 - 125 - - - - Renumber - - - - :/subsurface-icon - - - - - 1 - - - 3 - - - 3 - - - 3 - - - 3 - - - - - New set-point (0 for OC) - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - bar - - - 1 - - - 0.000000000000000 - - - 2.000000000000000 - - - 0.100000000000000 - - - 1.100000000000000 - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - SetpointDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - SetpointDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/shiftimagetimes.ui b/qt-ui/shiftimagetimes.ui deleted file mode 100644 index 0478a62b4..000000000 --- a/qt-ui/shiftimagetimes.ui +++ /dev/null @@ -1,293 +0,0 @@ - - - ShiftImageTimesDialog - - - Qt::WindowModal - - - - 0 - 0 - 693 - 606 - - - - - 0 - 0 - - - - Shift selected image times - - - - :/subsurface-icon - - - - - - - Shift times of image(s) by - - - - - - - 2000 - 1 - 1 - - - - - 23 - 59 - 59 - 2010 - 12 - 31 - - - - - 0 - 0 - 0 - 2000 - 1 - 1 - - - - - 2010 - 12 - 31 - - - - - 2000 - 1 - 1 - - - - - - - - - - h:mm - - - Qt::LocalTime - - - - - - - Earlier - - - - - - - Later - - - true - - - - - - - true - - - color: red; - - - Warning! -Not all images have timestamps in the range between -30 minutes before the start and 30 minutes after the end of any selected dive. - - - - - - - Load images even if the time does not match the dive time - - - - - - - color: red; - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - Qt::Horizontal - - - - - - - - 16777215 - 60 - - - - Qt::LeftToRight - - - To compute the offset between the clocks of your dive computer and your camera use your camera to take a picture of your dive compuer displaying the current time. Download that image to your computer and press this button. - - - true - - - - - - - Determine camera time offset - - - Select image of divecomputer showing time - - - - - - - false - - - - - - - - - - 16777215 - 60 - - - - Which date and time are displayed on the image? - - - true - - - - - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - - - - - - - - Qt::Vertical - - - - 20 - 193 - - - - - - - - - - - - - - - - - - buttonBox - accepted() - ShiftImageTimesDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - ShiftImageTimesDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/shifttimes.ui b/qt-ui/shifttimes.ui deleted file mode 100644 index 486b1f43b..000000000 --- a/qt-ui/shifttimes.ui +++ /dev/null @@ -1,214 +0,0 @@ - - - ShiftTimesDialog - - - Qt::WindowModal - - - - 0 - 0 - 343 - 224 - - - - - 0 - 0 - - - - Shift selected dive times - - - - :/subsurface-icon - - - - - 6 - - - 9 - - - 9 - - - 9 - - - 9 - - - - - Shift times of selected dives by - - - - 6 - - - 9 - - - 9 - - - 9 - - - 9 - - - - - - - Shifted time: - - - - - - - Current time: - - - - - - - 0:0 - - - - - - - 0:0 - - - - - - - - - - 2000 - 1 - 1 - - - - - 0 - 0 - 0 - 2000 - 1 - 1 - - - - - 2000 - 1 - 1 - - - - h:mm - - - Qt::LocalTime - - - - - - - Earlier - - - - - - - Later - - - true - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - accepted() - ShiftTimesDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - ShiftTimesDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - timeEdit - timeChanged(const QTime) - ShiftTimesDialog - changeTime() - - - backwards - toggled(bool) - ShiftTimesDialog - changeTime() - - - diff --git a/qt-ui/simplewidgets.cpp b/qt-ui/simplewidgets.cpp deleted file mode 100644 index 62a9cc646..000000000 --- a/qt-ui/simplewidgets.cpp +++ /dev/null @@ -1,736 +0,0 @@ -#include "simplewidgets.h" -#include "filtermodels.h" - -#include -#include -#include -#include -#include -#include - -#include "file.h" -#include "mainwindow.h" -#include "helpers.h" -#include "libdivecomputer/parser.h" -#include "divelistview.h" -#include "display.h" -#include "profile/profilewidget2.h" -#include "undocommands.h" - -class MinMaxAvgWidgetPrivate { -public: - QLabel *avgIco, *avgValue; - QLabel *minIco, *minValue; - QLabel *maxIco, *maxValue; - - MinMaxAvgWidgetPrivate(MinMaxAvgWidget *owner) - { - avgIco = new QLabel(owner); - avgIco->setPixmap(QIcon(":/average").pixmap(16, 16)); - avgIco->setToolTip(QObject::tr("Average")); - minIco = new QLabel(owner); - minIco->setPixmap(QIcon(":/minimum").pixmap(16, 16)); - minIco->setToolTip(QObject::tr("Minimum")); - maxIco = new QLabel(owner); - maxIco->setPixmap(QIcon(":/maximum").pixmap(16, 16)); - maxIco->setToolTip(QObject::tr("Maximum")); - avgValue = new QLabel(owner); - minValue = new QLabel(owner); - maxValue = new QLabel(owner); - - QGridLayout *formLayout = new QGridLayout(); - formLayout->addWidget(maxIco, 0, 0); - formLayout->addWidget(maxValue, 0, 1); - formLayout->addWidget(avgIco, 1, 0); - formLayout->addWidget(avgValue, 1, 1); - formLayout->addWidget(minIco, 2, 0); - formLayout->addWidget(minValue, 2, 1); - owner->setLayout(formLayout); - } -}; - -double MinMaxAvgWidget::average() const -{ - return d->avgValue->text().toDouble(); -} - -double MinMaxAvgWidget::maximum() const -{ - return d->maxValue->text().toDouble(); -} -double MinMaxAvgWidget::minimum() const -{ - return d->minValue->text().toDouble(); -} - -MinMaxAvgWidget::MinMaxAvgWidget(QWidget *parent) : d(new MinMaxAvgWidgetPrivate(this)) -{ -} - -MinMaxAvgWidget::~MinMaxAvgWidget() -{ -} - -void MinMaxAvgWidget::clear() -{ - d->avgValue->setText(QString()); - d->maxValue->setText(QString()); - d->minValue->setText(QString()); -} - -void MinMaxAvgWidget::setAverage(double average) -{ - d->avgValue->setText(QString::number(average)); -} - -void MinMaxAvgWidget::setMaximum(double maximum) -{ - d->maxValue->setText(QString::number(maximum)); -} -void MinMaxAvgWidget::setMinimum(double minimum) -{ - d->minValue->setText(QString::number(minimum)); -} - -void MinMaxAvgWidget::setAverage(const QString &average) -{ - d->avgValue->setText(average); -} - -void MinMaxAvgWidget::setMaximum(const QString &maximum) -{ - d->maxValue->setText(maximum); -} - -void MinMaxAvgWidget::setMinimum(const QString &minimum) -{ - d->minValue->setText(minimum); -} - -void MinMaxAvgWidget::overrideMinToolTipText(const QString &newTip) -{ - d->minIco->setToolTip(newTip); - d->minValue->setToolTip(newTip); -} - -void MinMaxAvgWidget::overrideAvgToolTipText(const QString &newTip) -{ - d->avgIco->setToolTip(newTip); - d->avgValue->setToolTip(newTip); -} - -void MinMaxAvgWidget::overrideMaxToolTipText(const QString &newTip) -{ - d->maxIco->setToolTip(newTip); - d->maxValue->setToolTip(newTip); -} - -RenumberDialog *RenumberDialog::instance() -{ - static RenumberDialog *self = new RenumberDialog(MainWindow::instance()); - return self; -} - -void RenumberDialog::renumberOnlySelected(bool selected) -{ - if (selected && amount_selected == 1) - ui.groupBox->setTitle(tr("New number")); - else - ui.groupBox->setTitle(tr("New starting number")); - selectedOnly = selected; -} - -void RenumberDialog::buttonClicked(QAbstractButton *button) -{ - if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { - MainWindow::instance()->dive_list()->rememberSelection(); - // we remember a map from dive uuid to a pair of old number / new number - QMap > renumberedDives; - int i; - int newNr = ui.spinBox->value(); - struct dive *dive = NULL; - for_each_dive (i, dive) { - if (!selectedOnly || dive->selected) - renumberedDives.insert(dive->id, QPair(dive->number, newNr++)); - } - UndoRenumberDives *undoCommand = new UndoRenumberDives(renumberedDives); - MainWindow::instance()->undoStack->push(undoCommand); - - MainWindow::instance()->dive_list()->fixMessyQtModelBehaviour(); - mark_divelist_changed(true); - MainWindow::instance()->dive_list()->restoreSelection(); - } -} - -RenumberDialog::RenumberDialog(QWidget *parent) : QDialog(parent), selectedOnly(false) -{ - ui.setupUi(this); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -SetpointDialog *SetpointDialog::instance() -{ - static SetpointDialog *self = new SetpointDialog(MainWindow::instance()); - return self; -} - -void SetpointDialog::setpointData(struct divecomputer *divecomputer, int second) -{ - dc = divecomputer; - time = second < 0 ? 0 : second; -} - -void SetpointDialog::buttonClicked(QAbstractButton *button) -{ - if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole && dc) - add_event(dc, time, SAMPLE_EVENT_PO2, 0, (int)(1000.0 * ui.spinbox->value()), "SP change"); - mark_divelist_changed(true); - MainWindow::instance()->graphics()->replot(); -} - -SetpointDialog::SetpointDialog(QWidget *parent) : - QDialog(parent), - dc(0) -{ - ui.setupUi(this); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -ShiftTimesDialog *ShiftTimesDialog::instance() -{ - static ShiftTimesDialog *self = new ShiftTimesDialog(MainWindow::instance()); - return self; -} - -void ShiftTimesDialog::buttonClicked(QAbstractButton *button) -{ - int amount; - - if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { - amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; - if (ui.backwards->isChecked()) - amount *= -1; - if (amount != 0) { - // DANGER, DANGER - this could get our dive_table unsorted... - int i; - struct dive *dive; - QList affectedDives; - for_each_dive (i, dive) { - if (!dive->selected) - continue; - - affectedDives.append(dive->id); - } - MainWindow::instance()->undoStack->push(new UndoShiftTime(affectedDives, amount)); - sort_table(&dive_table); - mark_divelist_changed(true); - MainWindow::instance()->dive_list()->rememberSelection(); - MainWindow::instance()->refreshDisplay(); - MainWindow::instance()->dive_list()->restoreSelection(); - } - } -} - -void ShiftTimesDialog::showEvent(QShowEvent *event) -{ - ui.timeEdit->setTime(QTime(0, 0, 0, 0)); - when = get_times(); //get time of first selected dive - ui.currentTime->setText(get_dive_date_string(when)); - ui.shiftedTime->setText(get_dive_date_string(when)); -} - -void ShiftTimesDialog::changeTime() -{ - int amount; - - amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; - if (ui.backwards->isChecked()) - amount *= -1; - - ui.shiftedTime->setText(get_dive_date_string(amount + when)); -} - -ShiftTimesDialog::ShiftTimesDialog(QWidget *parent) : - QDialog(parent), - when(0) -{ - ui.setupUi(this); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - connect(ui.timeEdit, SIGNAL(timeChanged(const QTime)), this, SLOT(changeTime())); - connect(ui.backwards, SIGNAL(toggled(bool)), this, SLOT(changeTime())); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -void ShiftImageTimesDialog::buttonClicked(QAbstractButton *button) -{ - if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { - m_amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; - if (ui.backwards->isChecked()) - m_amount *= -1; - } -} - -void ShiftImageTimesDialog::syncCameraClicked() -{ - QPixmap picture; - QDateTime dcDateTime = QDateTime(); - QStringList fileNames = QFileDialog::getOpenFileNames(this, - tr("Open image file"), - DiveListView::lastUsedImageDir(), - tr("Image files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); - if (fileNames.isEmpty()) - return; - - picture.load(fileNames.at(0)); - ui.displayDC->setEnabled(true); - QGraphicsScene *scene = new QGraphicsScene(this); - - scene->addPixmap(picture.scaled(ui.DCImage->size())); - ui.DCImage->setScene(scene); - - dcImageEpoch = picture_get_timestamp(fileNames.at(0).toUtf8().data()); - dcDateTime.setTime_t(dcImageEpoch - gettimezoneoffset(displayed_dive.when)); - ui.dcTime->setDateTime(dcDateTime); - connect(ui.dcTime, SIGNAL(dateTimeChanged(const QDateTime &)), this, SLOT(dcDateTimeChanged(const QDateTime &))); -} - -void ShiftImageTimesDialog::dcDateTimeChanged(const QDateTime &newDateTime) -{ - QDateTime newtime(newDateTime); - if (!dcImageEpoch) - return; - newtime.setTimeSpec(Qt::UTC); - setOffset(newtime.toTime_t() - dcImageEpoch); -} - -void ShiftImageTimesDialog::matchAllImagesToggled(bool state) -{ - matchAllImages = state; -} - -bool ShiftImageTimesDialog::matchAll() -{ - return matchAllImages; -} - -ShiftImageTimesDialog::ShiftImageTimesDialog(QWidget *parent, QStringList fileNames) : - QDialog(parent), - fileNames(fileNames), - m_amount(0), - matchAllImages(false) -{ - ui.setupUi(this); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - connect(ui.syncCamera, SIGNAL(clicked()), this, SLOT(syncCameraClicked())); - connect(ui.timeEdit, SIGNAL(timeChanged(const QTime &)), this, SLOT(timeEditChanged(const QTime &))); - connect(ui.matchAllImages, SIGNAL(toggled(bool)), this, SLOT(matchAllImagesToggled(bool))); - dcImageEpoch = (time_t)0; -} - -time_t ShiftImageTimesDialog::amount() const -{ - return m_amount; -} - -void ShiftImageTimesDialog::setOffset(time_t offset) -{ - if (offset >= 0) { - ui.forward->setChecked(true); - } else { - ui.backwards->setChecked(true); - offset *= -1; - } - ui.timeEdit->setTime(QTime(offset / 3600, (offset % 3600) / 60, offset % 60)); -} - -void ShiftImageTimesDialog::updateInvalid() -{ - timestamp_t timestamp; - QDateTime time; - bool allValid = true; - ui.warningLabel->hide(); - ui.invalidLabel->hide(); - time.setTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); - ui.invalidLabel->setText("Dive:" + time.toString() + "\n"); - - Q_FOREACH (const QString &fileName, fileNames) { - if (picture_check_valid(fileName.toUtf8().data(), m_amount)) - continue; - - // We've found invalid image - timestamp = picture_get_timestamp(fileName.toUtf8().data()); - time.setTime_t(timestamp + m_amount - gettimezoneoffset(displayed_dive.when)); - ui.invalidLabel->setText(ui.invalidLabel->text() + fileName + " " + time.toString() + "\n"); - allValid = false; - } - - if (!allValid){ - ui.warningLabel->show(); - ui.invalidLabel->show(); - } -} - -void ShiftImageTimesDialog::timeEditChanged(const QTime &time) -{ - m_amount = time.hour() * 3600 + time.minute() * 60; - if (ui.backwards->isChecked()) - m_amount *= -1; - updateInvalid(); -} - -URLDialog::URLDialog(QWidget *parent) : QDialog(parent) -{ - ui.setupUi(this); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -QString URLDialog::url() const -{ - return ui.urlField->toPlainText(); -} - -bool isGnome3Session() -{ -#if defined(QT_OS_WIW) || defined(QT_OS_MAC) - return false; -#else - if (qApp->style()->objectName() != "gtk+") - return false; - QProcess p; - p.start("pidof", QStringList() << "gnome-shell"); - p.waitForFinished(-1); - QString p_stdout = p.readAllStandardOutput(); - return !p_stdout.isEmpty(); -#endif -} - -DateWidget::DateWidget(QWidget *parent) : QWidget(parent), - calendarWidget(new QCalendarWidget()) -{ - setDate(QDate::currentDate()); - setMinimumSize(QSize(80, 64)); - setFocusPolicy(Qt::StrongFocus); - calendarWidget->setWindowFlags(Qt::FramelessWindowHint); - calendarWidget->setFirstDayOfWeek(getLocale().firstDayOfWeek()); - calendarWidget->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader); - - connect(calendarWidget, SIGNAL(activated(QDate)), calendarWidget, SLOT(hide())); - connect(calendarWidget, SIGNAL(clicked(QDate)), calendarWidget, SLOT(hide())); - connect(calendarWidget, SIGNAL(activated(QDate)), this, SLOT(setDate(QDate))); - connect(calendarWidget, SIGNAL(clicked(QDate)), this, SLOT(setDate(QDate))); - calendarWidget->installEventFilter(this); -} - -bool DateWidget::eventFilter(QObject *object, QEvent *event) -{ - if (event->type() == QEvent::FocusOut) { - calendarWidget->hide(); - return true; - } - if (event->type() == QEvent::KeyPress) { - QKeyEvent *ev = static_cast(event); - if (ev->key() == Qt::Key_Escape) { - calendarWidget->hide(); - } - } - return QObject::eventFilter(object, event); -} - - -void DateWidget::setDate(const QDate &date) -{ - mDate = date; - update(); - emit dateChanged(mDate); -} - -QDate DateWidget::date() const -{ - return mDate; -} - -void DateWidget::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::EnabledChange) { - update(); - } -} - -#define DATEWIDGETWIDTH 80 -void DateWidget::paintEvent(QPaintEvent *event) -{ - static QPixmap pix = QPixmap(":/calendar").scaled(DATEWIDGETWIDTH, 64); - - QPainter painter(this); - - painter.drawPixmap(QPoint(0, 0), isEnabled() ? pix : QPixmap::fromImage(grayImage(pix.toImage()))); - - QString month = mDate.toString("MMM"); - QString year = mDate.toString("yyyy"); - QString day = mDate.toString("dd"); - - QFont font = QFont("monospace", 10); - QFontMetrics metrics = QFontMetrics(font); - painter.setFont(font); - painter.setPen(QPen(QBrush(Qt::white), 0)); - painter.setBrush(QBrush(Qt::white)); - painter.drawText(QPoint(6, metrics.height() + 1), month); - painter.drawText(QPoint(DATEWIDGETWIDTH - metrics.width(year) - 6, metrics.height() + 1), year); - - font.setPointSize(14); - metrics = QFontMetrics(font); - painter.setPen(QPen(QBrush(Qt::black), 0)); - painter.setBrush(Qt::black); - painter.setFont(font); - painter.drawText(QPoint(DATEWIDGETWIDTH / 2 - metrics.width(day) / 2, 45), day); - - if (hasFocus()) { - QStyleOptionFocusRect option; - option.initFrom(this); - option.backgroundColor = palette().color(QPalette::Background); - style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this); - } -} - -void DateWidget::mousePressEvent(QMouseEvent *event) -{ - calendarWidget->setSelectedDate(mDate); - calendarWidget->move(event->globalPos()); - calendarWidget->show(); - calendarWidget->raise(); - calendarWidget->setFocus(); -} - -void DateWidget::focusInEvent(QFocusEvent *event) -{ - setFocus(); - QWidget::focusInEvent(event); -} - -void DateWidget::focusOutEvent(QFocusEvent *event) -{ - QWidget::focusOutEvent(event); -} - -void DateWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Return || - event->key() == Qt::Key_Enter || - event->key() == Qt::Key_Space) { - calendarWidget->move(mapToGlobal(QPoint(0, 64))); - calendarWidget->show(); - event->setAccepted(true); - } else { - QWidget::keyPressEvent(event); - } -} - -#define COMPONENT_FROM_UI(_component) what->_component = ui._component->isChecked() -#define UI_FROM_COMPONENT(_component) ui._component->setChecked(what->_component) - -DiveComponentSelection::DiveComponentSelection(QWidget *parent, struct dive *target, struct dive_components *_what) : targetDive(target) -{ - ui.setupUi(this); - what = _what; - UI_FROM_COMPONENT(divesite); - UI_FROM_COMPONENT(divemaster); - UI_FROM_COMPONENT(buddy); - UI_FROM_COMPONENT(rating); - UI_FROM_COMPONENT(visibility); - UI_FROM_COMPONENT(notes); - UI_FROM_COMPONENT(suit); - UI_FROM_COMPONENT(tags); - UI_FROM_COMPONENT(cylinders); - UI_FROM_COMPONENT(weights); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -void DiveComponentSelection::buttonClicked(QAbstractButton *button) -{ - if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { - COMPONENT_FROM_UI(divesite); - COMPONENT_FROM_UI(divemaster); - COMPONENT_FROM_UI(buddy); - COMPONENT_FROM_UI(rating); - COMPONENT_FROM_UI(visibility); - COMPONENT_FROM_UI(notes); - COMPONENT_FROM_UI(suit); - COMPONENT_FROM_UI(tags); - COMPONENT_FROM_UI(cylinders); - COMPONENT_FROM_UI(weights); - selective_copy_dive(&displayed_dive, targetDive, *what, true); - } -} - -TagFilter::TagFilter(QWidget *parent) : QWidget(parent) -{ - ui.setupUi(this); - ui.label->setText(tr("Tags: ")); -#if QT_VERSION >= 0x050200 - ui.filterInternalList->setClearButtonEnabled(true); -#endif - QSortFilterProxyModel *filter = new QSortFilterProxyModel(); - filter->setSourceModel(TagFilterModel::instance()); - filter->setFilterCaseSensitivity(Qt::CaseInsensitive); - connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); - ui.filterList->setModel(filter); -} - -void TagFilter::showEvent(QShowEvent *event) -{ - MultiFilterSortModel::instance()->addFilterModel(TagFilterModel::instance()); - QWidget::showEvent(event); -} - -void TagFilter::hideEvent(QHideEvent *event) -{ - MultiFilterSortModel::instance()->removeFilterModel(TagFilterModel::instance()); - QWidget::hideEvent(event); -} - -BuddyFilter::BuddyFilter(QWidget *parent) : QWidget(parent) -{ - ui.setupUi(this); - ui.label->setText(tr("Person: ")); - ui.label->setToolTip(tr("Searches for buddies and divemasters")); -#if QT_VERSION >= 0x050200 - ui.filterInternalList->setClearButtonEnabled(true); -#endif - QSortFilterProxyModel *filter = new QSortFilterProxyModel(); - filter->setSourceModel(BuddyFilterModel::instance()); - filter->setFilterCaseSensitivity(Qt::CaseInsensitive); - connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); - ui.filterList->setModel(filter); -} - -void BuddyFilter::showEvent(QShowEvent *event) -{ - MultiFilterSortModel::instance()->addFilterModel(BuddyFilterModel::instance()); - QWidget::showEvent(event); -} - -void BuddyFilter::hideEvent(QHideEvent *event) -{ - MultiFilterSortModel::instance()->removeFilterModel(BuddyFilterModel::instance()); - QWidget::hideEvent(event); -} - -LocationFilter::LocationFilter(QWidget *parent) : QWidget(parent) -{ - ui.setupUi(this); - ui.label->setText(tr("Location: ")); -#if QT_VERSION >= 0x050200 - ui.filterInternalList->setClearButtonEnabled(true); -#endif - QSortFilterProxyModel *filter = new QSortFilterProxyModel(); - filter->setSourceModel(LocationFilterModel::instance()); - filter->setFilterCaseSensitivity(Qt::CaseInsensitive); - connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); - ui.filterList->setModel(filter); -} - -void LocationFilter::showEvent(QShowEvent *event) -{ - MultiFilterSortModel::instance()->addFilterModel(LocationFilterModel::instance()); - QWidget::showEvent(event); -} - -void LocationFilter::hideEvent(QHideEvent *event) -{ - MultiFilterSortModel::instance()->removeFilterModel(LocationFilterModel::instance()); - QWidget::hideEvent(event); -} - -SuitFilter::SuitFilter(QWidget *parent) : QWidget(parent) -{ - ui.setupUi(this); - ui.label->setText(tr("Suits: ")); -#if QT_VERSION >= 0x050200 - ui.filterInternalList->setClearButtonEnabled(true); -#endif - QSortFilterProxyModel *filter = new QSortFilterProxyModel(); - filter->setSourceModel(SuitsFilterModel::instance()); - filter->setFilterCaseSensitivity(Qt::CaseInsensitive); - connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); - ui.filterList->setModel(filter); -} - -void SuitFilter::showEvent(QShowEvent *event) -{ - MultiFilterSortModel::instance()->addFilterModel(SuitsFilterModel::instance()); - QWidget::showEvent(event); -} - -void SuitFilter::hideEvent(QHideEvent *event) -{ - MultiFilterSortModel::instance()->removeFilterModel(SuitsFilterModel::instance()); - QWidget::hideEvent(event); -} - -MultiFilter::MultiFilter(QWidget *parent) : QWidget(parent) -{ - ui.setupUi(this); - - QWidget *expandedWidget = new QWidget(); - QHBoxLayout *l = new QHBoxLayout(); - - TagFilter *tagFilter = new TagFilter(this); - int minimumHeight = tagFilter->ui.filterInternalList->height() + - tagFilter->ui.verticalLayout->spacing() * tagFilter->ui.verticalLayout->count(); - - QListView *dummyList = new QListView(); - QStringListModel *dummy = new QStringListModel(QStringList() << "Dummy Text"); - dummyList->setModel(dummy); - - connect(ui.close, SIGNAL(clicked(bool)), this, SLOT(closeFilter())); - connect(ui.clear, SIGNAL(clicked(bool)), MultiFilterSortModel::instance(), SLOT(clearFilter())); - connect(ui.maximize, SIGNAL(clicked(bool)), this, SLOT(adjustHeight())); - - l->addWidget(tagFilter); - l->addWidget(new BuddyFilter()); - l->addWidget(new LocationFilter()); - l->addWidget(new SuitFilter()); - l->setContentsMargins(0, 0, 0, 0); - l->setSpacing(0); - expandedWidget->setLayout(l); - - ui.scrollArea->setWidget(expandedWidget); - expandedWidget->resize(expandedWidget->width(), minimumHeight + dummyList->sizeHintForRow(0) * 5 ); - ui.scrollArea->setMinimumHeight(expandedWidget->height() + 5); - - connect(MultiFilterSortModel::instance(), SIGNAL(filterFinished()), this, SLOT(filterFinished())); -} - -void MultiFilter::filterFinished() -{ - ui.filterText->setText(tr("Filter shows %1 (of %2) dives").arg(MultiFilterSortModel::instance()->divesDisplayed).arg(dive_table.nr)); -} - -void MultiFilter::adjustHeight() -{ - ui.scrollArea->setVisible(!ui.scrollArea->isVisible()); -} - -void MultiFilter::closeFilter() -{ - MultiFilterSortModel::instance()->clearFilter(); - hide(); -} diff --git a/qt-ui/simplewidgets.h b/qt-ui/simplewidgets.h deleted file mode 100644 index 595c4cd4b..000000000 --- a/qt-ui/simplewidgets.h +++ /dev/null @@ -1,237 +0,0 @@ -#ifndef SIMPLEWIDGETS_H -#define SIMPLEWIDGETS_H - -class MinMaxAvgWidgetPrivate; -class QAbstractButton; -class QNetworkReply; - -#include -#include -#include -#include - -#include "ui_renumber.h" -#include "ui_setpoint.h" -#include "ui_shifttimes.h" -#include "ui_shiftimagetimes.h" -#include "ui_urldialog.h" -#include "ui_divecomponentselection.h" -#include "ui_listfilter.h" -#include "ui_filterwidget.h" -#include "exif.h" -#include - - -class MinMaxAvgWidget : public QWidget { - Q_OBJECT - Q_PROPERTY(double minimum READ minimum WRITE setMinimum) - Q_PROPERTY(double maximum READ maximum WRITE setMaximum) - Q_PROPERTY(double average READ average WRITE setAverage) -public: - MinMaxAvgWidget(QWidget *parent); - ~MinMaxAvgWidget(); - double minimum() const; - double maximum() const; - double average() const; - void setMinimum(double minimum); - void setMaximum(double maximum); - void setAverage(double average); - void setMinimum(const QString &minimum); - void setMaximum(const QString &maximum); - void setAverage(const QString &average); - void overrideMinToolTipText(const QString &newTip); - void overrideAvgToolTipText(const QString &newTip); - void overrideMaxToolTipText(const QString &newTip); - void clear(); - -private: - QScopedPointer d; -}; - -class RenumberDialog : public QDialog { - Q_OBJECT -public: - static RenumberDialog *instance(); - void renumberOnlySelected(bool selected = true); -private -slots: - void buttonClicked(QAbstractButton *button); - -private: - explicit RenumberDialog(QWidget *parent); - Ui::RenumberDialog ui; - bool selectedOnly; -}; - -class SetpointDialog : public QDialog { - Q_OBJECT -public: - static SetpointDialog *instance(); - void setpointData(struct divecomputer *divecomputer, int time); -private -slots: - void buttonClicked(QAbstractButton *button); - -private: - explicit SetpointDialog(QWidget *parent); - Ui::SetpointDialog ui; - struct divecomputer *dc; - int time; -}; - -class ShiftTimesDialog : public QDialog { - Q_OBJECT -public: - static ShiftTimesDialog *instance(); - void showEvent(QShowEvent *event); -private -slots: - void buttonClicked(QAbstractButton *button); - void changeTime(); - -private: - explicit ShiftTimesDialog(QWidget *parent); - int64_t when; - Ui::ShiftTimesDialog ui; -}; - -class ShiftImageTimesDialog : public QDialog { - Q_OBJECT -public: - explicit ShiftImageTimesDialog(QWidget *parent, QStringList fileNames); - time_t amount() const; - void setOffset(time_t offset); - bool matchAll(); -private -slots: - void buttonClicked(QAbstractButton *button); - void syncCameraClicked(); - void dcDateTimeChanged(const QDateTime &); - void timeEditChanged(const QTime &time); - void updateInvalid(); - void matchAllImagesToggled(bool); - -private: - QStringList fileNames; - Ui::ShiftImageTimesDialog ui; - time_t m_amount; - time_t dcImageEpoch; - bool matchAllImages; -}; - -class URLDialog : public QDialog { - Q_OBJECT -public: - explicit URLDialog(QWidget *parent); - QString url() const; -private: - Ui::URLDialog ui; -}; - -class QCalendarWidget; - -class DateWidget : public QWidget { - Q_OBJECT -public: - DateWidget(QWidget *parent = 0); - QDate date() const; -public -slots: - void setDate(const QDate &date); - -protected: - void paintEvent(QPaintEvent *event); - void mousePressEvent(QMouseEvent *event); - void focusInEvent(QFocusEvent *); - void focusOutEvent(QFocusEvent *); - void keyPressEvent(QKeyEvent *); - void changeEvent(QEvent *); - bool eventFilter(QObject *, QEvent *); -signals: - void dateChanged(const QDate &date); - -private: - QDate mDate; - QCalendarWidget *calendarWidget; -}; - -class DiveComponentSelection : public QDialog { - Q_OBJECT -public: - explicit DiveComponentSelection(QWidget *parent, struct dive *target, struct dive_components *_what); -private -slots: - void buttonClicked(QAbstractButton *button); - -private: - Ui::DiveComponentSelectionDialog ui; - struct dive *targetDive; - struct dive_components *what; -}; - -namespace Ui{ - class FilterWidget2; -}; - -class MultiFilter : public QWidget { - Q_OBJECT -public -slots: - void closeFilter(); - void adjustHeight(); - void filterFinished(); - -public: - MultiFilter(QWidget *parent); - Ui::FilterWidget2 ui; -}; - -class TagFilter : public QWidget { - Q_OBJECT -public: - TagFilter(QWidget *parent = 0); - virtual void showEvent(QShowEvent *); - virtual void hideEvent(QHideEvent *); - -private: - Ui::FilterWidget ui; - friend class MultiFilter; -}; - -class BuddyFilter : public QWidget { - Q_OBJECT -public: - BuddyFilter(QWidget *parent = 0); - virtual void showEvent(QShowEvent *); - virtual void hideEvent(QHideEvent *); - -private: - Ui::FilterWidget ui; -}; - -class SuitFilter : public QWidget { - Q_OBJECT -public: - SuitFilter(QWidget *parent = 0); - virtual void showEvent(QShowEvent *); - virtual void hideEvent(QHideEvent *); - -private: - Ui::FilterWidget ui; -}; - -class LocationFilter : public QWidget { - Q_OBJECT -public: - LocationFilter(QWidget *parent = 0); - virtual void showEvent(QShowEvent *); - virtual void hideEvent(QHideEvent *); - -private: - Ui::FilterWidget ui; -}; - -bool isGnome3Session(); -QImage grayImage(const QImage &coloredImg); - -#endif // SIMPLEWIDGETS_H diff --git a/qt-ui/socialnetworks.cpp b/qt-ui/socialnetworks.cpp deleted file mode 100644 index 6e191267a..000000000 --- a/qt-ui/socialnetworks.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include "socialnetworks.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "mainwindow.h" -#include "profile/profilewidget2.h" -#include "pref.h" -#include "helpers.h" -#include "ui_socialnetworksdialog.h" - -#if SAVE_FB_CREDENTIALS -#define GET_TXT(name, field) \ - v = s.value(QString(name)); \ - if (v.isValid()) \ - prefs.field = strdup(v.toString().toUtf8().constData()); \ - else \ - prefs.field = default_prefs.field -#endif - -FacebookManager *FacebookManager::instance() -{ - static FacebookManager *self = new FacebookManager(); - return self; -} - -FacebookManager::FacebookManager(QObject *parent) : QObject(parent) -{ - sync(); -} - -QUrl FacebookManager::connectUrl() { - return QUrl("https://www.facebook.com/dialog/oauth?" - "client_id=427722490709000" - "&redirect_uri=http://www.facebook.com/connect/login_success.html" - "&response_type=token,granted_scopes" - "&display=popup" - "&scope=publish_actions,user_photos" - ); -} - -bool FacebookManager::loggedIn() { - return prefs.facebook.access_token != NULL; -} - -void FacebookManager::sync() -{ -#if SAVE_FB_CREDENTIALS - QSettings s; - s.beginGroup("WebApps"); - s.beginGroup("Facebook"); - - QVariant v; - GET_TXT("ConnectToken", facebook.access_token); - GET_TXT("UserId", facebook.user_id); - GET_TXT("AlbumId", facebook.album_id); -#endif -} - -void FacebookManager::tryLogin(const QUrl& loginResponse) -{ - QString result = loginResponse.toString(); - if (!result.contains("access_token")) - return; - - if (result.contains("denied_scopes=publish_actions") || result.contains("denied_scopes=user_photos")) { - qDebug() << "user did not allow us access" << result; - return; - } - int from = result.indexOf("access_token=") + strlen("access_token="); - int to = result.indexOf("&expires_in"); - QString securityToken = result.mid(from, to-from); - -#if SAVE_FB_CREDENTIALS - QSettings settings; - settings.beginGroup("WebApps"); - settings.beginGroup("Facebook"); - settings.setValue("ConnectToken", securityToken); - sync(); -#else - prefs.facebook.access_token = copy_string(securityToken.toUtf8().data()); -#endif - requestUserId(); - sync(); - emit justLoggedIn(true); -} - -void FacebookManager::logout() -{ -#if SAVE_FB_CREDENTIALS - QSettings settings; - settings.beginGroup("WebApps"); - settings.beginGroup("Facebook"); - settings.remove("ConnectToken"); - settings.remove("UserId"); - settings.remove("AlbumId"); - sync(); -#else - free(prefs.facebook.access_token); - free(prefs.facebook.album_id); - free(prefs.facebook.user_id); - prefs.facebook.access_token = NULL; - prefs.facebook.album_id = NULL; - prefs.facebook.user_id = NULL; -#endif - emit justLoggedOut(true); -} - -void FacebookManager::requestAlbumId() -{ - QUrl albumListUrl("https://graph.facebook.com/me/albums?access_token=" + QString(prefs.facebook.access_token)); - QNetworkAccessManager *manager = new QNetworkAccessManager(); - QNetworkReply *reply = manager->get(QNetworkRequest(albumListUrl)); - - QEventLoop loop; - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - -#if SAVE_FB_CREDENTIALS - QSettings s; - s.beginGroup("WebApps"); - s.beginGroup("Facebook"); -#endif - - QJsonDocument albumsDoc = QJsonDocument::fromJson(reply->readAll()); - QJsonArray albumObj = albumsDoc.object().value("data").toArray(); - foreach(const QJsonValue &v, albumObj){ - QJsonObject obj = v.toObject(); - if (obj.value("name").toString() == albumName) { -#if SAVE_FB_CREDENTIALS - s.setValue("AlbumId", obj.value("id").toString()); -#else - prefs.facebook.album_id = copy_string(obj.value("id").toString().toUtf8().data()); -#endif - return; - } - } - - QUrlQuery params; - params.addQueryItem("name", albumName ); - params.addQueryItem("description", "Subsurface Album"); - params.addQueryItem("privacy", "{'value': 'SELF'}"); - - QNetworkRequest request(albumListUrl); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); - reply = manager->post(request, params.query().toLocal8Bit()); - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - - albumsDoc = QJsonDocument::fromJson(reply->readAll()); - QJsonObject album = albumsDoc.object(); - if (album.contains("id")) { -#if SAVE_FB_CREDENTIALS - s.setValue("AlbumId", album.value("id").toString()); -#else - prefs.facebook.album_id = copy_string(album.value("id").toString().toUtf8().data()); -#endif - sync(); - return; - } -} - -void FacebookManager::requestUserId() -{ - QUrl userIdRequest("https://graph.facebook.com/me?fields=id&access_token=" + QString(prefs.facebook.access_token)); - QNetworkAccessManager *getUserID = new QNetworkAccessManager(); - QNetworkReply *reply = getUserID->get(QNetworkRequest(userIdRequest)); - - QEventLoop loop; - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - - QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); - QJsonObject obj = jsonDoc.object(); - if (obj.keys().contains("id")){ -#if SAVE_FB_CREDENTIALS - QSettings s; - s.beginGroup("WebApps"); - s.beginGroup("Facebook"); - s.setValue("UserId", obj.value("id").toVariant()); -#else - prefs.facebook.user_id = copy_string(obj.value("id").toString().toUtf8().data()); -#endif - return; - } -} - -void FacebookManager::setDesiredAlbumName(const QString& a) -{ - albumName = a; -} - -/* to be changed to export the currently selected dive as shown on the profile. - * Much much easier, and its also good to people do not select all the dives - * and send erroniously *all* of them to facebook. */ -void FacebookManager::sendDive() -{ - SocialNetworkDialog dialog(qApp->activeWindow()); - if (dialog.exec() != QDialog::Accepted) - return; - - setDesiredAlbumName(dialog.album()); - requestAlbumId(); - - ProfileWidget2 *profile = MainWindow::instance()->graphics(); - profile->setToolTipVisibile(false); - QPixmap pix = QPixmap::grabWidget(profile); - profile->setToolTipVisibile(true); - QByteArray bytes; - QBuffer buffer(&bytes); - buffer.open(QIODevice::WriteOnly); - pix.save(&buffer, "PNG"); - QUrl url("https://graph.facebook.com/v2.2/" + QString(prefs.facebook.album_id) + "/photos?" + - "&access_token=" + QString(prefs.facebook.access_token) + - "&source=image" + - "&message=" + dialog.text().replace(""", "%22")); - - QNetworkAccessManager *am = new QNetworkAccessManager(this); - QNetworkRequest request(url); - - QString bound="margin"; - - //according to rfc 1867 we need to put this string here: - QByteArray data(QString("--" + bound + "\r\n").toLocal8Bit()); - data.append("Content-Disposition: form-data; name=\"action\"\r\n\r\n"); - data.append("https://graph.facebook.com/v2.2/\r\n"); - data.append("--" + bound + "\r\n"); //according to rfc 1867 - data.append("Content-Disposition: form-data; name=\"uploaded\"; filename=\"" + QString::number(qrand()) + ".png\"\r\n"); //name of the input is "uploaded" in my form, next one is a file name. - data.append("Content-Type: image/jpeg\r\n\r\n"); //data type - data.append(bytes); //let's read the file - data.append("\r\n"); - data.append("--" + bound + "--\r\n"); //closing boundary according to rfc 1867 - - request.setRawHeader(QString("Content-Type").toLocal8Bit(),QString("multipart/form-data; boundary=" + bound).toLocal8Bit()); - request.setRawHeader(QString("Content-Length").toLocal8Bit(), QString::number(data.length()).toLocal8Bit()); - QNetworkReply *reply = am->post(request,data); - - QEventLoop loop; - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - - QByteArray response = reply->readAll(); - QJsonDocument jsonDoc = QJsonDocument::fromJson(response); - QJsonObject obj = jsonDoc.object(); - if (obj.keys().contains("id")){ - QMessageBox::information(qApp->activeWindow(), - tr("Photo upload sucessfull"), - tr("Your dive profile was updated to Facebook."), - QMessageBox::Ok); - } else { - QMessageBox::information(qApp->activeWindow(), - tr("Photo upload failed"), - tr("Your dive profile was not updated to Facebook, \n " - "please send the following to the developer. \n" - + response), - QMessageBox::Ok); - } -} - -SocialNetworkDialog::SocialNetworkDialog(QWidget *parent) : - QDialog(parent), - ui( new Ui::SocialnetworksDialog()) -{ - ui->setupUi(this); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - connect(ui->date, SIGNAL(clicked()), this, SLOT(selectionChanged())); - connect(ui->duration, SIGNAL(clicked()), this, SLOT(selectionChanged())); - connect(ui->Buddy, SIGNAL(clicked()), this, SLOT(selectionChanged())); - connect(ui->Divemaster, SIGNAL(clicked()), this, SLOT(selectionChanged())); - connect(ui->Location, SIGNAL(clicked()), this, SLOT(selectionChanged())); - connect(ui->Notes, SIGNAL(clicked()), this, SLOT(selectionChanged())); - connect(ui->album, SIGNAL(textChanged(QString)), this, SLOT(albumChanged())); -} - -void SocialNetworkDialog::albumChanged() -{ - QAbstractButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); - button->setEnabled(!ui->album->text().isEmpty()); -} - -void SocialNetworkDialog::selectionChanged() -{ - struct dive *d = current_dive; - QString fullText; - if (ui->date->isChecked()) { - fullText += tr("Dive date: %1 \n").arg(get_short_dive_date_string(d->when)); - } - if (ui->duration->isChecked()) { - fullText += tr("Duration: %1 \n").arg(get_dive_duration_string(d->duration.seconds, - tr("h:", "abbreviation for hours plus separator"), - tr("min", "abbreviation for minutes"))); - } - if (ui->Location->isChecked()) { - fullText += tr("Dive location: %1 \n").arg(get_dive_location(d)); - } - if (ui->Buddy->isChecked()) { - fullText += tr("Buddy: %1 \n").arg(d->buddy); - } - if (ui->Divemaster->isChecked()) { - fullText += tr("Divemaster: %1 \n").arg(d->divemaster); - } - if (ui->Notes->isChecked()) { - fullText += tr("\n%1").arg(d->notes); - } - ui->text->setPlainText(fullText); -} - -QString SocialNetworkDialog::text() const { - return ui->text->toPlainText().toHtmlEscaped(); -} - -QString SocialNetworkDialog::album() const { - return ui->album->text().toHtmlEscaped(); -} diff --git a/qt-ui/socialnetworks.h b/qt-ui/socialnetworks.h deleted file mode 100644 index 2f63915ca..000000000 --- a/qt-ui/socialnetworks.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef FACEBOOKMANAGER_H -#define FACEBOOKMANAGER_H - -#include -#include -#include - -class FacebookManager : public QObject -{ - Q_OBJECT -public: - static FacebookManager *instance(); - void requestAlbumId(); - void requestUserId(); - void sync(); - QUrl connectUrl(); - bool loggedIn(); -signals: - void justLoggedIn(bool triggererd); - void justLoggedOut(bool triggered); - -public slots: - void tryLogin(const QUrl& loginResponse); - void logout(); - void setDesiredAlbumName(const QString& albumName); - void sendDive(); - -private: - explicit FacebookManager(QObject *parent = 0); - QString albumName; -}; - -namespace Ui { - class SocialnetworksDialog; -} - -class SocialNetworkDialog : public QDialog { - Q_OBJECT -public: - SocialNetworkDialog(QWidget *parent); - QString text() const; - QString album() const; -public slots: - void selectionChanged(); - void albumChanged(); -private: - Ui::SocialnetworksDialog *ui; -}; -#endif // FACEBOOKMANAGER_H diff --git a/qt-ui/socialnetworksdialog.ui b/qt-ui/socialnetworksdialog.ui deleted file mode 100644 index e8953d1c7..000000000 --- a/qt-ui/socialnetworksdialog.ui +++ /dev/null @@ -1,184 +0,0 @@ - - - SocialnetworksDialog - - - - 0 - 0 - 475 - 416 - - - - Dialog - - - - - 290 - 380 - 166 - 22 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 10 - 15 - 451 - 361 - - - - - 1 - - - 1 - - - 1 - - - 1 - - - - - The text to the right will be posted as the description with your profile picture to Facebook. The album name is required (the profile picture will be posted to that album). - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - Album - - - - - - - The profile picture will be posted in this album (required) - - - - - - - Include - - - - - - - Date and time - - - - - - - Duration - - - - - - - Location - - - - - - - Divemaster - - - - - - - Buddy - - - - - - - Notes - - - - - - - - 75 - true - - - - Facebook post preview - - - - - - - - - - - - - buttonBox - accepted() - SocialnetworksDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - SocialnetworksDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/starwidget.cpp b/qt-ui/starwidget.cpp deleted file mode 100644 index d959ed3b9..000000000 --- a/qt-ui/starwidget.cpp +++ /dev/null @@ -1,164 +0,0 @@ -#include "starwidget.h" -#include "metrics.h" -#include -#include -#include "simplewidgets.h" - -QImage StarWidget::activeStar; -QImage StarWidget::inactiveStar; - -const QImage& StarWidget::starActive() -{ - return activeStar; -} - -const QImage& StarWidget::starInactive() -{ - return inactiveStar; -} - -QImage focusedImage(const QImage& coloredImg) -{ - QImage img = coloredImg; - for (int i = 0; i < img.width(); ++i) { - for (int j = 0; j < img.height(); ++j) { - QRgb rgb = img.pixel(i, j); - if (!rgb) - continue; - - QColor c(rgb); - c = c.dark(); - img.setPixel(i, j, c.rgb()); - } - } - - return img; -} - - -int StarWidget::currentStars() const -{ - return current; -} - -void StarWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (readOnly) { - return; - } - - int starClicked = event->pos().x() / defaultIconMetrics().sz_small + 1; - if (starClicked > TOTALSTARS) - starClicked = TOTALSTARS; - - if (current == starClicked) - current -= 1; - else - current = starClicked; - - Q_EMIT valueChanged(current); - update(); -} - -void StarWidget::paintEvent(QPaintEvent *event) -{ - QPainter p(this); - QImage star = hasFocus() ? focusedImage(starActive()) : starActive(); - QPixmap selected = QPixmap::fromImage(star); - QPixmap inactive = QPixmap::fromImage(starInactive()); - const IconMetrics& metrics = defaultIconMetrics(); - - - for (int i = 0; i < current; i++) - p.drawPixmap(i * metrics.sz_small + metrics.spacing, 0, selected); - - for (int i = current; i < TOTALSTARS; i++) - p.drawPixmap(i * metrics.sz_small + metrics.spacing, 0, inactive); - - if (hasFocus()) { - QStyleOptionFocusRect option; - option.initFrom(this); - option.backgroundColor = palette().color(QPalette::Background); - style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &p, this); - } -} - -void StarWidget::setCurrentStars(int value) -{ - current = value; - update(); - Q_EMIT valueChanged(current); -} - -StarWidget::StarWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), - current(0), - readOnly(false) -{ - int dim = defaultIconMetrics().sz_small; - - if (activeStar.isNull()) { - QSvgRenderer render(QString(":star")); - QPixmap renderedStar(dim, dim); - - renderedStar.fill(Qt::transparent); - QPainter painter(&renderedStar); - - render.render(&painter, QRectF(0, 0, dim, dim)); - activeStar = renderedStar.toImage(); - } - if (inactiveStar.isNull()) { - inactiveStar = grayImage(activeStar); - } - setFocusPolicy(Qt::StrongFocus); -} - -QImage grayImage(const QImage& coloredImg) -{ - QImage img = coloredImg; - for (int i = 0; i < img.width(); ++i) { - for (int j = 0; j < img.height(); ++j) { - QRgb rgb = img.pixel(i, j); - if (!rgb) - continue; - - QColor c(rgb); - int gray = 204 + (c.red() + c.green() + c.blue()) / 15; - img.setPixel(i, j, qRgb(gray, gray, gray)); - } - } - - return img; -} - -QSize StarWidget::sizeHint() const -{ - const IconMetrics& metrics = defaultIconMetrics(); - return QSize(metrics.sz_small * TOTALSTARS + metrics.spacing * (TOTALSTARS - 1), metrics.sz_small); -} - -void StarWidget::setReadOnly(bool r) -{ - readOnly = r; -} - -void StarWidget::focusInEvent(QFocusEvent *event) -{ - setFocus(); - QWidget::focusInEvent(event); -} - -void StarWidget::focusOutEvent(QFocusEvent *event) -{ - QWidget::focusOutEvent(event); -} - -void StarWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Right) { - if (currentStars() < TOTALSTARS) - setCurrentStars(currentStars() + 1); - } else if (event->key() == Qt::Key_Down || event->key() == Qt::Key_Left) { - if (currentStars() > 0) - setCurrentStars(currentStars() - 1); - } -} diff --git a/qt-ui/starwidget.h b/qt-ui/starwidget.h deleted file mode 100644 index 989aa527d..000000000 --- a/qt-ui/starwidget.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef STARWIDGET_H -#define STARWIDGET_H - -#include - -enum StarConfig { - TOTALSTARS = 5 -}; - -class StarWidget : public QWidget { - Q_OBJECT -public: - explicit StarWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); - int currentStars() const; - - /*reimp*/ QSize sizeHint() const; - - static const QImage& starActive(); - static const QImage& starInactive(); - -signals: - void valueChanged(int stars); - -public -slots: - void setCurrentStars(int value); - void setReadOnly(bool readOnly); - -protected: - /*reimp*/ void mouseReleaseEvent(QMouseEvent *); - /*reimp*/ void paintEvent(QPaintEvent *); - /*reimp*/ void focusInEvent(QFocusEvent *); - /*reimp*/ void focusOutEvent(QFocusEvent *); - /*reimp*/ void keyPressEvent(QKeyEvent *); - -private: - int current; - bool readOnly; - - static QImage activeStar; - static QImage inactiveStar; -}; - -#endif // STARWIDGET_H diff --git a/qt-ui/statistics/monthstatistics.cpp b/qt-ui/statistics/monthstatistics.cpp deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/statistics/monthstatistics.h b/qt-ui/statistics/monthstatistics.h deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/statistics/statisticsbar.cpp b/qt-ui/statistics/statisticsbar.cpp deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/statistics/statisticsbar.h b/qt-ui/statistics/statisticsbar.h deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/statistics/statisticswidget.cpp b/qt-ui/statistics/statisticswidget.cpp deleted file mode 100644 index 3e91fa317..000000000 --- a/qt-ui/statistics/statisticswidget.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "statisticswidget.h" -#include "yearlystatisticsmodel.h" -#include - -YearlyStatisticsWidget::YearlyStatisticsWidget(QWidget *parent): - QGraphicsView(parent), - m_model(NULL) -{ -} - -void YearlyStatisticsWidget::setModel(YearlyStatisticsModel *m) -{ - m_model = m; - connect(m, SIGNAL(dataChanged(QModelIndex,QModelIndex)), - this, SLOT(modelDataChanged(QModelIndex,QModelIndex))); - connect(m, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), - scene(), SLOT(clear())); - connect(m, SIGNAL(rowsInserted(QModelIndex,int,int)), - this, SLOT(modelRowsInserted(QModelIndex,int,int))); - - modelRowsInserted(QModelIndex(),0,m_model->rowCount()-1); -} - -void YearlyStatisticsWidget::modelRowsInserted(const QModelIndex &index, int first, int last) -{ - // stub -} - -void YearlyStatisticsWidget::modelDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight) -{ - Q_UNUSED(topLeft); - Q_UNUSED(bottomRight); - scene()->clear(); - modelRowsInserted(QModelIndex(),0,m_model->rowCount()-1); -} - -void YearlyStatisticsWidget::resizeEvent(QResizeEvent *event) -{ - QGraphicsView::resizeEvent(event); - fitInView(sceneRect(), Qt::IgnoreAspectRatio); -} diff --git a/qt-ui/statistics/statisticswidget.h b/qt-ui/statistics/statisticswidget.h deleted file mode 100644 index ae988292d..000000000 --- a/qt-ui/statistics/statisticswidget.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef YEARLYSTATISTICSWIDGET_H -#define YEARLYSTATISTICSWIDGET_H - -#include - -class YearlyStatisticsModel; -class QModelIndex; - -class YearlyStatisticsWidget : public QGraphicsView { - Q_OBJECT -public: - YearlyStatisticsWidget(QWidget *parent = 0); - void setModel(YearlyStatisticsModel *m); -protected: - virtual void resizeEvent(QResizeEvent *event); -public slots: - void modelRowsInserted(const QModelIndex& index, int first, int last); - void modelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); -private: - YearlyStatisticsModel *m_model; -}; - -#endif \ No newline at end of file diff --git a/qt-ui/statistics/yearstatistics.cpp b/qt-ui/statistics/yearstatistics.cpp deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/statistics/yearstatistics.h b/qt-ui/statistics/yearstatistics.h deleted file mode 100644 index e69de29bb..000000000 diff --git a/qt-ui/subsurfacewebservices.cpp b/qt-ui/subsurfacewebservices.cpp deleted file mode 100644 index ee079cc48..000000000 --- a/qt-ui/subsurfacewebservices.cpp +++ /dev/null @@ -1,1121 +0,0 @@ -#include "subsurfacewebservices.h" -#include "helpers.h" -#include "webservice.h" -#include "mainwindow.h" -#include "usersurvey.h" -#include "divelist.h" -#include "globe.h" -#include "maintab.h" -#include "display.h" -#include "membuffer.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef Q_OS_UNIX -#include // for dup(2) -#endif - -#include - -#ifndef PATH_MAX -#define PATH_MAX 4096 -#endif - -struct dive_table gps_location_table; - -// we don't overwrite any existing GPS info in the dive -// so get the dive site and if there is none or there is one without GPS fix, add it -static void copy_gps_location(struct dive *from, struct dive *to) -{ - struct dive_site *ds = get_dive_site_for_dive(to); - if (!ds || !dive_site_has_gps_location(ds)) { - struct dive_site *gds = get_dive_site_for_dive(from); - if (!ds) { - // simply link to the one created for the fake dive - to->dive_site_uuid = gds->uuid; - } else { - ds->latitude = gds->latitude; - ds->longitude = gds->longitude; - if (same_string(ds->name, "")) - ds->name = copy_string(gds->name); - } - } -} - -#define SAME_GROUP 6 * 3600 // six hours -//TODO: C Code. static functions are not good if we plan to have a test for them. -static bool merge_locations_into_dives(void) -{ - int i, j, tracer=0, changed=0; - struct dive *gpsfix, *nextgpsfix, *dive; - - sort_table(&gps_location_table); - - for_each_dive (i, dive) { - if (!dive_has_gps_location(dive)) { - for (j = tracer; (gpsfix = get_dive_from_table(j, &gps_location_table)) !=NULL; j++) { - if (time_during_dive_with_offset(dive, gpsfix->when, SAME_GROUP)) { - if (verbose) - qDebug() << "processing gpsfix @" << get_dive_date_string(gpsfix->when) << - "which is withing six hours of dive from" << - get_dive_date_string(dive->when) << "until" << - get_dive_date_string(dive->when + dive->duration.seconds); - /* - * If position is fixed during dive. This is the good one. - * Asign and mark position, and end gps_location loop - */ - if (time_during_dive_with_offset(dive, gpsfix->when, 0)) { - if (verbose) - qDebug() << "gpsfix is during the dive, pick that one"; - copy_gps_location(gpsfix, dive); - changed++; - tracer = j; - break; - } else { - /* - * If it is not, check if there are more position fixes in SAME_GROUP range - */ - if ((nextgpsfix = get_dive_from_table(j + 1, &gps_location_table)) && - time_during_dive_with_offset(dive, nextgpsfix->when, SAME_GROUP)) { - if (verbose) - qDebug() << "look at the next gps fix @" << get_dive_date_string(nextgpsfix->when); - /* first let's test if this one is during the dive */ - if (time_during_dive_with_offset(dive, nextgpsfix->when, 0)) { - if (verbose) - qDebug() << "which is during the dive, pick that one"; - copy_gps_location(nextgpsfix, dive); - changed++; - tracer = j + 1; - break; - } - /* we know the gps fixes are sorted; if they are both before the dive, ignore the first, - * if theay are both after the dive, take the first, - * if the first is before and the second is after, take the closer one */ - if (nextgpsfix->when < dive->when) { - if (verbose) - qDebug() << "which is closer to the start of the dive, do continue with that"; - continue; - } else if (gpsfix->when > dive->when + dive->duration.seconds) { - if (verbose) - qDebug() << "which is even later after the end of the dive, so pick the previous one"; - copy_gps_location(gpsfix, dive); - changed++; - tracer = j; - break; - } else { - /* ok, gpsfix is before, nextgpsfix is after */ - if (dive->when - gpsfix->when <= nextgpsfix->when - (dive->when + dive->duration.seconds)) { - if (verbose) - qDebug() << "pick the one before as it's closer to the start"; - copy_gps_location(gpsfix, dive); - changed++; - tracer = j; - break; - } else { - if (verbose) - qDebug() << "pick the one after as it's closer to the start"; - copy_gps_location(nextgpsfix, dive); - changed++; - tracer = j + 1; - break; - } - } - /* - * If no more positions in range, the actual is the one. Asign, mark and end loop. - */ - } else { - if (verbose) - qDebug() << "which seems to be the best one for this dive, so pick it"; - copy_gps_location(gpsfix, dive); - changed++; - tracer = j; - break; - } - } - } else { - /* If position is out of SAME_GROUP range and in the future, mark position for - * next dive iteration and end the gps_location loop - */ - if (gpsfix->when >= dive->when + dive->duration.seconds + SAME_GROUP) { - tracer = j; - break; - } - } - } - } - } - return changed > 0; -} - -// TODO: This looks like should be ported to C code. or a big part of it. -bool DivelogsDeWebServices::prepare_dives_for_divelogs(const QString &tempfile, const bool selected) -{ - static const char errPrefix[] = "divelog.de-upload:"; - if (!amount_selected) { - report_error(tr("no dives were selected").toUtf8()); - return false; - } - - xsltStylesheetPtr xslt = NULL; - struct zip *zip; - - xslt = get_stylesheet("divelogs-export.xslt"); - if (!xslt) { - qDebug() << errPrefix << "missing stylesheet"; - report_error(tr("stylesheet to export to divelogs.de is not found").toUtf8()); - return false; - } - - - int error_code; - zip = zip_open(QFile::encodeName(QDir::toNativeSeparators(tempfile)), ZIP_CREATE, &error_code); - if (!zip) { - char buffer[1024]; - zip_error_to_str(buffer, sizeof buffer, error_code, errno); - report_error(tr("failed to create zip file for upload: %s").toUtf8(), buffer); - return false; - } - - /* walk the dive list in chronological order */ - int i; - struct dive *dive; - for_each_dive (i, dive) { - FILE *f; - char filename[PATH_MAX]; - int streamsize; - const char *membuf; - xmlDoc *transformed; - struct zip_source *s; - struct membuffer mb = { 0 }; - - /* - * Get the i'th dive in XML format so we can process it. - * We need to save to a file before we can reload it back into memory... - */ - if (selected && !dive->selected) - continue; - /* make sure the buffer is empty and add the dive */ - mb.len = 0; - - struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); - - if (ds) { - put_format(&mb, "latitude.udeg || ds->longitude.udeg) { - put_degrees(&mb, ds->latitude, " gps='", " "); - put_degrees(&mb, ds->longitude, "", "'"); - } - put_format(&mb, "/>\n\n"); - } - - save_one_dive_to_mb(&mb, dive); - - if (ds) { - put_format(&mb, "\n"); - } - membuf = mb_cstring(&mb); - streamsize = strlen(membuf); - /* - * Parse the memory buffer into XML document and - * transform it to divelogs.de format, finally dumping - * the XML into a character buffer. - */ - xmlDoc *doc = xmlReadMemory(membuf, streamsize, "divelog", NULL, 0); - if (!doc) { - qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!"; - report_error(tr("internal error").toUtf8()); - goto error_close_zip; - } - free((void *)membuf); - - transformed = xsltApplyStylesheet(xslt, doc, NULL); - if (!transformed) { - qWarning() << errPrefix << "XSLT transform failed for dive: " << i; - report_error(tr("Conversion of dive %1 to divelogs.de format failed").arg(i).toUtf8()); - continue; - } - xmlDocDumpMemory(transformed, (xmlChar **)&membuf, &streamsize); - xmlFreeDoc(doc); - xmlFreeDoc(transformed); - - /* - * Save the XML document into a zip file. - */ - snprintf(filename, PATH_MAX, "%d.xml", i + 1); - s = zip_source_buffer(zip, membuf, streamsize, 1); - if (s) { - int64_t ret = zip_add(zip, filename, s); - if (ret == -1) - qDebug() << errPrefix << "failed to include dive:" << i; - } - } - xsltFreeStylesheet(xslt); - if (zip_close(zip)) { - int ze, se; -#if LIBZIP_VERSION_MAJOR >= 1 - zip_error_t *error = zip_get_error(zip); - ze = zip_error_code_zip(error); - se = zip_error_code_system(error); -#else - zip_error_get(zip, &ze, &se); -#endif - report_error(qPrintable(tr("error writing zip file: %s zip error %d system error %d - %s")), - qPrintable(QDir::toNativeSeparators(tempfile)), ze, se, zip_strerror(zip)); - return false; - } - return true; - -error_close_zip: - zip_close(zip); - QFile::remove(tempfile); - xsltFreeStylesheet(xslt); - return false; -} - -WebServices::WebServices(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), reply(0) -{ - ui.setupUi(this); - connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); - connect(ui.download, SIGNAL(clicked(bool)), this, SLOT(startDownload())); - connect(ui.upload, SIGNAL(clicked(bool)), this, SLOT(startUpload())); - connect(&timeout, SIGNAL(timeout()), this, SLOT(downloadTimedOut())); - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - timeout.setSingleShot(true); - defaultApplyText = ui.buttonBox->button(QDialogButtonBox::Apply)->text(); - userAgent = getUserAgent(); -} - -void WebServices::hidePassword() -{ - ui.password->hide(); - ui.passLabel->hide(); -} - -void WebServices::hideUpload() -{ - ui.upload->hide(); - ui.download->show(); -} - -void WebServices::hideDownload() -{ - ui.download->hide(); - ui.upload->show(); -} - -QNetworkAccessManager *WebServices::manager() -{ - static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); - return manager; -} - -void WebServices::downloadTimedOut() -{ - if (!reply) - return; - - reply->deleteLater(); - reply = NULL; - resetState(); - ui.status->setText(tr("Operation timed out")); -} - -void WebServices::updateProgress(qint64 current, qint64 total) -{ - if (!reply) - return; - if (total == -1) { - total = INT_MAX / 2 - 1; - } - if (total >= INT_MAX / 2) { - // over a gigabyte! - if (total >= Q_INT64_C(1) << 47) { - total >>= 16; - current >>= 16; - } - total >>= 16; - current >>= 16; - } - ui.progressBar->setRange(0, total); - ui.progressBar->setValue(current); - ui.status->setText(tr("Transferring data...")); - - // reset the timer: 30 seconds after we last got any data - timeout.start(); -} - -void WebServices::connectSignalsForDownload(QNetworkReply *reply) -{ - connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished())); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), - this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, - SLOT(updateProgress(qint64, qint64))); - - timeout.start(30000); // 30s -} - -void WebServices::resetState() -{ - ui.download->setEnabled(true); - ui.upload->setEnabled(true); - ui.userID->setEnabled(true); - ui.password->setEnabled(true); - ui.progressBar->reset(); - ui.progressBar->setRange(0, 1); - ui.status->setText(QString()); - ui.buttonBox->button(QDialogButtonBox::Apply)->setText(defaultApplyText); -} - -// # -// # -// # Subsurface Web Service Implementation. -// # -// # - -SubsurfaceWebServices::SubsurfaceWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f) -{ - QSettings s; - if (!prefs.save_userid_local || !*prefs.userid) - ui.userID->setText(s.value("subsurface_webservice_uid").toString().toUpper()); - else - ui.userID->setText(prefs.userid); - hidePassword(); - hideUpload(); - ui.progressBar->setFormat(tr("Enter User ID and click Download")); - ui.progressBar->setRange(0, 1); - ui.progressBar->setValue(-1); - ui.progressBar->setAlignment(Qt::AlignCenter); - ui.saveUidLocal->setChecked(prefs.save_userid_local); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -void SubsurfaceWebServices::buttonClicked(QAbstractButton *button) -{ - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - switch (ui.buttonBox->buttonRole(button)) { - case QDialogButtonBox::ApplyRole: { - int i; - struct dive *d; - struct dive_site *ds; - bool changed = false; - clear_table(&gps_location_table); - QByteArray url = tr("Webservice").toLocal8Bit(); - parse_xml_buffer(url.data(), downloadedData.data(), downloadedData.length(), &gps_location_table, NULL); - // make sure we mark all the dive sites that were created - for (i = 0; i < gps_location_table.nr; i++) { - d = get_dive_from_table(i, &gps_location_table); - ds = get_dive_site_by_uuid(d->dive_site_uuid); - if (ds) - ds->notes = strdup("SubsurfaceWebservice"); - } - /* now merge the data in the gps_location table into the dive_table */ - if (merge_locations_into_dives()) { - changed = true; - mark_divelist_changed(true); - MainWindow::instance()->information()->updateDiveInfo(); - } - - /* store last entered uid in config */ - QSettings s; - QString qDialogUid = ui.userID->text().toUpper(); - bool qSaveUid = ui.saveUidLocal->checkState(); - set_save_userid_local(qSaveUid); - if (qSaveUid) { - QString qSettingUid = s.value("subsurface_webservice_uid").toString(); - QString qFileUid = QString(prefs.userid); - bool s_eq_d = (qSettingUid == qDialogUid); - bool d_eq_f = (qDialogUid == qFileUid); - if (!d_eq_f || s_eq_d) - s.setValue("subsurface_webservice_uid", qDialogUid); - set_userid(qDialogUid.toLocal8Bit().data()); - } else { - s.setValue("subsurface_webservice_uid", qDialogUid); - } - s.sync(); - hide(); - close(); - resetState(); - /* and now clean up and remove all the extra dive sites that were created */ - QSet usedUuids; - for_each_dive(i, d) { - if (d->dive_site_uuid) - usedUuids.insert(d->dive_site_uuid); - } - for_each_dive_site(i, ds) { - if (!usedUuids.contains(ds->uuid) && same_string(ds->notes, "SubsurfaceWebservice")) { - delete_dive_site(ds->uuid); - i--; // otherwise we skip one site - } - } -#ifndef NO_MARBLE - // finally now that all the extra GPS fixes that weren't used have been deleted - // we can update the globe - if (changed) { - GlobeGPS::instance()->repopulateLabels(); - GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(current_dive->dive_site_uuid)); - } -#endif - - } break; - case QDialogButtonBox::RejectRole: - if (reply != NULL && reply->isOpen()) { - reply->abort(); - delete reply; - reply = NULL; - } - resetState(); - break; - case QDialogButtonBox::HelpRole: - QDesktopServices::openUrl(QUrl("http://api.hohndel.org")); - break; - default: - break; - } -} - -void SubsurfaceWebServices::startDownload() -{ - QUrl url("http://api.hohndel.org/api/dive/get/"); - QUrlQuery query; - query.addQueryItem("login", ui.userID->text().toUpper()); - url.setQuery(query); - - QNetworkRequest request; - request.setUrl(url); - request.setRawHeader("Accept", "text/xml"); - request.setRawHeader("User-Agent", userAgent.toUtf8()); - reply = manager()->get(request); - ui.status->setText(tr("Connecting...")); - ui.progressBar->setEnabled(true); - ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' - ui.download->setEnabled(false); - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - connectSignalsForDownload(reply); -} - -void SubsurfaceWebServices::downloadFinished() -{ - if (!reply) - return; - - ui.progressBar->setRange(0, 1); - ui.progressBar->setValue(1); - ui.progressBar->setFormat("%p%"); - downloadedData = reply->readAll(); - - ui.download->setEnabled(true); - ui.status->setText(tr("Download finished")); - - uint resultCode = download_dialog_parse_response(downloadedData); - setStatusText(resultCode); - if (resultCode == DD_STATUS_OK) { - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); - } - reply->deleteLater(); - reply = NULL; -} - -void SubsurfaceWebServices::downloadError(QNetworkReply::NetworkError) -{ - resetState(); - ui.status->setText(tr("Download error: %1").arg(reply->errorString())); - reply->deleteLater(); - reply = NULL; -} - -void SubsurfaceWebServices::setStatusText(int status) -{ - QString text; - switch (status) { - case DD_STATUS_ERROR_CONNECT: - text = tr("Connection error: "); - break; - case DD_STATUS_ERROR_ID: - text = tr("Invalid user identifier!"); - break; - case DD_STATUS_ERROR_PARSE: - text = tr("Cannot parse response!"); - break; - case DD_STATUS_OK: - text = tr("Download successful"); - break; - } - ui.status->setText(text); -} - -//TODO: C-Code. -/* requires that there is a or tag under the tag */ -void SubsurfaceWebServices::download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status) -{ - xmlNodePtr cur_node; - for (cur_node = node; cur_node; cur_node = cur_node->next) { - if ((!strcmp((const char *)cur_node->name, (const char *)"download")) && - (!strcmp((const char *)xmlNodeGetContent(cur_node), (const char *)"ok"))) { - *download_status = DD_STATUS_OK; - return; - } else if (!strcmp((const char *)cur_node->name, (const char *)"error")) { - *download_status = DD_STATUS_ERROR_ID; - return; - } - } -} - -// TODO: C-Code -unsigned int SubsurfaceWebServices::download_dialog_parse_response(const QByteArray &xml) -{ - xmlNodePtr root; - xmlDocPtr doc = xmlParseMemory(xml.data(), xml.length()); - unsigned int status = DD_STATUS_ERROR_PARSE; - - if (!doc) - return DD_STATUS_ERROR_PARSE; - root = xmlDocGetRootElement(doc); - if (!root) { - status = DD_STATUS_ERROR_PARSE; - goto end; - } - if (root->children) - download_dialog_traverse_xml(root->children, &status); -end: - xmlFreeDoc(doc); - return status; -} - -// # -// # -// # Divelogs DE Web Service Implementation. -// # -// # - -struct DiveListResult { - QString errorCondition; - QString errorDetails; - QByteArray idList; // comma-separated, suitable to be sent in the fetch request - int idCount; -}; - -static DiveListResult parseDiveLogsDeDiveList(const QByteArray &xmlData) -{ - /* XML format seems to be: - * - * - * DD.MM.YYYY hh:mm - * [repeat ] - * - * - */ - QXmlStreamReader reader(xmlData); - const QString invalidXmlError = QObject::tr("Invalid response from server"); - bool seenDiveDates = false; - DiveListResult result; - result.idCount = 0; - - if (reader.readNextStartElement() && reader.name() != "DiveDateReader") { - result.errorCondition = invalidXmlError; - result.errorDetails = - QObject::tr("Expected XML tag 'DiveDateReader', got instead '%1") - .arg(reader.name().toString()); - goto out; - } - - while (reader.readNextStartElement()) { - if (reader.name() != "DiveDates") { - if (reader.name() == "Login") { - QString status = reader.readElementText(); - // qDebug() << "Login status:" << status; - - // Note: there has to be a better way to determine a successful login... - if (status == "failed") { - result.errorCondition = "Login failed"; - goto out; - } - } else { - // qDebug() << "Skipping" << reader.name(); - } - continue; - } - - // process - seenDiveDates = true; - while (reader.readNextStartElement()) { - if (reader.name() != "date") { - // qDebug() << "Skipping" << reader.name(); - continue; - } - QStringRef id = reader.attributes().value("divelogsId"); - // qDebug() << "Found" << reader.name() << "with id =" << id; - if (!id.isEmpty()) { - result.idList += id.toLatin1(); - result.idList += ','; - ++result.idCount; - } - - reader.skipCurrentElement(); - } - } - - // chop the ending comma, if any - result.idList.chop(1); - - if (!seenDiveDates) { - result.errorCondition = invalidXmlError; - result.errorDetails = QObject::tr("Expected XML tag 'DiveDates' not found"); - } - -out: - if (reader.hasError()) { - // if there was an XML error, overwrite the result or other error conditions - result.errorCondition = invalidXmlError; - result.errorDetails = QObject::tr("Malformed XML response. Line %1: %2") - .arg(reader.lineNumber()) - .arg(reader.errorString()); - } - return result; -} - -DivelogsDeWebServices *DivelogsDeWebServices::instance() -{ - static DivelogsDeWebServices *self = new DivelogsDeWebServices(MainWindow::instance()); - self->setAttribute(Qt::WA_QuitOnClose, false); - return self; -} - -void DivelogsDeWebServices::downloadDives() -{ - uploadMode = false; - resetState(); - hideUpload(); - exec(); -} - -void DivelogsDeWebServices::prepareDivesForUpload(bool selected) -{ - /* generate a random filename and create/open that file with zip_open */ - QString filename = QDir::tempPath() + "/import-" + QString::number(qrand() % 99999999) + ".dld"; - if (prepare_dives_for_divelogs(filename, selected)) { - QFile f(filename); - if (f.open(QIODevice::ReadOnly)) { - uploadDives((QIODevice *)&f); - f.close(); - f.remove(); - return; - } else { - report_error("Failed to open upload file %s\n", qPrintable(filename)); - } - } else { - report_error("Failed to create upload file %s\n", qPrintable(filename)); - } - MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); -} - -void DivelogsDeWebServices::uploadDives(QIODevice *dldContent) -{ - QHttpMultiPart mp(QHttpMultiPart::FormDataType); - QHttpPart part; - QFile *f = (QFile *)dldContent; - QFileInfo fi(*f); - QString args("form-data; name=\"userfile\"; filename=\"" + fi.absoluteFilePath() + "\""); - part.setRawHeader("Content-Disposition", args.toLatin1()); - part.setBodyDevice(dldContent); - mp.append(part); - - multipart = ∓ - hideDownload(); - resetState(); - uploadMode = true; - ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done")); - exec(); - - multipart = NULL; - if (reply != NULL && reply->isOpen()) { - reply->abort(); - delete reply; - reply = NULL; - } -} - -DivelogsDeWebServices::DivelogsDeWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f), - multipart(NULL), - uploadMode(false) -{ - QSettings s; - ui.userID->setText(s.value("divelogde_user").toString()); - ui.password->setText(s.value("divelogde_pass").toString()); - ui.saveUidLocal->hide(); - hideUpload(); - QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(close, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quit, SIGNAL(activated()), parent, SLOT(close())); -} - -void DivelogsDeWebServices::startUpload() -{ - QSettings s; - s.setValue("divelogde_user", ui.userID->text()); - s.setValue("divelogde_pass", ui.password->text()); - s.sync(); - - ui.status->setText(tr("Uploading dive list...")); - ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' - ui.upload->setEnabled(false); - ui.userID->setEnabled(false); - ui.password->setEnabled(false); - - QNetworkRequest request; - request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php")); - request.setRawHeader("Accept", "text/xml, application/xml"); - request.setRawHeader("User-Agent", userAgent.toUtf8()); - - QHttpPart part; - part.setRawHeader("Content-Disposition", "form-data; name=\"user\""); - part.setBody(ui.userID->text().toUtf8()); - multipart->append(part); - - part.setRawHeader("Content-Disposition", "form-data; name=\"pass\""); - part.setBody(ui.password->text().toUtf8()); - multipart->append(part); - - reply = manager()->post(request, multipart); - connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, - SLOT(uploadError(QNetworkReply::NetworkError))); - connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this, - SLOT(updateProgress(qint64, qint64))); - - timeout.start(30000); // 30s -} - -void DivelogsDeWebServices::startDownload() -{ - ui.status->setText(tr("Downloading dive list...")); - ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' - ui.download->setEnabled(false); - ui.userID->setEnabled(false); - ui.password->setEnabled(false); - - QNetworkRequest request; - request.setUrl(QUrl("https://divelogs.de/xml_available_dives.php")); - request.setRawHeader("Accept", "text/xml, application/xml"); - request.setRawHeader("User-Agent", userAgent.toUtf8()); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - QUrlQuery body; - body.addQueryItem("user", ui.userID->text()); - body.addQueryItem("pass", ui.password->text()); - - reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); - connect(reply, SIGNAL(finished()), this, SLOT(listDownloadFinished())); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), - this, SLOT(downloadError(QNetworkReply::NetworkError))); - - timeout.start(30000); // 30s -} - -void DivelogsDeWebServices::listDownloadFinished() -{ - if (!reply) - return; - QByteArray xmlData = reply->readAll(); - reply->deleteLater(); - reply = NULL; - - // parse the XML data we downloaded - DiveListResult diveList = parseDiveLogsDeDiveList(xmlData); - if (!diveList.errorCondition.isEmpty()) { - // error condition - resetState(); - ui.status->setText(diveList.errorCondition); - return; - } - - ui.status->setText(tr("Downloading %1 dives...").arg(diveList.idCount)); - - QNetworkRequest request; - request.setUrl(QUrl("https://divelogs.de/DivelogsDirectExport.php")); - request.setRawHeader("Accept", "application/zip, */*"); - request.setRawHeader("User-Agent", userAgent.toUtf8()); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - QUrlQuery body; - body.addQueryItem("user", ui.userID->text()); - body.addQueryItem("pass", ui.password->text()); - body.addQueryItem("ids", diveList.idList); - - reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); - connect(reply, SIGNAL(readyRead()), this, SLOT(saveToZipFile())); - connectSignalsForDownload(reply); -} - -void DivelogsDeWebServices::saveToZipFile() -{ - if (!zipFile.isOpen()) { - zipFile.setFileTemplate(QDir::tempPath() + "/import-XXXXXX.dld"); - zipFile.open(); - } - - zipFile.write(reply->readAll()); -} - -void DivelogsDeWebServices::downloadFinished() -{ - if (!reply) - return; - - ui.download->setEnabled(true); - ui.status->setText(tr("Download finished - %1").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString())); - reply->deleteLater(); - reply = NULL; - - int errorcode; - zipFile.seek(0); -#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR) - int duppedfd = dup(zipFile.handle()); - struct zip *zip = NULL; - if (duppedfd >= 0) { - zip = zip_fdopen(duppedfd, 0, &errorcode); - if (!zip) - ::close(duppedfd); - } else { - QMessageBox::critical(this, tr("Problem with download"), - tr("The archive could not be opened:\n")); - return; - } -#else - struct zip *zip = zip_open(QFile::encodeName(zipFile.fileName()), 0, &errorcode); -#endif - if (!zip) { - char buf[512]; - zip_error_to_str(buf, sizeof(buf), errorcode, errno); - QMessageBox::critical(this, tr("Corrupted download"), - tr("The archive could not be opened:\n%1").arg(QString::fromLocal8Bit(buf))); - zipFile.close(); - return; - } - // now allow the user to cancel or accept - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); - - zip_close(zip); - zipFile.close(); -#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR) - ::close(duppedfd); -#endif -} - -void DivelogsDeWebServices::uploadFinished() -{ - if (!reply) - return; - - ui.progressBar->setRange(0, 1); - ui.upload->setEnabled(true); - ui.userID->setEnabled(true); - ui.password->setEnabled(true); - ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); - ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done")); - ui.status->setText(tr("Upload finished")); - - // check what the server sent us: it might contain - // an error condition, such as a failed login - QByteArray xmlData = reply->readAll(); - reply->deleteLater(); - reply = NULL; - char *resp = xmlData.data(); - if (resp) { - char *parsed = strstr(resp, ""); - if (parsed) { - if (strstr(resp, "succeeded")) { - if (strstr(resp, "failed")) { - ui.status->setText(tr("Upload failed")); - return; - } - ui.status->setText(tr("Upload successful")); - return; - } - ui.status->setText(tr("Login failed")); - return; - } - ui.status->setText(tr("Cannot parse response")); - } -} - -void DivelogsDeWebServices::setStatusText(int status) -{ -} - -void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError) -{ - resetState(); - ui.status->setText(tr("Error: %1").arg(reply->errorString())); - reply->deleteLater(); - reply = NULL; -} - -void DivelogsDeWebServices::uploadError(QNetworkReply::NetworkError error) -{ - downloadError(error); -} - -void DivelogsDeWebServices::buttonClicked(QAbstractButton *button) -{ - ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - switch (ui.buttonBox->buttonRole(button)) { - case QDialogButtonBox::ApplyRole: { - /* in 'uploadMode' button is called 'Done' and closes the dialog */ - if (uploadMode) { - hide(); - close(); - resetState(); - break; - } - /* parse file and import dives */ - parse_file(QFile::encodeName(zipFile.fileName())); - process_dives(true, false); - MainWindow::instance()->refreshDisplay(); - - /* store last entered user/pass in config */ - QSettings s; - s.setValue("divelogde_user", ui.userID->text()); - s.setValue("divelogde_pass", ui.password->text()); - s.sync(); - hide(); - close(); - resetState(); - } break; - case QDialogButtonBox::RejectRole: - // these two seem to be causing a crash: - // reply->deleteLater(); - resetState(); - break; - case QDialogButtonBox::HelpRole: - QDesktopServices::openUrl(QUrl("http://divelogs.de")); - break; - default: - break; - } -} - -UserSurveyServices::UserSurveyServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f) -{ - -} - -QNetworkReply* UserSurveyServices::sendSurvey(QString values) -{ - QNetworkRequest request; - request.setUrl(QString("http://subsurface-divelog.org/survey?%1").arg(values)); - request.setRawHeader("Accept", "text/xml"); - request.setRawHeader("User-Agent", userAgent.toUtf8()); - reply = manager()->get(request); - return reply; -} - -CloudStorageAuthenticate::CloudStorageAuthenticate(QObject *parent) : - QObject(parent), - reply(NULL) -{ - userAgent = getUserAgent(); -} - -#define CLOUDURL QString(prefs.cloud_base_url) -#define CLOUDBACKENDSTORAGE CLOUDURL + "/storage" -#define CLOUDBACKENDVERIFY CLOUDURL + "/verify" -#define CLOUDBACKENDUPDATE CLOUDURL + "/update" - -QNetworkReply* CloudStorageAuthenticate::backend(QString email, QString password, QString pin, QString newpasswd) -{ - QString payload(email + " " + password); - QUrl requestUrl; - if (pin == "" && newpasswd == "") { - requestUrl = QUrl(CLOUDBACKENDSTORAGE); - } else if (newpasswd != "") { - requestUrl = QUrl(CLOUDBACKENDUPDATE); - payload += " " + newpasswd; - } else { - requestUrl = QUrl(CLOUDBACKENDVERIFY); - payload += " " + pin; - } - QNetworkRequest *request = new QNetworkRequest(requestUrl); - request->setRawHeader("Accept", "text/xml, text/plain"); - request->setRawHeader("User-Agent", userAgent.toUtf8()); - request->setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); - reply = WebServices::manager()->post(*request, qPrintable(payload)); - connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, - SLOT(uploadError(QNetworkReply::NetworkError))); - return reply; -} - -void CloudStorageAuthenticate::uploadFinished() -{ - static QString myLastError; - - QString cloudAuthReply(reply->readAll()); - qDebug() << "Completed connection with cloud storage backend, response" << cloudAuthReply; - if (cloudAuthReply == "[VERIFIED]" || cloudAuthReply == "[OK]") { - prefs.cloud_verification_status = CS_VERIFIED; - NotificationWidget *nw = MainWindow::instance()->getNotificationWidget(); - if (nw->getNotificationText() == myLastError) - nw->hideNotification(); - myLastError.clear(); - } else if (cloudAuthReply == "[VERIFY]") { - prefs.cloud_verification_status = CS_NEED_TO_VERIFY; - } else if (cloudAuthReply == "[PASSWDCHANGED]") { - free(prefs.cloud_storage_password); - prefs.cloud_storage_password = prefs.cloud_storage_newpassword; - prefs.cloud_storage_newpassword = NULL; - emit passwordChangeSuccessful(); - return; - } else { - prefs.cloud_verification_status = CS_INCORRECT_USER_PASSWD; - myLastError = cloudAuthReply; - report_error("%s", qPrintable(cloudAuthReply)); - MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); - } - emit finishedAuthenticate(); -} - -void CloudStorageAuthenticate::uploadError(QNetworkReply::NetworkError error) -{ - qDebug() << "Received error response from cloud storage backend:" << reply->errorString(); -} - -void CloudStorageAuthenticate::sslErrors(QList errorList) -{ - if (verbose) { - qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; - Q_FOREACH (QSslError err, errorList) { - qDebug() << err.errorString(); - } - } - QSslConfiguration conf = reply->sslConfiguration(); - QSslCertificate cert = conf.peerCertificate(); - QByteArray hexDigest = cert.digest().toHex(); - if (reply->url().toString().contains(prefs.cloud_base_url) && - hexDigest == "13ff44c62996cfa5cd69d6810675490e") { - if (verbose) - qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; - reply->ignoreSslErrors(); - } else { - if (verbose) - qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; - } -} diff --git a/qt-ui/subsurfacewebservices.h b/qt-ui/subsurfacewebservices.h deleted file mode 100644 index 2b454ebc7..000000000 --- a/qt-ui/subsurfacewebservices.h +++ /dev/null @@ -1,142 +0,0 @@ -#ifndef SUBSURFACEWEBSERVICES_H -#define SUBSURFACEWEBSERVICES_H - -#include -#include -#include -#include -#include - -#include "ui_webservices.h" - -class QAbstractButton; -class QHttpMultiPart; - -class WebServices : public QDialog { - Q_OBJECT -public: - explicit WebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); - void hidePassword(); - void hideUpload(); - void hideDownload(); - - static QNetworkAccessManager *manager(); - -private -slots: - virtual void startDownload() = 0; - virtual void startUpload() = 0; - virtual void buttonClicked(QAbstractButton *button) = 0; - virtual void downloadTimedOut(); - -protected -slots: - void updateProgress(qint64 current, qint64 total); - -protected: - void resetState(); - void connectSignalsForDownload(QNetworkReply *reply); - void connectSignalsForUpload(); - - Ui::WebServices ui; - QNetworkReply *reply; - QTimer timeout; - QByteArray downloadedData; - QString defaultApplyText; - QString userAgent; -}; - -class SubsurfaceWebServices : public WebServices { - Q_OBJECT -public: - explicit SubsurfaceWebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); - -private -slots: - void startDownload(); - void buttonClicked(QAbstractButton *button); - void downloadFinished(); - void downloadError(QNetworkReply::NetworkError error); - void startUpload() - { - } /*no op*/ -private: - void setStatusText(int status); - void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status); - unsigned int download_dialog_parse_response(const QByteArray &length); -}; - -class DivelogsDeWebServices : public WebServices { - Q_OBJECT -public: - static DivelogsDeWebServices *instance(); - void downloadDives(); - void prepareDivesForUpload(bool selected); - -private -slots: - void startDownload(); - void buttonClicked(QAbstractButton *button); - void saveToZipFile(); - void listDownloadFinished(); - void downloadFinished(); - void uploadFinished(); - void downloadError(QNetworkReply::NetworkError error); - void uploadError(QNetworkReply::NetworkError error); - void startUpload(); - -private: - void uploadDives(QIODevice *dldContent); - explicit DivelogsDeWebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); - void setStatusText(int status); - bool prepare_dives_for_divelogs(const QString &filename, bool selected); - void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status); - unsigned int download_dialog_parse_response(const QByteArray &length); - - QHttpMultiPart *multipart; - QTemporaryFile zipFile; - bool uploadMode; -}; - -class UserSurveyServices : public WebServices { - Q_OBJECT -public: - QNetworkReply* sendSurvey(QString values); - explicit UserSurveyServices(QWidget *parent = 0, Qt::WindowFlags f = 0); -private -slots: - // need to declare them as no ops or Qt4 is unhappy - virtual void startDownload() { } - virtual void startUpload() { } - virtual void buttonClicked(QAbstractButton *button) { } -}; - -class CloudStorageAuthenticate : public QObject { - Q_OBJECT -public: - QNetworkReply* backend(QString email, QString password, QString pin = "", QString newpasswd = ""); - explicit CloudStorageAuthenticate(QObject *parent); -signals: - void finishedAuthenticate(); - void passwordChangeSuccessful(); -private -slots: - void uploadError(QNetworkReply::NetworkError error); - void sslErrors(QList errorList); - void uploadFinished(); -private: - QNetworkReply *reply; - QString userAgent; - -}; - -#ifdef __cplusplus -extern "C" { -#endif -extern void set_save_userid_local(short value); -extern void set_userid(char *user_id); -#ifdef __cplusplus -} -#endif - -#endif // SUBSURFACEWEBSERVICES_H diff --git a/qt-ui/tableview.cpp b/qt-ui/tableview.cpp deleted file mode 100644 index 40d5199ec..000000000 --- a/qt-ui/tableview.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "tableview.h" -#include "modeldelegates.h" - -#include -#include - -TableView::TableView(QWidget *parent) : QGroupBox(parent) -{ - ui.setupUi(this); - ui.tableView->setItemDelegate(new DiveListDelegate(this)); - - QFontMetrics fm(defaultModelFont()); - int text_ht = fm.height(); - - metrics.icon = &defaultIconMetrics(); - - metrics.rm_col_width = metrics.icon->sz_small + 2*metrics.icon->spacing; - metrics.header_ht = text_ht + 10; // TODO DPI - - /* We want to get rid of the margin around the table, but - * we must be careful with some styles (e.g. GTK+) where the top - * margin is actually used to hold the label. We thus check the - * rectangles for the label and contents to make sure they do not - * overlap, and adjust the top contentsMargin accordingly - */ - - // start by setting all the margins at zero - QMargins margins; - - // grab the label and contents dimensions and positions - QStyleOptionGroupBox option; - initStyleOption(&option); - QRect labelRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); - QRect contentsRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxContents, this); - - /* we need to ensure that the bottom of the label is higher - * than the top of the contents */ - int delta = contentsRect.top() - labelRect.bottom(); - const int min_gap = metrics.icon->spacing; - if (delta <= min_gap) { - margins.setTop(min_gap - delta); - } - layout()->setContentsMargins(margins); - - QIcon plusIcon(":plus"); - plusBtn = new QPushButton(plusIcon, QString(), this); - plusBtn->setFlat(true); - - /* now determine the icon and button size. Since the button will be - * placed in the label, make sure that we do not overflow, as it might - * get clipped - */ - int iconSize = metrics.icon->sz_small; - int btnSize = iconSize + 2*min_gap; - if (btnSize > labelRect.height()) { - btnSize = labelRect.height(); - iconSize = btnSize - 2*min_gap; - } - plusBtn->setIconSize(QSize(iconSize, iconSize)); - plusBtn->resize(btnSize, btnSize); - connect(plusBtn, SIGNAL(clicked(bool)), this, SIGNAL(addButtonClicked())); -} - -TableView::~TableView() -{ - QSettings s; - s.beginGroup(objectName()); - // remove the old default - bool oldDefault = (ui.tableView->columnWidth(0) == 30); - for (int i = 1; oldDefault && i < ui.tableView->model()->columnCount(); i++) { - if (ui.tableView->columnWidth(i) != 80) - oldDefault = false; - } - if (oldDefault) { - s.remove(""); - } else { - for (int i = 0; i < ui.tableView->model()->columnCount(); i++) { - if (ui.tableView->columnWidth(i) == defaultColumnWidth(i)) - s.remove(QString("colwidth%1").arg(i)); - else - s.setValue(QString("colwidth%1").arg(i), ui.tableView->columnWidth(i)); - } - } - s.endGroup(); -} - -void TableView::setBtnToolTip(const QString &tooltip) -{ - plusBtn->setToolTip(tooltip); -} - -void TableView::setModel(QAbstractItemModel *model) -{ - ui.tableView->setModel(model); - connect(ui.tableView, SIGNAL(clicked(QModelIndex)), model, SLOT(remove(QModelIndex))); - - QSettings s; - s.beginGroup(objectName()); - const int columnCount = ui.tableView->model()->columnCount(); - for (int i = 0; i < columnCount; i++) { - QVariant width = s.value(QString("colwidth%1").arg(i), defaultColumnWidth(i)); - ui.tableView->setColumnWidth(i, width.toInt()); - } - s.endGroup(); - - ui.tableView->horizontalHeader()->setMinimumHeight(metrics.header_ht); -} - -void TableView::fixPlusPosition() -{ - QStyleOptionGroupBox option; - initStyleOption(&option); - QRect labelRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); - QRect contentsRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::QStyle::SC_GroupBoxFrame, this); - plusBtn->setGeometry( contentsRect.width() - plusBtn->width(), labelRect.y(), plusBtn->width(), labelRect.height()); -} - -// We need to manually position the 'plus' on cylinder and weight. -void TableView::resizeEvent(QResizeEvent *event) -{ - fixPlusPosition(); - QWidget::resizeEvent(event); -} - -void TableView::showEvent(QShowEvent *event) -{ - QWidget::showEvent(event); - fixPlusPosition(); -} - -void TableView::edit(const QModelIndex &index) -{ - ui.tableView->edit(index); -} - -int TableView::defaultColumnWidth(int col) -{ - QString text = ui.tableView->model()->headerData(col, Qt::Horizontal).toString(); - return text.isEmpty() ? metrics.rm_col_width : defaultModelFontMetrics().width(text) + 4; // add small margin -} - -QTableView *TableView::view() -{ - return ui.tableView; -} diff --git a/qt-ui/tableview.h b/qt-ui/tableview.h deleted file mode 100644 index f72b256ea..000000000 --- a/qt-ui/tableview.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef TABLEVIEW_H -#define TABLEVIEW_H - -/* This TableView is prepared to have the CSS, - * the methods to restore / save the state of - * the column widths and the 'plus' button. - */ -#include - -#include "ui_tableview.h" - -#include "metrics.h" - -class QPushButton; -class QAbstractItemModel; -class QModelIndex; -class QTableView; - -class TableView : public QGroupBox { - Q_OBJECT - - struct TableMetrics { - const IconMetrics* icon; // icon metrics - int rm_col_width; // column width of REMOVE column - int header_ht; // height of the header - }; -public: - TableView(QWidget *parent = 0); - virtual ~TableView(); - /* The model is expected to have a 'remove' slot, that takes a QModelIndex as parameter. - * It's also expected to have the column '1' as a trash icon. I most probably should create a - * proxy model and add that column, will mark that as TODO. see? marked. - */ - void setModel(QAbstractItemModel *model); - void setBtnToolTip(const QString &tooltip); - void fixPlusPosition(); - void edit(const QModelIndex &index); - int defaultColumnWidth(int col); // default column width for column col - QTableView *view(); - -protected: - virtual void showEvent(QShowEvent *); - virtual void resizeEvent(QResizeEvent *); - -signals: - void addButtonClicked(); - -private: - Ui::TableView ui; - QPushButton *plusBtn; - TableMetrics metrics; -}; - -#endif // TABLEVIEW_H diff --git a/qt-ui/tableview.ui b/qt-ui/tableview.ui deleted file mode 100644 index 73867231e..000000000 --- a/qt-ui/tableview.ui +++ /dev/null @@ -1,27 +0,0 @@ - - - TableView - - - - 0 - 0 - 400 - 300 - - - - GroupBox - - - GroupBox - - - - - - - - - - diff --git a/qt-ui/tagwidget.cpp b/qt-ui/tagwidget.cpp deleted file mode 100644 index 3b61b492a..000000000 --- a/qt-ui/tagwidget.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include "tagwidget.h" -#include "mainwindow.h" -#include "maintab.h" -#include - -TagWidget::TagWidget(QWidget *parent) : GroupedLineEdit(parent), m_completer(NULL), lastFinishedTag(false) -{ - connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(reparse())); - connect(this, SIGNAL(textChanged()), this, SLOT(reparse())); - - QColor textColor = palette().color(QPalette::Text); - qreal h, s, l, a; - textColor.getHslF(&h, &s, &l, &a); - // I use dark themes - if (l <= 0.3) { // very dark text. get a brigth background - addColor(QColor(Qt::red).lighter(120)); - addColor(QColor(Qt::green).lighter(120)); - addColor(QColor(Qt::blue).lighter(120)); - } else if (l <= 0.6) { // moderated dark text. get a somewhat bright background - addColor(QColor(Qt::red).lighter(60)); - addColor(QColor(Qt::green).lighter(60)); - addColor(QColor(Qt::blue).lighter(60)); - } else { - addColor(QColor(Qt::red).darker(120)); - addColor(QColor(Qt::green).darker(120)); - addColor(QColor(Qt::blue).darker(120)); - } // light text. get a dark background. - setFocusPolicy(Qt::StrongFocus); -} - -void TagWidget::setCompleter(QCompleter *completer) -{ - m_completer = completer; - m_completer->setWidget(this); - connect(m_completer, SIGNAL(activated(QString)), this, SLOT(completionSelected(QString))); - connect(m_completer, SIGNAL(highlighted(QString)), this, SLOT(completionHighlighted(QString))); -} - -QPair TagWidget::getCursorTagPosition() -{ - int i = 0, start = 0, end = 0; - /* Parse string near cursor */ - i = cursorPosition(); - while (--i > 0) { - if (text().at(i) == ',') { - if (i > 0 && text().at(i - 1) != '\\') { - i++; - break; - } - } - } - start = i; - while (++i < text().length()) { - if (text().at(i) == ',') { - if (i > 0 && text().at(i - 1) != '\\') - break; - } - } - end = i; - if (start < 0 || end < 0) { - start = 0; - end = 0; - } - return qMakePair(start, end); -} - -void TagWidget::highlight() -{ - removeAllBlocks(); - int lastPos = 0; - Q_FOREACH (const QString& s, text().split(QChar(','), QString::SkipEmptyParts)) { - QString trimmed = s.trimmed(); - if (trimmed.isEmpty()) - continue; - int start = text().indexOf(trimmed, lastPos); - addBlock(start, trimmed.size() + start); - lastPos = trimmed.size() + start; - } -} - -void TagWidget::reparse() -{ - highlight(); - QPair pos = getCursorTagPosition(); - QString currentText; - if (pos.first >= 0 && pos.second > 0) - currentText = text().mid(pos.first, pos.second - pos.first).trimmed(); - - /* - * Do not show the completer when not in edit mode - basically - * this returns when we are accepting or discarding the changes. - */ - if (MainWindow::instance()->information()->isEditing() == false || currentText.length() == 0) { - return; - } - - if (m_completer) { - m_completer->setCompletionPrefix(currentText); - if (m_completer->completionCount() == 1) { - if (m_completer->currentCompletion() == currentText) { - QAbstractItemView *popup = m_completer->popup(); - if (popup) - popup->hide(); - } else { - m_completer->complete(); - } - } else { - m_completer->complete(); - } - } -} - -void TagWidget::completionSelected(const QString &completion) -{ - completionHighlighted(completion); - emit textChanged(); -} - -void TagWidget::completionHighlighted(const QString &completion) -{ - QPair pos = getCursorTagPosition(); - setText(text().remove(pos.first, pos.second - pos.first).insert(pos.first, completion)); - setCursorPosition(pos.first + completion.length()); -} - -void TagWidget::setCursorPosition(int position) -{ - blockSignals(true); - GroupedLineEdit::setCursorPosition(position); - blockSignals(false); -} - -void TagWidget::setText(const QString &text) -{ - blockSignals(true); - GroupedLineEdit::setText(text); - blockSignals(false); - highlight(); -} - -void TagWidget::clear() -{ - blockSignals(true); - GroupedLineEdit::clear(); - blockSignals(false); -} - -void TagWidget::keyPressEvent(QKeyEvent *e) -{ - QPair pos; - QAbstractItemView *popup; - bool finishedTag = false; - switch (e->key()) { - case Qt::Key_Escape: - pos = getCursorTagPosition(); - if (pos.first >= 0 && pos.second > 0) { - setText(text().remove(pos.first, pos.second - pos.first)); - setCursorPosition(pos.first); - } - popup = m_completer->popup(); - if (popup) - popup->hide(); - return; - case Qt::Key_Return: - case Qt::Key_Enter: - case Qt::Key_Tab: - /* - * Fake the QLineEdit behaviour by simply - * closing the QAbstractViewitem - */ - if (m_completer) { - popup = m_completer->popup(); - if (popup) - popup->hide(); - } - finishedTag = true; - break; - case Qt::Key_Comma: { /* if this is the last key, and the previous string is empty, ignore the comma. */ - QString temp = text(); - if (temp.split(QChar(',')).last().trimmed().isEmpty()){ - e->ignore(); - return; - } - } - } - if (e->key() == Qt::Key_Tab && lastFinishedTag) { // if we already end in comma, go to next/prev field - MainWindow::instance()->information()->nextInputField(e); // by sending the key event to the MainTab widget - } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Return) { // otherwise let's pretend this is a comma instead - QKeyEvent fakeEvent(e->type(), Qt::Key_Comma, e->modifiers(), QString(",")); - keyPressEvent(&fakeEvent); - } else { - GroupedLineEdit::keyPressEvent(e); - } - lastFinishedTag = finishedTag; -} - -void TagWidget::wheelEvent(QWheelEvent *event) -{ - if (hasFocus()) { - GroupedLineEdit::wheelEvent(event); - } -} - -void TagWidget::fixPopupPosition(int delta) -{ - if(m_completer->popup()->isVisible()){ - QRect toGlobal = m_completer->popup()->geometry(); - m_completer->popup()->setGeometry(toGlobal.x(), toGlobal.y() + delta +10, toGlobal.width(), toGlobal.height()); - } -} diff --git a/qt-ui/tagwidget.h b/qt-ui/tagwidget.h deleted file mode 100644 index 6a16129f3..000000000 --- a/qt-ui/tagwidget.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef TAGWIDGET_H -#define TAGWIDGET_H - -#include "groupedlineedit.h" -#include - -class QCompleter; - -class TagWidget : public GroupedLineEdit { - Q_OBJECT -public: - explicit TagWidget(QWidget *parent = 0); - void setCompleter(QCompleter *completer); - QPair getCursorTagPosition(); - void highlight(); - void setText(const QString &text); - void clear(); - void setCursorPosition(int position); - void wheelEvent(QWheelEvent *event); - void fixPopupPosition(int delta); -public -slots: - void reparse(); - void completionSelected(const QString &text); - void completionHighlighted(const QString &text); - -protected: - void keyPressEvent(QKeyEvent *e); -private: - QCompleter *m_completer; - bool lastFinishedTag; -}; - -#endif // TAGWIDGET_H diff --git a/qt-ui/templateedit.cpp b/qt-ui/templateedit.cpp deleted file mode 100644 index 4964016b9..000000000 --- a/qt-ui/templateedit.cpp +++ /dev/null @@ -1,227 +0,0 @@ -#include "templateedit.h" -#include "printoptions.h" -#include "printer.h" -#include "ui_templateedit.h" - -#include -#include - -TemplateEdit::TemplateEdit(QWidget *parent, struct print_options *printOptions, struct template_options *templateOptions) : - QDialog(parent), - ui(new Ui::TemplateEdit) -{ - ui->setupUi(this); - this->templateOptions = templateOptions; - newTemplateOptions = *templateOptions; - this->printOptions = printOptions; - - // restore the settings and init the UI - ui->fontSelection->setCurrentIndex(templateOptions->font_index); - ui->fontsize->setValue(templateOptions->font_size); - ui->colorpalette->setCurrentIndex(templateOptions->color_palette_index); - ui->linespacing->setValue(templateOptions->line_spacing); - - grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); - if (printOptions->type == print_options::DIVELIST) - grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); - else if (printOptions->type == print_options::STATISTICS) - grantlee_template = TemplateLayout::readTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template); - - // gui - btnGroup = new QButtonGroup; - btnGroup->addButton(ui->editButton1, 1); - btnGroup->addButton(ui->editButton2, 2); - btnGroup->addButton(ui->editButton3, 3); - btnGroup->addButton(ui->editButton4, 4); - btnGroup->addButton(ui->editButton5, 5); - btnGroup->addButton(ui->editButton6, 6); - connect(btnGroup, SIGNAL(buttonClicked(QAbstractButton*)), this, SLOT(colorSelect(QAbstractButton*))); - - ui->plainTextEdit->setPlainText(grantlee_template); - editingCustomColors = false; - updatePreview(); -} - -TemplateEdit::~TemplateEdit() -{ - delete btnGroup; - delete ui; -} - -void TemplateEdit::updatePreview() -{ - // update Qpixmap preview - int width = ui->label->width(); - int height = ui->label->height(); - QPixmap map(width * 2, height * 2); - map.fill(QColor::fromRgb(255, 255, 255)); - Printer printer(&map, printOptions, &newTemplateOptions, Printer::PREVIEW); - printer.previewOnePage(); - ui->label->setPixmap(map.scaled(width, height, Qt::IgnoreAspectRatio)); - - // update colors tab - ui->colorLable1->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color1.name() + "\";}"); - ui->colorLable2->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color2.name() + "\";}"); - ui->colorLable3->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color3.name() + "\";}"); - ui->colorLable4->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color4.name() + "\";}"); - ui->colorLable5->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color5.name() + "\";}"); - ui->colorLable6->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color6.name() + "\";}"); - - ui->colorLable1->setText(newTemplateOptions.color_palette.color1.name()); - ui->colorLable2->setText(newTemplateOptions.color_palette.color2.name()); - ui->colorLable3->setText(newTemplateOptions.color_palette.color3.name()); - ui->colorLable4->setText(newTemplateOptions.color_palette.color4.name()); - ui->colorLable5->setText(newTemplateOptions.color_palette.color5.name()); - ui->colorLable6->setText(newTemplateOptions.color_palette.color6.name()); - - // update critical UI elements - ui->colorpalette->setCurrentIndex(newTemplateOptions.color_palette_index); - - // update grantlee template string - grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); - if (printOptions->type == print_options::DIVELIST) - grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); - else if (printOptions->type == print_options::STATISTICS) - grantlee_template = TemplateLayout::readTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template); -} - -void TemplateEdit::on_fontsize_valueChanged(int font_size) -{ - newTemplateOptions.font_size = font_size; - updatePreview(); -} - -void TemplateEdit::on_linespacing_valueChanged(double line_spacing) -{ - newTemplateOptions.line_spacing = line_spacing; - updatePreview(); -} - -void TemplateEdit::on_fontSelection_currentIndexChanged(int index) -{ - newTemplateOptions.font_index = index; - updatePreview(); -} - -void TemplateEdit::on_colorpalette_currentIndexChanged(int index) -{ - newTemplateOptions.color_palette_index = index; - switch (newTemplateOptions.color_palette_index) { - case SSRF_COLORS: // subsurface derived default colors - newTemplateOptions.color_palette = ssrf_colors; - break; - case ALMOND: // almond - newTemplateOptions.color_palette = almond_colors; - break; - case BLUESHADES: // blueshades - newTemplateOptions.color_palette = blueshades_colors; - break; - case CUSTOM: // custom - if (!editingCustomColors) - newTemplateOptions.color_palette = custom_colors; - else - editingCustomColors = false; - break; - } - updatePreview(); -} - -void TemplateEdit::saveSettings() -{ - if ((*templateOptions) != newTemplateOptions || grantlee_template.compare(ui->plainTextEdit->toPlainText())) { - QMessageBox msgBox(this); - QString message = tr("Do you want to save your changes?"); - bool templateChanged = false; - if (grantlee_template.compare(ui->plainTextEdit->toPlainText())) - templateChanged = true; - msgBox.setText(message); - msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel); - msgBox.setDefaultButton(QMessageBox::Cancel); - if (msgBox.exec() == QMessageBox::Save) { - memcpy(templateOptions, &newTemplateOptions, sizeof(struct template_options)); - if (templateChanged) { - TemplateLayout::writeTemplate(printOptions->p_template, ui->plainTextEdit->toPlainText()); - if (printOptions->type == print_options::DIVELIST) - TemplateLayout::writeTemplate(printOptions->p_template, ui->plainTextEdit->toPlainText()); - else if (printOptions->type == print_options::STATISTICS) - TemplateLayout::writeTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template, ui->plainTextEdit->toPlainText()); - } - if (templateOptions->color_palette_index == CUSTOM) - custom_colors = templateOptions->color_palette; - } - } -} - -void TemplateEdit::on_buttonBox_clicked(QAbstractButton *button) -{ - QDialogButtonBox::StandardButton standardButton = ui->buttonBox->standardButton(button); - switch (standardButton) { - case QDialogButtonBox::Ok: - saveSettings(); - break; - case QDialogButtonBox::Cancel: - break; - case QDialogButtonBox::Apply: - saveSettings(); - updatePreview(); - break; - default: - ; - } -} - -void TemplateEdit::colorSelect(QAbstractButton *button) -{ - editingCustomColors = true; - // reset custom colors palette - switch (newTemplateOptions.color_palette_index) { - case SSRF_COLORS: // subsurface derived default colors - newTemplateOptions.color_palette = ssrf_colors; - break; - case ALMOND: // almond - newTemplateOptions.color_palette = almond_colors; - break; - case BLUESHADES: // blueshades - newTemplateOptions.color_palette = blueshades_colors; - break; - default: - break; - } - - //change selected color - QColor color; - switch (btnGroup->id(button)) { - case 1: - color = QColorDialog::getColor(newTemplateOptions.color_palette.color1, this); - if (color.isValid()) - newTemplateOptions.color_palette.color1 = color; - break; - case 2: - color = QColorDialog::getColor(newTemplateOptions.color_palette.color2, this); - if (color.isValid()) - newTemplateOptions.color_palette.color2 = color; - break; - case 3: - color = QColorDialog::getColor(newTemplateOptions.color_palette.color3, this); - if (color.isValid()) - newTemplateOptions.color_palette.color3 = color; - break; - case 4: - color = QColorDialog::getColor(newTemplateOptions.color_palette.color4, this); - if (color.isValid()) - newTemplateOptions.color_palette.color4 = color; - break; - case 5: - color = QColorDialog::getColor(newTemplateOptions.color_palette.color5, this); - if (color.isValid()) - newTemplateOptions.color_palette.color5 = color; - break; - case 6: - color = QColorDialog::getColor(newTemplateOptions.color_palette.color6, this); - if (color.isValid()) - newTemplateOptions.color_palette.color6 = color; - break; - } - newTemplateOptions.color_palette_index = CUSTOM; - updatePreview(); -} diff --git a/qt-ui/templateedit.h b/qt-ui/templateedit.h deleted file mode 100644 index 5e548ae19..000000000 --- a/qt-ui/templateedit.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef TEMPLATEEDIT_H -#define TEMPLATEEDIT_H - -#include -#include "templatelayout.h" - -namespace Ui { -class TemplateEdit; -} - -class TemplateEdit : public QDialog -{ - Q_OBJECT - -public: - explicit TemplateEdit(QWidget *parent, struct print_options *printOptions, struct template_options *templateOptions); - ~TemplateEdit(); -private slots: - void on_fontsize_valueChanged(int font_size); - - void on_linespacing_valueChanged(double line_spacing); - - void on_fontSelection_currentIndexChanged(int index); - - void on_colorpalette_currentIndexChanged(int index); - - void on_buttonBox_clicked(QAbstractButton *button); - - void colorSelect(QAbstractButton *button); - -private: - Ui::TemplateEdit *ui; - QButtonGroup *btnGroup; - bool editingCustomColors; - struct template_options *templateOptions; - struct template_options newTemplateOptions; - struct print_options *printOptions; - QString grantlee_template; - void saveSettings(); - void updatePreview(); - -}; - -#endif // TEMPLATEEDIT_H diff --git a/qt-ui/templateedit.ui b/qt-ui/templateedit.ui deleted file mode 100644 index 60a0fc7e6..000000000 --- a/qt-ui/templateedit.ui +++ /dev/null @@ -1,577 +0,0 @@ - - - TemplateEdit - - - - 0 - 0 - 770 - 433 - - - - Edit template - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Preview - - - - - - - - 0 - 0 - - - - - 180 - 240 - - - - - 180 - 254 - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - false - - - 0 - - - - Style - - - - - - - - - - Font - - - - - - - - Arial - - - - - Impact - - - - - Georgia - - - - - Courier - - - - - Verdana - - - - - - - - - - - - Font size - - - - - - - 9 - - - 18 - - - - - - - - - - - Color palette - - - - - - - - Default - - - - - Almond - - - - - Shades of blue - - - - - Custom - - - - - - - - - - - - Line spacing - - - - - - - 1.000000000000000 - - - 3.000000000000000 - - - 0.250000000000000 - - - 1.250000000000000 - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - Template - - - - - - Qt::ScrollBarAsNeeded - - - QPlainTextEdit::NoWrap - - - - - - - - - 16777215 - 16777215 - - - - Colors - - - - - - - - - - - 0 - 0 - - - - Background - - - - - - - - 0 - 0 - - - - color1 - - - Qt::AlignCenter - - - - - - - Edit - - - - - - - - - - - - 0 - 0 - - - - Table cells 1 - - - - - - - - 0 - 0 - - - - color2 - - - Qt::AlignCenter - - - - - - - Edit - - - - - - - - - - - - 0 - 0 - - - - Table cells 2 - - - - - - - - 0 - 0 - - - - color3 - - - Qt::AlignCenter - - - - - - - Edit - - - - - - - - - - - - 0 - 0 - - - - Text 1 - - - - - - - - 0 - 0 - - - - color4 - - - Qt::AlignCenter - - - - - - - Edit - - - - - - - - - - - - 0 - 0 - - - - Text 2 - - - - - - - - 0 - 0 - - - - color5 - - - Qt::AlignCenter - - - - - - - Edit - - - - - - - - - - - - 0 - 0 - - - - Borders - - - - - - - - 0 - 0 - - - - color6 - - - Qt::AlignCenter - - - - - - - Edit - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - TemplateEdit - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - TemplateEdit - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/templatelayout.cpp b/qt-ui/templatelayout.cpp deleted file mode 100644 index a376459a6..000000000 --- a/qt-ui/templatelayout.cpp +++ /dev/null @@ -1,182 +0,0 @@ -#include - -#include "templatelayout.h" -#include "helpers.h" -#include "display.h" - -QList grantlee_templates, grantlee_statistics_templates; - -int getTotalWork(print_options *printOptions) -{ - if (printOptions->print_selected) { - // return the correct number depending on all/selected dives - // but don't return 0 as we might divide by this number - return amount_selected ? amount_selected : 1; - } - int dives = 0, i; - struct dive *dive; - for_each_dive (i, dive) { - dives++; - } - return dives; -} - -void find_all_templates() -{ - grantlee_templates.clear(); - grantlee_statistics_templates.clear(); - QDir dir(getPrintingTemplatePathUser()); - QFileInfoList list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); - foreach (QFileInfo finfo, list) { - QString filename = finfo.fileName(); - if (filename.at(filename.size() - 1) != '~') { - grantlee_templates.append(finfo.fileName()); - } - } - // find statistics templates - dir.setPath(getPrintingTemplatePathUser() + QDir::separator() + "statistics"); - list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); - foreach (QFileInfo finfo, list) { - QString filename = finfo.fileName(); - if (filename.at(filename.size() - 1) != '~') { - grantlee_statistics_templates.append(finfo.fileName()); - } - } -} - -TemplateLayout::TemplateLayout(print_options *PrintOptions, template_options *templateOptions) : - m_engine(NULL) -{ - this->PrintOptions = PrintOptions; - this->templateOptions = templateOptions; -} - -TemplateLayout::~TemplateLayout() -{ - delete m_engine; -} - -QString TemplateLayout::generate() -{ - int progress = 0; - int totalWork = getTotalWork(PrintOptions); - - QString htmlContent; - m_engine = new Grantlee::Engine(this); - - QSharedPointer m_templateLoader = - QSharedPointer(new Grantlee::FileSystemTemplateLoader()); - m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser()); - m_engine->addTemplateLoader(m_templateLoader); - - Grantlee::registerMetaType(); - Grantlee::registerMetaType(); - Grantlee::registerMetaType(); - - QVariantList diveList; - - struct dive *dive; - int i; - for_each_dive (i, dive) { - //TODO check for exporting selected dives only - if (!dive->selected && PrintOptions->print_selected) - continue; - Dive d(dive); - diveList.append(QVariant::fromValue(d)); - progress++; - emit progressUpdated(progress * 100.0 / totalWork); - } - Grantlee::Context c; - c.insert("dives", diveList); - c.insert("template_options", QVariant::fromValue(*templateOptions)); - c.insert("print_options", QVariant::fromValue(*PrintOptions)); - - Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); - if (!t || t->error()) { - qDebug() << "Can't load template"; - return htmlContent; - } - - htmlContent = t->render(&c); - - if (t->error()) { - qDebug() << "Can't render template"; - return htmlContent; - } - return htmlContent; -} - -QString TemplateLayout::generateStatistics() -{ - QString htmlContent; - m_engine = new Grantlee::Engine(this); - - QSharedPointer m_templateLoader = - QSharedPointer(new Grantlee::FileSystemTemplateLoader()); - m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser() + QDir::separator() + QString("statistics")); - m_engine->addTemplateLoader(m_templateLoader); - - Grantlee::registerMetaType(); - Grantlee::registerMetaType(); - Grantlee::registerMetaType(); - - QVariantList years; - - int i = 0; - while (stats_yearly != NULL && stats_yearly[i].period) { - YearInfo year(stats_yearly[i]); - years.append(QVariant::fromValue(year)); - i++; - } - - Grantlee::Context c; - c.insert("years", years); - c.insert("template_options", QVariant::fromValue(*templateOptions)); - c.insert("print_options", QVariant::fromValue(*PrintOptions)); - - Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); - if (!t || t->error()) { - qDebug() << "Can't load template"; - return htmlContent; - } - - htmlContent = t->render(&c); - - if (t->error()) { - qDebug() << "Can't render template"; - return htmlContent; - } - - emit progressUpdated(100); - return htmlContent; -} - -QString TemplateLayout::readTemplate(QString template_name) -{ - QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); - if (qfile.open(QFile::ReadOnly | QFile::Text)) { - QTextStream in(&qfile); - return in.readAll(); - } - return ""; -} - -void TemplateLayout::writeTemplate(QString template_name, QString grantlee_template) -{ - QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); - if (qfile.open(QFile::ReadWrite | QFile::Text)) { - qfile.write(grantlee_template.toUtf8().data()); - qfile.resize(qfile.pos()); - qfile.close(); - } -} - -YearInfo::YearInfo() -{ - -} - -YearInfo::~YearInfo() -{ - -} diff --git a/qt-ui/templatelayout.h b/qt-ui/templatelayout.h deleted file mode 100644 index a2868e7ff..000000000 --- a/qt-ui/templatelayout.h +++ /dev/null @@ -1,168 +0,0 @@ -#ifndef TEMPLATELAYOUT_H -#define TEMPLATELAYOUT_H - -#include -#include "mainwindow.h" -#include "printoptions.h" -#include "statistics.h" -#include "qthelper.h" -#include "helpers.h" - -int getTotalWork(print_options *printOptions); -void find_all_templates(); - -extern QList grantlee_templates, grantlee_statistics_templates; - -class TemplateLayout : public QObject { - Q_OBJECT -public: - TemplateLayout(print_options *PrintOptions, template_options *templateOptions); - ~TemplateLayout(); - QString generate(); - QString generateStatistics(); - static QString readTemplate(QString template_name); - static void writeTemplate(QString template_name, QString grantlee_template); - -private: - Grantlee::Engine *m_engine; - print_options *PrintOptions; - template_options *templateOptions; - -signals: - void progressUpdated(int value); -}; - -class YearInfo { -public: - stats_t *year; - YearInfo(stats_t& year) - :year(&year) - { - - } - YearInfo(); - ~YearInfo(); -}; - -Q_DECLARE_METATYPE(Dive) -Q_DECLARE_METATYPE(template_options) -Q_DECLARE_METATYPE(print_options) -Q_DECLARE_METATYPE(YearInfo) - -GRANTLEE_BEGIN_LOOKUP(Dive) -if (property == "number") - return object.number(); -else if (property == "id") - return object.id(); -else if (property == "date") - return object.date(); -else if (property == "time") - return object.time(); -else if (property == "location") - return object.location(); -else if (property == "duration") - return object.duration(); -else if (property == "depth") - return object.depth(); -else if (property == "divemaster") - return object.divemaster(); -else if (property == "buddy") - return object.buddy(); -else if (property == "airTemp") - return object.airTemp(); -else if (property == "waterTemp") - return object.waterTemp(); -else if (property == "notes") - return object.notes(); -else if (property == "rating") - return object.rating(); -else if (property == "sac") - return object.sac(); -else if (property == "tags") - return object.tags(); -else if (property == "gas") - return object.gas(); -GRANTLEE_END_LOOKUP - -GRANTLEE_BEGIN_LOOKUP(template_options) -if (property == "font") { - switch (object.font_index) { - case 0: - return "Arial, Helvetica, sans-serif"; - case 1: - return "Impact, Charcoal, sans-serif"; - case 2: - return "Georgia, serif"; - case 3: - return "Courier, monospace"; - case 4: - return "Verdana, Geneva, sans-serif"; - } -} else if (property == "borderwidth") { - return object.border_width; -} else if (property == "font_size") { - return object.font_size / 9.0; -} else if (property == "line_spacing") { - return object.line_spacing; -} else if (property == "color1") { - return object.color_palette.color1.name(); -} else if (property == "color2") { - return object.color_palette.color2.name(); -} else if (property == "color3") { - return object.color_palette.color3.name(); -} else if (property == "color4") { - return object.color_palette.color4.name(); -} else if (property == "color5") { - return object.color_palette.color5.name(); -} else if (property == "color6") { - return object.color_palette.color6.name(); -} -GRANTLEE_END_LOOKUP - -GRANTLEE_BEGIN_LOOKUP(print_options) -if (property == "grayscale") { - if (object.color_selected) { - return ""; - } else { - return "-webkit-filter: grayscale(100%)"; - } -} -GRANTLEE_END_LOOKUP - -GRANTLEE_BEGIN_LOOKUP(YearInfo) -if (property == "year") { - return object.year->period; -} else if (property == "dives") { - return object.year->selection_size; -} else if (property == "min_temp") { - const char *unit; - double temp = get_temp_units(object.year->min_temp, &unit); - return object.year->min_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; -} else if (property == "max_temp") { - const char *unit; - double temp = get_temp_units(object.year->max_temp, &unit); - return object.year->max_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; -} else if (property == "total_time") { - return get_time_string(object.year->total_time.seconds, 0); -} else if (property == "avg_time") { - return get_minutes(object.year->total_time.seconds / object.year->selection_size); -} else if (property == "shortest_time") { - return get_minutes(object.year->shortest_time.seconds); -} else if (property == "longest_time") { - return get_minutes(object.year->longest_time.seconds); -} else if (property == "avg_depth") { - return get_depth_string(object.year->avg_depth); -} else if (property == "min_depth") { - return get_depth_string(object.year->min_depth); -} else if (property == "max_depth") { - return get_depth_string(object.year->max_depth); -} else if (property == "avg_sac") { - return get_volume_string(object.year->avg_sac); -} else if (property == "min_sac") { - return get_volume_string(object.year->min_sac); -} else if (property == "max_sac") { - return get_volume_string(object.year->max_sac); -} -GRANTLEE_END_LOOKUP - -#endif diff --git a/qt-ui/undocommands.cpp b/qt-ui/undocommands.cpp deleted file mode 100644 index 0fd182cb3..000000000 --- a/qt-ui/undocommands.cpp +++ /dev/null @@ -1,156 +0,0 @@ -#include "undocommands.h" -#include "mainwindow.h" -#include "divelist.h" - -UndoDeleteDive::UndoDeleteDive(QList deletedDives) : diveList(deletedDives) -{ - setText("delete dive"); - if (diveList.count() > 1) - setText(QString("delete %1 dives").arg(QString::number(diveList.count()))); -} - -void UndoDeleteDive::undo() -{ - // first bring back the trip(s) - Q_FOREACH(struct dive_trip *trip, tripList) - insert_trip(&trip); - - // now walk the list of deleted dives - for (int i = 0; i < diveList.count(); i++) { - struct dive *d = diveList.at(i); - // we adjusted the divetrip to point to the "new" divetrip - if (d->divetrip) { - struct dive_trip *trip = d->divetrip; - tripflag_t tripflag = d->tripflag; // this gets overwritten in add_dive_to_trip() - d->divetrip = NULL; - d->next = NULL; - d->pprev = NULL; - add_dive_to_trip(d, trip); - d->tripflag = tripflag; - } - record_dive(diveList.at(i)); - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} - -void UndoDeleteDive::redo() -{ - QList newList; - for (int i = 0; i < diveList.count(); i++) { - // make a copy of the dive before deleting it - struct dive* d = alloc_dive(); - copy_dive(diveList.at(i), d); - newList.append(d); - // check for trip - if this is the last dive in the trip - // the trip will get deleted, so we need to remember it as well - if (d->divetrip && d->divetrip->nrdives == 1) { - struct dive_trip *undo_trip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip)); - *undo_trip = *d->divetrip; - undo_trip->location = copy_string(d->divetrip->location); - undo_trip->notes = copy_string(d->divetrip->notes); - undo_trip->nrdives = 0; - undo_trip->next = NULL; - undo_trip->dives = NULL; - // update all the dives who were in this trip to point to the copy of the - // trip that we are about to delete implicitly when deleting its last dive below - Q_FOREACH(struct dive *inner_dive, newList) - if (inner_dive->divetrip == d->divetrip) - inner_dive->divetrip = undo_trip; - d->divetrip = undo_trip; - tripList.append(undo_trip); - } - //delete the dive - delete_single_dive(get_divenr(diveList.at(i))); - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); - diveList.clear(); - diveList = newList; -} - - -UndoShiftTime::UndoShiftTime(QList changedDives, int amount) - : diveList(changedDives), timeChanged(amount) -{ - setText("shift time"); -} - -void UndoShiftTime::undo() -{ - for (int i = 0; i < diveList.count(); i++) { - struct dive* d = get_dive_by_uniq_id(diveList.at(i)); - d->when -= timeChanged; - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} - -void UndoShiftTime::redo() -{ - for (int i = 0; i < diveList.count(); i++) { - struct dive* d = get_dive_by_uniq_id(diveList.at(i)); - d->when += timeChanged; - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} - - -UndoRenumberDives::UndoRenumberDives(QMap > originalNumbers) -{ - oldNumbers = originalNumbers; - if (oldNumbers.count() > 1) - setText(QString("renumber %1 dives").arg(QString::number(oldNumbers.count()))); - else - setText("renumber dive"); -} - -void UndoRenumberDives::undo() -{ - foreach (int key, oldNumbers.keys()) { - struct dive* d = get_dive_by_uniq_id(key); - d->number = oldNumbers.value(key).first; - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} - -void UndoRenumberDives::redo() -{ - foreach (int key, oldNumbers.keys()) { - struct dive* d = get_dive_by_uniq_id(key); - d->number = oldNumbers.value(key).second; - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} - - -UndoRemoveDivesFromTrip::UndoRemoveDivesFromTrip(QMap removedDives) -{ - divesToUndo = removedDives; - setText("remove dive(s) from trip"); -} - -void UndoRemoveDivesFromTrip::undo() -{ - QMapIterator i(divesToUndo); - while (i.hasNext()) { - i.next(); - add_dive_to_trip(i.key (), i.value()); - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} - -void UndoRemoveDivesFromTrip::redo() -{ - QMapIterator i(divesToUndo); - while (i.hasNext()) { - i.next(); - remove_dive_from_trip(i.key(), false); - } - mark_divelist_changed(true); - MainWindow::instance()->refreshDisplay(); -} diff --git a/qt-ui/undocommands.h b/qt-ui/undocommands.h deleted file mode 100644 index 8e359db51..000000000 --- a/qt-ui/undocommands.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef UNDOCOMMANDS_H -#define UNDOCOMMANDS_H - -#include -#include -#include "dive.h" - -class UndoDeleteDive : public QUndoCommand { -public: - UndoDeleteDive(QList deletedDives); - virtual void undo(); - virtual void redo(); - -private: - QList diveList; - QList tripList; -}; - -class UndoShiftTime : public QUndoCommand { -public: - UndoShiftTime(QList changedDives, int amount); - virtual void undo(); - virtual void redo(); - -private: - QList diveList; - int timeChanged; -}; - -class UndoRenumberDives : public QUndoCommand { -public: - UndoRenumberDives(QMap > originalNumbers); - virtual void undo(); - virtual void redo(); - -private: - QMap > oldNumbers; -}; - -class UndoRemoveDivesFromTrip : public QUndoCommand { -public: - UndoRemoveDivesFromTrip(QMap removedDives); - virtual void undo(); - virtual void redo(); - -private: - QMap divesToUndo; -}; - -#endif // UNDOCOMMANDS_H diff --git a/qt-ui/updatemanager.cpp b/qt-ui/updatemanager.cpp deleted file mode 100644 index 0760d6407..000000000 --- a/qt-ui/updatemanager.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "updatemanager.h" -#include "helpers.h" -#include -#include -#include -#include "subsurfacewebservices.h" -#include "version.h" -#include "mainwindow.h" - -UpdateManager::UpdateManager(QObject *parent) : - QObject(parent), - isAutomaticCheck(false) -{ - // is this the first time this version was run? - QSettings settings; - settings.beginGroup("UpdateManager"); - if (settings.contains("DontCheckForUpdates") && settings.value("DontCheckForUpdates") == "TRUE") - return; - if (settings.contains("LastVersionUsed")) { - // we have checked at least once before - if (settings.value("LastVersionUsed").toString() != subsurface_git_version()) { - // we have just updated - wait two weeks before you check again - settings.setValue("LastVersionUsed", QString(subsurface_git_version())); - settings.setValue("NextCheck", QDateTime::currentDateTime().addDays(14).toString(Qt::ISODate)); - } else { - // is it time to check again? - QString nextCheckString = settings.value("NextCheck").toString(); - QDateTime nextCheck = QDateTime::fromString(nextCheckString, Qt::ISODate); - if (nextCheck > QDateTime::currentDateTime()) - return; - } - } - settings.setValue("LastVersionUsed", QString(subsurface_git_version())); - settings.setValue("NextCheck", QDateTime::currentDateTime().addDays(14).toString(Qt::ISODate)); - checkForUpdates(true); -} - -void UpdateManager::checkForUpdates(bool automatic) -{ - QString os; - -#if defined(Q_OS_WIN) - os = "win"; -#elif defined(Q_OS_MAC) - os = "osx"; -#elif defined(Q_OS_LINUX) - os = "linux"; -#else - os = "unknown"; -#endif - isAutomaticCheck = automatic; - QString version = subsurface_canonical_version(); - QString uuidString = getUUID(); - QString url = QString("http://subsurface-divelog.org/updatecheck.html?os=%1&version=%2&uuid=%3").arg(os, version, uuidString); - QNetworkRequest request; - request.setUrl(url); - request.setRawHeader("Accept", "text/xml"); - QString userAgent = getUserAgent(); - request.setRawHeader("User-Agent", userAgent.toUtf8()); - connect(SubsurfaceWebServices::manager()->get(request), SIGNAL(finished()), this, SLOT(requestReceived()), Qt::UniqueConnection); -} - -QString UpdateManager::getUUID() -{ - QString uuidString; - QSettings settings; - settings.beginGroup("UpdateManager"); - if (settings.contains("UUID")) { - uuidString = settings.value("UUID").toString(); - } else { - QUuid uuid = QUuid::createUuid(); - uuidString = uuid.toString(); - settings.setValue("UUID", uuidString); - } - uuidString.replace("{", "").replace("}", ""); - return uuidString; -} - -void UpdateManager::requestReceived() -{ - bool haveNewVersion = false; - QMessageBox msgbox; - QString msgTitle = tr("Check for updates."); - QString msgText = "

" + tr("Subsurface was unable to check for updates.") + "

"; - - QNetworkReply *reply = qobject_cast(sender()); - if (reply->error() != QNetworkReply::NoError) { - //Network Error - msgText = msgText + "
" + tr("The following error occurred:") + "
" + reply->errorString() - + "

" + tr("Please check your internet connection.") + ""; - } else { - //No network error - QString responseBody(reply->readAll()); - QString responseLink; - if (responseBody.contains('"')) - responseLink = responseBody.split("\"").at(1); - - msgbox.setIcon(QMessageBox::Information); - if (responseBody == "OK") { - msgText = tr("You are using the latest version of Subsurface."); - } else if (responseBody.startsWith("[\"http")) { - haveNewVersion = true; - msgText = tr("A new version of Subsurface is available.
Click on:
%1
to download it.") - .arg(responseLink); - } else if (responseBody.startsWith("Latest version")) { - // the webservice backend doesn't localize - but it's easy enough to just replace the - // strings that it is likely to send back - haveNewVersion = true; - msgText = QString("") + tr("A new version of Subsurface is available.") + QString("

") + - tr("Latest version is %1, please check %2 our download page %3 for information in how to update.") - .arg(responseLink).arg("").arg(""); - } else { - // the webservice backend doesn't localize - but it's easy enough to just replace the - // strings that it is likely to send back - if (!responseBody.contains("latest development") && - !responseBody.contains("newer") && - !responseBody.contains("beta", Qt::CaseInsensitive)) - haveNewVersion = true; - if (responseBody.contains("Newest release version is ")) - responseBody.replace("Newest release version is ", tr("Newest release version is ")); - msgText = tr("The server returned the following information:").append("

").append(responseBody); - msgbox.setIcon(QMessageBox::Warning); - } - } -#ifndef SUBSURFACE_MOBILE - if (haveNewVersion || !isAutomaticCheck) { - msgbox.setWindowTitle(msgTitle); - msgbox.setWindowIcon(QIcon(":/subsurface-icon")); - msgbox.setText(msgText); - msgbox.setTextFormat(Qt::RichText); - msgbox.exec(); - } - if (isAutomaticCheck) { - QSettings settings; - settings.beginGroup("UpdateManager"); - if (!settings.contains("DontCheckForUpdates")) { - // we allow an opt out of future checks - QMessageBox response(MainWindow::instance()); - QString message = tr("Subsurface is checking every two weeks if a new version is available. If you don't want Subsurface to continue checking, please click Decline."); - response.addButton(tr("Decline"), QMessageBox::RejectRole); - response.addButton(tr("Accept"), QMessageBox::AcceptRole); - response.setText(message); - response.setWindowTitle(tr("Automatic check for updates")); - response.setIcon(QMessageBox::Question); - response.setWindowModality(Qt::WindowModal); - int ret = response.exec(); - if (ret == QMessageBox::Accepted) - settings.setValue("DontCheckForUpdates", "FALSE"); - else - settings.setValue("DontCheckForUpdates", "TRUE"); - } - } -#endif -} diff --git a/qt-ui/updatemanager.h b/qt-ui/updatemanager.h deleted file mode 100644 index f91c82dc8..000000000 --- a/qt-ui/updatemanager.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef UPDATEMANAGER_H -#define UPDATEMANAGER_H - -#include - -class QNetworkAccessManager; -class QNetworkReply; - -class UpdateManager : public QObject { - Q_OBJECT -public: - explicit UpdateManager(QObject *parent = 0); - void checkForUpdates(bool automatic = false); - static QString getUUID(); - -public -slots: - void requestReceived(); - -private: - bool isAutomaticCheck; -}; - -#endif // UPDATEMANAGER_H diff --git a/qt-ui/urldialog.ui b/qt-ui/urldialog.ui deleted file mode 100644 index 397f90a64..000000000 --- a/qt-ui/urldialog.ui +++ /dev/null @@ -1,91 +0,0 @@ - - - URLDialog - - - - 0 - 0 - 397 - 103 - - - - Dialog - - - - - 40 - 60 - 341 - 32 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 10 - 30 - 371 - 21 - - - - - - - 10 - 10 - 151 - 16 - - - - Enter URL for images - - - - - - - buttonBox - accepted() - URLDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - URLDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/qt-ui/usermanual.cpp b/qt-ui/usermanual.cpp deleted file mode 100644 index 6b676f16b..000000000 --- a/qt-ui/usermanual.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include -#include -#include - -#include "usermanual.h" -#include "mainwindow.h" -#include "helpers.h" - -SearchBar::SearchBar(QWidget *parent): QWidget(parent) -{ - ui.setupUi(this); - #if defined(Q_OS_MAC) || defined(Q_OS_WIN) - ui.findNext->setIcon(QIcon(":icons/subsurface/32x32/actions/go-down.png")); - ui.findPrev->setIcon(QIcon(":icons/subsurface/32x32/actions/go-up.png")); - ui.findClose->setIcon(QIcon(":icons/subsurface/32x32/actions/window-close.png")); - #endif - - connect(ui.findNext, SIGNAL(pressed()), this, SIGNAL(searchNext())); - connect(ui.findPrev, SIGNAL(pressed()), this, SIGNAL(searchPrev())); - connect(ui.searchEdit, SIGNAL(textChanged(QString)), this, SIGNAL(searchTextChanged(QString))); - connect(ui.searchEdit, SIGNAL(textChanged(QString)), this, SLOT(enableButtons(QString))); - connect(ui.findClose, SIGNAL(pressed()), this, SLOT(hide())); -} - -void SearchBar::setVisible(bool visible) -{ - QWidget::setVisible(visible); - ui.searchEdit->setFocus(); -} - -void SearchBar::enableButtons(const QString &s) -{ - ui.findPrev->setEnabled(s.length()); - ui.findNext->setEnabled(s.length()); -} - -UserManual::UserManual(QWidget *parent) : QWidget(parent) -{ - QShortcut *closeKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(closeKey, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quitKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quitKey, SIGNAL(activated()), qApp, SLOT(quit())); - - QAction *actionShowSearch = new QAction(this); - actionShowSearch->setShortcut(Qt::CTRL + Qt::Key_F); - actionShowSearch->setShortcutContext(Qt::WindowShortcut); - addAction(actionShowSearch); - - QAction *actionHideSearch = new QAction(this); - actionHideSearch->setShortcut(Qt::Key_Escape); - actionHideSearch->setShortcutContext(Qt::WindowShortcut); - addAction(actionHideSearch); - - setWindowTitle(tr("User manual")); - setWindowIcon(QIcon(":/subsurface-icon")); - - userManual = new QWebView(this); - QString colorBack = palette().highlight().color().name(QColor::HexRgb); - QString colorText = palette().highlightedText().color().name(QColor::HexRgb); - userManual->setStyleSheet(QString("QWebView { selection-background-color: %1; selection-color: %2; }") - .arg(colorBack).arg(colorText)); - userManual->page()->setLinkDelegationPolicy(QWebPage::DelegateExternalLinks); - QString searchPath = getSubsurfaceDataPath("Documentation"); - if (searchPath.size()) { - // look for localized versions of the manual first - QString lang = uiLanguage(NULL); - QString prefix = searchPath.append("/user-manual"); - QFile manual(prefix + "_" + lang + ".html"); - if (!manual.exists()) - manual.setFileName(prefix + "_" + lang.left(2) + ".html"); - if (!manual.exists()) - manual.setFileName(prefix + ".html"); - if (!manual.exists()) { - userManual->setHtml(tr("Cannot find the Subsurface manual")); - } else { - QString urlString = QString("file:///") + manual.fileName(); - userManual->setUrl(QUrl(urlString, QUrl::TolerantMode)); - } - } else { - userManual->setHtml(tr("Cannot find the Subsurface manual")); - } - - searchBar = new SearchBar(this); - searchBar->hide(); - connect(actionShowSearch, SIGNAL(triggered(bool)), searchBar, SLOT(show())); - connect(actionHideSearch, SIGNAL(triggered(bool)), searchBar, SLOT(hide())); - connect(userManual, SIGNAL(linkClicked(QUrl)), this, SLOT(linkClickedSlot(QUrl))); - connect(searchBar, SIGNAL(searchTextChanged(QString)), this, SLOT(searchTextChanged(QString))); - connect(searchBar, SIGNAL(searchNext()), this, SLOT(searchNext())); - connect(searchBar, SIGNAL(searchPrev()), this, SLOT(searchPrev())); - - QVBoxLayout *vboxLayout = new QVBoxLayout(); - userManual->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); - vboxLayout->addWidget(userManual); - vboxLayout->addWidget(searchBar); - setLayout(vboxLayout); -} - -void UserManual::search(QString text, QWebPage::FindFlags flags = 0) -{ - if (userManual->findText(text, QWebPage::FindWrapsAroundDocument | flags) || text.length() == 0) { - searchBar->setStyleSheet(""); - } else { - searchBar->setStyleSheet("QLineEdit{background: red;}"); - } -} - -void UserManual::searchTextChanged(const QString& text) -{ - mLastText = text; - search(text); -} - -void UserManual::searchNext() -{ - search(mLastText); -} - -void UserManual::searchPrev() -{ - search(mLastText, QWebPage::FindBackward); -} - -void UserManual::linkClickedSlot(const QUrl& url) -{ - QDesktopServices::openUrl(url); -} - -#ifdef Q_OS_MAC -void UserManual::showEvent(QShowEvent *e) { - filterAction = NULL; - closeAction = NULL; - MainWindow *m = MainWindow::instance(); - Q_FOREACH (QObject *o, m->children()) { - if (o->objectName() == "actionFilterTags") { - filterAction = qobject_cast(o); - filterAction->setShortcut(QKeySequence()); - } else if (o->objectName() == "actionClose") { - closeAction = qobject_cast(o); - closeAction->setShortcut(QKeySequence()); - } - } -} -void UserManual::hideEvent(QHideEvent *e) { - if (closeAction != NULL) - closeAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_W)); - if (filterAction != NULL) - filterAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F)); - closeAction = filterAction = NULL; -} -#endif diff --git a/qt-ui/usermanual.h b/qt-ui/usermanual.h deleted file mode 100644 index 5101a3c3b..000000000 --- a/qt-ui/usermanual.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef USERMANUAL_H -#define USERMANUAL_H - -#include - -#include "ui_searchbar.h" - -class SearchBar : public QWidget{ - Q_OBJECT -public: - SearchBar(QWidget *parent = 0); -signals: - void searchTextChanged(const QString& s); - void searchNext(); - void searchPrev(); -protected: - void setVisible(bool visible); -private slots: - void enableButtons(const QString& s); -private: - Ui::SearchBar ui; -}; - -class UserManual : public QWidget { - Q_OBJECT - -public: - explicit UserManual(QWidget *parent = 0); - -#ifdef Q_OS_MAC -protected: - void showEvent(QShowEvent *e); - void hideEvent(QHideEvent *e); - QAction *closeAction; - QAction *filterAction; -#endif - -private -slots: - void searchTextChanged(const QString& s); - void searchNext(); - void searchPrev(); - void linkClickedSlot(const QUrl& url); -private: - QWebView *userManual; - SearchBar *searchBar; - QString mLastText; - void search(QString, QWebPage::FindFlags); -}; -#endif // USERMANUAL_H diff --git a/qt-ui/usersurvey.cpp b/qt-ui/usersurvey.cpp deleted file mode 100644 index 05da582a1..000000000 --- a/qt-ui/usersurvey.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include -#include -#include - -#include "usersurvey.h" -#include "ui_usersurvey.h" -#include "version.h" -#include "subsurfacewebservices.h" -#include "updatemanager.h" - -#include "helpers.h" -#include "subsurfacesysinfo.h" - -UserSurvey::UserSurvey(QWidget *parent) : QDialog(parent), - ui(new Ui::UserSurvey) -{ - ui->setupUi(this); - ui->buttonBox->buttons().first()->setText(tr("Send")); - this->adjustSize(); - QShortcut *closeKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); - connect(closeKey, SIGNAL(activated()), this, SLOT(close())); - QShortcut *quitKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); - connect(quitKey, SIGNAL(activated()), parent, SLOT(close())); - - os = QString("ssrfVers=%1").arg(subsurface_version()); - os.append(QString("&prettyOsName=%1").arg(SubsurfaceSysInfo::prettyOsName())); - QString arch = SubsurfaceSysInfo::buildCpuArchitecture(); - os.append(QString("&appCpuArch=%1").arg(arch)); - if (arch == "i386") { - QString osArch = SubsurfaceSysInfo::currentCpuArchitecture(); - os.append(QString("&osCpuArch=%1").arg(osArch)); - } - os.append(QString("&uiLang=%1").arg(uiLanguage(NULL))); - os.append(QString("&uuid=%1").arg(UpdateManager::getUUID())); - ui->system->setPlainText(getVersion()); -} - -QString UserSurvey::getVersion() -{ - QString arch; - // fill in the system data - QString sysInfo = QString("Subsurface %1").arg(subsurface_version()); - sysInfo.append(tr("\nOperating system: %1").arg(SubsurfaceSysInfo::prettyOsName())); - arch = SubsurfaceSysInfo::buildCpuArchitecture(); - sysInfo.append(tr("\nCPU architecture: %1").arg(arch)); - if (arch == "i386") - sysInfo.append(tr("\nOS CPU architecture: %1").arg(SubsurfaceSysInfo::currentCpuArchitecture())); - sysInfo.append(tr("\nLanguage: %1").arg(uiLanguage(NULL))); - return sysInfo; -} - -UserSurvey::~UserSurvey() -{ - delete ui; -} - -#define ADD_OPTION(_name) values.append(ui->_name->isChecked() ? "&" #_name "=1" : "&" #_name "=0") - -void UserSurvey::on_buttonBox_accepted() -{ - // now we need to collect the data and submit it - QString values = os; - ADD_OPTION(recreational); - ADD_OPTION(tech); - ADD_OPTION(planning); - ADD_OPTION(download); - ADD_OPTION(divecomputer); - ADD_OPTION(manual); - ADD_OPTION(companion); - values.append(QString("&suggestion=%1").arg(ui->suggestions->toPlainText())); - UserSurveyServices uss(this); - connect(uss.sendSurvey(values), SIGNAL(finished()), SLOT(requestReceived())); - hide(); -} - -void UserSurvey::on_buttonBox_rejected() -{ - QMessageBox response(this); - response.setText(tr("Should we ask you later?")); - response.addButton(tr("Don't ask me again"), QMessageBox::RejectRole); - response.addButton(tr("Ask later"), QMessageBox::AcceptRole); - response.setWindowTitle(tr("Ask again?")); // Not displayed on MacOSX as described in Qt API - response.setIcon(QMessageBox::Question); - response.setWindowModality(Qt::WindowModal); - switch (response.exec()) { - case QDialog::Accepted: - // nothing to do here, we'll just ask again the next time they start - break; - case QDialog::Rejected: - QSettings s; - s.beginGroup("UserSurvey"); - s.setValue("SurveyDone", "declined"); - break; - } - hide(); -} - -void UserSurvey::requestReceived() -{ - QMessageBox msgbox; - QString msgTitle = tr("Submit user survey."); - QString msgText = "

" + tr("Subsurface was unable to submit the user survey.") + "

"; - - QNetworkReply *reply = qobject_cast(sender()); - if (reply->error() != QNetworkReply::NoError) { - //Network Error - msgText = msgText + "
" + tr("The following error occurred:") + "
" + reply->errorString() - + "

" + tr("Please check your internet connection.") + ""; - } else { - //No network error - QString response(reply->readAll()); - QString responseBody = response.split("\"").at(1); - - msgbox.setIcon(QMessageBox::Information); - - if (responseBody == "OK") { - msgText = tr("Survey successfully submitted."); - QSettings s; - s.beginGroup("UserSurvey"); - s.setValue("SurveyDone", "submitted"); - } else { - msgText = tr("There was an error while trying to check for updates.

%1").arg(responseBody); - msgbox.setIcon(QMessageBox::Warning); - } - } - - msgbox.setWindowTitle(msgTitle); - msgbox.setWindowIcon(QIcon(":/subsurface-icon")); - msgbox.setText(msgText); - msgbox.setTextFormat(Qt::RichText); - msgbox.exec(); - reply->deleteLater(); -} diff --git a/qt-ui/usersurvey.h b/qt-ui/usersurvey.h deleted file mode 100644 index 1dd5aaab3..000000000 --- a/qt-ui/usersurvey.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef USERSURVEY_H -#define USERSURVEY_H - -#include -class QNetworkAccessManager; -class QNetworkReply; - -namespace Ui { - class UserSurvey; -} - -class UserSurvey : public QDialog { - Q_OBJECT - -public: - explicit UserSurvey(QWidget *parent = 0); - ~UserSurvey(); - static QString getVersion(); - -private -slots: - void on_buttonBox_accepted(); - void on_buttonBox_rejected(); - void requestReceived(); - -private: - Ui::UserSurvey *ui; - QString os; -}; -#endif // USERSURVEY_H diff --git a/qt-ui/usersurvey.ui b/qt-ui/usersurvey.ui deleted file mode 100644 index c118fe89d..000000000 --- a/qt-ui/usersurvey.ui +++ /dev/null @@ -1,301 +0,0 @@ - - - UserSurvey - - - - 0 - 0 - 538 - 714 - - - - User survey - - - - - 180 - 10 - 241 - 21 - - - - - 11 - - - - Subsurface user survey - - - - - - 9 - 36 - 521 - 91 - - - - <html><head/><body><p>We would love to learn more about our users, their preferences and their usage habits. Please spare a minute to fill out this form and submit it to the Subsurface team.</p></body></html> - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - 10 - 190 - 161 - 26 - - - - Technical diver - - - - - - 180 - 190 - 181 - 26 - - - - Recreational diver - - - - - - 380 - 190 - 141 - 26 - - - - Dive planner - - - - - - 10 - 270 - 481 - 26 - - - - Supported dive computer - - - - - - 10 - 300 - 491 - 26 - - - - Other software/sources - - - - - - 10 - 330 - 501 - 26 - - - - Manually entering dives - - - - - - 10 - 360 - 491 - 26 - - - - Android/iPhone companion app - - - - - - 9 - 410 - 501 - 21 - - - - Any suggestions? (in English) - - - - - - 10 - 440 - 521 - 71 - - - - - - - 9 - 520 - 511 - 51 - - - - The following information about your system will also be submitted. - - - true - - - - - - 9 - 590 - 521 - 71 - - - - - 0 - 0 - - - - true - - - - - - 350 - 670 - 176 - 31 - - - - Qt::TabFocus - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - - - - - 10 - 160 - 451 - 21 - - - - What kind of diver are you? - - - - - - 10 - 140 - 511 - 20 - - - - Qt::Horizontal - - - - - - 10 - 220 - 511 - 20 - - - - Qt::Horizontal - - - - - - 10 - 240 - 491 - 21 - - - - Where are you importing data from? - - - - - - 10 - 390 - 511 - 20 - - - - Qt::Horizontal - - - - - tech - recreational - planning - download - divecomputer - manual - companion - suggestions - system - buttonBox - - - - diff --git a/qt-ui/webservices.ui b/qt-ui/webservices.ui deleted file mode 100644 index bcca35f1e..000000000 --- a/qt-ui/webservices.ui +++ /dev/null @@ -1,156 +0,0 @@ - - - WebServices - - - - 0 - 0 - 425 - 169 - - - - Web service connection - - - - :/subsurface-icon - - - - - - - Status: - - - - - - - Enter your ID here - - - - 420 - - - - - - - - Download - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help - - - - - - - 0 - - - - - - - User ID - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Save user ID locally? - - - - - - - Password - - - - - - - QLineEdit::Password - - - - - - - Upload - - - - - - - userID - password - download - upload - buttonBox - - - - - - - buttonBox - accepted() - WebServices - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - WebServices - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/subsurface-core/CMakeLists.txt b/subsurface-core/CMakeLists.txt index e7f531527..60efb7d92 100644 --- a/subsurface-core/CMakeLists.txt +++ b/subsurface-core/CMakeLists.txt @@ -70,6 +70,8 @@ set(SUBSURFACE_CORE_LIB_SRCS divelogexportlogic.cpp qt-init.cpp qtserialbluetooth.cpp + metrics.cpp + color.cpp ${SERIAL_FTDI} ${PLATFORM_SRC} ${BT_CORE_SRC_FILES} diff --git a/subsurface-core/color.cpp b/subsurface-core/color.cpp new file mode 100644 index 000000000..cf6f43916 --- /dev/null +++ b/subsurface-core/color.cpp @@ -0,0 +1,88 @@ +#include "color.h" + +QMap > profile_color; + +void fill_profile_color() +{ +#define COLOR(x, y, z) QVector() << x << y << z; + profile_color[SAC_1] = COLOR(FUNGREEN1, BLACK1_LOW_TRANS, FUNGREEN1); + profile_color[SAC_2] = COLOR(APPLE1, BLACK1_LOW_TRANS, APPLE1); + profile_color[SAC_3] = COLOR(ATLANTIS1, BLACK1_LOW_TRANS, ATLANTIS1); + profile_color[SAC_4] = COLOR(ATLANTIS2, BLACK1_LOW_TRANS, ATLANTIS2); + profile_color[SAC_5] = COLOR(EARLSGREEN1, BLACK1_LOW_TRANS, EARLSGREEN1); + profile_color[SAC_6] = COLOR(HOKEYPOKEY1, BLACK1_LOW_TRANS, HOKEYPOKEY1); + profile_color[SAC_7] = COLOR(TUSCANY1, BLACK1_LOW_TRANS, TUSCANY1); + profile_color[SAC_8] = COLOR(CINNABAR1, BLACK1_LOW_TRANS, CINNABAR1); + profile_color[SAC_9] = COLOR(REDORANGE1, BLACK1_LOW_TRANS, REDORANGE1); + + profile_color[VELO_STABLE] = COLOR(CAMARONE1, BLACK1_LOW_TRANS, CAMARONE1); + profile_color[VELO_SLOW] = COLOR(LIMENADE1, BLACK1_LOW_TRANS, LIMENADE1); + profile_color[VELO_MODERATE] = COLOR(RIOGRANDE1, BLACK1_LOW_TRANS, RIOGRANDE1); + profile_color[VELO_FAST] = COLOR(PIRATEGOLD1, BLACK1_LOW_TRANS, PIRATEGOLD1); + profile_color[VELO_CRAZY] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + + profile_color[PO2] = COLOR(APPLE1, BLACK1_LOW_TRANS, APPLE1); + profile_color[PO2_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[PN2] = COLOR(BLACK1_LOW_TRANS, BLACK1_LOW_TRANS, BLACK1_LOW_TRANS); + profile_color[PN2_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[PHE] = COLOR(PEANUT, BLACK1_LOW_TRANS, PEANUT); + profile_color[PHE_ALERT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[O2SETPOINT] = COLOR(RED1, BLACK1_LOW_TRANS, RED1); + profile_color[CCRSENSOR1] = COLOR(TUNDORA1_MED_TRANS, BLACK1_LOW_TRANS, TUNDORA1_MED_TRANS); + profile_color[CCRSENSOR2] = COLOR(ROYALBLUE2_LOW_TRANS, BLACK1_LOW_TRANS, ROYALBLUE2_LOW_TRANS); + profile_color[CCRSENSOR3] = COLOR(PEANUT, BLACK1_LOW_TRANS, PEANUT); + profile_color[PP_LINES] = COLOR(BLACK1_HIGH_TRANS, BLACK1_LOW_TRANS, BLACK1_HIGH_TRANS); + + profile_color[TEXT_BACKGROUND] = COLOR(CONCRETE1_LOWER_TRANS, WHITE1, CONCRETE1_LOWER_TRANS); + profile_color[ALERT_BG] = COLOR(BROOM1_LOWER_TRANS, BLACK1_LOW_TRANS, BROOM1_LOWER_TRANS); + profile_color[ALERT_FG] = COLOR(BLACK1_LOW_TRANS, WHITE1, BLACK1_LOW_TRANS); + profile_color[EVENTS] = COLOR(REDORANGE1, BLACK1_LOW_TRANS, REDORANGE1); + profile_color[SAMPLE_DEEP] = COLOR(QColor(Qt::red).darker(), BLACK1, PERSIANRED1); + profile_color[SAMPLE_SHALLOW] = COLOR(QColor(Qt::red).lighter(), BLACK1_LOW_TRANS, PERSIANRED1); + profile_color[SMOOTHED] = COLOR(REDORANGE1_HIGH_TRANS, BLACK1_LOW_TRANS, REDORANGE1_HIGH_TRANS); + profile_color[MINUTE] = COLOR(MEDIUMREDVIOLET1_HIGHER_TRANS, BLACK1_LOW_TRANS, MEDIUMREDVIOLET1_HIGHER_TRANS); + profile_color[TIME_GRID] = COLOR(WHITE1, BLACK1_HIGH_TRANS, TUNDORA1_MED_TRANS); + profile_color[TIME_TEXT] = COLOR(FORESTGREEN1, BLACK1, FORESTGREEN1); + profile_color[DEPTH_GRID] = COLOR(WHITE1, BLACK1_HIGH_TRANS, TUNDORA1_MED_TRANS); + profile_color[MEAN_DEPTH] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); + profile_color[HR_PLOT] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); + profile_color[HR_TEXT] = COLOR(REDORANGE1_MED_TRANS, BLACK1_LOW_TRANS, REDORANGE1_MED_TRANS); + profile_color[HR_AXIS] = COLOR(MED_GRAY_HIGH_TRANS, MED_GRAY_HIGH_TRANS, MED_GRAY_HIGH_TRANS); + profile_color[DEPTH_BOTTOM] = COLOR(GOVERNORBAY1_MED_TRANS, BLACK1_HIGH_TRANS, GOVERNORBAY1_MED_TRANS); + profile_color[DEPTH_TOP] = COLOR(MERCURY1_MED_TRANS, WHITE1_MED_TRANS, MERCURY1_MED_TRANS); + profile_color[TEMP_TEXT] = COLOR(GOVERNORBAY2, BLACK1_LOW_TRANS, GOVERNORBAY2); + profile_color[TEMP_PLOT] = COLOR(ROYALBLUE2_LOW_TRANS, BLACK1_LOW_TRANS, ROYALBLUE2_LOW_TRANS); + profile_color[SAC_DEFAULT] = COLOR(WHITE1, BLACK1_LOW_TRANS, FORESTGREEN1); + profile_color[BOUNDING_BOX] = COLOR(WHITE1, BLACK1_LOW_TRANS, TUNDORA1_MED_TRANS); + profile_color[PRESSURE_TEXT] = COLOR(KILLARNEY1, BLACK1_LOW_TRANS, KILLARNEY1); + profile_color[BACKGROUND] = COLOR(SPRINGWOOD1, WHITE1, SPRINGWOOD1); + profile_color[BACKGROUND_TRANS] = COLOR(SPRINGWOOD1_MED_TRANS, WHITE1_MED_TRANS, SPRINGWOOD1_MED_TRANS); + profile_color[CEILING_SHALLOW] = COLOR(REDORANGE1_HIGH_TRANS, BLACK1_HIGH_TRANS, REDORANGE1_HIGH_TRANS); + profile_color[CEILING_DEEP] = COLOR(RED1_MED_TRANS, BLACK1_HIGH_TRANS, RED1_MED_TRANS); + profile_color[CALC_CEILING_SHALLOW] = COLOR(FUNGREEN1_HIGH_TRANS, BLACK1_HIGH_TRANS, FUNGREEN1_HIGH_TRANS); + profile_color[CALC_CEILING_DEEP] = COLOR(APPLE1_HIGH_TRANS, BLACK1_HIGH_TRANS, APPLE1_HIGH_TRANS); + profile_color[TISSUE_PERCENTAGE] = COLOR(GOVERNORBAY2, BLACK1_LOW_TRANS, GOVERNORBAY2); + profile_color[GF_LINE] = COLOR(BLACK1, BLACK1_LOW_TRANS, BLACK1); + profile_color[AMB_PRESSURE_LINE] = COLOR(TUNDORA1_MED_TRANS, BLACK1_LOW_TRANS, ATLANTIS1); +#undef COLOR +} + +QColor getColor(const color_indice_t i, bool isGrayscale) +{ + if (profile_color.count() > i && i >= 0) + return profile_color[i].at((isGrayscale) ? 1 : 0); + return QColor(Qt::black); +} + +QColor getSacColor(int sac, int avg_sac) +{ + int sac_index = 0; + int delta = sac - avg_sac + 7000; + + sac_index = delta / 2000; + if (sac_index < 0) + sac_index = 0; + if (sac_index > SAC_COLORS - 1) + sac_index = SAC_COLORS - 1; + return getColor((color_indice_t)(SAC_COLORS_START_IDX + sac_index), false); +} diff --git a/subsurface-core/color.h b/subsurface-core/color.h index 7938e59a6..57ad77242 100644 --- a/subsurface-core/color.h +++ b/subsurface-core/color.h @@ -5,6 +5,8 @@ from http://chir.ag/projects/name-that-color */ #include +#include +#include // Greens #define CAMARONE1 QColor::fromRgbF(0.0, 0.4, 0.0, 1) @@ -64,4 +66,87 @@ // Magentas #define MEDIUMREDVIOLET1_HIGHER_TRANS QColor::fromRgbF(0.7, 0.2, 0.7, 0.1) +#define SAC_COLORS_START_IDX SAC_1 +#define SAC_COLORS 9 +#define VELOCITY_COLORS_START_IDX VELO_STABLE +#define VELOCITY_COLORS 5 + +typedef enum { + /* SAC colors. Order is important, the SAC_COLORS_START_IDX define above. */ + SAC_1, + SAC_2, + SAC_3, + SAC_4, + SAC_5, + SAC_6, + SAC_7, + SAC_8, + SAC_9, + + /* Velocity colors. Order is still important, ref VELOCITY_COLORS_START_IDX. */ + VELO_STABLE, + VELO_SLOW, + VELO_MODERATE, + VELO_FAST, + VELO_CRAZY, + + /* gas colors */ + PO2, + PO2_ALERT, + PN2, + PN2_ALERT, + PHE, + PHE_ALERT, + O2SETPOINT, + CCRSENSOR1, + CCRSENSOR2, + CCRSENSOR3, + PP_LINES, + + /* Other colors */ + TEXT_BACKGROUND, + ALERT_BG, + ALERT_FG, + EVENTS, + SAMPLE_DEEP, + SAMPLE_SHALLOW, + SMOOTHED, + MINUTE, + TIME_GRID, + TIME_TEXT, + DEPTH_GRID, + MEAN_DEPTH, + HR_TEXT, + HR_PLOT, + HR_AXIS, + DEPTH_TOP, + DEPTH_BOTTOM, + TEMP_TEXT, + TEMP_PLOT, + SAC_DEFAULT, + BOUNDING_BOX, + PRESSURE_TEXT, + BACKGROUND, + BACKGROUND_TRANS, + CEILING_SHALLOW, + CEILING_DEEP, + CALC_CEILING_SHALLOW, + CALC_CEILING_DEEP, + TISSUE_PERCENTAGE, + GF_LINE, + AMB_PRESSURE_LINE +} color_indice_t; + +extern QMap > profile_color; +void fill_profile_color(); +QColor getColor(const color_indice_t i, bool isGrayscale = false); +QColor getSacColor(int sac, int diveSac); +struct text_render_options { + double size; + color_indice_t color; + double hpos, vpos; +}; + +typedef text_render_options text_render_options_t; + #endif // COLOR_H diff --git a/subsurface-core/metrics.cpp b/subsurface-core/metrics.cpp new file mode 100644 index 000000000..203c2e5e2 --- /dev/null +++ b/subsurface-core/metrics.cpp @@ -0,0 +1,50 @@ +/* + * metrics.cpp + * + * methods to find/compute essential UI metrics + * (font properties, icon sizes, etc) + * + */ + +#include "metrics.h" + +static IconMetrics dfltIconMetrics = { -1 }; + +QFont defaultModelFont() +{ + QFont font; +// font.setPointSizeF(font.pointSizeF() * 0.8); + return font; +} + +QFontMetrics defaultModelFontMetrics() +{ + return QFontMetrics(defaultModelFont()); +} + +// return the default icon size, computed as the multiple of 16 closest to +// the given height +static int defaultIconSize(int height) +{ + int ret = (height + 8)/16; + ret *= 16; + if (ret < 16) + ret = 16; + return ret; +} + +const IconMetrics & defaultIconMetrics() +{ + if (dfltIconMetrics.sz_small == -1) { + int small = defaultIconSize(defaultModelFontMetrics().height()); + dfltIconMetrics.sz_small = small; + dfltIconMetrics.sz_med = small + small/2; + dfltIconMetrics.sz_big = 2*small; + + dfltIconMetrics.sz_pic = 8*small; + + dfltIconMetrics.spacing = small/8; + } + + return dfltIconMetrics; +} diff --git a/subsurface-core/metrics.h b/subsurface-core/metrics.h new file mode 100644 index 000000000..30295a3d8 --- /dev/null +++ b/subsurface-core/metrics.h @@ -0,0 +1,32 @@ +/* + * metrics.h + * + * header file for common function to find/compute essential UI metrics + * (font properties, icon sizes, etc) + * + */ +#ifndef METRICS_H +#define METRICS_H + +#include +#include +#include + +QFont defaultModelFont(); +QFontMetrics defaultModelFontMetrics(); + +// Collection of icon/picture sizes and other metrics, resolution independent +struct IconMetrics { + // icon sizes + int sz_small; // ex 16px + int sz_med; // ex 24px + int sz_big; // ex 32px + // picture size + int sz_pic; // ex 128px + // icon spacing + int spacing; // ex 2px +}; + +const IconMetrics & defaultIconMetrics(); + +#endif // METRICS_H -- cgit v1.2.3-70-g09d2