]> Pileus Git - aweather/blob - src/plugins/alert.c
Add warning/watch/alert plugin
[aweather] / src / plugins / alert.c
1 /*
2  * Copyright (C) 2010 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 <grits.h>
20 #include <GL/gl.h>
21 #include <stdio.h>
22 #include <string.h>
23
24 #include "alert.h"
25 #include "alert-info.h"
26
27 /**********
28  * Alerts *
29  **********/
30 /* Data types */
31 typedef struct {
32         char sep0;
33         char class [1 ]; char sep1;
34         char action[3 ]; char sep2;
35         char office[4 ]; char sep3;
36         char phenom[2 ]; char sep4;
37         char signif[1 ]; char sep5;
38         char event [4 ]; char sep6;
39         char begin [12]; char sep7;
40         char end   [12]; char sep8;
41 } AWeatherVtec;
42
43 typedef struct {
44         char *title;   // Winter Weather Advisory issued December 19 at 8:51PM
45         char *link;    // http://www.weather.gov/alerts-beta/wwacapget.php?x=AK20101219205100AFGWinterWeatherAdvisoryAFG20101220030000AK
46         char *summary; // ...WINTER WEATHER ADVISORY REMAINS IN EFFECT UNTIL 6
47         struct {
48                 time_t  effective;  // 2010-12-19T20:51:00-09:00
49                 time_t  expires;    // 2010-12-20T03:00:00-09:00
50                 char   *status;     // Actual
51                 char   *urgency;    // Expected
52                 char   *severity;   // Minor
53                 char   *certainty;  // Likely
54                 char   *area_desc;  // Northeastern Brooks Range; Northwestern Brooks Range
55                 char   *fips6;      // 006015 006023 006045 006105
56                 AWeatherVtec *vtec; // /X.CON.PAFG.WW.Y.0064.000000T0000Z-101220T0300Z/
57         } cap;
58 } AWeatherAlert;
59
60 /* Alert parsing */
61 typedef struct {
62         AWeatherAlert *alert;
63         GList         *alerts;
64         gchar         *text;
65         gchar         *value_name;
66 } ParseData;
67 time_t parse_time(gchar *iso8601)
68 {
69         GTimeVal tv = {};
70         g_time_val_from_iso8601(iso8601, &tv);
71         return tv.tv_sec;
72 }
73 AWeatherVtec *parse_vtec(char *buf)
74 {
75         AWeatherVtec *vtec = g_new0(AWeatherVtec, 1);
76         strncpy((char*)vtec, buf, sizeof(AWeatherVtec));
77         vtec->sep0 = vtec->sep1 = vtec->sep2 = '\0';
78         vtec->sep3 = vtec->sep4 = vtec->sep5 = '\0';
79         vtec->sep6 = vtec->sep7 = vtec->sep8 = '\0';
80         return vtec;
81 }
82 void alert_start(GMarkupParseContext *context, const gchar *name,
83                 const gchar **keys, const gchar **vals,
84                 gpointer user_data, GError **error)
85 {
86         //g_debug("start %s", name);
87         ParseData *data = user_data;
88         if (g_str_equal(name, "entry"))
89                 data->alert  = g_new0(AWeatherAlert, 1);
90 }
91 void alert_end(GMarkupParseContext *context, const gchar *name,
92                 gpointer user_data, GError **error)
93 {
94         //g_debug("end %s", name);
95         ParseData     *data  = user_data;
96         AWeatherAlert *alert = data->alert;
97         char          *text  = data->text;
98
99         if (g_str_equal(name, "entry"))
100                 data->alerts = g_list_prepend(data->alerts, data->alert);
101
102         if (!text || !alert) return;
103         if      (g_str_equal(name, "title"))         alert->title         = g_strdup(text);
104         else if (g_str_equal(name, "id"))            alert->link          = g_strdup(text); // hack
105         else if (g_str_equal(name, "summary"))       alert->summary       = g_strdup(text);
106         else if (g_str_equal(name, "cap:effective")) alert->cap.effective = parse_time(text);
107         else if (g_str_equal(name, "cap:expires"))   alert->cap.expires   = parse_time(text);
108         else if (g_str_equal(name, "cap:status"))    alert->cap.status    = g_strdup(text);
109         else if (g_str_equal(name, "cap:urgency"))   alert->cap.urgency   = g_strdup(text);
110         else if (g_str_equal(name, "cap:severity"))  alert->cap.severity  = g_strdup(text);
111         else if (g_str_equal(name, "cap:certainty")) alert->cap.certainty = g_strdup(text);
112         else if (g_str_equal(name, "cap:areaDesc"))  alert->cap.area_desc = g_strdup(text);
113
114         if (g_str_equal(name, "valueName")) {
115                 if (data->value_name)
116                         g_free(data->value_name);
117                 data->value_name = g_strdup(text);
118         }
119
120         if (g_str_equal(name, "value") && data->value_name) {
121                 if (g_str_equal(data->value_name, "FIPS6")) alert->cap.fips6 = g_strdup(text);
122                 if (g_str_equal(data->value_name, "VTEC"))  alert->cap.vtec  = parse_vtec(text);
123         }
124 }
125 void alert_text(GMarkupParseContext *context, const gchar *text,
126                 gsize len, gpointer user_data, GError **error)
127 {
128         //g_debug("text %s", text);
129         ParseData *data = user_data;
130         if (data->text)
131                 g_free(data->text);
132         data->text = strndup(text, len);
133 }
134
135 /* Alert methods */
136 GList *alert_parse(gchar *text, gsize len)
137 {
138         g_debug("GritsPluginAlert: alert_parse");
139         GMarkupParser parser = {
140                 .start_element = alert_start,
141                 .end_element   = alert_end,
142                 .text          = alert_text,
143         };
144         ParseData data = {};
145         GMarkupParseContext *context =
146                 g_markup_parse_context_new(&parser, 0, &data, NULL);
147         g_markup_parse_context_parse(context, text, len, NULL);
148         g_markup_parse_context_free(context);
149         if (data.text)
150                 g_free(data.text);
151         if (data.value_name)
152                 g_free(data.value_name);
153         return data.alerts;
154 }
155
156 void alert_free(AWeatherAlert *alert)
157 {
158         g_free(alert->title);
159         g_free(alert->link);
160         g_free(alert->summary);
161         g_free(alert->cap.status);
162         g_free(alert->cap.urgency);
163         g_free(alert->cap.severity);
164         g_free(alert->cap.certainty);
165         g_free(alert->cap.area_desc);
166         g_free(alert->cap.fips6);
167         g_free(alert->cap.vtec);
168         g_free(alert);
169 }
170
171 void alert_test(char *file)
172 {
173         gchar *text; gsize len;
174         g_file_get_contents(file, &text, &len, NULL);
175         GList *alerts = alert_parse(text, len);
176         g_free(text);
177         for (GList *cur = alerts; cur; cur = cur->next) {
178                 AWeatherAlert *alert = cur->data;
179                 g_message("alert:");
180                 g_message("     title         = %s",  alert->title        );
181                 g_message("     link          = %s",  alert->link         );
182                 g_message("     summary       = %s",  alert->summary      );
183                 g_message("     cat.effective = %lu", alert->cap.effective);
184                 g_message("     cat.expires   = %lu", alert->cap.expires  );
185                 g_message("     cat.status    = %s",  alert->cap.status   );
186                 g_message("     cat.urgency   = %s",  alert->cap.urgency  );
187                 g_message("     cat.severity  = %s",  alert->cap.severity );
188                 g_message("     cat.certainty = %s",  alert->cap.certainty);
189                 g_message("     cat.area_desc = %s",  alert->cap.area_desc);
190                 g_message("     cat.fips6     = %s",  alert->cap.fips6    );
191                 g_message("     cat.vtec      = %p",  alert->cap.vtec     );
192         }
193         g_list_foreach(alerts, (GFunc)alert_free, NULL);
194         g_list_free(alerts);
195 }
196
197
198 /********
199  * FIPS *
200  ********/
201 int int_compare(int a, int b)
202 {
203         return (a <  b) ? -1 :
204                (a == b) ?  0 : 1;
205 }
206
207 gdouble timed(void)
208 {
209         GTimeVal tv;
210         g_get_current_time(&tv);
211         return (gdouble)tv.tv_sec + (gdouble)tv.tv_usec/G_USEC_PER_SEC;
212 }
213
214 gdouble fips_area(gdouble *a, gdouble *b, gdouble *c)
215 {
216         gdouble cross[3];
217         crossd3(a, b, c, cross);
218         return  lengthd(cross)/2;
219 }
220
221 gdouble fips_remove_one(GList **poly, gdouble thresh)
222 {
223         gdouble min_area = thresh;
224         GList  *min_list = NULL;
225         if (g_list_length(*poly) <= 4)
226                 return 0;
227         for (GList *prev = *poly; prev->next->next; prev = prev->next) {
228                 GList *cur  = prev->next;
229                 GList *next = prev->next->next;
230                 gdouble area = fips_area(prev->data, cur->data, next->data);
231                 if (area < min_area) {
232                         min_area = area;
233                         min_list = cur;
234                 }
235         }
236         if (min_list) {
237                 *poly = g_list_delete_link(*poly, min_list);
238                 return min_area;
239         } else {
240                 return 0;
241         }
242 }
243
244 gpointer fips_simplify(gdouble (*coords)[3], gdouble thresh, gint key)
245 {
246         GList *poly = NULL;
247
248         /* Add points to poly list */
249         for (int i = 0; coords[i][0]; i++)
250                 poly = g_list_prepend(poly, coords[i]);
251         int before = g_list_length(poly);
252
253         /* Simplify poly list */
254         //fprintf(stderr, "GritsPluginAlert: remove_one ");
255         gdouble area;
256         while ((area = fips_remove_one(&poly, thresh)) > 0) {
257                 //fprintf(stderr, " %f", area);
258         }
259         //fprintf(stderr, "\n");
260         int after = g_list_length(poly);
261
262         /* Copy points back */
263         gdouble (*simp)[3] = (gpointer)g_new0(gdouble, 3*(after+1));
264         GList *cur = poly;
265         for (int i = 0; i < after; i++) {
266                 gdouble *coord = cur->data;
267                 simp[i][0] = coord[0];
268                 simp[i][1] = coord[1];
269                 simp[i][2] = coord[2];
270                 cur = cur->next;
271         }
272
273         (void)before;
274         (void)after;
275         g_debug("GritsPluginAlert: fips_simplify %d: %d -> %d",
276                         key, before, after);
277         g_list_free(poly);
278         g_free(coords);
279         return simp;
280 }
281
282 void fips_print(FILE *fd, gchar *fips, gchar *name, gchar *state,
283                 gdouble (**points)[3])
284 {
285         fprintf(fd, "%s\t%s\t%s", fips, name, state);
286         for (int i = 0; points[i]; i++) {
287                 fprintf(fd, "\t");
288                 for (int j = 0; points[i][j][0]; j++) {
289                         //fwrite(points[i][j], sizeof(double), 3, fd);
290                         gdouble lat, lon;
291                         xyz2ll(points[i][j][0], points[i][j][1],
292                                         points[i][j][2], &lat, &lon);
293                         fprintf(fd, "%.7lf,%.7lf ", lat, lon);
294                 }
295         }
296         fprintf(fd, "\n");
297 }
298
299 GTree *fips_parse(gchar *text)
300 {
301         g_debug("GritsPluginAlert: fips_parse");
302         //FILE *fd = fopen("/tmp/fips_polys_simp.txt", "w+");
303
304         GTree *tree = g_tree_new((GCompareFunc)int_compare);
305         gchar **lines = g_strsplit(text, "\n", -1);
306         for (gint li = 0; lines[li]; li++) {
307                 /* Split and count parts */
308                 gchar **sparts = g_strsplit(lines[li], "\t", -1);
309                 int     nparts = g_strv_length(sparts);
310                 if (nparts < 4) {
311                         g_strfreev(sparts);
312                         continue;
313                 }
314
315                 GritsPoint center = {0,0,0};
316                 gdouble (**polys)[3] = (gpointer)g_new0(double*, nparts-3+1);
317                 for (int pi = 3; pi < nparts; pi++) {
318                         /* Split and count coordinates */
319                         gchar **scoords = g_strsplit(sparts[pi], " ", -1);
320                         int     ncoords = g_strv_length(scoords);
321
322                         /* Create binary coords */
323                         gdouble (*coords)[3] = (gpointer)g_new0(gdouble, 3*(ncoords+1));
324                         for (int ci = 0; ci < ncoords; ci++) {
325                                 gdouble lat, lon;
326                                 //sscanf(scoords[ci], "%lf,%lf", &lon, &lat);
327                                 sscanf(scoords[ci], "%lf,%lf", &lat, &lon);
328                                 if (ci == 0) {
329                                         center.lat  = lat;
330                                         center.lon  = lon;
331                                         center.elev = 0;
332                                 }
333                                 lle2xyz(lat, lon, 0,
334                                         &coords[ci][0],
335                                         &coords[ci][1],
336                                         &coords[ci][2]);
337                         }
338
339                         /* Simplify coords/contour */
340                         //coords = fips_simplify(coords, 1000*1000*4, li);
341
342                         /* Insert coords into poly array */
343                         polys[pi-3] = coords;
344                         g_strfreev(scoords);
345                 }
346
347                 /* print simplified polys */
348                 //fips_print(fd, sparts[0], sparts[1], sparts[2], polys);
349
350                 /* Create GritsPoly */
351                 GritsPoly *poly = grits_poly_new(polys);
352                 GRITS_OBJECT(poly)->center  = center;
353                 GRITS_OBJECT(poly)->skip   |= GRITS_SKIP_CENTER;
354                 GRITS_OBJECT(poly)->skip   |= GRITS_SKIP_STATE;
355
356                 /* Insert polys into the tree */
357                 gint id = g_ascii_strtoll(sparts[0], NULL, 10);
358                 g_tree_insert(tree, (gpointer)id, poly);
359                 g_strfreev(sparts);
360         }
361         g_strfreev(lines);
362
363         //fclose(fd);
364         return tree;
365 }
366
367 /********************
368  * GritsPluginAlert *
369  ********************/
370 /* Setup helpers
371  * init:
372  *   parse counties
373  *   init county polys
374  *   init config area
375  * refresh:
376  *   parse alert
377  *   update buttons
378  *   update counties
379  * clicked:
380  *   update counties
381  */
382
383 /* Update counties */
384 static gboolean _hide_county(gchar *_, GritsObject *county)
385 {
386         g_object_set_data(G_OBJECT(county), "info", NULL);
387         //county->hidden = TRUE;
388         return FALSE;
389 }
390 static void _update_counties(GritsPluginAlert *alert)
391 {
392         /* Setup counties based on alerts:
393          * foreach alert in alerts:
394          *      info   = infotable.get(alert.type)
395          *      foreach fips  in alert.fipses:
396          *              county = counties.get(fips)
397          *              if info is higher priority
398          *                      county.color = info.color
399          */
400         g_debug("GritsPluginAlert: _update_counties");
401         g_tree_foreach(alert->counties, (GTraverseFunc)_hide_county, NULL);
402         for (GList *cur = alert->alerts; cur; cur = cur->next) {
403                 AWeatherAlert *msg = cur->data;
404
405                 /* Find alert info */
406                 AlertInfo *info = alert_info_find(msg->title);
407                 if (!info) {
408                         g_warning("GritsPluginAlert: unknown warning - %s", msg->title);
409                         continue;
410                 }
411                 if (!info->enabled)
412                         continue;
413
414                 /* Set color for each county in alert */
415                 gchar **fipses = g_strsplit(msg->cap.fips6, " ", -1);
416                 for (int i = 0; fipses[i]; i++) {
417                         gint fips = g_ascii_strtoll(fipses[i], NULL, 10);
418                         GritsPoly *county = g_tree_lookup(alert->counties, (gpointer)fips);
419                         if (!county)
420                                 continue;
421                         AlertInfo *old = g_object_get_data(G_OBJECT(county), "info");
422                         if (old != NULL && old < info)
423                                 continue;
424                         g_object_set_data(G_OBJECT(county), "info", info);
425                         county->color[0] = (float)info->color[0] / 256;
426                         county->color[1] = (float)info->color[1] / 256;
427                         county->color[2] = (float)info->color[2] / 256;
428                         county->color[3] = 0.25;
429                         GRITS_OBJECT(county)->hidden = FALSE;
430                 }
431                 g_strfreev(fipses);
432         }
433         gtk_widget_queue_draw(GTK_WIDGET(alert->viewer));
434 }
435
436 /* Update buttons */
437 static void _alert_click(GtkRadioButton *button, gpointer alert)
438 {
439         g_debug("GritsPluginAlert: _alert_click");
440         AlertInfo *info = g_object_get_data(G_OBJECT(button), "info");
441         info->enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
442         _update_counties(alert);
443 }
444
445 static GtkWidget *_make_button(AlertInfo *info)
446 {
447         g_debug("GritsPluginAlert: _make_button - %s", info->title);
448         GdkColor black = {0, 0, 0, 0};
449         GdkColor color = {0, info->color[0]<<8, info->color[1]<<8, info->color[2]<<8};
450
451         gchar text[6+7];
452         g_snprintf(text, sizeof(text), "<b>%.5s</b>", info->title);
453
454         GtkWidget *button = gtk_toggle_button_new();
455         GtkWidget *align  = gtk_alignment_new(0.5, 0.5, 1, 1);
456         GtkWidget *cbox   = gtk_event_box_new();
457         GtkWidget *label  = gtk_label_new(text);
458         for (int state = 0; state < GTK_STATE_INSENSITIVE; state++) {
459                 gtk_widget_modify_fg(label, state, &black);
460                 gtk_widget_modify_bg(cbox,  state, &color);
461         } /* Yuck.. */
462         g_object_set_data(G_OBJECT(button), "info", info);
463         gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
464         gtk_alignment_set_padding(GTK_ALIGNMENT(align), 2, 2, 4, 4);
465         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), info->enabled);
466         gtk_widget_set_tooltip_text(GTK_WIDGET(button), info->title);
467         gtk_container_add(GTK_CONTAINER(cbox), label);
468         gtk_container_add(GTK_CONTAINER(align), cbox);
469         gtk_container_add(GTK_CONTAINER(button), align);
470         return button;
471 }
472
473 static void _update_buttons(GritsPluginAlert *alert)
474 {
475         g_debug("GritsPluginAlert: _update_buttons");
476         /* Delete old buttons */
477         GList *frames = gtk_container_get_children(GTK_CONTAINER(alert->config));
478         for (GList *frame = frames; frame; frame = frame->next) {
479                 GtkWidget *table = gtk_bin_get_child(GTK_BIN(frame->data));
480                 GList *btns = gtk_container_get_children(GTK_CONTAINER(table));
481                 g_list_foreach(btns, (GFunc)gtk_widget_destroy, NULL);
482         }
483
484         /* Add new buttons */
485         for (int i = 0; alert_info[i].title; i++) {
486                 if (!alert_info[i].current)
487                         continue;
488
489                 GtkWidget *table = g_object_get_data(G_OBJECT(alert->config),
490                                 alert_info[i].category);
491                 GList *kids = gtk_container_get_children(GTK_CONTAINER(table));
492                 int nkids = g_list_length(kids);
493                 int x = nkids % 3;
494                 int y = nkids / 3;
495                 g_list_free(kids);
496
497                 GtkWidget *button = _make_button(&alert_info[i]);
498                 gtk_table_attach(GTK_TABLE(table), button, x, x+1, y, y+1,
499                                 GTK_FILL|GTK_EXPAND, GTK_FILL, 0, 0);
500                 g_signal_connect(button, "clicked",
501                                 G_CALLBACK(_alert_click), alert);
502         }
503 }
504
505 /* Init helpers */
506 static gboolean _add_county(gint key, GritsObject *poly, GritsViewer *viewer)
507 {
508         grits_viewer_add(viewer, poly, GRITS_LEVEL_WORLD+1, TRUE);
509         return FALSE;
510 }
511
512 static GtkWidget *_make_config(void)
513 {
514         gchar *labels[] = {"Warnings", "Watches", "Advisories", "Other"};
515         gchar *keys[]   = {"warning",  "watch",   "advisory",   "other"};
516         gint   cols[]   = {3, 2, 2, 2};
517         GtkWidget *config = gtk_hbox_new(FALSE, 10);
518         for (int i = 0; i < G_N_ELEMENTS(labels); i++) {
519                 GtkWidget *frame = gtk_frame_new(labels[i]);
520                 GtkWidget *table = gtk_table_new(1, cols[i], TRUE);
521                 gtk_container_add(GTK_CONTAINER(frame), table);
522                 gtk_box_pack_start(GTK_BOX(config), frame, TRUE, TRUE, 0);
523                 g_object_set_data(G_OBJECT(config), keys[i], table);
524         }
525         return config;
526 }
527
528 /* Methods */
529 GritsPluginAlert *grits_plugin_alert_new(GritsViewer *viewer, GritsPrefs *prefs)
530 {
531         g_debug("GritsPluginAlert: new");
532         GritsPluginAlert *alert = g_object_new(GRITS_TYPE_PLUGIN_ALERT, NULL);
533         g_tree_foreach(alert->counties, (GTraverseFunc)_add_county, viewer);
534         alert->viewer = viewer;
535         alert->prefs  = prefs;
536         return alert;
537 }
538
539 static GtkWidget *grits_plugin_alert_get_config(GritsPlugin *_alert)
540 {
541         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(_alert);
542         return alert->config;
543 }
544
545
546 /* GObject code */
547 static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface);
548 G_DEFINE_TYPE_WITH_CODE(GritsPluginAlert, grits_plugin_alert, G_TYPE_OBJECT,
549                 G_IMPLEMENT_INTERFACE(GRITS_TYPE_PLUGIN,
550                         grits_plugin_alert_plugin_init));
551 static void grits_plugin_alert_plugin_init(GritsPluginInterface *iface)
552 {
553         g_debug("GritsPluginAlert: plugin_init");
554         /* Add methods to the interface */
555         iface->get_config = grits_plugin_alert_get_config;
556 }
557 static void grits_plugin_alert_init(GritsPluginAlert *alert)
558 {
559         g_debug("GritsPluginAlert: class_init");
560         /* Set defaults */
561         alert->config   = _make_config();
562
563         /* Load counties */
564         gchar *text; gsize len;
565         const gchar *fips_file = "/scratch/aweather/data/fips_polys_simp.txt";
566         g_file_get_contents(fips_file, &text, &len, NULL);
567         alert->counties = fips_parse(text);
568         g_free(text);
569
570         /* Load alerts (once for testing) */
571         const gchar *alert_file = "/scratch/aweather/src/plugins/alert4.xml";
572         g_file_get_contents(alert_file, &text, &len, NULL);
573         alert->alerts = alert_parse(text, len);
574         g_free(text);
575
576         /* For now, set alert "current" at init */
577         for (GList *cur = alert->alerts; cur; cur = cur->next) {
578                 AWeatherAlert *msg  = cur->data;
579                 AlertInfo     *info = alert_info_find(msg->title);
580                 if (info) info->current = TRUE;
581         }
582
583         /* For now, run updates */
584         _update_buttons(alert);
585         _update_counties(alert);
586 }
587 static void grits_plugin_alert_dispose(GObject *gobject)
588 {
589         g_debug("GritsPluginAlert: dispose");
590         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
591         (void)alert; // TODO
592         /* Drop references */
593         G_OBJECT_CLASS(grits_plugin_alert_parent_class)->dispose(gobject);
594 }
595 static void grits_plugin_alert_finalize(GObject *gobject)
596 {
597         g_debug("GritsPluginAlert: finalize");
598         GritsPluginAlert *alert = GRITS_PLUGIN_ALERT(gobject);
599         /* Free data */
600         gtk_widget_destroy(alert->config);
601
602         /* Free countes */
603         g_tree_destroy(alert->counties);
604
605         /* Free alerts (once for testing) */
606         g_list_foreach(alert->alerts, (GFunc)alert_free, NULL);
607         g_list_free(alert->alerts);
608
609         G_OBJECT_CLASS(grits_plugin_alert_parent_class)->finalize(gobject);
610 }
611 static void grits_plugin_alert_class_init(GritsPluginAlertClass *klass)
612 {
613         g_debug("GritsPluginAlert: class_init");
614         GObjectClass *gobject_class = (GObjectClass*)klass;
615         gobject_class->dispose  = grits_plugin_alert_dispose;
616         gobject_class->finalize = grits_plugin_alert_finalize;
617 }