| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207 |
- /*
- * GPS Test Tool - GUI front-end
- *
- * Plots an NMEA log (input) and the GPX track the embedded code produces
- * from it (output) on an OpenStreetMap background. The GPX is generated by
- * running the command-line gps-test-tool (-o single-run mode) with the
- * settings chosen in the side panel; every change re-runs it.
- *
- * Usage: ./gps-test-tool-gui [input.nmea]
- */
- #include <gtk/gtk.h>
- #include <libsoup/soup.h>
- #include <glib/gstdio.h>
- #include <glib-unix.h>
- #include <signal.h>
- #include <string.h>
- #include <math.h>
- #define TILE_SIZE 256
- #define MIN_ZOOM 2
- #define MAX_ZOOM 19
- #define TILE_URL "https://tile.openstreetmap.org/%d/%d/%d.png"
- #define USER_AGENT "GPStracker-gps-test-tool-gui/1.0"
- #define MAX_DOWNLOADS 2 /* OSM tile usage policy: at most 2 connections */
- #define MEM_CACHE_MAX 600 /* tiles kept in memory before evicting unused ones */
- #define TILE_MAX_AGE (30 * 24 * 3600) /* re-download disk-cached tiles older than this */
- #define FALLBACK_LEVELS 6 /* how many zoom levels up to look for a placeholder tile */
- #define CLICK_SLOP 4 /* pixels a press may move and still count as a click */
- #define DOTS_ZOOM 16 /* draw individual points from this zoom level up */
- #define REPROCESS_DELAY_MS 150
- /* A track point in normalized Web Mercator coordinates (0..1 both ways) */
- struct mpoint {
- double x, y;
- };
- enum {
- DL_IDLE,
- DL_QUEUED,
- DL_LOADING,
- DL_FAILED,
- DL_DONE,
- };
- struct tile {
- int z, x, y;
- cairo_surface_t *surface;
- unsigned char dl_state;
- unsigned char stale;
- guint last_used;
- };
- struct dl_request {
- struct tile *tile;
- SoupMessage *msg;
- };
- struct setting_spin {
- const char *key;
- const char *label;
- const char *cli_opt;
- int min, max, def;
- GtkWidget *widget;
- };
- /* Ranges match soft/settings.c's limits_min_u8/limits_max_u8; defaults
- * match the command-line tool's. */
- static struct setting_spin spins[] = {
- { "skip_points", "Skip points", "-k", 0, 120, 0, NULL },
- { "min_sats", "Min. satellites", "-m", 4, 12, 4, NULL },
- { "auto_pause_time", "Time (s)", "-t", 10, 120, 30, NULL },
- { "auto_pause_dist", "Distance (m)", "-d", 2, 100, 10, NULL },
- { "auto_pause_speed", "Speed (km/h)", "-s", 0, 20, 3, NULL },
- };
- #define SPIN_SKIP 0
- #define SPIN_MIN_SATS 1
- #define SPIN_AP_FIRST 2
- #define SPIN_COUNT (sizeof(spins) / sizeof(spins[0]))
- /* Per-segment colours for the GPX output */
- static const double seg_colors[][3] = {
- { 0.12, 0.47, 0.71 },
- { 1.00, 0.50, 0.05 },
- { 0.17, 0.63, 0.17 },
- { 0.84, 0.15, 0.16 },
- { 0.58, 0.40, 0.74 },
- { 0.09, 0.75, 0.81 },
- { 0.89, 0.47, 0.76 },
- { 0.55, 0.34, 0.29 },
- { 0.74, 0.74, 0.13 },
- };
- #define SEG_COLOR_COUNT (sizeof(seg_colors) / sizeof(seg_colors[0]))
- static struct {
- GtkWidget *window, *map, *coord_label, *status_label, *summary_label;
- GtkWidget *save_button;
- GtkWidget *chk_disable_filters, *chk_auto_pause, *chk_show_nmea, *chk_show_gpx;
- gboolean loading_settings;
- char *input_file;
- GArray *nmea; /* struct mpoint */
- GPtrArray *segments; /* GArray of struct mpoint, one per <trkseg> */
- gsize gpx_points;
- char *cli_path;
- char *tmpdir, *out_gpx, *out_log;
- gboolean have_result;
- GPid child;
- gboolean running, pending;
- guint reprocess_id;
- int zoom;
- double cx, cy; /* view centre in world pixels at the current zoom */
- gboolean need_fit;
- gboolean pressed, dragging;
- double press_x, press_y, last_x, last_y;
- double scroll_accum;
- gboolean have_cursor, frozen;
- double cursor_mx, cursor_my, frozen_mx, frozen_my;
- GHashTable *tiles; /* "z/x/y" -> struct tile */
- GQueue dl_queue; /* struct tile, DL_QUEUED */
- int dl_active;
- guint frame;
- SoupSession *soup;
- char *cache_dir;
- GKeyFile *config;
- char *config_path;
- } app;
- /* ===== Projection ===== */
- static double world_size(int zoom) {
- return TILE_SIZE * (double)(1 << zoom);
- }
- static void latlon_to_merc(double lat, double lon, struct mpoint *p) {
- double rlat = lat * M_PI / 180.0;
- p->x = (lon + 180.0) / 360.0;
- p->y = (1.0 - log(tan(rlat) + 1.0 / cos(rlat)) / M_PI) / 2.0;
- }
- static void merc_to_latlon(double x, double y, double *lat, double *lon) {
- *lon = x * 360.0 - 180.0;
- *lat = atan(sinh(M_PI * (1.0 - 2.0 * y))) * 180.0 / M_PI;
- }
- static void screen_to_merc(double sx, double sy, double *mx, double *my) {
- double ws = world_size(app.zoom);
- /* Same whole-pixel origin as map_draw() */
- *mx = (floor(app.cx - gtk_widget_get_allocated_width(app.map) / 2.0) + sx) / ws;
- *my = (floor(app.cy - gtk_widget_get_allocated_height(app.map) / 2.0) + sy) / ws;
- }
- static void clamp_view(void) {
- double ws = world_size(app.zoom);
- if (app.cy < 0)
- app.cy = 0;
- if (app.cy > ws)
- app.cy = ws;
- app.cx = fmod(app.cx, ws);
- if (app.cx < 0)
- app.cx += ws;
- }
- /* ===== Config ===== */
- static void config_save(void) {
- char *dir = g_path_get_dirname(app.config_path);
- g_mkdir_with_parents(dir, 0755);
- g_free(dir);
- g_key_file_save_to_file(app.config, app.config_path, NULL);
- }
- static int config_get_int(const char *group, const char *key, int def) {
- GError *err = NULL;
- int v = g_key_file_get_integer(app.config, group, key, &err);
- if (err) {
- g_error_free(err);
- return def;
- }
- return v;
- }
- static void settings_store(void) {
- unsigned i;
- if (app.loading_settings)
- return;
- for (i = 0; i < SPIN_COUNT; i++)
- g_key_file_set_integer(app.config, "settings", spins[i].key,
- gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spins[i].widget)));
- g_key_file_set_boolean(app.config, "settings", "disable_filters",
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_disable_filters)));
- g_key_file_set_boolean(app.config, "settings", "auto_pause",
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_auto_pause)));
- g_key_file_set_boolean(app.config, "view", "show_nmea",
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_show_nmea)));
- g_key_file_set_boolean(app.config, "view", "show_gpx",
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_show_gpx)));
- config_save();
- }
- /* ===== Tiles ===== */
- static char *tile_cache_path(int z, int x, int y) {
- return g_strdup_printf("%s/%d/%d/%d.png", app.cache_dir, z, x, y);
- }
- static cairo_surface_t *surface_from_pixbuf(GdkPixbuf *pix) {
- cairo_surface_t *s;
- cairo_t *cr;
- if (!pix)
- return NULL;
- s = cairo_image_surface_create(CAIRO_FORMAT_RGB24, TILE_SIZE, TILE_SIZE);
- cr = cairo_create(s);
- gdk_cairo_set_source_pixbuf(cr, pix, 0, 0);
- cairo_paint(cr);
- cairo_destroy(cr);
- g_object_unref(pix);
- return s;
- }
- static void tile_free(gpointer data) {
- struct tile *t = data;
- if (t->surface)
- cairo_surface_destroy(t->surface);
- g_free(t);
- }
- static gboolean tile_visible(struct tile *t) {
- int w = gtk_widget_get_allocated_width(app.map);
- int h = gtk_widget_get_allocated_height(app.map);
- int n = 1 << app.zoom;
- int x0, x1, y0, y1, x;
- if (t->z != app.zoom)
- return FALSE;
- x0 = (int)floor((app.cx - w / 2.0) / TILE_SIZE);
- x1 = (int)floor((app.cx + w / 2.0) / TILE_SIZE);
- y0 = (int)floor((app.cy - h / 2.0) / TILE_SIZE);
- y1 = (int)floor((app.cy + h / 2.0) / TILE_SIZE);
- if (t->y < y0 || t->y > y1)
- return FALSE;
- for (x = x0; x <= x1; x++)
- if (((x % n) + n) % n == t->x)
- return TRUE;
- return FALSE;
- }
- static void dl_pump(void);
- static void dl_finished(GObject *source, GAsyncResult *res, gpointer data) {
- struct dl_request *req = data;
- struct tile *t = req->tile;
- GError *err = NULL;
- GBytes *bytes = soup_session_send_and_read_finish(SOUP_SESSION(source), res, &err);
- app.dl_active--;
- t->dl_state = DL_FAILED;
- if (bytes && soup_message_get_status(req->msg) == SOUP_STATUS_OK) {
- GInputStream *st = g_memory_input_stream_new_from_bytes(bytes);
- GdkPixbuf *pix = gdk_pixbuf_new_from_stream(st, NULL, NULL);
- g_object_unref(st);
- if (pix) {
- char *path = tile_cache_path(t->z, t->x, t->y);
- char *dir = g_path_get_dirname(path);
- g_mkdir_with_parents(dir, 0755);
- g_file_set_contents(path, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), NULL);
- g_free(dir);
- g_free(path);
- if (t->surface)
- cairo_surface_destroy(t->surface);
- t->surface = surface_from_pixbuf(pix);
- t->stale = 0;
- t->dl_state = DL_DONE;
- }
- } else if (err) {
- g_warning("Tile %d/%d/%d: %s", t->z, t->x, t->y, err->message);
- } else {
- g_warning("Tile %d/%d/%d: HTTP %u", t->z, t->x, t->y, soup_message_get_status(req->msg));
- }
- if (err)
- g_error_free(err);
- if (bytes)
- g_bytes_unref(bytes);
- g_object_unref(req->msg);
- g_free(req);
- gtk_widget_queue_draw(app.map);
- dl_pump();
- }
- static void dl_pump(void) {
- while (app.dl_active < MAX_DOWNLOADS && !g_queue_is_empty(&app.dl_queue)) {
- struct tile *t = g_queue_pop_head(&app.dl_queue);
- struct dl_request *req;
- char *url;
- if (!tile_visible(t)) {
- t->dl_state = DL_IDLE; /* may be queued again once it's back in view */
- continue;
- }
- url = g_strdup_printf(TILE_URL, t->z, t->x, t->y);
- req = g_new0(struct dl_request, 1);
- req->tile = t;
- req->msg = soup_message_new("GET", url);
- g_free(url);
- t->dl_state = DL_LOADING;
- app.dl_active++;
- soup_session_send_and_read_async(app.soup, req->msg, G_PRIORITY_DEFAULT, NULL, dl_finished, req);
- }
- }
- /* Look up a tile, loading it from the disk cache if needed. With fetch set,
- * a missing or stale tile is queued for download. */
- static struct tile *tile_get(int z, int x, int y, gboolean fetch) {
- char key[48];
- struct tile *t;
- g_snprintf(key, sizeof(key), "%d/%d/%d", z, x, y);
- t = g_hash_table_lookup(app.tiles, key);
- if (!t) {
- char *path = tile_cache_path(z, x, y);
- GStatBuf st;
- t = g_new0(struct tile, 1);
- t->z = z;
- t->x = x;
- t->y = y;
- if (g_stat(path, &st) == 0) {
- t->surface = surface_from_pixbuf(gdk_pixbuf_new_from_file(path, NULL));
- t->stale = time(NULL) - st.st_mtime > TILE_MAX_AGE;
- }
- g_free(path);
- g_hash_table_insert(app.tiles, g_strdup(key), t);
- }
- t->last_used = app.frame;
- if (fetch && (!t->surface || t->stale) && t->dl_state == DL_IDLE) {
- t->dl_state = DL_QUEUED;
- g_queue_push_tail(&app.dl_queue, t);
- }
- return t;
- }
- static gboolean tile_evictable(gpointer key, gpointer value, gpointer data) {
- struct tile *t = value;
- (void)key;
- (void)data;
- return t->dl_state != DL_QUEUED && t->dl_state != DL_LOADING && t->last_used + 1 < app.frame;
- }
- static void draw_tile(cairo_t *cr, int z, int x, int y, double sx, double sy) {
- struct tile *t = tile_get(z, x, y, TRUE);
- int d;
- if (t->surface) {
- cairo_set_source_surface(cr, t->surface, sx, sy);
- cairo_rectangle(cr, sx, sy, TILE_SIZE, TILE_SIZE);
- cairo_fill(cr);
- return;
- }
- /* Not available (yet): stretch a lower-zoom tile we already have */
- for (d = 1; d <= FALLBACK_LEVELS && z - d >= 0; d++) {
- struct tile *p = tile_get(z - d, x >> d, y >> d, FALSE);
- if (p->surface) {
- double scale = 1 << d;
- cairo_save(cr);
- cairo_rectangle(cr, sx, sy, TILE_SIZE, TILE_SIZE);
- cairo_clip(cr);
- cairo_translate(cr, sx - (x - ((x >> d) << d)) * TILE_SIZE,
- sy - (y - ((y >> d) << d)) * TILE_SIZE);
- cairo_scale(cr, scale, scale);
- cairo_set_source_surface(cr, p->surface, 0, 0);
- cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
- cairo_paint(cr);
- cairo_restore(cr);
- return;
- }
- }
- }
- /* ===== Drawing ===== */
- static void draw_track(cairo_t *cr, GArray *pts, double ox, double oy, double ws,
- double r, double g, double b, double a, double width, gboolean dots) {
- guint i;
- if (!pts || !pts->len)
- return;
- cairo_set_source_rgba(cr, r, g, b, a);
- cairo_set_line_width(cr, width);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- for (i = 0; i < pts->len; i++) {
- struct mpoint *p = &g_array_index(pts, struct mpoint, i);
- double sx = p->x * ws - ox, sy = p->y * ws - oy;
- if (i)
- cairo_line_to(cr, sx, sy);
- else
- cairo_move_to(cr, sx, sy);
- }
- if (pts->len == 1)
- cairo_rel_line_to(cr, 0, 0); /* round cap makes it a dot */
- cairo_stroke(cr);
- if (!dots)
- return;
- for (i = 0; i < pts->len; i++) {
- struct mpoint *p = &g_array_index(pts, struct mpoint, i);
- cairo_new_sub_path(cr);
- cairo_arc(cr, p->x * ws - ox, p->y * ws - oy, width * 0.9, 0, 2 * M_PI);
- }
- cairo_fill(cr);
- }
- static void fit_view(void);
- static gboolean map_draw(GtkWidget *w, cairo_t *cr, gpointer data) {
- int width = gtk_widget_get_allocated_width(w);
- int height = gtk_widget_get_allocated_height(w);
- double ws, ox, oy;
- int n, tx0, tx1, ty0, ty1, tx, ty;
- gboolean dots;
- cairo_text_extents_t ext;
- const char *attrib = "© OpenStreetMap contributors";
- (void)data;
- if (app.need_fit)
- fit_view();
- app.frame++;
- if (g_hash_table_size(app.tiles) > MEM_CACHE_MAX)
- g_hash_table_foreach_remove(app.tiles, tile_evictable, NULL);
- ws = world_size(app.zoom);
- n = 1 << app.zoom;
- /* Whole pixels: tiles drawn at fractional offsets show seams */
- ox = floor(app.cx - width / 2.0);
- oy = floor(app.cy - height / 2.0);
- cairo_set_source_rgb(cr, 0.85, 0.85, 0.85);
- cairo_paint(cr);
- tx0 = (int)floor(ox / TILE_SIZE);
- tx1 = (int)floor((ox + width) / TILE_SIZE);
- ty0 = MAX(0, (int)floor(oy / TILE_SIZE));
- ty1 = MIN(n - 1, (int)floor((oy + height) / TILE_SIZE));
- for (ty = ty0; ty <= ty1; ty++)
- for (tx = tx0; tx <= tx1; tx++)
- draw_tile(cr, app.zoom, ((tx % n) + n) % n, ty, tx * TILE_SIZE - ox, ty * TILE_SIZE - oy);
- dl_pump();
- dots = app.zoom >= DOTS_ZOOM;
- if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_show_gpx)) && app.segments) {
- guint i;
- for (i = 0; i < app.segments->len; i++) {
- const double *c = seg_colors[i % SEG_COLOR_COUNT];
- draw_track(cr, g_ptr_array_index(app.segments, i), ox, oy, ws,
- c[0], c[1], c[2], 0.85, 4.0, dots);
- }
- }
- /* Input on top, thin, so it stays visible where the output follows it */
- if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_show_nmea)))
- draw_track(cr, app.nmea, ox, oy, ws, 0.1, 0.1, 0.1, 0.75, 1.2, dots);
- if (app.frozen) {
- double sx = app.frozen_mx * ws - ox, sy = app.frozen_my * ws - oy;
- cairo_set_line_width(cr, 2.0);
- cairo_set_source_rgb(cr, 1, 1, 1);
- cairo_arc(cr, sx, sy, 7, 0, 2 * M_PI);
- cairo_stroke(cr);
- cairo_set_line_width(cr, 1.5);
- cairo_set_source_rgb(cr, 0.8, 0, 0);
- cairo_arc(cr, sx, sy, 7, 0, 2 * M_PI);
- cairo_move_to(cr, sx - 11, sy);
- cairo_line_to(cr, sx + 11, sy);
- cairo_move_to(cr, sx, sy - 11);
- cairo_line_to(cr, sx, sy + 11);
- cairo_stroke(cr);
- }
- /* Attribution, required by the OSM tile usage policy */
- cairo_set_font_size(cr, 11);
- cairo_text_extents(cr, attrib, &ext);
- cairo_set_source_rgba(cr, 1, 1, 1, 0.75);
- cairo_rectangle(cr, width - ext.x_advance - 8, height - 17, ext.x_advance + 8, 17);
- cairo_fill(cr);
- cairo_set_source_rgb(cr, 0.2, 0.2, 0.2);
- cairo_move_to(cr, width - ext.x_advance - 4, height - 5);
- cairo_show_text(cr, attrib);
- return TRUE;
- }
- /* ===== Coordinate bar ===== */
- static void update_coord_label(void) {
- double mx, my, lat, lon;
- char slat[G_ASCII_DTOSTR_BUF_SIZE], slon[G_ASCII_DTOSTR_BUF_SIZE];
- char *text;
- if (app.frozen || app.have_cursor) {
- mx = app.frozen ? app.frozen_mx : app.cursor_mx;
- my = app.frozen ? app.frozen_my : app.cursor_my;
- merc_to_latlon(mx, my, &lat, &lon);
- /* Always '.' as the decimal separator, whatever the locale */
- g_ascii_formatd(slat, sizeof(slat), "%.6f", lat);
- g_ascii_formatd(slon, sizeof(slon), "%.6f", lon);
- if (app.frozen)
- text = g_strdup_printf("<b>%s, %s</b> (frozen \u2013 right-click to release) zoom %d", slat, slon, app.zoom);
- else
- text = g_strdup_printf("%s, %s zoom %d", slat, slon, app.zoom);
- } else {
- text = g_strdup_printf("\u2013 zoom %d", app.zoom);
- }
- gtk_label_set_markup(GTK_LABEL(app.coord_label), text);
- g_free(text);
- }
- /* ===== View ===== */
- static gboolean bbox_add(GArray *a, double *x0, double *y0, double *x1, double *y1) {
- guint i;
- for (i = 0; a && i < a->len; i++) {
- struct mpoint *p = &g_array_index(a, struct mpoint, i);
- *x0 = MIN(*x0, p->x);
- *y0 = MIN(*y0, p->y);
- *x1 = MAX(*x1, p->x);
- *y1 = MAX(*y1, p->y);
- }
- return a && a->len;
- }
- static gboolean track_bbox(double *x0, double *y0, double *x1, double *y1) {
- gboolean any;
- guint i;
- *x0 = *y0 = 1.0;
- *x1 = *y1 = 0.0;
- any = bbox_add(app.nmea, x0, y0, x1, y1);
- for (i = 0; app.segments && i < app.segments->len; i++)
- any |= bbox_add(g_ptr_array_index(app.segments, i), x0, y0, x1, y1);
- return any;
- }
- static void fit_view(void) {
- int w = gtk_widget_get_allocated_width(app.map);
- int h = gtk_widget_get_allocated_height(app.map);
- double x0, y0, x1, y1;
- if (w <= 1 || h <= 1)
- return; /* not realized yet; retried on the next draw */
- app.need_fit = FALSE;
- if (!track_bbox(&x0, &y0, &x1, &y1))
- return;
- for (app.zoom = MAX_ZOOM; app.zoom > MIN_ZOOM; app.zoom--) {
- double ws = world_size(app.zoom);
- if ((x1 - x0) * ws <= w - 40 && (y1 - y0) * ws <= h - 40)
- break;
- }
- app.cx = (x0 + x1) / 2.0 * world_size(app.zoom);
- app.cy = (y0 + y1) / 2.0 * world_size(app.zoom);
- update_coord_label();
- }
- static void zoom_at(int new_zoom, double sx, double sy) {
- int w = gtk_widget_get_allocated_width(app.map);
- int h = gtk_widget_get_allocated_height(app.map);
- double f;
- new_zoom = CLAMP(new_zoom, MIN_ZOOM, MAX_ZOOM);
- if (new_zoom == app.zoom)
- return;
- f = ldexp(1.0, new_zoom - app.zoom);
- app.cx = (app.cx + sx - w / 2.0) * f - (sx - w / 2.0);
- app.cy = (app.cy + sy - h / 2.0) * f - (sy - h / 2.0);
- app.zoom = new_zoom;
- clamp_view();
- update_coord_label();
- gtk_widget_queue_draw(app.map);
- }
- static gboolean map_scroll(GtkWidget *w, GdkEventScroll *ev, gpointer data) {
- double dx, dy;
- (void)w;
- (void)data;
- switch (ev->direction) {
- case GDK_SCROLL_UP:
- zoom_at(app.zoom + 1, ev->x, ev->y);
- break;
- case GDK_SCROLL_DOWN:
- zoom_at(app.zoom - 1, ev->x, ev->y);
- break;
- case GDK_SCROLL_SMOOTH:
- if (!gdk_event_get_scroll_deltas((GdkEvent *)ev, &dx, &dy))
- break;
- app.scroll_accum += dy;
- while (app.scroll_accum <= -1.0) {
- app.scroll_accum += 1.0;
- zoom_at(app.zoom + 1, ev->x, ev->y);
- }
- while (app.scroll_accum >= 1.0) {
- app.scroll_accum -= 1.0;
- zoom_at(app.zoom - 1, ev->x, ev->y);
- }
- break;
- default:
- break;
- }
- return TRUE;
- }
- static gboolean map_button_press(GtkWidget *w, GdkEventButton *ev, gpointer data) {
- (void)w;
- (void)data;
- if (ev->type != GDK_BUTTON_PRESS)
- return FALSE;
- if (ev->button == 1) {
- app.pressed = TRUE;
- app.dragging = FALSE;
- app.press_x = app.last_x = ev->x;
- app.press_y = app.last_y = ev->y;
- } else if (ev->button == 3 && app.frozen) {
- app.frozen = FALSE;
- update_coord_label();
- gtk_widget_queue_draw(app.map);
- }
- return TRUE;
- }
- static gboolean map_button_release(GtkWidget *w, GdkEventButton *ev, gpointer data) {
- (void)w;
- (void)data;
- if (ev->button != 1 || !app.pressed)
- return FALSE;
- app.pressed = FALSE;
- if (!app.dragging) {
- screen_to_merc(ev->x, ev->y, &app.frozen_mx, &app.frozen_my);
- app.frozen = TRUE;
- update_coord_label();
- gtk_widget_queue_draw(app.map);
- }
- app.dragging = FALSE;
- return TRUE;
- }
- static gboolean map_motion(GtkWidget *w, GdkEventMotion *ev, gpointer data) {
- (void)w;
- (void)data;
- if (app.pressed) {
- if (!app.dragging && (fabs(ev->x - app.press_x) > CLICK_SLOP || fabs(ev->y - app.press_y) > CLICK_SLOP))
- app.dragging = TRUE;
- if (app.dragging) {
- app.cx -= ev->x - app.last_x;
- app.cy -= ev->y - app.last_y;
- clamp_view();
- gtk_widget_queue_draw(app.map);
- }
- app.last_x = ev->x;
- app.last_y = ev->y;
- }
- screen_to_merc(ev->x, ev->y, &app.cursor_mx, &app.cursor_my);
- app.have_cursor = TRUE;
- if (!app.frozen)
- update_coord_label();
- return TRUE;
- }
- static gboolean map_leave(GtkWidget *w, GdkEventCrossing *ev, gpointer data) {
- (void)w;
- (void)ev;
- (void)data;
- app.have_cursor = FALSE;
- if (!app.frozen)
- update_coord_label();
- return FALSE;
- }
- /* ===== Input / output parsing ===== */
- static double nmea_coord(const char *val, const char *hemi) {
- double v = g_ascii_strtod(val, NULL);
- double deg = floor(v / 100.0);
- v = deg + (v - deg * 100.0) / 60.0;
- return (*hemi == 'S' || *hemi == 'W') ? -v : v;
- }
- /* Extract a position from a valid, checksummed $..RMC sentence */
- static gboolean nmea_rmc_point(const char *line, struct mpoint *p) {
- const char *s = strchr(line, '$'), *c;
- char **f;
- unsigned char sum = 0;
- unsigned int want;
- gboolean ok = FALSE;
- char *body;
- if (!s || strncmp(s + 3, "RMC,", 4))
- return FALSE;
- for (c = s + 1; *c && *c != '*'; c++)
- sum ^= (unsigned char)*c;
- if (*c != '*' || sscanf(c + 1, "%2x", &want) != 1 || want != sum)
- return FALSE;
- body = g_strndup(s + 1, c - s - 1);
- f = g_strsplit(body, ",", 0);
- if (g_strv_length(f) >= 7 && !strcmp(f[2], "A") && *f[3] && *f[5]) {
- latlon_to_merc(nmea_coord(f[3], f[4]), nmea_coord(f[5], f[6]), p);
- ok = TRUE;
- }
- g_strfreev(f);
- g_free(body);
- return ok;
- }
- static gboolean load_nmea(const char *path) {
- char *data, **lines;
- guint i;
- GError *err = NULL;
- if (!g_file_get_contents(path, &data, NULL, &err)) {
- GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(app.window), GTK_DIALOG_MODAL,
- GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Cannot open %s: %s", path, err->message);
- gtk_dialog_run(GTK_DIALOG(d));
- gtk_widget_destroy(d);
- g_error_free(err);
- return FALSE;
- }
- if (app.nmea)
- g_array_free(app.nmea, TRUE);
- app.nmea = g_array_new(FALSE, FALSE, sizeof(struct mpoint));
- lines = g_strsplit(data, "\n", 0);
- for (i = 0; lines[i]; i++) {
- struct mpoint p;
- if (nmea_rmc_point(lines[i], &p))
- g_array_append_val(app.nmea, p);
- }
- g_strfreev(lines);
- g_free(data);
- return TRUE;
- }
- static void free_segments(void) {
- if (app.segments)
- g_ptr_array_free(app.segments, TRUE);
- app.segments = NULL;
- app.gpx_points = 0;
- }
- static void load_gpx(const char *path) {
- char *data, *s;
- GArray *seg = NULL;
- free_segments();
- app.segments = g_ptr_array_new_with_free_func((GDestroyNotify)g_array_unref);
- if (!g_file_get_contents(path, &data, NULL, NULL))
- return;
- for (s = data; (s = strchr(s, '<')); s++) {
- if (!strncmp(s, "<trkseg>", 8)) {
- seg = g_array_new(FALSE, FALSE, sizeof(struct mpoint));
- g_ptr_array_add(app.segments, seg);
- } else if (!strncmp(s, "<trkpt ", 7) && seg) {
- const char *la = strstr(s, "lat=\""), *lo = strstr(s, "lon=\""), *end = strchr(s, '>');
- struct mpoint p;
- if (!la || !lo || !end || la > end || lo > end)
- continue;
- latlon_to_merc(g_ascii_strtod(la + 5, NULL), g_ascii_strtod(lo + 5, NULL), &p);
- g_array_append_val(seg, p);
- app.gpx_points++;
- }
- }
- g_free(data);
- }
- static void load_summary(const char *path) {
- char *data, *s;
- if (!g_file_get_contents(path, &data, NULL, NULL)) {
- gtk_label_set_text(GTK_LABEL(app.summary_label), "");
- return;
- }
- s = strstr(data, "=== Summary ===\n");
- gtk_label_set_text(GTK_LABEL(app.summary_label), s ? g_strstrip(s + 16) : "");
- g_free(data);
- }
- /* ===== Processing (runs the command-line tool) ===== */
- static void set_status(const char *text) {
- gtk_label_set_text(GTK_LABEL(app.status_label), text);
- }
- static void start_processing(void);
- static void child_exited(GPid pid, gint status, gpointer data) {
- GError *err = NULL;
- (void)data;
- g_spawn_close_pid(pid);
- app.running = FALSE;
- if (app.pending) {
- start_processing();
- return;
- }
- if (!g_spawn_check_wait_status(status, &err)) {
- char *msg = g_strdup_printf("Processing failed: %s", err->message);
- set_status(msg);
- g_free(msg);
- g_error_free(err);
- app.have_result = FALSE;
- free_segments();
- } else {
- char *msg;
- load_gpx(app.out_gpx);
- load_summary(app.out_log);
- app.have_result = TRUE;
- msg = g_strdup_printf("%u NMEA fixes → %" G_GSIZE_FORMAT " GPX points in %u segment(s)",
- app.nmea ? app.nmea->len : 0, app.gpx_points, app.segments->len);
- set_status(msg);
- g_free(msg);
- }
- gtk_widget_set_sensitive(app.save_button, app.have_result);
- gtk_widget_queue_draw(app.map);
- }
- static void start_processing(void) {
- GPtrArray *argv;
- GError *err = NULL;
- char *vals[SPIN_COUNT];
- unsigned i;
- if (!app.input_file)
- return;
- if (app.running) {
- /* Restart with the new settings once this run is gone */
- app.pending = TRUE;
- kill(app.child, SIGTERM);
- return;
- }
- app.pending = FALSE;
- argv = g_ptr_array_new();
- g_ptr_array_add(argv, app.cli_path);
- g_ptr_array_add(argv, "-o");
- g_ptr_array_add(argv, app.out_gpx);
- g_ptr_array_add(argv, "-l");
- g_ptr_array_add(argv, app.out_log);
- if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_disable_filters)))
- g_ptr_array_add(argv, "-n");
- if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_auto_pause)))
- g_ptr_array_add(argv, "-A");
- for (i = 0; i < SPIN_COUNT; i++) {
- vals[i] = g_strdup_printf("%d", gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spins[i].widget)));
- g_ptr_array_add(argv, (gpointer)spins[i].cli_opt);
- g_ptr_array_add(argv, vals[i]);
- }
- g_ptr_array_add(argv, "--");
- g_ptr_array_add(argv, app.input_file);
- g_ptr_array_add(argv, NULL);
- if (g_spawn_async(NULL, (char **)argv->pdata, NULL,
- G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_STDOUT_TO_DEV_NULL,
- NULL, NULL, &app.child, &err)) {
- app.running = TRUE;
- g_child_watch_add(app.child, child_exited, NULL);
- set_status("Processing…");
- } else {
- char *msg = g_strdup_printf("Cannot run %s: %s", app.cli_path, err->message);
- set_status(msg);
- g_free(msg);
- g_error_free(err);
- }
- for (i = 0; i < SPIN_COUNT; i++)
- g_free(vals[i]);
- g_ptr_array_free(argv, TRUE);
- }
- static gboolean reprocess_timeout(gpointer data) {
- (void)data;
- app.reprocess_id = 0;
- start_processing();
- return G_SOURCE_REMOVE;
- }
- static void setting_changed(GtkWidget *w, gpointer data) {
- unsigned i;
- (void)w;
- (void)data;
- if (app.loading_settings)
- return;
- for (i = SPIN_AP_FIRST; i < SPIN_COUNT; i++)
- gtk_widget_set_sensitive(spins[i].widget,
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_auto_pause)));
- settings_store();
- /* Debounced: holding a spin button's arrow fires many changes */
- if (app.reprocess_id)
- g_source_remove(app.reprocess_id);
- app.reprocess_id = g_timeout_add(REPROCESS_DELAY_MS, reprocess_timeout, NULL);
- }
- static void view_changed(GtkWidget *w, gpointer data) {
- (void)w;
- (void)data;
- settings_store();
- gtk_widget_queue_draw(app.map);
- }
- /* ===== File handling ===== */
- static void open_file(const char *path) {
- char *title, *base;
- if (!load_nmea(path))
- return;
- g_free(app.input_file);
- app.input_file = g_canonicalize_filename(path, NULL);
- g_key_file_set_string(app.config, "view", "input_file", app.input_file);
- config_save();
- base = g_path_get_basename(path);
- title = g_strdup_printf("%s – GPS Test Tool", base);
- gtk_window_set_title(GTK_WINDOW(app.window), title);
- g_free(title);
- g_free(base);
- free_segments();
- app.have_result = FALSE;
- gtk_widget_set_sensitive(app.save_button, FALSE);
- gtk_label_set_text(GTK_LABEL(app.summary_label), "");
- app.need_fit = TRUE;
- gtk_widget_queue_draw(app.map);
- start_processing();
- }
- static void open_clicked(GtkWidget *w, gpointer data) {
- GtkWidget *d;
- GtkFileFilter *f;
- (void)w;
- (void)data;
- d = gtk_file_chooser_dialog_new("Open NMEA log", GTK_WINDOW(app.window),
- GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL,
- "_Open", GTK_RESPONSE_ACCEPT, NULL);
- f = gtk_file_filter_new();
- gtk_file_filter_set_name(f, "NMEA logs");
- gtk_file_filter_add_pattern(f, "*.nmea");
- gtk_file_filter_add_pattern(f, "*.NMEA");
- gtk_file_filter_add_pattern(f, "*.log");
- gtk_file_filter_add_pattern(f, "*.LOG");
- gtk_file_filter_add_pattern(f, "*.txt");
- gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(d), f);
- f = gtk_file_filter_new();
- gtk_file_filter_set_name(f, "All files");
- gtk_file_filter_add_pattern(f, "*");
- gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(d), f);
- if (app.input_file) {
- char *dir = g_path_get_dirname(app.input_file);
- gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(d), dir);
- g_free(dir);
- }
- if (gtk_dialog_run(GTK_DIALOG(d)) == GTK_RESPONSE_ACCEPT) {
- char *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(d));
- gtk_widget_destroy(d);
- open_file(path);
- g_free(path);
- return;
- }
- gtk_widget_destroy(d);
- }
- static void save_clicked(GtkWidget *w, gpointer data) {
- GtkWidget *d;
- (void)w;
- (void)data;
- if (!app.have_result)
- return;
- d = gtk_file_chooser_dialog_new("Save GPX", GTK_WINDOW(app.window),
- GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL,
- "_Save", GTK_RESPONSE_ACCEPT, NULL);
- gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(d), TRUE);
- if (app.input_file) {
- char *dir = g_path_get_dirname(app.input_file);
- char *base = g_path_get_basename(app.input_file);
- char *dot = strrchr(base, '.');
- char *name;
- if (dot)
- *dot = '\0';
- name = g_strdup_printf("%s.gpx", base);
- gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(d), dir);
- gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(d), name);
- g_free(name);
- g_free(base);
- g_free(dir);
- }
- if (gtk_dialog_run(GTK_DIALOG(d)) == GTK_RESPONSE_ACCEPT) {
- GFile *src = g_file_new_for_path(app.out_gpx);
- GFile *dst = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(d));
- GError *err = NULL;
- if (!g_file_copy(src, dst, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &err)) {
- GtkWidget *m = gtk_message_dialog_new(GTK_WINDOW(d), GTK_DIALOG_MODAL,
- GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Cannot save: %s", err->message);
- gtk_dialog_run(GTK_DIALOG(m));
- gtk_widget_destroy(m);
- g_error_free(err);
- }
- g_object_unref(src);
- g_object_unref(dst);
- }
- gtk_widget_destroy(d);
- }
- static void fit_clicked(GtkWidget *w, gpointer data) {
- (void)w;
- (void)data;
- app.need_fit = TRUE;
- gtk_widget_queue_draw(app.map);
- }
- /* ===== Setup ===== */
- static char *find_cli(void) {
- char *self = g_file_read_link("/proc/self/exe", NULL);
- if (self) {
- char *dir = g_path_get_dirname(self);
- char *path = g_build_filename(dir, "gps-test-tool", NULL);
- g_free(dir);
- g_free(self);
- if (g_file_test(path, G_FILE_TEST_IS_EXECUTABLE))
- return path;
- g_free(path);
- }
- return g_find_program_in_path("gps-test-tool");
- }
- static GtkWidget *section(GtkWidget *box, const char *title) {
- GtkWidget *frame = gtk_frame_new(title);
- GtkWidget *inner = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
- gtk_container_set_border_width(GTK_CONTAINER(inner), 6);
- gtk_container_add(GTK_CONTAINER(frame), inner);
- gtk_box_pack_start(GTK_BOX(box), frame, FALSE, FALSE, 0);
- return inner;
- }
- static GtkWidget *check(GtkWidget *box, const char *label, const char *group, const char *key,
- gboolean def, GCallback cb) {
- GtkWidget *w = gtk_check_button_new_with_label(label);
- GError *err = NULL;
- gboolean v = g_key_file_get_boolean(app.config, group, key, &err);
- if (err) {
- g_error_free(err);
- v = def;
- }
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), v);
- g_signal_connect(w, "toggled", cb, NULL);
- gtk_box_pack_start(GTK_BOX(box), w, FALSE, FALSE, 0);
- return w;
- }
- static void spin(GtkWidget *box, struct setting_spin *s) {
- GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- int v = CLAMP(config_get_int("settings", s->key, s->def), s->min, s->max);
- s->widget = gtk_spin_button_new_with_range(s->min, s->max, 1);
- gtk_spin_button_set_value(GTK_SPIN_BUTTON(s->widget), v);
- g_signal_connect(s->widget, "value-changed", G_CALLBACK(setting_changed), NULL);
- gtk_box_pack_start(GTK_BOX(row), gtk_label_new(s->label), FALSE, FALSE, 0);
- gtk_box_pack_end(GTK_BOX(row), s->widget, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0);
- }
- static void build_ui(void) {
- GtkWidget *header, *button, *hbox, *side, *sec, *vbox, *bar, *scroll;
- PangoAttrList *attrs;
- unsigned i;
- app.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- gtk_window_set_default_size(GTK_WINDOW(app.window),
- config_get_int("view", "width", 1200), config_get_int("view", "height", 800));
- g_signal_connect(app.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
- header = gtk_header_bar_new();
- gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header), TRUE);
- gtk_header_bar_set_title(GTK_HEADER_BAR(header), "GPS Test Tool");
- button = gtk_button_new_with_mnemonic("_Open NMEA…");
- g_signal_connect(button, "clicked", G_CALLBACK(open_clicked), NULL);
- gtk_header_bar_pack_start(GTK_HEADER_BAR(header), button);
- app.save_button = gtk_button_new_with_mnemonic("_Save GPX…");
- gtk_widget_set_sensitive(app.save_button, FALSE);
- g_signal_connect(app.save_button, "clicked", G_CALLBACK(save_clicked), NULL);
- gtk_header_bar_pack_start(GTK_HEADER_BAR(header), app.save_button);
- gtk_window_set_titlebar(GTK_WINDOW(app.window), header);
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
- side = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
- gtk_container_set_border_width(GTK_CONTAINER(side), 8);
- scroll = gtk_scrolled_window_new(NULL, NULL);
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
- gtk_container_add(GTK_CONTAINER(scroll), side);
- gtk_widget_set_size_request(scroll, 270, -1);
- gtk_box_pack_start(GTK_BOX(hbox), scroll, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(hbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
- app.loading_settings = TRUE;
- sec = section(side, "Filtering");
- app.chk_disable_filters = check(sec, "Disable filters", "settings", "disable_filters", FALSE, G_CALLBACK(setting_changed));
- spin(sec, &spins[SPIN_SKIP]);
- spin(sec, &spins[SPIN_MIN_SATS]);
- sec = section(side, "Auto-pause");
- app.chk_auto_pause = check(sec, "Enabled", "settings", "auto_pause", TRUE, G_CALLBACK(setting_changed));
- for (i = SPIN_AP_FIRST; i < SPIN_COUNT; i++) {
- spin(sec, &spins[i]);
- gtk_widget_set_sensitive(spins[i].widget,
- gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.chk_auto_pause)));
- }
- sec = section(side, "Display");
- app.chk_show_nmea = check(sec, "NMEA input (grey)", "view", "show_nmea", TRUE, G_CALLBACK(view_changed));
- app.chk_show_gpx = check(sec, "GPX output (colour per segment)", "view", "show_gpx", TRUE, G_CALLBACK(view_changed));
- button = gtk_button_new_with_label("Zoom to fit");
- g_signal_connect(button, "clicked", G_CALLBACK(fit_clicked), NULL);
- gtk_box_pack_start(GTK_BOX(sec), button, FALSE, FALSE, 0);
- app.loading_settings = FALSE;
- sec = section(side, "Summary");
- app.summary_label = gtk_label_new("");
- gtk_label_set_selectable(GTK_LABEL(app.summary_label), TRUE);
- gtk_label_set_xalign(GTK_LABEL(app.summary_label), 0);
- gtk_label_set_line_wrap(GTK_LABEL(app.summary_label), TRUE);
- attrs = pango_attr_list_new();
- pango_attr_list_insert(attrs, pango_attr_family_new("monospace"));
- pango_attr_list_insert(attrs, pango_attr_scale_new(0.85));
- gtk_label_set_attributes(GTK_LABEL(app.summary_label), attrs);
- pango_attr_list_unref(attrs);
- gtk_box_pack_start(GTK_BOX(sec), app.summary_label, FALSE, FALSE, 0);
- app.map = gtk_drawing_area_new();
- gtk_widget_add_events(app.map, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
- GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK | GDK_LEAVE_NOTIFY_MASK);
- g_signal_connect(app.map, "draw", G_CALLBACK(map_draw), NULL);
- g_signal_connect(app.map, "scroll-event", G_CALLBACK(map_scroll), NULL);
- g_signal_connect(app.map, "button-press-event", G_CALLBACK(map_button_press), NULL);
- g_signal_connect(app.map, "button-release-event", G_CALLBACK(map_button_release), NULL);
- g_signal_connect(app.map, "motion-notify-event", G_CALLBACK(map_motion), NULL);
- g_signal_connect(app.map, "leave-notify-event", G_CALLBACK(map_leave), NULL);
- gtk_box_pack_start(GTK_BOX(hbox), app.map, TRUE, TRUE, 0);
- bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
- gtk_container_set_border_width(GTK_CONTAINER(bar), 4);
- app.coord_label = gtk_label_new("");
- gtk_label_set_selectable(GTK_LABEL(app.coord_label), TRUE);
- gtk_box_pack_start(GTK_BOX(bar), app.coord_label, FALSE, FALSE, 4);
- app.status_label = gtk_label_new("Open an NMEA log to start");
- gtk_box_pack_end(GTK_BOX(bar), app.status_label, FALSE, FALSE, 4);
- gtk_box_pack_start(GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(vbox), bar, FALSE, FALSE, 0);
- gtk_container_add(GTK_CONTAINER(app.window), vbox);
- }
- static gboolean window_configure(GtkWidget *w, GdkEvent *ev, gpointer data) {
- int width, height;
- (void)ev;
- (void)data;
- gtk_window_get_size(GTK_WINDOW(w), &width, &height);
- g_key_file_set_integer(app.config, "view", "width", width);
- g_key_file_set_integer(app.config, "view", "height", height);
- return FALSE;
- }
- static gboolean quit_signal(gpointer data) {
- (void)data;
- gtk_main_quit();
- return G_SOURCE_REMOVE;
- }
- int main(int argc, char *argv[]) {
- char *input;
- gtk_init(&argc, &argv);
- app.config = g_key_file_new();
- app.config_path = g_build_filename(g_get_user_config_dir(), "gps-test-tool", "gui.ini", NULL);
- g_key_file_load_from_file(app.config, app.config_path, G_KEY_FILE_NONE, NULL);
- app.cache_dir = g_build_filename(g_get_user_cache_dir(), "gps-test-tool", "tiles", NULL);
- app.cli_path = find_cli();
- app.tmpdir = g_dir_make_tmp("gps-test-tool-gui-XXXXXX", NULL);
- if (!app.cli_path || !app.tmpdir) {
- fprintf(stderr, "Error: %s\n", app.cli_path ? "cannot create temporary directory"
- : "gps-test-tool binary not found next to this program or in PATH");
- return 1;
- }
- app.out_gpx = g_build_filename(app.tmpdir, "output.gpx", NULL);
- app.out_log = g_build_filename(app.tmpdir, "debug.txt", NULL);
- app.tiles = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, tile_free);
- g_queue_init(&app.dl_queue);
- app.soup = soup_session_new_with_options("user-agent", USER_AGENT,
- "max-conns-per-host", MAX_DOWNLOADS, NULL);
- app.zoom = MIN_ZOOM;
- app.cx = app.cy = world_size(app.zoom) / 2;
- build_ui();
- g_signal_connect(app.window, "configure-event", G_CALLBACK(window_configure), NULL);
- gtk_widget_show_all(app.window);
- update_coord_label();
- input = argc > 1 ? g_strdup(argv[1]) : g_key_file_get_string(app.config, "view", "input_file", NULL);
- if (input && (argc > 1 || g_file_test(input, G_FILE_TEST_EXISTS)))
- open_file(input);
- g_free(input);
- /* Clean up the temporary directory on Ctrl+C / kill too */
- g_unix_signal_add(SIGINT, quit_signal, NULL);
- g_unix_signal_add(SIGTERM, quit_signal, NULL);
- gtk_main();
- if (app.running)
- kill(app.child, SIGTERM);
- config_save();
- g_unlink(app.out_gpx);
- g_unlink(app.out_log);
- g_rmdir(app.tmpdir);
- return 0;
- }
|