/ src / loc-tree.c
loc-tree.c
  1  /*
  2    Gpredict: Real-time satellite tracking and orbit prediction program
  3  
  4    Copyright (C)  2001-2017  Alexandru Csete, OZ9AEC.
  5  
  6    This program is free software; you can redistribute it and/or modify
  7    it under the terms of the GNU General Public License as published by
  8    the Free Software Foundation; either version 2 of the License, or
  9    (at your option) any later version.
 10    
 11    This program is distributed in the hope that it will be useful,
 12    but WITHOUT ANY WARRANTY; without even the implied warranty of
 13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14    GNU General Public License for more details.
 15    
 16    You should have received a copy of the GNU General Public License
 17    along with this program; if not, visit http://www.fsf.org/
 18  */
 19  
 20  /** Tree widget containing locations info */
 21  
 22  #ifdef HAVE_CONFIG_H
 23  #include <build-config.h>
 24  #endif
 25  #include <glib.h>
 26  #include <glib/gi18n.h>
 27  #include <gtk/gtk.h>
 28  #include <math.h>
 29  #include "compat.h"
 30  #include "loc-tree.h"
 31  #include "sat-cfg.h"
 32  #include "sat-log.h"
 33  
 34  
 35  /* long story... */
 36  #define LTMN   123456.7
 37  #define LTMNI  123456
 38  #define LTEPS  1.0
 39  
 40  
 41  static GtkTreeModel *loc_tree_create_and_fill_model(const gchar * fname);
 42  
 43  static void     loc_tree_float_cell_data_function(GtkTreeViewColumn * col,
 44                                                    GtkCellRenderer * renderer,
 45                                                    GtkTreeModel * model,
 46                                                    GtkTreeIter * iter,
 47                                                    gpointer column);
 48  
 49  static void     loc_tree_int_cell_data_function(GtkTreeViewColumn * col,
 50                                                  GtkCellRenderer * renderer,
 51                                                  GtkTreeModel * model,
 52                                                  GtkTreeIter * iter,
 53                                                  gpointer column);
 54  
 55  static gboolean loc_tree_check_selection_cb(GtkTreeSelection * selection,
 56                                              GtkTreeModel * model,
 57                                              GtkTreePath * path,
 58                                              gboolean selpath, gpointer dialog);
 59  
 60  static void     loc_tree_get_selection(GtkWidget * view,
 61                                         gchar ** loc,
 62                                         gfloat * lat,
 63                                         gfloat * lon, guint * alt, gchar ** wx);
 64  
 65  /**
 66   * Create and initialise location selector.
 67   *
 68   * @param fname The name of the file, which contains locations data. Can be NULL.
 69   * @param flags Bitise or of flags indicating which columns to display.
 70   * @param location Newly allocated string containing location (city, country)
 71   * @param lat Pointer to where the latitude should be stored.
 72   * @param lon Pointer to where the longitude should be stored.
 73   * @param alt Pointer to where the altitude should be stored.
 74   * @param wx Newly allocated string containing the four letter weather station name.
 75   * @return TRUE if a location has been selected and the returned data is valid,
 76   *         FALSE otherwise, fx. if the user has clicked on the Cancel button.
 77   *
 78   * @note All data fields will be populated (and both strings allocated) no matter which
 79   *       flags have been passed by the user. The flags influence only how the tree view
 80   *       is displayed.
 81   */
 82  gboolean loc_tree_create(const gchar * fname,
 83                           guint flags,
 84                           gchar ** loc,
 85                           gfloat * lat, gfloat * lon, guint * alt, gchar ** wx)
 86  {
 87      GtkCellRenderer *renderer;  /* tree view cell renderer */
 88      GtkTreeViewColumn *column;  /* tree view column used to add columns */
 89      GtkTreeModel   *model;      /* tree model */
 90      GtkWidget      *view;       /* tree view widget */
 91      GtkTreeSelection *selection;        /* used to set selection checking func */
 92      GtkWidget      *swin;       /* scrolled window widget */
 93      GtkWidget      *dialog;     /* the dialog widget */
 94      gint            response;   /* response ID returned by gtk_dialog_run */
 95      gchar          *ffname;
 96      gboolean        retval;
 97  
 98      if (!fname)
 99          ffname = data_file_name("locations.dat");
100      else
101          ffname = g_strdup(fname);
102  
103      view = gtk_tree_view_new();
104  
105      /* Create columns.
106         Note that there are several ways to create and add the individual
107         columns, especially there are tree_view_insert_col functions, which
108         do not require explicit creation of columns. I have chosen to
109         explicitly create the columns in order to be able to hide them
110         according to the flags parameter.
111       */
112  
113      /* --- Column #1 --- */
114      renderer = gtk_cell_renderer_text_new();
115      column = gtk_tree_view_column_new_with_attributes(_("Location"),
116                                                        renderer,
117                                                        "text", TREE_COL_NAM,
118                                                        NULL);
119      gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, -1);
120      if (!(flags & TREE_COL_FLAG_NAME))
121      {
122          gtk_tree_view_column_set_visible(column, FALSE);
123      }
124  
125      /* --- Column #2 --- */
126      renderer = gtk_cell_renderer_text_new();
127      column = gtk_tree_view_column_new_with_attributes(_("Lat"),
128                                                        renderer,
129                                                        "text", TREE_COL_LAT,
130                                                        NULL);
131      gtk_tree_view_column_set_alignment(column, 0.5);
132      gtk_tree_view_column_set_cell_data_func(column,
133                                              renderer,
134                                              loc_tree_float_cell_data_function,
135                                              GUINT_TO_POINTER(TREE_COL_LAT),
136                                              NULL);
137      gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, -1);
138      if (!(flags & TREE_COL_FLAG_LAT))
139      {
140          gtk_tree_view_column_set_visible(column, FALSE);
141      }
142  
143      /* --- Column #3 --- */
144      renderer = gtk_cell_renderer_text_new();
145      column = gtk_tree_view_column_new_with_attributes(_("Lon"),
146                                                        renderer,
147                                                        "text", TREE_COL_LON,
148                                                        NULL);
149      gtk_tree_view_column_set_alignment(column, 0.5);
150      gtk_tree_view_column_set_cell_data_func(column,
151                                              renderer,
152                                              loc_tree_float_cell_data_function,
153                                              GUINT_TO_POINTER(TREE_COL_LON),
154                                              NULL);
155      gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, -1);
156      if (!(flags & TREE_COL_FLAG_LON))
157      {
158          gtk_tree_view_column_set_visible(column, FALSE);
159      }
160  
161      /* --- Column #4 --- */
162      renderer = gtk_cell_renderer_text_new();
163      column = gtk_tree_view_column_new_with_attributes(_("Alt"),
164                                                        renderer,
165                                                        "text", TREE_COL_ALT,
166                                                        NULL);
167      gtk_tree_view_column_set_alignment(column, 0.5);
168      gtk_tree_view_column_set_cell_data_func(column,
169                                              renderer,
170                                              loc_tree_int_cell_data_function,
171                                              GUINT_TO_POINTER(TREE_COL_ALT),
172                                              NULL);
173      gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, -1);
174      if (!(flags & TREE_COL_FLAG_ALT))
175      {
176          gtk_tree_view_column_set_visible(column, FALSE);
177      }
178  
179      /* --- Column #5 --- */
180      renderer = gtk_cell_renderer_text_new();
181      column = gtk_tree_view_column_new_with_attributes(_("WX"),
182                                                        renderer,
183                                                        "text", TREE_COL_WX,
184                                                        NULL);
185      gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, -1);
186      if (!(flags & TREE_COL_FLAG_WX))
187      {
188          gtk_tree_view_column_set_visible(column, FALSE);
189      }
190  
191      /* Invisible column holding 0 or 1 indicating whether a row can be selected
192         or not. We use this to prevent the user from selecting regions or
193         countries, since they are not valid locations.
194       */
195      renderer = gtk_cell_renderer_text_new();
196      column = gtk_tree_view_column_new_with_attributes(_("X"),
197                                                        renderer,
198                                                        "text", TREE_COL_SELECT,
199                                                        NULL);
200      gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, -1);
201      gtk_tree_view_column_set_visible(column, FALSE);
202  
203      /* create model and finalise treeview */
204      model = loc_tree_create_and_fill_model(ffname);
205  
206      /* we are done with it */
207      g_free(ffname);
208  
209      gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
210  
211      /* The tree view has acquired its own reference to the
212       *  model, so we can drop ours. That way the model will
213       *  be freed automatically when the tree view is destroyed */
214      g_object_unref(model);
215  
216      /* make sure rows are checked when they are selected */
217      /* ... but first create the dialog window .... */
218  
219      /* scrolled window */
220      swin = gtk_scrolled_window_new(NULL, NULL);
221      gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin),
222                                     GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
223      gtk_container_add(GTK_CONTAINER(swin), view);
224  
225      gtk_widget_show_all(swin);
226  
227      /* dialog window */
228      dialog = gtk_dialog_new_with_buttons(_("Select Location"),
229                                           NULL,
230                                           GTK_DIALOG_MODAL,
231                                           "_Cancel", GTK_RESPONSE_REJECT,
232                                           "_OK", GTK_RESPONSE_ACCEPT,
233                                           NULL);
234  
235      gtk_window_set_default_size(GTK_WINDOW(dialog), 450, 400);
236  
237      /* OK button disabled by default until a valid selection is made */
238      gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog),
239                                        GTK_RESPONSE_ACCEPT, FALSE);
240      gtk_box_pack_start(GTK_BOX
241                         (gtk_dialog_get_content_area(GTK_DIALOG(dialog))), swin,
242                         TRUE, TRUE, 0);
243  
244      /* connect selection checker for the tree-view;
245         we have waited so far, because we want to pass the dialog as
246         parameter
247       */
248      selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
249      gtk_tree_selection_set_select_function(selection,
250                                             loc_tree_check_selection_cb,
251                                             dialog, NULL);
252  
253      response = gtk_dialog_run(GTK_DIALOG(dialog));
254      if (response == GTK_RESPONSE_ACCEPT)
255      {
256          loc_tree_get_selection(view, loc, lat, lon, alt, wx);
257          sat_log_log(SAT_LOG_LEVEL_INFO, _("%s: Selected %s"), __func__, *loc);
258          retval = TRUE;
259      }
260      else
261      {
262          sat_log_log(SAT_LOG_LEVEL_INFO, _("%s: No location selected"),
263                      __func__);
264          retval = FALSE;
265      }
266  
267      gtk_widget_destroy(dialog);
268  
269      return retval;
270  }
271  
272  static GtkTreeModel *loc_tree_create_and_fill_model(const gchar * fname)
273  {
274      GtkTreeStore   *treestore;  /* tree store, which is loaded and returned */
275      GtkTreeIter     toplevel;   /* highest level rows, continent or region */
276      GtkTreeIter     midlevel;   /* mid level rows, country or state in the US */
277      GtkTreeIter     child;      /* lowest level rows, cities */
278      GIOChannel     *locfile;    /* file we read locations from */
279      gchar          *line;       /* line read from file */
280      gchar         **buff;       /* temporary buffer to store line pieces */
281      gsize           length;     /* line length */
282      guint           i = 0;      /* number of lines read */
283      gchar          *continent = g_strdup("DUMMY");      /* current continent */
284      gchar          *country = g_strdup("DUMMY");        /* current country */
285      GError         *error = NULL;       /* error data when reading file */
286  
287      treestore = gtk_tree_store_new(TREE_COL_NUM,
288                                     G_TYPE_STRING,
289                                     G_TYPE_FLOAT,
290                                     G_TYPE_FLOAT,
291                                     G_TYPE_UINT, G_TYPE_STRING, G_TYPE_UINT);
292  
293      /* if the supplied file does not exist
294         simply return the empty model
295         FIXME: should we fall back to PACKAGE_DATA_DIR/locations.dat ?
296       */
297      if (!g_file_test(fname, G_FILE_TEST_EXISTS))
298      {
299  
300          sat_log_log(SAT_LOG_LEVEL_ERROR,
301                      _("%s: %s does not exist!"), __func__, fname);
302  
303          g_free(continent);
304          g_free(country);
305          return GTK_TREE_MODEL(treestore);
306      }
307  
308      /* open file and read it line by line */
309      locfile = g_io_channel_new_file(fname, "r", &error);
310  
311      if (locfile)
312      {
313  
314          while (g_io_channel_read_line(locfile,
315                                        &line,
316                                        &length, NULL, NULL) != G_IO_STATUS_EOF)
317          {
318  
319              /* trim line and split it */
320              line = g_strdelimit(line, "\n", '\0');
321  
322              buff = g_strsplit(line, ";", 7);
323  
324              /* buff[0] = continent / region
325                 buff[1] = country or state in US
326                 buff[2] = city
327                 buff[3] = weather station
328                 buff[4] = latitude (dec. deg. north)
329                 buff[5] = longitude (dec. deg. east)
330                 buff[6] = altitude
331               */
332  
333              /* new region? */
334              if (g_ascii_strcasecmp(buff[0], continent))
335              {
336  
337                  g_free(continent);
338  
339                  continent = g_strdup(buff[0]);
340  
341                  gtk_tree_store_append(treestore, &toplevel, NULL);
342                  gtk_tree_store_set(treestore, &toplevel,
343                                     TREE_COL_NAM, continent,
344                                     TREE_COL_LAT, LTMN,
345                                     TREE_COL_LON, LTMN,
346                                     TREE_COL_ALT, LTMNI,
347                                     TREE_COL_SELECT, 0, -1);
348  
349              }
350  
351              /* new country? */
352              if (g_ascii_strcasecmp(buff[1], country))
353              {
354  
355                  g_free(country);
356  
357                  country = g_strdup(buff[1]);
358  
359                  gtk_tree_store_append(treestore, &midlevel, &toplevel);
360                  gtk_tree_store_set(treestore, &midlevel,
361                                     TREE_COL_NAM, country,
362                                     TREE_COL_LAT, LTMN,
363                                     TREE_COL_LON, LTMN,
364                                     TREE_COL_ALT, LTMNI,
365                                     TREE_COL_SELECT, 0, -1);
366              }
367  
368              /* add city */
369              gtk_tree_store_append(treestore, &child, &midlevel);
370              gtk_tree_store_set(treestore, &child,
371                                 TREE_COL_NAM, buff[2],
372                                 TREE_COL_WX, buff[3],
373                                 TREE_COL_LAT, g_ascii_strtod(buff[4], NULL),
374                                 TREE_COL_LON, g_ascii_strtod(buff[5], NULL),
375                                 /* Crashes here if type is not correctly cast */
376                                 TREE_COL_ALT, (guint) g_ascii_strtod(buff[6],
377                                                                      NULL),
378                                 TREE_COL_SELECT, 1, -1);
379  
380  
381              /* finish and clean up */
382              i++;
383  
384              /* free allocated memory */
385              g_free(line);
386              g_strfreev(buff);
387          }
388  
389          sat_log_log(SAT_LOG_LEVEL_DEBUG,
390                      _("%s: Read %d cities."), __func__, i);
391  
392          if (continent)
393              g_free(continent);
394  
395          if (country)
396              g_free(country);
397  
398          /* Close IO channel; don't care about status.
399             Shutdown will flush the stream and close the channel
400             as soon as the reference count is dropped. Order matters!
401           */
402          g_io_channel_shutdown(locfile, TRUE, NULL);
403          g_io_channel_unref(locfile);
404      }
405      else
406      {
407          sat_log_log(SAT_LOG_LEVEL_ERROR,
408                      _("%s: Failed to open locfile (%s)"),
409                      __func__, error->message);
410          g_clear_error(&error);
411      }
412  
413      return GTK_TREE_MODEL(treestore);
414  }
415  
416  /* render column containing float
417     by using this instead of the default data function, we can
418     disable lat,lon and alt for the continent and country rows.
419  
420     Please note that this function only affects how the numbers are
421     displayed (rendered), the tree_store will still contain the
422     original floating point numbers. Very cool!
423  */
424  static void loc_tree_float_cell_data_function(GtkTreeViewColumn * col,
425                                                GtkCellRenderer * renderer,
426                                                GtkTreeModel * model,
427                                                GtkTreeIter * iter,
428                                                gpointer column)
429  {
430      gfloat          number;
431      gchar          *buff;
432      guint           coli = GPOINTER_TO_UINT(column);
433      gchar           hmf = ' ';
434  
435      (void)col;
436  
437      gtk_tree_model_get(model, iter, coli, &number, -1);
438  
439      /* check whether configuration requests the use
440         of N, S, E and W instead of signs
441       */
442      if (sat_cfg_get_bool(SAT_CFG_BOOL_USE_NSEW))
443      {
444          if (coli == TREE_COL_LAT)
445          {
446              if (number < 0.0)
447              {
448                  number *= -1.0;
449                  hmf = 'S';
450              }
451              else
452              {
453                  hmf = 'N';
454              }
455          }
456          else if (coli == TREE_COL_LON)
457          {
458              if (number < 0.0)
459              {
460                  number *= -1.0;
461                  hmf = 'W';
462              }
463              else
464              {
465                  hmf = 'E';
466              }
467          }
468          else
469          {
470              sat_log_log(SAT_LOG_LEVEL_ERROR,
471                          _("%s: Invalid column: %d"), __func__, coli);
472              hmf = '?';
473          }
474      }
475  
476      if (fabs(LTMN - number) > LTEPS)
477          buff = g_strdup_printf("%.4f\302\260%c", number, hmf);
478      else
479          buff = g_strdup("");
480  
481      g_object_set(renderer, "text", buff, NULL);
482      g_free(buff);
483  }
484  
485  /** Render column containing integer */
486  static void loc_tree_int_cell_data_function(GtkTreeViewColumn * col,
487                                              GtkCellRenderer * renderer,
488                                              GtkTreeModel * model,
489                                              GtkTreeIter * iter,
490                                              gpointer column)
491  {
492      gint            number;
493      gchar          *buff;
494      guint           coli = GPOINTER_TO_UINT(column);
495  
496      (void)col;
497  
498      gtk_tree_model_get(model, iter, GPOINTER_TO_UINT(column), &number, -1);
499  
500      if (coli == TREE_COL_ALT)
501      {
502          if (number != LTMNI)
503              buff = g_strdup_printf("%d", number);
504          else
505              buff = g_strdup("");
506      }
507      else
508      {
509          buff = g_strdup_printf("%d", number);
510      }
511  
512      g_object_set(renderer, "text", buff, NULL);
513      g_free(buff);
514  }
515  
516  /**
517   * Check current selection.
518   *
519   * This function is used to check the currently selected row. This is to avoid
520   * selection of region and countries. The function is called as a callback function
521   * every time a row is selected.
522   * The decision is based on the integer value stored in the invisible column
523   * TREE_COL_SELECT. A value of 0 means row may not be selected, while a value of 1
524   * means that the row can be selected.
525   */
526  static gboolean loc_tree_check_selection_cb(GtkTreeSelection * selection,
527                                              GtkTreeModel * model,
528                                              GtkTreePath * path,
529                                              gboolean sel_path, gpointer dialog)
530  {
531  
532      GtkTreeIter     iter;
533  
534      (void)selection;
535      (void)sel_path;
536  
537      if (gtk_tree_model_get_iter(model, &iter, path))
538      {
539          guint           value;
540  
541          gtk_tree_model_get(model, &iter, TREE_COL_SELECT, &value, -1);
542  
543          if (value)
544          {
545              gtk_dialog_set_response_sensitive(GTK_DIALOG(GTK_WIDGET(dialog)),
546                                                GTK_RESPONSE_ACCEPT, TRUE);
547              return TRUE;
548          }
549      }
550  
551      return FALSE;
552  }
553  
554  /** get data fom selected row. */
555  static void loc_tree_get_selection(GtkWidget * view,
556                                     gchar ** loc,
557                                     gfloat * lat, gfloat * lon, guint * alt,
558                                     gchar ** wx)
559  {
560      GtkTreeSelection *selection;
561      GtkTreeModel   *model;
562      GtkTreeIter     iter;
563      GtkTreeIter     parent;
564      gchar          *city;
565      gchar          *country;
566  
567      selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
568      if (gtk_tree_selection_get_selected(selection, &model, &iter))
569      {
570          /* get values */
571          gtk_tree_model_get(model, &iter,
572                             TREE_COL_NAM, &city,
573                             TREE_COL_LAT, lat,
574                             TREE_COL_LON, lon,
575                             TREE_COL_ALT, alt, TREE_COL_WX, wx, -1);
576  
577          /* Location string shall be composed of "City, Country".
578             Currently we have City in _loc1 and so we need to obtain
579             the parent. */
580          if (gtk_tree_model_iter_parent(model, &parent, &iter))
581          {
582              gtk_tree_model_get(model, &parent, TREE_COL_NAM, &country, -1);
583  
584              *loc = g_strconcat(city, ", ", country, NULL);
585              g_free(city);
586              g_free(country);
587          }
588          else
589          {
590              /* well no luck; send a warning message and return city
591                 only (actually, this is a bug, if it happens).
592               */
593              sat_log_log(SAT_LOG_LEVEL_ERROR,
594                          _("%s: Failed to get parent for %s."), __func__, city);
595  
596              *loc = g_strdup(city);
597              g_free(city);
598          }
599      }
600      else
601      {
602          /* nothing selected; this function should not have been called
603             => BUG!
604           */
605          sat_log_log(SAT_LOG_LEVEL_ERROR,
606                      _("%s: No selection found!"), __func__);
607      }
608  }