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 }