/*
This file is part of darktable,
Copyright (C) 2012-2026 darktable developers.
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see .
*/
#include "common/gdk_event_utils.h"
#include "common/debug.h"
#include "common/file_location.h"
#include "common/image_cache.h"
#include "common/collection.h"
#include "common/selection.h"
#include "common/gpx.h"
#include "common/geo.h"
#include "common/datetime.h"
#include "control/conf.h"
#include "dtgtk/button.h"
#include "control/jobs.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "libs/lib_api.h"
#ifdef HAVE_MAP
#include "views/view.h"
#endif
#ifdef GDK_WINDOWING_QUARTZ
#include "osx/osx.h"
#endif
#include
DT_MODULE(1)
typedef struct tz_tuple_t
{
char *name, *display;
} tz_tuple_t;
#define DT_GEOTAG_PARTS_NB 7
typedef struct dt_lib_datetime_t
{
GtkWidget *widget[DT_GEOTAG_PARTS_NB];
GtkWidget *sign;
} dt_lib_datetime_t;
#ifdef HAVE_MAP
typedef struct dt_lib_tracks_data_t
{
GObject *track;
dt_map_box_t map_box;
} dt_lib_tracks_data_t;
typedef struct dt_lib_tracks_t
{
dt_lib_tracks_data_t td[1];
} dt_lib_tracks_t;
typedef enum dt_tracks_cols_t
{
DT_GEO_TRACKS_ACTIVE = 0, // active / deactivated track
DT_GEO_TRACKS_DATETIME, // displayed start datetime
DT_GEO_TRACKS_POINTS, // nb points
DT_GEO_TRACKS_IMAGES, // nb images
DT_GEO_TRACKS_SEGID, // id track segment
DT_GEO_TRACKS_TOOLTIP, // datetime details
DT_GEO_TRACKS_NUM_COLS
} dt_tracks_cols_t;
#endif
typedef struct dt_lib_geotagging_t
{
dt_lib_datetime_t dt;
dt_lib_datetime_t dt0;
dt_lib_datetime_t of;
GDateTime *datetime;
GDateTime *datetime0;
GTimeSpan offset;
gboolean editing;
dt_imgid_t imgid;
GList* imgs;
int nb_imgs;
GtkWidget *apply_offset;
GtkWidget *lock_offset;
GtkWidget *apply_datetime;
GtkWidget *timezone;
GList *timezones;
GtkWidget *timezone_changed;
GtkWidget *gpx_button;
GTimeZone *tz_camera;
#ifdef HAVE_MAP
struct
{
gboolean view;
GtkWidget *gpx_button, *gpx_file, *gpx_view;
struct dt_gpx_t *gpx;
dt_lib_tracks_t *tracks;
dt_map_box_t map_box;
int nb_tracks, nb_imgs;
GtkWidget *gpx_section, *preview_button, *apply_gpx_button,
*select_button, *nb_imgs_label;
GtkTreeViewColumn *sel_tracks;
} map;
#endif
} dt_lib_geotagging_t;
typedef struct dt_sel_img_t
{
dt_imgid_t imgid;
uint32_t segid;
gchar dt[DT_DATETIME_LENGTH];
gboolean counted;
dt_image_geoloc_t gl;
GObject *image;
} dt_sel_img_t;
static void _datetime_entry_changed(GtkWidget *entry, dt_lib_module_t *self);
static void _setup_selected_images_list(dt_lib_module_t *self);
static void free_tz_tuple(gpointer data)
{
tz_tuple_t *tz_tuple = (tz_tuple_t *)data;
g_free(tz_tuple->display);
#ifdef _WIN32
g_free(tz_tuple->name); // on non-Windows both point to the same string
#endif
free(tz_tuple);
}
const char *name(dt_lib_module_t *self)
{
return _("geotagging");
}
const char *description(dt_lib_module_t *self)
{
return _("set geolocation information for\n"
"the currently selected images");
}
dt_view_type_flags_t views(dt_lib_module_t *self)
{
#ifdef HAVE_MAP
return DT_VIEW_LIGHTTABLE | DT_VIEW_MAP;
#else
return DT_VIEW_LIGHTTABLE;
#endif
}
uint32_t container(dt_lib_module_t *self)
{
return DT_UI_CONTAINER_PANEL_RIGHT_CENTER;
}
int position(const dt_lib_module_t *self)
{
return 450;
}
// modify the datetime_taken field in the db/cache of selected images
static void _apply_offset_callback(GtkWidget *widget, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(d->offset)
dt_control_datetime(d->offset, NULL, NULL);
}
// modify the datetime_taken field in the db/cache of selected images
static void _apply_datetime_callback(GtkWidget *widget, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(d->datetime)
{
char dt[DT_DATETIME_LENGTH];
dt_datetime_gdatetime_to_exif(dt, sizeof(dt), d->datetime);
dt_control_datetime(0, dt, NULL);
}
}
static gboolean _lib_geotagging_filter_gpx(const GtkFileFilterInfo *filter_info, gpointer data)
{
if(!g_ascii_strcasecmp(filter_info->mime_type, "application/gpx+xml")) return TRUE;
const gchar *filename = filter_info->filename;
const char *cc = filename + strlen(filename);
for(; *cc != '.' && cc > filename; cc--)
;
if(!g_ascii_strcasecmp(cc, ".gpx")) return TRUE;
return FALSE;
}
static GtkWidget *_set_up_label(const char *name, const int align, GtkWidget *grid,
const int col, const int line, const int ellipsize)
{
GtkWidget *label = gtk_label_new(name);
gtk_label_set_ellipsize(GTK_LABEL(label), ellipsize);
if(ellipsize != PANGO_ELLIPSIZE_NONE)
gtk_widget_set_visible(label, TRUE);
gtk_widget_set_halign(label, align);
gtk_widget_set_hexpand(label, TRUE);
gtk_grid_attach(GTK_GRID(grid), label, col, line, 1, 1);
return label;
}
static gchar *_utc_timeval_to_localtime_text(GDateTime *utc_dt, GTimeZone *tz_camera,
const gboolean full)
{
GDateTime *local_dt = g_date_time_to_timezone(utc_dt, tz_camera);
gchar *dts = g_date_time_format(local_dt, full ? "%Y:%m:%d %H:%M:%S" : "%H:%M:%S");
g_date_time_unref(local_dt);
return dts;
}
static GDateTime *_localtime_text_to_utc_timeval(const char *date_time,
GTimeZone *tz_camera, GTimeZone *tz_utc,
GTimeSpan offset)
{
GDateTime *exif_time = dt_datetime_exif_to_gdatetime(date_time, tz_camera);
GDateTime *dt_offset = g_date_time_add(exif_time, offset);
GDateTime *utc_time = g_date_time_to_timezone(dt_offset, tz_utc);
g_date_time_unref(exif_time);
g_date_time_unref(dt_offset);
return utc_time;
}
static int _count_images_per_track(dt_gpx_track_segment_t *t, dt_gpx_track_segment_t *n,
dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
int nb_imgs = 0;
for(GList *i = d->imgs; i; i = g_list_next(i))
{
dt_sel_img_t *im = i->data;
if(im->segid == -1)
{
GDateTime *dt = _localtime_text_to_utc_timeval(im->dt, d->tz_camera, darktable.utc_tz, d->offset);
if((g_date_time_compare(dt, t->start_dt) >= 0
&& g_date_time_compare(dt, t->end_dt) <= 0)
|| (n && g_date_time_compare(dt, t->end_dt) >= 0
&& g_date_time_compare(dt, n->start_dt) <= 0))
{
nb_imgs++;
im->segid = t->id;
}
g_date_time_unref(dt);
}
}
return nb_imgs;
}
#ifdef HAVE_MAP
static gchar *_utc_timeval_to_utc_text(GDateTime *utc_dt, const gboolean full)
{
gchar *dts = g_date_time_format(utc_dt, full ? "%Y:%m:%d %H:%M:%S" : "%H:%M:%S");
return dts;
}
static gchar *_datetime_tooltip(GDateTime *start, GDateTime *end, GTimeZone *tz)
{
gchar *dtsl = _utc_timeval_to_localtime_text(start, tz, FALSE);
gchar *dtel = _utc_timeval_to_localtime_text(end, tz, FALSE);
gchar *dtsu = _utc_timeval_to_utc_text(start, FALSE);
gchar *dteu = _utc_timeval_to_utc_text(end, FALSE);
gchar *res = g_strdup_printf("%s -> %s LT\n%s -> %s UTC", dtsl, dtel, dtsu, dteu);
g_free(dtsl);
g_free(dtel);
g_free(dtsu);
g_free(dteu);
return res;
}
static void _remove_images_from_map(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
for(GList *i = d->imgs; i; i = g_list_next(i))
{
dt_sel_img_t *im = i->data;
if(im->image)
{
dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_THUMB, im->image);
im->image = NULL;
}
}
}
static void _refresh_images_displayed_on_track(const int segid, const gboolean active, dt_lib_module_t *self)
{
dt_gui_cursor_set_busy();
dt_lib_geotagging_t *d = self->data;
for(GList *i = d->imgs; i; i = g_list_next(i))
{
dt_sel_img_t *im = i->data;
if(im->segid == segid && active)
{
GDateTime *dt = _localtime_text_to_utc_timeval(im->dt, d->tz_camera, darktable.utc_tz, d->offset);
if(!dt_gpx_get_location(d->map.gpx, dt, &im->gl))
im->gl.latitude = NAN;
g_date_time_unref(dt);
}
else if(im->segid == segid && !active && im->image)
{
dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_THUMB, im->image);
im->image = NULL;
im->gl.latitude = NAN;
}
}
int count = 0;
for(GList *i = d->imgs; active && i; i = g_list_next(i))
{
dt_sel_img_t *im = i->data;
if(im->segid == segid && im->gl.latitude != NAN)
{
count++;
dt_sel_img_t *next = i->next ? (dt_sel_img_t *)i->next->data
: NULL;
if(!im->image && (!next
|| !((next->gl.latitude == im->gl.latitude)
&& (next->gl.longitude == im->gl.longitude))))
{
struct {dt_imgid_t imgid; float latitude; float longitude; int count;} p;
p.imgid = im->imgid;
p.latitude = im->gl.latitude;
p.longitude = im->gl.longitude;
p.count = count == 1 ? 0 : count;
GList *img = g_list_prepend(NULL, &p);
im->image = dt_view_map_add_marker(darktable.view_manager, MAP_DISPLAY_THUMB, img);
g_list_free(img);
count = 0;
}
}
}
dt_gui_cursor_clear_busy();
}
static void _update_nb_images(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
int nb_imgs = 0;
for(int segid = 0; valid && segid < d->map.nb_tracks; segid++)
{
gboolean active;
int nb;
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active,
DT_GEO_TRACKS_IMAGES, &nb, -1);
if(active)
nb_imgs += nb;
valid = gtk_tree_model_iter_next(model, &iter);
}
d->map.nb_imgs = nb_imgs;
gchar *nb = g_strdup_printf("%d/%d", nb_imgs, d->nb_imgs);
gtk_label_set_text(GTK_LABEL(d->map.nb_imgs_label), nb);
g_free(nb);
}
static void _update_buttons(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
gtk_widget_set_sensitive(d->map.preview_button, d->map.nb_tracks);
GtkWidget *label = gtk_bin_get_child(GTK_BIN(d->map.apply_gpx_button));
gtk_label_set_text(GTK_LABEL(label), d->offset ? _("apply offset and geo-location")
: _("apply geo-location"));
gtk_widget_set_tooltip_text(d->map.apply_gpx_button,
d->offset ? _("apply offset and geo-location to matching images"
"\ndouble operation: two ctrl+z to undo")
: _("apply geo-location to matching images"));
gtk_widget_set_sensitive(d->map.apply_gpx_button, d->map.nb_imgs);
gtk_widget_set_sensitive(d->map.select_button,
d->map.nb_imgs && d->map.nb_imgs != d->nb_imgs);
}
static GList *_get_images_on_active_tracks(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
int segid = 0;
GList *imgs = NULL;
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
while(valid)
{
gboolean active;
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1);
if(active)
{
for(GList *i = d->imgs; i; i = g_list_next(i))
{
dt_sel_img_t *im = i->data;
if(im->segid == segid)
imgs = g_list_prepend(imgs, GINT_TO_POINTER(im->imgid));
}
}
valid = gtk_tree_model_iter_next(model, &iter);
segid++;
}
return imgs;
}
static void _refresh_displayed_images(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
const gboolean preview = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->map.preview_button));
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
for(int segid = 0; valid && segid < d->map.nb_tracks; segid++)
{
gboolean active;
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1);
_refresh_images_displayed_on_track(segid, active && preview, self);
valid = gtk_tree_model_iter_next(model, &iter);
}
}
static gboolean _update_map_box(const guint segid, GList *pts, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
// box for this track
if(pts)
{
d->map.tracks->td[segid].map_box.lon1 = 180.0;
d->map.tracks->td[segid].map_box.lon2 = -180.0;
d->map.tracks->td[segid].map_box.lat1 = -90.0;
d->map.tracks->td[segid].map_box.lat2 = 90.0;
for(GList *pt = pts; pt; pt = g_list_next(pt))
{
dt_geo_map_display_point_t *p = pt->data;
if(p->lon < d->map.tracks->td[segid].map_box.lon1)
d->map.tracks->td[segid].map_box.lon1 = MAX(-180.0, p->lon);
if(p->lon > d->map.tracks->td[segid].map_box.lon2)
d->map.tracks->td[segid].map_box.lon2 = MIN(180.0, p->lon);
if(p->lat > d->map.tracks->td[segid].map_box.lat1)
d->map.tracks->td[segid].map_box.lat1 = MIN(90.0, p->lat);
if(p->lat < d->map.tracks->td[segid].map_box.lat2)
d->map.tracks->td[segid].map_box.lat2 = MAX(-90.0, p->lat);
}
}
// box for all tracks
float lon1 = 180.0;
float lon2 = -180.0;
float lat1 = -90.0;
float lat2 = 90.0;
for(int i = 0; i < d->map.nb_tracks; i++)
{
if(d->map.tracks->td[i].track)
{
if(d->map.tracks->td[i].map_box.lon1 < lon1)
lon1 = d->map.tracks->td[i].map_box.lon1;
if(d->map.tracks->td[i].map_box.lon2 > lon2)
lon2 = d->map.tracks->td[i].map_box.lon2;
if(d->map.tracks->td[i].map_box.lat1 > lat1)
lat1 = d->map.tracks->td[i].map_box.lat1;
if(d->map.tracks->td[i].map_box.lat2 < lat2)
lat2 = d->map.tracks->td[i].map_box.lat2;
}
}
const gboolean grow = lon1 < d->map.map_box.lon1 || lon2 > d->map.map_box.lon1 ||
lat1 > d->map.map_box.lat1 || lat2 < d->map.map_box.lat2;
d->map.map_box.lon1 = lon1;
d->map.map_box.lon2 = lon2;
d->map.map_box.lat1 = lat1;
d->map.map_box.lat2 = lat2;
return grow;
}
static void _remove_tracks_from_map(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(d->map.tracks)
{
for(int i = 0; i < d->map.nb_tracks; i++)
{
if(d->map.tracks->td[i].track)
{
dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_TRACK,
d->map.tracks->td[i].track);
d->map.tracks->td[i].track = NULL;
}
}
g_free(d->map.tracks);
d->map.tracks = NULL;
}
if(d->map.gpx)
{
dt_gpx_destroy(d->map.gpx);
d->map.gpx = NULL;
}
}
GdkRGBA color[] = {(GdkRGBA){.red = 1.0, .green = 0.0, .blue = 0.0, .alpha = 0.5 },
(GdkRGBA){.red = 0.0, .green = 1.0, .blue = 1.0, .alpha = 0.5 },
(GdkRGBA){.red = 0.0, .green = 0.0, .blue = 1.0, .alpha = 0.5 },
(GdkRGBA){.red = 1.0, .green = 1.0, .blue = 0.0, .alpha = 0.5 },
(GdkRGBA){.red = 0.0, .green = 1.0, .blue = 0.0, .alpha = 0.5 },
(GdkRGBA){.red = 1.0, .green = 0.0, .blue = 1.0, .alpha = 0.5 }};
static gboolean _refresh_display_track(const gboolean active, const int segid, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
gboolean grow = FALSE;
if(active)
{
GList *pts = dt_gpx_get_trkpts(d->map.gpx, segid);
if(!d->map.tracks->td[segid].track)
d->map.tracks->td[segid].track = dt_view_map_add_marker(darktable.view_manager,
MAP_DISPLAY_TRACK, pts);
osm_gps_map_track_set_color((OsmGpsMapTrack *)d->map.tracks->td[segid].track, &color[segid % 6]);
grow = _update_map_box(segid, pts, self);
g_list_free_full(pts, g_free);
}
else
{
if(d->map.tracks->td[segid].track != NULL)
dt_view_map_remove_marker(darktable.view_manager, MAP_DISPLAY_TRACK,
d->map.tracks->td[segid].track);
d->map.tracks->td[segid].track = NULL;
_update_map_box(segid, NULL, self);
}
return grow;
}
static void _refresh_display_all_tracks(GtkWidget *widget, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
gboolean grow = FALSE;
for(int segid = 0; valid && segid < d->map.nb_tracks; segid++)
{
gboolean active;
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1);
grow = _refresh_display_track(active, segid, self) ? TRUE : grow;
valid = gtk_tree_model_iter_next(model, &iter);
}
if(grow)
{
dt_view_map_center_on_bbox(darktable.view_manager, d->map.map_box.lon1, d->map.map_box.lat1,
d->map.map_box.lon2, d->map.map_box.lat2);
}
_refresh_displayed_images(self);
}
static void _click_for_entire_track_pressed(GtkGestureSingle *gesture,
int n_press,
double x,
double y,
dt_lib_module_t *self)
{
if(n_press == 2)
{
_refresh_display_all_tracks(NULL, self);
}
}
static void _track_seg_toggled(GtkCellRendererToggle *cell_renderer, gchar *path_str, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
GtkTreePath *path = gtk_tree_path_new_from_string(path_str);
gboolean active;
uint32_t segid;
gtk_tree_model_get_iter(model, &iter, path);
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active,
DT_GEO_TRACKS_SEGID, &segid, -1);
gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_GEO_TRACKS_ACTIVE, !active, -1);
gtk_tree_path_free(path);
active = !active;
if(_refresh_display_track(active, segid, self))
{
dt_view_map_center_on_bbox(darktable.view_manager, d->map.map_box.lon1, d->map.map_box.lat1,
d->map.map_box.lon2, d->map.map_box.lat2);
}
const gboolean preview = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->map.preview_button));
_refresh_images_displayed_on_track(segid, active && preview, self);
_update_nb_images(self);
_update_buttons(self);
}
static void _all_tracks_toggled(GtkTreeViewColumn *column, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkWidget *toggle = gtk_tree_view_column_get_widget(column);
gboolean active = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), active);
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
gboolean grow = FALSE;
for(int segid = 0; valid && segid < d->map.nb_tracks; segid++)
{
gtk_list_store_set(GTK_LIST_STORE(model), &iter,
DT_GEO_TRACKS_ACTIVE, active,-1);
grow = _refresh_display_track(active, segid, self) ? TRUE : grow;
valid = gtk_tree_model_iter_next(model, &iter);
}
if(active && grow)
{
dt_view_map_center_on_bbox(darktable.view_manager, d->map.map_box.lon1, d->map.map_box.lat1,
d->map.map_box.lon2, d->map.map_box.lat2);
}
_refresh_displayed_images(self);
_update_nb_images(self);
_update_buttons(self);
}
static void _select_images(GtkWidget *widget, dt_lib_module_t *self)
{
GList *imgs = _get_images_on_active_tracks(self);
dt_selection_clear(darktable.selection);
dt_selection_select_list(darktable.selection, imgs);
g_list_free(imgs);
}
static void _images_preview_toggled(GtkToggleButton *button, dt_lib_module_t *self)
{
_refresh_displayed_images(self);
}
static void _refresh_track_list(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(!d->map.gpx) return;
GList *trkseg = dt_gpx_get_trkseg(d->map.gpx);
_remove_images_from_map(self);
for(GList *i = d->imgs; i; i = g_list_next(i))
((dt_sel_img_t *)i->data)->segid = -1;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
GtkTreeIter iter;
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
for(GList *ts = trkseg; ts && valid; ts = g_list_next(ts))
{
dt_gpx_track_segment_t *t = ts->data;
gchar *dts = _utc_timeval_to_localtime_text(t->start_dt, d->tz_camera, TRUE);
const int nb_imgs = _count_images_per_track(t, ts->next ? ts->next->data : NULL, self);
gboolean active;
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_ACTIVE, &active, -1);
gchar *tooltip = _datetime_tooltip(t->start_dt, t->end_dt, d->tz_camera);
gtk_list_store_set(GTK_LIST_STORE(model), &iter,
DT_GEO_TRACKS_DATETIME, dts,
DT_GEO_TRACKS_POINTS, t->nb_trkpt,
DT_GEO_TRACKS_IMAGES, nb_imgs,
DT_GEO_TRACKS_TOOLTIP, tooltip,
-1);
g_free(dts);
g_free(tooltip);
valid = gtk_tree_model_iter_next(model, &iter);
}
_update_nb_images(self);
_refresh_displayed_images(self);
_update_buttons(self);
}
static void _show_gpx_tracks(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
_remove_tracks_from_map(self);
d->map.gpx = dt_gpx_new(gtk_label_get_text(GTK_LABEL(d->map.gpx_file)));
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->map.gpx_view));
g_object_ref(model);
gtk_tree_view_set_model(GTK_TREE_VIEW(d->map.gpx_view), NULL);
gtk_list_store_clear(GTK_LIST_STORE(model));
GList *trkseg = dt_gpx_get_trkseg(d->map.gpx);
d->map.nb_tracks = g_list_length(trkseg);
d->map.tracks = g_try_malloc0(sizeof(dt_lib_tracks_data_t) * d->map.nb_tracks);
if(!d->map.tracks)
{
d->map.nb_tracks = 0;
dt_print(DT_DEBUG_ALWAYS, "[geotagging] unable to allocate storage for tracks");
}
_remove_images_from_map(self);
for(GList *i = d->imgs; i; i = g_list_next(i))
((dt_sel_img_t *)i->data)->segid = -1;
int segid = 0;
const gboolean active = gtk_toggle_button_get_active(
GTK_TOGGLE_BUTTON(gtk_tree_view_column_get_widget(d->map.sel_tracks)));
for(GList *ts = trkseg; ts; ts = g_list_next(ts))
{
dt_gpx_track_segment_t *t = ts->data;
gchar *dts = _utc_timeval_to_localtime_text(t->start_dt, d->tz_camera, TRUE);
gchar *tooltip = _datetime_tooltip(t->start_dt, t->end_dt, d->tz_camera);
const int nb_imgs = _count_images_per_track(t, ts->next ? ts->next->data : NULL, self);
gtk_list_store_insert_with_values(GTK_LIST_STORE(model), NULL, -1,
DT_GEO_TRACKS_ACTIVE, active,
DT_GEO_TRACKS_DATETIME, dts,
DT_GEO_TRACKS_POINTS, t->nb_trkpt,
DT_GEO_TRACKS_IMAGES, nb_imgs,
DT_GEO_TRACKS_SEGID, segid,
DT_GEO_TRACKS_TOOLTIP, tooltip,
-1);
segid++;
g_free(dts);
g_free(tooltip);
}
gtk_tree_view_set_model(GTK_TREE_VIEW(d->map.gpx_view), model);
g_object_unref(model);
gtk_tree_view_column_set_clickable(d->map.sel_tracks, TRUE);
_update_nb_images(self);
_update_buttons(self);
_refresh_display_all_tracks(NULL, self);
}
static void _apply_gpx(GtkWidget *widget, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
gchar *tz = dt_conf_get_string("plugins/lighttable/geotagging/tz");
GList *imgs = _get_images_on_active_tracks(self);
if(imgs)
{
if(d->offset)
{
GList *imgs2 = g_list_copy(imgs);
dt_control_datetime(d->offset, NULL, imgs2);
}
dt_control_gpx_apply(gtk_label_get_text(GTK_LABEL(d->map.gpx_file)), -1, tz, imgs);
}
g_free(tz);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->map.preview_button), FALSE);
}
static void _update_layout(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
gtk_widget_set_visible(d->gpx_button, !d->map.view);
gtk_widget_set_visible(d->map.gpx_section, d->map.view);
}
static void _view_changed(gpointer instance, dt_view_t *old_view,
dt_view_t *new_view, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(old_view != new_view)
{
d->map.view = !g_strcmp0(new_view->module_name, "map");
if(d->map.view)
{
_setup_selected_images_list(self);
_refresh_track_list(self);
}
_update_layout(self);
}
}
static void _geotag_changed(gpointer instance, GList *imgs, const int locid, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(d->map.view && !locid)
{
_refresh_displayed_images(self);
_update_nb_images(self);
_update_buttons(self);
}
}
static void _refresh_selected_images_datetime(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
for(GList *i = d->imgs; i; i = g_list_next(i))
{
dt_sel_img_t *img = i->data;
const dt_image_t *cimg = dt_image_cache_get(img->imgid, 'r');
if(!cimg) continue;
dt_datetime_img_to_exif(img->dt, sizeof(img->dt), cimg);
dt_image_cache_read_release(cimg);
}
}
static gboolean _row_tooltip_setup(GtkWidget *view, gint x, gint y, gboolean kb_mode,
GtkTooltip* tooltip, dt_lib_module_t *self)
{
gboolean res = FALSE;
GtkTreePath *path = NULL;
GtkTreeViewColumn *column = NULL;
// Get view path mouse position
gint tx, ty;
gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(view), x, y, &tx, &ty);
if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), tx, ty, &path, &column, NULL, NULL))
{
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
GtkTreeIter iter;
if(gtk_tree_model_get_iter(model, &iter, path))
{
char *text = NULL;
gtk_tree_model_get(model, &iter, DT_GEO_TRACKS_TOOLTIP, &text, -1);
if(text && text[0] != '\0')
{
gtk_tooltip_set_text(tooltip, text);
res = TRUE;
}
g_free(text);
}
}
gtk_tree_path_free(path);
return res;
}
#endif // HAVE_MAP
static void _preview_gpx_file(GtkWidget *widget, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("GPX file track segments"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_done"), GTK_RESPONSE_CANCEL, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
gchar *filedir = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
struct dt_gpx_t *gpx = dt_gpx_new(filedir);
g_free(filedir);
GtkWidget *grid = gtk_grid_new();
gtk_grid_set_column_spacing(GTK_GRID(grid), DT_PIXEL_APPLY_DPI(10));
int line = 0;
GList *trkseg = dt_gpx_get_trkseg(gpx);
_set_up_label(_("name"), GTK_ALIGN_START, grid, 0, line, PANGO_ELLIPSIZE_NONE);
_set_up_label(_("start time"), GTK_ALIGN_START, grid, 1, line, PANGO_ELLIPSIZE_NONE);
_set_up_label(_("end time"), GTK_ALIGN_START, grid, 2, line, PANGO_ELLIPSIZE_NONE);
_set_up_label(_("points"), GTK_ALIGN_CENTER, grid, 3, line, PANGO_ELLIPSIZE_NONE);
_set_up_label(_("images"), GTK_ALIGN_CENTER, grid, 4, line, PANGO_ELLIPSIZE_NONE);
for(GList *i = d->imgs; i; i = g_list_next(i))
((dt_sel_img_t *)i->data)->segid = -1;
int total_imgs = 0;
int total_pts = 0;
line++;
for(GList *ts = trkseg; ts; ts = g_list_next(ts))
{
dt_gpx_track_segment_t *t = ts->data;
gchar *dts = _utc_timeval_to_localtime_text(t->start_dt, d->tz_camera, TRUE);
gchar *dte = _utc_timeval_to_localtime_text(t->end_dt, d->tz_camera, TRUE);
const int nb_imgs = _count_images_per_track(t, ts->next ? ts->next->data : NULL,self);
total_imgs += nb_imgs;
_set_up_label(t->name, GTK_ALIGN_START, grid, 0, line, PANGO_ELLIPSIZE_NONE);
_set_up_label(dts, GTK_ALIGN_START, grid, 1, line, PANGO_ELLIPSIZE_NONE);
_set_up_label(dte, GTK_ALIGN_START, grid, 2, line, PANGO_ELLIPSIZE_NONE);
char *nb = g_strdup_printf("%u", t->nb_trkpt);
_set_up_label(nb, GTK_ALIGN_CENTER, grid, 3, line, PANGO_ELLIPSIZE_NONE);
g_free(nb);
nb = g_strdup_printf("%d", nb_imgs);
_set_up_label(nb, GTK_ALIGN_CENTER, grid, 4, line, PANGO_ELLIPSIZE_NONE);
g_free(nb);
line++;
total_pts += t->nb_trkpt;
g_free(dts);
g_free(dte);
}
char *nb = g_strdup_printf("%d", total_pts);
_set_up_label(nb, GTK_ALIGN_CENTER, grid, 3, line, PANGO_ELLIPSIZE_NONE);
g_free(nb);
nb = g_strdup_printf("%d / %d", total_imgs, d->nb_imgs);
_set_up_label(nb, GTK_ALIGN_CENTER, grid, 4, line, PANGO_ELLIPSIZE_NONE);
g_free(nb);
if(gpx)
{
dt_gpx_destroy(gpx);
gpx = NULL;
}
GtkWidget *w = dt_gui_scroll_wrap(grid);
gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(100));
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
dt_gui_dialog_add(GTK_DIALOG(dialog), w);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
gtk_widget_show_all(dialog);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
}
static void _setup_selected_images_list(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(d->imgs)
{
#ifdef HAVE_MAP
_remove_images_from_map(self);
#endif
g_list_free_full(d->imgs, g_free);
}
d->imgs = NULL;
d->nb_imgs = 0;
sqlite3_stmt *stmt;
DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db),
"SELECT imgid FROM main.selected_images",
-1, &stmt, NULL);
while(sqlite3_step(stmt) == SQLITE_ROW)
{
const dt_imgid_t imgid = sqlite3_column_int(stmt, 0);
const dt_image_t *cimg = dt_image_cache_get(imgid, 'r');
char dt[DT_DATETIME_LENGTH];
if(!cimg) continue;
dt_datetime_img_to_exif(dt, sizeof(dt), cimg);
dt_image_cache_read_release(cimg);
dt_sel_img_t *img = g_try_malloc0(sizeof(dt_sel_img_t));
if(!img) continue;
memcpy(img->dt, dt, DT_DATETIME_LENGTH);
img->imgid = imgid;
d->imgs = g_list_prepend(d->imgs, img);
d->nb_imgs++;
}
sqlite3_finalize(stmt);
}
static void _choose_gpx_callback(GtkWidget *widget, dt_lib_module_t *self)
{
// bring a filechooser to select the gpx file to apply to selection
dt_lib_geotagging_t *d = self->data;
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *filechooser = gtk_file_chooser_dialog_new(
_("open GPX file"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
_("preview"), GTK_RESPONSE_ACCEPT,
_("_cancel"), GTK_RESPONSE_CANCEL,
_("_open"), GTK_RESPONSE_OK, (char *)NULL);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(filechooser);
#endif
dt_conf_get_folder_to_file_chooser("ui_last/gpx_last_directory", GTK_FILE_CHOOSER(filechooser));
GtkFileFilter *filter;
filter = GTK_FILE_FILTER(gtk_file_filter_new());
gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME | GTK_FILE_FILTER_MIME_TYPE,
_lib_geotagging_filter_gpx, NULL, NULL);
gtk_file_filter_set_name(filter, _("GPS data exchange format"));
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);
filter = GTK_FILE_FILTER(gtk_file_filter_new());
gtk_file_filter_add_pattern(filter, "*");
gtk_file_filter_set_name(filter, _("all files"));
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);
if(!d->imgs)
_setup_selected_images_list(self);
int res = gtk_dialog_run(GTK_DIALOG(filechooser));
while(res == GTK_RESPONSE_ACCEPT)
{
_preview_gpx_file(filechooser, self);
res = gtk_dialog_run(GTK_DIALOG(filechooser));
}
if(res == GTK_RESPONSE_OK)
{
dt_conf_set_folder_from_file_chooser("ui_last/gpx_last_directory", GTK_FILE_CHOOSER(filechooser));
gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
#ifdef HAVE_MAP
if(d->map.view)
{
gtk_label_set_text(GTK_LABEL(d->map.gpx_file), filename);
_show_gpx_tracks(self);
gtk_widget_set_visible(d->map.gpx_view, d->map.view);
}
else
#endif
{
gchar *tz = dt_conf_get_string("plugins/lighttable/geotagging/tz");
dt_control_gpx_apply(filename, -1, tz, NULL);
g_free(tz);
g_list_free_full(d->imgs, g_free);
d->imgs = NULL;
d->nb_imgs = 0;
}
g_free(filename);
}
gtk_widget_destroy(filechooser);
// dt_control_queue_redraw_center();
}
static int _sort_timezones(gconstpointer a, gconstpointer b)
{
const tz_tuple_t *tz_a = (tz_tuple_t *)a;
const tz_tuple_t *tz_b = (tz_tuple_t *)b;
#ifdef _WIN32
gboolean utc_neg_a = g_str_has_prefix(tz_a->display, "(UTC-");
gboolean utc_neg_b = g_str_has_prefix(tz_b->display, "(UTC-");
gboolean utc_pos_a = g_str_has_prefix(tz_a->display, "(UTC+");
gboolean utc_pos_b = g_str_has_prefix(tz_b->display, "(UTC+");
if(utc_neg_a && utc_neg_b)
{
char *iter_a = tz_a->display + strlen("(UTC-");
char *iter_b = tz_b->display + strlen("(UTC-");
while(((*iter_a >= '0' && *iter_a <= '9') || *iter_a == ':') &&
((*iter_b >= '0' && *iter_b <= '9') || *iter_b == ':'))
{
if(*iter_a != *iter_b) return *iter_b - *iter_a;
iter_a++;
iter_b++;
}
}
else if(utc_neg_a && utc_pos_b) return -1;
else if(utc_pos_a && utc_neg_b) return 1;
#endif
return g_strcmp0(tz_a->display, tz_b->display);
}
// create a list of possible time zones
static GList *_lib_geotagging_get_timezones(void)
{
GList *timezones = NULL;
#ifndef _WIN32
// possible locations for zone.tab:
// - /usr/share/zoneinfo
// - /usr/lib/zoneinfo
// - getenv("TZDIR")
// - apparently on solaris there is no zones.tab. we need to collect the information ourselves like this:
// /bin/grep -h ^Zone /usr/share/lib/zoneinfo/src/* | /bin/awk '{print "??\t+9999+99999\t" $2}'
#define MAX_LINE_LENGTH 256
FILE *fp;
char line[MAX_LINE_LENGTH];
// find the file using known possible locations
gchar *zone_tab = g_strdup("/usr/share/zoneinfo/zone.tab");
if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR))
{
g_free(zone_tab);
zone_tab = g_strdup("/usr/lib/zoneinfo/zone.tab");
if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR))
{
g_free(zone_tab);
zone_tab = g_build_filename(g_getenv("TZDIR"), "zone.tab", NULL);
if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR))
{
g_free(zone_tab);
char datadir[PATH_MAX] = { 0 };
dt_loc_get_datadir(datadir, sizeof(datadir));
zone_tab = g_build_filename(datadir, "zone.tab", NULL);
if(!g_file_test(zone_tab, G_FILE_TEST_IS_REGULAR))
{
g_free(zone_tab);
// TODO: Solaris test
return NULL;
}
}
}
}
// parse zone.tab and put all time zone descriptions into timezones
fp = g_fopen(zone_tab, "r");
g_free(zone_tab);
if(!fp) return NULL;
while(fgets(line, MAX_LINE_LENGTH, fp))
{
if(line[0] == '#' || line[0] == '\0') continue;
gchar **tokens = g_strsplit_set(line, " \t\n", 0);
// sometimes files are not separated by single tabs but multiple spaces, resulting in empty strings in tokens
// so we have to look for the 3rd non-empty entry
int n_found = -1, i;
for(i = 0; tokens[i] && n_found < 2; i++) if(*tokens[i]) n_found++;
if(n_found != 2)
{
g_strfreev(tokens);
continue;
}
gchar *name = g_strdup(tokens[i - 1]);
g_strfreev(tokens);
if(name[0] == '\0')
{
g_free(name);
continue;
}
size_t last_char = strlen(name) - 1;
if(name[last_char] == '\n') name[last_char] = '\0';
tz_tuple_t *tz_tuple = malloc(sizeof(tz_tuple_t));
if(tz_tuple)
{
tz_tuple->display = name;
tz_tuple->name = name;
timezones = g_list_prepend(timezones, tz_tuple);
}
}
fclose(fp);
// sort timezones
timezones = g_list_sort(timezones, _sort_timezones);
tz_tuple_t *utc = malloc(sizeof(tz_tuple_t));
if(utc)
{
utc->display = g_strdup("UTC");
utc->name = utc->display;
timezones = g_list_prepend(timezones, utc);
}
#undef MAX_LINE_LENGTH
#else // !_WIN32
// on Windows we have to grab the time zones from the registry
char *keypath = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\";
HKEY hKey;
if(RegOpenKeyExA(HKEY_LOCAL_MACHINE,
keypath,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
DWORD n_subkeys, max_subkey_len;
if(RegQueryInfoKey(hKey,
NULL,
NULL,
NULL,
&n_subkeys,
&max_subkey_len,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL) == ERROR_SUCCESS)
{
wchar_t *subkeyname = malloc(sizeof(wchar_t) * (max_subkey_len + 1));
for(DWORD i = 1; i < n_subkeys; i++)
{
DWORD subkeyname_length = max_subkey_len + 1;
if(RegEnumKeyExW(hKey,
i,
subkeyname,
&subkeyname_length,
NULL,
NULL,
NULL,
NULL) == ERROR_SUCCESS)
{
DWORD buffer_size;
char *subkeyname_utf8 = g_utf16_to_utf8(subkeyname, -1, NULL, NULL, NULL);
char *subkeypath_utf8 = g_strconcat(keypath, "\\", subkeyname_utf8, NULL);
wchar_t *subkeypath = g_utf8_to_utf16(subkeypath_utf8, -1, NULL, NULL, NULL);
if(RegGetValueW(HKEY_LOCAL_MACHINE,
subkeypath,
L"Display",
RRF_RT_ANY,
NULL,
NULL,
&buffer_size) == ERROR_SUCCESS)
{
wchar_t *display_name = malloc(buffer_size);
if(RegGetValueW(HKEY_LOCAL_MACHINE,
subkeypath,
L"Display",
RRF_RT_ANY,
NULL,
display_name,
&buffer_size) == ERROR_SUCCESS)
{
tz_tuple_t *tz = malloc(sizeof(tz_tuple_t));
if(tz)
{
tz->name = subkeyname_utf8;
tz->display = g_utf16_to_utf8(display_name, -1, NULL, NULL, NULL);
timezones = g_list_prepend(timezones, tz);
}
subkeyname_utf8 = NULL; // to not free it later
}
free(display_name);
}
g_free(subkeyname_utf8);
g_free(subkeypath_utf8);
g_free(subkeypath);
}
}
free(subkeyname);
}
}
RegCloseKey(hKey);
timezones = g_list_sort(timezones, _sort_timezones);
#endif // !_WIN32
return timezones;
}
static void _display_offset(const GTimeSpan offset_int, const gboolean valid, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GTimeSpan off2 = 0;
if(valid)
{
const gboolean neg = offset_int < 0;
gtk_label_set_text(GTK_LABEL(d->of.sign), neg ? "- " : "");
char text[5];
GTimeSpan off = neg ? -offset_int : offset_int;
off2 = off / 1000; // skip microseconds
off = off2;
off2 = off / 1000;
snprintf(text, sizeof(text), "%03d", (int)(off - off2 * 1000));
gtk_entry_set_text(GTK_ENTRY(d->of.widget[6]), text);
off = off2;
off2 = off / 60;
snprintf(text, sizeof(text), "%02d", (int)(off - off2 * 60));
gtk_entry_set_text(GTK_ENTRY(d->of.widget[5]), text);
off = off2;
off2 = off / 60;
snprintf(text, sizeof(text), "%02d", (int)(off - off2 * 60));
gtk_entry_set_text(GTK_ENTRY(d->of.widget[4]), text);
off = off2;
off2 = off / 24;
snprintf(text, sizeof(text), "%02d", (int)(off - off2 * 24));
gtk_entry_set_text(GTK_ENTRY(d->of.widget[3]), text);
off = off2;
off2 = off / 10000;
snprintf(text, sizeof(text), "%04d", (int)(off - off2 * 10000));
gtk_entry_set_text(GTK_ENTRY(d->of.widget[2]), text);
}
if(!valid || off2)
{
gtk_label_set_text(GTK_LABEL(d->of.sign), "");
for(int i = 2; i < DT_GEOTAG_PARTS_NB; i++)
gtk_entry_set_text(GTK_ENTRY(d->of.widget[i]), "-");
}
const gboolean locked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->lock_offset));
gtk_widget_set_sensitive(d->apply_offset, d->imgid && valid && !off2 && offset_int);
gtk_widget_set_sensitive(d->lock_offset, locked || (d->imgid && valid && !off2 && offset_int));
gtk_widget_set_sensitive(d->apply_datetime, d->imgid && !locked);
#ifdef HAVE_MAP
_update_buttons(self);
#endif
}
static void _display_datetime(dt_lib_datetime_t *dtw, GDateTime *datetime,
const gboolean lock, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
for(int i = 0; lock && i < DT_GEOTAG_PARTS_NB; i++)
g_signal_handlers_block_by_func(d->dt.widget[i], _datetime_entry_changed, self);
if(datetime)
{
char value[8] = {0};
snprintf(value, sizeof(value), "%04d", g_date_time_get_year(datetime));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[0]), value);
snprintf(value, sizeof(value), "%02d", g_date_time_get_month(datetime));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[1]), value);
snprintf(value, sizeof(value), "%02d", g_date_time_get_day_of_month(datetime));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[2]), value);
snprintf(value, sizeof(value), "%02d", g_date_time_get_hour(datetime));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[3]), value);
snprintf(value, sizeof(value), "%02d", g_date_time_get_minute(datetime));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[4]),value);
snprintf(value, sizeof(value), "%02d", g_date_time_get_second(datetime));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[5]), value);
snprintf(value, sizeof(value), "%03d", (int)(g_date_time_get_microsecond(datetime) * 0.001));
gtk_entry_set_text(GTK_ENTRY(dtw->widget[6]), value);
}
else
{
for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++)
gtk_entry_set_text(GTK_ENTRY(dtw->widget[i]), "-");
}
for(int i = 0; lock && i < DT_GEOTAG_PARTS_NB; i++)
g_signal_handlers_unblock_by_func(d->dt.widget[i], _datetime_entry_changed, self);
}
// read the current date/time and make correction (field under/overflow)
static GDateTime *_read_datetime_entry(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
const int year = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[0])));
const int month = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[1])));
const int day = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[2])));
const int hour = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[3])));
const int minute = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[4])));
const int second = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[5])));
const int millisecond = atoi(gtk_entry_get_text(GTK_ENTRY(d->dt.widget[6])));
const gdouble second2 = (gdouble)second + (gdouble)millisecond * 0.001;
return g_date_time_new(darktable.utc_tz, year, month, day, hour, minute, second2);
}
static void _new_datetime(GDateTime *datetime, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(datetime)
{
_display_datetime(&d->dt, datetime, TRUE, self);
if(d->datetime)
g_date_time_unref(d->datetime);
d->datetime = datetime;
d->offset = g_date_time_difference(d->datetime, d->datetime0);
_display_offset(d->offset, d->datetime != NULL, self);
#ifdef HAVE_MAP
if(d->map.view)
_refresh_track_list(self);
#endif
}
}
static void _datetime_entry_changed(GtkWidget *entry, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(!d->editing)
{
GDateTime *datetime = _read_datetime_entry(self);
_new_datetime(datetime, self);
}
}
static GDateTime *_get_image_datetime(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
GList *selected = dt_collection_get_selected(darktable.collection, 1);
const int selid = selected ? GPOINTER_TO_INT(selected->data) : 0;
const dt_imgid_t imgid = dt_act_on_get_main_image();
GDateTime *datetime = NULL;
if((selid != 0) || ((selid == 0) && (dt_is_valid_imgid(imgid))))
{
// consider act on only if no selected
char datetime_s[DT_DATETIME_LENGTH];
dt_image_get_datetime(selid ? selid : imgid, datetime_s);
if(datetime_s[0] != '\0')
datetime = dt_datetime_exif_to_gdatetime(datetime_s, darktable.utc_tz);
else
datetime = NULL;
}
d->imgid = selid;
return datetime;
}
static void _refresh_image_datetime(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
const gboolean locked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->lock_offset));
GDateTime *datetime = _get_image_datetime(self);
if(d->datetime0)
g_date_time_unref(d->datetime0);
d->datetime0 = datetime;
_display_datetime(&d->dt0, datetime, FALSE, self);
if(locked)
{
if(datetime)
{
GDateTime *datetime2 = g_date_time_add(datetime, d->offset);
_new_datetime(datetime2, self);
}
}
else
{
_display_offset(d->offset = 0, datetime != NULL, self);
if(datetime)
{
g_date_time_ref(datetime);
_new_datetime(datetime, self);
}
}
}
static void _image_info_changed(gpointer instance, gpointer imgs, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
for(GList *i = imgs; i; i = g_list_next(i))
{
if(GPOINTER_TO_INT(i->data) == d->imgid)
{
_refresh_image_datetime(self);
break;
}
}
#ifdef HAVE_MAP
if(d->map.view)
{
_refresh_selected_images_datetime(self);
_refresh_track_list(self);
}
#endif
}
static void _mouse_over_image_callback(gpointer instance, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(!d->imgid)
_refresh_image_datetime(self);
}
static void _selection_changed_callback(gpointer instance, dt_lib_module_t *self)
{
_refresh_image_datetime(self);
#ifdef HAVE_MAP
dt_lib_geotagging_t *d = self->data;
if(d->map.view)
{
_setup_selected_images_list(self);
_refresh_track_list(self);
}
#endif
}
static void _datetime_scroll_over(GtkEventControllerScroll *controller,
double dx, double dy,
dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
if(!d->editing)
{
GtkWidget* w = dt_gui_get_widget(controller);
int i = 0;
for(i = 0; i < DT_GEOTAG_PARTS_NB; i++)
if(w == d->dt.widget[i]) break;
int increment = CLAMPF(-dy, -1, 1);
if(dt_modifier_eq(controller, GDK_SHIFT_MASK))
increment *= 10;
GDateTime *datetime;
switch(i)
{
case 0:
datetime = g_date_time_add_years(d->datetime, increment);
break;
case 1:
datetime = g_date_time_add_months(d->datetime, increment);
break;
case 2:
datetime = g_date_time_add_days(d->datetime, increment);
break;
case 3:
datetime = g_date_time_add_hours(d->datetime, increment);
break;
case 4:
datetime = g_date_time_add_minutes(d->datetime, increment);
break;
case 5:
datetime = g_date_time_add_seconds(d->datetime, increment);
break;
case 6:
datetime = g_date_time_add(d->datetime, increment * 1000);
break;
default:
datetime = NULL;
}
_new_datetime(datetime, self);
}
}
// type 0 date/time, 1 original date/time, 2 offset
static GtkWidget *_gui_init_datetime(gchar *text,
dt_lib_datetime_t *dt,
const int type,
dt_lib_module_t *self,
GtkSizeGroup *group,
GtkWidget *button,
gchar *tip)
{
GtkWidget *flow = gtk_flow_box_new();
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow), 2);
GtkWidget *t = dt_ui_label_new(text);
gtk_size_group_add_widget(group, t);
gtk_container_add(GTK_CONTAINER(flow), t);
gtk_widget_set_tooltip_text(flow, tip);
GtkWidget *flow2 = gtk_flow_box_new();
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(flow2), TRUE); // otherwise weird behavior
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(flow2), 2);
gtk_container_add(GTK_CONTAINER(flow), flow2);
GtkBox *box = NULL;
for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++)
{
if(!box) box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
if(i == 0 && type == 2)
{
gtk_box_set_homogeneous(box, TRUE); // creates some spacing
gtk_box_pack_start(box, button, TRUE, TRUE, 0);
dt->sign = gtk_label_new("");
gtk_box_pack_start(box, dt->sign, FALSE, FALSE, 0);
}
if(i >= 2 || type != 2)
{
// 1st field of dates (offset line is excluded by "if" above) is year, so 4 digits
dt->widget[i] = dt_ui_entry_new(i == 0 ? 4
// 7th field (optional, may be hidden) is msec, so 3 digits
: i == 6 ? 3
// 2nd widget of offset line should accommodate offsets up to 9999 days, so 4 digits
// all other fields for 2 digit numbers
: ((type == 2) && (i == 2)) ? 4 : 2);
gtk_entry_set_alignment(GTK_ENTRY(dt->widget[i]), 0.5);
gtk_box_pack_start(box, dt->widget[i], FALSE, FALSE, 0);
if(type == 0)
{
dt_action_define(DT_ACTION(self), NULL, i <= 2 ? N_("date") : N_("time"), dt->widget[i], &dt_action_def_entry);
gtk_widget_add_events(dt->widget[i], darktable.gui->scroll_mask);
}
else
{
gtk_widget_set_sensitive(dt->widget[i], FALSE);
}
}
if(i == 2 || i == 6)
{
gtk_widget_set_halign(GTK_WIDGET(box), GTK_ALIGN_END);
gtk_widget_set_hexpand(GTK_WIDGET(box), TRUE);
gtk_container_add(GTK_CONTAINER(flow2), GTK_WIDGET(box));
box = NULL;
}
else if(i > 2 || type != 2)
{
GtkWidget *label = gtk_label_new(i < 2 ? "-" : i == 5 ? "." :":");
if(i == 5)
g_object_set_data(G_OBJECT(dt->widget[i]), "msec_label", label);
gtk_box_pack_start(box, label, FALSE, FALSE, 0);
}
}
gtk_container_foreach(GTK_CONTAINER(flow2), (GtkCallback)gtk_widget_set_can_focus, GINT_TO_POINTER(FALSE));
return flow;
}
static gboolean _datetime_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
switch(keyval)
{
case GDK_KEY_Escape:
{
// reset
_refresh_image_datetime(self);
#ifdef HAVE_MAP
if(d->map.view)
_refresh_track_list(self);
#endif
gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL);
d->editing = FALSE;
return FALSE;
}
// allow 0 .. 9, left/right/home/end movement using arrow keys and del/backspace
case GDK_KEY_0:
case GDK_KEY_KP_0:
case GDK_KEY_1:
case GDK_KEY_KP_1:
case GDK_KEY_2:
case GDK_KEY_KP_2:
case GDK_KEY_3:
case GDK_KEY_KP_3:
case GDK_KEY_4:
case GDK_KEY_KP_4:
case GDK_KEY_5:
case GDK_KEY_KP_5:
case GDK_KEY_6:
case GDK_KEY_KP_6:
case GDK_KEY_7:
case GDK_KEY_KP_7:
case GDK_KEY_8:
case GDK_KEY_KP_8:
case GDK_KEY_9:
case GDK_KEY_KP_9:
case GDK_KEY_Delete:
case GDK_KEY_KP_Delete:
case GDK_KEY_BackSpace:
case GDK_KEY_Left:
case GDK_KEY_Right:
case GDK_KEY_Home:
case GDK_KEY_KP_Home:
case GDK_KEY_End:
case GDK_KEY_KP_End:
d->editing = TRUE;
return FALSE;
case GDK_KEY_Tab:
case GDK_KEY_KP_Tab:
case GDK_KEY_ISO_Left_Tab:
case GDK_KEY_Return:
case GDK_KEY_KP_Enter:
d->editing = FALSE;
g_signal_emit_by_name(d->dt.widget[0], "changed");
return FALSE;
default:
return TRUE;
}
}
static void _timezone_save(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
const gchar *tz = gtk_entry_get_text(GTK_ENTRY(d->timezone));
gchar *name = NULL;
for(GList *iter = d->timezones; iter; iter = g_list_next(iter))
{
tz_tuple_t *tz_tuple = (tz_tuple_t *)iter->data;
if(!strcmp(tz_tuple->display, tz))
name = tz_tuple->name;
}
if(d->tz_camera) g_time_zone_unref(d->tz_camera);
d->tz_camera = !name ? g_time_zone_new_utc() : g_time_zone_new(name);
dt_conf_set_string("plugins/lighttable/geotagging/tz", name ? name : "UTC");
gtk_entry_set_text(GTK_ENTRY(d->timezone), name ? name : "UTC");
gtk_label_set_text(GTK_LABEL (d->timezone_changed), "");
gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL);
#ifdef HAVE_MAP
if(d->map.view)
_refresh_track_list(self);
#endif
}
static gboolean _timezone_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
dt_lib_module_t *self)
{
switch(keyval)
{
case GDK_KEY_Return:
case GDK_KEY_KP_Enter:
case GDK_KEY_Tab:
_timezone_save(self);
return TRUE;
case GDK_KEY_Escape:
gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL);
return TRUE;
default: ;
dt_lib_geotagging_t *d = self->data;
gtk_label_set_text(GTK_LABEL (d->timezone_changed), " *");
break;
}
return FALSE;
}
static void _timezone_focus_out(GtkEventController *controller, dt_lib_module_t *self)
{
_timezone_save(self);
}
static gboolean _completion_match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter,
gpointer user_data)
{
gboolean res = FALSE;
GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
if(!GTK_IS_EDITABLE(e))
return FALSE;
GtkTreeModel *model = gtk_entry_completion_get_model(completion);
const int column = gtk_entry_completion_get_text_column(completion);
if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING)
return FALSE;
char *tag = NULL;
gtk_tree_model_get(model, iter, column, &tag, -1);
if(tag)
{
char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL);
if(normalized)
{
char *casefold = g_utf8_casefold(normalized, -1);
if(casefold)
{
res = g_strstr_len(casefold, -1, key) != NULL;
}
g_free(casefold);
}
g_free(normalized);
g_free(tag);
}
return res;
}
static void _toggle_lock_button_callback(GtkToggleButton *button, dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
const gboolean locked = gtk_toggle_button_get_active(button);
for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++)
{
gtk_widget_set_sensitive(d->dt.widget[i], !locked);
}
gtk_widget_set_sensitive(d->apply_datetime, d->imgid && !locked);
}
GtkTreeViewColumn *_new_tree_text_column(const char *name, const gboolean expand,
const float xalign, const int m_col,
const int ellipsize)
{
GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
GtkTreeViewColumn *column = gtk_tree_view_column_new();
gtk_tree_view_column_pack_start(column, renderer, TRUE);
gtk_tree_view_column_set_attributes(column, renderer, "text", m_col, NULL);
gtk_tree_view_column_set_expand(column, expand);
GtkWidget *label = gtk_label_new(name);
gtk_widget_show(label);
gtk_tree_view_column_set_widget(column, label);
gtk_label_set_ellipsize(GTK_LABEL(label), expand ? PANGO_ELLIPSIZE_MIDDLE : PANGO_ELLIPSIZE_NONE);
g_object_set(renderer, "xalign", xalign, "ellipsize", ellipsize, NULL);
return column;
}
static void _show_milliseconds(dt_lib_geotagging_t *d)
{
const gboolean milliseconds = dt_conf_get_bool("lighttable/ui/milliseconds");
gtk_widget_set_visible(d->dt.widget[6], milliseconds);
gtk_widget_set_visible(d->dt0.widget[6], milliseconds);
gtk_widget_set_visible(d->of.widget[6], milliseconds);
gtk_widget_set_visible(g_object_get_data(G_OBJECT(d->dt.widget[5]), "msec_label"), milliseconds);
gtk_widget_set_visible(g_object_get_data(G_OBJECT(d->dt0.widget[5]), "msec_label"), milliseconds);
gtk_widget_set_visible(g_object_get_data(G_OBJECT(d->of.widget[5]), "msec_label"), milliseconds);
}
static void _dt_pref_change_callback(gpointer instance, dt_lib_module_t *self)
{
_show_milliseconds(self->data);
}
void gui_reset(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->lock_offset), FALSE);
_refresh_image_datetime(self);
#ifdef HAVE_MAP
if(d->map.view)
{
gtk_label_set_text(GTK_LABEL(d->map.gpx_file), "");
_remove_tracks_from_map(self);
gtk_widget_set_visible(d->map.gpx_view, FALSE);
}
#endif
}
void gui_init(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = g_malloc0(sizeof(dt_lib_geotagging_t));
self->data = (void *)d;
d->timezones = _lib_geotagging_get_timezones();
self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkGrid *grid = GTK_GRID(gtk_grid_new());
gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
int line = 0;
GtkSizeGroup *group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
GtkWidget *box = _gui_init_datetime(_("date/time"), &d->dt, 0, self, group, NULL,
_("enter the new date/time (YYYY:MM:DD hh:mm:ss[.sss])"
"\nkey in the new numbers or scroll over the cell"));
gtk_grid_attach(grid, box, 0, line++, 4, 1);
box = _gui_init_datetime(_("original date/time"), &d->dt0, 1, self, group, NULL, NULL);
gtk_grid_attach(grid, box, 0, line++, 4, 1);
d->lock_offset = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lock, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("lock date/time offset value to apply it onto another selection"),
.clicked_cb = G_CALLBACK(_toggle_lock_button_callback),
.clicked_data = (gpointer)self,
});
gtk_widget_set_halign(d->lock_offset, GTK_ALIGN_START);
box = _gui_init_datetime(_("date/time offset"), &d->of, 2, self, group, d->lock_offset,
_("offset or difference ([-]dd hh:mm:ss[.sss])"));
gtk_grid_attach(grid, box, 0, line++, 4, 1);
// apply
d->apply_offset = dt_action_button_new(self, N_("apply offset"), _apply_offset_callback, self,
_("apply offset to selected images"), 0, 0);
gtk_grid_attach(grid, d->apply_offset , 0, line, 2, 1);
d->apply_datetime = dt_action_button_new(self, N_("apply date/time"), _apply_datetime_callback, self,
_("apply the same date/time to selected images"), 0, 0);
gtk_grid_attach(grid, d->apply_datetime , 2, line++, 2, 1);
// time zone entry
GtkWidget *label = dt_ui_label_new(_(dt_confgen_get_label("plugins/lighttable/geotagging/tz")));
gtk_widget_set_tooltip_markup(label, _(dt_confgen_get_tooltip("plugins/lighttable/geotagging/tz")));
gtk_grid_attach(grid, label, 0, line, 2, 1);
d->timezone = dt_ui_entry_new(0);
gtk_widget_set_tooltip_text(d->timezone,
_("start typing to show a list of permitted values and select your timezone.\n"
"press enter to confirm, so that the asterisk * disappears"));
d->timezone_changed = dt_ui_label_new("");
GtkWidget *timezone_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start(GTK_BOX(timezone_box), d->timezone, TRUE, TRUE, 0);
gtk_box_pack_end(GTK_BOX(timezone_box), d->timezone_changed, FALSE, FALSE, 0);
gtk_grid_attach(grid, timezone_box, 2, line++, 2, 1);
GtkCellRenderer *renderer;
GtkListStore *model = gtk_list_store_new(2, G_TYPE_STRING /*display*/, G_TYPE_STRING /*name*/);
GtkWidget *tz_selection = gtk_combo_box_new_with_model(GTK_TREE_MODEL(model));
renderer = gtk_cell_renderer_text_new();
gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(tz_selection), renderer, FALSE);
gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(tz_selection), renderer, "text", 0, NULL);
gchar *tz = dt_conf_get_string("plugins/lighttable/geotagging/tz");
d->tz_camera = (tz == NULL) ? g_time_zone_new_utc() : g_time_zone_new(tz);
for(GList *iter = d->timezones; iter; iter = g_list_next(iter))
{
tz_tuple_t *tz_tuple = (tz_tuple_t *)iter->data;
gtk_list_store_insert_with_values(model, NULL, -1, 0, tz_tuple->display, 1, tz_tuple->name, -1);
if(!strcmp(tz_tuple->name, tz))
gtk_entry_set_text(GTK_ENTRY(d->timezone), tz_tuple->display);
}
g_free(tz);
// add entry completion
GtkEntryCompletion *completion = gtk_entry_completion_new();
gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
gtk_entry_completion_set_text_column(completion, 0);
gtk_entry_completion_set_inline_completion(completion, TRUE);
gtk_entry_completion_set_popup_set_width(completion, FALSE);
gtk_entry_completion_set_match_func(completion, _completion_match_func, NULL, NULL);
gtk_entry_completion_set_minimum_key_length(completion, 0);
gtk_entry_set_completion(GTK_ENTRY(d->timezone), completion);
dt_gui_connect_key(d->timezone, _timezone_key_pressed, self);
dt_gui_connect_focus(d->timezone, NULL, _timezone_focus_out, self);
// gpx
d->gpx_button = dt_action_button_new(self, N_("apply GPX track file..."), _choose_gpx_callback, self,
_("parses a GPX file and updates location of selected images"), 0, 0);
gtk_grid_attach(grid, d->gpx_button, 0, line++, 4, 1);
gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(grid), TRUE, TRUE, 0);
#ifdef HAVE_MAP
grid = GTK_GRID(gtk_grid_new());
gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
line = 0;
d->map.gpx_section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->map.gpx_section), TRUE, TRUE, 0);
label = dt_ui_section_label_new(C_("section", "GPX file"));
gtk_grid_attach(grid, label, 0, line++, 4, 1);
d->map.gpx_button = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL,
&(dtgtk_button_config_t){
.tooltip = _("select a GPX track file..."),
.clicked_cb = G_CALLBACK(_choose_gpx_callback),
.clicked_data = self,
});
gtk_widget_set_hexpand(d->map.gpx_button, FALSE);
gtk_widget_set_halign(d->map.gpx_button, GTK_ALIGN_START);
gtk_widget_set_name(d->map.gpx_button, "non-flat");
gtk_grid_attach(grid, d->map.gpx_button, 0, line, 1, 1);
d->map.gpx_file = dt_ui_label_new("");
gtk_label_set_ellipsize(GTK_LABEL(d->map.gpx_file ), PANGO_ELLIPSIZE_MIDDLE);
gtk_widget_set_hexpand(d->map.gpx_file, TRUE);
gtk_grid_attach(grid, d->map.gpx_file, 1, line++, 3, 1);
gtk_box_pack_start(GTK_BOX(d->map.gpx_section), GTK_WIDGET(grid), TRUE, TRUE, 0);
model = gtk_list_store_new(DT_GEO_TRACKS_NUM_COLS, G_TYPE_BOOLEAN, G_TYPE_STRING,
G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING);
d->map.gpx_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model));
g_object_unref(model);
gtk_widget_set_name(d->map.gpx_view, "gpx_list");
gtk_widget_set_tooltip_text(GTK_WIDGET(d->map.gpx_view),
_("list of track segments in the GPX file, for each segment:"
"\n- the start date/time in local time (LT)"
"\n- the number of track points"
"\n- the number of matching images"
" based on images date/time, offset and time zone"
"\n- more detailed time information hovering the row"));
renderer = gtk_cell_renderer_toggle_new();
g_signal_connect(renderer, "toggled", G_CALLBACK(_track_seg_toggled), self);
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("", renderer, "active", DT_GEO_TRACKS_ACTIVE, NULL);
gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column);
d->map.sel_tracks = column;
GtkWidget *button = gtk_check_button_new();
gtk_widget_show(button);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
gtk_tree_view_column_set_widget(column, button);
gtk_tree_view_column_set_alignment(column, 0.5);
g_signal_connect(column, "clicked", G_CALLBACK(_all_tracks_toggled), self);
column = _new_tree_text_column(_("start time"), TRUE, 0.0, DT_GEO_TRACKS_DATETIME, PANGO_ELLIPSIZE_START);
gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column);
column = _new_tree_text_column(_("points"), FALSE, 1.0, DT_GEO_TRACKS_POINTS, PANGO_ELLIPSIZE_NONE);
gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column);
column = _new_tree_text_column(_("images"), FALSE, 1.0, DT_GEO_TRACKS_IMAGES, PANGO_ELLIPSIZE_NONE);
gtk_tree_view_append_column(GTK_TREE_VIEW(d->map.gpx_view), column);
g_object_set(G_OBJECT(d->map.gpx_view), "has-tooltip", TRUE, NULL);
g_signal_connect(G_OBJECT(d->map.gpx_view), "query-tooltip", G_CALLBACK(_row_tooltip_setup), self);
dt_gui_connect_click(d->map.gpx_view, _click_for_entire_track_pressed, NULL, self);
// avoid ugly console pixman messages due to headers
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(d->map.gpx_view), FALSE);
GtkWidget *w = dt_ui_resize_wrap(GTK_WIDGET(d->map.gpx_view), 100, "plugins/lighttable/geotagging/heighttracklist");
gtk_widget_set_size_request(w, -1, 100);
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(d->map.gpx_view), TRUE);
// the gpx_view_button is invisible and the label and tooltip of cannot be displayed on GUI.
// but label is use in "shortcuts" dialog.
GtkWidget *gpx_view_button = dt_action_button_new(self, N_("view entire track"), _refresh_display_all_tracks, self,
_("refresh map to view entire selected track segments"), 0, 0);
gtk_widget_set_opacity(gpx_view_button, 0);
gtk_widget_set_valign(gpx_view_button, GTK_ALIGN_START);
gtk_widget_set_margin_start(gpx_view_button, DT_PIXEL_APPLY_DPI(25));
gtk_widget_set_size_request(gpx_view_button, -1, DT_PIXEL_APPLY_DPI(25));
GtkWidget *overlay = gtk_overlay_new();
gtk_container_add(GTK_CONTAINER(overlay), w);
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), GTK_WIDGET(gpx_view_button));
gtk_box_pack_start(GTK_BOX(d->map.gpx_section), overlay, TRUE, TRUE, 0);
grid = GTK_GRID(gtk_grid_new());
gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(5));
line = 0;
d->map.preview_button = gtk_check_button_new_with_label(_("preview images"));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->map.preview_button), TRUE);
gtk_widget_set_sensitive(d->map.preview_button, FALSE);
gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(d->map.preview_button))), PANGO_ELLIPSIZE_END);
gtk_grid_attach(grid, d->map.preview_button, 0, line, 1, 1);
gtk_widget_set_tooltip_text(d->map.preview_button, _("show on map matching images"));
g_signal_connect(GTK_TOGGLE_BUTTON(d->map.preview_button), "toggled", G_CALLBACK(_images_preview_toggled), self);
d->map.select_button = dt_action_button_new(self, N_("select images"), _select_images, self,
_("select matching images"), 0, 0);
gtk_widget_set_hexpand(d->map.select_button, TRUE);
gtk_widget_set_sensitive(d->map.select_button, FALSE);
gtk_grid_attach(grid, d->map.select_button, 1, line, 1, 1);
d->map.nb_imgs_label = dt_ui_label_new("0/0");
gtk_widget_set_halign(d->map.nb_imgs_label, GTK_ALIGN_END);
gtk_widget_set_tooltip_text(GTK_WIDGET(d->map.nb_imgs_label),
_("number of matching images versus selected images"));
gtk_grid_attach(grid, d->map.nb_imgs_label, 2, line++, 1, 1);
d->map.apply_gpx_button = dt_action_button_new(self, N_("apply geo-location"), _apply_gpx, self,
_("apply geo-location to matching images"), 0, 0);
gtk_widget_set_hexpand(d->map.apply_gpx_button, TRUE);
gtk_widget_set_sensitive(d->map.apply_gpx_button, FALSE);
gtk_grid_attach(grid, d->map.apply_gpx_button, 0, line++, 3, 1);
gtk_box_pack_start(GTK_BOX(d->map.gpx_section), GTK_WIDGET(grid), TRUE, TRUE, 0);
d->map.view = FALSE;
gtk_widget_show_all(self->widget);
gtk_widget_set_no_show_all(self->widget, TRUE);
_update_layout(self);
#endif
d->imgid = NO_IMGID;
d->datetime = d->datetime0 = _get_image_datetime(self);
if(d->datetime)
g_date_time_ref(d->datetime);
_display_datetime(&d->dt0, d->datetime0, FALSE, self);
_display_datetime(&d->dt, d->datetime, TRUE, self);
d->offset = 0;
_display_offset(d->offset, TRUE, self);
for(int i = 0; i < DT_GEOTAG_PARTS_NB; i++)
{
g_signal_connect(d->dt.widget[i], "changed", G_CALLBACK(_datetime_entry_changed), self);
dt_gui_connect_key(d->dt.widget[i], _datetime_key_pressed, self);
dt_gui_connect_scroll(d->dt.widget[i],
GTK_EVENT_CONTROLLER_SCROLL_VERTICAL
| GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
_datetime_scroll_over,
self);
}
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_SELECTION_CHANGED, _selection_changed_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, _mouse_over_image_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_IMAGE_INFO_CHANGED, _image_info_changed);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PREFERENCES_CHANGE, _dt_pref_change_callback);
#ifdef HAVE_MAP
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_VIEWMANAGER_VIEW_CHANGED, _view_changed);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_GEOTAG_CHANGED, _geotag_changed);
#endif
_show_milliseconds(d);
gtk_widget_show_all(self->widget);
gtk_widget_set_no_show_all(self->widget, TRUE);
}
void gui_cleanup(dt_lib_module_t *self)
{
dt_lib_geotagging_t *d = self->data;
g_list_free_full(d->timezones, free_tz_tuple);
d->timezones = NULL;
g_time_zone_unref(d->tz_camera);
if(d->datetime)
g_date_time_unref(d->datetime);
if(d->datetime0)
g_date_time_unref(d->datetime0);
if(d->imgs)
{
#ifdef HAVE_MAP
_remove_images_from_map(self);
#endif
g_list_free_full(d->imgs, g_free);
}
d->imgs = NULL;
d->imgs = 0;
#ifdef HAVE_MAP
_remove_tracks_from_map(self);
#endif
free(self->data);
self->data = NULL;
}
// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on