subsurface/core/divelog.cpp
Berthold Stoeger 176f544106 core: move process_import_dives() and related functions to divelog
These functions accessed the global divelog make this explicit.

I'm still not happy about the situation, because these functions
access global state, such as the selection. I think these
should be moved up the call-chain.

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 = 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 = 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();
}