First cut of explicit trip tracking

This code establishes the explicit trip data structures and loads and
saves them in the XML data. No attempts are made to edit / modify the
trips, yet.

Loading XML files without trip data creates the trips based on timing as
before. Saving out the same, unmodified data will create 'trip' entries in
the XML file with a 'number' that reflects the number of dives in that
trip. The trip tag also stores the beginning time of the first dive in the
trip and the location of the trip (which we display in the summary entries
in the UI).

The logic allows for dives that aren't part of a dive trip. All other
dives simply belong to the "previous" dive trip - i.e. the dive trip with
the latest start time that is earlier or equal to the start time of this
dive.

This logic significantly simplifies the tracking of trips compared to
other approaches that I have tried.

The automatic grouping into trips now is an option that defaults to off
(as it makes changes to the XML file - and people who don't want this
feature shouldn't have trips added to their XML files that they then need
to manually remove).

For now you have to select this option, then exit the program and start it
again. Still to do is to trigger the trip generation at run time.

We also need a way to mark dives as not part of trips and to allow options
to combine trips, split trips, edit trip location data, etc.

The code has only had some limited testing when opening multiple files.

The code is known to fail if a location name contains unquoted special
characters like an "'".

This commit also fixes a visual inconsistency in the preferences dialog
where the font selector button didn't have a frame around it that told you
what this option was about.

Inspired-by: Linus Torvalds <torvalds@linux-foundation.org>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
This commit is contained in:
Dirk Hohndel 2012-08-21 22:04:24 -07:00
parent 5726a50d89
commit e315abf645
5 changed files with 236 additions and 75 deletions

25
dive.h
View file

@ -234,8 +234,12 @@ struct event {
#define W_IDX_PRIMARY 0 #define W_IDX_PRIMARY 0
#define W_IDX_SECONDARY 1 #define W_IDX_SECONDARY 1
typedef enum { TF_NONE, NO_TRIP, IN_TRIP, NUM_TRIPFLAGS } tripflag_t;
extern const char *tripflag_names[NUM_TRIPFLAGS];
struct dive { struct dive {
int number; int number;
tripflag_t tripflag;
int selected; int selected;
time_t when; time_t when;
char *location; char *location;
@ -256,6 +260,27 @@ struct dive {
struct sample sample[]; struct sample sample[];
}; };
extern GList *dive_trip_list;
extern gboolean 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)
#define NEXT_TRIP(_entry, _list) ((_entry) ? g_list_next(_entry) : (_list))
#define PREV_TRIP(_entry, _list) ((_entry) ? g_list_previous(_entry) : g_list_last(_list))
#define DIVE_TRIP(_trip) ((struct dive *)(_trip)->data)
#define DIVE_FITS_TRIP(_dive, _dive_trip) ((_dive_trip)->when - TRIP_THRESHOLD <= (_dive)->when)
static inline int dive_date_cmp(gconstpointer _a, gconstpointer _b) {
return ((struct dive *)(_a))->when - ((struct dive *)(_b))->when;
}
#define INSERT_TRIP(_trip, _list) g_list_insert_sorted((_list), (_trip), dive_date_cmp)
#define FIND_TRIP(_trip, _list) g_list_find_custom((_list), (_trip), dive_date_cmp)
/* /*
* We keep our internal data in well-specified units, but * We keep our internal data in well-specified units, but
* the input and output may come in some random format. This * the input and output may come in some random format. This

View file

@ -31,6 +31,10 @@ struct DiveList {
}; };
static struct DiveList dive_list; static struct DiveList dive_list;
GList *dive_trip_list;
gboolean autogroup = FALSE;
const char *tripflag_names[NUM_TRIPFLAGS] = { "TF_NONE", "NOTRIP", "INTRIP" };
/* /*
* The dive list has the dive data in both string format (for showing) * The dive list has the dive data in both string format (for showing)
@ -54,19 +58,22 @@ enum {
DIVELIST_COLUMNS DIVELIST_COLUMNS
}; };
/* magic numbers that indicate (as negative values) model entries that
* are summary entries for a divetrip */
#define NEW_TRIP 1
#ifdef DEBUG_MODEL #ifdef DEBUG_MODEL
static gboolean dump_model_entry(GtkTreeModel *model, GtkTreePath *path, static gboolean dump_model_entry(GtkTreeModel *model, GtkTreePath *path,
GtkTreeIter *iter, gpointer data) GtkTreeIter *iter, gpointer data)
{ {
char *location; char *location;
int idx, nr, rating, depth; int idx, nr, duration;
struct dive *dive;
gtk_tree_model_get(model, iter, DIVE_INDEX, &idx, DIVE_NR, &nr, DIVE_DURATION, &duration, DIVE_LOCATION, &location, -1);
printf("entry #%d : nr %d duration %d location %s ", idx, nr, duration, location);
dive = get_dive(idx);
if (dive)
printf("tripflag %d\n", dive->tripflag);
else
printf("without matching dive\n");
gtk_tree_model_get(model, iter, DIVE_INDEX, &idx, DIVE_NR, &nr, DIVE_RATING, &rating, DIVE_DEPTH, &depth, DIVE_LOCATION, &location, -1);
printf("entry #%d : nr %d rating %d depth %d location %s \n", idx, nr, rating, depth, location);
free(location); free(location);
return FALSE; return FALSE;
@ -327,16 +334,14 @@ static void date_data_func(GtkTreeViewColumn *col,
when = val; when = val;
tm = gmtime(&when); tm = gmtime(&when);
switch(idx) { if (idx < 0) {
case -NEW_TRIP:
snprintf(buffer, sizeof(buffer), snprintf(buffer, sizeof(buffer),
"Trip %s, %s %d, %d (%d dive%s)", "Trip %s, %s %d, %d (%d dive%s)",
weekday(tm->tm_wday), weekday(tm->tm_wday),
monthname(tm->tm_mon), monthname(tm->tm_mon),
tm->tm_mday, tm->tm_year + 1900, tm->tm_mday, tm->tm_year + 1900,
nr, nr > 1 ? "s" : ""); nr, nr > 1 ? "s" : "");
break; } else {
default:
snprintf(buffer, sizeof(buffer), snprintf(buffer, sizeof(buffer),
"%s, %s %d, %d %02d:%02d", "%s, %s %d, %d %02d:%02d",
weekday(tm->tm_wday), weekday(tm->tm_wday),
@ -877,75 +882,101 @@ void update_dive_list_col_visibility(void)
return; return;
} }
/* random heuristic - not diving in three days implies new dive trip */
#define TRIP_THRESHOLD 3600*24*3
static int new_group(struct dive *dive, struct dive **last_dive, time_t *tm_date)
{
if (!last_dive)
return TRUE;
if (*last_dive) {
struct dive *ldive = *last_dive;
if (abs(dive->when - ldive->when) < TRIP_THRESHOLD) {
*last_dive = dive;
return FALSE;
}
}
*last_dive = dive;
if (tm_date) {
struct tm *tm1 = gmtime(&dive->when);
tm1->tm_sec = 0;
tm1->tm_min = 0;
tm1->tm_hour = 0;
*tm_date = mktime(tm1);
}
return TRUE;
}
static void fill_dive_list(void) static void fill_dive_list(void)
{ {
int i, group_size; int i;
GtkTreeIter iter, parent_iter; GtkTreeIter iter, parent_iter, *parent_ptr = NULL;
GtkTreeStore *liststore, *treestore; GtkTreeStore *liststore, *treestore;
struct dive *last_dive = NULL; struct dive *last_trip = NULL;
struct dive *last_trip_dive = NULL; GList *trip;
const char *last_location = NULL; struct dive *dive_trip = NULL;
time_t dive_date;
/* if we have pre-existing trips, start on the last one */
trip = g_list_last(dive_trip_list);
if (trip)
dive_trip = DIVE_TRIP(trip);
treestore = GTK_TREE_STORE(dive_list.treemodel); treestore = GTK_TREE_STORE(dive_list.treemodel);
liststore = GTK_TREE_STORE(dive_list.listmodel); liststore = GTK_TREE_STORE(dive_list.listmodel);
i = dive_table.nr; i = dive_table.nr;
while (--i >= 0) { while (--i >= 0) {
struct dive *dive = dive_table.dives[i]; struct dive *dive = get_dive(i);
if (new_group(dive, &last_dive, &dive_date)) /* make sure we display the first date of the trip in previous summary */
{ if (dive_trip && parent_ptr) {
/* make sure we display the first date of the trip in previous summary */ gtk_tree_store_set(treestore, parent_ptr,
if (last_trip_dive) DIVE_NR, dive_trip->number,
gtk_tree_store_set(treestore, &parent_iter, DIVE_DATE, dive_trip->when,
DIVE_NR, group_size, DIVE_LOCATION, dive_trip->location,
DIVE_DATE, last_trip_dive->when,
DIVE_LOCATION, last_location,
-1); -1);
gtk_tree_store_append(treestore, &parent_iter, NULL);
gtk_tree_store_set(treestore, &parent_iter,
DIVE_INDEX, -NEW_TRIP,
DIVE_NR, 1,
DIVE_TEMPERATURE, 0,
DIVE_SAC, 0,
-1);
group_size = 0;
/* This might be NULL */
last_location = dive->location;
} }
group_size++; /* tripflag defines how dives are handled;
last_trip_dive = dive; * TF_NONE "not handled yet" - create time based group if autogroup == TRUE
if (dive->location) * NO_TRIP "set as no group" - simply leave at top level
last_location = dive->location; * IN_TRIP "use the trip with the largest trip time (when) that is <= this dive"
*/
if (UNGROUPED_DIVE(dive)) {
/* first dives that go to the top level */
parent_ptr = NULL;
dive_trip = NULL;
} else if (autogroup && !DIVE_IN_TRIP(dive)) {
if ( ! dive_trip || ! DIVE_FITS_TRIP(dive, dive_trip)) {
/* allocate new trip - all fields default to 0
and get filled in further down */
dive_trip = alloc_dive();
dive_trip_list = INSERT_TRIP(dive_trip, dive_trip_list);
trip = FIND_TRIP(dive_trip, dive_trip_list);
}
} else { /* either the dive has a trip or we aren't creating trips */
if (! (trip && DIVE_FITS_TRIP(dive, DIVE_TRIP(trip)))) {
GList *last_trip = trip;
trip = PREV_TRIP(trip, dive_trip_list);
if (! (trip && DIVE_FITS_TRIP(dive, DIVE_TRIP(trip)))) {
/* we could get here if there are no trips in the XML file
* and we aren't creating trips, either.
* Otherwise we need to create a new trip */
if (autogroup) {
dive_trip = alloc_dive();
dive_trip_list = INSERT_TRIP(dive_trip, dive_trip_list);
trip = FIND_TRIP(dive_trip, dive_trip_list);
} else {
/* let's go back to the last valid trip */
trip = last_trip;
}
} else {
dive_trip = trip->data;
dive_trip->number = 0;
}
}
}
/* update dive_trip to include this dive, increase number of dives in
the trip and update location if necessary */
if (dive_trip) {
dive->tripflag = IN_TRIP;
dive_trip->number++;
dive_trip->when = dive->when;
if (!dive_trip->location && dive->location)
dive_trip->location = dive->location;
if (dive_trip != last_trip) {
last_trip = dive_trip;
/* create trip entry */
gtk_tree_store_append(treestore, &parent_iter, NULL);
parent_ptr = &parent_iter;
/* a duration of 0 (and negative index) identifies a group */
gtk_tree_store_set(treestore, parent_ptr,
DIVE_INDEX, -1,
DIVE_NR, dive_trip->number,
DIVE_DATE, dive_trip->when,
DIVE_LOCATION, dive_trip->location,
DIVE_DURATION, 0,
-1);
}
}
/* store dive */
update_cylinder_related_info(dive); update_cylinder_related_info(dive);
gtk_tree_store_append(treestore, &iter, &parent_iter); gtk_tree_store_append(treestore, &iter, parent_ptr);
gtk_tree_store_set(treestore, &iter, gtk_tree_store_set(treestore, &iter,
DIVE_INDEX, i, DIVE_INDEX, i,
DIVE_NR, dive->number, DIVE_NR, dive->number,
@ -974,13 +1005,12 @@ static void fill_dive_list(void)
} }
/* make sure we display the first date of the trip in previous summary */ /* make sure we display the first date of the trip in previous summary */
if (last_trip_dive) if (parent_ptr && dive_trip)
gtk_tree_store_set(treestore, &parent_iter, gtk_tree_store_set(treestore, parent_ptr,
DIVE_NR, group_size, DIVE_NR, dive_trip->number,
DIVE_DATE, last_trip_dive->when, DIVE_DATE, dive_trip->when,
DIVE_LOCATION, last_location, DIVE_LOCATION, dive_trip->location,
-1); -1);
update_dive_list_units(); update_dive_list_units();
if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(dive_list.model), &iter)) { if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(dive_list.model), &iter)) {
GtkTreeSelection *selection; GtkTreeSelection *selection;

View file

@ -389,6 +389,7 @@ OPTIONCALLBACK(temperature_toggle, visible_cols.temperature)
OPTIONCALLBACK(totalweight_toggle, visible_cols.totalweight) OPTIONCALLBACK(totalweight_toggle, visible_cols.totalweight)
OPTIONCALLBACK(suit_toggle, visible_cols.suit) OPTIONCALLBACK(suit_toggle, visible_cols.suit)
OPTIONCALLBACK(cylinder_toggle, visible_cols.cylinder) OPTIONCALLBACK(cylinder_toggle, visible_cols.cylinder)
OPTIONCALLBACK(autogroup_toggle, autogroup)
static void event_toggle(GtkWidget *w, gpointer _data) static void event_toggle(GtkWidget *w, gpointer _data)
{ {
@ -484,8 +485,22 @@ static void preferences_dialog(GtkWidget *w, gpointer data)
gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6); gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(suit_toggle), NULL); g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(suit_toggle), NULL);
frame = gtk_frame_new("Divelist Font");
gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), frame, FALSE, FALSE, 5);
font = gtk_font_button_new_with_font(divelist_font); font = gtk_font_button_new_with_font(divelist_font);
gtk_box_pack_start(GTK_BOX(vbox), font, FALSE, FALSE, 5); gtk_container_add(GTK_CONTAINER(frame),font);
frame = gtk_frame_new("Misc. Options");
gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), frame, FALSE, FALSE, 5);
box = gtk_hbox_new(FALSE, 6);
gtk_container_add(GTK_CONTAINER(frame), box);
button = gtk_check_button_new_with_label("Automatically group dives in trips");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), autogroup);
gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(autogroup_toggle), NULL);
gtk_widget_show_all(dialog); gtk_widget_show_all(dialog);
result = gtk_dialog_run(GTK_DIALOG(dialog)); result = gtk_dialog_run(GTK_DIALOG(dialog));
@ -514,6 +529,7 @@ static void preferences_dialog(GtkWidget *w, gpointer data)
subsurface_set_conf("SAC", PREF_BOOL, BOOL_TO_PTR(visible_cols.sac)); subsurface_set_conf("SAC", PREF_BOOL, BOOL_TO_PTR(visible_cols.sac));
subsurface_set_conf("OTU", PREF_BOOL, BOOL_TO_PTR(visible_cols.otu)); subsurface_set_conf("OTU", PREF_BOOL, BOOL_TO_PTR(visible_cols.otu));
subsurface_set_conf("divelist_font", PREF_STRING, divelist_font); subsurface_set_conf("divelist_font", PREF_STRING, divelist_font);
subsurface_set_conf("autogroup", PREF_BOOL, BOOL_TO_PTR(autogroup));
/* Flush the changes out to the system */ /* Flush the changes out to the system */
subsurface_flush_conf(); subsurface_flush_conf();
@ -794,6 +810,8 @@ void init_ui(int *argcp, char ***argvp)
divelist_font = subsurface_get_conf("divelist_font", PREF_STRING); divelist_font = subsurface_get_conf("divelist_font", PREF_STRING);
autogroup = PTR_TO_BOOL(subsurface_get_conf("autogroup", PREF_BOOL));
default_dive_computer_vendor = subsurface_get_conf("dive_computer_vendor", PREF_STRING); default_dive_computer_vendor = subsurface_get_conf("dive_computer_vendor", PREF_STRING);
default_dive_computer_product = subsurface_get_conf("dive_computer_product", PREF_STRING); default_dive_computer_product = subsurface_get_conf("dive_computer_product", PREF_STRING);
default_dive_computer_device = subsurface_get_conf("dive_computer_device", PREF_STRING); default_dive_computer_device = subsurface_get_conf("dive_computer_device", PREF_STRING);

View file

@ -39,6 +39,11 @@ void record_dive(struct dive *dive)
dive_table.nr = nr+1; dive_table.nr = nr+1;
} }
void record_trip(struct dive *trip)
{
dive_trip_list = INSERT_TRIP(trip, dive_trip_list);
}
static void delete_dive_renumber(struct dive **dives, int i, int nr) static void delete_dive_renumber(struct dive **dives, int i, int nr)
{ {
struct dive *dive = dives[i]; struct dive *dive = dives[i];
@ -156,7 +161,7 @@ const struct units IMPERIAL_units = {
/* /*
* Dive info as it is being built up.. * Dive info as it is being built up..
*/ */
static struct dive *cur_dive; static struct dive *cur_dive, *cur_trip = NULL;
static struct sample *cur_sample; static struct sample *cur_sample;
static struct { static struct {
int active; int active;
@ -535,6 +540,17 @@ static void get_index(char *buffer, void *_i)
free(buffer); free(buffer);
} }
static void get_tripflag(char *buffer, void *_tf)
{
tripflag_t *tf = _tf;
tripflag_t i;
*tf = TF_NONE;
for (i = NO_TRIP; i < NUM_TRIPFLAGS; i++)
if(! strcmp(buffer, tripflag_names[i]))
*tf = i;
}
static void centibar(char *buffer, void *_pressure) static void centibar(char *buffer, void *_pressure)
{ {
pressure_t *pressure = _pressure; pressure_t *pressure = _pressure;
@ -1062,6 +1078,8 @@ static void try_to_fill_dive(struct dive **divep, const char *name, char *buf)
if (MATCH(".number", get_index, &dive->number)) if (MATCH(".number", get_index, &dive->number))
return; return;
if (MATCH(".tripflag", get_tripflag, &dive->tripflag))
return;
if (MATCH(".date", divedate, &dive->when)) if (MATCH(".date", divedate, &dive->when))
return; return;
if (MATCH(".time", divetime, &dive->when)) if (MATCH(".time", divetime, &dive->when))
@ -1138,6 +1156,27 @@ static void try_to_fill_dive(struct dive **divep, const char *name, char *buf)
nonmatch("dive", name, buf); 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(struct dive **divep, const char *name, char *buf)
{
int len = strlen(name);
start_match("trip", name, buf);
struct dive *dive = *divep;
if (MATCH(".date", divedate, &dive->when)) {
dive->when = utc_mktime(&cur_tm);
return;
}
if (MATCH(".location", utf8_string, &dive->location))
return;
if (MATCH(".notes", utf8_string, &dive->notes))
return;
nonmatch("trip", name, buf);
}
/* /*
* File boundaries are dive boundaries. But sometimes there are * File boundaries are dive boundaries. But sometimes there are
* multiple dives per file, so there can be other events too that * multiple dives per file, so there can be other events too that
@ -1162,6 +1201,22 @@ static void dive_end(void)
cur_ws_index = 0; cur_ws_index = 0;
} }
static void trip_start(void)
{
if (cur_trip)
return;
cur_trip = alloc_dive();
memset(&cur_tm, 0, sizeof(cur_tm));
}
static void trip_end(void)
{
if (!cur_trip)
return;
record_trip(cur_trip);
cur_trip = NULL;
}
static void event_start(void) static void event_start(void)
{ {
memset(&cur_event, 0, sizeof(cur_event)); memset(&cur_event, 0, sizeof(cur_event));
@ -1225,6 +1280,10 @@ static void entry(const char *name, int size, const char *raw)
try_to_fill_sample(cur_sample, name, buf); try_to_fill_sample(cur_sample, name, buf);
return; return;
} }
if (cur_trip) {
try_to_fill_trip(&cur_trip, name, buf);
return;
}
if (cur_dive) { if (cur_dive) {
try_to_fill_dive(&cur_dive, name, buf); try_to_fill_dive(&cur_dive, name, buf);
return; return;
@ -1350,6 +1409,7 @@ static struct nesting {
} nesting[] = { } nesting[] = {
{ "dive", dive_start, dive_end }, { "dive", dive_start, dive_end },
{ "Dive", dive_start, dive_end }, { "Dive", dive_start, dive_end },
{ "trip", trip_start, trip_end },
{ "sample", sample_start, sample_end }, { "sample", sample_start, sample_end },
{ "waypoint", sample_start, sample_end }, { "waypoint", sample_start, sample_end },
{ "SAMPLE", sample_start, sample_end }, { "SAMPLE", sample_start, sample_end },

View file

@ -97,6 +97,12 @@ static void quote(FILE *f, const char *text)
case '&': case '&':
escape = "&amp;"; escape = "&amp;";
break; break;
case '\'':
escape = "&apos;";
break;
case '\"':
escape = "&quot;";
break;
} }
fwrite(text, (p - text - 1), 1, f); fwrite(text, (p - text - 1), 1, f);
if (!escape) if (!escape)
@ -275,6 +281,18 @@ static void save_events(FILE *f, struct event *ev)
} }
} }
static void save_trip(FILE *f, struct dive *trip)
{
struct tm *tm = gmtime(&trip->when);
fprintf(f, "<trip");
fprintf(f, " date='%04u-%02u-%02u'",
tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday);
if (trip->location)
show_utf8(f, trip->location, " location=\'","\'");
fprintf(f, " />\n");
}
static void save_dive(FILE *f, struct dive *dive) static void save_dive(FILE *f, struct dive *dive)
{ {
int i; int i;
@ -283,6 +301,8 @@ static void save_dive(FILE *f, struct dive *dive)
fputs("<dive", f); fputs("<dive", f);
if (dive->number) if (dive->number)
fprintf(f, " number='%d'", dive->number); fprintf(f, " number='%d'", dive->number);
if (dive->tripflag != TF_NONE)
fprintf(f, " tripflag='%s'", tripflag_names[dive->tripflag]);
if (dive->rating) if (dive->rating)
fprintf(f, " rating='%d'", dive->rating); fprintf(f, " rating='%d'", dive->rating);
fprintf(f, " date='%04u-%02u-%02u'", fprintf(f, " date='%04u-%02u-%02u'",
@ -305,6 +325,8 @@ static void save_dive(FILE *f, struct dive *dive)
void save_dives(const char *filename) void save_dives(const char *filename)
{ {
int i; int i;
GList *trip = NULL;
FILE *f = fopen(filename, "w"); FILE *f = fopen(filename, "w");
if (!f) if (!f)
@ -314,6 +336,12 @@ void save_dives(const char *filename)
update_dive(current_dive); update_dive(current_dive);
fprintf(f, "<dives>\n<program name='subsurface' version='%d'></program>\n", VERSION); fprintf(f, "<dives>\n<program name='subsurface' version='%d'></program>\n", VERSION);
/* save the trips */
while ((trip = NEXT_TRIP(trip, dive_trip_list)) != 0)
save_trip(f, trip->data);
/* save the dives */
for (i = 0; i < dive_table.nr; i++) for (i = 0; i < dive_table.nr; i++)
save_dive(f, get_dive(i)); save_dive(f, get_dive(i));
fprintf(f, "</dives>\n"); fprintf(f, "</dives>\n");