2 * Copyright (C) 2010-2011 Andy Spencer <andy753421@gmail.com>
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 #include "alert-info.h"
29 #define MSG_INDEX "http://alerts.weather.gov/cap/us.php?x=0"
30 #define CONFIG_HEIGHT 3
32 /* Format for single cap alert:
33 * "http://alerts.weather.gov/cap/wwacapget.php?x="
34 * "AK20111012000500CoastalFloodWarning20111012151500"
35 * "AKAFGCFWAFG.6258a84eb1e8c34cd888057248224d10" */
40 /* AlertMsg data types */
43 char class [1 ]; char sep1;
44 char action[3 ]; char sep2;
45 char office[4 ]; char sep3;
46 char phenom[2 ]; char sep4;
47 char signif[1 ]; char sep5;
48 char event [4 ]; char sep6;
49 char begin [12]; char sep7;
50 char end [12]; char sep8;
54 /* Basic info (from alert index) */
55 char *title; // Winter Weather Advisory issued December 19 at 8:51PM
56 char *link; // http://www.weather.gov/alerts-beta/wwacapget.php?x=AK20101219205100AFGWinterWeatherAdvisoryAFG20101220030000AK
57 char *summary; // ...WINTER WEATHER ADVISORY REMAINS IN EFFECT UNTIL 6
59 time_t effective; // 2010-12-19T20:51:00-09:00
60 time_t expires; // 2010-12-20T03:00:00-09:00
61 char *status; // Actual
62 char *urgency; // Expected
63 char *severity; // Minor
64 char *certainty; // Likely
65 char *area_desc; // Northeastern Brooks Range; Northwestern Brooks Range
66 char *fips6; // 006015 006023 006045 006105
67 AlertVtec *vtec; // /X.CON.PAFG.WW.Y.0064.000000T0000Z-101220T0300Z/
70 /* Advanced info (from CAP file) */
76 AlertInfo *info; // Link to info structure for this alert
77 GritsPoly *county_based; // Polygon for county-based warning
78 GritsPoly *storm_based; // Polygon for storm-based warning
81 /* AlertMsg parsing */
89 time_t msg_parse_time(gchar *iso8601)
92 g_time_val_from_iso8601(iso8601, &tv);
95 AlertVtec *msg_parse_vtec(char *buf)
97 AlertVtec *vtec = g_new0(AlertVtec, 1);
98 strncpy((char*)vtec, buf, sizeof(AlertVtec));
99 vtec->sep0 = vtec->sep1 = vtec->sep2 = '\0';
100 vtec->sep3 = vtec->sep4 = vtec->sep5 = '\0';
101 vtec->sep6 = vtec->sep7 = vtec->sep8 = '\0';
104 void msg_parse_text(GMarkupParseContext *context, const gchar *text,
105 gsize len, gpointer user_data, GError **error)
107 //g_debug("text %s", text);
108 ParseData *data = user_data;
111 data->text = g_strndup(text, len);
113 void msg_parse_index_start(GMarkupParseContext *context, const gchar *name,
114 const gchar **keys, const gchar **vals,
115 gpointer user_data, GError **error)
117 //g_debug("start %s", name);
118 ParseData *data = user_data;
119 if (g_str_equal(name, "entry"))
120 data->msg = g_new0(AlertMsg, 1);
122 void msg_parse_index_end(GMarkupParseContext *context, const gchar *name,
123 gpointer user_data, GError **error)
125 //g_debug("end %s", name);
126 ParseData *data = user_data;
127 AlertMsg *msg = data->msg;
128 char *text = data->text;
130 if (g_str_equal(name, "updated") && text && !data->updated)
131 data->updated = msg_parse_time(text);
133 if (g_str_equal(name, "entry"))
134 data->msgs = g_list_prepend(data->msgs, data->msg);
136 if (!text || !msg) return;
137 else if (g_str_equal(name, "title")) msg->title = g_strdup(text);
138 else if (g_str_equal(name, "id")) msg->link = g_strdup(text); // hack
139 else if (g_str_equal(name, "summary")) msg->summary = g_strdup(text);
140 else if (g_str_equal(name, "cap:effective")) msg->cap.effective = msg_parse_time(text);
141 else if (g_str_equal(name, "cap:expires")) msg->cap.expires = msg_parse_time(text);
142 else if (g_str_equal(name, "cap:status")) msg->cap.status = g_strdup(text);
143 else if (g_str_equal(name, "cap:urgency")) msg->cap.urgency = g_strdup(text);
144 else if (g_str_equal(name, "cap:severity")) msg->cap.severity = g_strdup(text);
145 else if (g_str_equal(name, "cap:certainty")) msg->cap.certainty = g_strdup(text);
146 else if (g_str_equal(name, "cap:areaDesc")) msg->cap.area_desc = g_strdup(text);
148 if (g_str_equal(name, "title"))
149 msg->info = alert_info_find(msg->title);
151 if (g_str_equal(name, "valueName")) {
152 if (data->value_name)
153 g_free(data->value_name);
154 data->value_name = g_strdup(text);
157 if (g_str_equal(name, "value") && data->value_name) {
158 if (g_str_equal(data->value_name, "FIPS6")) msg->cap.fips6 = g_strdup(text);
159 if (g_str_equal(data->value_name, "VTEC")) msg->cap.vtec = msg_parse_vtec(text);
162 void msg_parse_cap_end(GMarkupParseContext *context, const gchar *name,
163 gpointer user_data, GError **error)
165 ParseData *data = user_data;
166 AlertMsg *msg = data->msg;
167 char *text = data->text;
168 if (!text || !msg) return;
169 if (g_str_equal(name, "description")) msg->description = g_strdup(text);
170 else if (g_str_equal(name, "instruction")) msg->instruction = g_strdup(text);
171 else if (g_str_equal(name, "polygon")) msg->polygon = g_strdup(text);
174 /* AlertMsg methods */
175 GList *msg_parse_index(gchar *text, gsize len, time_t *updated)
177 g_debug("GritsPluginAlert: msg_parse");
178 GMarkupParser parser = {
179 .start_element = msg_parse_index_start,
180 .end_element = msg_parse_index_end,
181 .text = msg_parse_text,
184 GMarkupParseContext *context =
185 g_markup_parse_context_new(&parser, 0, &data, NULL);
186 g_markup_parse_context_parse(context, text, len, NULL);
187 g_markup_parse_context_free(context);
191 g_free(data.value_name);
192 *updated = data.updated;
196 void msg_parse_cap(AlertMsg *msg, gchar *text, gsize len)
198 g_debug("GritsPluginAlert: msg_parse_cap");
199 GMarkupParser parser = {
200 .end_element = msg_parse_cap_end,
201 .text = msg_parse_text,
203 ParseData data = { .msg = msg };
204 GMarkupParseContext *context =
205 g_markup_parse_context_new(&parser, 0, &data, NULL);
206 g_markup_parse_context_parse(context, text, len, NULL);
207 g_markup_parse_context_free(context);
211 /* Add spaces to description... */
212 static GRegex *regex = NULL;
214 regex = g_regex_new("\\.\\n", 0, G_REGEX_MATCH_NEWLINE_ANY, NULL);
215 if (msg->description && regex) {
216 char *old = msg->description;
217 msg->description = g_regex_replace_literal(
218 regex, old, -1, 0, ".\n\n", 0, NULL);
223 void msg_free(AlertMsg *msg)
227 g_free(msg->summary);
228 g_free(msg->cap.status);
229 g_free(msg->cap.urgency);
230 g_free(msg->cap.severity);
231 g_free(msg->cap.certainty);
232 g_free(msg->cap.area_desc);
233 g_free(msg->cap.fips6);
234 g_free(msg->cap.vtec);
235 g_free(msg->description);
236 g_free(msg->instruction);
237 g_free(msg->polygon);
241 void msg_print(GList *msgs)
243 g_message("msg_print");
244 for (GList *cur = msgs; cur; cur = cur->next) {
245 AlertMsg *msg = cur->data;
247 g_message(" title = %s", msg->title );
248 g_message(" link = %s", msg->link );
249 g_message(" summary = %s", msg->summary );
250 g_message(" cat.effective = %lu", msg->cap.effective);
251 g_message(" cat.expires = %lu", msg->cap.expires );
252 g_message(" cat.status = %s", msg->cap.status );
253 g_message(" cat.urgency = %s", msg->cap.urgency );
254 g_message(" cat.severity = %s", msg->cap.severity );
255 g_message(" cat.certainty = %s", msg->cap.certainty);
256 g_message(" cat.area_desc = %s", msg->cap.area_desc);
257 g_message(" cat.fips6 = %s", msg->cap.fips6 );
258 g_message(" cat.vtec = %p", msg->cap.vtec );
262 gchar *msg_find_nearest(GritsHttp *http, time_t when, gboolean offline)
264 GList *files = grits_http_available(http,
265 "^[0-9]*.xml$", "index", NULL, NULL);
267 time_t this_time = 0;
268 time_t nearest_time = offline ? 0 : time(NULL);
269 char *nearest_file = NULL;
271 for (GList *cur = files; cur; cur = cur->next) {
272 gchar *file = cur->data;
273 sscanf(file, "%ld", &this_time);
274 if (ABS(when - this_time) <
275 ABS(when - nearest_time)) {
277 nearest_time = this_time;
282 return g_strconcat("index/", nearest_file, NULL);
284 return g_strdup_printf("index/%ld.xml", time(NULL));
289 GList *msg_load_index(GritsHttp *http, time_t when, time_t *updated, gboolean offline)
291 /* Fetch current alerts */
292 gchar *tmp = msg_find_nearest(http, when, offline);
295 gchar *file = grits_http_fetch(http, MSG_INDEX, tmp, GRITS_ONCE, NULL, NULL);
301 gchar *text; gsize len;
302 g_file_get_contents(file, &text, &len, NULL);
303 GList *msgs = msg_parse_index(text, len, updated);
308 /* Delete unrecognized messages */
310 for (GList *cur = msgs; cur; cur = cur->next)
311 if (!((AlertMsg*)cur->data)->info)
312 dead = g_list_prepend(dead, cur->data);
313 for (GList *cur = dead; cur; cur = cur->next) {
314 AlertMsg *msg = cur->data;
315 g_warning("GritsPluginAlert: unknown msg type - %s", msg->title);
316 msgs = g_list_remove(msgs, msg);
324 gboolean msg_load_cap(GritsHttp *http, AlertMsg *msg)
326 if (msg->description || msg->instruction || msg->polygon)
328 g_debug("GritsPlguinAlert: update_cap");
331 gchar *id = strrchr(msg->link, '=');
332 if (!id) return FALSE; id++;
333 gchar *dir = g_strdelimit(g_strdup(msg->info->abbr), " ", '_');
334 gchar *tmp = g_strdup_printf("%s/%s.xml", dir, id);
335 gchar *file = grits_http_fetch(http, msg->link, tmp, GRITS_ONCE, NULL, NULL);
342 gchar *text; gsize len;
343 g_file_get_contents(file, &text, &len, NULL);
344 msg_parse_cap(msg, text, len);
354 int fips_compare(int a, int b)
356 return (a < b) ? -1 :
360 GritsPoly *fips_combine(GList *polys)
362 /* Create points list */
363 GPtrArray *array = g_ptr_array_new();
364 for (GList *cur = polys; cur; cur = cur->next) {
365 GritsPoly *poly = cur->data;
366 gdouble (**points)[3] = poly->points;
367 for (int i = 0; points[i]; i++)
368 g_ptr_array_add(array, points[i]);
370 g_ptr_array_add(array, NULL);
371 gdouble (**points)[3] = (gpointer)g_ptr_array_free(array, FALSE);
373 /* Calculate center */
374 GritsBounds bounds = {-90, 90, -180, 180};
375 for (GList *cur = polys; cur; cur = cur->next) {
376 GritsObject *poly = cur->data;
377 gdouble lat = poly->center.lat;
378 gdouble lon = poly->center.lon;
379 if (lat > bounds.n) bounds.n = lat;
380 if (lat < bounds.s) bounds.s = lat;
381 if (lon > bounds.e) bounds.e = lon;
382 if (lon < bounds.w) bounds.w = lon;
384 GritsPoint center = {
385 .lat = (bounds.n + bounds.s)/2,
386 .lon = lon_avg(bounds.e, bounds.w),
390 GritsPoly *poly = grits_poly_new(points);
391 GRITS_OBJECT(poly)->skip |= GRITS_SKIP_CENTER;
392 GRITS_OBJECT(poly)->skip |= GRITS_SKIP_STATE;
393 GRITS_OBJECT(poly)->center = center;
394 g_object_weak_ref(G_OBJECT(poly), (GWeakNotify)g_free, points);
398 gboolean fips_group_state(gpointer key, gpointer value, gpointer data)
400 GList *counties = value;
401 GList **states = data;
402 GritsPoly *poly = fips_combine(counties);
403 GRITS_OBJECT(poly)->lod = EARTH_R/10;
404 *states = g_list_prepend(*states, poly);
405 g_list_free(counties);
409 void fips_parse(gchar *text, GTree **_counties, GList **_states)
411 g_debug("GritsPluginAlert: fips_parse");
412 GTree *counties = g_tree_new((GCompareFunc)fips_compare);
413 GTree *states = g_tree_new_full((GCompareDataFunc)g_strcmp0,
415 gchar **lines = g_strsplit(text, "\n", -1);
416 for (gint li = 0; lines[li]; li++) {
418 gchar **sparts = g_strsplit(lines[li], "\t", 4);
419 int nparts = g_strv_length(sparts);
425 /* Create GritsPoly */
426 GritsPoly *poly = grits_poly_parse(sparts[3], "\t", " ", ",");
428 /* Insert polys into the tree */
429 glong id = g_ascii_strtoll(sparts[0], NULL, 10);
430 g_tree_insert(counties, (gpointer)id, poly);
432 /* Insert into states list */
433 GList *list = g_tree_lookup(states, sparts[2]);
434 list = g_list_prepend(list, poly);
435 g_tree_replace(states, g_strdup(sparts[2]), list);
441 /* Group state counties */
442 *_counties = counties;
444 g_tree_foreach(states, (GTraverseFunc)fips_group_state, _states);
445 g_tree_destroy(states);
448 /********************
450 ********************/
451 static GtkWidget *_make_msg_details(AlertMsg *msg)
454 GtkWidget *title = gtk_label_new("");
455 gchar *title_str = g_markup_printf_escaped("<big><b>%s</b></big>",
456 msg->title ?: "No title provided");
457 gtk_label_set_use_markup(GTK_LABEL(title), TRUE);
458 gtk_label_set_markup(GTK_LABEL(title), title_str);
459 gtk_label_set_line_wrap(GTK_LABEL(title), TRUE);
460 gtk_misc_set_alignment(GTK_MISC(title), 0, 0);
461 gtk_widget_set_size_request(GTK_WIDGET(title), 500, -1);
464 GtkWidget *alert = gtk_scrolled_window_new(NULL, NULL);
465 GtkWidget *alert_view = gtk_text_view_new();
466 GtkTextBuffer *alert_buf = gtk_text_buffer_new(NULL);
467 gchar *alert_str = g_markup_printf_escaped("%s\n\n%s",
468 msg->description ?: "No description provided",
469 msg->instruction ?: "No instructions provided");
470 PangoFontDescription *alert_font = pango_font_description_from_string(
472 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(alert),
473 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
474 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(alert),
476 gtk_text_buffer_set_text(alert_buf, alert_str, -1);
477 gtk_text_view_set_buffer(GTK_TEXT_VIEW(alert_view), alert_buf);
478 gtk_widget_modify_font(GTK_WIDGET(alert_view), alert_font);
479 gtk_container_add(GTK_CONTAINER(alert), alert_view);
482 GtkWidget *align = gtk_alignment_new(0, 0, 1, 1);
483 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
484 gtk_alignment_set_padding(GTK_ALIGNMENT(align), 10, 10, 10, 10);
485 gtk_container_add(GTK_CONTAINER(align), box);
486 gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0);
487 gtk_box_pack_start(GTK_BOX(box), alert, TRUE, TRUE, 0);
492 static GtkWidget *_find_details(GtkNotebook *notebook, AlertMsg *msg)
494 int pages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
495 for (int i = 0; i < pages; i++) {
496 GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
497 if (msg == g_object_get_data(G_OBJECT(page), "msg"))
503 static gboolean _show_details(GritsPoly *county, GdkEvent *_, GritsPluginAlert *alert)
505 /* Add details for this messages */
506 AlertMsg *msg = g_object_get_data(G_OBJECT(county), "msg");
508 // TODO: move this to a thread since it blocks on net access
509 if (!msg_load_cap(alert->http, msg))
512 GtkWidget *dialog = alert->details;
513 GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
514 GList *list = gtk_container_get_children(GTK_CONTAINER(content));
515 GtkWidget *notebook = list->data;
516 GtkWidget *details = _find_details(GTK_NOTEBOOK(notebook), msg);
519 if (details == NULL) {
520 details = _make_msg_details(msg);
521 GtkWidget *label = gtk_label_new(msg->info->abbr);
522 g_object_set_data(G_OBJECT(details), "msg", msg);
523 gtk_notebook_append_page(GTK_NOTEBOOK(notebook), details, label);
526 gtk_widget_show_all(dialog);
527 gint num = gtk_notebook_page_num(GTK_NOTEBOOK(notebook), details);
528 gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), num);
533 /* Update counties */
534 static gboolean _alert_leave(GritsPoly *county, GdkEvent *_, GritsPluginAlert *alert)
536 g_debug("_alert_leave");
537 if (county->width == 3) {
538 county->color[3] = 0;
540 county->border[3] = 0.25;
543 grits_object_queue_draw(GRITS_OBJECT(county));
547 static gboolean _alert_enter(GritsPoly *county, GdkEvent *_, GritsPluginAlert *alert)
549 g_debug("_alert_enter");
550 if (county->width == 3) {
551 county->color[3] = 0.25;
553 county->border[3] = 1.0;
556 grits_object_queue_draw(GRITS_OBJECT(county));
560 /* Update polygons */
561 static void _load_common(GritsPluginAlert *alert, AlertMsg *msg, GritsPoly *poly,
562 float color, float border, int width, gchar *hidden)
564 g_object_set_data(G_OBJECT(poly), "msg", msg);
565 poly->color[0] = poly->border[0] = (float)msg->info->color[0] / 256;
566 poly->color[1] = poly->border[1] = (float)msg->info->color[1] / 256;
567 poly->color[2] = poly->border[2] = (float)msg->info->color[2] / 256;
568 poly->color[3] = color;
569 poly->border[3] = border;
571 GRITS_OBJECT(poly)->lod = 0;
572 GRITS_OBJECT(poly)->hidden = msg->info->hidden ||
573 grits_prefs_get_boolean(alert->prefs, hidden, NULL);
574 g_signal_connect(poly, "enter", G_CALLBACK(_alert_enter), alert);
575 g_signal_connect(poly, "leave", G_CALLBACK(_alert_leave), alert);
576 g_signal_connect(poly, "clicked", G_CALLBACK(_show_details), alert);
579 static GritsPoly *_load_storm_based(GritsPluginAlert *alert, AlertMsg *msg)
581 if (!msg->info->ispoly)
584 if (!msg_load_cap(alert->http, msg))
590 GritsPoly *poly = grits_poly_parse(msg->polygon, "\t", " ", ",");
591 _load_common(alert, msg, poly, 0, 1, 3, "alert/hide_storm_based");
592 grits_viewer_add(alert->viewer, GRITS_OBJECT(poly), GRITS_LEVEL_WORLD+4, FALSE);
597 static GritsPoly *_load_county_based(GritsPluginAlert *alert, AlertMsg *msg)
599 /* Locate counties in the path of the storm */
600 gchar **fipses = g_strsplit(msg->cap.fips6, " ", -1);
601 GList *counties = NULL;
602 for (int i = 0; fipses[i]; i++) {
603 glong fips = g_ascii_strtoll(fipses[i], NULL, 10);
604 GritsPoly *county = g_tree_lookup(alert->counties, (gpointer)fips);
607 counties = g_list_prepend(counties, county);
611 /* No county based warning.. */
615 /* Create new county based warning */
616 GritsPoly *poly = fips_combine(counties);
617 _load_common(alert, msg, poly, 0.25, 0.25, 0, "alert/hide_county_based");
618 grits_viewer_add(alert->viewer, GRITS_OBJECT(poly), GRITS_LEVEL_WORLD+1, FALSE);
620 g_list_free(counties);
625 static gboolean _show_hide(GtkToggleButton *button, GritsPluginAlert *alert)
627 g_debug("GritsPluginAlert: _show_hide - alert=%p, config=%p", alert, alert->config);
629 /* Check if we've clicked a alert type button */
630 AlertInfo *info = g_object_get_data(G_OBJECT(button), "info");
632 info->hidden = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
634 /* Update county/storm based hiding */
635 GtkWidget *ctoggle = g_object_get_data(G_OBJECT(alert->config), "county_based");
636 GtkWidget *stoggle = g_object_get_data(G_OBJECT(alert->config), "storm_based");
638 gboolean cshow = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ctoggle));
639 gboolean sshow = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(stoggle));
641 grits_prefs_set_boolean(alert->prefs, "alert/hide_county_based", !cshow);
642 grits_prefs_set_boolean(alert->prefs, "alert/hide_storm_based", !sshow);
644 /* Show/hide each message */
645 for (GList *cur = alert->msgs; cur; cur = cur->next) {
646 AlertMsg *msg = cur->data;
647 gboolean hide = msg->info->hidden;
648 if (msg->county_based)
649 GRITS_OBJECT(msg->county_based)->hidden = !cshow || hide;
650 if (msg->storm_based)
651 GRITS_OBJECT(msg->storm_based)->hidden = !sshow || hide;
654 grits_viewer_queue_draw(alert->viewer);
659 static GtkWidget *_button_new(AlertInfo *info)
661 g_debug("GritsPluginAlert: _button_new - %s", info->title);
662 GdkColor black = {0, 0, 0, 0};
663 GdkColor color = {0, info->color[0]<<8, info->color[1]<<8, info->color[2]<<8};
666 g_snprintf(text, sizeof(text), "<b>%.10s</b>", info->abbr);
668 GtkWidget *button = gtk_toggle_button_new();
669 GtkWidget *align = gtk_alignment_new(0.5, 0.5, 1, 1);
670 GtkWidget *cbox = gtk_event_box_new();
671 GtkWidget *label = gtk_label_new(text);
672 for (int state = 0; state < GTK_STATE_INSENSITIVE; state++) {
673 gtk_widget_modify_fg(label, state, &black);
674 gtk_widget_modify_bg(cbox, state, &color);
676 g_object_set_data(G_OBJECT(button), "info", info);
677 gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
678 gtk_alignment_set_padding(GTK_ALIGNMENT(align), 2, 2, 4, 4);
679 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), !info->hidden);
680 gtk_widget_set_tooltip_text(GTK_WIDGET(button), info->title);
681 gtk_container_add(GTK_CONTAINER(cbox), label);
682 gtk_container_add(GTK_CONTAINER(align), cbox);
683 gtk_container_add(GTK_CONTAINER(button), align);
687 static gboolean _update_buttons(GritsPluginAlert *alert)
689 g_debug("GritsPluginAlert: _update_buttons");
690 GtkWidget *alerts = g_object_get_data(G_OBJECT(alert->config), "alerts");
691 GtkWidget *updated = g_object_get_data(G_OBJECT(alert->config), "updated");
693 /* Determine which buttons to show */
694 for (int i = 0; alert_info[i].title; i++)
695 alert_info[i].current = FALSE;
696 for (GList *cur = alert->msgs; cur; cur = cur->next) {
697 AlertMsg *msg = cur->data;
698 msg->info->current = TRUE;
701 /* Delete old buttons */
702 GList *frames = gtk_container_get_children(GTK_CONTAINER(alerts));
703 for (GList *frame = frames; frame; frame = frame->next) {
704 GtkWidget *table = gtk_bin_get_child(GTK_BIN(frame->data));
705 GList *btns = gtk_container_get_children(GTK_CONTAINER(table));
706 g_list_foreach(btns, (GFunc)gtk_widget_destroy, NULL);
711 /* Add new buttons */
712 for (int i = 0; alert_info[i].title; i++) {
713 if (!alert_info[i].current)
716 GtkWidget *table = g_object_get_data(G_OBJECT(alerts),
717 alert_info[i].category);
718 GList *kids = gtk_container_get_children(GTK_CONTAINER(table));
719 gint nkids = g_list_length(kids);
720 gint x = nkids / CONFIG_HEIGHT;
721 gint y = nkids % CONFIG_HEIGHT;
724 GtkWidget *button = _button_new(&alert_info[i]);
725 gtk_table_attach(GTK_TABLE(table), button, x, x+1, y, y+1,
726 GTK_FILL|GTK_EXPAND, GTK_FILL, 0, 0);
727 g_signal_connect(button, "clicked", G_CALLBACK(_show_hide), alert);
730 /* Set time widget */
731 struct tm *tm = gmtime(&alert->updated);
732 gchar *date_str = g_strdup_printf(" <b><i>%04d-%02d-%02d %02d:%02d</i></b>",
733 tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
734 tm->tm_hour, tm->tm_min);
735 gtk_label_set_markup(GTK_LABEL(updated), date_str);
738 gtk_widget_show_all(GTK_WIDGET(alert->config));
739 alert->update_source = 0;
743 static gint _sort_warnings(gconstpointer _a, gconstpointer _b)
745 const AlertMsg *a=_a, *b=_b;
746 return (a->info->prior < b->info->prior) ? -1 :
747 (a->info->prior == b->info->prior) ? 0 : 1;
750 static void _update_warnings(GritsPluginAlert *alert, GList *old)
752 g_debug("GritsPluginAlert: _update_warnings");
755 * reversed here since they're added
756 * to the viewer in reverse order */
757 alert->msgs = g_list_sort(alert->msgs, _sort_warnings);
759 /* Remove old messages */
760 for (GList *cur = old; cur; cur = cur->next) {
761 AlertMsg *msg = cur->data;
762 if (msg->county_based) grits_viewer_remove(alert->viewer,
763 GRITS_OBJECT(msg->county_based));
764 if (msg->storm_based) grits_viewer_remove(alert->viewer,
765 GRITS_OBJECT(msg->storm_based));
768 /* Add new messages */
769 /* Load counties first since it does not require network access */
770 for (GList *cur = alert->msgs; cur; cur = cur->next) {
771 AlertMsg *msg = cur->data;
772 msg->county_based = _load_county_based(alert, msg);
774 grits_viewer_queue_draw(alert->viewer);
775 for (GList *cur = alert->msgs; cur; cur = cur->next) {
776 AlertMsg *msg = cur->data;
777 msg->storm_based = _load_storm_based(alert, msg);
781 grits_viewer_queue_draw(alert->viewer);
783 g_debug("GritsPluginAlert: _load_warnings - end");
787 static void _update(gpointer _, gpointer _alert)
789 GritsPluginAlert *alert = _alert;
792 GList *old = alert->msgs;
793 g_debug("GritsPluginAlert: _update");
795 time_t when = grits_viewer_get_time(alert->viewer);
796 gboolean offline = grits_viewer_get_offline(alert->viewer);
797 if (!(alert->msgs = msg_load_index(alert->http, when, &alert->updated, offline)))
800 if (!alert->update_source)
801 alert->update_source = g_idle_add((GSourceFunc)_update_buttons, alert);
802 _update_warnings(alert, old);
804 g_list_foreach(old, (GFunc)msg_free, NULL);
807 g_debug("GritsPluginAlert: _update - end");
810 static void _on_update(GritsPluginAlert *alert)
812 g_thread_pool_push(alert->threads, NULL+1, NULL);
816 static GtkWidget *_make_config(GritsPluginAlert *alert)
818 GtkWidget *config = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
820 /* Setup tools area */
821 GtkWidget *tools = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
822 GtkWidget *updated = gtk_label_new(" Loading...");
823 GtkWidget *storm_based = gtk_toggle_button_new_with_label("Storm based");
824 GtkWidget *county_based = gtk_toggle_button_new_with_label("County based");
825 gtk_label_set_use_markup(GTK_LABEL(updated), TRUE);
826 gtk_box_pack_start(GTK_BOX(tools), updated, FALSE, FALSE, 0);
827 gtk_box_pack_end (GTK_BOX(tools), storm_based, FALSE, FALSE, 0);
828 gtk_box_pack_end (GTK_BOX(tools), county_based, FALSE, FALSE, 0);
829 gtk_box_pack_start(GTK_BOX(config), tools, FALSE, FALSE, 0);
830 g_object_set_data(G_OBJECT(config), "updated", updated);
831 g_object_set_data(G_OBJECT(config), "storm_based", storm_based);
832 g_object_set_data(G_OBJECT(config), "county_based", county_based);
833 g_signal_connect(storm_based, "toggled", G_CALLBACK(_show_hide), alert);
834 g_signal_connect(county_based, "toggled", G_CALLBACK(_show_hide), alert);
837 gchar *labels[] = {"<b>Warnings</b>", "<b>Watches</b>",
838 "<b>Advisories</b>", "<b>Other</b>"};
839 gchar *keys[] = {"warning", "watch", "advisory", "other"};
840 GtkWidget *alerts = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
841 for (int i = 0; i < G_N_ELEMENTS(labels); i++) {
842 GtkWidget *frame = gtk_frame_new(labels[i]);
843 GtkWidget *table = gtk_table_new(1, 1, TRUE);
844 GtkWidget *label = gtk_frame_get_label_widget(GTK_FRAME(frame));
845 gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
846 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE);
847 gtk_container_add(GTK_CONTAINER(frame), table);
848 gtk_box_pack_start(GTK_BOX(alerts), frame, TRUE, TRUE, 0);
849 g_object_set_data(G_OBJECT(alerts), keys[i], table);
851 gtk_box_pack_start(GTK_BOX(config), alerts, TRUE, TRUE, 0);
852 g_object_set_data(G_OBJECT(config), "alerts", alerts);
857 static gboolean _clear_details(GtkWidget *dialog)
859 GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
860 GList *list = gtk_container_get_children(GTK_CONTAINER(content));
861 GtkWidget *notebook = list->data;
863 gtk_widget_hide(dialog);
864 while (gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook)))
865 gtk_notebook_remove_page(GTK_NOTEBOOK(notebook), 0);
869 static gboolean _set_details_uri(GtkWidget *notebook, gpointer _,
870 guint num, GtkWidget *button)
872 g_debug("_set_details_uri");
873 GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), num);
874 AlertMsg *msg = g_object_get_data(G_OBJECT(page), "msg");
875 gtk_link_button_set_uri(GTK_LINK_BUTTON(button), msg->link);
879 static GtkWidget *_make_details(GritsViewer *viewer)
881 GtkWidget *dialog = gtk_dialog_new();
882 GtkWidget *action = gtk_dialog_get_action_area (GTK_DIALOG(dialog));
883 GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
884 GtkWidget *notebook = gtk_notebook_new();
885 GtkWidget *win = gtk_widget_get_toplevel(GTK_WIDGET(viewer));
886 GtkWidget *link = gtk_link_button_new_with_label("", "Full Text");
888 gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(win));
889 gtk_window_set_title(GTK_WINDOW(dialog), "Alert Details - AWeather");
890 gtk_window_set_default_size(GTK_WINDOW(dialog), 625, 500);
891 gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), TRUE);
892 gtk_container_add(GTK_CONTAINER(content), notebook);
893 gtk_box_pack_end(GTK_BOX(action), link, 0, 0, 0);
894 gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE);
896 g_signal_connect(dialog, "response", G_CALLBACK(_clear_details), NULL);
897 g_signal_connect(dialog, "delete-event", G_CALLBACK(_clear_details), NULL);
898 g_signal_connect(notebook, "switch-page", G_CALLBACK(_set_details_uri), link);
904 GritsPluginAlert *grits_plugin_alert_new(GritsViewer *viewer, GritsPrefs *prefs)
906 g_debug("GritsPluginAlert: new");
907 GritsPluginAlert *alert = g_object_new(GRITS_TYPE_PLUGIN_ALERT, NULL);
908 alert->details = _make_details(viewer);
909 alert->viewer = g_object_ref(viewer);
910 alert->prefs = g_object_ref(prefs);
912 alert->refresh_id = g_signal_connect_swapped(alert->viewer, "refresh",
913 G_CALLBACK(_on_update), alert);
914 alert->time_changed_id = g_signal_connect_swapped(alert->viewer, "time_changed",
915 G_CALLBACK(_on_update), alert);
917 for (GList *cur = alert->states; cur; cur = cur->next)
918 grits_viewer_add(viewer, cur->data, GRITS_LEVEL_WORLD+1, FALSE);
920 gboolean chide = grits_prefs_get_boolean(alert->prefs, "alert/hide_county_based", NULL);
921 gboolean shide = grits_prefs_get_boolean(alert->prefs, "alert/hide_storm_based", NULL);
922 GtkWidget *ctoggle = g_object_get_data(G_OBJECT(alert->config), "county_based");
923 GtkWidget *stoggle = g_object_get_data(G_OBJECT(alert->config), "storm_based");
924 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ctoggle), !chide);
925 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(stoggle), !shide);
931 static GtkWidget *grits_plugin_alert_get_config(GritsPlugin *_alert)
933 GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(_alert);
934 return alert->config;
939 static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface);
940 G_DEFINE_TYPE_WITH_CODE(GritsPluginAlert, grits_plugin_alert, G_TYPE_OBJECT,
941 G_IMPLEMENT_INTERFACE(GRITS_TYPE_PLUGIN,
942 grits_plugin_alert_plugin_init));
943 static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface)
945 g_debug("GritsPluginAlert: plugin_init");
946 /* Add methods to the interface */
947 iface->get_config = grits_plugin_alert_get_config;
949 static void grits_plugin_alert_init(GritsPluginAlert *alert)
951 g_debug("GritsPluginAlert: init");
953 alert->threads = g_thread_pool_new(_update, alert, 1, FALSE, NULL);
954 alert->config = _make_config(alert);
955 alert->http = grits_http_new(G_DIR_SEPARATOR_S
956 "alerts" G_DIR_SEPARATOR_S
957 "cap" G_DIR_SEPARATOR_S);
960 gchar *text; gsize len;
961 const gchar *file = PKGDATADIR G_DIR_SEPARATOR_S "fips.txt";
962 if (!g_file_get_contents(file, &text, &len, NULL))
963 g_error("GritsPluginAlert: init - error loading fips polygons");
964 fips_parse(text, &alert->counties, &alert->states);
967 static void grits_plugin_alert_dispose(GObject *gobject)
969 g_debug("GritsPluginAlert: dispose");
970 GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
971 alert->aborted = TRUE;
972 /* Drop references */
974 GritsViewer *viewer = alert->viewer;
975 g_signal_handler_disconnect(viewer, alert->refresh_id);
976 g_signal_handler_disconnect(viewer, alert->time_changed_id);
977 grits_http_abort(alert->http);
978 g_thread_pool_free(alert->threads, TRUE, TRUE);
979 if (alert->update_source)
980 g_source_remove(alert->update_source);
981 alert->viewer = NULL;
982 for (GList *cur = alert->msgs; cur; cur = cur->next) {
983 AlertMsg *msg = cur->data;
984 if (msg->county_based) grits_viewer_remove(viewer,
985 GRITS_OBJECT(msg->county_based));
986 if (msg->storm_based) grits_viewer_remove(viewer,
987 GRITS_OBJECT(msg->storm_based));
989 for (GList *cur = alert->states; cur; cur = cur->next)
990 grits_viewer_remove(viewer, cur->data);
991 gtk_widget_destroy(alert->details);
992 g_object_unref(alert->prefs);
993 g_object_unref(viewer);
995 G_OBJECT_CLASS(grits_plugin_alert_parent_class)->dispose(gobject);
997 static gboolean _unref_county(gpointer key, gpointer val, gpointer data)
1002 static void grits_plugin_alert_finalize(GObject *gobject)
1004 g_debug("GritsPluginAlert: finalize");
1005 GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
1006 g_list_foreach(alert->msgs, (GFunc)msg_free, NULL);
1007 g_list_free(alert->msgs);
1008 g_list_free(alert->states);
1009 g_tree_foreach(alert->counties, (GTraverseFunc)_unref_county, NULL);
1010 g_tree_destroy(alert->counties);
1011 grits_http_free(alert->http);
1012 G_OBJECT_CLASS(grits_plugin_alert_parent_class)->finalize(gobject);
1014 static void grits_plugin_alert_class_init(GritsPluginAlertClass *klass)
1016 g_debug("GritsPluginAlert: class_init");
1017 GObjectClass *gobject_class = (GObjectClass*)klass;
1018 gobject_class->dispose = grits_plugin_alert_dispose;
1019 gobject_class->finalize = grits_plugin_alert_finalize;