]> Pileus Git - aweather/blob - src/plugins/alert.c
Update compat includes
[aweather] / src / plugins / alert.c
1 /*
2  * Copyright (C) 2010-2011 Andy Spencer <andy753421@gmail.com>
3  * 
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.
8  * 
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.
13  * 
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/>.
16  */
17
18
19 #include <time.h>
20 #include <grits.h>
21 #include <stdio.h>
22 #include <string.h>
23
24 #include "alert.h"
25 #include "alert-info.h"
26
27 #include "../compat.h"
28
29 #define MSG_INDEX "http://alerts.weather.gov/cap/us.php?x=0"
30 #define CONFIG_HEIGHT 3
31
32 /* Format for single cap alert:
33  *   "http://alerts.weather.gov/cap/wwacapget.php?x="
34  *   "AK20111012000500CoastalFloodWarning20111012151500"
35  *   "AKAFGCFWAFG.6258a84eb1e8c34cd888057248224d10" */
36
37 /************
38  * AlertMsg *
39  ************/
40 /* AlertMsg data types */
41 typedef struct {
42         char sep0;
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;
51 } AlertVtec;
52
53 typedef struct {
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
58         struct {
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/
68         } cap;
69
70         /* Advanced info (from CAP file) */
71         char *description;
72         char *instruction;
73         char *polygon;
74
75         /* Internal state */
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
79 } AlertMsg;
80
81 /* AlertMsg parsing */
82 typedef struct {
83         time_t    updated;
84         AlertMsg *msg;
85         GList    *msgs;
86         gchar    *text;
87         gchar    *value_name;
88 } ParseData;
89 time_t msg_parse_time(gchar *iso8601)
90 {
91         GTimeVal tv = {};
92         g_time_val_from_iso8601(iso8601, &tv);
93         return tv.tv_sec;
94 }
95 AlertVtec *msg_parse_vtec(char *buf)
96 {
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';
102         return vtec;
103 }
104 void msg_parse_text(GMarkupParseContext *context, const gchar *text,
105                 gsize len, gpointer user_data, GError **error)
106 {
107         //g_debug("text %s", text);
108         ParseData *data = user_data;
109         if (data->text)
110                 g_free(data->text);
111         data->text = g_strndup(text, len);
112 }
113 void msg_parse_index_start(GMarkupParseContext *context, const gchar *name,
114                 const gchar **keys, const gchar **vals,
115                 gpointer user_data, GError **error)
116 {
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);
121 }
122 void msg_parse_index_end(GMarkupParseContext *context, const gchar *name,
123                 gpointer user_data, GError **error)
124 {
125         //g_debug("end %s", name);
126         ParseData *data = user_data;
127         AlertMsg  *msg  = data->msg;
128         char      *text = data->text;
129
130         if (g_str_equal(name, "updated") && text && !data->updated)
131                 data->updated = msg_parse_time(text);
132
133         if (g_str_equal(name, "entry"))
134                 data->msgs = g_list_prepend(data->msgs, data->msg);
135
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);
147
148         if (g_str_equal(name, "title"))
149                 msg->info = alert_info_find(msg->title);
150
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);
155         }
156
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);
160         }
161 }
162 void msg_parse_cap_end(GMarkupParseContext *context, const gchar *name,
163                 gpointer user_data, GError **error)
164 {
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);
172 }
173
174 /* AlertMsg methods */
175 GList *msg_parse_index(gchar *text, gsize len, time_t *updated)
176 {
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,
182         };
183         ParseData data = {};
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);
188         if (data.text)
189                 g_free(data.text);
190         if (data.value_name)
191                 g_free(data.value_name);
192         *updated = data.updated;
193         return data.msgs;
194 }
195
196 void msg_parse_cap(AlertMsg *msg, gchar *text, gsize len)
197 {
198         g_debug("GritsPluginAlert: msg_parse_cap");
199         GMarkupParser parser = {
200                 .end_element   = msg_parse_cap_end,
201                 .text          = msg_parse_text,
202         };
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);
208         if (data.text)
209                 g_free(data.text);
210
211         /* Add spaces to description... */
212         static GRegex *regex = NULL;
213         if (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);
219                 g_free(old);
220         }
221 }
222
223 void msg_free(AlertMsg *msg)
224 {
225         g_free(msg->title);
226         g_free(msg->link);
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);
238         g_free(msg);
239 }
240
241 void msg_print(GList *msgs)
242 {
243         g_message("msg_print");
244         for (GList *cur = msgs; cur; cur = cur->next) {
245                 AlertMsg *msg = cur->data;
246                 g_message("alert:");
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     );
259         }
260 }
261
262 gchar *msg_find_nearest(GritsHttp *http, time_t when, gboolean offline)
263 {
264         GList *files = grits_http_available(http,
265                         "^[0-9]*.xml$", "index", NULL, NULL);
266
267         time_t  this_time    = 0;
268         time_t  nearest_time = offline ? 0 : time(NULL);
269         char   *nearest_file = NULL;
270
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)) {
276                         nearest_file = file;
277                         nearest_time = this_time;
278                 }
279         }
280
281         if (nearest_file)
282                 return g_strconcat("index/", nearest_file, NULL);
283         else if (!offline)
284                 return g_strdup_printf("index/%ld.xml", time(NULL));
285         else
286                 return NULL;
287 }
288
289 GList *msg_load_index(GritsHttp *http, time_t when, time_t *updated, gboolean offline)
290 {
291         /* Fetch current alerts */
292         gchar *tmp = msg_find_nearest(http, when, offline);
293         if (!tmp)
294                 return NULL;
295         gchar *file = grits_http_fetch(http, MSG_INDEX, tmp, GRITS_ONCE, NULL, NULL);
296         g_free(tmp);
297         if (!file)
298                 return NULL;
299
300         /* Load file */
301         gchar *text; gsize len;
302         g_file_get_contents(file, &text, &len, NULL);
303         GList *msgs = msg_parse_index(text, len, updated);
304         //msg_print(msgs);
305         g_free(file);
306         g_free(text);
307
308         /* Delete unrecognized messages */
309         GList *dead = NULL;
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);
317                 msg_free(msg);
318         }
319         g_list_free(dead);
320
321         return msgs;
322 }
323
324 gboolean msg_load_cap(GritsHttp *http, AlertMsg *msg)
325 {
326         if (msg->description || msg->instruction || msg->polygon)
327                 return TRUE;
328         g_debug("GritsPlguinAlert: update_cap");
329
330         /* Download */
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);
336         g_free(tmp);
337         g_free(dir);
338         if (!file)
339                 return FALSE;
340
341         /* Parse */
342         gchar *text; gsize len;
343         g_file_get_contents(file, &text, &len, NULL);
344         msg_parse_cap(msg, text, len);
345         g_free(file);
346         g_free(text);
347         return TRUE;
348 }
349
350
351 /********
352  * FIPS *
353  ********/
354 int fips_compare(int a, int b)
355 {
356         return (a <  b) ? -1 :
357                (a == b) ?  0 : 1;
358 }
359
360 GritsPoly *fips_combine(GList *polys)
361 {
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]);
369         }
370         g_ptr_array_add(array, NULL);
371         gdouble (**points)[3] = (gpointer)g_ptr_array_free(array, FALSE);
372
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;
383         }
384         GritsPoint center = {
385                 .lat = (bounds.n + bounds.s)/2,
386                 .lon = lon_avg(bounds.e, bounds.w),
387         };
388
389         /* Create polygon */
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);
395         return poly;
396 }
397
398 gboolean fips_group_state(gpointer key, gpointer value, gpointer data)
399 {
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);
406         return FALSE;
407 }
408
409 void fips_parse(gchar *text, GTree **_counties, GList **_states)
410 {
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,
414                         NULL, g_free, NULL);
415         gchar **lines = g_strsplit(text, "\n", -1);
416         for (gint li = 0; lines[li]; li++) {
417                 /* Split line */
418                 gchar **sparts = g_strsplit(lines[li], "\t", 4);
419                 int     nparts = g_strv_length(sparts);
420                 if (nparts < 4) {
421                         g_strfreev(sparts);
422                         continue;
423                 }
424
425                 /* Create GritsPoly */
426                 GritsPoly *poly = grits_poly_parse(sparts[3], "\t", " ", ",");
427
428                 /* Insert polys into the tree */
429                 glong id = g_ascii_strtoll(sparts[0], NULL, 10);
430                 g_tree_insert(counties, (gpointer)id, poly);
431
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);
436
437                 g_strfreev(sparts);
438         }
439         g_strfreev(lines);
440
441         /* Group state counties */
442         *_counties = counties;
443         *_states   = NULL;
444         g_tree_foreach(states, (GTraverseFunc)fips_group_state, _states);
445         g_tree_destroy(states);
446 }
447
448 /********************
449  * GritsPluginAlert *
450  ********************/
451 static GtkWidget *_make_msg_details(AlertMsg *msg)
452 {
453
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);
462         g_free(title_str);
463
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(
471                         "monospace");
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),
475                         GTK_SHADOW_IN);
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);
480         g_free(alert_str);
481
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);
488
489         return align;
490 }
491
492 static GtkWidget *_find_details(GtkNotebook *notebook, AlertMsg *msg)
493 {
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"))
498                         return page;
499         }
500         return NULL;
501 }
502
503 static gboolean _show_details(GritsPoly *county, GdkEvent *_, GritsPluginAlert *alert)
504 {
505         /* Add details for this messages */
506         AlertMsg *msg = g_object_get_data(G_OBJECT(county), "msg");
507
508         // TODO: move this to a thread since it blocks on net access
509         if (!msg_load_cap(alert->http, msg))
510                 return FALSE;
511
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);
517         g_list_free(list);
518
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);
524         }
525
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);
529
530         return FALSE;
531 }
532
533 /* Update counties */
534 static gboolean _alert_leave(GritsPoly *county, GdkEvent *_, GritsPluginAlert *alert)
535 {
536         g_debug("_alert_leave");
537         if (county->width == 3) {
538                 county->color[3]  = 0;
539         } else {
540                 county->border[3] = 0.25;
541                 county->width     = 1;
542         }
543         grits_object_queue_draw(GRITS_OBJECT(county));
544         return FALSE;
545 }
546
547 static gboolean _alert_enter(GritsPoly *county, GdkEvent *_, GritsPluginAlert *alert)
548 {
549         g_debug("_alert_enter");
550         if (county->width == 3) {
551                 county->color[3]  = 0.25;
552         } else {
553                 county->border[3] = 1.0;
554                 county->width     = 2;
555         }
556         grits_object_queue_draw(GRITS_OBJECT(county));
557         return FALSE;
558 }
559
560 /* Update polygons */
561 static void _load_common(GritsPluginAlert *alert, AlertMsg *msg, GritsPoly *poly,
562                 float color, float border, int width, gchar *hidden)
563 {
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;
570         poly->width     = width;
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);
577 }
578
579 static GritsPoly *_load_storm_based(GritsPluginAlert *alert, AlertMsg *msg)
580 {
581         if (!msg->info->ispoly)
582                 return NULL;
583
584         if (!msg_load_cap(alert->http, msg))
585                 return NULL;
586
587         if (!msg->polygon)
588                 return NULL;
589
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);
593
594         return poly;
595 }
596
597 static GritsPoly *_load_county_based(GritsPluginAlert *alert, AlertMsg *msg)
598 {
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);
605                 if (!county)
606                         continue;
607                 counties = g_list_prepend(counties, county);
608         }
609         g_strfreev(fipses);
610
611         /* No county based warning.. */
612         if (!counties)
613                 return NULL;
614
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);
619
620         g_list_free(counties);
621         return poly;
622 }
623
624 /* Update buttons */
625 static gboolean _show_hide(GtkToggleButton *button, GritsPluginAlert *alert)
626 {
627         g_debug("GritsPluginAlert: _show_hide - alert=%p, config=%p", alert, alert->config);
628
629         /* Check if we've clicked a alert type button */
630         AlertInfo *info = g_object_get_data(G_OBJECT(button), "info");
631         if (info)
632                 info->hidden = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
633
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");
637
638         gboolean   cshow   = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ctoggle));
639         gboolean   sshow   = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(stoggle));
640
641         grits_prefs_set_boolean(alert->prefs, "alert/hide_county_based", !cshow);
642         grits_prefs_set_boolean(alert->prefs, "alert/hide_storm_based",  !sshow);
643
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;
652         }
653
654         grits_viewer_queue_draw(alert->viewer);
655         return TRUE;
656 }
657
658
659 static GtkWidget *_button_new(AlertInfo *info)
660 {
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};
664
665         gchar text[64];
666         g_snprintf(text, sizeof(text), "<b>%.10s</b>", info->abbr);
667
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);
675         } /* Yuck.. */
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);
684         return button;
685 }
686
687 static gboolean _update_buttons(GritsPluginAlert *alert)
688 {
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");
692
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;
699         }
700
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);
707                 g_list_free(btns);
708         }
709         g_list_free(frames);
710
711         /* Add new buttons */
712         for (int i = 0; alert_info[i].title; i++) {
713                 if (!alert_info[i].current)
714                         continue;
715
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;
722                 g_list_free(kids);
723
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);
728         }
729
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);
736         g_free(date_str);
737
738         gtk_widget_show_all(GTK_WIDGET(alert->config));
739         alert->update_source = 0;
740         return FALSE;
741 }
742
743 static gint _sort_warnings(gconstpointer _a, gconstpointer _b)
744 {
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;
748 }
749
750 static void _update_warnings(GritsPluginAlert *alert, GList *old)
751 {
752         g_debug("GritsPluginAlert: _update_warnings");
753
754         /* Sort by priority:
755          *   reversed here since they're added
756          *   to the viewer in reverse order */
757         alert->msgs = g_list_sort(alert->msgs, _sort_warnings);
758
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));
766         }
767
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);
773         }
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);
778                 if (alert->aborted)
779                         return;
780         }
781         grits_viewer_queue_draw(alert->viewer);
782
783         g_debug("GritsPluginAlert: _load_warnings - end");
784 }
785
786 /* Callbacks */
787 static void _update(gpointer _, gpointer _alert)
788 {
789         GritsPluginAlert *alert = _alert;
790         if (alert->aborted)
791                 return;
792         GList *old = alert->msgs;
793         g_debug("GritsPluginAlert: _update");
794
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)))
798                 return;
799
800         if (!alert->update_source)
801                 alert->update_source = g_idle_add((GSourceFunc)_update_buttons, alert);
802         _update_warnings(alert, old);
803
804         g_list_foreach(old, (GFunc)msg_free, NULL);
805         g_list_free(old);
806
807         g_debug("GritsPluginAlert: _update - end");
808 }
809
810 static void _on_update(GritsPluginAlert *alert)
811 {
812         g_thread_pool_push(alert->threads, NULL+1, NULL);
813 }
814
815 /* Init helpers */
816 static GtkWidget *_make_config(GritsPluginAlert *alert)
817 {
818         GtkWidget *config = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
819
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);
835
836         /* Setup alerts */
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);
850         }
851         gtk_box_pack_start(GTK_BOX(config), alerts, TRUE, TRUE, 0);
852         g_object_set_data(G_OBJECT(config), "alerts",  alerts);
853
854         return config;
855 }
856
857 static gboolean _clear_details(GtkWidget *dialog)
858 {
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;
862         g_list_free(list);
863         gtk_widget_hide(dialog);
864         while (gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook)))
865                 gtk_notebook_remove_page(GTK_NOTEBOOK(notebook), 0);
866         return TRUE;
867 }
868
869 static gboolean _set_details_uri(GtkWidget *notebook, gpointer _,
870                 guint num, GtkWidget *button)
871 {
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);
876         return FALSE;
877 }
878
879 static GtkWidget *_make_details(GritsViewer *viewer)
880 {
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");
887
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);
895
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);
899
900         return dialog;
901 }
902
903 /* Methods */
904 GritsPluginAlert *grits_plugin_alert_new(GritsViewer *viewer, GritsPrefs *prefs)
905 {
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);
911
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);
916
917         for (GList *cur = alert->states; cur; cur = cur->next)
918                 grits_viewer_add(viewer, cur->data, GRITS_LEVEL_WORLD+1, FALSE);
919
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);
926
927         _on_update(alert);
928         return alert;
929 }
930
931 static GtkWidget *grits_plugin_alert_get_config(GritsPlugin *_alert)
932 {
933         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(_alert);
934         return alert->config;
935 }
936
937
938 /* GObject code */
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)
944 {
945         g_debug("GritsPluginAlert: plugin_init");
946         /* Add methods to the interface */
947         iface->get_config = grits_plugin_alert_get_config;
948 }
949 static void grits_plugin_alert_init(GritsPluginAlert *alert)
950 {
951         g_debug("GritsPluginAlert: init");
952         /* Set defaults */
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);
958
959         /* Load counties */
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);
965         g_free(text);
966 }
967 static void grits_plugin_alert_dispose(GObject *gobject)
968 {
969         g_debug("GritsPluginAlert: dispose");
970         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
971         alert->aborted = TRUE;
972         /* Drop references */
973         if (alert->viewer) {
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));
988                 }
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);
994         }
995         G_OBJECT_CLASS(grits_plugin_alert_parent_class)->dispose(gobject);
996 }
997 static gboolean _unref_county(gpointer key, gpointer val, gpointer data)
998 {
999         g_object_unref(val);
1000         return FALSE;
1001 }
1002 static void grits_plugin_alert_finalize(GObject *gobject)
1003 {
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);
1013 }
1014 static void grits_plugin_alert_class_init(GritsPluginAlertClass *klass)
1015 {
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;
1020 }