]> Pileus Git - aweather/blobdiff - src/plugins/alert.c
Add warning/watch/alert plugin
[aweather] / src / plugins / alert.c
diff --git a/src/plugins/alert.c b/src/plugins/alert.c
new file mode 100644 (file)
index 0000000..d3a7018
--- /dev/null
@@ -0,0 +1,966 @@
+/*
+ * Copyright (C) 2010-2011 Andy Spencer <andy753421@gmail.com>
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include <sys/time.h>
+#include <grits.h>
+#include <GL/gl.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "alert.h"
+#include "alert-info.h"
+
+#define MSG_INDEX "http://alerts.weather.gov/cap/us.php?x=0"
+
+/* Format for single cap alert:
+ *   "http://alerts.weather.gov/cap/wwacapget.php?x="
+ *   "AK20111012000500CoastalFloodWarning20111012151500"
+ *   "AKAFGCFWAFG.6258a84eb1e8c34cd888057248224d10" */
+
+/************
+ * AlertMsg *
+ ************/
+/* AlertMsg data types */
+typedef struct {
+       char sep0;
+       char class [1 ]; char sep1;
+       char action[3 ]; char sep2;
+       char office[4 ]; char sep3;
+       char phenom[2 ]; char sep4;
+       char signif[1 ]; char sep5;
+       char event [4 ]; char sep6;
+       char begin [12]; char sep7;
+       char end   [12]; char sep8;
+} AlertVtec;
+
+typedef struct {
+       /* Basic info (from alert index) */
+       char *title;   // Winter Weather Advisory issued December 19 at 8:51PM
+       char *link;    // http://www.weather.gov/alerts-beta/wwacapget.php?x=AK20101219205100AFGWinterWeatherAdvisoryAFG20101220030000AK
+       char *summary; // ...WINTER WEATHER ADVISORY REMAINS IN EFFECT UNTIL 6
+       struct {
+               time_t     effective; // 2010-12-19T20:51:00-09:00
+               time_t     expires;   // 2010-12-20T03:00:00-09:00
+               char      *status;    // Actual
+               char      *urgency;   // Expected
+               char      *severity;  // Minor
+               char      *certainty; // Likely
+               char      *area_desc; // Northeastern Brooks Range; Northwestern Brooks Range
+               char      *fips6;     // 006015 006023 006045 006105
+               AlertVtec *vtec;      // /X.CON.PAFG.WW.Y.0064.000000T0000Z-101220T0300Z/
+       } cap;
+
+       /* Advanced info (from CAP file) */
+       char *description;
+       char *instruction;
+       char *polygon;
+
+       /* Internal state */
+       AlertInfo *info;         // Link to info structure for this alert
+       GritsPoly *county_based; // Polygon for county-based warning
+       GritsPoly *storm_based;  // Polygon for storm-based warning
+} AlertMsg;
+
+/* AlertMsg parsing */
+typedef struct {
+       time_t    updated;
+       AlertMsg *msg;
+       GList    *msgs;
+       gchar    *text;
+       gchar    *value_name;
+} ParseData;
+time_t msg_parse_time(gchar *iso8601)
+{
+       GTimeVal tv = {};
+       g_time_val_from_iso8601(iso8601, &tv);
+       return tv.tv_sec;
+}
+AlertVtec *msg_parse_vtec(char *buf)
+{
+       AlertVtec *vtec = g_new0(AlertVtec, 1);
+       strncpy((char*)vtec, buf, sizeof(AlertVtec));
+       vtec->sep0 = vtec->sep1 = vtec->sep2 = '\0';
+       vtec->sep3 = vtec->sep4 = vtec->sep5 = '\0';
+       vtec->sep6 = vtec->sep7 = vtec->sep8 = '\0';
+       return vtec;
+}
+void msg_parse_text(GMarkupParseContext *context, const gchar *text,
+               gsize len, gpointer user_data, GError **error)
+{
+       //g_debug("text %s", text);
+       ParseData *data = user_data;
+       if (data->text)
+               g_free(data->text);
+       data->text = g_strndup(text, len);
+}
+void msg_parse_index_start(GMarkupParseContext *context, const gchar *name,
+               const gchar **keys, const gchar **vals,
+               gpointer user_data, GError **error)
+{
+       //g_debug("start %s", name);
+       ParseData *data = user_data;
+       if (g_str_equal(name, "entry"))
+               data->msg  = g_new0(AlertMsg, 1);
+}
+void msg_parse_index_end(GMarkupParseContext *context, const gchar *name,
+               gpointer user_data, GError **error)
+{
+       //g_debug("end %s", name);
+       ParseData *data = user_data;
+       AlertMsg  *msg  = data->msg;
+       char      *text = data->text;
+
+       if (g_str_equal(name, "updated") && text && !data->updated)
+               data->updated = msg_parse_time(text);
+
+       if (g_str_equal(name, "entry"))
+               data->msgs = g_list_prepend(data->msgs, data->msg);
+
+       if (!text || !msg) return;
+       else if (g_str_equal(name, "title"))         msg->title         = g_strdup(text);
+       else if (g_str_equal(name, "id"))            msg->link          = g_strdup(text); // hack
+       else if (g_str_equal(name, "summary"))       msg->summary       = g_strdup(text);
+       else if (g_str_equal(name, "cap:effective")) msg->cap.effective = msg_parse_time(text);
+       else if (g_str_equal(name, "cap:expires"))   msg->cap.expires   = msg_parse_time(text);
+       else if (g_str_equal(name, "cap:status"))    msg->cap.status    = g_strdup(text);
+       else if (g_str_equal(name, "cap:urgency"))   msg->cap.urgency   = g_strdup(text);
+       else if (g_str_equal(name, "cap:severity"))  msg->cap.severity  = g_strdup(text);
+       else if (g_str_equal(name, "cap:certainty")) msg->cap.certainty = g_strdup(text);
+       else if (g_str_equal(name, "cap:areaDesc"))  msg->cap.area_desc = g_strdup(text);
+
+       if (g_str_equal(name, "title"))
+               msg->info = alert_info_find(msg->title);
+
+       if (g_str_equal(name, "valueName")) {
+               if (data->value_name)
+                       g_free(data->value_name);
+               data->value_name = g_strdup(text);
+       }
+
+       if (g_str_equal(name, "value") && data->value_name) {
+               if (g_str_equal(data->value_name, "FIPS6")) msg->cap.fips6 = g_strdup(text);
+               if (g_str_equal(data->value_name, "VTEC"))  msg->cap.vtec  = msg_parse_vtec(text);
+       }
+}
+void msg_parse_cap_end(GMarkupParseContext *context, const gchar *name,
+               gpointer user_data, GError **error)
+{
+       ParseData *data = user_data;
+       AlertMsg  *msg  = data->msg;
+       char      *text = data->text;
+       if (!text || !msg) return;
+       if      (g_str_equal(name, "description")) msg->description = g_strdup(text);
+       else if (g_str_equal(name, "instruction")) msg->instruction = g_strdup(text);
+       else if (g_str_equal(name, "polygon"))     msg->polygon     = g_strdup(text);
+}
+
+/* AlertMsg methods */
+GList *msg_parse_index(gchar *text, gsize len, time_t *updated)
+{
+       g_debug("GritsPluginAlert: msg_parse");
+       GMarkupParser parser = {
+               .start_element = msg_parse_index_start,
+               .end_element   = msg_parse_index_end,
+               .text          = msg_parse_text,
+       };
+       ParseData data = {};
+       GMarkupParseContext *context =
+               g_markup_parse_context_new(&parser, 0, &data, NULL);
+       g_markup_parse_context_parse(context, text, len, NULL);
+       g_markup_parse_context_free(context);
+       if (data.text)
+               g_free(data.text);
+       if (data.value_name)
+               g_free(data.value_name);
+       *updated = data.updated;
+       return data.msgs;
+}
+
+void msg_parse_cap(AlertMsg *msg, gchar *text, gsize len)
+{
+       g_debug("GritsPluginAlert: msg_parse_cap");
+       GMarkupParser parser = {
+               .end_element   = msg_parse_cap_end,
+               .text          = msg_parse_text,
+       };
+       ParseData data = { .msg = msg };
+       GMarkupParseContext *context =
+               g_markup_parse_context_new(&parser, 0, &data, NULL);
+       g_markup_parse_context_parse(context, text, len, NULL);
+       g_markup_parse_context_free(context);
+       if (data.text)
+               g_free(data.text);
+
+       /* Add spaces to description... */
+       static GRegex *regex = NULL;
+       if (regex == NULL)
+               regex = g_regex_new("\\.\\n", 0, G_REGEX_MATCH_NEWLINE_ANY, NULL);
+       if (msg->description && regex) {
+               char *old = msg->description;
+               msg->description = g_regex_replace_literal(
+                               regex, old, -1, 0, ".\n\n", 0, NULL);
+               g_free(old);
+       }
+}
+
+void msg_free(AlertMsg *msg)
+{
+       g_free(msg->title);
+       g_free(msg->link);
+       g_free(msg->summary);
+       g_free(msg->cap.status);
+       g_free(msg->cap.urgency);
+       g_free(msg->cap.severity);
+       g_free(msg->cap.certainty);
+       g_free(msg->cap.area_desc);
+       g_free(msg->cap.fips6);
+       g_free(msg->cap.vtec);
+       g_free(msg->description);
+       g_free(msg->instruction);
+       g_free(msg->polygon);
+       g_free(msg);
+}
+
+void msg_print(GList *msgs)
+{
+       g_message("msg_print");
+       for (GList *cur = msgs; cur; cur = cur->next) {
+               AlertMsg *msg = cur->data;
+               g_message("alert:");
+               g_message("     title         = %s",  msg->title        );
+               g_message("     link          = %s",  msg->link         );
+               g_message("     summary       = %s",  msg->summary      );
+               g_message("     cat.effective = %lu", msg->cap.effective);
+               g_message("     cat.expires   = %lu", msg->cap.expires  );
+               g_message("     cat.status    = %s",  msg->cap.status   );
+               g_message("     cat.urgency   = %s",  msg->cap.urgency  );
+               g_message("     cat.severity  = %s",  msg->cap.severity );
+               g_message("     cat.certainty = %s",  msg->cap.certainty);
+               g_message("     cat.area_desc = %s",  msg->cap.area_desc);
+               g_message("     cat.fips6     = %s",  msg->cap.fips6    );
+               g_message("     cat.vtec      = %p",  msg->cap.vtec     );
+       }
+}
+
+gchar *msg_find_nearest(GritsHttp *http, time_t when, gboolean offline)
+{
+       GList *files = grits_http_available(http,
+                       "^[0-9]*.xml$", "index", NULL, NULL);
+
+       time_t  this_time    = 0;
+       time_t  nearest_time = offline ? 0 : time(NULL);
+       char   *nearest_file = NULL;
+
+       for (GList *cur = files; cur; cur = cur->next) {
+               gchar *file = cur->data;
+               sscanf(file, "%ld", &this_time);
+               if (ABS(when - this_time) <
+                   ABS(when - nearest_time)) {
+                       nearest_file = file;
+                       nearest_time = this_time;
+               }
+       }
+
+       if (nearest_file)
+               return g_strconcat("index/", nearest_file, NULL);
+       else if (!offline)
+               return g_strdup_printf("index/%ld.xml", time(NULL));
+       else
+               return NULL;
+}
+
+GList *msg_load_index(GritsHttp *http, time_t when, time_t *updated, gboolean offline)
+{
+       /* Fetch current alerts */
+       gchar *tmp = msg_find_nearest(http, when, offline);
+       if (!tmp)
+               return NULL;
+       gchar *file = grits_http_fetch(http, MSG_INDEX, tmp, GRITS_ONCE, NULL, NULL);
+       g_free(tmp);
+       if (!file)
+               return NULL;
+
+       /* Load file */
+       gchar *text; gsize len;
+       g_file_get_contents(file, &text, &len, NULL);
+       GList *msgs = msg_parse_index(text, len, updated);
+       //msg_print(msgs);
+       g_free(file);
+       g_free(text);
+
+       /* Delete unrecognized messages */
+       GList *dead = NULL;
+       for (GList *cur = msgs; cur; cur = cur->next)
+               if (!((AlertMsg*)cur->data)->info)
+                       dead = g_list_prepend(dead, cur->data);
+       for (GList *cur = dead; cur; cur = cur->next) {
+               AlertMsg *msg = cur->data;
+               g_warning("GritsPluginAlert: unknown msg type - %s", msg->title);
+               msgs = g_list_remove(msgs, msg);
+               msg_free(msg);
+       }
+       g_list_free(dead);
+
+       return msgs;
+}
+
+void msg_load_cap(GritsHttp *http, AlertMsg *msg)
+{
+       if (msg->description || msg->instruction || msg->polygon)
+               return;
+       g_debug("GritsPlguinAlert: update_cap");
+
+       /* Download */
+       gchar *id = strrchr(msg->link, '=');
+       if (!id) return; id++;
+       gchar *dir  = g_strdelimit(g_strdup(msg->info->abbr), " ", '_');
+       gchar *tmp  = g_strdup_printf("%s/%s.xml", dir, id);
+       gchar *file = grits_http_fetch(http, msg->link, tmp, GRITS_ONCE, NULL, NULL);
+       g_free(tmp);
+       g_free(dir);
+       if (!file)
+               return;
+
+       /* Parse */
+       gchar *text; gsize len;
+       g_file_get_contents(file, &text, &len, NULL);
+       msg_parse_cap(msg, text, len);
+       g_free(file);
+       g_free(text);
+}
+
+
+/********
+ * FIPS *
+ ********/
+int fips_compare(int a, int b)
+{
+       return (a <  b) ? -1 :
+              (a == b) ?  0 : 1;
+}
+
+GritsPoly *fips_combine(GList *polys)
+{
+       /* Create points list */
+       GPtrArray *array = g_ptr_array_new();
+       for (GList *cur = polys; cur; cur = cur->next) {
+               GritsPoly *poly       = cur->data;
+               gdouble (**points)[3] = poly->points;
+               for (int i = 0; points[i]; i++)
+                       g_ptr_array_add(array, points[i]);
+       }
+       g_ptr_array_add(array, NULL);
+       gdouble (**points)[3] = (gpointer)g_ptr_array_free(array, FALSE);
+
+       /* Calculate center */
+       GritsBounds bounds = {-90, 90, -180, 180};
+       for (GList *cur = polys; cur; cur = cur->next) {
+               GritsObject *poly = cur->data;
+               gdouble lat = poly->center.lat;
+               gdouble lon = poly->center.lon;
+               if (lat > bounds.n) bounds.n = lat;
+               if (lat < bounds.s) bounds.s = lat;
+               if (lon > bounds.e) bounds.e = lon;
+               if (lon < bounds.w) bounds.w = lon;
+       }
+       GritsPoint center = {
+               .lat = (bounds.n + bounds.s)/2,
+               .lon = lon_avg(bounds.e, bounds.w),
+       };
+
+       /* Create polygon */
+       GritsPoly *poly = grits_poly_new(points);
+       GRITS_OBJECT(poly)->skip  |= GRITS_SKIP_CENTER;
+       GRITS_OBJECT(poly)->skip  |= GRITS_SKIP_STATE;
+       GRITS_OBJECT(poly)->center = center;
+       g_object_weak_ref(G_OBJECT(poly), (GWeakNotify)g_free, points);
+       return poly;
+}
+
+gboolean fips_group_state(gpointer key, gpointer value, gpointer data)
+{
+       GList  *counties = value;
+       GList **states   = data;
+       GritsPoly *poly  = fips_combine(counties);
+       GRITS_OBJECT(poly)->lod = EARTH_R/10;
+       *states = g_list_prepend(*states, poly);
+       g_list_free(counties);
+       return FALSE;
+}
+
+void fips_parse(gchar *text, GTree **_counties, GList **_states)
+{
+       g_debug("GritsPluginAlert: fips_parse");
+       GTree *counties = g_tree_new((GCompareFunc)fips_compare);
+       GTree *states   = g_tree_new_full((GCompareDataFunc)g_strcmp0,
+                       NULL, g_free, NULL);
+       gchar **lines = g_strsplit(text, "\n", -1);
+       for (gint li = 0; lines[li]; li++) {
+               /* Split line */
+               gchar **sparts = g_strsplit(lines[li], "\t", 4);
+               int     nparts = g_strv_length(sparts);
+               if (nparts < 4) {
+                       g_strfreev(sparts);
+                       continue;
+               }
+
+               /* Create GritsPoly */
+               GritsPoly *poly = grits_poly_parse(sparts[3], "\t", " ", ",");
+
+               /* Insert polys into the tree */
+               gint id = g_ascii_strtoll(sparts[0], NULL, 10);
+               g_tree_insert(counties, (gpointer)id, poly);
+
+               /* Insert into states list */
+               GList *list = g_tree_lookup(states, sparts[2]);
+               list = g_list_prepend(list, poly);
+               g_tree_replace(states, g_strdup(sparts[2]), list);
+
+               g_strfreev(sparts);
+       }
+       g_strfreev(lines);
+
+       /* Group state counties */
+       *_counties = counties;
+       *_states   = NULL;
+       g_tree_foreach(states, (GTraverseFunc)fips_group_state, _states);
+       g_tree_destroy(states);
+}
+
+/********************
+ * GritsPluginAlert *
+ ********************/
+static GtkWidget *_make_msg_details(AlertMsg *msg)
+{
+
+       GtkWidget *title     = gtk_label_new("");
+       gchar     *title_str = g_markup_printf_escaped(
+                       "<big><b>%s</b></big>", msg->title);
+       gtk_label_set_use_markup(GTK_LABEL(title), TRUE);
+       gtk_label_set_markup(GTK_LABEL(title), title_str);
+       gtk_label_set_line_wrap(GTK_LABEL(title), TRUE);
+       gtk_misc_set_alignment(GTK_MISC(title), 0, 0);
+       gtk_widget_set_size_request(GTK_WIDGET(title), 500, -1);
+       g_free(title_str);
+
+       GtkWidget     *alert      = gtk_scrolled_window_new(NULL, NULL);
+       GtkWidget     *alert_view = gtk_text_view_new();
+       GtkTextBuffer *alert_buf  = gtk_text_buffer_new(NULL);
+       gchar         *alert_str  = g_markup_printf_escaped(
+                       "%s\n\n%s", msg->description, msg->instruction);
+       PangoFontDescription *alert_font = pango_font_description_from_string(
+                       "monospace");
+       gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(alert),
+                       GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+       gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(alert),
+                       GTK_SHADOW_IN);
+       gtk_text_buffer_set_text(alert_buf, alert_str, -1);
+       gtk_text_view_set_buffer(GTK_TEXT_VIEW(alert_view), alert_buf);
+       gtk_widget_modify_font(GTK_WIDGET(alert_view), alert_font);
+       gtk_container_add(GTK_CONTAINER(alert), alert_view);
+       g_free(alert_str);
+
+       GtkWidget *align = gtk_alignment_new(0, 0, 1, 1);
+       GtkWidget *box   = gtk_vbox_new(FALSE, 10);
+       gtk_alignment_set_padding(GTK_ALIGNMENT(align), 10, 10, 10, 10);
+       gtk_container_add(GTK_CONTAINER(align), box);
+       gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0);
+       gtk_box_pack_start(GTK_BOX(box), alert, TRUE,  TRUE,  0);
+
+       return align;
+}
+
+static GtkWidget *_find_details(GtkNotebook *notebook, AlertMsg *msg)
+{
+       int pages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
+       for (int i = 0; i < pages; i++) {
+               GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
+               if (msg == g_object_get_data(G_OBJECT(page), "msg"))
+                       return page;
+       }
+       return NULL;
+}
+
+static void _show_details(GritsPoly *county, GritsPluginAlert *alert)
+{
+       /* Add details for this messages */
+       AlertMsg *msg = g_object_get_data(G_OBJECT(county), "msg");
+       msg_load_cap(alert->http, msg);
+
+       GtkWidget *dialog   = alert->details;
+       GtkWidget *content  = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+       GList     *list     = gtk_container_get_children(GTK_CONTAINER(content));
+       GtkWidget *notebook = list->data;
+       GtkWidget *details  = _find_details(GTK_NOTEBOOK(notebook), msg);
+       g_list_free(list);
+
+       if (details == NULL) {
+               details          = _make_msg_details(msg);
+               GtkWidget *label = gtk_label_new(msg->info->abbr);
+               g_object_set_data(G_OBJECT(details), "msg", msg);
+               gtk_notebook_append_page(GTK_NOTEBOOK(notebook), details, label);
+       }
+
+       gtk_widget_show_all(dialog);
+       gint num = gtk_notebook_page_num(GTK_NOTEBOOK(notebook), details);
+       gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), num);
+}
+
+/* Update counties */
+static void _alert_leave(GritsPoly *county, GritsPluginAlert *alert)
+{
+       g_debug("_alert_leave");
+       if (county->width == 3) {
+               county->color[3]  = 0;
+       } else {
+               county->border[3] = 0.25;
+               county->width     = 1;
+       }
+       grits_object_queue_draw(GRITS_OBJECT(county));
+}
+
+static void _alert_enter(GritsPoly *county, GritsPluginAlert *alert)
+{
+       g_debug("_alert_enter");
+       if (county->width == 3) {
+               county->color[3]  = 0.25;
+       } else {
+               county->border[3] = 1.0;
+               county->width     = 2;
+       }
+       grits_object_queue_draw(GRITS_OBJECT(county));
+}
+
+/* Update polygons */
+static void _load_common(GritsPluginAlert *alert, AlertMsg *msg, GritsPoly *poly,
+               float color, float border, int width)
+{
+       g_object_set_data(G_OBJECT(poly), "msg", msg);
+       poly->color[0]  = poly->border[0] = (float)msg->info->color[0] / 256;
+       poly->color[1]  = poly->border[1] = (float)msg->info->color[1] / 256;
+       poly->color[2]  = poly->border[2] = (float)msg->info->color[2] / 256;
+       poly->color[3]  = color;
+       poly->border[3] = border;
+       poly->width     = width;
+       GRITS_OBJECT(poly)->lod    = 0;
+       GRITS_OBJECT(poly)->hidden = msg->info->hidden;
+       g_signal_connect(poly, "enter",   G_CALLBACK(_alert_enter),  alert);
+       g_signal_connect(poly, "leave",   G_CALLBACK(_alert_leave),  alert);
+       g_signal_connect(poly, "clicked", G_CALLBACK(_show_details), alert);
+}
+
+static GritsPoly *_load_storm_based(GritsPluginAlert *alert, AlertMsg *msg)
+{
+       if (!msg->info->ispoly)
+               return NULL;
+
+       msg_load_cap(alert->http, msg);
+
+       GritsPoly *poly = grits_poly_parse(msg->polygon, "\t", " ", ",");
+       _load_common(alert, msg, poly, 0, 1, 3);
+       grits_viewer_add(alert->viewer, GRITS_OBJECT(poly), GRITS_LEVEL_WORLD+2, TRUE);
+
+       return poly;
+}
+
+static GritsPoly *_load_county_based(GritsPluginAlert *alert, AlertMsg *msg)
+{
+       /* Locate counties in the path of the storm */
+       gchar **fipses  = g_strsplit(msg->cap.fips6, " ", -1);
+       GList *counties = NULL;
+       for (int i = 0; fipses[i]; i++) {
+               gint fips = g_ascii_strtoll(fipses[i], NULL, 10);
+               GritsPoly *county = g_tree_lookup(alert->counties, (gpointer)fips);
+               if (!county)
+                       continue;
+               counties = g_list_prepend(counties, county);
+       }
+       g_strfreev(fipses);
+
+       /* No county based warning.. */
+       if (!counties)
+               return NULL;
+
+       /* Create new county based warning */
+       GritsPoly *poly = fips_combine(counties);
+       _load_common(alert, msg, poly, 0.25, 0.25, 0);
+       grits_viewer_add(alert->viewer, GRITS_OBJECT(poly), GRITS_LEVEL_WORLD+1, FALSE);
+
+       g_list_free(counties);
+       return poly;
+}
+
+/* Update buttons */
+static void _button_click(GtkToggleButton *button, gpointer _alert)
+{
+       g_debug("GritsPluginAlert: _button_click");
+       GritsPluginAlert *alert = _alert;
+       AlertInfo *info = g_object_get_data(G_OBJECT(button), "info");
+       info->hidden = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
+
+       /* Show/hide each polygons */
+       for (GList *cur = alert->msgs; cur; cur = cur->next) {
+               AlertMsg *msg = cur->data;
+               if (msg->info != info)
+                       continue;
+               if (msg->county_based) GRITS_OBJECT(msg->county_based)->hidden = info->hidden;
+               if (msg->storm_based)  GRITS_OBJECT(msg->storm_based)->hidden  = info->hidden;
+       }
+       gtk_widget_queue_draw(GTK_WIDGET(alert->viewer));
+}
+
+static GtkWidget *_button_new(AlertInfo *info)
+{
+       g_debug("GritsPluginAlert: _button_new - %s", info->title);
+       GdkColor black = {0, 0, 0, 0};
+       GdkColor color = {0, info->color[0]<<8, info->color[1]<<8, info->color[2]<<8};
+
+       gchar text[64];
+       g_snprintf(text, sizeof(text), "<b>%.10s</b>", info->abbr);
+
+       GtkWidget *button = gtk_toggle_button_new();
+       GtkWidget *align  = gtk_alignment_new(0.5, 0.5, 1, 1);
+       GtkWidget *cbox   = gtk_event_box_new();
+       GtkWidget *label  = gtk_label_new(text);
+       for (int state = 0; state < GTK_STATE_INSENSITIVE; state++) {
+               gtk_widget_modify_fg(label, state, &black);
+               gtk_widget_modify_bg(cbox,  state, &color);
+       } /* Yuck.. */
+       g_object_set_data(G_OBJECT(button), "info", info);
+       gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
+       gtk_alignment_set_padding(GTK_ALIGNMENT(align), 2, 2, 4, 4);
+       gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), !info->hidden);
+       gtk_widget_set_tooltip_text(GTK_WIDGET(button), info->title);
+       gtk_container_add(GTK_CONTAINER(cbox), label);
+       gtk_container_add(GTK_CONTAINER(align), cbox);
+       gtk_container_add(GTK_CONTAINER(button), align);
+       return button;
+}
+
+static gboolean _update_buttons(GritsPluginAlert *alert)
+{
+       g_debug("GritsPluginAlert: _update_buttons");
+       GtkWidget *alerts  = g_object_get_data(G_OBJECT(alert->config), "alerts");
+       GtkWidget *updated = g_object_get_data(G_OBJECT(alert->config), "updated");
+
+       /* Determine which buttons to show */
+       for (int i = 0; alert_info[i].title; i++)
+               alert_info[i].current = FALSE;
+       for (GList *cur = alert->msgs; cur; cur = cur->next) {
+               AlertMsg *msg = cur->data;
+               msg->info->current = TRUE;
+       }
+
+       /* Delete old buttons */
+       GList *frames = gtk_container_get_children(GTK_CONTAINER(alerts));
+       for (GList *frame = frames; frame; frame = frame->next) {
+               GtkWidget *table = gtk_bin_get_child(GTK_BIN(frame->data));
+               GList *btns = gtk_container_get_children(GTK_CONTAINER(table));
+               g_list_foreach(btns, (GFunc)gtk_widget_destroy, NULL);
+               g_list_free(btns);
+       }
+       g_list_free(frames);
+
+       /* Add new buttons */
+       for (int i = 0; alert_info[i].title; i++) {
+               if (!alert_info[i].current)
+                       continue;
+
+               GtkWidget *table = g_object_get_data(G_OBJECT(alerts),
+                               alert_info[i].category);
+               GList *kids = gtk_container_get_children(GTK_CONTAINER(table));
+               gint nkids = g_list_length(kids);
+               guint rows, cols;
+               gtk_table_get_size(GTK_TABLE(table), &rows, &cols);
+               gint x = nkids % cols;
+               gint y = nkids / cols;
+               g_list_free(kids);
+
+               GtkWidget *button = _button_new(&alert_info[i]);
+               gtk_table_attach(GTK_TABLE(table), button, x, x+1, y, y+1,
+                               GTK_FILL|GTK_EXPAND, GTK_FILL, 0, 0);
+               g_signal_connect(button, "clicked",
+                               G_CALLBACK(_button_click), alert);
+       }
+
+       /* Set time widget */
+       struct tm tm;
+       gmtime_r(&alert->updated, &tm);
+       gchar *date_str = g_strdup_printf(" <b><i>%04d-%02d-%02d %02d:%02d</i></b>",
+                       tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
+                       tm.tm_hour,      tm.tm_min);
+       gtk_label_set_markup(GTK_LABEL(updated), date_str);
+       g_free(date_str);
+
+       gtk_widget_show_all(GTK_WIDGET(alert->config));
+       return FALSE;
+}
+
+static gint _sort_warnings(gconstpointer _a, gconstpointer _b)
+{
+       const AlertMsg *a=_a, *b=_b;
+       return (a->info->prior <  b->info->prior) ? -1 :
+              (a->info->prior == b->info->prior) ?  0 : 1;
+}
+
+static void _update_warnings(GritsPluginAlert *alert, GList *old)
+{
+       g_debug("GritsPluginAlert: _update_warnings");
+
+       /* Sort by priority:
+        *   reversed here since they're added
+        *   to the viewer in reverse order */
+       alert->msgs = g_list_sort(alert->msgs, _sort_warnings);
+
+       /* Remove old messages */
+       for (GList *cur = old; cur; cur = cur->next) {
+               AlertMsg *msg = cur->data;
+               if (msg->county_based) grits_viewer_remove(alert->viewer,
+                               GRITS_OBJECT(msg->county_based));
+               if (msg->storm_based) grits_viewer_remove(alert->viewer,
+                               GRITS_OBJECT(msg->storm_based));
+       }
+
+       /* Add new messages */
+       for (GList *cur = alert->msgs; cur; cur = cur->next) {
+               AlertMsg *msg = cur->data;
+               msg->county_based = _load_county_based(alert, msg);
+               msg->storm_based  = _load_storm_based(alert, msg);
+       }
+
+       g_debug("GritsPluginAlert: _load_warnings - end");
+}
+
+/* Callbacks */
+static void _update(gpointer _, gpointer _alert)
+{
+       GritsPluginAlert *alert = _alert;
+       GList *old = alert->msgs;
+       g_debug("GritsPluginAlert: _update");
+
+       time_t   when    = grits_viewer_get_time(alert->viewer);
+       gboolean offline = grits_viewer_get_offline(alert->viewer);
+       if (!(alert->msgs = msg_load_index(alert->http, when, &alert->updated, offline)))
+               return;
+
+       g_idle_add((GSourceFunc)_update_buttons, alert);
+       _update_warnings(alert, old);
+
+       g_list_foreach(old, (GFunc)msg_free, NULL);
+       g_list_free(old);
+
+       gtk_widget_queue_draw(GTK_WIDGET(alert->viewer));
+       g_debug("GritsPluginAlert: _update - end");
+}
+
+static void _on_update(GritsPluginAlert *alert)
+{
+       g_thread_pool_push(alert->threads, NULL+1, NULL);
+}
+
+/* Init helpers */
+static GtkWidget *_make_config(void)
+{
+       GtkWidget *config = gtk_vbox_new(FALSE, 0);
+
+       /* Setup tools area */
+       GtkWidget *tools   = gtk_hbox_new(FALSE, 10);
+       GtkWidget *updated = gtk_label_new(" Loading...");
+       gtk_label_set_use_markup(GTK_LABEL(updated), TRUE);
+       gtk_box_pack_start(GTK_BOX(tools), updated, FALSE, FALSE, 0);
+       gtk_box_pack_start(GTK_BOX(config), tools, FALSE, FALSE, 0);
+       g_object_set_data(G_OBJECT(config), "updated", updated);
+
+       /* Setup alerts */
+       gchar *labels[] = {"<b>Warnings</b>", "<b>Watches</b>",
+                          "<b>Advisories</b>", "<b>Other</b>"};
+       gchar *keys[]   = {"warning",  "watch",   "advisory",   "other"};
+       gint   cols[]   = {2, 2, 3, 2};
+       GtkWidget *alerts = gtk_hbox_new(FALSE, 10);
+       for (int i = 0; i < G_N_ELEMENTS(labels); i++) {
+               GtkWidget *frame = gtk_frame_new(labels[i]);
+               GtkWidget *table = gtk_table_new(1, cols[i], TRUE);
+               GtkWidget *label = gtk_frame_get_label_widget(GTK_FRAME(frame));
+               gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
+               gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE);
+               gtk_container_add(GTK_CONTAINER(frame), table);
+               gtk_box_pack_start(GTK_BOX(alerts), frame, TRUE, TRUE, 0);
+               g_object_set_data(G_OBJECT(alerts), keys[i], table);
+       }
+       gtk_box_pack_start(GTK_BOX(config), alerts, TRUE, TRUE, 0);
+       g_object_set_data(G_OBJECT(config), "alerts",  alerts);
+
+       return config;
+}
+
+static gboolean _clear_details(GtkWidget *dialog)
+{
+       GtkWidget *content  = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+       GList     *list     = gtk_container_get_children(GTK_CONTAINER(content));
+       GtkWidget *notebook = list->data;
+       g_list_free(list);
+       gtk_widget_hide(dialog);
+       while (gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook)))
+               gtk_notebook_remove_page(GTK_NOTEBOOK(notebook), 0);
+       return TRUE;
+}
+
+static gboolean _set_details_uri(GtkWidget *notebook, GtkNotebookPage *_,
+               guint num, GtkWidget *button)
+{
+       g_debug("_set_details_uri");
+       GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), num);
+       AlertMsg  *msg  = g_object_get_data(G_OBJECT(page), "msg");
+       gtk_link_button_set_uri(GTK_LINK_BUTTON(button), msg->link);
+       return FALSE;
+}
+
+static GtkWidget *_make_details(GritsViewer *viewer)
+{
+       GtkWidget *dialog   = gtk_dialog_new();
+       GtkWidget *action   = gtk_dialog_get_action_area (GTK_DIALOG(dialog));
+       GtkWidget *content  = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+       GtkWidget *notebook = gtk_notebook_new();
+       GtkWidget *win      = gtk_widget_get_toplevel(GTK_WIDGET(viewer));
+       GtkWidget *link     = gtk_link_button_new_with_label("", "Full Text");
+
+       gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(win));
+       gtk_window_set_title(GTK_WINDOW(dialog), "Alert Details - AWeather");
+       gtk_window_set_default_size(GTK_WINDOW(dialog), 625, 500);
+       gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), TRUE);
+       gtk_container_add(GTK_CONTAINER(content), notebook);
+       gtk_box_pack_end(GTK_BOX(action), link, 0, 0, 0);
+       gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE);
+
+       g_signal_connect(dialog,   "response",     G_CALLBACK(_clear_details),   NULL);
+       g_signal_connect(dialog,   "delete-event", G_CALLBACK(_clear_details),   NULL);
+       g_signal_connect(notebook, "switch-page",  G_CALLBACK(_set_details_uri), link);
+
+       return dialog;
+}
+
+/* Methods */
+GritsPluginAlert *grits_plugin_alert_new(GritsViewer *viewer, GritsPrefs *prefs)
+{
+       g_debug("GritsPluginAlert: new");
+       GritsPluginAlert *alert = g_object_new(GRITS_TYPE_PLUGIN_ALERT, NULL);
+       alert->details = _make_details(viewer);
+       alert->viewer  = g_object_ref(viewer);
+       alert->prefs   = g_object_ref(prefs);
+
+       alert->refresh_id      = g_signal_connect_swapped(alert->viewer, "refresh",
+                       G_CALLBACK(_on_update), alert);
+       alert->time_changed_id = g_signal_connect_swapped(alert->viewer, "time_changed",
+                       G_CALLBACK(_on_update), alert);
+
+       for (GList *cur = alert->states; cur; cur = cur->next)
+               grits_viewer_add(viewer, cur->data, GRITS_LEVEL_WORLD+1, FALSE);
+
+       _on_update(alert);
+       return alert;
+}
+
+static GtkWidget *grits_plugin_alert_get_config(GritsPlugin *_alert)
+{
+       GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(_alert);
+       return alert->config;
+}
+
+
+/* GObject code */
+static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface);
+G_DEFINE_TYPE_WITH_CODE(GritsPluginAlert, grits_plugin_alert, G_TYPE_OBJECT,
+               G_IMPLEMENT_INTERFACE(GRITS_TYPE_PLUGIN,
+                       grits_plugin_alert_plugin_init));
+static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface)
+{
+       g_debug("GritsPluginAlert: plugin_init");
+       /* Add methods to the interface */
+       iface->get_config = grits_plugin_alert_get_config;
+}
+static void grits_plugin_alert_init(GritsPluginAlert *alert)
+{
+       g_debug("GritsPluginAlert: init");
+       /* Set defaults */
+       alert->threads = g_thread_pool_new(_update, alert, 1, TRUE, NULL);
+       alert->config  = _make_config();
+       alert->http    = grits_http_new(G_DIR_SEPARATOR_S
+                       "alerts" G_DIR_SEPARATOR_S
+                       "cap"    G_DIR_SEPARATOR_S);
+
+       /* Load counties */
+       gchar *text; gsize len;
+       const gchar *file = PKGDATADIR G_DIR_SEPARATOR_S "fips.txt";
+       if (!g_file_get_contents(file, &text, &len, NULL))
+               g_error("GritsPluginAlert: init - error loading fips polygons");
+       fips_parse(text, &alert->counties, &alert->states);
+       g_free(text);
+}
+static void grits_plugin_alert_dispose(GObject *gobject)
+{
+       g_debug("GritsPluginAlert: dispose");
+       GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
+       /* Drop references */
+       if (alert->viewer) {
+               GritsViewer *viewer = alert->viewer;
+               alert->viewer       = NULL;
+               g_signal_handler_disconnect(viewer, alert->refresh_id);
+               g_signal_handler_disconnect(viewer, alert->time_changed_id);
+               soup_session_abort(alert->http->soup);
+               g_thread_pool_free(alert->threads, TRUE, TRUE);
+               gtk_widget_destroy(alert->details);
+               while (gtk_events_pending())
+                       gtk_main_iteration();
+               for (GList *cur = alert->msgs; cur; cur = cur->next) {
+                       AlertMsg *msg = cur->data;
+                       if (msg->county_based) grits_viewer_remove(viewer,
+                                       GRITS_OBJECT(msg->county_based));
+                       if (msg->storm_based) grits_viewer_remove(viewer,
+                                       GRITS_OBJECT(msg->storm_based));
+               }
+               for (GList *cur = alert->states; cur; cur = cur->next)
+                       grits_viewer_remove(viewer, cur->data);
+               g_object_unref(alert->prefs);
+               g_object_unref(viewer);
+       }
+       G_OBJECT_CLASS(grits_plugin_alert_parent_class)->dispose(gobject);
+}
+static gboolean _unref_county(gpointer key, gpointer val, gpointer data)
+{
+       g_object_unref(val);
+       return FALSE;
+}
+static void grits_plugin_alert_finalize(GObject *gobject)
+{
+       g_debug("GritsPluginAlert: finalize");
+       GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
+       g_list_foreach(alert->msgs, (GFunc)msg_free, NULL);
+       g_list_free(alert->msgs);
+       g_list_free(alert->states);
+       g_tree_foreach(alert->counties, (GTraverseFunc)_unref_county, NULL);
+       g_tree_destroy(alert->counties);
+       grits_http_free(alert->http);
+       G_OBJECT_CLASS(grits_plugin_alert_parent_class)->finalize(gobject);
+}
+static void grits_plugin_alert_class_init(GritsPluginAlertClass *klass)
+{
+       g_debug("GritsPluginAlert: class_init");
+       GObjectClass *gobject_class = (GObjectClass*)klass;
+       gobject_class->dispose  = grits_plugin_alert_dispose;
+       gobject_class->finalize = grits_plugin_alert_finalize;
+}