]> Pileus Git - aweather/blob - src/plugins/alert.c
Fix threading bug in Alert plugin
[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 <GL/gl.h>
22 #include <stdio.h>
23 #include <string.h>
24
25 #include "alert.h"
26 #include "alert-info.h"
27
28 #define MSG_INDEX "http://alerts.weather.gov/cap/us.php?x=0"
29
30 /* Format for single cap alert:
31  *   "http://alerts.weather.gov/cap/wwacapget.php?x="
32  *   "AK20111012000500CoastalFloodWarning20111012151500"
33  *   "AKAFGCFWAFG.6258a84eb1e8c34cd888057248224d10" */
34
35 /************
36  * AlertMsg *
37  ************/
38 /* AlertMsg data types */
39 typedef struct {
40         char sep0;
41         char class [1 ]; char sep1;
42         char action[3 ]; char sep2;
43         char office[4 ]; char sep3;
44         char phenom[2 ]; char sep4;
45         char signif[1 ]; char sep5;
46         char event [4 ]; char sep6;
47         char begin [12]; char sep7;
48         char end   [12]; char sep8;
49 } AlertVtec;
50
51 typedef struct {
52         /* Basic info (from alert index) */
53         char *title;   // Winter Weather Advisory issued December 19 at 8:51PM
54         char *link;    // http://www.weather.gov/alerts-beta/wwacapget.php?x=AK20101219205100AFGWinterWeatherAdvisoryAFG20101220030000AK
55         char *summary; // ...WINTER WEATHER ADVISORY REMAINS IN EFFECT UNTIL 6
56         struct {
57                 time_t     effective; // 2010-12-19T20:51:00-09:00
58                 time_t     expires;   // 2010-12-20T03:00:00-09:00
59                 char      *status;    // Actual
60                 char      *urgency;   // Expected
61                 char      *severity;  // Minor
62                 char      *certainty; // Likely
63                 char      *area_desc; // Northeastern Brooks Range; Northwestern Brooks Range
64                 char      *fips6;     // 006015 006023 006045 006105
65                 AlertVtec *vtec;      // /X.CON.PAFG.WW.Y.0064.000000T0000Z-101220T0300Z/
66         } cap;
67
68         /* Advanced info (from CAP file) */
69         char *description;
70         char *instruction;
71         char *polygon;
72
73         /* Internal state */
74         AlertInfo *info;         // Link to info structure for this alert
75         GritsPoly *county_based; // Polygon for county-based warning
76         GritsPoly *storm_based;  // Polygon for storm-based warning
77 } AlertMsg;
78
79 /* AlertMsg parsing */
80 typedef struct {
81         time_t    updated;
82         AlertMsg *msg;
83         GList    *msgs;
84         gchar    *text;
85         gchar    *value_name;
86 } ParseData;
87 time_t msg_parse_time(gchar *iso8601)
88 {
89         GTimeVal tv = {};
90         g_time_val_from_iso8601(iso8601, &tv);
91         return tv.tv_sec;
92 }
93 AlertVtec *msg_parse_vtec(char *buf)
94 {
95         AlertVtec *vtec = g_new0(AlertVtec, 1);
96         strncpy((char*)vtec, buf, sizeof(AlertVtec));
97         vtec->sep0 = vtec->sep1 = vtec->sep2 = '\0';
98         vtec->sep3 = vtec->sep4 = vtec->sep5 = '\0';
99         vtec->sep6 = vtec->sep7 = vtec->sep8 = '\0';
100         return vtec;
101 }
102 void msg_parse_text(GMarkupParseContext *context, const gchar *text,
103                 gsize len, gpointer user_data, GError **error)
104 {
105         //g_debug("text %s", text);
106         ParseData *data = user_data;
107         if (data->text)
108                 g_free(data->text);
109         data->text = g_strndup(text, len);
110 }
111 void msg_parse_index_start(GMarkupParseContext *context, const gchar *name,
112                 const gchar **keys, const gchar **vals,
113                 gpointer user_data, GError **error)
114 {
115         //g_debug("start %s", name);
116         ParseData *data = user_data;
117         if (g_str_equal(name, "entry"))
118                 data->msg  = g_new0(AlertMsg, 1);
119 }
120 void msg_parse_index_end(GMarkupParseContext *context, const gchar *name,
121                 gpointer user_data, GError **error)
122 {
123         //g_debug("end %s", name);
124         ParseData *data = user_data;
125         AlertMsg  *msg  = data->msg;
126         char      *text = data->text;
127
128         if (g_str_equal(name, "updated") && text && !data->updated)
129                 data->updated = msg_parse_time(text);
130
131         if (g_str_equal(name, "entry"))
132                 data->msgs = g_list_prepend(data->msgs, data->msg);
133
134         if (!text || !msg) return;
135         else if (g_str_equal(name, "title"))         msg->title         = g_strdup(text);
136         else if (g_str_equal(name, "id"))            msg->link          = g_strdup(text); // hack
137         else if (g_str_equal(name, "summary"))       msg->summary       = g_strdup(text);
138         else if (g_str_equal(name, "cap:effective")) msg->cap.effective = msg_parse_time(text);
139         else if (g_str_equal(name, "cap:expires"))   msg->cap.expires   = msg_parse_time(text);
140         else if (g_str_equal(name, "cap:status"))    msg->cap.status    = g_strdup(text);
141         else if (g_str_equal(name, "cap:urgency"))   msg->cap.urgency   = g_strdup(text);
142         else if (g_str_equal(name, "cap:severity"))  msg->cap.severity  = g_strdup(text);
143         else if (g_str_equal(name, "cap:certainty")) msg->cap.certainty = g_strdup(text);
144         else if (g_str_equal(name, "cap:areaDesc"))  msg->cap.area_desc = g_strdup(text);
145
146         if (g_str_equal(name, "title"))
147                 msg->info = alert_info_find(msg->title);
148
149         if (g_str_equal(name, "valueName")) {
150                 if (data->value_name)
151                         g_free(data->value_name);
152                 data->value_name = g_strdup(text);
153         }
154
155         if (g_str_equal(name, "value") && data->value_name) {
156                 if (g_str_equal(data->value_name, "FIPS6")) msg->cap.fips6 = g_strdup(text);
157                 if (g_str_equal(data->value_name, "VTEC"))  msg->cap.vtec  = msg_parse_vtec(text);
158         }
159 }
160 void msg_parse_cap_end(GMarkupParseContext *context, const gchar *name,
161                 gpointer user_data, GError **error)
162 {
163         ParseData *data = user_data;
164         AlertMsg  *msg  = data->msg;
165         char      *text = data->text;
166         if (!text || !msg) return;
167         if      (g_str_equal(name, "description")) msg->description = g_strdup(text);
168         else if (g_str_equal(name, "instruction")) msg->instruction = g_strdup(text);
169         else if (g_str_equal(name, "polygon"))     msg->polygon     = g_strdup(text);
170 }
171
172 /* AlertMsg methods */
173 GList *msg_parse_index(gchar *text, gsize len, time_t *updated)
174 {
175         g_debug("GritsPluginAlert: msg_parse");
176         GMarkupParser parser = {
177                 .start_element = msg_parse_index_start,
178                 .end_element   = msg_parse_index_end,
179                 .text          = msg_parse_text,
180         };
181         ParseData data = {};
182         GMarkupParseContext *context =
183                 g_markup_parse_context_new(&parser, 0, &data, NULL);
184         g_markup_parse_context_parse(context, text, len, NULL);
185         g_markup_parse_context_free(context);
186         if (data.text)
187                 g_free(data.text);
188         if (data.value_name)
189                 g_free(data.value_name);
190         *updated = data.updated;
191         return data.msgs;
192 }
193
194 void msg_parse_cap(AlertMsg *msg, gchar *text, gsize len)
195 {
196         g_debug("GritsPluginAlert: msg_parse_cap");
197         GMarkupParser parser = {
198                 .end_element   = msg_parse_cap_end,
199                 .text          = msg_parse_text,
200         };
201         ParseData data = { .msg = msg };
202         GMarkupParseContext *context =
203                 g_markup_parse_context_new(&parser, 0, &data, NULL);
204         g_markup_parse_context_parse(context, text, len, NULL);
205         g_markup_parse_context_free(context);
206         if (data.text)
207                 g_free(data.text);
208
209         /* Add spaces to description... */
210         static GRegex *regex = NULL;
211         if (regex == NULL)
212                 regex = g_regex_new("\\.\\n", 0, G_REGEX_MATCH_NEWLINE_ANY, NULL);
213         if (msg->description && regex) {
214                 char *old = msg->description;
215                 msg->description = g_regex_replace_literal(
216                                 regex, old, -1, 0, ".\n\n", 0, NULL);
217                 g_free(old);
218         }
219 }
220
221 void msg_free(AlertMsg *msg)
222 {
223         g_free(msg->title);
224         g_free(msg->link);
225         g_free(msg->summary);
226         g_free(msg->cap.status);
227         g_free(msg->cap.urgency);
228         g_free(msg->cap.severity);
229         g_free(msg->cap.certainty);
230         g_free(msg->cap.area_desc);
231         g_free(msg->cap.fips6);
232         g_free(msg->cap.vtec);
233         g_free(msg->description);
234         g_free(msg->instruction);
235         g_free(msg->polygon);
236         g_free(msg);
237 }
238
239 void msg_print(GList *msgs)
240 {
241         g_message("msg_print");
242         for (GList *cur = msgs; cur; cur = cur->next) {
243                 AlertMsg *msg = cur->data;
244                 g_message("alert:");
245                 g_message("     title         = %s",  msg->title        );
246                 g_message("     link          = %s",  msg->link         );
247                 g_message("     summary       = %s",  msg->summary      );
248                 g_message("     cat.effective = %lu", msg->cap.effective);
249                 g_message("     cat.expires   = %lu", msg->cap.expires  );
250                 g_message("     cat.status    = %s",  msg->cap.status   );
251                 g_message("     cat.urgency   = %s",  msg->cap.urgency  );
252                 g_message("     cat.severity  = %s",  msg->cap.severity );
253                 g_message("     cat.certainty = %s",  msg->cap.certainty);
254                 g_message("     cat.area_desc = %s",  msg->cap.area_desc);
255                 g_message("     cat.fips6     = %s",  msg->cap.fips6    );
256                 g_message("     cat.vtec      = %p",  msg->cap.vtec     );
257         }
258 }
259
260 gchar *msg_find_nearest(GritsHttp *http, time_t when, gboolean offline)
261 {
262         GList *files = grits_http_available(http,
263                         "^[0-9]*.xml$", "index", NULL, NULL);
264
265         time_t  this_time    = 0;
266         time_t  nearest_time = offline ? 0 : time(NULL);
267         char   *nearest_file = NULL;
268
269         for (GList *cur = files; cur; cur = cur->next) {
270                 gchar *file = cur->data;
271                 sscanf(file, "%ld", &this_time);
272                 if (ABS(when - this_time) <
273                     ABS(when - nearest_time)) {
274                         nearest_file = file;
275                         nearest_time = this_time;
276                 }
277         }
278
279         if (nearest_file)
280                 return g_strconcat("index/", nearest_file, NULL);
281         else if (!offline)
282                 return g_strdup_printf("index/%ld.xml", time(NULL));
283         else
284                 return NULL;
285 }
286
287 GList *msg_load_index(GritsHttp *http, time_t when, time_t *updated, gboolean offline)
288 {
289         /* Fetch current alerts */
290         gchar *tmp = msg_find_nearest(http, when, offline);
291         if (!tmp)
292                 return NULL;
293         gchar *file = grits_http_fetch(http, MSG_INDEX, tmp, GRITS_ONCE, NULL, NULL);
294         g_free(tmp);
295         if (!file)
296                 return NULL;
297
298         /* Load file */
299         gchar *text; gsize len;
300         g_file_get_contents(file, &text, &len, NULL);
301         GList *msgs = msg_parse_index(text, len, updated);
302         //msg_print(msgs);
303         g_free(file);
304         g_free(text);
305
306         /* Delete unrecognized messages */
307         GList *dead = NULL;
308         for (GList *cur = msgs; cur; cur = cur->next)
309                 if (!((AlertMsg*)cur->data)->info)
310                         dead = g_list_prepend(dead, cur->data);
311         for (GList *cur = dead; cur; cur = cur->next) {
312                 AlertMsg *msg = cur->data;
313                 g_warning("GritsPluginAlert: unknown msg type - %s", msg->title);
314                 msgs = g_list_remove(msgs, msg);
315                 msg_free(msg);
316         }
317         g_list_free(dead);
318
319         return msgs;
320 }
321
322 void msg_load_cap(GritsHttp *http, AlertMsg *msg)
323 {
324         if (msg->description || msg->instruction || msg->polygon)
325                 return;
326         g_debug("GritsPlguinAlert: update_cap");
327
328         /* Download */
329         gchar *id = strrchr(msg->link, '=');
330         if (!id) return; id++;
331         gchar *dir  = g_strdelimit(g_strdup(msg->info->abbr), " ", '_');
332         gchar *tmp  = g_strdup_printf("%s/%s.xml", dir, id);
333         gchar *file = grits_http_fetch(http, msg->link, tmp, GRITS_ONCE, NULL, NULL);
334         g_free(tmp);
335         g_free(dir);
336         if (!file)
337                 return;
338
339         /* Parse */
340         gchar *text; gsize len;
341         g_file_get_contents(file, &text, &len, NULL);
342         msg_parse_cap(msg, text, len);
343         g_free(file);
344         g_free(text);
345 }
346
347
348 /********
349  * FIPS *
350  ********/
351 int fips_compare(int a, int b)
352 {
353         return (a <  b) ? -1 :
354                (a == b) ?  0 : 1;
355 }
356
357 GritsPoly *fips_combine(GList *polys)
358 {
359         /* Create points list */
360         GPtrArray *array = g_ptr_array_new();
361         for (GList *cur = polys; cur; cur = cur->next) {
362                 GritsPoly *poly       = cur->data;
363                 gdouble (**points)[3] = poly->points;
364                 for (int i = 0; points[i]; i++)
365                         g_ptr_array_add(array, points[i]);
366         }
367         g_ptr_array_add(array, NULL);
368         gdouble (**points)[3] = (gpointer)g_ptr_array_free(array, FALSE);
369
370         /* Calculate center */
371         GritsBounds bounds = {-90, 90, -180, 180};
372         for (GList *cur = polys; cur; cur = cur->next) {
373                 GritsObject *poly = cur->data;
374                 gdouble lat = poly->center.lat;
375                 gdouble lon = poly->center.lon;
376                 if (lat > bounds.n) bounds.n = lat;
377                 if (lat < bounds.s) bounds.s = lat;
378                 if (lon > bounds.e) bounds.e = lon;
379                 if (lon < bounds.w) bounds.w = lon;
380         }
381         GritsPoint center = {
382                 .lat = (bounds.n + bounds.s)/2,
383                 .lon = lon_avg(bounds.e, bounds.w),
384         };
385
386         /* Create polygon */
387         GritsPoly *poly = grits_poly_new(points);
388         GRITS_OBJECT(poly)->skip  |= GRITS_SKIP_CENTER;
389         GRITS_OBJECT(poly)->skip  |= GRITS_SKIP_STATE;
390         GRITS_OBJECT(poly)->center = center;
391         g_object_weak_ref(G_OBJECT(poly), (GWeakNotify)g_free, points);
392         return poly;
393 }
394
395 gboolean fips_group_state(gpointer key, gpointer value, gpointer data)
396 {
397         GList  *counties = value;
398         GList **states   = data;
399         GritsPoly *poly  = fips_combine(counties);
400         GRITS_OBJECT(poly)->lod = EARTH_R/10;
401         *states = g_list_prepend(*states, poly);
402         g_list_free(counties);
403         return FALSE;
404 }
405
406 void fips_parse(gchar *text, GTree **_counties, GList **_states)
407 {
408         g_debug("GritsPluginAlert: fips_parse");
409         GTree *counties = g_tree_new((GCompareFunc)fips_compare);
410         GTree *states   = g_tree_new_full((GCompareDataFunc)g_strcmp0,
411                         NULL, g_free, NULL);
412         gchar **lines = g_strsplit(text, "\n", -1);
413         for (gint li = 0; lines[li]; li++) {
414                 /* Split line */
415                 gchar **sparts = g_strsplit(lines[li], "\t", 4);
416                 int     nparts = g_strv_length(sparts);
417                 if (nparts < 4) {
418                         g_strfreev(sparts);
419                         continue;
420                 }
421
422                 /* Create GritsPoly */
423                 GritsPoly *poly = grits_poly_parse(sparts[3], "\t", " ", ",");
424
425                 /* Insert polys into the tree */
426                 glong id = g_ascii_strtoll(sparts[0], NULL, 10);
427                 g_tree_insert(counties, (gpointer)id, poly);
428
429                 /* Insert into states list */
430                 GList *list = g_tree_lookup(states, sparts[2]);
431                 list = g_list_prepend(list, poly);
432                 g_tree_replace(states, g_strdup(sparts[2]), list);
433
434                 g_strfreev(sparts);
435         }
436         g_strfreev(lines);
437
438         /* Group state counties */
439         *_counties = counties;
440         *_states   = NULL;
441         g_tree_foreach(states, (GTraverseFunc)fips_group_state, _states);
442         g_tree_destroy(states);
443 }
444
445 /********************
446  * GritsPluginAlert *
447  ********************/
448 static GtkWidget *_make_msg_details(AlertMsg *msg)
449 {
450
451         GtkWidget *title     = gtk_label_new("");
452         gchar     *title_str = g_markup_printf_escaped("<big><b>%s</b></big>",
453                         msg->title ?: "No title provided");
454         gtk_label_set_use_markup(GTK_LABEL(title), TRUE);
455         gtk_label_set_markup(GTK_LABEL(title), title_str);
456         gtk_label_set_line_wrap(GTK_LABEL(title), TRUE);
457         gtk_misc_set_alignment(GTK_MISC(title), 0, 0);
458         gtk_widget_set_size_request(GTK_WIDGET(title), 500, -1);
459         g_free(title_str);
460
461         GtkWidget     *alert      = gtk_scrolled_window_new(NULL, NULL);
462         GtkWidget     *alert_view = gtk_text_view_new();
463         GtkTextBuffer *alert_buf  = gtk_text_buffer_new(NULL);
464         gchar         *alert_str  = g_markup_printf_escaped("%s\n\n%s",
465                         msg->description ?: "No description provided",
466                         msg->instruction ?: "No instructions provided");
467         PangoFontDescription *alert_font = pango_font_description_from_string(
468                         "monospace");
469         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(alert),
470                         GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
471         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(alert),
472                         GTK_SHADOW_IN);
473         gtk_text_buffer_set_text(alert_buf, alert_str, -1);
474         gtk_text_view_set_buffer(GTK_TEXT_VIEW(alert_view), alert_buf);
475         gtk_widget_modify_font(GTK_WIDGET(alert_view), alert_font);
476         gtk_container_add(GTK_CONTAINER(alert), alert_view);
477         g_free(alert_str);
478
479         GtkWidget *align = gtk_alignment_new(0, 0, 1, 1);
480         GtkWidget *box   = gtk_vbox_new(FALSE, 10);
481         gtk_alignment_set_padding(GTK_ALIGNMENT(align), 10, 10, 10, 10);
482         gtk_container_add(GTK_CONTAINER(align), box);
483         gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0);
484         gtk_box_pack_start(GTK_BOX(box), alert, TRUE,  TRUE,  0);
485
486         return align;
487 }
488
489 static GtkWidget *_find_details(GtkNotebook *notebook, AlertMsg *msg)
490 {
491         int pages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
492         for (int i = 0; i < pages; i++) {
493                 GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
494                 if (msg == g_object_get_data(G_OBJECT(page), "msg"))
495                         return page;
496         }
497         return NULL;
498 }
499
500 static void _show_details(GritsPoly *county, GritsPluginAlert *alert)
501 {
502         /* Add details for this messages */
503         AlertMsg *msg = g_object_get_data(G_OBJECT(county), "msg");
504         msg_load_cap(alert->http, msg);
505
506         GtkWidget *dialog   = alert->details;
507         GtkWidget *content  = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
508         GList     *list     = gtk_container_get_children(GTK_CONTAINER(content));
509         GtkWidget *notebook = list->data;
510         GtkWidget *details  = _find_details(GTK_NOTEBOOK(notebook), msg);
511         g_list_free(list);
512
513         if (details == NULL) {
514                 details          = _make_msg_details(msg);
515                 GtkWidget *label = gtk_label_new(msg->info->abbr);
516                 g_object_set_data(G_OBJECT(details), "msg", msg);
517                 gtk_notebook_append_page(GTK_NOTEBOOK(notebook), details, label);
518         }
519
520         gtk_widget_show_all(dialog);
521         gint num = gtk_notebook_page_num(GTK_NOTEBOOK(notebook), details);
522         gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), num);
523 }
524
525 /* Update counties */
526 static void _alert_leave(GritsPoly *county, GritsPluginAlert *alert)
527 {
528         g_debug("_alert_leave");
529         if (county->width == 3) {
530                 county->color[3]  = 0;
531         } else {
532                 county->border[3] = 0.25;
533                 county->width     = 1;
534         }
535         grits_object_queue_draw(GRITS_OBJECT(county));
536 }
537
538 static void _alert_enter(GritsPoly *county, GritsPluginAlert *alert)
539 {
540         g_debug("_alert_enter");
541         if (county->width == 3) {
542                 county->color[3]  = 0.25;
543         } else {
544                 county->border[3] = 1.0;
545                 county->width     = 2;
546         }
547         grits_object_queue_draw(GRITS_OBJECT(county));
548 }
549
550 /* Update polygons */
551 static void _load_common(GritsPluginAlert *alert, AlertMsg *msg, GritsPoly *poly,
552                 float color, float border, int width)
553 {
554         g_object_set_data(G_OBJECT(poly), "msg", msg);
555         poly->color[0]  = poly->border[0] = (float)msg->info->color[0] / 256;
556         poly->color[1]  = poly->border[1] = (float)msg->info->color[1] / 256;
557         poly->color[2]  = poly->border[2] = (float)msg->info->color[2] / 256;
558         poly->color[3]  = color;
559         poly->border[3] = border;
560         poly->width     = width;
561         GRITS_OBJECT(poly)->lod    = 0;
562         GRITS_OBJECT(poly)->hidden = msg->info->hidden;
563         g_signal_connect(poly, "enter",   G_CALLBACK(_alert_enter),  alert);
564         g_signal_connect(poly, "leave",   G_CALLBACK(_alert_leave),  alert);
565         g_signal_connect(poly, "clicked", G_CALLBACK(_show_details), alert);
566 }
567
568 static GritsPoly *_load_storm_based(GritsPluginAlert *alert, AlertMsg *msg)
569 {
570         if (!msg->info->ispoly)
571                 return NULL;
572
573         msg_load_cap(alert->http, msg);
574
575         GritsPoly *poly = grits_poly_parse(msg->polygon, "\t", " ", ",");
576         _load_common(alert, msg, poly, 0, 1, 3);
577         grits_viewer_add(alert->viewer, GRITS_OBJECT(poly), GRITS_LEVEL_WORLD+2, TRUE);
578
579         return poly;
580 }
581
582 static GritsPoly *_load_county_based(GritsPluginAlert *alert, AlertMsg *msg)
583 {
584         /* Locate counties in the path of the storm */
585         gchar **fipses  = g_strsplit(msg->cap.fips6, " ", -1);
586         GList *counties = NULL;
587         for (int i = 0; fipses[i]; i++) {
588                 glong fips = g_ascii_strtoll(fipses[i], NULL, 10);
589                 GritsPoly *county = g_tree_lookup(alert->counties, (gpointer)fips);
590                 if (!county)
591                         continue;
592                 counties = g_list_prepend(counties, county);
593         }
594         g_strfreev(fipses);
595
596         /* No county based warning.. */
597         if (!counties)
598                 return NULL;
599
600         /* Create new county based warning */
601         GritsPoly *poly = fips_combine(counties);
602         _load_common(alert, msg, poly, 0.25, 0.25, 0);
603         grits_viewer_add(alert->viewer, GRITS_OBJECT(poly), GRITS_LEVEL_WORLD+1, FALSE);
604
605         g_list_free(counties);
606         return poly;
607 }
608
609 /* Update buttons */
610 static void _button_click(GtkToggleButton *button, gpointer _alert)
611 {
612         g_debug("GritsPluginAlert: _button_click");
613         GritsPluginAlert *alert = _alert;
614         AlertInfo *info = g_object_get_data(G_OBJECT(button), "info");
615         info->hidden = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
616
617         /* Show/hide each polygons */
618         for (GList *cur = alert->msgs; cur; cur = cur->next) {
619                 AlertMsg *msg = cur->data;
620                 if (msg->info != info)
621                         continue;
622                 if (msg->county_based) GRITS_OBJECT(msg->county_based)->hidden = info->hidden;
623                 if (msg->storm_based)  GRITS_OBJECT(msg->storm_based)->hidden  = info->hidden;
624         }
625         gtk_widget_queue_draw(GTK_WIDGET(alert->viewer));
626 }
627
628 static GtkWidget *_button_new(AlertInfo *info)
629 {
630         g_debug("GritsPluginAlert: _button_new - %s", info->title);
631         GdkColor black = {0, 0, 0, 0};
632         GdkColor color = {0, info->color[0]<<8, info->color[1]<<8, info->color[2]<<8};
633
634         gchar text[64];
635         g_snprintf(text, sizeof(text), "<b>%.10s</b>", info->abbr);
636
637         GtkWidget *button = gtk_toggle_button_new();
638         GtkWidget *align  = gtk_alignment_new(0.5, 0.5, 1, 1);
639         GtkWidget *cbox   = gtk_event_box_new();
640         GtkWidget *label  = gtk_label_new(text);
641         for (int state = 0; state < GTK_STATE_INSENSITIVE; state++) {
642                 gtk_widget_modify_fg(label, state, &black);
643                 gtk_widget_modify_bg(cbox,  state, &color);
644         } /* Yuck.. */
645         g_object_set_data(G_OBJECT(button), "info", info);
646         gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
647         gtk_alignment_set_padding(GTK_ALIGNMENT(align), 2, 2, 4, 4);
648         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), !info->hidden);
649         gtk_widget_set_tooltip_text(GTK_WIDGET(button), info->title);
650         gtk_container_add(GTK_CONTAINER(cbox), label);
651         gtk_container_add(GTK_CONTAINER(align), cbox);
652         gtk_container_add(GTK_CONTAINER(button), align);
653         return button;
654 }
655
656 static gboolean _update_buttons(GritsPluginAlert *alert)
657 {
658         g_debug("GritsPluginAlert: _update_buttons");
659         GtkWidget *alerts  = g_object_get_data(G_OBJECT(alert->config), "alerts");
660         GtkWidget *updated = g_object_get_data(G_OBJECT(alert->config), "updated");
661
662         /* Determine which buttons to show */
663         for (int i = 0; alert_info[i].title; i++)
664                 alert_info[i].current = FALSE;
665         for (GList *cur = alert->msgs; cur; cur = cur->next) {
666                 AlertMsg *msg = cur->data;
667                 msg->info->current = TRUE;
668         }
669
670         /* Delete old buttons */
671         GList *frames = gtk_container_get_children(GTK_CONTAINER(alerts));
672         for (GList *frame = frames; frame; frame = frame->next) {
673                 GtkWidget *table = gtk_bin_get_child(GTK_BIN(frame->data));
674                 GList *btns = gtk_container_get_children(GTK_CONTAINER(table));
675                 g_list_foreach(btns, (GFunc)gtk_widget_destroy, NULL);
676                 g_list_free(btns);
677         }
678         g_list_free(frames);
679
680         /* Add new buttons */
681         for (int i = 0; alert_info[i].title; i++) {
682                 if (!alert_info[i].current)
683                         continue;
684
685                 GtkWidget *table = g_object_get_data(G_OBJECT(alerts),
686                                 alert_info[i].category);
687                 GList *kids = gtk_container_get_children(GTK_CONTAINER(table));
688                 gint nkids = g_list_length(kids);
689                 guint rows, cols;
690                 gtk_table_get_size(GTK_TABLE(table), &rows, &cols);
691                 gint x = nkids % cols;
692                 gint y = nkids / cols;
693                 g_list_free(kids);
694
695                 GtkWidget *button = _button_new(&alert_info[i]);
696                 gtk_table_attach(GTK_TABLE(table), button, x, x+1, y, y+1,
697                                 GTK_FILL|GTK_EXPAND, GTK_FILL, 0, 0);
698                 g_signal_connect(button, "clicked",
699                                 G_CALLBACK(_button_click), alert);
700         }
701
702         /* Set time widget */
703         struct tm *tm = gmtime(&alert->updated);
704         gchar *date_str = g_strdup_printf(" <b><i>%04d-%02d-%02d %02d:%02d</i></b>",
705                         tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
706                         tm->tm_hour,      tm->tm_min);
707         gtk_label_set_markup(GTK_LABEL(updated), date_str);
708         g_free(date_str);
709
710         gtk_widget_show_all(GTK_WIDGET(alert->config));
711         return FALSE;
712 }
713
714 static gint _sort_warnings(gconstpointer _a, gconstpointer _b)
715 {
716         const AlertMsg *a=_a, *b=_b;
717         return (a->info->prior <  b->info->prior) ? -1 :
718                (a->info->prior == b->info->prior) ?  0 : 1;
719 }
720
721 static gboolean _update_end(gpointer _alert)
722 {
723         GritsPluginAlert *alert = _alert;
724         gtk_widget_queue_draw(GTK_WIDGET(alert->viewer));
725         return FALSE;
726 }
727
728 static void _update_warnings(GritsPluginAlert *alert, GList *old)
729 {
730         g_debug("GritsPluginAlert: _update_warnings");
731
732         /* Sort by priority:
733          *   reversed here since they're added
734          *   to the viewer in reverse order */
735         alert->msgs = g_list_sort(alert->msgs, _sort_warnings);
736
737         /* Remove old messages */
738         for (GList *cur = old; cur; cur = cur->next) {
739                 AlertMsg *msg = cur->data;
740                 if (msg->county_based) grits_viewer_remove(alert->viewer,
741                                 GRITS_OBJECT(msg->county_based));
742                 if (msg->storm_based) grits_viewer_remove(alert->viewer,
743                                 GRITS_OBJECT(msg->storm_based));
744         }
745
746         /* Add new messages */
747         /* Load counties first since it does not require network access */
748         for (GList *cur = alert->msgs; cur; cur = cur->next) {
749                 AlertMsg *msg = cur->data;
750                 msg->county_based = _load_county_based(alert, msg);
751         }
752         g_idle_add(_update_end, alert);
753         for (GList *cur = alert->msgs; cur; cur = cur->next) {
754                 AlertMsg *msg = cur->data;
755                 msg->storm_based  = _load_storm_based(alert, msg);
756         }
757         g_idle_add(_update_end, alert);
758
759         g_debug("GritsPluginAlert: _load_warnings - end");
760 }
761
762 /* Callbacks */
763 static void _update(gpointer _, gpointer _alert)
764 {
765         GritsPluginAlert *alert = _alert;
766         GList *old = alert->msgs;
767         g_debug("GritsPluginAlert: _update");
768
769         time_t   when    = grits_viewer_get_time(alert->viewer);
770         gboolean offline = grits_viewer_get_offline(alert->viewer);
771         if (!(alert->msgs = msg_load_index(alert->http, when, &alert->updated, offline)))
772                 return;
773
774         g_idle_add((GSourceFunc)_update_buttons, alert);
775         _update_warnings(alert, old);
776
777         g_list_foreach(old, (GFunc)msg_free, NULL);
778         g_list_free(old);
779
780         g_debug("GritsPluginAlert: _update - end");
781 }
782
783 static void _on_update(GritsPluginAlert *alert)
784 {
785         g_thread_pool_push(alert->threads, NULL+1, NULL);
786 }
787
788 /* Init helpers */
789 static GtkWidget *_make_config(void)
790 {
791         GtkWidget *config = gtk_vbox_new(FALSE, 0);
792
793         /* Setup tools area */
794         GtkWidget *tools   = gtk_hbox_new(FALSE, 10);
795         GtkWidget *updated = gtk_label_new(" Loading...");
796         gtk_label_set_use_markup(GTK_LABEL(updated), TRUE);
797         gtk_box_pack_start(GTK_BOX(tools), updated, FALSE, FALSE, 0);
798         gtk_box_pack_start(GTK_BOX(config), tools, FALSE, FALSE, 0);
799         g_object_set_data(G_OBJECT(config), "updated", updated);
800
801         /* Setup alerts */
802         gchar *labels[] = {"<b>Warnings</b>", "<b>Watches</b>",
803                            "<b>Advisories</b>", "<b>Other</b>"};
804         gchar *keys[]   = {"warning",  "watch",   "advisory",   "other"};
805         gint   cols[]   = {2, 2, 3, 2};
806         GtkWidget *alerts = gtk_hbox_new(FALSE, 10);
807         for (int i = 0; i < G_N_ELEMENTS(labels); i++) {
808                 GtkWidget *frame = gtk_frame_new(labels[i]);
809                 GtkWidget *table = gtk_table_new(1, cols[i], TRUE);
810                 GtkWidget *label = gtk_frame_get_label_widget(GTK_FRAME(frame));
811                 gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
812                 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE);
813                 gtk_container_add(GTK_CONTAINER(frame), table);
814                 gtk_box_pack_start(GTK_BOX(alerts), frame, TRUE, TRUE, 0);
815                 g_object_set_data(G_OBJECT(alerts), keys[i], table);
816         }
817         gtk_box_pack_start(GTK_BOX(config), alerts, TRUE, TRUE, 0);
818         g_object_set_data(G_OBJECT(config), "alerts",  alerts);
819
820         return config;
821 }
822
823 static gboolean _clear_details(GtkWidget *dialog)
824 {
825         GtkWidget *content  = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
826         GList     *list     = gtk_container_get_children(GTK_CONTAINER(content));
827         GtkWidget *notebook = list->data;
828         g_list_free(list);
829         gtk_widget_hide(dialog);
830         while (gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook)))
831                 gtk_notebook_remove_page(GTK_NOTEBOOK(notebook), 0);
832         return TRUE;
833 }
834
835 static gboolean _set_details_uri(GtkWidget *notebook, GtkNotebookPage *_,
836                 guint num, GtkWidget *button)
837 {
838         g_debug("_set_details_uri");
839         GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), num);
840         AlertMsg  *msg  = g_object_get_data(G_OBJECT(page), "msg");
841         gtk_link_button_set_uri(GTK_LINK_BUTTON(button), msg->link);
842         return FALSE;
843 }
844
845 static GtkWidget *_make_details(GritsViewer *viewer)
846 {
847         GtkWidget *dialog   = gtk_dialog_new();
848         GtkWidget *action   = gtk_dialog_get_action_area (GTK_DIALOG(dialog));
849         GtkWidget *content  = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
850         GtkWidget *notebook = gtk_notebook_new();
851         GtkWidget *win      = gtk_widget_get_toplevel(GTK_WIDGET(viewer));
852         GtkWidget *link     = gtk_link_button_new_with_label("", "Full Text");
853
854         gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(win));
855         gtk_window_set_title(GTK_WINDOW(dialog), "Alert Details - AWeather");
856         gtk_window_set_default_size(GTK_WINDOW(dialog), 625, 500);
857         gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), TRUE);
858         gtk_container_add(GTK_CONTAINER(content), notebook);
859         gtk_box_pack_end(GTK_BOX(action), link, 0, 0, 0);
860         gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE);
861
862         g_signal_connect(dialog,   "response",     G_CALLBACK(_clear_details),   NULL);
863         g_signal_connect(dialog,   "delete-event", G_CALLBACK(_clear_details),   NULL);
864         g_signal_connect(notebook, "switch-page",  G_CALLBACK(_set_details_uri), link);
865
866         return dialog;
867 }
868
869 /* Methods */
870 GritsPluginAlert *grits_plugin_alert_new(GritsViewer *viewer, GritsPrefs *prefs)
871 {
872         g_debug("GritsPluginAlert: new");
873         GritsPluginAlert *alert = g_object_new(GRITS_TYPE_PLUGIN_ALERT, NULL);
874         alert->details = _make_details(viewer);
875         alert->viewer  = g_object_ref(viewer);
876         alert->prefs   = g_object_ref(prefs);
877
878         alert->refresh_id      = g_signal_connect_swapped(alert->viewer, "refresh",
879                         G_CALLBACK(_on_update), alert);
880         alert->time_changed_id = g_signal_connect_swapped(alert->viewer, "time_changed",
881                         G_CALLBACK(_on_update), alert);
882
883         for (GList *cur = alert->states; cur; cur = cur->next)
884                 grits_viewer_add(viewer, cur->data, GRITS_LEVEL_WORLD+1, FALSE);
885
886         _on_update(alert);
887         return alert;
888 }
889
890 static GtkWidget *grits_plugin_alert_get_config(GritsPlugin *_alert)
891 {
892         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(_alert);
893         return alert->config;
894 }
895
896
897 /* GObject code */
898 static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface);
899 G_DEFINE_TYPE_WITH_CODE(GritsPluginAlert, grits_plugin_alert, G_TYPE_OBJECT,
900                 G_IMPLEMENT_INTERFACE(GRITS_TYPE_PLUGIN,
901                         grits_plugin_alert_plugin_init));
902 static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface)
903 {
904         g_debug("GritsPluginAlert: plugin_init");
905         /* Add methods to the interface */
906         iface->get_config = grits_plugin_alert_get_config;
907 }
908 static void grits_plugin_alert_init(GritsPluginAlert *alert)
909 {
910         g_debug("GritsPluginAlert: init");
911         /* Set defaults */
912         alert->threads = g_thread_pool_new(_update, alert, 1, FALSE, NULL);
913         alert->config  = _make_config();
914         alert->http    = grits_http_new(G_DIR_SEPARATOR_S
915                         "alerts" G_DIR_SEPARATOR_S
916                         "cap"    G_DIR_SEPARATOR_S);
917
918         /* Load counties */
919         gchar *text; gsize len;
920         const gchar *file = PKGDATADIR G_DIR_SEPARATOR_S "fips.txt";
921         if (!g_file_get_contents(file, &text, &len, NULL))
922                 g_error("GritsPluginAlert: init - error loading fips polygons");
923         fips_parse(text, &alert->counties, &alert->states);
924         g_free(text);
925 }
926 static void grits_plugin_alert_dispose(GObject *gobject)
927 {
928         g_debug("GritsPluginAlert: dispose");
929         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
930         /* Drop references */
931         if (alert->viewer) {
932                 GritsViewer *viewer = alert->viewer;
933                 alert->viewer       = NULL;
934                 g_signal_handler_disconnect(viewer, alert->refresh_id);
935                 g_signal_handler_disconnect(viewer, alert->time_changed_id);
936                 soup_session_abort(alert->http->soup);
937                 g_thread_pool_free(alert->threads, TRUE, TRUE);
938                 gtk_widget_destroy(alert->details);
939                 while (gtk_events_pending())
940                         gtk_main_iteration();
941                 for (GList *cur = alert->msgs; cur; cur = cur->next) {
942                         AlertMsg *msg = cur->data;
943                         if (msg->county_based) grits_viewer_remove(viewer,
944                                         GRITS_OBJECT(msg->county_based));
945                         if (msg->storm_based) grits_viewer_remove(viewer,
946                                         GRITS_OBJECT(msg->storm_based));
947                 }
948                 for (GList *cur = alert->states; cur; cur = cur->next)
949                         grits_viewer_remove(viewer, cur->data);
950                 g_object_unref(alert->prefs);
951                 g_object_unref(viewer);
952         }
953         G_OBJECT_CLASS(grits_plugin_alert_parent_class)->dispose(gobject);
954 }
955 static gboolean _unref_county(gpointer key, gpointer val, gpointer data)
956 {
957         g_object_unref(val);
958         return FALSE;
959 }
960 static void grits_plugin_alert_finalize(GObject *gobject)
961 {
962         g_debug("GritsPluginAlert: finalize");
963         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
964         g_list_foreach(alert->msgs, (GFunc)msg_free, NULL);
965         g_list_free(alert->msgs);
966         g_list_free(alert->states);
967         g_tree_foreach(alert->counties, (GTraverseFunc)_unref_county, NULL);
968         g_tree_destroy(alert->counties);
969         grits_http_free(alert->http);
970         G_OBJECT_CLASS(grits_plugin_alert_parent_class)->finalize(gobject);
971 }
972 static void grits_plugin_alert_class_init(GritsPluginAlertClass *klass)
973 {
974         g_debug("GritsPluginAlert: class_init");
975         GObjectClass *gobject_class = (GObjectClass*)klass;
976         gobject_class->dispose  = grits_plugin_alert_dispose;
977         gobject_class->finalize = grits_plugin_alert_finalize;
978 }