subsurface/core/divelog.cpp
Berthold Stoeger 4d7291d4a1 core: move merge_dives() functios to struct dive_table
These functions have to access other dives in the list to
calculate CNS, etc, so let's call them from there.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-08-13 19:28:30 +02:00

536 lines
18 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "divelog.h"
#include "divelist.h"
#include "divesite.h"
#include "device.h"
#include "dive.h"
#include "errorhelper.h"
#include "filterpreset.h"
#include "filterpresettable.h"
#include "qthelper.h" // for emit_reset_signal() -> should be removed
#include "range.h"
#include "selection.h" // clearly, a layering violation -> should be removed
#include "trip.h"
struct divelog divelog;
divelog::divelog() = default;
divelog::~divelog() = default;
divelog::divelog(divelog &&) = default;
struct divelog &divelog::operator=(divelog &&) = default;
/* this implements the mechanics of removing the dive from the
* dive log and the trip, but doesn't deal with updating dive trips, etc */
void divelog::delete_single_dive(int idx)
{
if (idx < 0 || static_cast<size_t>(idx) >= dives.size()) {
report_info("Warning: deleting non-existing dive with index %d", idx);
return;
}
struct dive *dive = dives[idx].get();
struct dive_trip *trip = unregister_dive_from_trip(dive);
// Deleting a dive may change the order of trips!
if (trip)
trips.sort();
if (trip && trip->dives.empty())
trips.pull(trip);
unregister_dive_from_dive_site(dive);
dives.erase(dives.begin() + idx);
}
void divelog::delete_multiple_dives(const std::vector<dive *> &dives_to_delete)
{
bool trips_changed = false;
for (dive *d: dives_to_delete) {
// Delete dive from trip and delete trip if empty
struct dive_trip *trip = unregister_dive_from_trip(d);
if (trip && trip->dives.empty()) {
trips_changed = true;
trips.pull(trip);
}
unregister_dive_from_dive_site(d);
dives.pull(d);
}
// Deleting a dive may change the order of trips!
if (trips_changed)
trips.sort();
}
void divelog::clear()
{
dives.clear();
sites.clear();
trips.clear();
devices.clear();
filter_presets.clear();
}
/* check if we have a trip right before / after this dive */
bool divelog::is_trip_before_after(const struct dive *dive, bool before) const
{
auto it = std::find_if(dives.begin(), dives.end(),
[dive](auto &d) { return d.get() == dive; });
if (it == dives.end())
return false;
if (before) {
do {
if (it == dives.begin())
return false;
--it;
} while ((*it)->invalid);
return (*it)->divetrip != nullptr;
} else {
++it;
while (it != dives.end() && (*it)->invalid)
++it;
return it != dives.end() && (*it)->divetrip != nullptr;
}
}
/*
* Merge subsequent dives in a table, if mergeable. This assumes
* that the dives are neither selected, not part of a trip, as
* is the case of freshly imported dives.
*/
static void merge_imported_dives(struct dive_table &table)
{
for (size_t i = 1; i < table.size(); i++) {
auto &prev = table[i - 1];
auto &dive = table[i];
/* 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->endtime() < dive->when)
continue;
auto merged = table.try_to_merge(*prev, *dive, false);
if (!merged)
continue;
/* Add dive to dive site; try_to_merge() does not do that! */
struct dive_site *ds = merged->dive_site;
if (ds) {
merged->dive_site = NULL;
ds->add_dive(merged.get());
}
unregister_dive_from_dive_site(prev.get());
unregister_dive_from_dive_site(dive.get());
unregister_dive_from_trip(prev.get());
unregister_dive_from_trip(dive.get());
/* Overwrite the first of the two dives and remove the second */
table[i - 1] = std::move(merged);
table.erase(table.begin() + i);
/* Redo the new 'i'th dive */
i--;
}
}
/*
* Walk the dives from the oldest dive in the given table, and see if we
* can autogroup them. But only do this when the user selected autogrouping.
*/
static void autogroup_dives(struct divelog &log)
{
if (!log.autogroup)
return;
for (auto &entry: get_dives_to_autogroup(log.dives)) {
for (auto it = log.dives.begin() + entry.from; it != log.dives.begin() + entry.to; ++it)
entry.trip->add_dive(it->get());
/* If this was newly allocated, add trip to list */
if (entry.created_trip)
log.trips.put(std::move(entry.created_trip));
}
log.trips.sort(); // Necessary?
#ifdef DEBUG_TRIP
dump_trip_list();
#endif
}
/* Check if a dive is ranked after the last dive of the global dive list */
static bool dive_is_after_last(const struct dive &d)
{
if (divelog.dives.empty())
return true;
return dive_less_than(*divelog.dives.back(), d);
}
/*
* Try to merge a new dive into the dive at position idx. Return
* true on success. On success, the old dive will be added to the
* dives_to_remove table and the merged dive to the dives_to_add
* table. On failure everything stays unchanged.
* If "prefer_imported" is true, use data of the new dive.
*/
static bool try_to_merge_into(struct dive &dive_to_add, struct dive *old_dive, bool prefer_imported,
/* output parameters: */
struct dive_table &dives_to_add, struct std::vector<dive *> &dives_to_remove)
{
auto merged = dives_to_add.try_to_merge(*old_dive, dive_to_add, prefer_imported);
if (!merged)
return false;
merged->divetrip = old_dive->divetrip;
range_insert_sorted(dives_to_remove, old_dive, comp_dives_ptr);
dives_to_add.put(std::move(merged));
return true;
}
/* Merge dives from "dives_from", owned by "delete" into the owned by "dives_to".
* Overlapping dives will be merged, non-overlapping dives will be moved. The results
* will be added to the "dives_to_add" table. Dives that were merged are added to
* the "dives_to_remove" table. Any newly added (not merged) dive will be assigned
* to the trip of the "trip" paremeter. If "delete_from" is non-null dives will be
* removed from this table.
* This function supposes that all input tables are sorted.
* Returns true if any dive was added (not merged) that is not past the
* last dive of the global dive list (i.e. the sequence will change).
* The integer referenced by "num_merged" will be increased for every
* merged dive that is added to "dives_to_add" */
static bool merge_dive_tables(const std::vector<dive *> &dives_from, struct dive_table &delete_from,
const std::vector<dive *> &dives_to,
bool prefer_imported, struct dive_trip *trip,
/* output parameters: */
struct dive_table &dives_to_add, struct std::vector<dive *> &dives_to_remove,
int &num_merged)
{
bool sequence_changed = false;
/* Merge newly imported dives into the dive table.
* Since both lists (old and new) are sorted, we can step
* through them concurrently and locate the insertions points.
* Once found, check if the new dive can be merged in the
* previous or next dive.
* Note that this doesn't consider pathological cases such as:
* - New dive "connects" two old dives (turn three into one).
* - New dive can not be merged into adjacent but some further dive.
*/
size_t j = 0; /* Index in dives_to */
size_t last_merged_into = std::string::npos;
for (dive *add: dives_from) {
/* This gets an owning pointer to the dive to add and removes it from
* the delete_from table. If the dive is not explicitly stored, it will
* be automatically deleting when ending the loop iteration */
auto [dive_to_add, idx] = delete_from.pull(add);
if (!dive_to_add) {
report_info("merging unknown dives!");
continue;
}
/* Find insertion point. */
while (j < dives_to.size() && dive_less_than(*dives_to[j], *dive_to_add))
j++;
/* Try to merge into previous dive.
* We are extra-careful to not merge into the same dive twice, as that
* would put the merged-into dive twice onto the dives-to-delete list.
* In principle that shouldn't happen as all dives that compare equal
* by is_same_dive() were already merged, and is_same_dive() should be
* transitive. But let's just go *completely* sure for the odd corner-case. */
if (j > 0 && (last_merged_into == std::string::npos || j > last_merged_into + 1) &&
dives_to[j - 1]->endtime() > dive_to_add->when) {
if (try_to_merge_into(*dive_to_add, dives_to[j - 1], prefer_imported,
dives_to_add, dives_to_remove)) {
last_merged_into = j - 1;
num_merged++;
continue;
}
}
/* That didn't merge into the previous dive.
* Try to merge into next dive. */
if (j < dives_to.size() && (last_merged_into == std::string::npos || j > last_merged_into) &&
dive_to_add->endtime() > dives_to[j]->when) {
if (try_to_merge_into(*dive_to_add, dives_to[j], prefer_imported,
dives_to_add, dives_to_remove)) {
last_merged_into = j;
num_merged++;
continue;
}
}
sequence_changed |= !dive_is_after_last(*dive_to_add);
dives_to_add.put(std::move(dive_to_add));
}
return sequence_changed;
}
/* Helper function for process_imported_dives():
* Try to merge a trip into one of the existing trips.
* The bool pointed to by "sequence_changed" is set to true, if the sequence of
* the existing dives changes.
* The int pointed to by "start_renumbering_at" keeps track of the first dive
* to be renumbered in the dives_to_add table.
* For other parameters see process_imported_dives()
* Returns true if trip was merged. In this case, the trip will be
* freed.
*/
static bool try_to_merge_trip(dive_trip &trip_import, struct dive_table &import_table, bool prefer_imported,
/* output parameters: */
struct dive_table &dives_to_add, std::vector<dive *> &dives_to_remove,
bool &sequence_changed, int &start_renumbering_at)
{
for (auto &trip_old: divelog.trips) {
if (trips_overlap(trip_import, *trip_old)) {
sequence_changed |= merge_dive_tables(trip_import.dives, import_table, trip_old->dives,
prefer_imported, trip_old.get(),
dives_to_add, dives_to_remove,
start_renumbering_at);
/* we took care of all dives of the trip, clean up the table */
trip_import.dives.clear();
return true;
}
}
return false;
}
// Helper function to convert a table of owned dives into a table of non-owning pointers.
// Used to merge *all* dives of a log into a different table.
static std::vector<dive *> dive_table_to_non_owning(const dive_table &dives)
{
std::vector<dive *> res;
res.reserve(dives.size());
for (auto &d: dives)
res.push_back(d.get());
return res;
}
/* Process imported dives: take a log.trips of dives to be imported and
* generate five lists:
* 1) Dives to be added (newly created, owned)
* 2) Dives to be removed (old, non-owned, references global divelog)
* 3) Trips to be added (newly created, owned)
* 4) Dive sites to be added (newly created, owned)
* 5) Devices to be added (newly created, owned)
* The dives, trips and sites in import_log are consumed.
* On return, the tables have * size 0.
*
* Note: The new dives will have their divetrip- and divesites-fields
* set, but will *not* be part of the trip and site. The caller has to
* add them to the trip and site.
*
* The lists are generated by merging dives if possible. This is
* performed trip-wise. Finer control on merging is provided by
* the "flags" parameter:
* - If import_flags::prefer_imported is set, data of the new dives are
* prioritized on merging.
* - If import_flags::merge_all_trips is set, all overlapping trips will
* be merged, not only non-autogenerated trips.
* - If import_flags::is_downloaded is true, only the divecomputer of the
* first dive will be considered, as it is assumed that all dives
* come from the same computer.
* - If import_flags::add_to_new_trip is true, dives that are not assigned
* to a trip will be added to a newly generated trip.
*/
divelog::process_imported_dives_result divelog::process_imported_dives(struct divelog &import_log, int flags)
{
int start_renumbering_at = 0;
bool sequence_changed = false;
bool last_old_dive_is_numbered;
process_imported_dives_result res;
/* If no dives were imported, don't bother doing anything */
if (import_log.dives.empty())
return res;
/* Check if any of the new dives has a number. This will be
* important later to decide if we want to renumber the added
* dives */
bool new_dive_has_number = std::any_of(import_log.dives.begin(), import_log.dives.end(),
[](auto &d) { return d->number > 0; });
/* Add only the devices that we don't know about yet. */
for (auto &dev: import_log.devices) {
if (!device_exists(devices, dev))
add_to_device_table(res.devices_to_add, dev);
}
/* Sort the table of dives to be imported and combine mergable dives */
import_log.dives.sort();
merge_imported_dives(import_log.dives);
/* Autogroup tripless dives if desired by user. But don't autogroup
* if tripless dives should be added to a new trip. */
if (!(flags & import_flags::add_to_new_trip))
autogroup_dives(import_log);
/* If dive sites already exist, use the existing versions. */
for (auto &new_ds: import_log.sites) {
/* Check if it dive site is actually used by new dives. */
if (std::none_of(import_log.dives.begin(), import_log.dives.end(), [ds=new_ds.get()]
(auto &d) { return d->dive_site == ds; }))
continue;
struct dive_site *old_ds = sites.get_same(*new_ds);
if (!old_ds) {
/* Dive site doesn't exist. Add it to list of dive sites to be added. */
new_ds->dives.clear(); /* Caller is responsible for adding dives to site */
res.sites_to_add.put(std::move(new_ds));
} else {
/* Dive site already exists - use the old one. */
for (auto &d: import_log.dives) {
if (d->dive_site == new_ds.get())
d->dive_site = old_ds;
}
}
}
import_log.sites.clear();
/* Merge overlapping trips. Since both trip tables are sorted, we
* could be smarter here, but realistically not a whole lot of trips
* will be imported so do a simple n*m loop until someone complains.
*/
for (auto &trip_import: import_log.trips) {
if ((flags & import_flags::merge_all_trips) || trip_import->autogen) {
if (try_to_merge_trip(*trip_import, import_log.dives, flags & import_flags::prefer_imported,
res.dives_to_add, res.dives_to_remove,
sequence_changed, start_renumbering_at))
continue;
}
/* If no trip to merge-into was found, add trip as-is.
* First, add dives to list of dives to add */
for (struct dive *d: trip_import->dives) {
/* Add dive to list of dives to-be-added. */
auto [owned, idx] = import_log.dives.pull(d);
if (!owned)
continue;
sequence_changed |= !dive_is_after_last(*owned);
res.dives_to_add.put(std::move(owned));
}
trip_import->dives.clear(); /* Caller is responsible for adding dives to trip */
/* Finally, add trip to list of trips to add */
res.trips_to_add.put(std::move(trip_import));
}
import_log.trips.clear(); /* All trips were consumed */
if ((flags & import_flags::add_to_new_trip) && !import_log.dives.empty()) {
/* Create a new trip for unassigned dives, if desired. */
auto [new_trip, idx] = res.trips_to_add.put(
create_trip_from_dive(import_log.dives.front().get())
);
/* Add all remaining dives to this trip */
for (auto &d: import_log.dives) {
sequence_changed |= !dive_is_after_last(*d);
d->divetrip = new_trip;
res.dives_to_add.put(std::move(d));
}
import_log.dives.clear(); /* All dives were consumed */
} else if (!import_log.dives.empty()) {
/* The remaining dives in import_log.dives are those that don't belong to
* a trip and the caller does not want them to be associated to a
* new trip. Merge them into the global table. */
sequence_changed |= merge_dive_tables(dive_table_to_non_owning(import_log.dives),
import_log.dives,
dive_table_to_non_owning(dives),
flags & import_flags::prefer_imported, NULL,
res.dives_to_add, res.dives_to_remove, start_renumbering_at);
}
/* If new dives were only added at the end, renumber the added dives.
* But only if
* - The last dive in the old dive table had a number itself (if there is a last dive).
* - None of the new dives has a number.
*/
last_old_dive_is_numbered = dives.empty() || dives.back()->number > 0;
/* We counted the number of merged dives that were added to dives_to_add.
* Skip those. Since sequence_changed is false all added dives are *after*
* all merged dives. */
if (!sequence_changed && last_old_dive_is_numbered && !new_dive_has_number) {
int nr = !dives.empty() ? dives.back()->number : 0;
for (auto it = res.dives_to_add.begin() + start_renumbering_at; it < res.dives_to_add.end(); ++it)
(*it)->number = ++nr;
}
return res;
}
// TODO: This accesses global state, namely fulltext and selection.
// TODO: Move up the call chain?
void divelog::process_loaded_dives()
{
dives.sort();
trips.sort();
/* Autogroup dives if desired by user. */
autogroup_dives(*this);
fulltext_populate();
/* Inform frontend of reset data. This should reset all the models. */
emit_reset_signal();
/* Now that everything is settled, select the newest dive. */
select_newest_visible_dive();
}
/* Merge the dives of the trip "from" and the dive_table "dives_from" into the trip "to"
* and dive_table "dives_to". If "prefer_imported" is true, dive data of "from" takes
* precedence */
void divelog::add_imported_dives(struct divelog &import_log, int flags)
{
/* Process imported dives and generate lists of dives
* to-be-added and to-be-removed */
auto [dives_to_add, dives_to_remove, trips_to_add, dive_sites_to_add, devices_to_add] =
process_imported_dives(import_log, flags);
/* Start by deselecting all dives, so that we don't end up with an invalid selection */
select_single_dive(NULL);
/* Add new dives to trip and site to get reference count correct. */
for (auto &d: dives_to_add) {
struct dive_trip *trip = d->divetrip;
struct dive_site *site = d->dive_site;
d->divetrip = NULL;
d->dive_site = NULL;
trip->add_dive(d.get());
if (site)
site->add_dive(d.get());
}
/* Remove old dives */
delete_multiple_dives(dives_to_remove);
/* Add new dives */
for (auto &d: dives_to_add)
dives.put(std::move(d));
dives_to_add.clear();
/* Add new trips */
for (auto &trip: trips_to_add)
trips.put(std::move(trip));
trips_to_add.clear();
/* Add new dive sites */
for (auto &ds: dive_sites_to_add)
sites.register_site(std::move(ds));
/* Add new devices */
for (auto &dev: devices_to_add)
add_to_device_table(devices, dev);
/* We might have deleted the old selected dive.
* Choose the newest dive as selected (if any) */
current_dive = !dives.empty() ? dives.back().get() : nullptr;
/* Inform frontend of reset data. This should reset all the models. */
emit_reset_signal();
}