aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorGravatar Dirk Hohndel <dirk@hohndel.org>2016-04-04 22:02:03 -0700
committerGravatar Dirk Hohndel <dirk@hohndel.org>2016-04-04 22:33:58 -0700
commit7be962bfc2879a72c32ff67518731347dcdff6de (patch)
treed05bf7ab234a448ee37a15b608e2b939f2285d07 /core
parent2d760a7bff71c46c5aeba37c40d236ea16eefea2 (diff)
downloadsubsurface-7be962bfc2879a72c32ff67518731347dcdff6de.tar.gz
Move subsurface-core to core and qt-mobile to mobile-widgets
Having subsurface-core as a directory name really messes with autocomplete and is obviously redundant. Simmilarly, qt-mobile caused an autocomplete conflict and also was inconsistent with the desktop-widget name for the directory containing the "other" UI. And while cleaning up the resulting change in the path name for include files, I decided to clean up those even more to make them consistent overall. This could have been handled in more commits, but since this requires a make clean before the build, it seemed more sensible to do it all in one. Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'core')
-rw-r--r--core/CMakeLists.txt98
-rw-r--r--core/android.cpp199
-rw-r--r--core/checkcloudconnection.cpp106
-rw-r--r--core/checkcloudconnection.h22
-rw-r--r--core/cloudstorage.cpp109
-rw-r--r--core/cloudstorage.h27
-rw-r--r--core/cochran.c809
-rw-r--r--core/cochran.h44
-rw-r--r--core/color.cpp88
-rw-r--r--core/color.h152
-rw-r--r--core/compressibility.r115
-rw-r--r--core/configuredivecomputer.cpp681
-rw-r--r--core/configuredivecomputer.h68
-rw-r--r--core/configuredivecomputerthreads.cpp1778
-rw-r--r--core/configuredivecomputerthreads.h60
-rw-r--r--core/datatrak.c698
-rw-r--r--core/datatrak.h41
-rw-r--r--core/deco.c601
-rw-r--r--core/deco.h20
-rw-r--r--core/device.c184
-rw-r--r--core/device.h18
-rw-r--r--core/devicedetails.cpp70
-rw-r--r--core/devicedetails.h104
-rw-r--r--core/display.h63
-rw-r--r--core/dive.c3561
-rw-r--r--core/dive.h913
-rw-r--r--core/divecomputer.cpp228
-rw-r--r--core/divecomputer.h38
-rw-r--r--core/divelist.c1207
-rw-r--r--core/divelist.h62
-rw-r--r--core/divelogexportlogic.cpp161
-rw-r--r--core/divelogexportlogic.h20
-rw-r--r--core/divesite.c337
-rw-r--r--core/divesite.cpp31
-rw-r--r--core/divesite.h80
-rw-r--r--core/divesitehelpers.cpp208
-rw-r--r--core/divesitehelpers.h18
-rw-r--r--core/equipment.c238
-rw-r--r--core/exif.cpp587
-rw-r--r--core/exif.h147
-rw-r--r--core/file.c1115
-rw-r--r--core/file.h24
-rw-r--r--core/gas-model.c64
-rw-r--r--core/gaspressures.c430
-rw-r--r--core/gaspressures.h35
-rw-r--r--core/gettext.h10
-rw-r--r--core/gettextfromc.cpp27
-rw-r--r--core/gettextfromc.h18
-rw-r--r--core/git-access.c929
-rw-r--r--core/git-access.h36
-rw-r--r--core/gpslocation.cpp606
-rw-r--r--core/gpslocation.h66
-rw-r--r--core/helpers.h56
-rw-r--r--core/imagedownloader.cpp113
-rw-r--r--core/imagedownloader.h34
-rw-r--r--core/isocialnetworkintegration.cpp6
-rw-r--r--core/isocialnetworkintegration.h73
-rw-r--r--core/libdivecomputer.c1081
-rw-r--r--core/libdivecomputer.h72
-rw-r--r--core/linux.c232
-rw-r--r--core/liquivision.c420
-rw-r--r--core/load-git.c1709
-rw-r--r--core/macos.c218
-rw-r--r--core/membuffer.c288
-rw-r--r--core/membuffer.h74
-rw-r--r--core/metrics.cpp65
-rw-r--r--core/metrics.h36
-rw-r--r--core/ostctools.c193
-rw-r--r--core/parse-xml.c3751
-rw-r--r--core/planner.c1471
-rw-r--r--core/planner.h32
-rw-r--r--core/pluginmanager.cpp53
-rw-r--r--core/pluginmanager.h19
-rw-r--r--core/pref.h172
-rw-r--r--core/prefs-macros.h68
-rw-r--r--core/profile.c1544
-rw-r--r--core/profile.h111
-rw-r--r--core/qt-gui.h15
-rw-r--r--core/qt-init.cpp48
-rw-r--r--core/qthelper.cpp1615
-rw-r--r--core/qthelper.h48
-rw-r--r--core/qthelperfromc.h22
-rw-r--r--core/qtserialbluetooth.cpp416
-rw-r--r--core/save-git.c1249
-rw-r--r--core/save-html.c559
-rw-r--r--core/save-html.h31
-rw-r--r--core/save-xml.c749
-rw-r--r--core/serial_ftdi.c665
-rw-r--r--core/sha1.c300
-rw-r--r--core/sha1.h38
-rw-r--r--core/statistics.c404
-rw-r--r--core/statistics.h59
-rw-r--r--core/strndup.h21
-rw-r--r--core/strtod.c128
-rw-r--r--core/subsurface-qt/DiveObjectHelper.cpp338
-rw-r--r--core/subsurface-qt/DiveObjectHelper.h89
-rw-r--r--core/subsurface-qt/SettingsObjectWrapper.cpp1617
-rw-r--r--core/subsurface-qt/SettingsObjectWrapper.h642
-rw-r--r--core/subsurfacestartup.c310
-rw-r--r--core/subsurfacestartup.h26
-rw-r--r--core/subsurfacesysinfo.cpp620
-rw-r--r--core/subsurfacesysinfo.h65
-rw-r--r--core/taxonomy.c48
-rw-r--r--core/taxonomy.h41
-rw-r--r--core/time.c98
-rw-r--r--core/uemis-downloader.c1403
-rw-r--r--core/uemis.c392
-rw-r--r--core/uemis.h54
-rw-r--r--core/units.h280
-rw-r--r--core/version.c18
-rw-r--r--core/version.h19
-rw-r--r--core/webservice.h24
-rw-r--r--core/windows.c454
-rw-r--r--core/windowtitleupdate.cpp32
-rw-r--r--core/windowtitleupdate.h20
-rw-r--r--core/worldmap-options.h7
-rw-r--r--core/worldmap-save.c117
-rw-r--r--core/worldmap-save.h15
118 files changed, 42338 insertions, 0 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
new file mode 100644
index 000000000..d9b1d3421
--- /dev/null
+++ b/core/CMakeLists.txt
@@ -0,0 +1,98 @@
+set(PLATFORM_SRC unknown_platform.c)
+message(STATUS "system name ${CMAKE_SYSTEM_NAME}")
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ if(ANDROID)
+ set(PLATFORM_SRC android.cpp)
+ else()
+ set(PLATFORM_SRC linux.c)
+ endif()
+elseif(CMAKE_SYSTEM_NAME STREQUAL "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 desktop-widgets/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
+ gas-model.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
+ metrics.cpp
+ color.cpp
+ pluginmanager.cpp
+ imagedownloader.cpp
+ isocialnetworkintegration.cpp
+ gpslocation.cpp
+ cloudstorage.cpp
+
+ #Subsurface Qt have the Subsurface structs QObjectified for easy access via QML.
+ subsurface-qt/DiveObjectHelper.cpp
+ subsurface-qt/SettingsObjectWrapper.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/core/android.cpp b/core/android.cpp
new file mode 100644
index 000000000..3631b07a1
--- /dev/null
+++ b/core/android.cpp
@@ -0,0 +1,199 @@
+/* implements Android specific functions */
+#include "dive.h"
+#include "display.h"
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <libusb.h>
+#include <errno.h>
+
+#include <QtAndroidExtras/QtAndroidExtras>
+#include <QtAndroidExtras/QAndroidJniObject>
+#include <QtAndroid>
+
+#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 = -1;
+
+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)
+{
+ // Qt appears to find a working path for us - let's just go with that
+ QString path = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first();
+
+ 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<Name, UsbDevice> of all USB devices attached to Android
+ QAndroidJniObject deviceMap = usbManager.callObjectMethod("getDeviceList", "()Ljava/util/HashMap;");
+ jint num_devices = deviceMap.callMethod<jint>("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<jint>("getVendorId", "()I");
+ productid = usbDevice.callMethod<jint>("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<jboolean>("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<jint>("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/core/checkcloudconnection.cpp b/core/checkcloudconnection.cpp
new file mode 100644
index 000000000..f29d971ba
--- /dev/null
+++ b/core/checkcloudconnection.cpp
@@ -0,0 +1,106 @@
+#include <QObject>
+#include <QTimer>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QEventLoop>
+
+#include "pref.h"
+#include "helpers.h"
+#include "git-access.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()
+{
+ if (verbose)
+ fprintf(stderr, "Checking cloud connection...\n");
+
+ QTimer timer;
+ timer.setSingleShot(true);
+ QEventLoop loop;
+ QNetworkRequest request;
+ request.setRawHeader("Accept", "text/plain");
+ request.setRawHeader("User-Agent", getUserAgent().toUtf8());
+ request.setRawHeader("Client-Id", getUUID().toUtf8());
+ request.setUrl(QString(prefs.cloud_base_url) + TEAPOT);
+ QNetworkAccessManager *mgr = new QNetworkAccessManager();
+ reply = mgr->get(request);
+ connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::sslErrors, this, &CheckCloudConnection::sslErrors);
+ for (int seconds = 1; seconds <= 5; seconds++) {
+ timer.start(1000); // 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";
+ git_storage_update_progress(last_git_storage_update_val + 1, "successfully checked cloud connection");
+ return true;
+ }
+ } else if (seconds < 5) {
+ git_storage_update_progress(last_git_storage_update_val + 1, "waited 1 sec for cloud connection");
+ } else {
+ disconnect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
+ reply->abort();
+ }
+ }
+ git_storage_update_progress(last_git_storage_update_val + 1, "cloud connection failed");
+ 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<QSslError> 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/core/checkcloudconnection.h b/core/checkcloudconnection.h
new file mode 100644
index 000000000..58a412797
--- /dev/null
+++ b/core/checkcloudconnection.h
@@ -0,0 +1,22 @@
+#ifndef CHECKCLOUDCONNECTION_H
+#define CHECKCLOUDCONNECTION_H
+
+#include <QObject>
+#include <QNetworkReply>
+#include <QSsl>
+
+#include "checkcloudconnection.h"
+
+class CheckCloudConnection : public QObject {
+ Q_OBJECT
+public:
+ CheckCloudConnection(QObject *parent = 0);
+ bool checkServer();
+private:
+ QNetworkReply *reply;
+private
+slots:
+ void sslErrors(QList<QSslError> errorList);
+};
+
+#endif // CHECKCLOUDCONNECTION_H
diff --git a/core/cloudstorage.cpp b/core/cloudstorage.cpp
new file mode 100644
index 000000000..575191891
--- /dev/null
+++ b/core/cloudstorage.cpp
@@ -0,0 +1,109 @@
+#include "cloudstorage.h"
+#include "pref.h"
+#include "dive.h"
+#include "helpers.h"
+
+#include <QApplication>
+
+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(const QString& email,const QString& password,const QString& pin,const QString& newpasswd)
+{
+ QString payload(email + QChar(' ') + password);
+ QUrl requestUrl;
+ if (pin.isEmpty() && newpasswd.isEmpty()) {
+ requestUrl = QUrl(CLOUDBACKENDSTORAGE);
+ } else if (!newpasswd.isEmpty()) {
+ requestUrl = QUrl(CLOUDBACKENDUPDATE);
+ payload += QChar(' ') + newpasswd;
+ } else {
+ requestUrl = QUrl(CLOUDBACKENDVERIFY);
+ payload += QChar(' ') + 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 = manager()->post(*request, qPrintable(payload));
+ connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished()));
+ connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(sslErrors(QList<QSslError>)));
+ 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 == QLatin1String("[VERIFIED]") || cloudAuthReply == QLatin1String("[OK]")) {
+ prefs.cloud_verification_status = CS_VERIFIED;
+ /* TODO: Move this to a correct place
+ NotificationWidget *nw = MainWindow::instance()->getNotificationWidget();
+ if (nw->getNotificationText() == myLastError)
+ nw->hideNotification();
+ */
+ myLastError.clear();
+ } else if (cloudAuthReply == QLatin1String("[VERIFY]")) {
+ prefs.cloud_verification_status = CS_NEED_TO_VERIFY;
+ } else if (cloudAuthReply == QLatin1String("[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));
+ /* TODO: Emit a signal with the error
+ MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error);
+ */
+ }
+ emit finishedAuthenticate();
+}
+
+void CloudStorageAuthenticate::uploadError(QNetworkReply::NetworkError)
+{
+ qDebug() << "Received error response from cloud storage backend:" << reply->errorString();
+}
+
+void CloudStorageAuthenticate::sslErrors(QList<QSslError> 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;
+ }
+}
+
+QNetworkAccessManager *manager()
+{
+ static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
+ return manager;
+}
diff --git a/core/cloudstorage.h b/core/cloudstorage.h
new file mode 100644
index 000000000..6addb739d
--- /dev/null
+++ b/core/cloudstorage.h
@@ -0,0 +1,27 @@
+#ifndef CLOUD_STORAGE_H
+#define CLOUD_STORAGE_H
+
+#include <QObject>
+#include <QNetworkReply>
+
+class CloudStorageAuthenticate : public QObject {
+ Q_OBJECT
+public:
+ QNetworkReply* backend(const QString& email,const QString& password,const QString& pin = QString(),const QString& newpasswd = QString());
+ explicit CloudStorageAuthenticate(QObject *parent);
+signals:
+ void finishedAuthenticate();
+ void passwordChangeSuccessful();
+private
+slots:
+ void uploadError(QNetworkReply::NetworkError error);
+ void sslErrors(QList<QSslError> errorList);
+ void uploadFinished();
+private:
+ QNetworkReply *reply;
+ QString userAgent;
+ bool verbose;
+};
+
+QNetworkAccessManager *manager();
+#endif \ No newline at end of file
diff --git a/core/cochran.c b/core/cochran.c
new file mode 100644
index 000000000..b42ed8233
--- /dev/null
+++ b/core/cochran.c
@@ -0,0 +1,809 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "dive.h"
+#include "file.h"
+#include "units.h"
+#include "gettext.h"
+#include "cochran.h"
+#include "divelist.h"
+
+#include <libdivecomputer/parser.h>
+
+#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 ??? <copy>
+ *
+ * 0x45415: Time stamp 17 bytes
+ * 0x45426: Computer configuration page 1, 512 bytes <copy>
+ * 0x45626: Computer configuration page 2, 512 bytes <copy>
+ *
+ */
+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, unsigned 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)
+{
+ (void) filename;
+ 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/core/cochran.h b/core/cochran.h
new file mode 100644
index 000000000..97d4361c8
--- /dev/null
+++ b/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/core/color.cpp b/core/color.cpp
new file mode 100644
index 000000000..cf6f43916
--- /dev/null
+++ b/core/color.cpp
@@ -0,0 +1,88 @@
+#include "color.h"
+
+QMap<color_indice_t, QVector<QColor> > profile_color;
+
+void fill_profile_color()
+{
+#define COLOR(x, y, z) QVector<QColor>() << 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/core/color.h b/core/color.h
new file mode 100644
index 000000000..57ad77242
--- /dev/null
+++ b/core/color.h
@@ -0,0 +1,152 @@
+#ifndef COLOR_H
+#define COLOR_H
+
+/* The colors are named by picking the closest match
+ from http://chir.ag/projects/name-that-color */
+
+#include <QColor>
+#include <QMap>
+#include <QVector>
+
+// 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)
+
+#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<color_indice_t, QVector<QColor> > 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/core/compressibility.r b/core/compressibility.r
new file mode 100644
index 000000000..66310f3aa
--- /dev/null
+++ b/core/compressibility.r
@@ -0,0 +1,115 @@
+# Compressibility data gathered by Lubomir I Ivanov:
+#
+# "Data obtained by finding two books online:
+#
+# [1]
+# PERRY’S CHEMICAL ENGINEERS’ HANDBOOK SEVENTH EDITION
+# pretty serious book, from which the wiki AIR values come from!
+#
+# http://www.unhas.ac.id/rhiza/arsip/kuliah/Sistem-dan-Tekn-Kendali-Proses/PDF_Collections/REFERENSI/Perrys_Chemical_Engineering_Handbook.pdf
+# page 2-165
+#
+# [*](Computed from pressure-volume-temperature tables in Vasserman monographs)
+# ^ i have no idea idea what this means, but the values might not be exactly
+# experimental?!
+#
+# the only thing this book is missing is helium, thus [2]!
+#
+# [2]
+# VOLUMETRIC BEHAVIOR OF HELIUM-ARGON MIXTURES AT HIGH PRESSURE AND MODERATE TEMPERATURE.
+#
+# https://shareok.org/bitstream/handle/11244/2062/6614196.PDF?sequence=1
+# page 108
+#
+#
+# the book has some tables with pressure values in atmosphere units. i'm
+# converting them bars. one of the relevant tables is for 323K and one for 273K
+# (both almost equal distance from 300K).
+#
+# this again is a linear mix operation between isotherms, which is probably not
+# the most accurate solution but it works.
+#
+# all data sets contain Z values at 300k, while the pressures are in bars in
+# the 1 to 500 range
+#
+#
+
+x = c(1, 5, 10, 20, 40, 60, 80, 100, 200, 300, 400, 500)
+o2 = c(0.9994, 0.9968, 0.9941, 0.9884, 0.9771, 0.9676, 0.9597, 0.9542, 0.9560, 0.9972, 1.0689, 1.1572)
+n2 = c(0.9998, 0.9990, 0.9983, 0.9971, 0.9964, 0.9973, 1.0000, 1.0052, 1.0559, 1.1422, 1.2480, 1.3629)
+he = c(1.0005, 1.0024, 1.0048, 1.0096, 1.0191, 1.0286, 1.0381, 1.0476, 1.0943, 1.1402, 1.1854, 1.2297)
+
+options(digits=15)
+
+#
+# Get the O2 virial coefficients
+#
+plot(x,o2)
+o2fit = nls(o2 ~ 1.0 + p1*x + p2 *x^2 + p3*x^3, start=list(p1=0,p2=0,p3=0))
+summary(o2fit)
+
+new = data.frame(x = seq(min(x),max(x),len=200))
+lines(new$x,predict(o2fit,newdata=new))
+
+#
+# Get the N2 virial coefficients
+#
+plot(x,n2)
+n2fit = nls(n2 ~ 1.0 + p1*x + p2 *x^2 + p3*x^3, start=list(p1=0,p2=0,p3=0))
+summary(n2fit)
+
+new = data.frame(x = seq(min(x),max(x),len=200))
+lines(new$x,predict(n2fit,newdata=new))
+
+#
+# Get the He virial coefficients
+#
+# NOTE! This will not confirm convergence, thus the warnOnly.
+# That may be a sign that the data is possibly artificial.
+#
+plot(x,he)
+hefit = nls(he ~ 1.0 + p1*x + p2 *x^2 + p3*x^3,
+ start=list(p1=0,p2=0,p3=0),
+ control=nls.control(warnOnly=TRUE))
+summary(hefit)
+
+new = data.frame(x = seq(min(x),max(x),len=200))
+lines(new$x,predict(hefit,newdata=new))
+
+#
+# Raw data from VOLUMETRIC BEHAVIOR OF HELIUM-ARGON MIXTURES [..]
+# T=323.15K (50 C)
+p323atm = c(674.837, 393.223, 237.310, 146.294, 91.4027, 57.5799, 36.4620, 23.1654, 14.7478, 9.4017, 5.9987, 3.8300,
+ 540.204, 319.943, 195.008, 120.951, 75.8599, 47.9005, 30.3791, 19.3193, 12.3080, 7.8495, 5.0100, 3.1992)
+
+Hez323 = c(1.28067, 1.16782, 1.10289, 1.06407, 1.04028, 1.02548, 1.01617, 1.01029, 1.00656, 1.00418, 1.00267, 1.00171,
+ 1.22738, 1.13754, 1.08493, 1.05312, 1.03349, 1.02122, 1.01349, 1.00859, 1.00548, 1.00349, 1.00223, 1.00143)
+
+
+# T=273.15 (0 C)
+p273atm = c(683.599, 391.213, 233.607, 143.091, 89.0521, 55.9640, 35.3851, 22.4593, 14.2908, 9.1072, 5.8095, 3.7083,
+ 534.047, 312.144, 188.741, 116.508, 72.8529, 45.9194, 29.0883, 18.4851, 11.7702, 7.5040, 4.7881, 3.0570)
+
+Hez273 = c(1.33969, 1.19985, 1.12121, 1.07494, 1.04689, 1.02957, 1.01874, 1.01191, 1.00758, 1.00484, 1.00309, 1.00197,
+ 1.26914, 1.16070, 1.09837, 1.06118, 1.03843, 1.02429, 1.01541, 1.00980, 1.00625, 1.00398, 1.00254, 1.00162)
+
+p323 = p323atm * 1.01325
+p273 = p273atm * 1.01325
+
+x2=append(p323,p273)
+he2=append(Hez323,Hez273)
+
+plot(x2,he2)
+
+hefit2 = nls(he2 ~ 1.0 + p1*x2 + p2*x2^2 + p3*x2^3,
+ start=list(p1=0,p2=0,p3=0))
+summary(hefit2)
+
+he3 = function(bar)
+{
+ 1.0 +0.00047961098687979363 * bar -0.00000004077670019935 * bar^2 +0.00000000000077707035 * bar^3
+}
+
+new = data.frame(x2 = seq(min(x2),max(x2),len=200))
+lines(new$x2,predict(hefit2,newdata=new))
+curve(he3, min(x2),max(x2),add=TRUE)
diff --git a/core/configuredivecomputer.cpp b/core/configuredivecomputer.cpp
new file mode 100644
index 000000000..2457ffe82
--- /dev/null
+++ b/core/configuredivecomputer.cpp
@@ -0,0 +1,681 @@
+#include "configuredivecomputer.h"
+#include "libdivecomputer/hw.h"
+#include <QTextStream>
+#include <QFile>
+#include <libxml/parser.h>
+#include <libxml/parserInternals.h>
+#include <libxml/tree.h>
+#include <libxslt/transform.h>
+#include <QStringList>
+#include <QXmlStreamWriter>
+
+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/core/configuredivecomputer.h b/core/configuredivecomputer.h
new file mode 100644
index 000000000..f14eeeca3
--- /dev/null
+++ b/core/configuredivecomputer.h
@@ -0,0 +1,68 @@
+#ifndef CONFIGUREDIVECOMPUTER_H
+#define CONFIGUREDIVECOMPUTER_H
+
+#include <QObject>
+#include <QThread>
+#include <QVariant>
+#include "libdivecomputer.h"
+#include "configuredivecomputerthreads.h"
+#include <QDateTime>
+
+#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/core/configuredivecomputerthreads.cpp b/core/configuredivecomputerthreads.cpp
new file mode 100644
index 000000000..b229fc808
--- /dev/null
+++ b/core/configuredivecomputerthreads.cpp
@@ -0,0 +1,1778 @@
+#include "configuredivecomputerthreads.h"
+#include "libdivecomputer/hw.h"
+#include "libdivecomputer.h"
+#include <libdivecomputer/version.h>
+
+#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_TEMP_SENSOR_OFFSET 0x42
+#define OSTC3_SAFETY_STOP_LENGTH 0x43
+#define OSTC3_SAFETY_STOP_START_DEPTH 0x44
+#define OSTC3_SAFETY_STOP_END_DEPTH 0x45
+#define OSTC3_SAFETY_STOP_RESET_DEPTH 0x46
+
+#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 = 57;
+ 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);
+ READ_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate);
+ READ_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator);
+ READ_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2);
+ READ_SETTING(OSTC3_SAFETY_STOP_LENGTH, safetyStopLength);
+ READ_SETTING(OSTC3_SAFETY_STOP_START_DEPTH, safetyStopStartDepth);
+ READ_SETTING(OSTC3_SAFETY_STOP_END_DEPTH, safetyStopEndDepth);
+ READ_SETTING(OSTC3_SAFETY_STOP_RESET_DEPTH, safetyStopResetDepth);
+
+#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();
+
+ rc = hw_ostc3_device_config_read(device, OSTC3_TEMP_SENSOR_OFFSET, uData, sizeof(uData));
+ if (rc != DC_STATUS_SUCCESS)
+ return rc;
+ // OSTC3 stores the tempSensorOffset in two-complement
+ m_deviceDetails->tempSensorOffset = (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 = 56;
+
+ //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);
+ WRITE_SETTING(OSTC3_DYNAMIC_ASCEND_RATE, dynamicAscendRate);
+ WRITE_SETTING(OSTC3_GRAPHICAL_SPEED_INDICATOR, graphicalSpeedIndicator);
+ WRITE_SETTING(OSTC3_ALWAYS_SHOW_PPO2, alwaysShowppO2);
+ WRITE_SETTING(OSTC3_TEMP_SENSOR_OFFSET, tempSensorOffset);
+ WRITE_SETTING(OSTC3_SAFETY_STOP_LENGTH, safetyStopLength);
+ WRITE_SETTING(OSTC3_SAFETY_STOP_START_DEPTH, safetyStopStartDepth);
+ WRITE_SETTING(OSTC3_SAFETY_STOP_END_DEPTH, safetyStopEndDepth);
+ WRITE_SETTING(OSTC3_SAFETY_STOP_RESET_DEPTH, safetyStopResetDepth);
+
+#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();
+
+ // OSTC3 stores the tempSensorOffset in two-complement
+ data[0] = (unsigned char)m_deviceDetails->pressureSensorOffset;
+ rc = hw_ostc3_device_config_write(device, OSTC3_TEMP_SENSOR_OFFSET, data, sizeof(data));
+ if (rc != DC_STATUS_SUCCESS)
+ return rc;
+ EMIT_PROGRESS();
+
+ //sync date and time
+ if (m_deviceDetails->syncTime) {
+ dc_datetime_t now;
+ dc_datetime_localtime(&now, dc_datetime_now());
+
+ rc = hw_ostc3_device_clock(device, &now);
+ }
+ 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(data[97], data[98]);
+ // Byte100-101:
+ // Gasuent 2 Default (%O2,%He)
+ // Byte102-103:
+ // Gasuent 2 Current (%O2,%He)
+ gas dil2(data[101], data[102]);
+ // Byte104-105:
+ // Gasuent 3 Default (%O2,%He)
+ // Byte106-107:
+ // Gasuent 3 Current (%O2,%He)
+ gas dil3(data[105], data[106]);
+ // Byte108-109:
+ // Gasuent 4 Default (%O2,%He)
+ // Byte110-111:
+ // Gasuent 4 Current (%O2,%He)
+ gas dil4(data[109], data[110]);
+ // Byte112-113:
+ // Gasuent 5 Default (%O2,%He)
+ // Byte114-115:
+ // Gasuent 5 Current (%O2,%He)
+ gas dil5(data[113], 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);
+ // CF70: Safety Stop Duration [s]
+ m_deviceDetails->safetyStopLength = read_ostc_cf(data, 70);
+ // CF71: Safety Stop Start Depth [m]
+ m_deviceDetails->safetyStopStartDepth = read_ostc_cf(data, 71);
+ // CF72: Safety Stop End Depth [m]
+ m_deviceDetails->safetyStopEndDepth = read_ostc_cf(data, 72);
+ // CF73: Safety Stop Reset Depth [m]
+ m_deviceDetails->safetyStopResetDepth = read_ostc_cf(data, 73);
+ // CF74: Battery Timeout [min]
+
+#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);
+ // CF70: Safety Stop Duration [s]
+ write_ostc_cf(data, 70, max_CF, m_deviceDetails->safetyStopLength);
+ // CF71: Safety Stop Start Depth [m]
+ write_ostc_cf(data, 71, max_CF, m_deviceDetails->safetyStopStartDepth);
+ // CF72: Safety Stop End Depth [m]
+ write_ostc_cf(data, 72, max_CF, m_deviceDetails->safetyStopEndDepth);
+ // CF73: Safety Stop Reset Depth [m]
+ write_ostc_cf(data, 73, max_CF, m_deviceDetails->safetyStopResetDepth);
+ // CF74: Battery Timeout [min]
+
+#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)
+{
+ Q_UNUSED(device);
+
+ const dc_event_progress_t *progress = (dc_event_progress_t *) data;
+ DeviceThread *dt = static_cast<DeviceThread*>(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/core/configuredivecomputerthreads.h b/core/configuredivecomputerthreads.h
new file mode 100644
index 000000000..8817d848a
--- /dev/null
+++ b/core/configuredivecomputerthreads.h
@@ -0,0 +1,60 @@
+#ifndef CONFIGUREDIVECOMPUTERTHREADS_H
+#define CONFIGUREDIVECOMPUTERTHREADS_H
+
+#include <QObject>
+#include <QThread>
+#include "libdivecomputer.h"
+#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/core/datatrak.c b/core/datatrak.c
new file mode 100644
index 000000000..204ebd9b3
--- /dev/null
+++ b/core/datatrak.c
@@ -0,0 +1,698 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#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 subtracts
+ * (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 necessarily 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/core/datatrak.h b/core/datatrak.h
new file mode 100644
index 000000000..3a37e0465
--- /dev/null
+++ b/core/datatrak.h
@@ -0,0 +1,41 @@
+#ifndef DATATRAK_HEADER_H
+#define DATATRAK_HEADER_H
+
+#include <string.h>
+
+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/core/deco.c b/core/deco.c
new file mode 100644
index 000000000..af3e06126
--- /dev/null
+++ b/core/deco.c
@@ -0,0 +1,601 @@
+/* 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 <seconds> 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 <math.h>
+#include <string.h>
+#include "dive.h"
+#include <assert.h>
+#include "core/planner.h"
+
+#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).
+ 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) {
+ 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)
+{
+ (void) 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)
+ 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));
+}
+
+int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth)
+{
+ 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/core/deco.h b/core/deco.h
new file mode 100644
index 000000000..fd3b94a9f
--- /dev/null
+++ b/core/deco.h
@@ -0,0 +1,20 @@
+#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;
+
+extern int deco_allowed_depth(double tissues_tolerance, double surface_pressure, struct dive *dive, bool smooth);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // DECO_H
diff --git a/core/device.c b/core/device.c
new file mode 100644
index 000000000..6c4452f78
--- /dev/null
+++ b/core/device.c
@@ -0,0 +1,184 @@
+#include <string.h>
+#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 rectangular 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, bool alloc)
+{
+ static struct sample fake_samples[6];
+ static struct divecomputer fakedc;
+ struct sample *fake = fake_samples;
+
+ fakedc = (*dc);
+ if (alloc)
+ fake = malloc(sizeof(fake_samples));
+
+ 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_samples));
+ 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 absence 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/core/device.h b/core/device.h
new file mode 100644
index 000000000..8a00b96d3
--- /dev/null
+++ b/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, bool alloc);
+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/core/devicedetails.cpp b/core/devicedetails.cpp
new file mode 100644
index 000000000..a917a4d0e
--- /dev/null
+++ b/core/devicedetails.cpp
@@ -0,0 +1,70 @@
+#include "devicedetails.h"
+
+gas::gas(unsigned char oxygen, unsigned char helium, unsigned char type, unsigned char depth) :
+ oxygen(oxygen), helium(helium), type(type), depth(depth)
+{
+}
+
+setpoint::setpoint(unsigned char sp, unsigned char depth) :
+ sp(sp), depth(depth)
+{
+}
+
+DeviceDetails::DeviceDetails(QObject *parent) :
+ QObject(parent),
+ data(0),
+ syncTime(false),
+ 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),
+ tempSensorOffset(0),
+ safetyStopLength(0),
+ safetyStopStartDepth(0),
+ safetyStopEndDepth(0),
+ safetyStopResetDepth(0)
+{
+}
diff --git a/core/devicedetails.h b/core/devicedetails.h
new file mode 100644
index 000000000..ff3009bc5
--- /dev/null
+++ b/core/devicedetails.h
@@ -0,0 +1,104 @@
+#ifndef DEVICEDETAILS_H
+#define DEVICEDETAILS_H
+
+#include <QObject>
+#include <QDateTime>
+#include "libdivecomputer.h"
+
+struct gas {
+ unsigned char oxygen;
+ unsigned char helium;
+ unsigned char type;
+ unsigned char depth;
+ gas(unsigned char oxygen = 0, unsigned char helium = 0, unsigned char type = 0, unsigned char depth = 0);
+};
+
+struct setpoint {
+ unsigned char sp;
+ unsigned char depth;
+ setpoint(unsigned char sp = 0, unsigned char depth = 0);
+};
+
+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;
+ int tempSensorOffset;
+ unsigned safetyStopLength;
+ unsigned safetyStopStartDepth;
+ unsigned safetyStopEndDepth;
+ unsigned safetyStopResetDepth;
+};
+
+
+#endif // DEVICEDETAILS_H
diff --git a/core/display.h b/core/display.h
new file mode 100644
index 000000000..9e3e1d159
--- /dev/null
+++ b/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/core/dive.c b/core/dive.c
new file mode 100644
index 000000000..ce730da28
--- /dev/null
+++ b/core/dive.c
@@ -0,0 +1,3561 @@
+/* dive.c */
+/* maintains the internal dive list structure */
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+#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, unsigned 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 = &current_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);
+ invalidate_dive_cache(d);
+}
+
+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;
+ invalidate_dive_cache(d);
+ 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);
+
+ // Copy the first dc explicitly, then the list of subsequent dc's
+ copy_dc(&s->dc, &d->dc);
+ STRUCTURED_LIST_COPY(struct divecomputer, s->dc.next, d->dc.next, copy_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
+
+struct event *clone_event(const struct event *src_ev)
+{
+ struct event *ev;
+ if (!src_ev)
+ return NULL;
+
+ size_t size = sizeof(*src_ev) + strlen(src_ev->name) + 1;
+ ev = (struct event*) malloc(size);
+ if (!ev)
+ exit(1);
+ memcpy(ev, src_ev, size);
+ ev->next = NULL;
+
+ return ev;
+}
+
+/* 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) {
+ struct event *new_ev = clone_event(ev);
+ *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, };
+ uint32_t lasttime = 0;
+ int 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, false);
+ for (i = 0; i < dc->samples; i++) {
+ struct sample *sample = dc->sample + i;
+ uint32_t 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 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;
+ }
+}
+
+int gas_volume(cylinder_t *cyl, pressure_t p)
+{
+ double bar = p.mbar / 1000.0;
+ double z_factor = gas_compressibility_factor(&cyl->gasmix, bar);
+ return cyl->type.size.mliter * bar_to_atm(bar) / z_factor;
+}
+
+/*
+ * 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".
+ *
+ * NOTE! We don't take compressibility into account when naming
+ * cylinders. That makes a certain amount of sense, since the
+ * cylinder name is independent from the gasmix, and different
+ * gasmixes have different compressibility.
+ */
+static void match_standard_cylinder(cylinder_type_t *type)
+{
+ double cuft, bar;
+ int psi, len;
+ const char *fmt;
+ char buffer[40], *p;
+
+ /* Do we already have a cylinder description? */
+ if (type->description)
+ return;
+
+ bar = type->workingpressure.mbar / 1000.0;
+ cuft = ml_to_cuft(type->size.mliter);
+ cuft *= bar_to_atm(bar);
+ 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) {
+ double bar = type->workingpressure.mbar / 1000.0;
+ /* confusing - we don't really start from ml but millicuft !*/
+ volume_of_air = cuft_to_l(type->size.mliter);
+ /* milliliters at 1 atm: not corrected for compressibility! */
+ volume = volume_of_air / bar_to_atm(bar);
+ 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) {
+ if (dc->salinity < 500)
+ dc->salinity += FRESHWATER_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_dc_depths(struct dive *dive, struct divecomputer *dc)
+{
+ int i;
+ int maxdepth = dc->maxdepth.mm;
+ int lasttime = 0, lastdepth = 0;
+
+ for (i = 0; i < dc->samples; i++) {
+ struct sample *sample = dc->sample + i;
+ int time = sample->time.seconds;
+ int depth = sample->depth.mm;
+
+ if (depth < 0) {
+ depth = interpolate_depth(dc, i, lastdepth, lasttime, time);
+ sample->depth.mm = depth;
+ }
+
+ if (depth > SURFACE_THRESHOLD) {
+ if (depth > maxdepth)
+ maxdepth = depth;
+ }
+
+ lastdepth = depth;
+ lasttime = time;
+ if (sample->cns > dive->maxcns)
+ dive->maxcns = sample->cns;
+ }
+
+ update_depth(&dc->maxdepth, maxdepth);
+ if (maxdepth > dive->maxdepth.mm)
+ dive->maxdepth.mm = maxdepth;
+}
+
+static void fixup_dc_temp(struct dive *dive, struct divecomputer *dc)
+{
+ int i;
+ int mintemp = 0, lasttemp = 0;
+
+ for (i = 0; i < dc->samples; i++) {
+ struct sample *sample = dc->sample + i;
+ int temp = sample->temperature.mkelvin;
+
+ 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);
+ }
+ update_temperature(&dc->watertemp, mintemp);
+ update_min_max_temperatures(dive, dc->watertemp);
+}
+
+/*
+ * Fix up cylinder sensor information in the samples if we have
+ * an explicit first cylinder
+ */
+static void fixup_dc_cylinder_index(struct dive *dive, struct divecomputer *dc)
+{
+ int i;
+ int first_cylinder = explicit_first_cylinder(dive, dc);
+
+ if (!first_cylinder)
+ return;
+
+ for (i = 0; i < dc->samples; i++) {
+ struct sample *sample = dc->sample + i;
+
+ if (sample->sensor == 0)
+ sample->sensor = first_cylinder;
+ }
+}
+
+/*
+ * Simplify dc pressure information:
+ * (a) Remove redundant pressure information
+ * (b) Remove linearly interpolated pressure data
+ */
+static void simplify_dc_pressures(struct dive *dive, struct divecomputer *dc)
+{
+ int i, j;
+ int lastindex = -1;
+ int lastpressure = 0, lasto2pressure = 0;
+ int pressure_delta[MAX_CYLINDERS] = { INT_MAX, };
+
+ for (i = 0; i < dc->samples; i++) {
+ struct sample *sample = dc->sample + i;
+ int pressure = sample->cylinderpressure.mbar;
+ int o2_pressure = sample->o2cylinderpressure.mbar;
+ int index;
+
+ 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 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;
+ }
+ }
+}
+
+/* FIXME! sensor -> cylinder mapping? */
+static void fixup_start_pressure(struct dive *dive, int idx, pressure_t p)
+{
+ if (idx >= 0 && idx < MAX_CYLINDERS) {
+ cylinder_t *cyl = dive->cylinder + idx;
+ if (p.mbar && !cyl->sample_start.mbar)
+ cyl->sample_start = p;
+ }
+}
+
+static void fixup_end_pressure(struct dive *dive, int idx, pressure_t p)
+{
+ if (idx >= 0 && idx < MAX_CYLINDERS) {
+ cylinder_t *cyl = dive->cylinder + idx;
+ if (p.mbar && !cyl->sample_end.mbar)
+ cyl->sample_end = p;
+ }
+}
+
+/*
+ * Check the cylinder pressure sample information and fill in the
+ * overall cylinder pressures from those.
+ *
+ * We 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).
+ */
+static void fixup_dive_pressures(struct dive *dive, struct divecomputer *dc)
+{
+ int i, o2index = -1;
+
+ if (dive->dc.divemode == CCR)
+ o2index = get_cylinder_idx_by_use(dive, OXYGEN);
+
+ /* Walk the samples from the beginning to find starting pressures.. */
+ for (i = 0; i < dc->samples; i++) {
+ struct sample *sample = dc->sample + i;
+
+ if (sample->depth.mm < SURFACE_THRESHOLD)
+ continue;
+
+ fixup_start_pressure(dive, sample->sensor, sample->cylinderpressure);
+ fixup_start_pressure(dive, o2index, sample->o2cylinderpressure);
+ }
+
+ /* ..and from the end for ending pressures */
+ for (i = dc->samples; --i >= 0; ) {
+ struct sample *sample = dc->sample + i;
+
+ if (sample->depth.mm < SURFACE_THRESHOLD)
+ continue;
+
+ fixup_end_pressure(dive, sample->sensor, sample->cylinderpressure);
+ fixup_end_pressure(dive, o2index, sample->o2cylinderpressure);
+ }
+
+ simplify_dc_pressures(dive, dc);
+}
+
+static void fixup_dive_dc(struct dive *dive, struct divecomputer *dc)
+{
+ int i;
+
+ /* 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);
+
+ /* Fix up sample depth data */
+ fixup_dc_depths(dive, dc);
+
+ /* Fix up dive temperatures based on dive computer samples */
+ fixup_dc_temp(dive, dc);
+
+ /* Fix up cylinder sensor data */
+ fixup_dc_cylinder_index(dive, dc);
+
+ /* Fix up cylinder pressures based on DC info */
+ fixup_dive_pressures(dive, dc);
+
+ 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 gas 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)
+ return 1;
+
+ 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 synchronizes 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)
+{
+ size_t i = 0;
+ int 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()
+{
+ size_t 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 the 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 might 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 might 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_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, nr;
+ uint32_t t;
+ 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;
+ }
+
+ // Going 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;
+ invalidate_dive_cache(dive);
+ }
+}
+
+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);
+ invalidate_dive_cache(dive);
+}
+
+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);
+ invalidate_dive_cache(dive);
+ }
+ }
+}
+
+void picture_free(struct picture *picture)
+{
+ if (!picture)
+ return;
+ free(picture->filename);
+ free(picture->hash);
+ free(picture);
+}
+
+// When handling pictures in different threads, we need to copy them so we don't
+// run into problems when the main thread frees the picture.
+
+struct picture *clone_picture(struct picture *src)
+{
+ struct picture *dst;
+
+ dst = alloc_picture();
+ copy_pl(src, dst);
+ return dst;
+}
+
+void dive_remove_picture(char *filename)
+{
+ struct picture **picture = &current_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;
+ invalidate_dive_cache(current_dive);
+ }
+}
+
+/* this always acts on the current divecomputer of the current dive */
+void make_first_dc()
+{
+ struct divecomputer *dc = &current_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);
+ invalidate_dive_cache(current_dive);
+}
+
+/* always acts on the current dive */
+unsigned 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 == &current_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 = &current_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--;
+ invalidate_dive_cache(current_dive);
+}
+
+/* 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, unsigned 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/core/dive.h b/core/dive.h
new file mode 100644
index 000000000..ae24b7409
--- /dev/null
+++ b/core/dive.h
@@ -0,0 +1,913 @@
+#ifndef DIVE_H
+#define DIVE_H
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <time.h>
+#include <math.h>
+#include <zip.h>
+#include <sqlite3.h>
+#include <string.h>
+#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 <libxml/tree.h>
+#include <libxslt/transform.h>
+#include <libxslt/xsltutils.h>
+
+#include "sha1.h"
+#include "units.h"
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#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 double gas_compressibility_factor(struct gasmix *gas, double bar);
+
+
+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
+ unsigned char git_id[20];
+};
+
+static inline void invalidate_dive_cache(struct dive *dive)
+{
+ memset(dive->git_id, 0, 20);
+}
+
+static inline bool dive_cache_is_valid(const struct dive *dive)
+{
+ static const unsigned char null_id[20] = { 0, };
+ return !!memcmp(dive->git_id, null_id, 20);
+}
+
+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 struct picture *clone_picture(struct picture *src);
+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 void picture_free(struct picture *picture);
+
+extern int explicit_first_cylinder(struct dive *dive, struct divecomputer *dc);
+extern int get_depth_at_time(struct divecomputer *dc, unsigned 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;
+ if (salinity < 500)
+ salinity += FRESHWATER_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, force_root;
+
+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 unsigned 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 void report_message(const char *msg);
+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 check_git_sha(const char *filename);
+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 bool subsurface_user_is_root(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 struct event *clone_event(const struct event *src_ev);
+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, unsigned 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);
+extern void invalidate_dive_cache(struct dive *dc);
+
+#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 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;
+ 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/core/divecomputer.cpp b/core/divecomputer.cpp
new file mode 100644
index 000000000..e4081e1cd
--- /dev/null
+++ b/core/divecomputer.cpp
@@ -0,0 +1,228 @@
+#include "divecomputer.h"
+#include "dive.h"
+
+#include <QSettings>
+
+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<QString, DiveComputerNode>::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<QString, DiveComputerNode>::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<DiveComputerNode> 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/core/divecomputer.h b/core/divecomputer.h
new file mode 100644
index 000000000..98d12ab8b
--- /dev/null
+++ b/core/divecomputer.h
@@ -0,0 +1,38 @@
+#ifndef DIVECOMPUTER_H
+#define DIVECOMPUTER_H
+
+#include <QString>
+#include <QMap>
+#include <stdint.h>
+
+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<QString, DiveComputerNode> dcMap;
+ QMultiMap<QString, DiveComputerNode> dcWorkingMap;
+};
+
+extern DiveComputerList dcList;
+
+#endif
diff --git a/core/divelist.c b/core/divelist.c
new file mode 100644
index 000000000..543d9e17b
--- /dev/null
+++ b/core/divelist.c
@@ -0,0 +1,1207 @@
+/* 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 <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <math.h>
+#include "gettext.h"
+#include <assert.h>
+#include <zip.h>
+#include <libxslt/transform.h>
+
+#include "dive.h"
+#include "divelist.h"
+#include "display.h"
+#include "planner.h"
+#include "qthelperfromc.h"
+#include "git-access.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, divenr;
+ size_t j;
+ 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++;
+
+ if (idx < 0) {
+ // convert an idx of -1 so we do insert-in-chronological-order
+ idx = dive_table.nr - 1;
+ for (int i = 0; i < dive_table.nr - 1; i++) {
+ if (dive->when <= dive_table.dives[i]->when) {
+ idx = i;
+ break;
+ }
+ }
+ }
+
+ 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;
+}
+
+/*
+ * Merge two dives. 'a' is always before 'b' in the dive list
+ * (and thus in time).
+ */
+struct dive *merge_two_dives(struct dive *a, struct dive *b)
+{
+ struct dive *res;
+ int i, j, nr, nrdiff;
+ 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;
+
+ /*
+ * If 'a' and 'b' were numbered, and in proper order,
+ * then the resulting dive will get the first number,
+ * and the subsequent dives will be renumbered by the
+ * difference.
+ *
+ * So if you had a dive list 1 3 6 7 8, and you
+ * merge 1 and 3, the resulting numbered list will
+ * be 1 4 5 6, because we assume that there were
+ * some missing dives (originally dives 4 and 5),
+ * that now will still be missing (dives 2 and 3
+ * in the renumbered world).
+ *
+ * Obviously the normal case is that everything is
+ * consecutive, and the difference will be 1, so the
+ * above example is not supposed to be normal.
+ */
+ nrdiff = 0;
+ nr = a->number;
+ if (a->number && b->number > a->number) {
+ res->number = nr;
+ nrdiff = b->number - nr;
+ }
+
+ 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;
+
+ // renumber dives from merged one in advance by difference between
+ // merged dives numbers. Do not renumber if actual number is zero.
+ for (; j < dive_table.nr; j++) {
+ struct dive *dive = dive_table.dives[j];
+ int newnr;
+
+ if (!dive->number)
+ continue;
+ newnr = dive->number - nrdiff;
+
+ /*
+ * Don't renumber stuff that isn't in order!
+ *
+ * So if the new dive number isn't larger than the
+ * previous dive number, just stop here.
+ */
+ if (newnr <= nr)
+ break;
+ dive->number = newnr;
+ nr = newnr;
+ }
+
+ 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();
+ saved_git_id = "";
+}
diff --git a/core/divelist.h b/core/divelist.h
new file mode 100644
index 000000000..5bae09cff
--- /dev/null
+++ b/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/core/divelogexportlogic.cpp b/core/divelogexportlogic.cpp
new file mode 100644
index 000000000..af5157f4a
--- /dev/null
+++ b/core/divelogexportlogic.cpp
@@ -0,0 +1,161 @@
+#include <QString>
+#include <QFile>
+#include <QFileInfo>
+#include <QDir>
+#include <QSettings>
+#include <QTextStream>
+#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/core/divelogexportlogic.h b/core/divelogexportlogic.h
new file mode 100644
index 000000000..84f09c362
--- /dev/null
+++ b/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/core/divesite.c b/core/divesite.c
new file mode 100644
index 000000000..e9eed2a07
--- /dev/null
+++ b/core/divesite.c
@@ -0,0 +1,337 @@
+/* divesite.c */
+#include "divesite.h"
+#include "dive.h"
+#include "divelist.h"
+
+#include <math.h>
+
+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(&copy->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/core/divesite.cpp b/core/divesite.cpp
new file mode 100644
index 000000000..ae102a14b
--- /dev/null
+++ b/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 = "<small><small>(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 += ")</small></small>";
+ return locationTag;
+}
diff --git a/core/divesite.h b/core/divesite.h
new file mode 100644
index 000000000..f18b2e8e8
--- /dev/null
+++ b/core/divesite.h
@@ -0,0 +1,80 @@
+#ifndef DIVESITE_H
+#define DIVESITE_H
+
+#include "units.h"
+#include "taxonomy.h"
+#include <stdlib.h>
+
+#ifdef __cplusplus
+#include <QString>
+extern "C" {
+#else
+#include <stdbool.h>
+#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/core/divesitehelpers.cpp b/core/divesitehelpers.cpp
new file mode 100644
index 000000000..3542f96fa
--- /dev/null
+++ b/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 <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QNetworkAccessManager>
+#include <QUrlQuery>
+#include <QEventLoop>
+#include <QTimer>
+
+struct GeoLookupInfo {
+ degrees_t lat;
+ degrees_t lon;
+ uint32_t uuid;
+};
+
+QVector<GeoLookupInfo> 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/core/divesitehelpers.h b/core/divesitehelpers.h
new file mode 100644
index 000000000..a08069bc0
--- /dev/null
+++ b/core/divesitehelpers.h
@@ -0,0 +1,18 @@
+#ifndef DIVESITEHELPERS_H
+#define DIVESITEHELPERS_H
+
+#include "units.h"
+#include <QThread>
+
+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/core/equipment.c b/core/equipment.c
new file mode 100644
index 000000000..9f3e49039
--- /dev/null
+++ b/core/equipment.c
@@ -0,0 +1,238 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+/* equipment.c */
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <time.h>
+#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/core/exif.cpp b/core/exif.cpp
new file mode 100644
index 000000000..0b1cda2bc
--- /dev/null
+++ b/core/exif.cpp
@@ -0,0 +1,587 @@
+#include <stdio.h>
+/**************************************************************************
+ 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 <algorithm>
+#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/core/exif.h b/core/exif.h
new file mode 100644
index 000000000..0fb3a7d4a
--- /dev/null
+++ b/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 <string>
+
+//
+// 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/core/file.c b/core/file.c
new file mode 100644
index 000000000..1337da3a2
--- /dev/null
+++ b/core/file.c
@@ -0,0 +1,1115 @@
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "gettext.h"
+#include <zip.h>
+#include <time.h>
+
+#include "dive.h"
+#include "divelist.h"
+#include "file.h"
+#include "git-access.h"
+#include "qthelperfromc.h"
+
+/* For SAMPLE_* */
+#include <libdivecomputer/parser.h>
+
+/* to check XSLT version number */
+#include <libxslt/xsltconfig.h>
+
+/* 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 == (int)mem->size) // converting to int loses a bit but size will never be that big
+ 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)
+{
+ 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);
+
+ if (!success)
+ return report_error(translate("gettextFromC", "No dives in the input file '%s'"), filename);
+ }
+ 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</%s>", 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)
+{
+ (void) param;
+ (void) columns;
+ (void) 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(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);
+
+ /* 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(mem, CSV_DEPTH);
+ if (!strcasecmp(fmt, "LVD"))
+ return try_to_open_liquivision(filename, mem);
+ if (!strcasecmp(fmt, "TMP"))
+ return try_to_open_csv(mem, CSV_TEMP);
+ if (!strcasecmp(fmt, "HP1"))
+ return try_to_open_csv(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 check_git_sha(const char *filename)
+{
+ struct git_repository *git;
+ const char *branch = NULL;
+
+ 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,
+ * so we don't know if there is additional data in the remote */
+ return 1;
+
+ /* if this is a git repository, do we already have this exact state loaded ?
+ * get the SHA and compare with what we currently have */
+ if (git && git != dummy_git_repository) {
+ const char *sha = get_sha(git, branch);
+ if (!same_string(sha, "") &&
+ same_string(sha, saved_git_id)) {
+ fprintf(stderr, "already have loaded SHA %s - don't load again\n", sha);
+ return 0;
+ }
+ }
+ return 1;
+}
+
+int parse_file(const char *filename)
+{
+ struct git_repository *git;
+ const char *branch = NULL;
+ char *current_sha = copy_string(saved_git_id);
+ 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 */
+ free(current_sha);
+ return 0;
+ }
+ /* if this is a git repository, do we already have this exact state loaded ?
+ * get the SHA and compare with what we currently have */
+ if (git && git != dummy_git_repository) {
+ const char *sha = get_sha(git, branch);
+ if (!same_string(sha, "") &&
+ same_string(sha, current_sha) &&
+ !unsaved_changes()) {
+ fprintf(stderr, "already have loaded SHA %s - don't load again\n", sha);
+ free(current_sha);
+ return 0;
+ }
+ }
+ free(current_sha);
+ if (git)
+ return git_load_dives(git, branch);
+
+ 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.
+ */
+
+#ifndef SUBSURFACE_MOBILE
+ if (verbose >= 2) {
+ fprintf(stderr, "(echo '<csv>'; cat %s;echo '</csv>') | 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);
+ }
+#endif
+ 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/core/file.h b/core/file.h
new file mode 100644
index 000000000..1c1dfc116
--- /dev/null
+++ b/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);
+#ifdef __cplusplus
+}
+#endif
+
+#endif // FILE_H
diff --git a/core/gas-model.c b/core/gas-model.c
new file mode 100644
index 000000000..ad1160f3b
--- /dev/null
+++ b/core/gas-model.c
@@ -0,0 +1,64 @@
+/* gas-model.c */
+/* gas compressibility model */
+#include <stdio.h>
+#include <stdlib.h>
+#include "dive.h"
+
+/* "Virial minus one" - the virial cubic form without the initial 1.0 */
+#define virial_m1(C, x1, x2, x3) (C[0]*x1+C[1]*x2+C[2]*x3)
+
+/*
+ * Cubic virial least-square coefficients for O2/N2/He based on data from
+ *
+ * PERRY’S CHEMICAL ENGINEERS’ HANDBOOK SEVENTH EDITION
+ *
+ * with the lookup and curve fitting by Lubomir.
+ *
+ * The "virial" form of the compression factor polynomial is
+ *
+ * Z = 1.0 + C[0]*P + C[1]*P^2 + C[2]*P^3 ...
+ *
+ * and these tables do not contain the initial 1.0 term.
+ *
+ * NOTE! Helium coefficients are a linear mix operation between the
+ * 323K and one for 273K isotherms, to make everything be at 300K.
+ */
+double gas_compressibility_factor(struct gasmix *gas, double bar)
+{
+ static const double o2_coefficients[3] = {
+ -7.18092073703e-04,
+ +2.81852572808e-06,
+ -1.50290620492e-09
+ };
+ static const double n2_coefficients[3] = {
+ -2.19260353292e-04,
+ +2.92844845532e-06,
+ -2.07613482075e-09
+ };
+ static const double he_coefficients[3] = {
+ +4.87320026468e-04,
+ -8.83632921053e-08,
+ +5.33304543646e-11
+ };
+ int o2, he;
+ double x1, x2, x3;
+ double Z;
+
+ o2 = get_o2(gas);
+ he = get_he(gas);
+
+ x1 = bar; x2 = x1*x1; x3 = x2*x1;
+
+ Z = virial_m1(o2_coefficients, x1, x2, x3) * o2 +
+ virial_m1(he_coefficients, x1, x2, x3) * he +
+ virial_m1(n2_coefficients, x1, x2, x3) * (1000 - o2 - he);
+
+ /*
+ * We add the 1.0 at the very end - the linear mixing of the
+ * three 1.0 terms is still 1.0 regardless of the gas mix.
+ *
+ * The * 0.001 is because we did the linear mixing using the
+ * raw permille gas values.
+ */
+ return Z * 0.001 + 1.0;
+}
diff --git a/core/gaspressures.c b/core/gaspressures.c
new file mode 100644
index 000000000..5d3fc9791
--- /dev/null
+++ b/core/gaspressures.c
@@ -0,0 +1,430 @@
+/* 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)
+{ // 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;
+ interpolate.pressure_time += entry->pressure_time;
+ if (entry->sec >= segment->t_end)
+ break;
+ if (i <= cur)
+ interpolate.acc_pressure_time += entry->pressure_time;
+ }
+ 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;
+ pr_interpolate_t interpolate = { 0, 0, 0, 0 };
+ pr_track_t *last_segment = NULL;
+ 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;
+ 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,
+ last_segment = NULL; // get rid of interpolation data,
+ 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 ..
+ if (segment == last_segment) {
+ interpolate.acc_pressure_time += entry->pressure_time;
+ } else {
+ // Set up an interpolation structure
+ interpolate = get_pr_interpolate_data(segment, pi, i);
+ last_segment = segment;
+ }
+
+ 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 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)
+{
+ (void) dc;
+ int i, cylinderid, cylinderindex = -1;
+ pr_track_t *track_pr[MAX_CYLINDERS] = { NULL, };
+ pr_track_t *current = NULL;
+ bool missing_pr = false;
+ bool found_any_pr_data = false;
+
+ /* if we have no pressure data whatsoever, this is pointless, so let's just return */
+ for (i = 0; i < MAX_CYLINDERS; i++) {
+ if (dive->cylinder[i].start.mbar || dive->cylinder[i].sample_start.mbar ||
+ dive->cylinder[i].end.mbar || dive->cylinder[i].sample_end.mbar) {
+ found_any_pr_data = true;
+ break;
+ }
+ }
+ if (!found_any_pr_data)
+ return;
+
+ 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, 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/core/gaspressures.h b/core/gaspressures.h
new file mode 100644
index 000000000..420c117a2
--- /dev/null
+++ b/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/core/gettext.h b/core/gettext.h
new file mode 100644
index 000000000..43ff023c7
--- /dev/null
+++ b/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/core/gettextfromc.cpp b/core/gettextfromc.cpp
new file mode 100644
index 000000000..43ee8da50
--- /dev/null
+++ b/core/gettextfromc.cpp
@@ -0,0 +1,27 @@
+#include <QCoreApplication>
+#include <QString>
+#include "gettextfromc.h"
+
+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<gettextFromC> self(new gettextFromC());
+ return self.data();
+}
+
+extern "C" const char *trGettext(const char *text)
+{
+ return gettextFromC::instance()->trGettext(text);
+}
diff --git a/core/gettextfromc.h b/core/gettextfromc.h
new file mode 100644
index 000000000..53df18365
--- /dev/null
+++ b/core/gettextfromc.h
@@ -0,0 +1,18 @@
+#ifndef GETTEXTFROMC_H
+#define GETTEXTFROMC_H
+
+#include <QHash>
+#include <QCoreApplication>
+
+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<QByteArray, QByteArray> translationCache;
+};
+
+#endif // GETTEXTFROMC_H
diff --git a/core/git-access.c b/core/git-access.c
new file mode 100644
index 000000000..d10139d3d
--- /dev/null
+++ b/core/git-access.c
@@ -0,0 +1,929 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <git2.h>
+
+#include "dive.h"
+#include "membuffer.h"
+#include "strndup.h"
+#include "qthelperfromc.h"
+#include "git-access.h"
+#include "gettext.h"
+
+bool is_subsurface_cloud = false;
+
+int (*update_progress_cb)(int, const char *) = NULL;
+
+void set_git_update_cb(int(*cb)(int, const char *))
+{
+ update_progress_cb = cb;
+}
+
+// total overkill, but this allows us to get good timing in various scenarios;
+// the various parts of interacting with the local and remote git repositories send
+// us updates which indicate progress (and no, this is not smooth and definitely not
+// proportional - some parts are based on compute performance, some on network speed)
+// they also provide information where in the process we are so we can analyze the log
+// to understand which parts of the process take how much time.
+
+// last_git_storage_update_val is used to detect when we suddenly go back to smaller
+// "percentage" value because we are back to executing earlier code a second (or third
+// time) in that case a negative percentage value is sent to the callback function as a
+// special case to mark that situation. Overall this ensures monotonous percentage values
+int last_git_storage_update_val;
+
+int git_storage_update_progress(int percent, const char *text)
+{
+ static int delta = 0;
+
+ if (percent == 0) {
+ delta = 0;
+ } else if (percent > 0 && percent < last_git_storage_update_val) {
+ delta = last_git_storage_update_val + delta;
+ if (update_progress_cb)
+ (*update_progress_cb)(-delta, "DELTA");
+ if (verbose)
+ fprintf(stderr, "set git storage percentage delta to %d\n", delta);
+ }
+
+ last_git_storage_update_val = percent;
+
+ percent += delta;
+
+ int ret = 0;
+ if (update_progress_cb)
+ ret = (*update_progress_cb)(percent, text);
+ return ret;
+}
+
+// the checkout_progress_cb doesn't allow canceling of the operation
+// map the git progress to 70..90% of overall progress
+static void progress_cb(const char *path, size_t completed_steps, size_t total_steps, void *payload)
+{
+ (void) path;
+ (void) payload;
+
+ int percent = 0;
+ if (total_steps)
+ percent = 70 + 20 * completed_steps / total_steps;
+ (void)git_storage_update_progress(percent, "checkout_progress_cb");
+}
+
+// this randomly assumes that 80% of the time is spent on the objects and 20% on the deltas
+// map the git progress to 70..90% of overall progress
+// if the user cancels the dialog this is passed back to libgit2
+static int transfer_progress_cb(const git_transfer_progress *stats, void *payload)
+{
+ (void) payload;
+
+ int percent = 0;
+ if (stats->total_objects)
+ percent = 70 + 16 * stats->received_objects / stats->total_objects;
+ if (stats->total_deltas)
+ percent += 4 * stats->indexed_deltas / stats->total_deltas;
+ /* for debugging this is useful
+ char buf[100];
+ snprintf(buf, 100, "transfer cb rec_obj %d tot_obj %d idx_delta %d total_delta %d local obj %d", stats->received_objects, stats->total_objects, stats->indexed_deltas, stats->total_deltas, stats->local_objects);
+ return git_storage_update_progress(percent, buf);
+ */
+ return git_storage_update_progress(percent, "transfer cb");
+}
+
+// the initial push to sync the repos is mapped to 10..15% of overall progress
+static int push_transfer_progress_cb(unsigned int current, unsigned int total, size_t bytes, void *payload)
+{
+ (void) bytes;
+ (void) payload;
+
+ int percent = 0;
+ if (total != 0)
+ percent = 12 + 5 * current / total;
+ return git_storage_update_progress(percent, "push trasfer cb");
+}
+
+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)
+{
+ (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;
+}
+
+int credential_ssh_cb(git_cred **out,
+ const char *url,
+ const char *username_from_url,
+ unsigned int allowed_types,
+ void *payload)
+{
+ (void) url;
+ (void) 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)
+{
+ (void) url;
+ (void) username_from_url;
+ (void) payload;
+ (void) allowed_types;
+ 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)
+{
+ (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;
+}
+
+static int update_remote(git_repository *repo, git_remote *origin, git_reference *local, git_reference *remote, enum remote_transport rt)
+{
+ (void) repo;
+ (void) remote;
+
+ 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;
+
+ 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;
+
+ 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_p, git_reference *remote, git_oid *base, const git_oid *local_id, const git_oid *remote_id)
+{
+ (void) remote;
+ 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);
+ merge_options.tree_flags = GIT_MERGE_TREE_FIND_RENAMES;
+ 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_p) && !git_repository_is_bare(repo)) {
+ git_object *parent;
+ git_reference_peel(&parent, *local_p, GIT_OBJ_COMMIT);
+ if (update_git_checkout(repo, parent, merged_tree)) {
+ goto write_error;
+ }
+ }
+ if (git_reference_set_target(local_p, *local_p, &commit_oid, "Subsurface merge event"))
+ goto write_error;
+ set_git_id(&commit_oid);
+ git_signature_free(author);
+ if (verbose)
+ fprintf(stderr, "Successfully merged repositories");
+ 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;
+ int ret = 0;
+
+ if (verbose)
+ fprintf(stderr, "git storage: try to update\n");
+ git_storage_update_progress(9, "try to update");
+ 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)) {
+ if (verbose)
+ fprintf(stderr, "local is newer than remote, update remote\n");
+ git_storage_update_progress(10, "git_update_remote, local was newer");
+ 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 */
+ git_storage_update_progress(11, "try to merge");
+ ret = try_to_git_merge(repo, &local, remote, &base, local_id, remote_id);
+ if (ret == 0)
+ return update_remote(repo, origin, local, remote, rt);
+ else
+ return ret;
+
+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");
+ git_storage_update_progress(7, "git check remote status");
+
+ 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);
+ 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;
+ git_storage_update_progress(8, "git remote push (no remote existed)");
+ error = git_remote_push(origin, &refspec, &opts);
+ } 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 (prefs.git_local_only) {
+ if (verbose)
+ fprintf(stderr, "don't sync with remote - read from cache only\n");
+ return 0;
+ }
+ if (verbose)
+ fprintf(stderr, "sync with remote %s[%s]\n", remote, branch);
+ git_storage_update_progress(2, "sync with remote");
+ 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");
+ git_storage_update_progress(18, "can't reach cloud server, working with local copy");
+ return 0;
+ }
+ if (verbose)
+ fprintf(stderr, "git storage: fetch remote\n");
+ 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;
+ git_storage_update_progress(6, "git fetch remote");
+ error = git_remote_fetch(origin, NULL, &opts, NULL);
+ // 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);
+ git_storage_update_progress(18, "done with sync with remote");
+ 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)
+{
+ (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");
+
+ 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;
+
+ 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) && !defined(SUBSURFACE_MOBILE)
+ } 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_remote_repo: accessing %s\n", remote);
+ }
+ git_storage_update_progress(1, "start git interaction");
+ /* 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);
+ }
+ if (!prefs.git_local_only)
+ return create_local_repo(localdir, remote, branch, rt);
+ else
+ return 0;
+
+}
+
+/*
+ * 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/core/git-access.h b/core/git-access.h
new file mode 100644
index 000000000..3a0c5160a
--- /dev/null
+++ b/core/git-access.h
@@ -0,0 +1,36 @@
+#ifndef GITACCESS_H
+#define GITACCESS_H
+
+#include "git2.h"
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#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 const char *get_sha(git_repository *repo, const char *branch);
+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 (*)(int, const char *));
+int git_storage_update_progress(int percent, const char *text);
+char *get_local_dir(const char *remote, const char *branch);
+
+extern int last_git_storage_update_val;
+
+#ifdef __cplusplus
+}
+#endif
+#endif // GITACCESS_H
+
diff --git a/core/gpslocation.cpp b/core/gpslocation.cpp
new file mode 100644
index 000000000..8595cc45d
--- /dev/null
+++ b/core/gpslocation.cpp
@@ -0,0 +1,606 @@
+#include "core/gpslocation.h"
+#include "qt-models/gpslistmodel.h"
+#include "core/pref.h"
+#include "core/dive.h"
+#include "core/helpers.h"
+#include <time.h>
+#include <unistd.h>
+#include <QDebug>
+#include <QVariant>
+#include <QUrlQuery>
+#include <QApplication>
+#include <QTimer>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#define GPS_FIX_ADD_URL "http://api.subsurface-divelog.org/api/dive/add/"
+#define GPS_FIX_DELETE_URL "http://api.subsurface-divelog.org/api/dive/delete/"
+#define GPS_FIX_DOWNLOAD_URL "http://api.subsurface-divelog.org/api/dive/get/"
+#define GET_WEBSERVICE_UID_URL "https://cloud.subsurface-divelog.org/webuserid/"
+
+GpsLocation *GpsLocation::m_Instance = NULL;
+
+GpsLocation::GpsLocation(void (*showMsgCB)(const char *), QObject *parent) : QObject(parent)
+{
+ Q_ASSERT_X(m_Instance == NULL, "GpsLocation", "GpsLocation recreated");
+ m_Instance = this;
+ m_GpsSource = 0;
+ waitingForPosition = false;
+ showMessageCB = showMsgCB;
+ // create a QSettings object that's separate from the main application settings
+ geoSettings = new QSettings(QSettings::NativeFormat, QSettings::UserScope,
+ QString("org.subsurfacedivelog"), QString("subsurfacelocation"), this);
+#ifdef SUBSURFACE_MOBILE
+ if (hasLocationsSource())
+ status(QString("Found GPS with positioning methods %1").arg(QString::number(m_GpsSource->supportedPositioningMethods(), 16)));
+#endif
+ userAgent = getUserAgent();
+ loadFromStorage();
+}
+
+GpsLocation *GpsLocation::instance()
+{
+ Q_ASSERT(m_Instance != NULL);
+
+ return m_Instance;
+}
+
+GpsLocation::~GpsLocation()
+{
+ m_Instance = NULL;
+}
+
+QGeoPositionInfoSource *GpsLocation::getGpsSource()
+{
+ if (!m_GpsSource) {
+ m_GpsSource = QGeoPositionInfoSource::createDefaultSource(this);
+ if (m_GpsSource != 0) {
+#ifndef SUBSURFACE_MOBILE
+ if (verbose)
+#endif
+ status(QString("Created position source %1").arg(m_GpsSource->sourceName()));
+ connect(m_GpsSource, SIGNAL(positionUpdated(QGeoPositionInfo)), this, SLOT(newPosition(QGeoPositionInfo)));
+ connect(m_GpsSource, SIGNAL(updateTimeout()), this, SLOT(updateTimeout()));
+ m_GpsSource->setUpdateInterval(5 * 60 * 1000); // 5 minutes so the device doesn't drain the battery
+ } else {
+#ifdef SUBSURFACE_MOBILE
+ status("don't have GPS source");
+#endif
+ }
+ }
+ return m_GpsSource;
+}
+
+bool GpsLocation::hasLocationsSource()
+{
+ QGeoPositionInfoSource *gpsSource = getGpsSource();
+ return gpsSource != 0 && (gpsSource->supportedPositioningMethods() & QGeoPositionInfoSource::SatellitePositioningMethods);
+}
+
+void GpsLocation::serviceEnable(bool toggle)
+{
+ QGeoPositionInfoSource *gpsSource = getGpsSource();
+ if (!gpsSource) {
+ if (toggle)
+ status("Can't start location service, no location source available");
+ return;
+ }
+ if (toggle) {
+ gpsSource->startUpdates();
+ status(QString("Starting Subsurface GPS service with update interval %1").arg(gpsSource->updateInterval()));
+ } else {
+ gpsSource->stopUpdates();
+ status("Stopping Subsurface GPS service");
+ }
+}
+
+QString GpsLocation::currentPosition()
+{
+ if (!hasLocationsSource())
+ return tr("Unknown GPS location");
+ if (m_trackers.count() && m_trackers.lastKey() + 300 >= QDateTime::currentMSecsSinceEpoch() / 1000) {
+ // we can simply use the last position that we tracked
+ gpsTracker gt = m_trackers.last();
+ QString gpsString = printGPSCoords(gt.latitude.udeg, gt.longitude.udeg);
+ qDebug() << "returning last position" << gpsString;
+ return gpsString;
+ }
+ qDebug() << "requesting new GPS position";
+ m_GpsSource->requestUpdate();
+ // ok, we need to get the current position and somehow in the callback update the location in the QML UI
+ // punting right now
+ waitingForPosition = true;
+ return QString("waiting for the next gps location");
+}
+
+void GpsLocation::newPosition(QGeoPositionInfo pos)
+{
+ int64_t lastTime = 0;
+ QGeoCoordinate lastCoord;
+ int nr = m_trackers.count();
+ if (nr) {
+ gpsTracker gt = m_trackers.last();
+ lastCoord.setLatitude(gt.latitude.udeg / 1000000.0);
+ lastCoord.setLongitude(gt.longitude.udeg / 1000000.0);
+ lastTime = gt.when;
+ }
+ // if we are waiting for a position update or
+ // if we have no record stored or if at least the configured minimum
+ // time has passed or we moved at least the configured minimum distance
+ int64_t delta = (int64_t)pos.timestamp().toTime_t() + gettimezoneoffset() - lastTime;
+ if (!nr || waitingForPosition || delta > prefs.time_threshold ||
+ lastCoord.distanceTo(pos.coordinate()) > prefs.distance_threshold) {
+ QString msg("received new position %1 after delta %2 threshold %3");
+ status(qPrintable(msg.arg(pos.coordinate().toString()).arg(delta).arg(prefs.time_threshold)));
+ waitingForPosition = false;
+ gpsTracker gt;
+ gt.when = pos.timestamp().toTime_t();
+ gt.when += gettimezoneoffset(gt.when);
+ gt.latitude.udeg = rint(pos.coordinate().latitude() * 1000000);
+ gt.longitude.udeg = rint(pos.coordinate().longitude() * 1000000);
+ addFixToStorage(gt);
+ }
+}
+
+void GpsLocation::updateTimeout()
+{
+ status("request to get new position timed out");
+}
+
+void GpsLocation::status(QString msg)
+{
+ qDebug() << msg;
+ if (showMessageCB)
+ (*showMessageCB)(qPrintable(msg));
+}
+
+QString GpsLocation::getUserid(QString user, QString passwd)
+{
+ qDebug() << "called getUserid";
+ QEventLoop loop;
+ QTimer timer;
+ timer.setSingleShot(true);
+
+ QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
+ QUrl url(GET_WEBSERVICE_UID_URL);
+ QString data;
+ data = user + " " + passwd;
+ QNetworkRequest request;
+ request.setUrl(url);
+ request.setRawHeader("User-Agent", getUserAgent().toUtf8());
+ request.setRawHeader("Accept", "text/html");
+ request.setRawHeader("Content-type", "application/x-www-form-urlencoded");
+ reply = manager->post(request, data.toUtf8());
+ connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(getUseridError(QNetworkReply::NetworkError)));
+ timer.start(10000);
+ loop.exec();
+ if (timer.isActive()) {
+ timer.stop();
+ if (reply->error() == QNetworkReply::NoError) {
+ QString result = reply->readAll();
+ status(QString("received ") + result);
+ result.remove("WebserviceID:");
+ reply->deleteLater();
+ return result;
+ }
+ } else {
+ status("Getting Web service ID timed out");
+ }
+ reply->deleteLater();
+ return QString();
+}
+
+int GpsLocation::getGpsNum() const
+{
+ return m_trackers.count();
+}
+
+static void copy_gps_location(struct gpsTracker &gps, struct dive *d)
+{
+ struct dive_site *ds = get_dive_site_by_uuid(d->dive_site_uuid);
+ if (!ds) {
+ d->dive_site_uuid = create_dive_site(qPrintable(gps.name), gps.when);
+ ds = get_dive_site_by_uuid(d->dive_site_uuid);
+ }
+ ds->latitude = gps.latitude;
+ ds->longitude = gps.longitude;
+}
+
+#define SAME_GROUP 6 * 3600 /* six hours */
+bool GpsLocation::applyLocations()
+{
+ int i;
+ bool changed = false;
+ int last = 0;
+ int cnt = m_trackers.count();
+ if (cnt == 0)
+ return false;
+
+ // create a table with the GPS information
+ QList<struct gpsTracker> gpsTable = m_trackers.values();
+
+ // now walk the dive table and see if we can fill in missing gps data
+ struct dive *d;
+ for_each_dive(i, d) {
+ if (dive_has_gps_location(d))
+ continue;
+ for (int j = last; j < cnt; j++) {
+ if (time_during_dive_with_offset(d, gpsTable[j].when, SAME_GROUP)) {
+ if (verbose)
+ qDebug() << "processing gpsFix @" << get_dive_date_string(gpsTable[j].when) <<
+ "which is withing six hours of dive from" <<
+ get_dive_date_string(d->when) << "until" <<
+ get_dive_date_string(d->when + d->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(d, gpsTable[j].when, 0)) {
+ if (verbose)
+ qDebug() << "gpsFix is during the dive, pick that one";
+ copy_gps_location(gpsTable[j], d);
+ changed = true;
+ last = j;
+ break;
+ } else {
+ /*
+ * If it is not, check if there are more position fixes in SAME_GROUP range
+ */
+ if (j + 1 < cnt && time_during_dive_with_offset(d, gpsTable[j+1].when, SAME_GROUP)) {
+ if (verbose)
+ qDebug() << "look at the next gps fix @" << get_dive_date_string(gpsTable[j+1].when);
+ /* first let's test if this one is during the dive */
+ if (time_during_dive_with_offset(d, gpsTable[j+1].when, 0)) {
+ if (verbose)
+ qDebug() << "which is during the dive, pick that one";
+ copy_gps_location(gpsTable[j+1], d);
+ changed = true;
+ last = 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 (gpsTable[j+1].when < d->when) {
+ if (verbose)
+ qDebug() << "which is closer to the start of the dive, do continue with that";
+ continue;
+ } else if (gpsTable[j].when > d->when + d->duration.seconds) {
+ if (verbose)
+ qDebug() << "which is even later after the end of the dive, so pick the previous one";
+ copy_gps_location(gpsTable[j], d);
+ changed = true;
+ last = j;
+ break;
+ } else {
+ /* ok, gpsFix is before, nextgpsFix is after */
+ if (d->when - gpsTable[j].when <= gpsTable[j+1].when - (d->when + d->duration.seconds)) {
+ if (verbose)
+ qDebug() << "pick the one before as it's closer to the start";
+ copy_gps_location(gpsTable[j], d);
+ changed = true;
+ last = j;
+ break;
+ } else {
+ if (verbose)
+ qDebug() << "pick the one after as it's closer to the start";
+ copy_gps_location(gpsTable[j+1], d);
+ changed = true;
+ last = 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(gpsTable[j], d);
+ changed = true;
+ last = 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 (gpsTable[j].when >= d->when + d->duration.seconds + SAME_GROUP) {
+ last = j;
+ break;
+ }
+ }
+
+ }
+ }
+ if (changed)
+ mark_divelist_changed(true);
+ return changed;
+}
+
+QMap<qint64, gpsTracker> GpsLocation::currentGPSInfo() const
+{
+ return m_trackers;
+}
+
+void GpsLocation::loadFromStorage()
+{
+ int nr = geoSettings->value(QString("count")).toInt();
+ for (int i = 0; i < nr; i++) {
+ struct gpsTracker gt;
+ gt.when = geoSettings->value(QString("gpsFix%1_time").arg(i)).toLongLong();
+ gt.latitude.udeg = geoSettings->value(QString("gpsFix%1_lat").arg(i)).toInt();
+ gt.longitude.udeg = geoSettings->value(QString("gpsFix%1_lon").arg(i)).toInt();
+ gt.name = geoSettings->value(QString("gpsFix%1_name").arg(i)).toString();
+ gt.idx = i;
+ m_trackers.insert(gt.when, gt);
+ }
+}
+
+void GpsLocation::replaceFixToStorage(gpsTracker &gt)
+{
+ if (!m_trackers.keys().contains(gt.when)) {
+ addFixToStorage(gt);
+ return;
+ }
+ gpsTracker replacedTracker = m_trackers.value(gt.when);
+ geoSettings->setValue(QString("gpsFix%1_time").arg(replacedTracker.idx), gt.when);
+ geoSettings->setValue(QString("gpsFix%1_lat").arg(replacedTracker.idx), gt.latitude.udeg);
+ geoSettings->setValue(QString("gpsFix%1_lon").arg(replacedTracker.idx), gt.longitude.udeg);
+ geoSettings->setValue(QString("gpsFix%1_name").arg(replacedTracker.idx), gt.name);
+ replacedTracker.latitude = gt.latitude;
+ replacedTracker.longitude = gt.longitude;
+ replacedTracker.name = gt.name;
+}
+
+void GpsLocation::addFixToStorage(gpsTracker &gt)
+{
+ int nr = m_trackers.count();
+ geoSettings->setValue("count", nr + 1);
+ geoSettings->setValue(QString("gpsFix%1_time").arg(nr), gt.when);
+ geoSettings->setValue(QString("gpsFix%1_lat").arg(nr), gt.latitude.udeg);
+ geoSettings->setValue(QString("gpsFix%1_lon").arg(nr), gt.longitude.udeg);
+ geoSettings->setValue(QString("gpsFix%1_name").arg(nr), gt.name);
+ gt.idx = nr;
+ geoSettings->sync();
+ m_trackers.insert(gt.when, gt);
+}
+
+void GpsLocation::deleteFixFromStorage(gpsTracker &gt)
+{
+ qint64 when = gt.when;
+ int cnt = m_trackers.count();
+ if (cnt == 0 || !m_trackers.keys().contains(when)) {
+ qDebug() << "no gps fix with timestamp" << when;
+ return;
+ }
+ if (geoSettings->value(QString("gpsFix%1_time").arg(gt.idx)).toLongLong() != when) {
+ qDebug() << "uh oh - index for tracker has gotten out of sync...";
+ return;
+ }
+ if (cnt > 1) {
+ // now we move the last tracker into that spot (so our settings stay consecutive)
+ // and delete the last settings entry
+ when = geoSettings->value(QString("gpsFix%1_time").arg(cnt - 1)).toLongLong();
+ struct gpsTracker movedTracker = m_trackers.value(when);
+ movedTracker.idx = gt.idx;
+ m_trackers.remove(movedTracker.when);
+ m_trackers.insert(movedTracker.when, movedTracker);
+ geoSettings->setValue(QString("gpsFix%1_time").arg(movedTracker.idx), when);
+ geoSettings->setValue(QString("gpsFix%1_lat").arg(movedTracker.idx), movedTracker.latitude.udeg);
+ geoSettings->setValue(QString("gpsFix%1_lon").arg(movedTracker.idx), movedTracker.longitude.udeg);
+ geoSettings->setValue(QString("gpsFix%1_name").arg(movedTracker.idx), movedTracker.name);
+ geoSettings->remove(QString("gpsFix%1_lat").arg(cnt - 1));
+ geoSettings->remove(QString("gpsFix%1_lon").arg(cnt - 1));
+ geoSettings->remove(QString("gpsFix%1_time").arg(cnt - 1));
+ geoSettings->remove(QString("gpsFix%1_name").arg(cnt - 1));
+ }
+ geoSettings->setValue("count", cnt - 1);
+ geoSettings->sync();
+ m_trackers.remove(gt.when);
+}
+
+void GpsLocation::deleteGpsFix(qint64 when)
+{
+ struct gpsTracker defaultTracker;
+ defaultTracker.when = 0;
+ struct gpsTracker deletedTracker = m_trackers.value(when, defaultTracker);
+ if (deletedTracker.when != when) {
+ qDebug() << "can't find tracker for timestamp" << when;
+ return;
+ }
+ deleteFixFromStorage(deletedTracker);
+ m_deletedTrackers.append(deletedTracker);
+}
+
+void GpsLocation::clearGpsData()
+{
+ m_trackers.clear();
+ geoSettings->clear();
+ geoSettings->sync();
+}
+
+void GpsLocation::postError(QNetworkReply::NetworkError error)
+{
+ Q_UNUSED(error);
+ status(QString("error when sending a GPS fix: %1").arg(reply->errorString()));
+}
+
+void GpsLocation::getUseridError(QNetworkReply::NetworkError error)
+{
+ Q_UNUSED(error);
+ status(QString("error when retrieving Subsurface webservice user id: %1").arg(reply->errorString()));
+}
+
+void GpsLocation::deleteFixesFromServer()
+{
+ QEventLoop loop;
+ QTimer timer;
+ timer.setSingleShot(true);
+
+ QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
+ QUrl url(GPS_FIX_DELETE_URL);
+ QList<qint64> keys = m_trackers.keys();
+ while (!m_deletedTrackers.isEmpty()) {
+ gpsTracker gt = m_deletedTrackers.takeFirst();
+ QDateTime dt;
+ QUrlQuery data;
+ dt.setTime_t(gt.when - gettimezoneoffset(gt.when));
+ data.addQueryItem("login", prefs.userid);
+ data.addQueryItem("dive_date", dt.toString("yyyy-MM-dd"));
+ data.addQueryItem("dive_time", dt.toString("hh:mm"));
+ status(data.toString(QUrl::FullyEncoded).toUtf8());
+ QNetworkRequest request;
+ request.setUrl(url);
+ request.setRawHeader("User-Agent", getUserAgent().toUtf8());
+ request.setRawHeader("Accept", "text/json");
+ request.setRawHeader("Content-type", "application/x-www-form-urlencoded");
+ reply = manager->post(request, data.toString(QUrl::FullyEncoded).toUtf8());
+ connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(postError(QNetworkReply::NetworkError)));
+ timer.start(10000);
+ loop.exec();
+ if (timer.isActive()) {
+ timer.stop();
+ if (reply->error() != QNetworkReply::NoError) {
+ QString response = reply->readAll();
+ status(QString("Server response:") + reply->readAll());
+ }
+ } else {
+ status("Deleting on the server timed out - try again later");
+ m_deletedTrackers.prepend(gt);
+ break;
+ }
+ reply->deleteLater();
+ status(QString("completed deleting gps fix %1 - response: ").arg(gt.idx) + reply->readAll());
+ }
+}
+
+void GpsLocation::uploadToServer()
+{
+ // we want to do this one at a time (the server prefers that)
+ QEventLoop loop;
+ QTimer timer;
+ timer.setSingleShot(true);
+
+ QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
+ QUrl url(GPS_FIX_ADD_URL);
+ Q_FOREACH(qint64 key, m_trackers.keys()) {
+ struct gpsTracker gt = m_trackers.value(key);
+ QDateTime dt;
+ QUrlQuery data;
+ dt.setTime_t(gt.when - gettimezoneoffset(gt.when));
+ data.addQueryItem("login", prefs.userid);
+ data.addQueryItem("dive_date", dt.toString("yyyy-MM-dd"));
+ data.addQueryItem("dive_time", dt.toString("hh:mm"));
+ data.addQueryItem("dive_latitude", QString::number(gt.latitude.udeg / 1000000.0, 'f', 9));
+ data.addQueryItem("dive_longitude", QString::number(gt.longitude.udeg / 1000000.0, 'f', 9));
+ if (gt.name.isEmpty())
+ gt.name = "Auto-created dive";
+ data.addQueryItem("dive_name", gt.name);
+ status(data.toString(QUrl::FullyEncoded).toUtf8());
+ QNetworkRequest request;
+ request.setUrl(url);
+ request.setRawHeader("User-Agent", getUserAgent().toUtf8());
+ request.setRawHeader("Accept", "text/json");
+ request.setRawHeader("Content-type", "application/x-www-form-urlencoded");
+ reply = manager->post(request, data.toString(QUrl::FullyEncoded).toUtf8());
+ connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ // somehoe I cannot get this to work with the new connect syntax:
+ // connect(reply, &QNetworkReply::error, this, &GpsLocation::postError);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(postError(QNetworkReply::NetworkError)));
+ timer.start(10000);
+ loop.exec();
+ if (timer.isActive()) {
+ timer.stop();
+ if (reply->error() != QNetworkReply::NoError) {
+ QString response = reply->readAll();
+ if (!response.contains("Duplicate entry")) {
+ status(QString("Server response:") + reply->readAll());
+ break;
+ }
+ }
+ } else {
+ status("Uploading to server timed out");
+ break;
+ }
+ reply->deleteLater();
+ status(QString("completed sending gps fix %1 - response: ").arg(gt.idx) + reply->readAll());
+ }
+ // and now remove the ones that were locally deleted
+ deleteFixesFromServer();
+}
+
+void GpsLocation::downloadFromServer()
+{
+ QEventLoop loop;
+ QTimer timer;
+ timer.setSingleShot(true);
+ QNetworkAccessManager *manager = new QNetworkAccessManager(qApp);
+ QUrl url(QString(GPS_FIX_DOWNLOAD_URL "?login=%1").arg(prefs.userid));
+ QNetworkRequest request;
+ request.setUrl(url);
+ request.setRawHeader("User-Agent", getUserAgent().toUtf8());
+ request.setRawHeader("Accept", "text/json");
+ request.setRawHeader("Content-type", "text/html");
+ qDebug() << "downloadFromServer accessing" << url;
+ reply = manager->get(request);
+ connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(getUseridError(QNetworkReply::NetworkError)));
+ timer.start(10000);
+ loop.exec();
+ if (timer.isActive()) {
+ timer.stop();
+ if (!reply->error()) {
+ QString response = reply->readAll();
+ QJsonDocument json = QJsonDocument::fromJson(response.toLocal8Bit());
+ QJsonObject object = json.object();
+ if (object.value("download").toString() != "ok") {
+ qDebug() << "problems downloading GPS fixes";
+ return;
+ }
+ qDebug() << "already have" << m_trackers.count() << "GPS fixes";
+ QJsonArray downloadedFixes = object.value("dives").toArray();
+ qDebug() << downloadedFixes.count() << "GPS fixes downloaded";
+ for (int i = 0; i < downloadedFixes.count(); i++) {
+ QJsonObject fix = downloadedFixes[i].toObject();
+ QString date = fix.value("date").toString();
+ QString time = fix.value("time").toString();
+ QString name = fix.value("name").toString();
+ QString latitude = fix.value("latitude").toString();
+ QString longitude = fix.value("longitude").toString();
+ QDateTime timestamp = QDateTime::fromString(date + " " + time, "yyyy-M-d hh:m:s");
+
+ struct gpsTracker gt;
+ gt.when = timestamp.toMSecsSinceEpoch() / 1000 + gettimezoneoffset(timestamp.toMSecsSinceEpoch() / 1000);
+ gt.latitude.udeg = latitude.toDouble() * 1000000;
+ gt.longitude.udeg = longitude.toDouble() * 1000000;
+ gt.name = name;
+ // add this GPS fix to the QMap and the settings (remove existing fix at the same timestamp first)
+ if (m_trackers.keys().contains(gt.when)) {
+ qDebug() << "already have a fix at time stamp" << gt.when;
+ replaceFixToStorage(gt);
+ } else {
+ addFixToStorage(gt);
+ }
+ }
+ } else {
+ qDebug() << "network error" << reply->error() << reply->errorString() << reply->readAll();
+ }
+ } else {
+ qDebug() << "download timed out";
+ status("Download from server timed out");
+ }
+ reply->deleteLater();
+}
diff --git a/core/gpslocation.h b/core/gpslocation.h
new file mode 100644
index 000000000..82b51a291
--- /dev/null
+++ b/core/gpslocation.h
@@ -0,0 +1,66 @@
+#ifndef GPSLOCATION_H
+#define GPSLOCATION_H
+
+#include "units.h"
+#include <QObject>
+#include <QGeoCoordinate>
+#include <QGeoPositionInfoSource>
+#include <QGeoPositionInfo>
+#include <QSettings>
+#include <QNetworkReply>
+#include <QMap>
+
+struct gpsTracker {
+ degrees_t latitude;
+ degrees_t longitude;
+ qint64 when;
+ QString name;
+ int idx;
+};
+
+class GpsLocation : QObject {
+ Q_OBJECT
+public:
+ GpsLocation(void (*showMsgCB)(const char *msg), QObject *parent);
+ ~GpsLocation();
+ static GpsLocation *instance();
+ bool applyLocations();
+ int getGpsNum() const;
+ QString getUserid(QString user, QString passwd);
+ bool hasLocationsSource();
+ QString currentPosition();
+
+ QMap<qint64, gpsTracker> currentGPSInfo() const;
+
+private:
+ QGeoPositionInfo lastPos;
+ QGeoPositionInfoSource *getGpsSource();
+ QGeoPositionInfoSource *m_GpsSource;
+ void status(QString msg);
+ QSettings *geoSettings;
+ QNetworkReply *reply;
+ QString userAgent;
+ void (*showMessageCB)(const char *msg);
+ static GpsLocation *m_Instance;
+ bool waitingForPosition;
+ QMap<qint64, gpsTracker> m_trackers;
+ QList<gpsTracker> m_deletedTrackers;
+ void loadFromStorage();
+ void addFixToStorage(gpsTracker &gt);
+ void replaceFixToStorage(gpsTracker &gt);
+ void deleteFixFromStorage(gpsTracker &gt);
+ void deleteFixesFromServer();
+
+public slots:
+ void serviceEnable(bool toggle);
+ void newPosition(QGeoPositionInfo pos);
+ void updateTimeout();
+ void uploadToServer();
+ void downloadFromServer();
+ void postError(QNetworkReply::NetworkError error);
+ void getUseridError(QNetworkReply::NetworkError error);
+ void clearGpsData();
+ void deleteGpsFix(qint64 when);
+};
+
+#endif // GPSLOCATION_H
diff --git a/core/helpers.h b/core/helpers.h
new file mode 100644
index 000000000..f88da015c
--- /dev/null
+++ b/core/helpers.h
@@ -0,0 +1,56 @@
+/*
+ * helpers.h
+ *
+ * header file for random helper functions of Subsurface
+ *
+ */
+#ifndef HELPERS_H
+#define HELPERS_H
+
+#include <QString>
+#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);
+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 parseLengthToMm(const QString &text);
+int parseTemperatureToMkelvin(const QString &text);
+int parseWeightToGrams(const QString &text);
+int parsePressureToMbar(const QString &text);
+int parseGasMixO2(const QString &text);
+int parseGasMixHE(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();
+void selectedDivesGasUsed(QVector<QPair<QString, int> > &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/core/imagedownloader.cpp b/core/imagedownloader.cpp
new file mode 100644
index 000000000..f406ee45a
--- /dev/null
+++ b/core/imagedownloader.cpp
@@ -0,0 +1,113 @@
+#include "dive.h"
+#include "metrics.h"
+#include "divelist.h"
+#include "qthelper.h"
+#include "imagedownloader.h"
+#include <unistd.h>
+
+#include <QtConcurrent>
+
+QUrl cloudImageURL(const char *hash)
+{
+ return QUrl::fromUserInput(QString("https://cloud.subsurface-divelog.org/images/").append(hash));
+}
+
+ImageDownloader::ImageDownloader(struct picture *pic)
+{
+ picture = pic;
+}
+
+ImageDownloader::~ImageDownloader()
+{
+ picture_free(picture);
+}
+
+void ImageDownloader::load(bool fromHash){
+ QUrl url;
+ loadFromHash = fromHash;
+ if(fromHash)
+ url = cloudImageURL(picture->hash);
+ else
+ 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()) {
+ if (loadFromHash)
+ load(false);
+ 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());
+ }
+ reply->manager()->deleteLater();
+ reply->deleteLater();
+ // This should be called to make the picture actually show.
+ // Problem is DivePictureModel is not in core.
+ // Nevertheless, the image shows when the dive is selected the next time.
+ // DivePictureModel::instance()->updateDivePictures();
+
+}
+
+void loadPicture(struct picture *picture, bool fromHash)
+{
+ ImageDownloader download(picture);
+ download.load(fromHash);
+}
+
+SHashedImage::SHashedImage(struct picture *picture) : QImage()
+{
+ QUrl url = QUrl::fromUserInput(localFilePath(QString(picture->filename)));
+ if(url.isLocalFile())
+ load(url.toLocalFile());
+ if (isNull()) {
+ // This did not load anything. Let's try to get the image from other sources
+ // Let's try to load it locally via its hash
+ QString filename = fileFromHash(picture->hash);
+ if (filename.isNull()) {
+ // That didn't produce a local filename.
+ // Try the cloud server
+ QtConcurrent::run(loadPicture, clone_picture(picture), true);
+ } else {
+ // Load locally from translated file name
+ load(filename);
+ if (!isNull()) {
+ // Make sure the hash still matches the image file
+ QtConcurrent::run(updateHash, clone_picture(picture));
+ } else {
+ // Interpret filename as URL
+ QtConcurrent::run(loadPicture, clone_picture(picture), false);
+ }
+ }
+ } else {
+ // We loaded successfully. Now, make sure hash is up to date.
+ QtConcurrent::run(hashPicture, clone_picture(picture));
+ }
+}
+
diff --git a/core/imagedownloader.h b/core/imagedownloader.h
new file mode 100644
index 000000000..f4e3df875
--- /dev/null
+++ b/core/imagedownloader.h
@@ -0,0 +1,34 @@
+#ifndef IMAGEDOWNLOADER_H
+#define IMAGEDOWNLOADER_H
+
+#include <QImage>
+#include <QFuture>
+#include <QNetworkReply>
+
+typedef QPair<QString, QByteArray> SHashedFilename;
+
+extern QUrl cloudImageURL(const char *hash);
+
+
+class ImageDownloader : public QObject {
+ Q_OBJECT;
+public:
+ ImageDownloader(struct picture *picture);
+ ~ImageDownloader();
+ void load(bool fromHash);
+
+private:
+ struct picture *picture;
+ QNetworkAccessManager manager;
+ bool loadFromHash;
+
+private slots:
+ void saveImage(QNetworkReply *reply);
+};
+
+class SHashedImage : public QImage {
+public:
+ SHashedImage(struct picture *picture);
+};
+
+#endif // IMAGEDOWNLOADER_H
diff --git a/core/isocialnetworkintegration.cpp b/core/isocialnetworkintegration.cpp
new file mode 100644
index 000000000..eb1e82a49
--- /dev/null
+++ b/core/isocialnetworkintegration.cpp
@@ -0,0 +1,6 @@
+#include "isocialnetworkintegration.h"
+
+//Hack for moc.
+ISocialNetworkIntegration::ISocialNetworkIntegration(QObject* parent) : QObject(parent)
+{
+}
diff --git a/core/isocialnetworkintegration.h b/core/isocialnetworkintegration.h
new file mode 100644
index 000000000..70ea3d9ab
--- /dev/null
+++ b/core/isocialnetworkintegration.h
@@ -0,0 +1,73 @@
+#ifndef ISOCIALNETWORKINTEGRATION_H
+#define ISOCIALNETWORKINTEGRATION_H
+
+#include <QtPlugin>
+
+/* This Interface represents a Plugin for Social Network integration,
+ * with it you may be able to create plugins for facebook, instagram,
+ * twitpic, google plus and any other thing you may imagine.
+ *
+ * We bundle facebook integration as an example.
+ */
+
+class ISocialNetworkIntegration : public QObject {
+ Q_OBJECT
+public:
+ ISocialNetworkIntegration(QObject* parent = 0);
+
+ /*!
+ * @name socialNetworkName
+ * @brief The name of this social network
+ * @return The name of this social network
+ *
+ * The name of this social network will be used to populate the Menu to toggle states
+ * between connected/disconnected, and also submit stuff to it.
+ */
+ virtual QString socialNetworkName() const = 0;
+
+ /*!
+ * @name socialNetworkIcon
+ * @brief The icon of this social network
+ * @return The icon of this social network
+ *
+ * The icon of this social network will be used to populate the menu, and can also be
+ * used on a toolbar if requested.
+ */
+ virtual QString socialNetworkIcon() const = 0;
+
+ /*!
+ * @name isConnected
+ * @brief returns true if connected to this social network, false otherwise
+ * @return true if connected to this social network, false otherwise
+ */
+ virtual bool isConnected() = 0;
+
+ /*!
+ * @name requestLogin
+ * @brief try to login on this social network.
+ *
+ * Try to login on this social network. All widget implementation that
+ * manages login should be done inside this function.
+ */
+ virtual void requestLogin() = 0;
+
+ /*!
+ * @name requestLogoff
+ * @brief tries to logoff from this social network
+ *
+ * Try to logoff from this social network.
+ */
+ virtual void requestLogoff() = 0;
+
+ /*!
+ * @name uploadCurrentDive
+ * @brief send the current dive info to the Social Network
+ *
+ * Should format all the options and pixmaps from the current dive
+ * to update to the social network. All widget stuff related to sendint
+ * dive information should be executed inside this function.
+ */
+ virtual void requestUpload() = 0;
+};
+
+#endif \ No newline at end of file
diff --git a/core/libdivecomputer.c b/core/libdivecomputer.c
new file mode 100644
index 000000000..549b894ce
--- /dev/null
+++ b/core/libdivecomputer.c
@@ -0,0 +1,1081 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <inttypes.h>
+#include <string.h>
+#include "gettext.h"
+#include "dive.h"
+#include "device.h"
+#include "divelist.h"
+#include "display.h"
+
+#include <libdivecomputer/uwatec.h>
+#include <libdivecomputer/hw.h>
+#include <libdivecomputer/version.h>
+#include "libdivecomputer.h"
+
+/* 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, unsigned int ngases)
+{
+ static bool shown_warning = false;
+ unsigned int i;
+ int rc;
+
+#if DC_VERSION_CHECK(0, 5, 0) && defined(DC_GASMIX_UNKNOWN)
+ unsigned int ntanks = 0;
+ rc = dc_parser_get_field(parser, DC_FIELD_TANK_COUNT, 0, &ntanks);
+ if (rc == DC_STATUS_SUCCESS) {
+ if (ntanks && 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(" <vendor time='%u:%02u' type=\"%u\" size=\"%u\">", 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("</vendor>\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, ...)
+{
+ (void) devdata;
+ 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)
+{
+ (void) devdata;
+ // 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;
+}
+
+/*
+ * 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 (dive->dc.airtemp.mkelvin == 0 && 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)
+{
+ (void) device;
+ 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)
+{
+ (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)
+{
+ (void) context;
+ 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) {
+#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL)
+ 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/core/libdivecomputer.h b/core/libdivecomputer.h
new file mode 100644
index 000000000..99f1c2490
--- /dev/null
+++ b/core/libdivecomputer.h
@@ -0,0 +1,72 @@
+#ifndef LIBDIVECOMPUTER_H
+#define LIBDIVECOMPUTER_H
+
+
+/* libdivecomputer */
+
+#ifdef DC_VERSION /* prevent a warning with wingdi.h */
+#undef DC_VERSION
+#endif
+#ifdef HAVE_LIBDIVECOMPUTER
+#include <libdivecomputer/version.h>
+#endif
+#include <libdivecomputer/device.h>
+#include <libdivecomputer/parser.h>
+
+#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/core/linux.c b/core/linux.c
new file mode 100644
index 000000000..b81f6bf53
--- /dev/null
+++ b/core/linux.c
@@ -0,0 +1,232 @@
+/* linux.c */
+/* implements Linux specific functions */
+#include "dive.h"
+#include "display.h"
+#include "membuffer.h"
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <fnmatch.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <pwd.h>
+
+// 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
+ (void)font;
+ 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 = {};
+ 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 >= (int)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)
+{
+ (void)dedicated;
+ /* NOP */
+}
+
+void subsurface_console_exit(void)
+{
+ /* NOP */
+}
+
+bool subsurface_user_is_root()
+{
+ return (geteuid() == 0);
+}
diff --git a/core/liquivision.c b/core/liquivision.c
new file mode 100644
index 000000000..9347a724a
--- /dev/null
+++ b/core/liquivision.c
@@ -0,0 +1,420 @@
+#include <string.h>
+
+#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)
+{
+ (void) code;
+ (void) ps;
+ (void) ps_ptr;
+ (void) 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 = strdup("Xen");
+ break;
+ case 1:
+ case 2:
+ dc->model = strdup("Xeo");
+ break;
+ case 4:
+ dc->model = strdup("Lynx");
+ break;
+ default:
+ dc->model = strdup("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)
+{
+ (void) filename;
+ 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/core/load-git.c b/core/load-git.c
new file mode 100644
index 000000000..a1f2d2031
--- /dev/null
+++ b/core/load-git.c
@@ -0,0 +1,1709 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <git2.h>
+#include <libdivecomputer/parser.h>
+
+#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)
+{
+ (void) str;
+ 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)
+{
+ (void) line;
+ 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)
+{ (void) line; struct dive *dive = _dive; dive->divemaster = get_utf8(str); }
+
+static void parse_dive_buddy(char *line, struct membuffer *str, void *_dive)
+{ (void) line; struct dive *dive = _dive; dive->buddy = get_utf8(str); }
+
+static void parse_dive_suit(char *line, struct membuffer *str, void *_dive)
+{ (void) line; struct dive *dive = _dive; dive->suit = get_utf8(str); }
+
+static void parse_dive_notes(char *line, struct membuffer *str, void *_dive)
+{ (void) line; struct dive *dive = _dive; dive->notes = get_utf8(str); }
+
+static void parse_dive_divesiteid(char *line, struct membuffer *str, void *_dive)
+{ (void) str; 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)
+{
+ (void) line;
+ 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)
+{ (void) str; struct dive *dive = _dive; dive->airtemp = get_temperature(line); }
+
+static void parse_dive_watertemp(char *line, struct membuffer *str, void *_dive)
+{ (void) str; struct dive *dive = _dive; dive->watertemp = get_temperature(line); }
+
+static void parse_dive_duration(char *line, struct membuffer *str, void *_dive)
+{ (void) str; struct dive *dive = _dive; dive->duration = get_duration(line); }
+
+static void parse_dive_rating(char *line, struct membuffer *str, void *_dive)
+{ (void) str; struct dive *dive = _dive; dive->rating = get_index(line); }
+
+static void parse_dive_visibility(char *line, struct membuffer *str, void *_dive)
+{ (void) str; struct dive *dive = _dive; dive->visibility = get_index(line); }
+
+static void parse_dive_notrip(char *line, struct membuffer *str, void *_dive)
+{
+ (void) str;
+ (void) line;
+ struct dive *dive = _dive; dive->tripflag = NO_TRIP;
+}
+
+static void parse_site_description(char *line, struct membuffer *str, void *_ds)
+{ (void) line; struct dive_site *ds = _ds; ds->description = strdup(mb_cstring(str)); }
+
+static void parse_site_name(char *line, struct membuffer *str, void *_ds)
+{ (void) line; struct dive_site *ds = _ds; ds->name = strdup(mb_cstring(str)); }
+
+static void parse_site_notes(char *line, struct membuffer *str, void *_ds)
+{ (void) line; 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)
+{
+ (void) str;
+ 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)
+{ (void) str; struct divecomputer *dc = _dc; dc->airtemp = get_temperature(line); }
+
+static void parse_dc_date(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; update_date(&dc->when, line); }
+
+static void parse_dc_deviceid(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->deviceid = get_hex(line); }
+
+static void parse_dc_diveid(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->diveid = get_hex(line); }
+
+static void parse_dc_duration(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->duration = get_duration(line); }
+
+static void parse_dc_dctype(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->divemode = get_dctype(line); }
+
+static void parse_dc_maxdepth(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->maxdepth = get_depth(line); }
+
+static void parse_dc_meandepth(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->meandepth = get_depth(line); }
+
+static void parse_dc_model(char *line, struct membuffer *str, void *_dc)
+{ (void) line; struct divecomputer *dc = _dc; dc->model = get_utf8(str); }
+
+static void parse_dc_numberofoxygensensors(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->no_o2sensors = get_index(line); }
+
+static void parse_dc_surfacepressure(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->surface_pressure = get_pressure(line); }
+
+static void parse_dc_salinity(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->salinity = get_salinity(line); }
+
+static void parse_dc_surfacetime(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; dc->surfacetime = get_duration(line); }
+
+static void parse_dc_time(char *line, struct membuffer *str, void *_dc)
+{ (void) str; struct divecomputer *dc = _dc; update_time(&dc->when, line); }
+
+static void parse_dc_watertemp(char *line, struct membuffer *str, void *_dc)
+{ (void) str; 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);
+
+ /*
+ * Older logs might mark the dive to be CCR by having an "SP change" event at time 0:00.
+ * Better to mark them being CCR on import so no need for special treatments elsewhere on
+ * the code.
+ */
+ if (ev && event.time.seconds == 0 && event.type == SAMPLE_EVENT_PO2 && dc->divemode==OC) {
+ dc->divemode = CCR;
+ }
+
+ 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)
+{ (void) str; dive_trip_t *trip = _trip; update_date(&trip->when, line); }
+
+static void parse_trip_time(char *line, struct membuffer *str, void *_trip)
+{ (void) str; dive_trip_t *trip = _trip; update_time(&trip->when, line); }
+
+static void parse_trip_location(char *line, struct membuffer *str, void *_trip)
+{ (void) line; dive_trip_t *trip = _trip; trip->location = get_utf8(str); }
+
+static void parse_trip_notes(char *line, struct membuffer *str, void *_trip)
+{ (void) line; dive_trip_t *trip = _trip; trip->notes = get_utf8(str); }
+
+static void parse_settings_autogroup(char *line, struct membuffer *str, void *_unused)
+{
+ (void) line;
+ (void) str;
+ (void) _unused;
+ set_autogroup(1);
+}
+
+static void parse_settings_units(char *line, struct membuffer *str, void *unused)
+{
+ (void) str;
+ (void) unused;
+ if (line)
+ set_informational_units(line);
+}
+
+static void parse_settings_userid(char *line, struct membuffer *str, void *_unused)
+{
+ (void) 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)
+{
+ (void) 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)
+{
+ (void) line;
+ (void) 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)
+{
+ (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)
+{
+ (void) line;
+ struct picture *pic = _pic;
+ pic->filename = get_utf8(str);
+}
+
+static void parse_picture_gps(char *line, struct membuffer *str, void *_pic)
+{
+ (void) str;
+ 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)
+{
+ (void) line;
+ 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)
+{
+ (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 git_tree_entry *entry, 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));
+ memcpy(active_dive->git_id, git_tree_entry_id(entry)->id, 20);
+ return GIT_WALK_OK;
+}
+
+static int picture_directory(const char *root, const char *name)
+{
+ (void) root;
+ (void) 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, entry, 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)
+{
+ (void) 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;
+}
+
+/*
+ * NOTE! The "git_id" for the dive is the hash for the whole dive directory.
+ * As such, it covers not just the dive, but the divecomputers and the
+ * pictures too. So if any of the dive computers change, the dive cache
+ * has to be invalidated too.
+ */
+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-<hash> */
+ 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 find_commit(git_repository *repo, const char *branch, git_commit **commit_p)
+{
+ git_object *object;
+
+ if (git_revparse_single(&object, repo, branch))
+ return report_error("Unable to look up revision '%s'", branch);
+ if (git_object_peel((git_object **)commit_p, object, GIT_OBJ_COMMIT))
+ return report_error("Revision '%s' is not a valid commit", branch);
+ return 0;
+}
+
+static int do_git_load(git_repository *repo, const char *branch)
+{
+ int ret;
+ git_commit *commit;
+ git_tree *tree;
+
+ ret = find_commit(repo, branch, &commit);
+ if (ret)
+ return ret;
+ 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;
+}
+
+const char *get_sha(git_repository *repo, const char *branch)
+{
+ static char git_id_buffer[GIT_OID_HEXSZ+1];
+ git_commit *commit;
+ if (find_commit(repo, branch, &commit))
+ return NULL;
+ git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), (const git_oid *)commit);
+ return git_id_buffer;
+}
+
+/*
+ * 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/core/macos.c b/core/macos.c
new file mode 100644
index 000000000..500412cd8
--- /dev/null
+++ b/core/macos.c
@@ -0,0 +1,218 @@
+/* macos.c */
+/* implements Mac OS X specific functions */
+#include <stdlib.h>
+#include <dirent.h>
+#include <fnmatch.h>
+#include "dive.h"
+#include "display.h"
+#include <CoreFoundation/CoreFoundation.h>
+#if !defined(__IPHONE_5_0)
+#include <CoreServices/CoreServices.h>
+#endif
+#include <mach-o/dyld.h>
+#include <sys/syslimits.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+void subsurface_user_info(struct user_info *info)
+{
+ (void) 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)
+{
+ (void) 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 >= (int)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 >= (int)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)
+{
+ (void) dedicated;
+ /* NOP */
+}
+
+void subsurface_console_exit(void)
+{
+ /* NOP */
+}
+
+bool subsurface_user_is_root()
+{
+ return (geteuid() == 0);
+}
diff --git a/core/membuffer.c b/core/membuffer.c
new file mode 100644
index 000000000..053edb8f0
--- /dev/null
+++ b/core/membuffer.c
@@ -0,0 +1,288 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#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 = "&lt;";
+ break;
+ case '>':
+ escape = "&gt;";
+ break;
+ case '&':
+ escape = "&amp;";
+ break;
+ case '\'':
+ if (!is_attribute)
+ continue;
+ escape = "&apos;";
+ break;
+ case '\"':
+ if (!is_attribute)
+ continue;
+ escape = "&quot;";
+ break;
+ case '\n':
+ if (!is_html)
+ continue;
+ else
+ escape = "<br>";
+ }
+ 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/core/membuffer.h b/core/membuffer.h
new file mode 100644
index 000000000..434b34c71
--- /dev/null
+++ b/core/membuffer.h
@@ -0,0 +1,74 @@
+#ifndef MEMBUFFER_H
+#define MEMBUFFER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <ctype.h>
+
+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/core/metrics.cpp b/core/metrics.cpp
new file mode 100644
index 000000000..3c66528b8
--- /dev/null
+++ b/core/metrics.cpp
@@ -0,0 +1,65 @@
+/*
+ * metrics.cpp
+ *
+ * methods to find/compute essential UI metrics
+ * (font properties, icon sizes, etc)
+ *
+ */
+
+#include "metrics.h"
+
+static IconMetrics dfltIconMetrics;
+
+IconMetrics::IconMetrics() :
+ sz_small(-1),
+ sz_med(-1),
+ sz_big(-1),
+ sz_pic(-1),
+ spacing(-1),
+ dpr(1.0)
+{
+}
+
+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;
+}
+
+void updateDevicePixelRatio(double dpr)
+{
+ dfltIconMetrics.dpr = dpr;
+}
diff --git a/core/metrics.h b/core/metrics.h
new file mode 100644
index 000000000..ca281b3b1
--- /dev/null
+++ b/core/metrics.h
@@ -0,0 +1,36 @@
+/*
+ * 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 <QFont>
+#include <QFontMetrics>
+#include <QSize>
+
+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
+ // devicePixelRatio
+ double dpr; // 1.0 for traditional screens, HiDPI screens up to 3.0
+ IconMetrics();
+};
+
+const IconMetrics & defaultIconMetrics();
+void updateDevicePixelRatio(double dpr);
+
+#endif // METRICS_H
diff --git a/core/ostctools.c b/core/ostctools.c
new file mode 100644
index 000000000..9be591b0e
--- /dev/null
+++ b/core/ostctools.c
@@ -0,0 +1,193 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#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 necessary 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/core/parse-xml.c b/core/parse-xml.c
new file mode 100644
index 000000000..e8782251e
--- /dev/null
+++ b/core/parse-xml.c
@@ -0,0 +1,3751 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <assert.h>
+#define __USE_XOPEN
+#include <time.h>
+#include <libxml/parser.h>
+#include <libxml/parserInternals.h>
+#include <libxml/tree.h>
+#include <libxslt/transform.h>
+#include <libdivecomputer/parser.h>
+
+#include "gettext.h"
+
+#include "dive.h"
+#include "divelist.h"
+#include "device.h"
+#include "membuffer.h"
+
+int verbose, quit, force_root;
+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)
+{
+ (void) buffer;
+ (void) 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
+ *
+ * <Temp>32.0</Temp>
+ *
+ * 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)
+{
+ (void) name;
+ 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 (cur_ws_index < MAX_WEIGHTSYSTEMS) {
+ 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 (cur_cylinder_index < MAX_CYLINDERS) {
+ 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("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)
+{
+ /* WARN: reset dc info does't touch the dc? */
+ (void) 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);
+
+ /*
+ * Older logs might mark the dive to be CCR by having an "SP change" event at time 0:00. Better
+ * to mark them being CCR on import so no need for special treatments elsewhere on the code.
+ */
+ if (ev && cur_event.time.seconds == 0 && cur_event.type == SAMPLE_EVENT_PO2 && dc->divemode==OC) {
+ dc->divemode = CCR;
+ }
+
+ 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, "<DIVELOGSDATA>");
+
+ if (ret) {
+ xmlParserCtxtPtr ctx;
+ char buf[] = "";
+ size_t 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)
+{
+ (void) size;
+ 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)
+{
+ (void) csv;
+ (void) txt;
+ dive_start();
+ divedate(starttime, &cur_dive->when);
+ dive_end();
+}
+
+extern int dm4_events(void *handle, int columns, char **data, char **column)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) columns;
+ (void) column;
+ unsigned int i;
+ int 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; i<columns;++i) {
+ fprintf(stderr, "%s\t", column[i]);
+ }
+ fprintf(stderr, "\n");
+ for (i=0; i<columns;++i) {
+ fprintf(stderr, "%s\t", data[i]);
+ }
+ fprintf(stderr, "\n");
+ //exit(0);
+ */
+ return SQLITE_OK;
+}
+
+extern int dm5_dive(void *param, int columns, char **data, char **column)
+{
+ (void) columns;
+ (void) column;
+ unsigned int i;
+ int interval, retval = 0, block_size;
+ sqlite3 *handle = (sqlite3 *)param;
+ unsigned const char *sampleBlob;
+ 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_cylinders_template[] = "select * from DiveMixture where DiveId = %d";
+ char get_gaschange_template[] = "select GasChangeTime,Oxygen,Helium from DiveGasChange join DiveMixture on DiveGasChange.DiveMixtureId=DiveMixture.DiveMixtureId where DiveId = %d";
+ char get_events[512];
+
+ 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);
+
+ 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)
+{
+ (void) buffer;
+ (void) size;
+
+ 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)
+{
+ (void) buffer;
+ (void) size;
+
+ 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) column;
+ (void) data;
+ return 0;
+}
+
+extern int cobalt_location(void *handle, int columns, char **data, char **column)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) columns;
+ (void) 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)
+{
+ (void) buffer;
+ (void) size;
+
+ 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)
+{
+ (void) buffer;
+ (void) size;
+
+ 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) handle;
+ (void) columns;
+ (void) 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)
+{
+ (void) columns;
+ (void) 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)
+{
+ (void) buffer;
+ (void) size;
+
+ 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/core/planner.c b/core/planner.c
new file mode 100644
index 000000000..705aad1cb
--- /dev/null
+++ b/core/planner.c
@@ -0,0 +1,1471 @@
+/* planner.c
+ *
+ * code that allows us to plan future dives
+ *
+ * (c) Dirk Hohndel 2013
+ */
+#include <assert.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <string.h>
+#include "dive.h"
+#include "deco.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)
+{
+ uint32_t 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 int *sort_stops(int *dstops, int dnr, struct gaschanges *gstops, int gnr)
+{
+ int i, gi, di;
+ int total = dnr + gnr;
+ 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 *deco, *segmentsymbol;
+ static char buf[1000];
+ 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, "<span style='color: red;'>%s </span> %s<br>",
+ translate("gettextFromC", "Warning:"), temp);
+ dive->notes = strdup(buffer);
+
+ free((void *)buffer);
+ free((void *)temp);
+ return;
+ }
+
+ len = show_disclaimer ? snprintf(buffer, sz_buffer, "<div><b>%s<b></div><br>", 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, "<div><b>%s</b><br>%s</div><br>",
+ translate("gettextFromC", "Subsurface dive plan"), temp);
+
+ if (!plan_verbatim) {
+ len += snprintf(buffer + len, sz_buffer - len, "<div><table><thead><tr><th></th><th>%s</th>",
+ translate("gettextFromC", "depth"));
+ if (plan_display_duration)
+ len += snprintf(buffer + len, sz_buffer - len, "<th style='padding-left: 10px;'>%s</th>",
+ translate("gettextFromC", "duration"));
+ if (plan_display_runtime)
+ len += snprintf(buffer + len, sz_buffer - len, "<th style='padding-left: 10px;'>%s</th>",
+ translate("gettextFromC", "runtime"));
+ len += snprintf(buffer + len, sz_buffer - len,
+ "<th style='padding-left: 10px; float: left;'>%s</th></tr></thead><tbody style='float: left;'>",
+ 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<br>", 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<br>", 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 )) {
+ // Print a symbol to indicate whether segment is an ascent, descent, constant depth (user entered) or deco stop
+ if (isascent)
+ segmentsymbol = "&#10138;"; // up-right arrow for ascent
+ else if (dp->depth > lastdepth)
+ segmentsymbol = "&#10136;"; // down-right arrow for descent
+ else if (dp->entered)
+ segmentsymbol = "&#10137;"; // right arrow for entered entered segment at constant depth
+ else
+ segmentsymbol = "&#10134;"; // heavey minus sign for deco stop
+
+ len += snprintf(buffer + len, sz_buffer - len, "<tr><td style='padding-left: 10px; float: right;'>%s</td>", segmentsymbol);
+
+ snprintf(temp, sz_temp, translate("gettextFromC", "%3.0f%s"), depthvalue, depth_unit);
+ len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; float: right;'>%s</td>", temp);
+ if (plan_display_duration) {
+ snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time - lasttime + 30) / 60);
+ len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; float: right;'>%s</td>", temp);
+ }
+ if (plan_display_runtime) {
+ snprintf(temp, sz_temp, translate("gettextFromC", "%3dmin"), (dp->time + 30) / 60);
+ len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; float: right;'>%s</td>", 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, "<td style='padding-left: 10px; color: red; float: left;'><b>%s %s</b></td>", gasname(&newgasmix),
+ temp);
+ } else {
+ len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; color: red; float: left;'><b>%s</b></td>", 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, "<td style='padding-left: 10px; color: red; float: left;'><b>%s %s</b></td>", gasname(&gasmix),
+ temp);
+ } else {
+ len += snprintf(buffer + len, sz_buffer - len, "<td style='padding-left: 10px; color: red; float: left;'><b>%s</b></td>", 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, "<td>&nbsp;</td>");
+ }
+ len += snprintf(buffer + len, sz_buffer - len, "</tr>");
+ 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<br>", temp);
+ }
+ gaschange_after = false;
+ gasmix = newgasmix;
+ }
+ }
+ lastprintdepth = newdepth;
+ lastdepth = dp->depth;
+ lastsetpoint = dp->setpoint;
+ lastentered = dp->entered;
+ } while ((dp = nextdp) != NULL);
+ if (!plan_verbatim)
+ len += snprintf(buffer + len, sz_buffer - len, "</tbody></table></div>");
+
+ 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, "<div><br>%s: %i%%", temp, dive->cns);
+ snprintf(temp, sz_temp, "%s", translate("gettextFromC", "OTU"));
+ len += snprintf(buffer + len, sz_buffer - len, "<br>%s: %i</div>", 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, "<div><br>%s<br>", 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), " &mdash; <span style='color: red;'>%s </span> %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), " &mdash; <span style='color: red;'>%s </span> %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<br>", 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, "<span style='color: red;'>%s </span> %s<br>",
+ 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, "<span style='color: red;'>%s </span> %s<br>",
+ translate("gettextFromC", "Warning:"), temp);
+
+ }
+ }
+ dp = dp->next;
+ }
+ }
+ snprintf(buffer + len, sz_buffer - len, "</div>");
+ dive->notes = strdup(buffer);
+
+ free((void *)buffer);
+ free((void *)temp);
+}
+
+int ascent_velocity(int depth, int avg_depth, int bottom_time)
+{
+ (void) 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;
+ int stopidx;
+ int depth;
+ struct gaschanges *gaschanges = NULL;
+ int gaschangenr;
+ int *decostoplevels;
+ int decostoplevelcount;
+ 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/core/planner.h b/core/planner.h
new file mode 100644
index 000000000..a675989e0
--- /dev/null
+++ b/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/core/pluginmanager.cpp b/core/pluginmanager.cpp
new file mode 100644
index 000000000..28c978280
--- /dev/null
+++ b/core/pluginmanager.cpp
@@ -0,0 +1,53 @@
+#include "pluginmanager.h"
+
+#include <QApplication>
+#include <QDir>
+#include <QPluginLoader>
+#include <QDebug>
+
+static QList<ISocialNetworkIntegration*> _socialNetworks;
+
+PluginManager& PluginManager::instance()
+{
+ static PluginManager self;
+ return self;
+}
+
+PluginManager::PluginManager()
+{
+}
+
+void PluginManager::loadPlugins()
+{
+ QDir pluginsDir(qApp->applicationDirPath());
+
+#if defined(Q_OS_WIN)
+ if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release")
+ pluginsDir.cdUp();
+#elif defined(Q_OS_MAC)
+ if (pluginsDir.dirName() == "MacOS") {
+ pluginsDir.cdUp();
+ pluginsDir.cdUp();
+ pluginsDir.cdUp();
+ }
+#endif
+ pluginsDir.cd("plugins");
+
+ qDebug() << "Plugins Directory: " << pluginsDir;
+ foreach (const QString& fileName, pluginsDir.entryList(QDir::Files)) {
+ QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
+ QObject *plugin = loader.instance();
+ if(!plugin)
+ continue;
+
+ if (ISocialNetworkIntegration *social = qobject_cast<ISocialNetworkIntegration*>(plugin)) {
+ qDebug() << "Adding the plugin: " << social->socialNetworkName();
+ _socialNetworks.push_back(social);
+ }
+ }
+}
+
+QList<ISocialNetworkIntegration*> PluginManager::socialNetworkIntegrationPlugins() const
+{
+ return _socialNetworks;
+}
diff --git a/core/pluginmanager.h b/core/pluginmanager.h
new file mode 100644
index 000000000..3f43b5db1
--- /dev/null
+++ b/core/pluginmanager.h
@@ -0,0 +1,19 @@
+#ifndef PLUGINMANAGER_H
+#define PLUGINMANAGER_H
+
+#include <QObject>
+
+#include "isocialnetworkintegration.h"
+
+class PluginManager {
+public:
+ static PluginManager& instance();
+ void loadPlugins();
+ QList<ISocialNetworkIntegration*> socialNetworkIntegrationPlugins() const;
+private:
+ PluginManager();
+ PluginManager(const PluginManager&);
+ PluginManager& operator=(const PluginManager&);
+};
+
+#endif
diff --git a/core/pref.h b/core/pref.h
new file mode 100644
index 000000000..be684fd90
--- /dev/null
+++ b/core/pref.h
@@ -0,0 +1,172 @@
+#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;
+
+typedef struct {
+ const char *language;
+ bool use_system_language;
+} locale_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;
+ const char *time_format;
+ const char *date_format;
+ const char *date_format_short;
+ bool time_format_override;
+ bool date_format_override;
+ 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;
+ int time_threshold;
+ int distance_threshold;
+ bool git_local_only;
+ locale_prefs_t locale; //: TODO: move the rest of locale based info here.
+};
+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/core/prefs-macros.h b/core/prefs-macros.h
new file mode 100644
index 000000000..fe459d3da
--- /dev/null
+++ b/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/core/profile.c b/core/profile.c
new file mode 100644
index 000000000..6576f6453
--- /dev/null
+++ b/core/profile.c
@@ -0,0 +1,1544 @@
+/* profile.c */
+/* creates all the necessary data for drawing the dive profile
+ */
+#include "gettext.h"
+#include <limits.h>
+#include <string.h>
+#include <assert.h>
+
+#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);
+
+extern bool in_planner();
+extern pressure_t first_ceiling_pressure;
+
+#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 {
+#ifndef SUBSURFACE_MOBILE
+ /* 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));
+#else
+ /* just add 2.5 minutes so we have a consistant right margin */
+ return seconds + DURATION_THR / 4;
+#endif
+ }
+}
+
+/* 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++;
+}
+
+/* UNUSED! */
+static int get_local_sac(struct plot_data *entry1, struct plot_data *entry2, struct dive *dive) __attribute__((unused));
+
+/* 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, 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, 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, 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, INT_MAX);
+
+ 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, INT_MAX);
+}
+
+static void check_setpoint_events(struct dive *dive, struct divecomputer *dc, struct plot_info *pi)
+{
+ int i = 0;
+ pressure_t setpoint;
+ (void) dive;
+ 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, INT_MAX);
+}
+
+
+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;
+ unsigned 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;
+ (void) dive;
+ 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 && (int)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 && (int)ev->time.seconds == lasttime + offset)
+ ev = ev->next;
+ }
+
+ /* Add events if they are between plot entries */
+ while (ev && (int)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 && (int)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;
+
+ 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)
+{
+ for (int i = 0; i < pi->nr; i++)
+ fill_sac(dive, pi, i);
+}
+
+static void populate_secondary_sensor_data(struct divecomputer *dc, struct plot_info *pi)
+{
+ (void) dc;
+ (void) 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);
+}
+
+#ifndef SUBSURFACE_MOBILE
+/* 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 iterating through a dive, entry->tts_calc needs to be reset */
+ entry->tts_calc = 0;
+
+ /* 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, count_iteration = 0;
+ 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;
+ int first_ceiling = 0;
+ bool first_iteration = true;
+ int final_tts = 0 , time_clear_ceiling = 0, time_deep_ceiling = 0, deco_time = 0, prev_deco_time = 10000000;
+ char *cache_data_initial = NULL;
+ /* For VPM-B outside the planner, cache the initial deco state for CVA iterations */
+ if (prefs.deco_mode == VPMB && !in_planner())
+ cache_deco_state(&cache_data_initial);
+ /* For VPM-B outside the planner, iterate until deco time converges (usually one or two iterations after the initial)
+ * Set maximum number of iterations to 10 just in case */
+ while ((abs(prev_deco_time - deco_time) >= 30) && (count_iteration < 10)) {
+ 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 {
+ /* Keep updating the VPM-B gradients until the start of the ascent phase of the dive. */
+ if (prefs.deco_mode == VPMB && !in_planner() && (entry - 1)->ceiling >= first_ceiling && first_iteration == true) {
+ nuclear_regeneration(t1);
+ vpmb_start_gradient();
+ /* For CVA calculations, start by guessing deco time = dive time remaining */
+ deco_time = pi->maxtime - t1;
+ vpmb_next_gradient(deco_time, surface_pressure / 1000.0);
+ }
+ entry->ceiling = deco_allowed_depth(tissue_tolerance_calc(dive, depth_to_bar(entry->depth, dive)), surface_pressure, dive, !prefs.calcceiling3m);
+ /* If using VPM-B outside the planner, take first_ceiling_pressure as the deepest ceiling */
+ if (prefs.deco_mode == VPMB && !in_planner()) {
+ if (entry->ceiling >= first_ceiling) {
+ time_deep_ceiling = t1;
+ first_ceiling = entry->ceiling;
+ first_ceiling_pressure.mbar = depth_to_mbar(first_ceiling, dive);
+ if (first_iteration) {
+ nuclear_regeneration(t1);
+ vpmb_start_gradient();
+ /* For CVA calculations, start by guessing deco time = dive time remaining */
+ deco_time = pi->maxtime - t1;
+ vpmb_next_gradient(deco_time, surface_pressure / 1000.0);
+ }
+ }
+ // Use the point where the ceiling clears as the end of deco phase for CVA calculations
+ if (entry->ceiling > 0)
+ time_clear_ceiling = 0;
+ else if (time_clear_ceiling == 0)
+ time_clear_ceiling = t1;
+ }
+ }
+ 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 the ceiling hasn't cleared by the last data point, we need tts for VPM-B CVA calculation
+ * It is not necessary to do these calculation on the first VPMB iteration, except for the last data point */
+ if ((prefs.calcndltts && !print_mode && (prefs.deco_mode != VPMB || in_planner() || !first_iteration)) ||
+ (prefs.deco_mode == VPMB && !in_planner() && i == pi->nr - 1)) {
+ /* only calculate ndl/tts on every 30 seconds */
+ if ((entry->sec - last_ndl_tts_calc_time) < 30 && i != pi->nr - 1) {
+ 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);
+ if (prefs.deco_mode == VPMB && !in_planner() && i == pi->nr - 1)
+ final_tts = entry->tts_calc;
+ /* Restore "real" deco state for next real time step */
+ restore_deco_state(cache_data);
+ free(cache_data);
+ }
+ }
+ if (prefs.deco_mode == VPMB && !in_planner()) {
+ prev_deco_time = deco_time;
+ // Do we need to update deco_time?
+ if (final_tts > 0)
+ deco_time = pi->maxtime + final_tts - time_deep_ceiling;
+ else if (time_clear_ceiling > 0)
+ deco_time = time_clear_ceiling - time_deep_ceiling;
+ vpmb_next_gradient(deco_time, surface_pressure / 1000.0);
+ final_tts = 0;
+ last_ndl_tts_calc_time = 0;
+ first_ceiling = 0;
+ first_iteration = false;
+ count_iteration ++;
+ restore_deco_state(cache_data_initial);
+ } else {
+ // With Buhlmann, or not in planner, iterating isn't needed. This makes the while condition false.
+ prev_deco_time = deco_time = 0;
+ }
+ }
+ free(cache_data_initial);
+#if DECO_CALC_DEBUG & 1
+ dump_tissues();
+#endif
+}
+#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;
+#ifndef SUBSURFACE_MOBILE
+ init_decompression(dive);
+#endif
+ /* 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 */
+#ifndef SUBSURFACE_MOBILE
+ calculate_deco_information(dive, dc, pi, false); /* and ceiling information, using gradient factor values in Preferences) */
+#endif
+ 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/core/profile.h b/core/profile.h
new file mode 100644
index 000000000..abac9dd49
--- /dev/null
+++ b/core/profile.h
@@ -0,0 +1,111 @@
+#ifndef PROFILE_H
+#define PROFILE_H
+
+#include "dive.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/core/qt-gui.h b/core/qt-gui.h
new file mode 100644
index 000000000..92532d22f
--- /dev/null
+++ b/core/qt-gui.h
@@ -0,0 +1,15 @@
+#ifndef QT_GUI_H
+#define QT_GUI_H
+
+void init_qt_late();
+void init_ui();
+
+void run_ui();
+void exit_ui();
+
+#if defined(SUBSURFACE_MOBILE)
+#include <QQuickWindow>
+extern QObject *qqWindowObject;
+#endif
+
+#endif // QT_GUI_H
diff --git a/core/qt-init.cpp b/core/qt-init.cpp
new file mode 100644
index 000000000..b52dfd970
--- /dev/null
+++ b/core/qt-init.cpp
@@ -0,0 +1,48 @@
+#include <QApplication>
+#include <QNetworkProxy>
+#include <QLibraryInfo>
+#include <QTextCodec>
+#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/core/qthelper.cpp b/core/qthelper.cpp
new file mode 100644
index 000000000..1d3951088
--- /dev/null
+++ b/core/qthelper.cpp
@@ -0,0 +1,1615 @@
+#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 <sys/time.h>
+#include "exif.h"
+#include "file.h"
+#include "prefs-macros.h"
+#include <QFile>
+#include <QRegExp>
+#include <QDir>
+#include <QDebug>
+#include <QSettings>
+#include <QStandardPaths>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QNetworkAccessManager>
+#include <QNetworkProxy>
+#include <QDateTime>
+#include <QImageReader>
+#include <QtConcurrent>
+#include <QFont>
+#include <QApplication>
+#include <QTextDocument>
+
+#include <libxslt/documents.h>
+
+const char *existing_filename;
+static QLocale loc;
+
+#define translate(_context, arg) trGettext(arg)
+static const QString DEGREE_SIGNS("dD" UTF8_DEGREE);
+
+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("%1").arg(kg + (gr >= 500 ? 1 : 0));
+ else
+ str = QString("%1.%2").arg(kg).arg((unsigned)(gr + 50) / 100);
+ } else {
+ double lbs = grams_to_lbs(weight_in_grams);
+ if (lbs >= 40.0)
+ lbs = rint(lbs + 0.5);
+ else
+ lbs = rint(lbs + 0.05);
+ 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<int> getDivesInTrip(dive_trip_t *trip)
+{
+ QList<int> 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<int> 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<QString, int> &a, const QPair<QString, int> &b)
+{
+ return a.second < b.second;
+}
+
+void selectedDivesGasUsed(QVector<QPair<QString, int> > &gasUsedOrdered)
+{
+ int i, j;
+ struct dive *d;
+ QMap<QString, int> 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
+#ifdef SUBSURFACE_MOBILE
+ QString userAgent = QString("Subsurface-mobile:%1(%2):").arg(subsurface_mobile_version()).arg(subsurface_canonical_version());
+#else
+ QString userAgent = QString("Subsurface:%1:").arg(subsurface_canonical_version());
+#endif
+ 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;
+
+}
+
+extern "C" const char *subsurface_user_agent()
+{
+ static QString uA = getUserAgent();
+
+ return strdup(qPrintable(uA));
+}
+
+QString uiLanguage(QLocale *callerLoc)
+{
+ QString shortDateFormat;
+ QString dateFormat;
+ QString timeFormat;
+ QSettings s;
+ QVariant v;
+ s.beginGroup("Language");
+
+ if (!s.value("UseSystemLanguage", true).toBool()) {
+ loc = QLocale(s.value("UiLanguage", QLocale().uiLanguages().first()).toString());
+ } else {
+ loc = QLocale(QLocale().uiLanguages().first());
+ }
+ QStringList languages = loc.uiLanguages();
+ QString uiLang;
+ if (languages[0].contains('-'))
+ uiLang = languages[0];
+ else if (languages.count() > 1 && languages[1].contains('-'))
+ uiLang = languages[1];
+ else if (languages.count() > 2 && languages[2].contains('-'))
+ uiLang = languages[2];
+ else
+ uiLang = languages[0];
+ GET_BOOL("time_format_override", time_format_override);
+ GET_BOOL("date_format_override", date_format_override);
+ GET_TXT("time_format", time_format);
+ GET_TXT("date_format", date_format);
+ GET_TXT("date_format_short", date_format_short);
+ 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;
+ QStringList languages = loc2.uiLanguages();
+ if (languages[0].contains('-'))
+ uiLang = languages[0];
+ else if (languages.count() > 1 && languages[1].contains('-'))
+ uiLang = languages[1];
+ else if (languages.count() > 2 && languages[2].contains('-'))
+ uiLang = languages[2];
+ }
+ if (callerLoc)
+ *callerLoc = loc;
+
+ if (!prefs.date_format_override || same_string(prefs.date_format_short, "") || same_string(prefs.date_format, "")) {
+ // derive our standard date format from what the locale gives us
+ // 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");
+ if (!prefs.date_format_override || same_string(prefs.date_format, "")) {
+ free((void*)prefs.date_format);
+ prefs.date_format = strdup(qPrintable(dateFormat));
+ }
+ if (!prefs.date_format_override || same_string(prefs.date_format_short, "")) {
+ free((void*)prefs.date_format_short);
+ prefs.date_format_short = strdup(qPrintable(shortDateFormat));
+ }
+ }
+ if (!prefs.time_format_override || same_string(prefs.time_format, "")) {
+ 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", "");
+ free((void*)prefs.time_format);
+ prefs.time_format = strdup(qPrintable(timeFormat));
+ }
+ return uiLang;
+}
+
+QLocale getLocale()
+{
+ return loc;
+}
+
+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)
+{
+ const char *unit;
+ int decimals;
+ double value = get_volume_units(volume.mliter, &decimals, &unit);
+ 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 parseLengthToMm(const QString &text)
+{
+ int mm;
+ QString numOnly = text;
+ numOnly.replace(",", ".").remove(QRegExp("[^-0-9.]"));
+ if (numOnly.isEmpty())
+ return 0;
+ double number = numOnly.toDouble();
+ if (text.contains(QObject::tr("m"), Qt::CaseInsensitive)) {
+ mm = number * 1000;
+ } else if (text.contains(QObject::tr("ft"), Qt::CaseInsensitive)) {
+ mm = feet_to_mm(number);
+ } else {
+ switch (prefs.units.length) {
+ case units::FEET:
+ mm = feet_to_mm(number);
+ break;
+ case units::METERS:
+ mm = number * 1000;
+ break;
+ default:
+ mm = 0;
+ }
+ }
+ return mm;
+
+}
+
+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();
+ if (text.contains(QObject::tr("C"), Qt::CaseInsensitive)) {
+ mkelvin = C_to_mkelvin(number);
+ } else if (text.contains(QObject::tr("F"), Qt::CaseInsensitive)) {
+ mkelvin = F_to_mkelvin(number);
+ } else {
+ 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;
+}
+
+int parseWeightToGrams(const QString &text)
+{
+ int grams;
+ QString numOnly = text;
+ numOnly.replace(",", ".").remove(QRegExp("[^0-9.]"));
+ if (numOnly.isEmpty())
+ return 0;
+ double number = numOnly.toDouble();
+ if (text.contains(QObject::tr("kg"), Qt::CaseInsensitive)) {
+ grams = rint(number * 1000);
+ } else if (text.contains(QObject::tr("lbs"), Qt::CaseInsensitive)) {
+ grams = lbs_to_grams(number);
+ } else {
+ switch (prefs.units.weight) {
+ case units::KG:
+ grams = rint(number * 1000);
+ break;
+ case units::LBS:
+ grams = lbs_to_grams(number);
+ break;
+ default:
+ grams = 0;
+ }
+ }
+ return grams;
+}
+
+int parsePressureToMbar(const QString &text)
+{
+ int mbar;
+ QString numOnly = text;
+ numOnly.replace(",", ".").remove(QRegExp("[^0-9.]"));
+ if (numOnly.isEmpty())
+ return 0;
+ double number = numOnly.toDouble();
+ if (text.contains(QObject::tr("bar"), Qt::CaseInsensitive)) {
+ mbar = rint(number * 1000);
+ } else if (text.contains(QObject::tr("psi"), Qt::CaseInsensitive)) {
+ mbar = psi_to_mbar(number);
+ } else {
+ switch (prefs.units.pressure) {
+ case units::BAR:
+ mbar = rint(number * 1000);
+ break;
+ case units::PSI:
+ mbar = psi_to_mbar(number);
+ break;
+ default:
+ mbar = 0;
+ }
+ }
+ return mbar;
+}
+
+int parseGasMixO2(const QString &text)
+{
+ QString gasString = text;
+ int o2, number;
+ if (gasString.contains(QObject::tr("AIR"), Qt::CaseInsensitive)) {
+ o2 = O2_IN_AIR;
+ } else if (gasString.contains(QObject::tr("EAN"), Qt::CaseInsensitive)) {
+ gasString.remove(QRegExp("[^0-9]"));
+ number = gasString.toInt();
+ o2 = number * 10;
+ } else if (gasString.contains("/")) {
+ QStringList gasSplit = gasString.split("/");
+ number = gasSplit[0].toInt();
+ o2 = number * 10;
+ } else {
+ number = gasString.toInt();
+ o2 = number * 10;
+ }
+ return o2;
+}
+
+int parseGasMixHE(const QString &text)
+{
+ QString gasString = text;
+ int he, number;
+ if (gasString.contains("/")) {
+ QStringList gasSplit = gasString.split("/");
+ number = gasSplit[1].toInt();
+ he = number * 10;
+ } else {
+ he = 0;
+ }
+ return he;
+}
+
+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(), QString(prefs.date_format) + " " + prefs.time_format);
+}
+
+QString get_short_dive_date_string(timestamp_t when)
+{
+ QDateTime ts;
+ ts.setMSecsSinceEpoch(when * 1000L);
+ return loc.toString(ts.toUTC(), QString(prefs.date_format_short) + " " + prefs.time_format);
+}
+
+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(prefs.date_format) + 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<QString, QByteArray> hashOf;
+QMutex hashOfMutex;
+QHash<QByteArray, QString> localFilenameOf;
+QHash <QString, QImage > thumbnailCache;
+
+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;
+ stream >> thumbnailCache;
+ hashfile.close();
+ }
+}
+
+void write_hashes()
+{
+ QSaveFile hashfile(hashfile_name());
+ if (hashfile.open(QIODevice::WriteOnly)) {
+ QDataStream stream(&hashfile);
+ stream << localFilenameOf;
+ stream << hashOf;
+ stream << thumbnailCache;
+ 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)];
+}
+
+// This needs to operate on a copy of picture as it frees it after finishing!
+void updateHash(struct picture *picture) {
+ QByteArray hash = hashFile(fileFromHash(picture->hash));
+ learnHash(picture, hash);
+ picture_free(picture);
+}
+
+// This needs to operate on a copy of picture as it frees it after finishing!
+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);
+ picture_free(picture);
+}
+
+extern "C" void cache_picture(struct picture *picture)
+{
+ QString filename = picture->filename;
+ if (!hashOf.contains(filename))
+ QtConcurrent::run(hashPicture, clone_picture(picture));
+}
+
+void learnImages(const QDir dir, int max_recursions)
+{
+ QStringList filters, files;
+
+ if (max_recursions) {
+ foreach (QString dirname, dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs)) {
+ learnImages(QDir(dir.filePath(dirname)), max_recursions - 1);
+ }
+ }
+
+ 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;
+
+ uiLanguage(NULL);
+ 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("RulerBar", rulergraph);
+ 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<QFont>();
+ 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);
+#ifndef SUBSURFACE_MOBILE
+ GET_BOOL("save_password_local", save_password_local);
+#else
+ // always save the password in Subsurface-mobile
+ prefs.save_password_local = true;
+#endif
+ 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);
+ GET_BOOL("git_local_only", git_local_only);
+
+ // 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);
+
+ // but the related time / distance threshold (only used in the mobile app)
+ // are in their own group
+ s.beginGroup("locationService");
+ GET_INT("distance_threshold", distance_threshold);
+ GET_INT("time_threshold", time_threshold);
+ s.endGroup();
+
+ // 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();
+
+ // GPS service time and distance thresholds
+ s.beginGroup("LocationService");
+ GET_INT("time_threshold", time_threshold);
+ GET_INT("distance_threshold", distance_threshold);
+ 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");
+}
+
+void init_proxy()
+{
+ 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);
+}
+
+QString 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;
+}
diff --git a/core/qthelper.h b/core/qthelper.h
new file mode 100644
index 000000000..3a5ef60e4
--- /dev/null
+++ b/core/qthelper.h
@@ -0,0 +1,48 @@
+#ifndef QTHELPER_H
+#define QTHELPER_H
+
+#include <QMultiMap>
+#include <QString>
+#include <stdint.h>
+#include "dive.h"
+#include "divelist.h"
+#include <QTranslator>
+#include <QDir>
+
+// 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<int> 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);
+void add_hash(const QString filename, QByteArray hash);
+void hashPicture(struct picture *picture);
+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);
+void init_proxy();
+QString getUUID();
+
+#endif // QTHELPER_H
diff --git a/core/qthelperfromc.h b/core/qthelperfromc.h
new file mode 100644
index 000000000..32aed8949
--- /dev/null
+++ b/core/qthelperfromc.h
@@ -0,0 +1,22 @@
+#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();
+const char *subsurface_user_agent();
+
+#endif // QTHELPERFROMC_H
diff --git a/core/qtserialbluetooth.cpp b/core/qtserialbluetooth.cpp
new file mode 100644
index 000000000..6b104157a
--- /dev/null
+++ b/core/qtserialbluetooth.cpp
@@ -0,0 +1,416 @@
+#include <errno.h>
+
+#include <QtBluetooth/QBluetoothAddress>
+#include <QtBluetooth/QBluetoothSocket>
+#include <QEventLoop>
+#include <QTimer>
+#include <QDebug>
+
+#include <libdivecomputer/version.h>
+
+#if defined(SSRF_CUSTOM_SERIAL)
+
+#if defined(Q_OS_WIN)
+ #include <winsock2.h>
+ #include <windows.h>
+ #include <ws2bth.h>
+#endif
+
+#include <libdivecomputer/custom_serial.h>
+
+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)
+{
+ (void)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/core/save-git.c b/core/save-git.c
new file mode 100644
index 000000000..e22019ab0
--- /dev/null
+++ b/core/save-git.c
@@ -0,0 +1,1249 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <git2.h>
+
+#include "dive.h"
+#include "divelist.h"
+#include "device.h"
+#include "membuffer.h"
+#include "git-access.h"
+#include "version.h"
+#include "qthelperfromc.h"
+
+#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;
+}
+
+void report_message(const char *msg)
+{
+ (void)report_error("%s", msg);
+}
+
+/*
+ * 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 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 0
+ /* storing pictures into git was a mistake. This makes for HUGE git repositories */
+ 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);
+ }
+#endif
+ 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, bool cached_ok)
+{
+ struct divecomputer *dc;
+ struct membuffer buf = { 0 }, name = { 0 };
+ struct dir *subdir;
+ int ret, nr;
+
+ /* Create dive directory */
+ create_dive_name(dive, &name, tm);
+
+ /*
+ * If the dive git ID is valid, we just create the whole directory
+ * with that ID
+ */
+ if (cached_ok && dive_cache_is_valid(dive)) {
+ git_oid oid;
+ git_oid_fromraw(&oid, dive->git_id);
+ ret = tree_insert(tree->files, mb_cstring(&name), 1,
+ &oid, GIT_FILEMODE_TREE);
+ free_buffer(&name);
+ if (ret)
+ return report_error("cached dive tree insert failed");
+ return 0;
+ }
+
+ 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, bool cached_ok)
+{
+ 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, cached_ok);
+ }
+
+ 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, bool cached_ok)
+{
+ 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 */
+ int notify_increment = dive_table.nr > 10 ? dive_table.nr / 10 : 1;
+ int last_threshold = 0;
+ for_each_dive(i, dive) {
+ struct tm tm;
+ struct dir *tree;
+ char buf[] = "save dives x0%";
+
+ if (i / notify_increment > last_threshold) {
+ // notify of progress - we cover the range of 20..50
+ last_threshold = i / notify_increment;
+ buf[11] = last_threshold + '0';
+ git_storage_update_progress(20 + 3 * last_threshold, buf);
+ }
+
+ 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, cached_ok);
+ continue;
+ }
+
+ save_one_dive(repo, tree, dive, &tm, cached_ok);
+ }
+ 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)
+{
+ (void) baseline;
+ (void) target;
+ (void) workdir;
+ (void) payload;
+ (void) why;
+ 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_user_agent());
+}
+
+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))
+ 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, "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, 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;
+ bool cached_ok;
+
+ if (verbose)
+ fprintf(stderr, "git storage: do git save\n");
+
+ if (!create_empty) // so we are actually saving the dives
+ git_storage_update_progress(19, "start git save");
+
+ /*
+ * Check if we can do the cached writes - we need to
+ * have the original git commit we loaded in the repo
+ */
+ cached_ok = try_to_find_parent(saved_git_id, repo);
+
+ /* 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, cached_ok))
+ return -1;
+
+ if (verbose)
+ fprintf(stderr, "git storage, write git tree\n");
+
+ 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/core/save-html.c b/core/save-html.c
new file mode 100644
index 000000000..2d0ea9cf3
--- /dev/null
+++ b/core/save-html.c
@@ -0,0 +1,559 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#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_depth(struct membuffer *b, struct dive *dive, const char *pre, const char *post)
+{
+ const char *unit;
+ double value;
+ struct units *units_p = get_units();
+
+ if (!dive->maxdepth.mm) {
+ put_format(b, "%s--%s", pre, post);
+ return;
+ }
+ value = get_depth_units(dive->maxdepth.mm, NULL, &unit);
+
+ switch (units_p->length) {
+ case METERS:
+ default:
+ put_format(b, "%s%.1f %s%s", pre, value, unit, post);
+ break;
+ case FEET:
+ put_format(b, "%s%.0f %s%s", pre, value, unit, post);
+ break;
+ }
+}
+
+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/core/save-html.h b/core/save-html.h
new file mode 100644
index 000000000..13bb102b1
--- /dev/null
+++ b/core/save-html.h
@@ -0,0 +1,31 @@
+#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_depth(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/core/save-xml.c b/core/save-xml.c
new file mode 100644
index 000000000..2335637e8
--- /dev/null
+++ b/core/save-xml.c
@@ -0,0 +1,749 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#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, " <depth");
+ put_depth(b, dc->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, " <divetemperature");
+ if (dive->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, " <temperature");
+ put_temperature(b, dc->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");
+ put_pressure(b, dc->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, " <water");
+ put_salinity(b, dc->salinity, " salinity='", " g/l'");
+ put_string(b, " />\n");
+}
+
+static void save_overview(struct membuffer *b, struct dive *dive)
+{
+ show_utf8(b, dive->divemaster, " <divemaster>", "</divemaster>\n", 0);
+ show_utf8(b, dive->buddy, " <buddy>", "</buddy>\n", 0);
+ show_utf8(b, dive->notes, " <notes>", "</notes>\n", 0);
+ show_utf8(b, dive->suit, " <suit>", "</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, " <cylinder");
+ if (volume)
+ put_milli(b, " size='", volume, " l'");
+ put_pressure(b, cylinder->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, " <weightsystem");
+ put_milli(b, " weight='", grams, " kg'");
+ show_utf8(b, description, " description='", "'", 1);
+ 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, " <sample time='%u:%02u min'", FRACTION(sample->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, " <event time='%d:%02d min'", 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, 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, " <extradata");
+ show_utf8(b, ed->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, " <divecomputer");
+ show_utf8(b, dc->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, " <surfacetime>", " min</surfacetime>\n");
+ save_extra_data(b, dc->extra_data);
+ save_events(b, dc->events);
+ save_samples(b, dc->samples, dc->sample);
+
+ put_format(b, " </divecomputer>\n");
+}
+
+static void save_picture(struct membuffer *b, struct picture *pic)
+{
+ put_string(b, " <picture filename='");
+ put_quoted(b, pic->filename, true, false);
+ put_string(b, "'");
+ if (pic->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, "<dive");
+ if (dive->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, "</dive>\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, "<trip");
+ show_date(b, trip->when);
+ show_utf8(b, trip->location, " location=\'", "\'", 1);
+ put_format(b, ">\n");
+ show_utf8(b, trip->notes, "<notes>", "</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, "</trip>\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, "<divecomputerid");
+ show_utf8(b, model, " model='", "'", 1);
+ put_format(b, " deviceid='%08x'", deviceid);
+ show_utf8(b, serial_nr, " serial='", "'", 1);
+ show_utf8(b, firmware, " firmware='", "'", 1);
+ show_utf8(b, nickname, " nickname='", "'", 1);
+ 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, "<divelog program='subsurface' version='%d'>\n<settings>\n", DATAFORMAT_VERSION);
+
+ if (prefs.save_userid_local)
+ put_format(b, " <userid>%30s</userid>\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, " <autogroup state='1' />\n");
+ put_format(b, "</settings>\n");
+
+ /* save the dive sites - to make the output consistent let's sort the table, first */
+ dive_site_table_sort();
+ put_format(b, "<divesites>\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, "<site uuid='%8x'", ds->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, " <notes>", " </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, " <geo cat='%d'", t->category);
+ put_format(b, " origin='%d'", t->origin);
+ show_utf8(b, t->value, " value='", "'/>\n", 1);
+ }
+ }
+ }
+ put_format(b, "</site>\n");
+ }
+ put_format(b, "</divesites>\n<dives>\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, "</dives>\n</divelog>\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);
+
+ save_dives_buffer(&buf, select_only);
+
+ if (same_string(filename, "-")) {
+ f = stdout;
+ } else {
+ try_to_backup(filename);
+ 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/core/serial_ftdi.c b/core/serial_ftdi.c
new file mode 100644
index 000000000..ff1335171
--- /dev/null
+++ b/core/serial_ftdi.c
@@ -0,0 +1,665 @@
+/*
+ * 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 <stdlib.h> // malloc, free
+#include <string.h> // strerror
+#include <errno.h> // errno
+#include <sys/time.h> // gettimeofday
+#include <time.h> // nanosleep
+#include <stdio.h>
+
+#include <libusb.h>
+#include <ftdi.h>
+
+#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 <android/log.h>
+#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 <libdivecomputer/custom_serial.h>
+
+/* 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)) {
+ free(device);
+ 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/core/sha1.c b/core/sha1.c
new file mode 100644
index 000000000..acf8c5d9f
--- /dev/null
+++ b/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 <string.h>
+#include <stdint.h>
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <arpa/inet.h>
+#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/core/sha1.h b/core/sha1.h
new file mode 100644
index 000000000..cab6ff77d
--- /dev/null
+++ b/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/core/statistics.c b/core/statistics.c
new file mode 100644
index 000000000..6a05cffc1
--- /dev/null
+++ b/core/statistics.c
@@ -0,0 +1,404 @@
+/* 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 <string.h>
+#include <ctype.h>
+
+#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;
+stats_t *stats_by_type = 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;
+ uint32_t 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, tsize;
+
+ *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);
+ free(stats_by_type);
+
+ size = sizeof(stats_t) * (dive_table.nr + 1);
+ tsize = sizeof(stats_t) * (NUM_DC_TYPE + 1);
+ stats_yearly = malloc(size);
+ stats_monthly = malloc(size);
+ stats_by_trip = malloc(size);
+ stats_by_type = malloc(tsize);
+ if (!stats_yearly || !stats_monthly || !stats_by_trip || !stats_by_type)
+ return;
+ memset(stats_yearly, 0, size);
+ memset(stats_monthly, 0, size);
+ memset(stats_by_trip, 0, size);
+ memset(stats_by_type, 0, tsize);
+ stats_yearly[0].is_year = true;
+
+ /* Setting the is_trip to true to show the location as first
+ * field in the statistics window */
+ stats_by_type[0].location = strdup("All (by type stats)");
+ stats_by_type[0].is_trip = true;
+ stats_by_type[1].location = strdup("OC");
+ stats_by_type[1].is_trip = true;
+ stats_by_type[2].location = strdup("CCR");
+ stats_by_type[2].is_trip = true;
+ stats_by_type[3].location = strdup("pSCR");
+ stats_by_type[3].is_trip = true;
+ stats_by_type[4].location = strdup("Freedive");
+ stats_by_type[4].is_trip = 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;
+
+ /* stats_by_type[0] is all the dives combined */
+ stats_by_type[0].selection_size++;
+ process_dive(dp, &(stats_by_type[0]));
+
+ process_dive(dp, &(stats_by_type[dp->dc.divemode + 1]));
+ stats_by_type[dp->dc.divemode + 1].selection_size++;
+
+ 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, size_t 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 == (unsigned int)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 */
+ size_t 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/core/statistics.h b/core/statistics.h
new file mode 100644
index 000000000..015c3481e
--- /dev/null
+++ b/core/statistics.h
@@ -0,0 +1,59 @@
+/*
+ * 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 stats_t *stats_by_type;
+
+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, size_t 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/core/strndup.h b/core/strndup.h
new file mode 100644
index 000000000..84e18b60f
--- /dev/null
+++ b/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/core/strtod.c b/core/strtod.c
new file mode 100644
index 000000000..81e5d42d1
--- /dev/null
+++ b/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 <ctype.h>
+#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/core/subsurface-qt/DiveObjectHelper.cpp b/core/subsurface-qt/DiveObjectHelper.cpp
new file mode 100644
index 000000000..ab88d2f3c
--- /dev/null
+++ b/core/subsurface-qt/DiveObjectHelper.cpp
@@ -0,0 +1,338 @@
+#include "DiveObjectHelper.h"
+
+#include <QDateTime>
+#include <QTextDocument>
+
+#include "../qthelper.h"
+#include "../helpers.h"
+
+static QString EMPTY_DIVE_STRING = QStringLiteral("--");
+enum returnPressureSelector {START_PRESSURE, END_PRESSURE};
+
+static QString getFormattedWeight(struct dive *dive, unsigned int idx)
+{
+ weightsystem_t *weight = &dive->weightsystem[idx];
+ if (!weight->description)
+ return QString(EMPTY_DIVE_STRING);
+ QString fmt = QString(weight->description);
+ fmt += ", " + get_weight_string(weight->weight, true);
+ return fmt;
+}
+
+static QString getFormattedCylinder(struct dive *dive, unsigned int idx)
+{
+ cylinder_t *cyl = &dive->cylinder[idx];
+ const char *desc = cyl->type.description;
+ if (!desc && idx > 0)
+ return QString(EMPTY_DIVE_STRING);
+ QString fmt = desc ? QString(desc) : QObject::tr("unknown");
+ fmt += ", " + get_volume_string(cyl->type.size, true);
+ fmt += ", " + get_pressure_string(cyl->type.workingpressure, true);
+ fmt += ", " + get_pressure_string(cyl->start, false) + " - " + get_pressure_string(cyl->end, true);
+ fmt += ", " + get_gas_string(cyl->gasmix);
+ return fmt;
+}
+
+static QString getPressures(struct dive *dive, enum returnPressureSelector ret)
+{
+ cylinder_t *cyl = &dive->cylinder[0];
+ QString fmt;
+ if (ret == START_PRESSURE) {
+ if (cyl->start.mbar)
+ fmt = get_pressure_string(cyl->start, true);
+ else if (cyl->sample_start.mbar)
+ fmt = get_pressure_string(cyl->sample_start, true);
+ }
+ if (ret == END_PRESSURE) {
+ if (cyl->end.mbar)
+ fmt = get_pressure_string(cyl->end, true);
+ else if(cyl->sample_end.mbar)
+ fmt = get_pressure_string(cyl->sample_end, true);
+ }
+ return fmt;
+}
+
+DiveObjectHelper::DiveObjectHelper(struct dive *d) :
+ m_dive(d)
+{
+}
+
+DiveObjectHelper::~DiveObjectHelper()
+{
+}
+
+int DiveObjectHelper::number() const
+{
+ return m_dive->number;
+}
+
+int DiveObjectHelper::id() const
+{
+ return m_dive->id;
+}
+
+QString DiveObjectHelper::date() const
+{
+ QDateTime localTime = QDateTime::fromTime_t(m_dive->when - gettimezoneoffset(m_dive->when));
+ localTime.setTimeSpec(Qt::UTC);
+ return localTime.date().toString(prefs.date_format);
+}
+
+timestamp_t DiveObjectHelper::timestamp() const
+{
+ return m_dive->when;
+}
+
+QString DiveObjectHelper::time() const
+{
+ QDateTime localTime = QDateTime::fromTime_t(m_dive->when - gettimezoneoffset(m_dive->when));
+ localTime.setTimeSpec(Qt::UTC);
+ return localTime.time().toString(prefs.time_format);
+}
+
+QString DiveObjectHelper::location() const
+{
+ return get_dive_location(m_dive) ? QString::fromUtf8(get_dive_location(m_dive)) : EMPTY_DIVE_STRING;
+}
+
+QString DiveObjectHelper::gps() const
+{
+ struct dive_site *ds = get_dive_site_by_uuid(m_dive->dive_site_uuid);
+ return ds ? QString(printGPSCoords(ds->latitude.udeg, ds->longitude.udeg)) : QString();
+}
+QString DiveObjectHelper::duration() const
+{
+ return get_dive_duration_string(m_dive->duration.seconds, QObject::tr("h:"), QObject::tr("min"));
+}
+
+bool DiveObjectHelper::noDive() const
+{
+ return m_dive->duration.seconds == 0 && m_dive->dc.duration.seconds == 0;
+}
+
+QString DiveObjectHelper::depth() const
+{
+ return get_depth_string(m_dive->dc.maxdepth.mm, true, true);
+}
+
+QString DiveObjectHelper::divemaster() const
+{
+ return m_dive->divemaster ? m_dive->divemaster : EMPTY_DIVE_STRING;
+}
+
+QString DiveObjectHelper::buddy() const
+{
+ return m_dive->buddy ? m_dive->buddy : EMPTY_DIVE_STRING;
+}
+
+QString DiveObjectHelper::airTemp() const
+{
+ QString temp = get_temperature_string(m_dive->airtemp, true);
+ if (temp.isEmpty()) {
+ temp = EMPTY_DIVE_STRING;
+ }
+ return temp;
+}
+
+QString DiveObjectHelper::waterTemp() const
+{
+ QString temp = get_temperature_string(m_dive->watertemp, true);
+ if (temp.isEmpty()) {
+ temp = EMPTY_DIVE_STRING;
+ }
+ return temp;
+}
+
+QString DiveObjectHelper::notes() const
+{
+ QString tmp = m_dive->notes ? QString::fromUtf8(m_dive->notes) : EMPTY_DIVE_STRING;
+ if (same_string(m_dive->dc.model, "planned dive")) {
+ QTextDocument notes;
+ #define _NOTES_BR "&#92n"
+ tmp.replace("<thead>", "<thead>" _NOTES_BR)
+ .replace("<br>", "<br>" _NOTES_BR)
+ .replace("<tr>", "<tr>" _NOTES_BR)
+ .replace("</tr>", "</tr>" _NOTES_BR);
+ notes.setHtml(tmp);
+ tmp = notes.toPlainText();
+ tmp.replace(_NOTES_BR, "<br>");
+ #undef _NOTES_BR
+ } else {
+ tmp.replace("\n", "<br>");
+ }
+ return tmp;
+}
+
+QString DiveObjectHelper::tags() const
+{
+ static char buffer[256];
+ taglist_get_tagstring(m_dive->tag_list, buffer, 256);
+ return QString(buffer);
+}
+
+QString DiveObjectHelper::gas() const
+{
+ /*WARNING: here should be the gastlist, returned
+ * from the get_gas_string function or this is correct?
+ */
+ QString gas, gases;
+ for (int i = 0; i < MAX_CYLINDERS; i++) {
+ if (!is_cylinder_used(m_dive, i))
+ continue;
+ gas = m_dive->cylinder[i].type.description;
+ if (!gas.isEmpty())
+ gas += QChar(' ');
+ gas += gasname(&m_dive->cylinder[i].gasmix);
+ // if has a description and if such gas is not already present
+ if (!gas.isEmpty() && gases.indexOf(gas) == -1) {
+ if (!gases.isEmpty())
+ gases += QString(" / ");
+ gases += gas;
+ }
+ }
+ return gases;
+}
+
+QString DiveObjectHelper::sac() const
+{
+ if (!m_dive->sac)
+ return QString();
+ const char *unit;
+ int decimal;
+ double value = get_volume_units(m_dive->sac, &decimal, &unit);
+ return QString::number(value, 'f', decimal).append(unit);
+}
+
+QString DiveObjectHelper::weightList() const
+{
+ QString weights;
+ for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) {
+ QString w = getFormattedWeight(m_dive, i);
+ if (w == EMPTY_DIVE_STRING)
+ continue;
+ weights += w + "; ";
+ }
+ return weights;
+}
+
+QStringList DiveObjectHelper::weights() const
+{
+ QStringList weights;
+ for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++)
+ weights << getFormattedWeight(m_dive, i);
+ return weights;
+}
+
+bool DiveObjectHelper::singleWeight() const
+{
+ return weightsystem_none(&m_dive->weightsystem[1]);
+}
+
+QString DiveObjectHelper::weight(int idx) const
+{
+ if ( (idx < 0) || idx > MAX_WEIGHTSYSTEMS )
+ return QString(EMPTY_DIVE_STRING);
+ return getFormattedWeight(m_dive, idx);
+}
+
+QString DiveObjectHelper::suit() const
+{
+ return m_dive->suit ? m_dive->suit : EMPTY_DIVE_STRING;
+}
+
+QString DiveObjectHelper::cylinderList() const
+{
+ QString cylinders;
+ for (int i = 0; i < MAX_CYLINDERS; i++) {
+ QString cyl = getFormattedCylinder(m_dive, i);
+ if (cyl == EMPTY_DIVE_STRING)
+ continue;
+ cylinders += cyl + "; ";
+ }
+ return cylinders;
+}
+
+QStringList DiveObjectHelper::cylinders() const
+{
+ QStringList cylinders;
+ for (int i = 0; i < MAX_CYLINDERS; i++)
+ cylinders << getFormattedCylinder(m_dive, i);
+ return cylinders;
+}
+
+QString DiveObjectHelper::cylinder(int idx) const
+{
+ if ( (idx < 0) || idx > MAX_CYLINDERS)
+ return QString(EMPTY_DIVE_STRING);
+ return getFormattedCylinder(m_dive, idx);
+}
+
+QString DiveObjectHelper::trip() const
+{
+ return m_dive->divetrip ? m_dive->divetrip->location : EMPTY_DIVE_STRING;
+}
+
+// combine the pointer address with the trip location so that
+// we detect multiple, destinct trips to the same location
+QString DiveObjectHelper::tripMeta() const
+{
+ QString ret = EMPTY_DIVE_STRING;
+ if (m_dive->divetrip)
+ ret = QString::number((quint64)m_dive->divetrip, 16) + QLatin1Literal("::") + m_dive->divetrip->location;
+ return ret;
+}
+
+QString DiveObjectHelper::maxcns() const
+{
+ return QString(m_dive->maxcns);
+}
+
+QString DiveObjectHelper::otu() const
+{
+ return QString(m_dive->otu);
+}
+
+int DiveObjectHelper::rating() const
+{
+ return m_dive->rating;
+}
+
+QString DiveObjectHelper::sumWeight() const
+{
+ weight_t sum = { 0 };
+ for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++){
+ sum.grams += m_dive->weightsystem[i].weight.grams;
+ }
+ return get_weight_string(sum, true);
+}
+
+QString DiveObjectHelper::getCylinder() const
+{
+ QString getCylinder;
+ if (is_cylinder_used(m_dive, 1)){
+ getCylinder = QObject::tr("Multiple");
+ }
+ else {
+ getCylinder = m_dive->cylinder[0].type.description;
+ }
+ return getCylinder;
+}
+
+QString DiveObjectHelper::startPressure() const
+{
+ QString startPressure = getPressures(m_dive, START_PRESSURE);
+ return startPressure;
+}
+
+QString DiveObjectHelper::endPressure() const
+{
+ QString endPressure = getPressures(m_dive, END_PRESSURE);
+ return endPressure;
+}
+
+QString DiveObjectHelper::firstGas() const
+{
+ QString gas;
+ gas = get_gas_string(m_dive->cylinder[0].gasmix);
+ return gas;
+}
diff --git a/core/subsurface-qt/DiveObjectHelper.h b/core/subsurface-qt/DiveObjectHelper.h
new file mode 100644
index 000000000..602775ef8
--- /dev/null
+++ b/core/subsurface-qt/DiveObjectHelper.h
@@ -0,0 +1,89 @@
+#ifndef DIVE_QOBJECT_H
+#define DIVE_QOBJECT_H
+
+#include "../dive.h"
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+class DiveObjectHelper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(int number READ number CONSTANT)
+ Q_PROPERTY(int id READ id CONSTANT)
+ Q_PROPERTY(int rating READ rating CONSTANT)
+ Q_PROPERTY(QString date READ date CONSTANT)
+ Q_PROPERTY(QString time READ time CONSTANT)
+ Q_PROPERTY(QString location READ location CONSTANT)
+ Q_PROPERTY(QString gps READ gps CONSTANT)
+ Q_PROPERTY(QString duration READ duration CONSTANT)
+ Q_PROPERTY(bool noDive READ noDive CONSTANT)
+ Q_PROPERTY(QString depth READ depth CONSTANT)
+ Q_PROPERTY(QString divemaster READ divemaster CONSTANT)
+ Q_PROPERTY(QString buddy READ buddy CONSTANT)
+ Q_PROPERTY(QString airTemp READ airTemp CONSTANT)
+ Q_PROPERTY(QString waterTemp READ waterTemp CONSTANT)
+ Q_PROPERTY(QString notes READ notes CONSTANT)
+ Q_PROPERTY(QString tags READ tags CONSTANT)
+ Q_PROPERTY(QString gas READ gas CONSTANT)
+ Q_PROPERTY(QString sac READ sac CONSTANT)
+ Q_PROPERTY(QString weightList READ weightList CONSTANT)
+ Q_PROPERTY(QStringList weights READ weights CONSTANT)
+ Q_PROPERTY(bool singleWeight READ singleWeight CONSTANT)
+ Q_PROPERTY(QString suit READ suit CONSTANT)
+ Q_PROPERTY(QString cylinderList READ cylinderList CONSTANT)
+ Q_PROPERTY(QStringList cylinders READ cylinders CONSTANT)
+ Q_PROPERTY(QString trip READ trip CONSTANT)
+ Q_PROPERTY(QString tripMeta READ tripMeta CONSTANT)
+ Q_PROPERTY(QString maxcns READ maxcns CONSTANT)
+ Q_PROPERTY(QString otu READ otu CONSTANT)
+ Q_PROPERTY(QString sumWeight READ sumWeight CONSTANT)
+ Q_PROPERTY(QString getCylinder READ getCylinder CONSTANT)
+ Q_PROPERTY(QString startPressure READ startPressure CONSTANT)
+ Q_PROPERTY(QString endPressure READ endPressure CONSTANT)
+ Q_PROPERTY(QString firstGas READ firstGas CONSTANT)
+public:
+ DiveObjectHelper(struct dive *dive = NULL);
+ ~DiveObjectHelper();
+ int number() const;
+ int id() const;
+ int rating() const;
+ QString date() const;
+ timestamp_t timestamp() const;
+ QString time() const;
+ QString location() const;
+ QString gps() const;
+ QString duration() const;
+ bool noDive() 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 weightList() const;
+ QStringList weights() const;
+ QString weight(int idx) const;
+ bool singleWeight() const;
+ QString suit() const;
+ QString cylinderList() const;
+ QStringList cylinders() const;
+ QString cylinder(int idx) const;
+ QString trip() const;
+ QString tripMeta() const;
+ QString maxcns() const;
+ QString otu() const;
+ QString sumWeight() const;
+ QString getCylinder() const;
+ QString startPressure() const;
+ QString endPressure() const;
+ QString firstGas() const;
+
+private:
+ struct dive *m_dive;
+};
+ Q_DECLARE_METATYPE(DiveObjectHelper *)
+
+#endif
diff --git a/core/subsurface-qt/SettingsObjectWrapper.cpp b/core/subsurface-qt/SettingsObjectWrapper.cpp
new file mode 100644
index 000000000..e43be1a9b
--- /dev/null
+++ b/core/subsurface-qt/SettingsObjectWrapper.cpp
@@ -0,0 +1,1617 @@
+#include "SettingsObjectWrapper.h"
+#include <QSettings>
+#include <QApplication>
+#include <QFont>
+
+#include "../dive.h" // TODO: remove copy_string from dive.h
+
+
+static QString tecDetails = QStringLiteral("TecDetails");
+
+PartialPressureGasSettings::PartialPressureGasSettings(QObject* parent):
+ QObject(parent),
+ group("TecDetails")
+{
+
+}
+
+short PartialPressureGasSettings::showPo2() const
+{
+ return prefs.pp_graphs.po2;
+}
+
+short PartialPressureGasSettings::showPn2() const
+{
+ return prefs.pp_graphs.pn2;
+}
+
+short PartialPressureGasSettings::showPhe() const
+{
+ return prefs.pp_graphs.phe;
+}
+
+double PartialPressureGasSettings::po2Threshold() const
+{
+ return prefs.pp_graphs.po2_threshold;
+}
+
+double PartialPressureGasSettings::pn2Threshold() const
+{
+ return prefs.pp_graphs.pn2_threshold;
+}
+
+double PartialPressureGasSettings::pheThreshold() const
+{
+ return prefs.pp_graphs.phe_threshold;
+}
+
+void PartialPressureGasSettings::setShowPo2(short value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("po2graph", value);
+ prefs.pp_graphs.po2 = value;
+ emit showPo2Changed(value);
+}
+
+void PartialPressureGasSettings::setShowPn2(short value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("pn2graph", value);
+ prefs.pp_graphs.pn2 = value;
+ emit showPn2Changed(value);
+}
+
+void PartialPressureGasSettings::setShowPhe(short value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("phegraph", value);
+ prefs.pp_graphs.phe = value;
+ emit showPheChanged(value);
+}
+
+void PartialPressureGasSettings::setPo2Threshold(double value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("po2threshold", value);
+ prefs.pp_graphs.po2_threshold = value;
+ emit po2ThresholdChanged(value);
+}
+
+void PartialPressureGasSettings::setPn2Threshold(double value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("pn2threshold", value);
+ prefs.pp_graphs.pn2_threshold = value;
+ emit pn2ThresholdChanged(value);
+}
+
+void PartialPressureGasSettings::setPheThreshold(double value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("phethreshold", value);
+ prefs.pp_graphs.phe_threshold = value;
+ emit pheThresholdChanged(value);
+}
+
+
+TechnicalDetailsSettings::TechnicalDetailsSettings(QObject* parent): QObject(parent)
+{
+
+}
+
+double TechnicalDetailsSettings:: modp02() const
+{
+ return prefs.modpO2;
+}
+
+bool TechnicalDetailsSettings::ead() const
+{
+ return prefs.ead;
+}
+
+bool TechnicalDetailsSettings::dcceiling() const
+{
+ return prefs.dcceiling;
+}
+
+bool TechnicalDetailsSettings::redceiling() const
+{
+ return prefs.redceiling;
+}
+
+bool TechnicalDetailsSettings::calcceiling() const
+{
+ return prefs.calcceiling;
+}
+
+bool TechnicalDetailsSettings::calcceiling3m() const
+{
+ return prefs.calcceiling3m;
+}
+
+bool TechnicalDetailsSettings::calcalltissues() const
+{
+ return prefs.calcalltissues;
+}
+
+bool TechnicalDetailsSettings::calcndltts() const
+{
+ return prefs.calcndltts;
+}
+
+bool TechnicalDetailsSettings::gflow() const
+{
+ return prefs.gflow;
+}
+
+bool TechnicalDetailsSettings::gfhigh() const
+{
+ return prefs.gfhigh;
+}
+
+bool TechnicalDetailsSettings::hrgraph() const
+{
+ return prefs.hrgraph;
+}
+
+bool TechnicalDetailsSettings::tankBar() const
+{
+ return prefs.tankbar;
+}
+
+bool TechnicalDetailsSettings::percentageGraph() const
+{
+ return prefs.percentagegraph;
+}
+
+bool TechnicalDetailsSettings::rulerGraph() const
+{
+ return prefs.rulergraph;
+}
+
+bool TechnicalDetailsSettings::showCCRSetpoint() const
+{
+ return prefs.show_ccr_setpoint;
+}
+
+bool TechnicalDetailsSettings::showCCRSensors() const
+{
+ return prefs.show_ccr_sensors;
+}
+
+bool TechnicalDetailsSettings::zoomedPlot() const
+{
+ return prefs.zoomed_plot;
+}
+
+bool TechnicalDetailsSettings::showSac() const
+{
+ return prefs.show_sac;
+}
+
+bool TechnicalDetailsSettings::gfLowAtMaxDepth() const
+{
+ return prefs.gf_low_at_maxdepth;
+}
+
+bool TechnicalDetailsSettings::displayUnusedTanks() const
+{
+ return prefs.display_unused_tanks;
+}
+
+bool TechnicalDetailsSettings::showAverageDepth() const
+{
+ return prefs.show_average_depth;
+}
+
+bool TechnicalDetailsSettings::mod() const
+{
+ return prefs.mod;
+}
+
+bool TechnicalDetailsSettings::showPicturesInProfile() const
+{
+ return prefs.show_pictures_in_profile;
+}
+
+void TechnicalDetailsSettings::setModp02(double value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("modpO2", value);
+ prefs.modpO2 = value;
+ emit modpO2Changed(value);
+}
+
+void TechnicalDetailsSettings::setShowPicturesInProfile(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("show_pictures_in_profile", value);
+ prefs.show_pictures_in_profile = value;
+ emit showPicturesInProfileChanged(value);
+}
+
+void TechnicalDetailsSettings::setEad(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("ead", value);
+ prefs.ead = value;
+ emit eadChanged(value);
+}
+
+void TechnicalDetailsSettings::setMod(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("mod", value);
+ prefs.mod = value;
+ emit modChanged(value);
+}
+
+void TechnicalDetailsSettings::setDCceiling(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("dcceiling", value);
+ prefs.dcceiling = value;
+ emit dcceilingChanged(value);
+}
+
+void TechnicalDetailsSettings::setRedceiling(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("redceiling", value);
+ prefs.redceiling = value;
+ emit redceilingChanged(value);
+}
+
+void TechnicalDetailsSettings::setCalcceiling(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("calcceiling", value);
+ prefs.calcceiling = value;
+ emit calcceilingChanged(value);
+}
+
+void TechnicalDetailsSettings::setCalcceiling3m(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("calcceiling3m", value);
+ prefs.calcceiling3m = value;
+ emit calcceiling3mChanged(value);
+}
+
+void TechnicalDetailsSettings::setCalcalltissues(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("calcalltissues", value);
+ prefs.calcalltissues = value;
+ emit calcalltissuesChanged(value);
+}
+
+void TechnicalDetailsSettings::setCalcndltts(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("calcndltts", value);
+ prefs.calcndltts = value;
+ emit calcndlttsChanged(value);
+}
+
+void TechnicalDetailsSettings::setGflow(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("gflow", value);
+ prefs.gflow = value;
+ set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth);
+ emit gflowChanged(value);
+}
+
+void TechnicalDetailsSettings::setGfhigh(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("gfhigh", value);
+ prefs.gfhigh = value;
+ set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth);
+ emit gfhighChanged(value);
+}
+
+void TechnicalDetailsSettings::setHRgraph(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("hrgraph", value);
+ prefs.hrgraph = value;
+ emit hrgraphChanged(value);
+}
+
+void TechnicalDetailsSettings::setTankBar(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("tankbar", value);
+ prefs.tankbar = value;
+ emit tankBarChanged(value);
+}
+
+void TechnicalDetailsSettings::setPercentageGraph(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("percentagegraph", value);
+ prefs.percentagegraph = value;
+ emit percentageGraphChanged(value);
+}
+
+void TechnicalDetailsSettings::setRulerGraph(bool value)
+{
+ /* TODO: search for the QSettings of the RulerBar */
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("RulerBar", value);
+ prefs.rulergraph = value;
+ emit rulerGraphChanged(value);
+}
+
+void TechnicalDetailsSettings::setShowCCRSetpoint(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("show_ccr_setpoint", value);
+ prefs.show_ccr_setpoint = value;
+ emit showCCRSetpointChanged(value);
+}
+
+void TechnicalDetailsSettings::setShowCCRSensors(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("show_ccr_sensors", value);
+ prefs.show_ccr_sensors = value;
+ emit showCCRSensorsChanged(value);
+}
+
+void TechnicalDetailsSettings::setZoomedPlot(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("zoomed_plot", value);
+ prefs.zoomed_plot = value;
+ emit zoomedPlotChanged(value);
+}
+
+void TechnicalDetailsSettings::setShowSac(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("show_sac", value);
+ prefs.show_sac = value;
+ emit showSacChanged(value);
+}
+
+void TechnicalDetailsSettings::setGfLowAtMaxDepth(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("gf_low_at_maxdepth", value);
+ prefs.gf_low_at_maxdepth = value;
+ set_gf(prefs.gflow, prefs.gfhigh, prefs.gf_low_at_maxdepth);
+ emit gfLowAtMaxDepthChanged(value);
+}
+
+void TechnicalDetailsSettings::setDisplayUnusedTanks(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("display_unused_tanks", value);
+ prefs.display_unused_tanks = value;
+ emit displayUnusedTanksChanged(value);
+}
+
+void TechnicalDetailsSettings::setShowAverageDepth(bool value)
+{
+ QSettings s;
+ s.beginGroup(tecDetails);
+ s.setValue("show_average_depth", value);
+ prefs.show_average_depth = value;
+ emit showAverageDepthChanged(value);
+}
+
+
+
+FacebookSettings::FacebookSettings(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("WebApps")),
+ subgroup(QStringLiteral("Facebook"))
+{
+}
+
+QString FacebookSettings::accessToken() const
+{
+ return QString(prefs.facebook.access_token);
+}
+
+QString FacebookSettings::userId() const
+{
+ return QString(prefs.facebook.user_id);
+}
+
+QString FacebookSettings::albumId() const
+{
+ return QString(prefs.facebook.album_id);
+}
+
+void FacebookSettings::setAccessToken (const QString& value)
+{
+#if SAVE_FB_CREDENTIALS
+ QSettings s;
+ s.beginGroup(group);
+ s.beginGroup(subgroup);
+ s.setValue("ConnectToken", value);
+#endif
+ prefs.facebook.access_token = copy_string(qPrintable(value));
+ emit accessTokenChanged(value);
+}
+
+void FacebookSettings::setUserId(const QString& value)
+{
+#if SAVE_FB_CREDENTIALS
+ QSettings s;
+ s.beginGroup(group);
+ s.beginGroup(subgroup);
+ s.setValue("UserId", value);
+#endif
+ prefs.facebook.user_id = copy_string(qPrintable(value));
+ emit userIdChanged(value);
+}
+
+void FacebookSettings::setAlbumId(const QString& value)
+{
+#if SAVE_FB_CREDENTIALS
+ QSettings s;
+ s.beginGroup(group);
+ s.beginGroup(subgroup);
+ s.setValue("AlbumId", value);
+#endif
+ prefs.facebook.album_id = copy_string(qPrintable(value));
+ emit albumIdChanged(value);
+}
+
+
+GeocodingPreferences::GeocodingPreferences(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("geocoding"))
+{
+
+}
+
+bool GeocodingPreferences::enableGeocoding() const
+{
+ return prefs.geocoding.enable_geocoding;
+}
+
+bool GeocodingPreferences::parseDiveWithoutGps() const
+{
+ return prefs.geocoding.parse_dive_without_gps;
+}
+
+bool GeocodingPreferences::tagExistingDives() const
+{
+ return prefs.geocoding.tag_existing_dives;
+}
+
+taxonomy_category GeocodingPreferences::firstTaxonomyCategory() const
+{
+ return prefs.geocoding.category[0];
+}
+
+taxonomy_category GeocodingPreferences::secondTaxonomyCategory() const
+{
+ return prefs.geocoding.category[1];
+}
+
+taxonomy_category GeocodingPreferences::thirdTaxonomyCategory() const
+{
+ return prefs.geocoding.category[2];
+}
+
+void GeocodingPreferences::setEnableGeocoding(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("enable_geocoding", value);
+ prefs.geocoding.enable_geocoding = value;
+ emit enableGeocodingChanged(value);
+}
+
+void GeocodingPreferences::setParseDiveWithoutGps(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("parse_dives_without_gps", value);
+ prefs.geocoding.parse_dive_without_gps = value;
+ emit parseDiveWithoutGpsChanged(value);
+}
+
+void GeocodingPreferences::setTagExistingDives(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("tag_existing_dives", value);
+ prefs.geocoding.tag_existing_dives = value;
+ emit tagExistingDivesChanged(value);
+}
+
+void GeocodingPreferences::setFirstTaxonomyCategory(taxonomy_category value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("cat0", value);
+ prefs.show_average_depth = value;
+ emit firstTaxonomyCategoryChanged(value);
+}
+
+void GeocodingPreferences::setSecondTaxonomyCategory(taxonomy_category value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("cat1", value);
+ prefs.show_average_depth = value;
+ emit secondTaxonomyCategoryChanged(value);
+}
+
+void GeocodingPreferences::setThirdTaxonomyCategory(taxonomy_category value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("cat2", value);
+ prefs.show_average_depth = value;
+ emit thirdTaxonomyCategoryChanged(value);
+}
+
+ProxySettings::ProxySettings(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("Network"))
+{
+}
+
+int ProxySettings::type() const
+{
+ return prefs.proxy_type;
+}
+
+QString ProxySettings::host() const
+{
+ return prefs.proxy_host;
+}
+
+int ProxySettings::port() const
+{
+ return prefs.proxy_port;
+}
+
+short ProxySettings::auth() const
+{
+ return prefs.proxy_auth;
+}
+
+QString ProxySettings::user() const
+{
+ return prefs.proxy_user;
+}
+
+QString ProxySettings::pass() const
+{
+ return prefs.proxy_pass;
+}
+
+void ProxySettings::setType(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("proxy_type", value);
+ prefs.proxy_type = value;
+ emit typeChanged(value);
+}
+
+void ProxySettings::setHost(const QString& value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("proxy_host", value);
+ free(prefs.proxy_host);
+ prefs.proxy_host = copy_string(qPrintable(value));;
+ emit hostChanged(value);
+}
+
+void ProxySettings::setPort(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("proxy_port", value);
+ prefs.proxy_port = value;
+ emit portChanged(value);
+}
+
+void ProxySettings::setAuth(short value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("proxy_auth", value);
+ prefs.proxy_auth = value;
+ emit authChanged(value);
+}
+
+void ProxySettings::setUser(const QString& value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("proxy_user", value);
+ free(prefs.proxy_user);
+ prefs.proxy_user = copy_string(qPrintable(value));
+ emit userChanged(value);
+}
+
+void ProxySettings::setPass(const QString& value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("proxy_pass", value);
+ free(prefs.proxy_pass);
+ prefs.proxy_pass = copy_string(qPrintable(value));
+ emit passChanged(value);
+}
+
+CloudStorageSettings::CloudStorageSettings(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("CloudStorage"))
+{
+
+}
+
+bool CloudStorageSettings::gitLocalOnly() const
+{
+ return prefs.git_local_only;
+}
+
+QString CloudStorageSettings::password() const
+{
+ return QString(prefs.cloud_storage_password);
+}
+
+QString CloudStorageSettings::newPassword() const
+{
+ return QString(prefs.cloud_storage_newpassword);
+}
+
+QString CloudStorageSettings::email() const
+{
+ return QString(prefs.cloud_storage_email);
+}
+
+QString CloudStorageSettings::emailEncoded() const
+{
+ return QString(prefs.cloud_storage_email_encoded);
+}
+
+bool CloudStorageSettings::savePasswordLocal() const
+{
+ return prefs.save_password_local;
+}
+
+short CloudStorageSettings::verificationStatus() const
+{
+ return prefs.cloud_verification_status;
+}
+
+bool CloudStorageSettings::backgroundSync() const
+{
+ return prefs.cloud_background_sync;
+}
+
+QString CloudStorageSettings::userId() const
+{
+ return QString(prefs.userid);
+}
+
+QString CloudStorageSettings::baseUrl() const
+{
+ return QString(prefs.cloud_base_url);
+}
+
+QString CloudStorageSettings::gitUrl() const
+{
+ return QString(prefs.cloud_git_url);
+}
+
+void CloudStorageSettings::setPassword(const QString& value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("password", value);
+ free(prefs.proxy_pass);
+ prefs.proxy_pass = copy_string(qPrintable(value));
+ emit passwordChanged(value);
+}
+
+void CloudStorageSettings::setNewPassword(const QString& value)
+{
+ /*TODO: This looks like wrong, but 'new password' is not saved on disk, why it's on prefs? */
+ free(prefs.cloud_storage_newpassword);
+ prefs.cloud_storage_newpassword = copy_string(qPrintable(value));
+ emit newPasswordChanged(value);
+}
+
+void CloudStorageSettings::setEmail(const QString& value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("email", value);
+ free(prefs.cloud_storage_email);
+ prefs.cloud_storage_email = copy_string(qPrintable(value));
+ emit emailChanged(value);
+}
+
+void CloudStorageSettings::setUserId(const QString& value)
+{
+ //WARNING: UserId is stored outside of any group, but it belongs to Cloud Storage.
+ QSettings s;
+ s.setValue("subsurface_webservice_uid", value);
+ free(prefs.userid);
+ prefs.userid = copy_string(qPrintable(value));
+ emit userIdChanged(value);
+}
+
+void CloudStorageSettings::setEmailEncoded(const QString& value)
+{
+ /*TODO: This looks like wrong, but 'email encoded' is not saved on disk, why it's on prefs? */
+ free(prefs.cloud_storage_email_encoded);
+ prefs.cloud_storage_email_encoded = copy_string(qPrintable(value));
+ emit emailEncodedChanged(value);
+}
+
+void CloudStorageSettings::setSavePasswordLocal(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("save_password_local", value);
+ prefs.save_password_local = value;
+ emit savePasswordLocalChanged(value);
+}
+
+void CloudStorageSettings::setVerificationStatus(short value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("cloud_verification_status", value);
+ prefs.cloud_verification_status = value;
+ emit verificationStatusChanged(value);
+}
+
+void CloudStorageSettings::setBackgroundSync(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("cloud_background_sync", value);
+ prefs.cloud_background_sync = value;
+ emit backgroundSyncChanged(value);
+}
+
+void CloudStorageSettings::setBaseUrl(const QString& value)
+{
+ free((void*)prefs.cloud_base_url);
+ free((void*)prefs.cloud_git_url);
+ prefs.cloud_base_url = copy_string(qPrintable(value));
+ prefs.cloud_git_url = copy_string(qPrintable(QString(prefs.cloud_base_url) + "/git"));
+}
+
+void CloudStorageSettings::setGitUrl(const QString& value)
+{
+ Q_UNUSED(value); /* no op */
+}
+
+void CloudStorageSettings::setGitLocalOnly(bool value)
+{
+ prefs.git_local_only = value;
+}
+
+DivePlannerSettings::DivePlannerSettings(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("Planner"))
+{
+}
+
+bool DivePlannerSettings::lastStop() const
+{
+ return prefs.last_stop;
+}
+
+bool DivePlannerSettings::verbatimPlan() const
+{
+ return prefs.verbatim_plan;
+}
+
+bool DivePlannerSettings::displayRuntime() const
+{
+ return prefs.display_runtime;
+}
+
+bool DivePlannerSettings::displayDuration() const
+{
+ return prefs.display_duration;
+}
+
+bool DivePlannerSettings::displayTransitions() const
+{
+ return prefs.display_transitions;
+}
+
+bool DivePlannerSettings::doo2breaks() const
+{
+ return prefs.doo2breaks;
+}
+
+bool DivePlannerSettings::dropStoneMode() const
+{
+ return prefs.drop_stone_mode;
+}
+
+bool DivePlannerSettings::safetyStop() const
+{
+ return prefs.safetystop;
+}
+
+bool DivePlannerSettings::switchAtRequiredStop() const
+{
+ return prefs.switch_at_req_stop;
+}
+
+int DivePlannerSettings::ascrate75() const
+{
+ return prefs.ascrate75;
+}
+
+int DivePlannerSettings::ascrate50() const
+{
+ return prefs.ascrate50;
+}
+
+int DivePlannerSettings::ascratestops() const
+{
+ return prefs.ascratestops;
+}
+
+int DivePlannerSettings::ascratelast6m() const
+{
+ return prefs.ascratelast6m;
+}
+
+int DivePlannerSettings::descrate() const
+{
+ return prefs.descrate;
+}
+
+int DivePlannerSettings::bottompo2() const
+{
+ return prefs.bottompo2;
+}
+
+int DivePlannerSettings::decopo2() const
+{
+ return prefs.decopo2;
+}
+
+int DivePlannerSettings::reserveGas() const
+{
+ return prefs.reserve_gas;
+}
+
+int DivePlannerSettings::minSwitchDuration() const
+{
+ return prefs.min_switch_duration;
+}
+
+int DivePlannerSettings::bottomSac() const
+{
+ return prefs.bottomsac;
+}
+
+int DivePlannerSettings::decoSac() const
+{
+ return prefs.decosac;
+}
+
+short DivePlannerSettings::conservatismLevel() const
+{
+ return prefs.conservatism_level;
+}
+
+deco_mode DivePlannerSettings::decoMode() const
+{
+ return prefs.deco_mode;
+}
+
+void DivePlannerSettings::setLastStop(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("last_stop", value);
+ prefs.last_stop = value;
+ emit lastStopChanged(value);
+}
+
+void DivePlannerSettings::setVerbatimPlan(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("verbatim_plan", value);
+ prefs.verbatim_plan = value;
+ emit verbatimPlanChanged(value);
+}
+
+void DivePlannerSettings::setDisplayRuntime(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("display_runtime", value);
+ prefs.display_runtime = value;
+ emit displayRuntimeChanged(value);
+}
+
+void DivePlannerSettings::setDisplayDuration(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("display_duration", value);
+ prefs.display_duration = value;
+ emit displayDurationChanged(value);
+}
+
+void DivePlannerSettings::setDisplayTransitions(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("display_transitions", value);
+ prefs.display_transitions = value;
+ emit displayTransitionsChanged(value);
+}
+
+void DivePlannerSettings::setDoo2breaks(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("doo2breaks", value);
+ prefs.doo2breaks = value;
+ emit doo2breaksChanged(value);
+}
+
+void DivePlannerSettings::setDropStoneMode(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("drop_stone_mode", value);
+ prefs.drop_stone_mode = value;
+ emit dropStoneModeChanged(value);
+}
+
+void DivePlannerSettings::setSafetyStop(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("safetystop", value);
+ prefs.safetystop = value;
+ emit safetyStopChanged(value);
+}
+
+void DivePlannerSettings::setSwitchAtRequiredStop(bool value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("switch_at_req_stop", value);
+ prefs.switch_at_req_stop = value;
+ emit switchAtRequiredStopChanged(value);
+}
+
+void DivePlannerSettings::setAscrate75(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("ascrate75", value);
+ prefs.ascrate75 = value;
+ emit ascrate75Changed(value);
+}
+
+void DivePlannerSettings::setAscrate50(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("ascrate50", value);
+ prefs.ascrate50 = value;
+ emit ascrate50Changed(value);
+}
+
+void DivePlannerSettings::setAscratestops(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("ascratestops", value);
+ prefs.ascratestops = value;
+ emit ascratestopsChanged(value);
+}
+
+void DivePlannerSettings::setAscratelast6m(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("ascratelast6m", value);
+ prefs.ascratelast6m = value;
+ emit ascratelast6mChanged(value);
+}
+
+void DivePlannerSettings::setDescrate(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("descrate", value);
+ prefs.descrate = value;
+ emit descrateChanged(value);
+}
+
+void DivePlannerSettings::setBottompo2(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("bottompo2", value);
+ prefs.bottompo2 = value;
+ emit bottompo2Changed(value);
+}
+
+void DivePlannerSettings::setDecopo2(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("decopo2", value);
+ prefs.decopo2 = value;
+ emit decopo2Changed(value);
+}
+
+void DivePlannerSettings::setReserveGas(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("reserve_gas", value);
+ prefs.reserve_gas = value;
+ emit reserveGasChanged(value);
+}
+
+void DivePlannerSettings::setMinSwitchDuration(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("min_switch_duration", value);
+ prefs.min_switch_duration = value;
+ emit minSwitchDurationChanged(value);
+}
+
+void DivePlannerSettings::setBottomSac(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("bottomsac", value);
+ prefs.bottomsac = value;
+ emit bottomSacChanged(value);
+}
+
+void DivePlannerSettings::setSecoSac(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("decosac", value);
+ prefs.decosac = value;
+ emit decoSacChanged(value);
+}
+
+void DivePlannerSettings::setConservatismLevel(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("conservatism", value);
+ prefs.conservatism_level = value;
+ emit conservatismLevelChanged(value);
+}
+
+void DivePlannerSettings::setDecoMode(deco_mode value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("deco_mode", value);
+ prefs.deco_mode = value;
+ emit decoModeChanged(value);
+}
+
+UnitsSettings::UnitsSettings(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("Units"))
+{
+
+}
+
+int UnitsSettings::length() const
+{
+ return prefs.units.length;
+}
+
+int UnitsSettings::pressure() const
+{
+ return prefs.units.pressure;
+}
+
+int UnitsSettings::volume() const
+{
+ return prefs.units.volume;
+}
+
+int UnitsSettings::temperature() const
+{
+ return prefs.units.temperature;
+}
+
+int UnitsSettings::weight() const
+{
+ return prefs.units.weight;
+}
+
+int UnitsSettings::verticalSpeedTime() const
+{
+ return prefs.units.vertical_speed_time;
+}
+
+QString UnitsSettings::unitSystem() const
+{
+ return QString(); /*FIXME: there's no char * units on the prefs. */
+}
+
+bool UnitsSettings::coordinatesTraditional() const
+{
+ return prefs.coordinates_traditional;
+}
+
+void UnitsSettings::setLength(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("length", value);
+ prefs.units.length = (units::LENGHT) value;
+ emit lengthChanged(value);
+}
+
+void UnitsSettings::setPressure(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("pressure", value);
+ prefs.units.pressure = (units::PRESSURE) value;
+ emit pressureChanged(value);
+}
+
+void UnitsSettings::setVolume(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("volume", value);
+ prefs.units.volume = (units::VOLUME) value;
+ emit volumeChanged(value);
+}
+
+void UnitsSettings::setTemperature(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("temperature", value);
+ prefs.units.temperature = (units::TEMPERATURE) value;
+ emit temperatureChanged(value);
+}
+
+void UnitsSettings::setWeight(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("weight", value);
+ prefs.units.weight = (units::WEIGHT) value;
+ emit weightChanged(value);
+}
+
+void UnitsSettings::setVerticalSpeedTime(int value)
+{
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("vertical_speed_time", value);
+ prefs.units.vertical_speed_time = (units::TIME) value;
+ emit verticalSpeedTimeChanged(value);
+}
+
+void UnitsSettings::setCoordinatesTraditional(bool value)
+{
+ QSettings s;
+ s.setValue("coordinates", value);
+ prefs.coordinates_traditional = value;
+ emit coordinatesTraditionalChanged(value);
+}
+
+void UnitsSettings::setUnitSystem(const QString& value)
+{
+ QSettings s;
+ s.setValue("unit_system", value);
+
+ if (value == QStringLiteral("metric")) {
+ prefs.unit_system = METRIC;
+ prefs.units = SI_units;
+ } else if (value == QStringLiteral("imperial")) {
+ prefs.unit_system = IMPERIAL;
+ prefs.units = IMPERIAL_units;
+ } else {
+ prefs.unit_system = PERSONALIZE;
+ }
+
+ emit unitSystemChanged(value);
+ // TODO: emit the other values here?
+}
+
+GeneralSettingsObjectWrapper::GeneralSettingsObjectWrapper(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("GeneralSettings"))
+{
+}
+
+QString GeneralSettingsObjectWrapper::defaultFilename() const
+{
+ return prefs.default_filename;
+}
+
+QString GeneralSettingsObjectWrapper::defaultCylinder() const
+{
+ return prefs.default_cylinder;
+}
+
+short GeneralSettingsObjectWrapper::defaultFileBehavior() const
+{
+ return prefs.default_file_behavior;
+}
+
+bool GeneralSettingsObjectWrapper::useDefaultFile() const
+{
+ return prefs.use_default_file;
+}
+
+int GeneralSettingsObjectWrapper::defaultSetPoint() const
+{
+ return prefs.defaultsetpoint;
+}
+
+int GeneralSettingsObjectWrapper::o2Consumption() const
+{
+ return prefs.o2consumption;
+}
+
+int GeneralSettingsObjectWrapper::pscrRatio() const
+{
+ return prefs.pscr_ratio;
+}
+
+void GeneralSettingsObjectWrapper::setDefaultFilename(const QString& value)
+{
+ QSettings s;
+ s.setValue("default_filename", value);
+ prefs.default_filename = copy_string(qPrintable(value));
+ emit defaultFilenameChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setDefaultCylinder(const QString& value)
+{
+ QSettings s;
+ s.setValue("default_cylinder", value);
+ prefs.default_cylinder = copy_string(qPrintable(value));
+ emit defaultCylinderChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setDefaultFileBehavior(short value)
+{
+ QSettings s;
+ s.setValue("default_file_behavior", value);
+ prefs.default_file_behavior = value;
+ 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;
+ }
+ emit defaultFileBehaviorChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setUseDefaultFile(bool value)
+{
+ QSettings s;
+ s.setValue("use_default_file", value);
+ prefs.use_default_file = value;
+ emit useDefaultFileChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setDefaultSetPoint(int value)
+{
+ QSettings s;
+ s.setValue("defaultsetpoint", value);
+ prefs.defaultsetpoint = value;
+ emit defaultSetPointChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setO2Consumption(int value)
+{
+ QSettings s;
+ s.setValue("o2consumption", value);
+ prefs.o2consumption = value;
+ emit o2ConsumptionChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setPscrRatio(int value)
+{
+ QSettings s;
+ s.setValue("pscr_ratio", value);
+ prefs.pscr_ratio = value;
+ emit pscrRatioChanged(value);
+}
+
+DisplaySettingsObjectWrapper::DisplaySettingsObjectWrapper(QObject *parent) :
+ QObject(parent),
+ group(QStringLiteral("Display"))
+{
+}
+
+QString DisplaySettingsObjectWrapper::divelistFont() const
+{
+ return prefs.divelist_font;
+}
+
+double DisplaySettingsObjectWrapper::fontSize() const
+{
+ return prefs.font_size;
+}
+
+short DisplaySettingsObjectWrapper::displayInvalidDives() const
+{
+ return prefs.display_invalid_dives;
+}
+
+void DisplaySettingsObjectWrapper::setDivelistFont(const QString& value)
+{
+ QSettings s;
+ s.setValue("divelist_font", value);
+ QString newValue = value;
+ if (value.contains(","))
+ newValue = value.left(value.indexOf(","));
+
+ if (!subsurface_ignore_font(newValue.toUtf8().constData())) {
+ free((void *)prefs.divelist_font);
+ prefs.divelist_font = strdup(newValue.toUtf8().constData());
+ qApp->setFont(QFont(newValue));
+ }
+ emit divelistFontChanged(newValue);
+}
+
+void DisplaySettingsObjectWrapper::setFontSize(double value)
+{
+ QSettings s;
+ s.setValue("font_size", value);
+ prefs.font_size = value;
+ QFont defaultFont = qApp->font();
+ defaultFont.setPointSizeF(prefs.font_size);
+ qApp->setFont(defaultFont);
+ emit fontSizeChanged(value);
+}
+
+void DisplaySettingsObjectWrapper::setDisplayInvalidDives(short value)
+{
+ QSettings s;
+ s.setValue("displayinvalid", value);
+ prefs.display_invalid_dives = value;
+ emit displayInvalidDivesChanged(value);
+}
+
+LanguageSettingsObjectWrapper::LanguageSettingsObjectWrapper(QObject *parent) :
+ QObject(parent),
+ group("Language")
+{
+}
+
+QString LanguageSettingsObjectWrapper::language() const
+{
+ return prefs.locale.language;
+}
+
+QString LanguageSettingsObjectWrapper::timeFormat() const
+{
+ return prefs.time_format;
+}
+
+QString LanguageSettingsObjectWrapper::dateFormat() const
+{
+ return prefs.date_format;
+}
+
+QString LanguageSettingsObjectWrapper::dateFormatShort() const
+{
+ return prefs.date_format_short;
+}
+
+bool LanguageSettingsObjectWrapper::timeFormatOverride() const
+{
+ return prefs.time_format_override;
+}
+
+bool LanguageSettingsObjectWrapper::dateFormatOverride() const
+{
+ return prefs.date_format_override;
+}
+
+bool LanguageSettingsObjectWrapper::useSystemLanguage() const
+{
+ return prefs.locale.use_system_language;
+}
+
+void LanguageSettingsObjectWrapper::setUseSystemLanguage(bool value)
+{
+ QSettings s;
+ s.setValue("UseSystemLanguage", value);
+ prefs.locale.use_system_language = copy_string(qPrintable(value));
+ emit useSystemLanguageChanged(value);
+}
+
+void LanguageSettingsObjectWrapper::setLanguage(const QString& value)
+{
+ QSettings s;
+ s.setValue("UiLanguage", value);
+ prefs.locale.language = copy_string(qPrintable(value));
+ emit languageChanged(value);
+}
+
+void LanguageSettingsObjectWrapper::setTimeFormat(const QString& value)
+{
+ QSettings s;
+ s.setValue("time_format", value);
+ prefs.time_format = copy_string(qPrintable(value));;
+ emit timeFormatChanged(value);
+}
+
+void LanguageSettingsObjectWrapper::setDateFormat(const QString& value)
+{
+ QSettings s;
+ s.setValue("date_format", value);
+ prefs.date_format = copy_string(qPrintable(value));;
+ emit dateFormatChanged(value);
+}
+
+void LanguageSettingsObjectWrapper::setDateFormatShort(const QString& value)
+{
+ QSettings s;
+ s.setValue("date_format_short", value);
+ prefs.date_format_short = copy_string(qPrintable(value));;
+ emit dateFormatShortChanged(value);
+}
+
+void LanguageSettingsObjectWrapper::setTimeFormatOverride(bool value)
+{
+ QSettings s;
+ s.setValue("time_format_override", value);
+ prefs.time_format_override = value;
+ emit timeFormatOverrideChanged(value);
+}
+
+void LanguageSettingsObjectWrapper::setDateFormatOverride(bool value)
+{
+ QSettings s;
+ s.setValue("date_format_override", value);
+ prefs.date_format_override = value;
+ emit dateFormatOverrideChanged(value);
+}
+
+AnimationsSettingsObjectWrapper::AnimationsSettingsObjectWrapper(QObject* parent):
+ QObject(parent),
+ group("Animations")
+
+{
+}
+
+int AnimationsSettingsObjectWrapper::animationSpeed() const
+{
+ return prefs.animation_speed;
+}
+
+void AnimationsSettingsObjectWrapper::setAnimationSpeed(int value)
+{
+ QSettings s;
+ s.setValue("animation_speed", value);
+ prefs.animation_speed = value;
+ emit animationSpeedChanged(value);
+}
+
+LocationServiceSettingsObjectWrapper::LocationServiceSettingsObjectWrapper(QObject* parent):
+ QObject(parent),
+ group("locationService")
+{
+}
+
+int LocationServiceSettingsObjectWrapper::distanceThreshold() const
+{
+ return prefs.distance_threshold;
+}
+
+int LocationServiceSettingsObjectWrapper::timeThreshold() const
+{
+ return prefs.time_threshold;
+}
+
+void LocationServiceSettingsObjectWrapper::setDistanceThreshold(int value)
+{
+ QSettings s;
+ s.setValue("distance_threshold", value);
+ prefs.distance_threshold = value;
+ emit distanceThresholdChanged(value);
+}
+
+void LocationServiceSettingsObjectWrapper::setTimeThreshold(int value)
+{
+ QSettings s;
+ s.setValue("time_threshold", value);
+ prefs.time_threshold = value;
+ emit timeThresholdChanged( value);
+}
+
+SettingsObjectWrapper::SettingsObjectWrapper(QObject* parent):
+QObject(parent),
+ techDetails(new TechnicalDetailsSettings(this)),
+ pp_gas(new PartialPressureGasSettings(this)),
+ facebook(new FacebookSettings(this)),
+ geocoding(new GeocodingPreferences(this)),
+ proxy(new ProxySettings(this)),
+ cloud_storage(new CloudStorageSettings(this)),
+ planner_settings(new DivePlannerSettings(this)),
+ unit_settings(new UnitsSettings(this)),
+ general_settings(new GeneralSettingsObjectWrapper(this)),
+ display_settings(new DisplaySettingsObjectWrapper(this)),
+ language_settings(new LanguageSettingsObjectWrapper(this)),
+ animation_settings(new AnimationsSettingsObjectWrapper(this)),
+ location_settings(new LocationServiceSettingsObjectWrapper(this))
+{
+}
+
+void SettingsObjectWrapper::setSaveUserIdLocal(short int value)
+{
+ Q_UNUSED(value);
+ //TODO: Find where this is stored on the preferences.
+}
+
+short int SettingsObjectWrapper::saveUserIdLocal() const
+{
+ return prefs.save_userid_local;
+}
+
+SettingsObjectWrapper* SettingsObjectWrapper::instance()
+{
+ static SettingsObjectWrapper settings;
+ return &settings;
+}
diff --git a/core/subsurface-qt/SettingsObjectWrapper.h b/core/subsurface-qt/SettingsObjectWrapper.h
new file mode 100644
index 000000000..f115e2d86
--- /dev/null
+++ b/core/subsurface-qt/SettingsObjectWrapper.h
@@ -0,0 +1,642 @@
+#ifndef SETTINGSOBJECTWRAPPER_H
+#define SETTINGSOBJECTWRAPPER_H
+
+#include <QObject>
+
+#include "../pref.h"
+#include "../prefs-macros.h"
+
+/* Wrapper class for the Settings. This will allow
+ * seamlessy integration of the settings with the QML
+ * and QWidget frontends. This class will be huge, since
+ * I need tons of properties, one for each option. */
+
+/* Control the state of the Partial Pressure Graphs preferences */
+class PartialPressureGasSettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(short show_po2 READ showPo2 WRITE setShowPo2 NOTIFY showPo2Changed)
+ Q_PROPERTY(short show_pn2 READ showPn2 WRITE setShowPn2 NOTIFY showPn2Changed)
+ Q_PROPERTY(short show_phe READ showPhe WRITE setShowPhe NOTIFY showPheChanged)
+ Q_PROPERTY(double po2_threshold READ po2Threshold WRITE setPo2Threshold NOTIFY po2ThresholdChanged)
+ Q_PROPERTY(double pn2_threshold READ pn2Threshold WRITE setPn2Threshold NOTIFY pn2ThresholdChanged)
+ Q_PROPERTY(double phe_threshold READ pheThreshold WRITE setPheThreshold NOTIFY pheThresholdChanged)
+
+public:
+ PartialPressureGasSettings(QObject *parent);
+ short showPo2() const;
+ short showPn2() const;
+ short showPhe() const;
+ double po2Threshold() const;
+ double pn2Threshold() const;
+ double pheThreshold() const;
+
+public slots:
+ void setShowPo2(short value);
+ void setShowPn2(short value);
+ void setShowPhe(short value);
+ void setPo2Threshold(double value);
+ void setPn2Threshold(double value);
+ void setPheThreshold(double value);
+
+signals:
+ void showPo2Changed(short value);
+ void showPn2Changed(short value);
+ void showPheChanged(short value);
+ void po2ThresholdChanged(double value);
+ void pn2ThresholdChanged(double value);
+ void pheThresholdChanged(double value);
+private:
+ QString group;
+};
+
+class TechnicalDetailsSettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(double modpO2 READ modp02 WRITE setModp02 NOTIFY modpO2Changed)
+ Q_PROPERTY(bool ead READ ead WRITE setEad NOTIFY eadChanged)
+ Q_PROPERTY(bool mod READ mod WRITE setMod NOTIFY modChanged);
+ Q_PROPERTY(bool dcceiling READ dcceiling WRITE setDCceiling NOTIFY dcceilingChanged)
+ Q_PROPERTY(bool redceiling READ redceiling WRITE setRedceiling NOTIFY redceilingChanged)
+ Q_PROPERTY(bool calcceiling READ calcceiling WRITE setCalcceiling NOTIFY calcceilingChanged)
+ Q_PROPERTY(bool calcceiling3m READ calcceiling3m WRITE setCalcceiling3m NOTIFY calcceiling3mChanged)
+ Q_PROPERTY(bool calcalltissues READ calcalltissues WRITE setCalcalltissues NOTIFY calcalltissuesChanged)
+ Q_PROPERTY(bool calcndltts READ calcndltts WRITE setCalcndltts NOTIFY calcndlttsChanged)
+ Q_PROPERTY(bool gflow READ gflow WRITE setGflow NOTIFY gflowChanged)
+ Q_PROPERTY(bool gfhigh READ gfhigh WRITE setGfhigh NOTIFY gfhighChanged)
+ Q_PROPERTY(bool hrgraph READ hrgraph WRITE setHRgraph NOTIFY hrgraphChanged)
+ Q_PROPERTY(bool tankbar READ tankBar WRITE setTankBar NOTIFY tankBarChanged)
+ Q_PROPERTY(bool percentagegraph READ percentageGraph WRITE setPercentageGraph NOTIFY percentageGraphChanged)
+ Q_PROPERTY(bool rulergraph READ rulerGraph WRITE setRulerGraph NOTIFY rulerGraphChanged)
+ Q_PROPERTY(bool show_ccr_setpoint READ showCCRSetpoint WRITE setShowCCRSetpoint NOTIFY showCCRSetpointChanged)
+ Q_PROPERTY(bool show_ccr_sensors READ showCCRSensors WRITE setShowCCRSensors NOTIFY showCCRSensorsChanged)
+ Q_PROPERTY(bool zoomed_plot READ zoomedPlot WRITE setZoomedPlot NOTIFY zoomedPlotChanged)
+ Q_PROPERTY(bool show_sac READ showSac WRITE setShowSac NOTIFY showSacChanged)
+ Q_PROPERTY(bool gf_low_at_maxdepth READ gfLowAtMaxDepth WRITE setGfLowAtMaxDepth NOTIFY gfLowAtMaxDepthChanged)
+ Q_PROPERTY(bool display_unused_tanks READ displayUnusedTanks WRITE setDisplayUnusedTanks NOTIFY displayUnusedTanksChanged)
+ Q_PROPERTY(bool show_average_depth READ showAverageDepth WRITE setShowAverageDepth NOTIFY showAverageDepthChanged)
+ Q_PROPERTY(bool show_pictures_in_profile READ showPicturesInProfile WRITE setShowPicturesInProfile NOTIFY showPicturesInProfileChanged)
+public:
+ TechnicalDetailsSettings(QObject *parent);
+
+ double modp02() const;
+ bool ead() const;
+ bool mod() const;
+ bool dcceiling() const;
+ bool redceiling() const;
+ bool calcceiling() const;
+ bool calcceiling3m() const;
+ bool calcalltissues() const;
+ bool calcndltts() const;
+ bool gflow() const;
+ bool gfhigh() const;
+ bool hrgraph() const;
+ bool tankBar() const;
+ bool percentageGraph() const;
+ bool rulerGraph() const;
+ bool showCCRSetpoint() const;
+ bool showCCRSensors() const;
+ bool zoomedPlot() const;
+ bool showSac() const;
+ bool gfLowAtMaxDepth() const;
+ bool displayUnusedTanks() const;
+ bool showAverageDepth() const;
+ bool showPicturesInProfile() const;
+
+public slots:
+ void setMod(bool value);
+ void setModp02(double value);
+ void setEad(bool value);
+ void setDCceiling(bool value);
+ void setRedceiling(bool value);
+ void setCalcceiling(bool value);
+ void setCalcceiling3m(bool value);
+ void setCalcalltissues(bool value);
+ void setCalcndltts(bool value);
+ void setGflow(bool value);
+ void setGfhigh(bool value);
+ void setHRgraph(bool value);
+ void setTankBar(bool value);
+ void setPercentageGraph(bool value);
+ void setRulerGraph(bool value);
+ void setShowCCRSetpoint(bool value);
+ void setShowCCRSensors(bool value);
+ void setZoomedPlot(bool value);
+ void setShowSac(bool value);
+ void setGfLowAtMaxDepth(bool value);
+ void setDisplayUnusedTanks(bool value);
+ void setShowAverageDepth(bool value);
+ void setShowPicturesInProfile(bool value);
+
+signals:
+ void modpO2Changed(double value);
+ void eadChanged(bool value);
+ void modChanged(bool value);
+ void dcceilingChanged(bool value);
+ void redceilingChanged(bool value);
+ void calcceilingChanged(bool value);
+ void calcceiling3mChanged(bool value);
+ void calcalltissuesChanged(bool value);
+ void calcndlttsChanged(bool value);
+ void gflowChanged(bool value);
+ void gfhighChanged(bool value);
+ void hrgraphChanged(bool value);
+ void tankBarChanged(bool value);
+ void percentageGraphChanged(bool value);
+ void rulerGraphChanged(bool value);
+ void showCCRSetpointChanged(bool value);
+ void showCCRSensorsChanged(bool value);
+ void zoomedPlotChanged(bool value);
+ void showSacChanged(bool value);
+ void gfLowAtMaxDepthChanged(bool value);
+ void displayUnusedTanksChanged(bool value);
+ void showAverageDepthChanged(bool value);
+ void showPicturesInProfileChanged(bool value);
+};
+
+/* Control the state of the Facebook preferences */
+class FacebookSettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged)
+ Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged)
+ Q_PROPERTY(QString albumId READ albumId WRITE setAlbumId NOTIFY albumIdChanged)
+
+public:
+ FacebookSettings(QObject *parent);
+ QString accessToken() const;
+ QString userId() const;
+ QString albumId() const;
+
+public slots:
+ void setAccessToken (const QString& value);
+ void setUserId(const QString& value);
+ void setAlbumId(const QString& value);
+
+signals:
+ void accessTokenChanged(const QString& value);
+ void userIdChanged(const QString& value);
+ void albumIdChanged(const QString& value);
+private:
+ QString group;
+ QString subgroup;
+};
+
+/* Control the state of the Geocoding preferences */
+class GeocodingPreferences : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(bool enable_geocoding READ enableGeocoding WRITE setEnableGeocoding NOTIFY enableGeocodingChanged)
+ Q_PROPERTY(bool parse_dive_without_gps READ parseDiveWithoutGps WRITE setParseDiveWithoutGps NOTIFY parseDiveWithoutGpsChanged)
+ Q_PROPERTY(bool tag_existing_dives READ tagExistingDives WRITE setTagExistingDives NOTIFY tagExistingDivesChanged)
+ Q_PROPERTY(taxonomy_category first_category READ firstTaxonomyCategory WRITE setFirstTaxonomyCategory NOTIFY firstTaxonomyCategoryChanged)
+ Q_PROPERTY(taxonomy_category second_category READ secondTaxonomyCategory WRITE setSecondTaxonomyCategory NOTIFY secondTaxonomyCategoryChanged)
+ Q_PROPERTY(taxonomy_category third_category READ thirdTaxonomyCategory WRITE setThirdTaxonomyCategory NOTIFY thirdTaxonomyCategoryChanged)
+public:
+ GeocodingPreferences(QObject *parent);
+ bool enableGeocoding() const;
+ bool parseDiveWithoutGps() const;
+ bool tagExistingDives() const;
+ taxonomy_category firstTaxonomyCategory() const;
+ taxonomy_category secondTaxonomyCategory() const;
+ taxonomy_category thirdTaxonomyCategory() const;
+
+public slots:
+ void setEnableGeocoding(bool value);
+ void setParseDiveWithoutGps(bool value);
+ void setTagExistingDives(bool value);
+ void setFirstTaxonomyCategory(taxonomy_category value);
+ void setSecondTaxonomyCategory(taxonomy_category value);
+ void setThirdTaxonomyCategory(taxonomy_category value);
+
+signals:
+ void enableGeocodingChanged(bool value);
+ void parseDiveWithoutGpsChanged(bool value);
+ void tagExistingDivesChanged(bool value);
+ void firstTaxonomyCategoryChanged(taxonomy_category value);
+ void secondTaxonomyCategoryChanged(taxonomy_category value);
+ void thirdTaxonomyCategoryChanged(taxonomy_category value);
+private:
+ QString group;
+};
+
+class ProxySettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged)
+ Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged)
+ Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged)
+ Q_PROPERTY(short auth READ auth WRITE setAuth NOTIFY authChanged)
+ Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged)
+ Q_PROPERTY(QString pass READ pass WRITE setPass NOTIFY passChanged)
+
+public:
+ ProxySettings(QObject *parent);
+ int type() const;
+ QString host() const;
+ int port() const;
+ short auth() const;
+ QString user() const;
+ QString pass() const;
+
+public slots:
+ void setType(int value);
+ void setHost(const QString& value);
+ void setPort(int value);
+ void setAuth(short value);
+ void setUser(const QString& value);
+ void setPass(const QString& value);
+
+signals:
+ void typeChanged(int value);
+ void hostChanged(const QString& value);
+ void portChanged(int value);
+ void authChanged(short value);
+ void userChanged(const QString& value);
+ void passChanged(const QString& value);
+private:
+ QString group;
+};
+
+class CloudStorageSettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
+ Q_PROPERTY(QString newpassword READ newPassword WRITE setNewPassword NOTIFY newPasswordChanged)
+ Q_PROPERTY(QString email READ email WRITE setEmail NOTIFY emailChanged)
+ Q_PROPERTY(QString email_encoded READ emailEncoded WRITE setEmailEncoded NOTIFY emailEncodedChanged)
+ Q_PROPERTY(QString userid READ userId WRITE setUserId NOTIFY userIdChanged)
+ Q_PROPERTY(QString base_url READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
+ Q_PROPERTY(QString git_url READ gitUrl WRITE setGitUrl NOTIFY gitUrlChanged)
+ Q_PROPERTY(bool git_local_only READ gitLocalOnly WRITE setGitLocalOnly NOTIFY gitLocalOnlyChanged)
+ Q_PROPERTY(bool save_password_local READ savePasswordLocal WRITE setSavePasswordLocal NOTIFY savePasswordLocalChanged)
+ Q_PROPERTY(short verification_status READ verificationStatus WRITE setVerificationStatus NOTIFY verificationStatusChanged)
+ Q_PROPERTY(bool background_sync READ backgroundSync WRITE setBackgroundSync NOTIFY backgroundSyncChanged)
+public:
+ CloudStorageSettings(QObject *parent);
+ QString password() const;
+ QString newPassword() const;
+ QString email() const;
+ QString emailEncoded() const;
+ QString userId() const;
+ QString baseUrl() const;
+ QString gitUrl() const;
+ bool savePasswordLocal() const;
+ short verificationStatus() const;
+ bool backgroundSync() const;
+ bool gitLocalOnly() const;
+
+public slots:
+ void setPassword(const QString& value);
+ void setNewPassword(const QString& value);
+ void setEmail(const QString& value);
+ void setEmailEncoded(const QString& value);
+ void setUserId(const QString& value);
+ void setBaseUrl(const QString& value);
+ void setGitUrl(const QString& value);
+ void setSavePasswordLocal(bool value);
+ void setVerificationStatus(short value);
+ void setBackgroundSync(bool value);
+ void setGitLocalOnly(bool value);
+
+signals:
+ void passwordChanged(const QString& value);
+ void newPasswordChanged(const QString& value);
+ void emailChanged(const QString& value);
+ void emailEncodedChanged(const QString& value);
+ void userIdChanged(const QString& value);
+ void baseUrlChanged(const QString& value);
+ void gitUrlChanged(const QString& value);
+ void savePasswordLocalChanged(bool value);
+ void verificationStatusChanged(short value);
+ void backgroundSyncChanged(bool value);
+ void gitLocalOnlyChanged(bool value);
+private:
+ QString group;
+};
+
+class DivePlannerSettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(bool last_stop READ lastStop WRITE setLastStop NOTIFY lastStopChanged)
+ Q_PROPERTY(bool verbatim_plan READ verbatimPlan WRITE setVerbatimPlan NOTIFY verbatimPlanChanged)
+ Q_PROPERTY(bool display_runtime READ displayRuntime WRITE setDisplayRuntime NOTIFY displayRuntimeChanged)
+ Q_PROPERTY(bool display_duration READ displayDuration WRITE setDisplayDuration NOTIFY displayDurationChanged)
+ Q_PROPERTY(bool display_transitions READ displayTransitions WRITE setDisplayTransitions NOTIFY displayTransitionsChanged)
+ Q_PROPERTY(bool doo2breaks READ doo2breaks WRITE setDoo2breaks NOTIFY doo2breaksChanged)
+ Q_PROPERTY(bool drop_stone_mode READ dropStoneMode WRITE setDropStoneMode NOTIFY dropStoneModeChanged)
+ Q_PROPERTY(bool safetystop READ safetyStop WRITE setSafetyStop NOTIFY safetyStopChanged)
+ Q_PROPERTY(bool switch_at_req_stop READ switchAtRequiredStop WRITE setSwitchAtRequiredStop NOTIFY switchAtRequiredStopChanged)
+ Q_PROPERTY(int ascrate75 READ ascrate75 WRITE setAscrate75 NOTIFY ascrate75Changed)
+ Q_PROPERTY(int ascrate50 READ ascrate50 WRITE setAscrate50 NOTIFY ascrate50Changed)
+ Q_PROPERTY(int ascratestops READ ascratestops WRITE setAscratestops NOTIFY ascratestopsChanged)
+ Q_PROPERTY(int ascratelast6m READ ascratelast6m WRITE setAscratelast6m NOTIFY ascratelast6mChanged)
+ Q_PROPERTY(int descrate READ descrate WRITE setDescrate NOTIFY descrateChanged)
+ Q_PROPERTY(int bottompo2 READ bottompo2 WRITE setBottompo2 NOTIFY bottompo2Changed)
+ Q_PROPERTY(int decopo2 READ decopo2 WRITE setDecopo2 NOTIFY decopo2Changed)
+ Q_PROPERTY(int reserve_gas READ reserveGas WRITE setReserveGas NOTIFY reserveGasChanged)
+ Q_PROPERTY(int min_switch_duration READ minSwitchDuration WRITE setMinSwitchDuration NOTIFY minSwitchDurationChanged)
+ Q_PROPERTY(int bottomsac READ bottomSac WRITE setBottomSac NOTIFY bottomSacChanged)
+ Q_PROPERTY(int decosac READ decoSac WRITE setSecoSac NOTIFY decoSacChanged)
+ Q_PROPERTY(short conservatism_level READ conservatismLevel WRITE setConservatismLevel NOTIFY conservatismLevelChanged)
+ Q_PROPERTY(deco_mode decoMode READ decoMode WRITE setDecoMode NOTIFY decoModeChanged)
+
+public:
+ DivePlannerSettings(QObject *parent = 0);
+ bool lastStop() const;
+ bool verbatimPlan() const;
+ bool displayRuntime() const;
+ bool displayDuration() const;
+ bool displayTransitions() const;
+ bool doo2breaks() const;
+ bool dropStoneMode() const;
+ bool safetyStop() const;
+ bool switchAtRequiredStop() const;
+ int ascrate75() const;
+ int ascrate50() const;
+ int ascratestops() const;
+ int ascratelast6m() const;
+ int descrate() const;
+ int bottompo2() const;
+ int decopo2() const;
+ int reserveGas() const;
+ int minSwitchDuration() const;
+ int bottomSac() const;
+ int decoSac() const;
+ short conservatismLevel() const;
+ deco_mode decoMode() const;
+
+public slots:
+ void setLastStop(bool value);
+ void setVerbatimPlan(bool value);
+ void setDisplayRuntime(bool value);
+ void setDisplayDuration(bool value);
+ void setDisplayTransitions(bool value);
+ void setDoo2breaks(bool value);
+ void setDropStoneMode(bool value);
+ void setSafetyStop(bool value);
+ void setSwitchAtRequiredStop(bool value);
+ void setAscrate75(int value);
+ void setAscrate50(int value);
+ void setAscratestops(int value);
+ void setAscratelast6m(int value);
+ void setDescrate(int value);
+ void setBottompo2(int value);
+ void setDecopo2(int value);
+ void setReserveGas(int value);
+ void setMinSwitchDuration(int value);
+ void setBottomSac(int value);
+ void setSecoSac(int value);
+ void setConservatismLevel(int value);
+ void setDecoMode(deco_mode value);
+
+signals:
+ void lastStopChanged(bool value);
+ void verbatimPlanChanged(bool value);
+ void displayRuntimeChanged(bool value);
+ void displayDurationChanged(bool value);
+ void displayTransitionsChanged(bool value);
+ void doo2breaksChanged(bool value);
+ void dropStoneModeChanged(bool value);
+ void safetyStopChanged(bool value);
+ void switchAtRequiredStopChanged(bool value);
+ void ascrate75Changed(int value);
+ void ascrate50Changed(int value);
+ void ascratestopsChanged(int value);
+ void ascratelast6mChanged(int value);
+ void descrateChanged(int value);
+ void bottompo2Changed(int value);
+ void decopo2Changed(int value);
+ void reserveGasChanged(int value);
+ void minSwitchDurationChanged(int value);
+ void bottomSacChanged(int value);
+ void decoSacChanged(int value);
+ void conservatismLevelChanged(int value);
+ void decoModeChanged(deco_mode value);
+
+private:
+ QString group;
+};
+
+class UnitsSettings : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(int length READ length WRITE setLength NOTIFY lengthChanged)
+ Q_PROPERTY(int pressure READ pressure WRITE setPressure NOTIFY pressureChanged)
+ Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged)
+ Q_PROPERTY(int temperature READ temperature WRITE setTemperature NOTIFY temperatureChanged)
+ Q_PROPERTY(int weight READ weight WRITE setWeight NOTIFY weightChanged)
+ Q_PROPERTY(QString unit_system READ unitSystem WRITE setUnitSystem NOTIFY unitSystemChanged)
+ Q_PROPERTY(bool coordinates_traditional READ coordinatesTraditional WRITE setCoordinatesTraditional NOTIFY coordinatesTraditionalChanged)
+ Q_PROPERTY(int vertical_speed_time READ verticalSpeedTime WRITE setVerticalSpeedTime NOTIFY verticalSpeedTimeChanged)
+
+public:
+ UnitsSettings(QObject *parent = 0);
+ int length() const;
+ int pressure() const;
+ int volume() const;
+ int temperature() const;
+ int weight() const;
+ int verticalSpeedTime() const;
+ QString unitSystem() const;
+ bool coordinatesTraditional() const;
+
+public slots:
+ void setLength(int value);
+ void setPressure(int value);
+ void setVolume(int value);
+ void setTemperature(int value);
+ void setWeight(int value);
+ void setVerticalSpeedTime(int value);
+ void setUnitSystem(const QString& value);
+ void setCoordinatesTraditional(bool value);
+
+signals:
+ void lengthChanged(int value);
+ void pressureChanged(int value);
+ void volumeChanged(int value);
+ void temperatureChanged(int value);
+ void weightChanged(int value);
+ void verticalSpeedTimeChanged(int value);
+ void unitSystemChanged(const QString& value);
+ void coordinatesTraditionalChanged(bool value);
+private:
+ QString group;
+};
+
+class GeneralSettingsObjectWrapper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QString default_filename READ defaultFilename WRITE setDefaultFilename NOTIFY defaultFilenameChanged)
+ Q_PROPERTY(QString default_cylinder READ defaultCylinder WRITE setDefaultCylinder NOTIFY defaultCylinderChanged)
+ Q_PROPERTY(short default_file_behavior READ defaultFileBehavior WRITE setDefaultFileBehavior NOTIFY defaultFileBehaviorChanged)
+ Q_PROPERTY(bool use_default_file READ useDefaultFile WRITE setUseDefaultFile NOTIFY useDefaultFileChanged)
+ Q_PROPERTY(int defaultsetpoint READ defaultSetPoint WRITE setDefaultSetPoint NOTIFY defaultSetPointChanged)
+ Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
+ Q_PROPERTY(int pscr_ratio READ pscrRatio WRITE setPscrRatio NOTIFY pscrRatioChanged)
+
+public:
+ GeneralSettingsObjectWrapper(QObject *parent);
+ QString defaultFilename() const;
+ QString defaultCylinder() const;
+ short defaultFileBehavior() const;
+ bool useDefaultFile() const;
+ int defaultSetPoint() const;
+ int o2Consumption() const;
+ int pscrRatio() const;
+
+public slots:
+ void setDefaultFilename (const QString& value);
+ void setDefaultCylinder (const QString& value);
+ void setDefaultFileBehavior (short value);
+ void setUseDefaultFile (bool value);
+ void setDefaultSetPoint (int value);
+ void setO2Consumption (int value);
+ void setPscrRatio (int value);
+
+signals:
+ void defaultFilenameChanged(const QString& value);
+ void defaultCylinderChanged(const QString& value);
+ void defaultFileBehaviorChanged(short value);
+ void useDefaultFileChanged(bool value);
+ void defaultSetPointChanged(int value);
+ void o2ConsumptionChanged(int value);
+ void pscrRatioChanged(int value);
+private:
+ QString group;
+};
+
+class DisplaySettingsObjectWrapper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QString divelist_font READ divelistFont WRITE setDivelistFont NOTIFY divelistFontChanged)
+ Q_PROPERTY(double font_size READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
+ Q_PROPERTY(short display_invalid_dives READ displayInvalidDives WRITE setDisplayInvalidDives NOTIFY displayInvalidDivesChanged)
+public:
+ DisplaySettingsObjectWrapper(QObject *parent);
+ QString divelistFont() const;
+ double fontSize() const;
+ short displayInvalidDives() const;
+public slots:
+ void setDivelistFont(const QString& value);
+ void setFontSize(double value);
+ void setDisplayInvalidDives(short value);
+signals:
+ void divelistFontChanged(const QString& value);
+ void fontSizeChanged(double value);
+ void displayInvalidDivesChanged(short value);
+private:
+ QString group;
+};
+
+class LanguageSettingsObjectWrapper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
+ Q_PROPERTY(QString time_format READ timeFormat WRITE setTimeFormat NOTIFY timeFormatChanged)
+ Q_PROPERTY(QString date_format READ dateFormat WRITE setDateFormat NOTIFY dateFormatChanged)
+ Q_PROPERTY(QString date_format_short READ dateFormatShort WRITE setDateFormatShort NOTIFY dateFormatShortChanged)
+ Q_PROPERTY(bool time_format_override READ timeFormatOverride WRITE setTimeFormatOverride NOTIFY timeFormatOverrideChanged)
+ Q_PROPERTY(bool date_format_override READ dateFormatOverride WRITE setDateFormatOverride NOTIFY dateFormatOverrideChanged)
+ Q_PROPERTY(bool use_system_language READ useSystemLanguage WRITE setUseSystemLanguage NOTIFY useSystemLanguageChanged)
+
+public:
+ LanguageSettingsObjectWrapper(QObject *parent);
+ QString language() const;
+ QString timeFormat() const;
+ QString dateFormat() const;
+ QString dateFormatShort() const;
+ bool timeFormatOverride() const;
+ bool dateFormatOverride() const;
+ bool useSystemLanguage() const;
+
+public slots:
+ void setLanguage (const QString& value);
+ void setTimeFormat (const QString& value);
+ void setDateFormat (const QString& value);
+ void setDateFormatShort (const QString& value);
+ void setTimeFormatOverride (bool value);
+ void setDateFormatOverride (bool value);
+ void setUseSystemLanguage (bool value);
+signals:
+ void languageChanged(const QString& value);
+ void timeFormatChanged(const QString& value);
+ void dateFormatChanged(const QString& value);
+ void dateFormatShortChanged(const QString& value);
+ void timeFormatOverrideChanged(bool value);
+ void dateFormatOverrideChanged(bool value);
+ void useSystemLanguageChanged(bool value);
+
+private:
+ QString group;
+};
+
+class AnimationsSettingsObjectWrapper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(int animation_speed READ animationSpeed WRITE setAnimationSpeed NOTIFY animationSpeedChanged)
+public:
+ AnimationsSettingsObjectWrapper(QObject *parent);
+ int animationSpeed() const;
+
+public slots:
+ void setAnimationSpeed(int value);
+
+signals:
+ void animationSpeedChanged(int value);
+
+private:
+ QString group;
+};
+
+class LocationServiceSettingsObjectWrapper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(int time_threshold READ timeThreshold WRITE setTimeThreshold NOTIFY timeThresholdChanged)
+ Q_PROPERTY(int distance_threshold READ distanceThreshold WRITE setDistanceThreshold NOTIFY distanceThresholdChanged)
+public:
+ LocationServiceSettingsObjectWrapper(QObject *parent);
+ int timeThreshold() const;
+ int distanceThreshold() const;
+public slots:
+ void setTimeThreshold(int value);
+ void setDistanceThreshold(int value);
+signals:
+ void timeThresholdChanged(int value);
+ void distanceThresholdChanged(int value);
+private:
+ QString group;
+};
+
+class SettingsObjectWrapper : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(short save_userid_local READ saveUserIdLocal WRITE setSaveUserIdLocal NOTIFY saveUserIdLocalChanged)
+
+ Q_PROPERTY(TechnicalDetailsSettings* techical_details MEMBER techDetails CONSTANT)
+ Q_PROPERTY(PartialPressureGasSettings* pp_gas MEMBER pp_gas CONSTANT)
+ Q_PROPERTY(FacebookSettings* facebook MEMBER facebook CONSTANT)
+ Q_PROPERTY(GeocodingPreferences* geocoding MEMBER geocoding CONSTANT)
+ Q_PROPERTY(ProxySettings* proxy MEMBER proxy CONSTANT)
+ Q_PROPERTY(CloudStorageSettings* cloud_storage MEMBER cloud_storage CONSTANT)
+ Q_PROPERTY(DivePlannerSettings* planner MEMBER planner_settings CONSTANT)
+ Q_PROPERTY(UnitsSettings* units MEMBER unit_settings CONSTANT)
+
+ Q_PROPERTY(GeneralSettingsObjectWrapper* general MEMBER general_settings CONSTANT)
+ Q_PROPERTY(DisplaySettingsObjectWrapper* display MEMBER display_settings CONSTANT)
+ Q_PROPERTY(LanguageSettingsObjectWrapper* language MEMBER language_settings CONSTANT)
+ Q_PROPERTY(AnimationsSettingsObjectWrapper* animation MEMBER animation_settings CONSTANT)
+ Q_PROPERTY(LocationServiceSettingsObjectWrapper* Location MEMBER location_settings CONSTANT)
+public:
+ static SettingsObjectWrapper *instance();
+ short saveUserIdLocal() const;
+
+ TechnicalDetailsSettings *techDetails;
+ PartialPressureGasSettings *pp_gas;
+ FacebookSettings *facebook;
+ GeocodingPreferences *geocoding;
+ ProxySettings *proxy;
+ CloudStorageSettings *cloud_storage;
+ DivePlannerSettings *planner_settings;
+ UnitsSettings *unit_settings;
+ GeneralSettingsObjectWrapper *general_settings;
+ DisplaySettingsObjectWrapper *display_settings;
+ LanguageSettingsObjectWrapper *language_settings;
+ AnimationsSettingsObjectWrapper *animation_settings;
+ LocationServiceSettingsObjectWrapper *location_settings;
+
+public slots:
+ void setSaveUserIdLocal(short value);
+private:
+ SettingsObjectWrapper(QObject *parent = NULL);
+signals:
+ void saveUserIdLocalChanged(short value);
+};
+
+#endif
diff --git a/core/subsurfacestartup.c b/core/subsurfacestartup.c
new file mode 100644
index 000000000..6e0dede1c
--- /dev/null
+++ b/core/subsurfacestartup.c
@@ -0,0 +1,310 @@
+#include "subsurfacestartup.h"
+#include "version.h"
+#include <stdbool.h>
+#include <string.h>
+#include "gettext.h"
+#include "qthelperfromc.h"
+#include "git-access.h"
+#include "libdivecomputer/version.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,
+ .distance_threshold = 1000,
+ .time_threshold = 600
+};
+
+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, "--allow_run_as_root") == 0) {
+ ++force_root;
+ 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);
+}
+
+/*
+ * 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->time_format = copy_string(src->time_format);
+ dest->date_format = copy_string(src->date_format);
+ dest->date_format_short = copy_string(src->date_format_short);
+ 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)
+{
+ // nop
+}
diff --git a/core/subsurfacestartup.h b/core/subsurfacestartup.h
new file mode 100644
index 000000000..3ccc24aa4
--- /dev/null
+++ b/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 <stdbool.h>
+#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/core/subsurfacesysinfo.cpp b/core/subsurfacesysinfo.cpp
new file mode 100644
index 000000000..a7173b169
--- /dev/null
+++ b/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 <QString>
+
+#ifdef Q_OS_UNIX
+#include <sys/utsname.h>
+#endif
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 4, 0)
+
+#ifndef QStringLiteral
+# define QStringLiteral QString::fromUtf8
+#endif
+
+#ifdef Q_OS_UNIX
+#include <qplatformdefs.h>
+#endif
+
+#ifdef __APPLE__
+#include <CoreServices/CoreServices.h>
+#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<const char *>(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/core/subsurfacesysinfo.h b/core/subsurfacesysinfo.h
new file mode 100644
index 000000000..b2c267b83
--- /dev/null
+++ b/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 <QSysInfo>
+
+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/core/taxonomy.c b/core/taxonomy.c
new file mode 100644
index 000000000..670d85ad0
--- /dev/null
+++ b/core/taxonomy.c
@@ -0,0 +1,48 @@
+#include "taxonomy.h"
+#include "gettext.h"
+#include <stdlib.h>
+
+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/core/taxonomy.h b/core/taxonomy.h
new file mode 100644
index 000000000..51245d562
--- /dev/null
+++ b/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/core/time.c b/core/time.c
new file mode 100644
index 000000000..0893f19d8
--- /dev/null
+++ b/core/time.c
@@ -0,0 +1,98 @@
+#include <string.h>
+#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 unsigned int mdays[] = {
+ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
+ };
+ static const unsigned int mdays_leap[] = {
+ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
+ };
+ unsigned long val;
+ unsigned int leapyears;
+ int m;
+ const unsigned 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/core/uemis-downloader.c b/core/uemis-downloader.c
new file mode 100644
index 000000000..b9b532303
--- /dev/null
+++ b/core/uemis-downloader.c
@@ -0,0 +1,1403 @@
+/*
+ * uemis-downloader.c
+ *
+ * Copyright (c) Dirk Hohndel <dirk@hohndel.org>
+ * released under GPL2
+ *
+ * very (VERY) loosely based on the algorithms found in Java code by Fabian Gast <fgast@only640k.net>
+ * 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 <fcntl.h>
+#include <dirent.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#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;
+
+static int max_deleted_seen = -1;
+
+/* 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;
+ if (verbose)
+ fprintf(stderr, "Uemis downloader: %s\n", 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 protocol 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 delimiter ('{') */
+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
+ int written = write(reqtxt_file, sb, strlen(sb));
+ if (written == -1 || (size_t)written != 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);
+ free(mbuf);
+ 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 addresses 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, 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];
+ size_t 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 don't 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 variable 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-<section> */
+ 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;
+ }
+ }
+ if (max_deleted_seen >= 0 && maxdiveid < (uint32_t)max_deleted_seen) {
+ maxdiveid = max_deleted_seen;
+#if UEMIS_DEBUG & 4
+ fprintf(debugfile, "overriding max seen with max deleted seen %d\n", max_deleted_seen);
+#endif
+ }
+ 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 memory for a full round
+ * UEMIS_MEM_CRITICAL if the memory is good for reading the dive logs
+ * UEMIS_MEM_FULL if the memory is exhausted
+ */
+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 UEMIS_DEBUG & 4
+ fprintf(debugfile, "max_mem_used %d (from td->nr %d) * block_size %d > max_files %d - filenr %d?\n", max_mem_used, td->nr, UEMIS_LOG_BLOCK_SIZE, UEMIS_MAX_FILES, filenr);
+#endif
+ 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 duplicate and can be deleted */
+ if (nds->uuid != ods->uuid) {
+ delete_dive_site(nds->uuid);
+ dive->dive_site_uuid = ods->uuid;
+ }
+ }
+ } else {
+ /* if we can't 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;
+ bool found_below = false;
+ bool found_above = false;
+ int deleted_files = 0;
+
+ snprintf(log_file_no_to_find, sizeof(log_file_no_to_find), "logfilenr{int{%d", dive->dc.diveid);
+#if UEMIS_DEBUG & 2
+ fprintf(debugfile, "Looking for dive details to go with divelog id %d\n", dive->dc.diveid);
+#endif
+ 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 successful even if the dive has been deleted. */
+ found = true;
+ if (strstr(mbuf, "deleted{bool{true") == NULL) {
+ process_raw_buffer(data, deviceidnr, mbuf, &newmax, 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++;
+ max_deleted_seen = dive_to_read;
+ /* 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 -- newmax is %s\n", d_time, dive->dc.diveid, newmax);
+#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 || nr_found == 0) {
+ found_above = true;
+ dive_to_read = dive_to_read - 2;
+ } else {
+ found_below = true;
+ }
+ if (dive_to_read < -1)
+ dive_to_read = -1;
+ }
+ }
+ }
+ if (found_above && found_below)
+ break;
+ 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, 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
+ 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);
+ if (verbose)
+ fprintf(stderr, "Uemis downloader: start looking at dive nr %s", newmax);
+
+ 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);
+ /* first, remove any leading garbage... this needs to start with a '{' */
+ char *realmbuf = mbuf;
+ if (mbuf)
+ realmbuf = strchr(mbuf, '{');
+ if (success && realmbuf && uemis_mem_status != UEMIS_MEM_FULL) {
+#if UEMIS_DEBUG & 16
+ do_dump_buffer_to_file(realmbuf, "Divelogs");
+#endif
+ /* process the buffer we have assembled */
+ if (!process_raw_buffer(data, deviceidnr, realmbuf, &newmax, 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(realmbuf);
+ if (t && atoi(t) > start)
+ start = atoi(t);
+ free(t);
+ once = false;
+ }
+ /* clean up mbuf */
+ endptr = strstr(realmbuf, "{{{");
+ 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 details 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) {
+#if UEMIS_DEBUG & 4
+ fprintf(debugfile, "d_u_i out of memory, bailing\n");
+#endif
+ break;
+ }
+ /* if the user clicked cancel, exit gracefully */
+ if (import_thread_cancelled) {
+#if UEMIS_DEBUG & 4
+ fprintf(debugfile, "d_u_i thread canceled, bailing\n");
+#endif
+ break;
+ }
+ /* if we got an error or got nothing back, stop trying */
+ if (!success || !param_buff[3]) {
+#if UEMIS_DEBUG & 4
+ fprintf(debugfile, "d_u_i after download nothing found, giving up\n");
+#endif
+ break;
+ }
+#if UEMIS_DEBUG & 2
+ if (debug_round != -1)
+ if (debug_round-- == 0) {
+ fprintf(debugfile, "d_u_i debug_round is now 0, bailing\n");
+ goto bail;
+ }
+#endif
+ } else {
+ /* some of the loading from the UEMIS failed at the divelog level
+ * if the memory status = full, we can't even load the divespots and/or buddies.
+ * 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);
+#if UEMIS_DEBUG & 4
+ fprintf(debugfile, "d_u_i out of memory, bailing instead of processing\n");
+#endif
+ 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/core/uemis.c b/core/uemis.c
new file mode 100644
index 000000000..5635d5630
--- /dev/null
+++ b/core/uemis.c
@@ -0,0 +1,392 @@
+/*
+ * uemis.c
+ *
+ * UEMIS SDA file importer
+ * AUTHOR: Dirk Hohndel - Copyright 2011
+ *
+ * Licensed under the MIT license.
+ */
+#include <stdio.h>
+#include <string.h>
+
+#include "gettext.h"
+
+#include "dive.h"
+#include "uemis.h"
+#include <libdivecomputer/parser.h>
+#include <libdivecomputer/version.h>
+
+/*
+ * 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 {
+ uint32_t 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(uint32_t 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(uint32_t 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/core/uemis.h b/core/uemis.h
new file mode 100644
index 000000000..1758b4b32
--- /dev/null
+++ b/core/uemis.h
@@ -0,0 +1,54 @@
+/*
+ * defines and prototypes for the uemis Zurich SDA file parser
+ */
+
+#ifndef UEMIS_H
+#define UEMIS_H
+
+#include <stdint.h>
+#include "dive.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void uemis_parse_divelog_binary(char *base64, void *divep);
+int uemis_get_weight_unit(uint32_t 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/core/units.h b/core/units.h
new file mode 100644
index 000000000..029bb64fa
--- /dev/null
+++ b/core/units.h
@@ -0,0 +1,280 @@
+#ifndef UNITS_H
+#define UNITS_H
+
+#include <math.h>
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+#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 <stdint.h>
+/*
+ * 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
+{
+ uint32_t mkelvin; // up to 1750 degrees K (temperatures in K are always positive)
+} 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 LENGHT {
+ METERS,
+ FEET
+ } length;
+ enum VOLUME {
+ LITER,
+ CUFT
+ } volume;
+ enum PRESSURE {
+ BAR,
+ PSI,
+ PASCAL
+ } pressure;
+ enum TEMPERATURE {
+ CELSIUS,
+ FAHRENHEIT,
+ KELVIN
+ } temperature;
+ enum WEIGHT {
+ KG,
+ LBS
+ } weight;
+ enum TIME {
+ 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/core/version.c b/core/version.c
new file mode 100644
index 000000000..764e4d2db
--- /dev/null
+++ b/core/version.c
@@ -0,0 +1,18 @@
+#include "ssrf-version.h"
+
+const char *subsurface_git_version(void)
+{
+ return GIT_VERSION_STRING;
+}
+
+const char *subsurface_canonical_version(void)
+{
+ return CANONICAL_VERSION_STRING;
+}
+
+#ifdef SUBSURFACE_MOBILE
+const char *subsurface_mobile_version(void)
+{
+ return MOBILE_VERSION_STRING;
+}
+#endif
diff --git a/core/version.h b/core/version.h
new file mode 100644
index 000000000..0a3204bd9
--- /dev/null
+++ b/core/version.h
@@ -0,0 +1,19 @@
+#ifndef VERSION_H
+#define VERSION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+const char *subsurface_git_version(void);
+const char *subsurface_canonical_version(void);
+
+#ifdef SUBSURFACE_MOBILE
+const char *subsurface_mobile_version(void);
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/core/webservice.h b/core/webservice.h
new file mode 100644
index 000000000..052b8aae7
--- /dev/null
+++ b/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/core/windows.c b/core/windows.c
new file mode 100644
index 000000000..58d3beaad
--- /dev/null
+++ b/core/windows.c
@@ -0,0 +1,454 @@
+/* windows.c */
+/* implements Windows specific functions */
+#include <io.h>
+#include "dive.h"
+#include "display.h"
+#undef _WIN32_WINNT
+#define _WIN32_WINNT 0x500
+#include <windows.h>
+#include <shlobj.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <assert.h>
+#include <dirent.h>
+#include <zip.h>
+#include <lmcons.h>
+
+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 "\\<USERNAME>.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
+}
+
+bool subsurface_user_is_root()
+{
+ /* FIXME: Detect admin rights */
+ return (false);
+}
diff --git a/core/windowtitleupdate.cpp b/core/windowtitleupdate.cpp
new file mode 100644
index 000000000..963455f1d
--- /dev/null
+++ b/core/windowtitleupdate.cpp
@@ -0,0 +1,32 @@
+#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();
+ if (wt)
+ wt->emitSignal();
+}
diff --git a/core/windowtitleupdate.h b/core/windowtitleupdate.h
new file mode 100644
index 000000000..8650e5868
--- /dev/null
+++ b/core/windowtitleupdate.h
@@ -0,0 +1,20 @@
+#ifndef WINDOWTITLEUPDATE_H
+#define WINDOWTITLEUPDATE_H
+
+#include <QObject>
+
+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/core/worldmap-options.h b/core/worldmap-options.h
new file mode 100644
index 000000000..177443563
--- /dev/null
+++ b/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/core/worldmap-save.c b/core/worldmap-save.c
new file mode 100644
index 000000000..e7e8bcc30
--- /dev/null
+++ b/core/worldmap-save.c
@@ -0,0 +1,117 @@
+// Clang has a bug on zero-initialization of C structs.
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#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: '<div id=\"content\">'+'<div id=\"siteNotice\">'+'</div>'+'<div id=\"bodyContent\">");
+ snprintf(pre, sizeof(pre), "<p>%s ", translate("gettextFromC", "Date:"));
+ put_HTML_date(b, dive, pre, "</p>");
+ snprintf(pre, sizeof(pre), "<p>%s ", translate("gettextFromC", "Time:"));
+ put_HTML_time(b, dive, pre, "</p>");
+ snprintf(pre, sizeof(pre), "<p>%s ", translate("gettextFromC", "Duration:"));
+ snprintf(post, sizeof(post), " %s</p>", translate("gettextFromC", "min"));
+ put_duration(b, dive->duration, pre, post);
+ put_string(b, "<p> ");
+ put_HTML_quoted(b, translate("gettextFromC", "Max. depth:"));
+ put_HTML_depth(b, dive, " ", "</p>");
+ put_string(b, "<p> ");
+ put_HTML_quoted(b, translate("gettextFromC", "Air temp.:"));
+ put_HTML_airtemp(b, dive, " ", "</p>");
+ put_string(b, "<p> ");
+ put_HTML_quoted(b, translate("gettextFromC", "Water temp.:"));
+ put_HTML_watertemp(b, dive, " ", "</p>");
+ snprintf(pre, sizeof(pre), "<p>%s <b>", translate("gettextFromC", "Location:"));
+ put_string(b, pre);
+ put_HTML_quoted(b, get_dive_location(dive));
+ put_string(b, "</b></p>");
+ snprintf(pre, sizeof(pre), "<p> %s ", translate("gettextFromC", "Notes:"));
+ put_HTML_notes(b, dive, pre, " </p>");
+ put_string(b, "</p>'+'</div>'+'</div>'});\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, "<!DOCTYPE html>\n<html>\n<head>\n");
+ put_string(b, "<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\" />\n<title>World Map</title>\n");
+ put_string(b, "<meta charset=\"UTF-8\">");
+}
+
+void insert_css(struct membuffer *b)
+{
+ put_format(b, "<style type=\"text/css\">%s</style>\n", css);
+}
+
+void insert_javascript(struct membuffer *b, const bool selected_only)
+{
+ put_string(b, "<script type=\"text/javascript\" src=\"");
+ put_string(b, getGoogleApi());
+ put_string(b, "&amp;sensor=false\">\n</script>\n<script type=\"text/javascript\">\nvar map;\n");
+ put_format(b, "function initialize() {\nvar mapOptions = {\n\t%s,", map_options);
+ put_string(b, "rotateControl: false,\n\tstreetViewControl: false,\n\tmapTypeControl: false\n};\n");
+ put_string(b, "map = new google.maps.Map(document.getElementById(\"map-canvas\"),mapOptions);\nvar markers = new Array();");
+ put_string(b, "\nvar infowindows = new Array();\nvar temp;\nvar tempinfowindow;\n");
+ writeMarkers(b, selected_only);
+ put_string(b, "\nfor(var i=0;i<markers.length;i++)\n\tmarkers[i].setMap(map);\n}\n");
+ put_string(b, "google.maps.event.addDomListener(window, 'load', initialize);</script>\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</head>\n<body>\n<div id=\"map-canvas\"></div>\n</body>\n</html>");
+}
+
+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/core/worldmap-save.h b/core/worldmap-save.h
new file mode 100644
index 000000000..102ea40e5
--- /dev/null
+++ b/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