]> Pileus Git - aweather/commitdiff
Add warning/watch/alert plugin
authorAndy Spencer <andy753421@gmail.com>
Mon, 31 Oct 2011 06:59:22 +0000 (06:59 +0000)
committerAndy Spencer <andy753421@gmail.com>
Wed, 2 Nov 2011 03:37:03 +0000 (03:37 +0000)
Alert information is shown similar to what is shown on the main
weather.gov page. Alerts are retrieved in real time using the NWS CAP
feed [1] which includes most, but not all, of the information from
weather.gov. More details about the CAP feed can be found on the NWS CAP
Wiki [2].

Both county based warnings and polygon based warnings are support. In
addition, when zoomed in close enough county outlines will be drawn for
each state.

Additional information can be retrieved about a particular alert by
clicking on the highlighted area in the viewer.

Alerts can be hidden by toggling the alert type in the alert plugin's
configuration tab.

If you are developing from Git, you will need to download a FIPS county
polygons file [2]. This should be done automatically on the first build.
If it is not successful, the polygons are also included in the >= 0.6
tarballs.

There are currently several drawbacks to this plugin:

  - NWS warning zones are not use, only FIPS counties are used.

  - The county polygons have been simplified for performance reasons.

  - The NWS CAP feed does not include all the text products showed on
    the main weather.gov page. For example, hazardous weather outlooks
    are not included. This may be improved slightly by the NWS in future
    revision of the CAP feed.

  - Historic data is not available but previously downloaded alerts are
    cached and can be viewed.

[1] http://alerts.weather.gov/cap/us.php?x=0
[2] http://wiki.citizen.apps.gov/nws_developers/
[3] http://lug.rose-hulman.edu/proj/aweather/files/fips/

data/.gitignore
data/Makefile.am
data/defaults.ini
src/plugins/Makefile.am
src/plugins/alert-info.c [new file with mode: 0644]
src/plugins/alert-info.h [new file with mode: 0644]
src/plugins/alert.c [new file with mode: 0644]
src/plugins/alert.h [new file with mode: 0644]
src/plugins/level2.c
src/plugins/radar.c

index 27de9f1013300d21dcb2b220ae87302d374d845e..c915745771805f14db4f601f8937908ff75033da 100644 (file)
@@ -1,3 +1,4 @@
 *.ico
 aweather
 main.ui
+fips.txt
index c782b17d115e7f893bd51a3bf5ff0f6231bd1d83..3d3d6fa6bd70da866cbfd07d6a81016e97d1cd7f 100644 (file)
@@ -4,6 +4,9 @@ dist_gtkbuilder_DATA = main.ui
 configdir = $(datadir)/aweather/
 dist_config_DATA = defaults.ini
 
+fipsdir = $(datadir)/aweather/
+dist_fips_DATA = fips.txt
+
 logodir = $(datadir)/aweather/
 dist_logo_DATA = logo.svg
 
@@ -28,6 +31,9 @@ dist_iconSC_DATA = icons/scalable/aweather.svg
 
 noinst_DATA = icons/48x48/aweather.ico
 
+fips.txt:
+       wget http://lug.rose-hulman.edu/proj/aweather/files/fips/fips.txt
+
 install-data-hook:
        if test -z "$(DESTDIR)"; then \
                gtk-update-icon-cache -f -t $(datadir)/icons/hicolor; \
index aeafbe62cfc6a2c6751f0090a7a49893e4d7e47e..18e1d67a998a573bda41649ec08229e7a747c5ae 100644 (file)
@@ -14,3 +14,4 @@ map=true
 env=true
 elev=false
 radar=true
+alert=true
index 502bb260fa7471b5a389bf8a04f28a9c32f85d4b..55166a05e72d011c627a2d4eec59f7ac4f7a4721 100644 (file)
@@ -5,7 +5,16 @@ LIBS        = $(GRITS_LIBS)
 
 pluginsdir  = $(pkglibdir)
 
-plugins_LTLIBRARIES =
+plugins_LTLIBRARIES = alert.la
+
+alert_la_SOURCES = \
+       alert.c      alert.h \
+       alert-info.c alert-info.h
+alert_la_CPPFLAGS = \
+       -DPKGDATADIR="\"$(dots)$(pkgdatadir)\"" \
+       -I$(top_srcdir)/lib
+alert_la_LIBADD  = $(GRITS_LIBS) \
+                  $(addprefix $(top_srcdir)/lib/,$(gl_LTLIBOBJS))
 
 if HAVE_RSL
 plugins_LTLIBRARIES += radar.la
diff --git a/src/plugins/alert-info.c b/src/plugins/alert-info.c
new file mode 100644 (file)
index 0000000..952595d
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * 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/>.
+ */
+
+/* Taken from: http://www.weather.gov/wwamap-prd/faq.php */
+
+#include "alert-info.h"
+
+AlertInfo alert_info[] = {
+       /* Warnings */
+       //                                                                  hide  poly
+       // title                            category    abbr          prio  | cur |  color
+       {"Ashfall Warning"                , "warning",  "Ashfall",    72  , 0, 0, 0, {169, 169, 169}},  // darkgray
+       {"Avalanche Warning"              , "warning",  "Avalanche",  21  , 0, 0, 0, {30 , 144, 255}},  // dodgerblue
+       {"Blizzard Warning"               , "warning",  "Blizzard",   12  , 0, 0, 0, {255, 69 , 0  }},  // orangered
+       {"Civil Danger Warning"           , "warning",  "Cvl Danger", 7   , 0, 0, 0, {255, 182, 193}},  // lightpink
+       {"Coastal Flood Warning"          , "warning",  "C Flood",    24  , 0, 0, 0, {34 , 139, 34 }},  // forestgreen
+       {"Dust Storm Warning"             , "warning",  "Dust Storm", 32  , 0, 0, 0, {255, 228, 196}},  // bisque
+       {"Earthquake Warning"             , "warning",  "Earthquake", 22  , 0, 0, 0, {139, 69 , 19 }},  // saddlebrown
+       {"Excessive Heat Warning"         , "warning",  "Heat",       31  , 0, 0, 0, {199, 21 , 133}},  // mediumvioletred
+       {"Extreme Cold Warning"           , "warning",  "Cold",       44  , 0, 0, 0, {0  , 0  , 255}},  // blue
+       {"Extreme Wind Warning"           , "warning",  "Ext Wind",   3   , 0, 0, 1, {255, 20 , 147}},  // deeppink
+       {"Fire Warning"                   , "warning",  "Fire",       7   , 0, 0, 0, {160, 82 , 45 }},  // sienna
+       {"Flash Flood Warning"            , "warning",  "F Flood",    5   , 0, 0, 1, {139, 0  , 0  }},  // darkred
+       {"Flood Warning"                  , "warning",  "Flood",      5   , 0, 0, 1, {0  , 255, 0  }},  // lime
+       {"Freeze Warning"                 , "warning",  "Freeze",     45  , 0, 0, 0, {0  , 255, 255}},  // cyan
+       {"Gale Warning"                   , "warning",  "Gale",       40  , 0, 0, 0, {221, 160, 221}},  // plum
+       {"Hard Freeze Warning"            , "warning",  "H Freeze",   45  , 0, 0, 0, {0  , 0  , 255}},  // blue
+       {"Hazardous Materials Warning"    , "warning",  "Haz Mat",    7   , 0, 0, 0, {75 , 0  , 130}},  // indigo
+       {"Hazardous Seas Warning"         , "warning",  "Haz Seas",   67  , 0, 0, 0, {221, 160, 221}},  // plum
+       {"Heavy Freezing Spray Warning"   , "warning",  "Fz Spray",   63  , 0, 0, 0, {0  , 191, 255}},  // deepskyblue
+       {"Heavy Snow Warning"             , "warning",  "Heavy Snow", 15  , 0, 0, 0, {138, 43 , 226}},  // blueviolet
+       {"High Surf Warning"              , "warning",  "High Surf",  28  , 0, 0, 0, {34 , 139, 34 }},  // forestgreen
+       {"High Wind Warning"              , "warning",  "High Wind",  17  , 0, 0, 0, {218, 165, 32 }},  // goldenrod
+       {"Hurricane Force Wind Warning"   , "warning",  "Hur F Wind", 9   , 0, 0, 0, {205, 92 , 92 }},  // westernred
+       {"Hurricane Warning"              , "warning",  "Hurricane",  9   , 0, 0, 0, {220, 20 , 60 }},  // crimson
+       {"Hurricane Wind Warning"         , "warning",  "Hur Wind",   9   , 0, 0, 0, {205, 92 , 92 }},  // westernred
+       {"Ice Storm Warning"              , "warning",  "Ice Storm",  13  , 0, 0, 0, {139, 0  , 139}},  // darkmagenta
+       {"Inland Hurricane Warning"       , "warning",  "I Hurr",     9   , 0, 0, 0, {220, 20 , 60 }},  // crimson
+       {"Inland Tropical Storm Warning"  , "warning",  "I Trop Stm", 14  , 0, 0, 0, {178, 34 , 34 }},  // firebrick
+       {"Lake Effect Snow Warning"       , "warning",  "Lake Snow",  30  , 0, 0, 0, {0  , 139, 139}},  // darkcyan
+       {"Lakeshore Flood Warning"        , "warning",  "L Flood",    25  , 0, 0, 0, {34 , 139, 34 }},  // forestgreen
+       {"Law Enforcement Warning"        , "warning",  "Law Enf.",   8   , 0, 0, 0, {192, 192, 192}},  // silver
+       {"Nuclear Power Plant Warning"    , "warning",  "Nuke Pwr",   7   , 0, 0, 0, {75 , 0  , 130}},  // indigo
+       {"Radiological Hazard Warning"    , "warning",  "Radiology",  7   , 0, 0, 0, {75 , 0  , 130}},  // indigo
+       {"Red Flag Warning"               , "warning",  "Red Flag",   47  , 0, 0, 0, {255, 20 , 147}},  // deeppink
+       {"Severe Thunderstorm Warning"    , "warning",  "Svr Storm",  4   , 0, 0, 1, {255, 165, 0  }},  // orange
+       {"Shelter In Place Warning"       , "warning",  "Shelter",    6   , 0, 0, 0, {250, 128, 114}},  // salmon
+       {"Sleet Warning"                  , "warning",  "Sleet",      29  , 0, 0, 0, {135, 206, 235}},  // skyblue
+       {"Special Marine Warning"         , "warning",  "Sp Marine",  11  , 0, 0, 1, {255, 165, 0  }},  // orange
+       {"Storm Warning"                  , "warning",  "Storm",      13  , 0, 0, 0, {148, 0  , 211}},  // darkviolet
+       {"Tornado Warning"                , "warning",  "Tornado",    2   , 0, 0, 1, {255, 0  , 0  }},  // red
+       {"Tropical Storm Warning"         , "warning",  "Trop Storm", 14  , 0, 0, 0, {178, 34 , 34 }},  // firebrick
+       {"Tropical Storm Wind Warning"    , "warning",  "Trop Wind",  9   , 0, 0, 0, {178, 34 , 34 }},  // firebrick
+       {"Tsunami Warning"                , "warning",  "Tsunami",    1   , 0, 0, 0, {253, 99 , 71 }},  // tomato
+       {"Typhoon Warning"                , "warning",  "Typhoon",    10  , 0, 0, 0, {220, 20 , 60 }},  // crimson
+       {"Volcano Warning"                , "warning",  "Volcano",    23  , 0, 0, 0, {47 , 79 , 79 }},  // darkslategray
+       {"Wind Chill Warning"             , "warning",  "Wind Chill", 43  , 0, 0, 0, {176, 196, 222}},  // lightsteelblue
+       {"Winter Storm Warning"           , "warning",  "Wntr Storm", 16  , 0, 0, 0, {255, 105, 180}},  // hotpink
+
+       /* Advisorys */
+       {"Air Stagnation Advisory"        , "advisory", "Air Stag",   76  , 0, 0, 0, {128, 128, 128}},  // gray
+       {"Ashfall Advisory"               , "advisory", "Ashfall",    73  , 0, 0, 0, {105, 105, 105}},  // dimgray
+       {"Blowing Dust Advisory"          , "advisory", "Blow Dust",  71  , 0, 0, 0, {189, 183, 107}},  // darkkhaki
+       {"Blowing Snow Advisory"          , "advisory", "Blow Snow",  50  , 0, 0, 0, {173, 216, 230}},  // lightblue
+       {"Brisk Wind Advisory"            , "advisory", "Brsk Wind",  66  , 0, 0, 0, {216, 191, 216}},  // thistle
+       {"Coastal Flood Advisory"         , "advisory", "C Flood",    58  , 0, 0, 0, {124, 252, 0  }},  // lawngreen
+       {"Dense Fog Advisory"             , "advisory", "Fog",        68  , 0, 0, 0, {112, 128, 144}},  // slategray
+       {"Dense Smoke Advisory"           , "advisory", "Smoke",      64  , 0, 0, 0, {240, 230, 140}},  // khaki
+       {"Flood Advisory"                 , "advisory", "Flood",      57  , 0, 0, 1, {0  , 255, 127}},  // springgreen
+       {"Freezing Drizzle Advisory"      , "advisory", "Fz Drzl",    51  , 0, 0, 0, {218, 112, 214}},  // orchid
+       {"Freezing Fog Advisory"          , "advisory", "Fz Fog",     74  , 0, 0, 0, {0  , 128, 128}},  // teal
+       {"Freezing Rain Advisory"         , "advisory", "Fz Rain",    51  , 0, 0, 0, {218, 112, 214}},  // orchid
+       {"Freezing Spray Advisory"        , "advisory", "Fz Spray",   75  , 0, 0, 0, {0  , 191, 255}},  // deepskyblue
+       {"Frost Advisory"                 , "advisory", "Frost",      72  , 0, 0, 0, {100, 149, 237}},  // cornflowerblue
+       {"Heat Advisory"                  , "advisory", "Heat",       56  , 0, 0, 0, {255, 127, 80 }},  // coral
+       {"High Surf Advisory"             , "advisory", "Surf",       60  , 0, 0, 0, {186, 85 , 211}},  // mediumorchid
+       {"Hydrologic Advisory"            , "advisory", "Hydro",      57  , 0, 0, 0, {0  , 255, 127}},  // springgreen
+       {"Lake Effect Snow Advisory"      , "advisory", "Lake Snow",  54  , 0, 0, 0, {72 , 209, 204}},  // mediumturquoise
+       {"Lake Wind Advisory"             , "advisory", "Lake Wind",  69  , 0, 0, 0, {210, 180, 140}},  // tan
+       {"Lakeshore Flood Advisory"       , "advisory", "L Flood",    58  , 0, 0, 0, {124, 252, 0  }},  // lawngreen
+       {"Low Water Advisory"             , "advisory", "Low Water",  77  , 0, 0, 0, {165, 42 , 42 }},  // brown
+       {"Sleet Advisory"                 , "advisory", "Sleet",      52  , 0, 0, 0, {123, 104, 238}},  // mediumslateblue
+       {"Small Craft Advisory"           , "advisory", "S Craft",    65  , 0, 0, 0, {216, 191, 216}},  // thistle
+       {"Small Stream Flood Advisory"    , "advisory", "Flood",      57  , 0, 0, 1, {0  , 255, 127}},  // springgreen
+       {"Snow and Blowing Snow Advisory" , "advisory", "S/B Snow",   50  , 0, 0, 0, {176, 224, 230}},  // powderblue
+       {"Tsunami Advisory"               , "advisory", "Tsunami",    42  , 0, 0, 0, {210, 105, 30 }},  // chocolate
+       {"Wind Advisory"                  , "advisory", "Wind",       66  , 0, 0, 0, {210, 180, 140}},  // tan
+       {"Wind Chill Advisory"            , "advisory", "Wind Chill", 55  , 0, 0, 0, {175, 238, 238}},  // paleturquoise
+       {"Winter Weather Advisory"        , "advisory", "Winter Wx",  53  , 0, 0, 0, {123, 104, 238}},  // mediumslateblue
+       {"Lake Effect Snow and Blowing Snow Advisory"
+                                         , "advisory", "L/S Snow",   54  , 0, 0, 0, {72 , 209, 204}},  // mediumturquoise
+       {"Urban and Small Stream Flood Advisory"
+                                         , "advisory", "Flood",      57  , 0, 0, 1, {0  , 255, 127}},  // springgreen
+       {"Arroyo and Small Stream Advisory"
+                                         , "advisory", "Flood",      57  , 0, 0, 1, {0  , 255, 127}},  // springgreen
+
+
+       /* Watches */
+       {"Tornado Watch"                  , "watch",    "Tornado",    33  , 0, 0, 0, {255, 255, 0  }},  // yellow
+       {"Severe Thunderstorm Watch"      , "watch",    "Svr Storm",  35  , 0, 0, 0, {219, 112, 147}},  // palevioletred
+       {"Avalanche Watch"                , "watch",    "Avalanche",  79  , 0, 0, 0, {244, 164, 96 }},  // sandybrown
+       {"Blizzard Watch"                 , "watch",    "Blizzard",   80  , 0, 0, 0, {173, 255, 47 }},  // greenyellow
+       {"Coastal Flood Watch"            , "watch",    "C Flood",    84  , 0, 0, 0, {102, 205, 170}},  // mediumaquamarine
+       {"Excessive Heat Watch"           , "watch",    "Heat",       87  , 0, 0, 0, {128, 0  , 0  }},  // maroon
+       {"Extreme Cold Watch"             , "watch",    "Ext Cold",   88  , 0, 0, 0, {0  , 0  , 255}},  // blue
+       {"Fire Weather Watch"             , "watch",    "Fire Wx",    93  , 0, 0, 0, {255, 222, 173}},  // navajowhite
+       {"Flash Flood Watch"              , "watch",    "F Flood",    37  , 0, 0, 0, {50 , 205, 50 }},  // limegreen
+       {"Flood Watch"                    , "watch",    "Flood",      37  , 0, 0, 0, {46 , 139, 87 }},  // seagreen
+       {"Freeze Watch"                   , "watch",    "Freeze",     91  , 0, 0, 0, {65 , 105, 225}},  // royalblue
+       {"Gale Watch"                     , "watch",    "Gale",       1000, 0, 0, 0, {255, 192, 203}},  // pink
+       {"Hard Freeze Watch"              , "watch",    "H Freeze",   91  , 0, 0, 0, {65 , 105, 225}},  // royalblue
+       {"Hazardous Seas Watch"           , "watch",    "Haz Seas",   1000, 0, 0, 0, {72 , 61 , 139}},  // darkslateblue
+       {"Heavy Freezing Spray Watch"     , "watch",    "Fz Spray",   1000, 0, 0, 0, {188, 143, 143}},  // rosybrown
+       {"High Wind Watch"                , "watch",    "Wind",       86  , 0, 0, 0, {184, 134, 11 }},  // darkgoldenrod
+       {"Hurricane Force Wind Watch"     , "watch",    "Hur Wind",   1000, 0, 0, 0, {153, 50 , 204}},  // darkorchid
+       {"Hurricane Watch"                , "watch",    "Hurricane",  48  , 0, 0, 0, {255, 0  , 255}},  // magenta
+       {"Hurricane Wind Watch"           , "watch",    "Hur F Wind", 1000, 0, 0, 0, {255, 160, 122}},  // lightsalmon
+       {"Lake Effect Snow Watch"         , "watch",    "Lake Snow",  1000, 0, 0, 0, {135, 206, 250}},  // lightskyblue
+       {"Lakeshore Flood Watch"          , "watch",    "L Flood",    84  , 0, 0, 0, {102, 205, 170}},  // mediumaquamarine
+       {"Storm Watch"                    , "watch",    "Storm",      81  , 0, 0, 0, {238, 130, 238}},  // violet
+       {"Tropical Storm Watch"           , "watch",    "Trop Storm", 81  , 0, 0, 0, {240, 128, 128}},  // lightcoral
+       {"Tropical Storm Wind Watch"      , "watch",    "Trop Wind",  1000, 0, 0, 0, {240, 128, 128}},  // lightcoral
+       {"Typhoon Watch"                  , "watch",    "Typhoon",    48  , 0, 0, 0, {255, 0  , 255}},  // magenta
+       {"Wind Chill Watch"               , "watch",    "Wind Chill", 89  , 0, 0, 0, {95 , 158, 160}},  // cadetblue
+       {"Winter Storm Watch"             , "watch",    "Wntr Storm", 83  , 0, 0, 0, {70 , 130, 180}},  // steelblue
+
+       /* Statements */
+       {"Coastal Flood Statement"        , "other",    "C Flood",    97  , 0, 0, 0, {107, 142, 35 }},  // olivedrab
+       {"Flash Flood Statement"          , "other",    "F Flood",    39  , 0, 0, 1, {154, 205, 50 }},  // yellowgreen
+       {"Flood Statement"                , "other",    "Flood",      39  , 0, 0, 1, {0  , 255, 0  }},  // lime
+       {"Hurricane Statement"            , "other",    "Hurricane",  49  , 0, 0, 0, {147, 112, 219}},  // mediumpurple
+       {"Lakeshore Flood Statement"      , "other",    "L Flood",    98  , 0, 0, 0, {107, 142, 35 }},  // olivedrab
+       {"Marine Weather Statement"       , "other",    "Marine Wx",  11  , 0, 0, 1, {255, 239, 213}},  // peachpuff
+       {"Rip Current Statement"          , "other",    "Rip Curnt",  1000, 0, 0, 0, {64 , 224, 208}},  // turquoise
+       {"Severe Weather Statement"       , "other",    "Severe Wx",  38  , 0, 0, 1, {0  , 255, 255}},  // aqua
+       {"Special Weather Statement"      , "other",    "Special Wx", 99  , 0, 0, 0, {255, 228, 181}},  // moccasin
+       {"Typhoon Statement"              , "other",    "Typhoon",    49  , 0, 0, 0, {147, 112, 219}},  // mediumpurple
+
+       /* Misc */
+       {"Air Quality Alert"              , "other",    "Air Qual",   1000, 0, 0, 0, {128, 128, 128}},  // gray
+
+       {"Extreme Fire Danger"            , "other",    "Fire Dngr",  94  , 0, 0, 0, {233, 150, 122}},  // darksalmon
+
+       {"Child Abduction Emergency"      , "other",    "Child Abd",  95  , 0, 0, 0, {255, 215, 0  }},  // gold
+       {"Local Area Emergency"           , "other",    "Local",      78  , 0, 0, 0, {192, 192, 192}},  // silver
+
+       {"Short Term Forecast"            , "other",    "S Term Fc",  102 , 0, 0, 0, {152, 251, 152}},  // palegreen
+
+       {"Evacuation Immediate"           , "other",    "Evacuate",   6   , 0, 0, 0, {127, 255, 0  }},  // chartreuse
+
+       {"Administrative Message"         , "other",    "Admin",      103 , 0, 0, 0, {255, 255, 255}},  // white
+       {"Civil Emergency Message"        , "other",    "Civil Em",   7   , 0, 0, 0, {255, 182, 193}},  // lightpink
+
+       {"911 Telephone Outage"           , "other",    "911 Out",    96  , 0, 0, 0, {192, 192, 192}},  // silver
+
+       {"Hazardous Weather Outlook"      , "other",    "Haz Wx Ol",  101 , 0, 0, 0, {238, 232, 170}},  // palegoldenrod
+       {"Hydrologic Outlook"             , "other",    "Hydro Ol",   1000, 0, 0, 0, {144, 238, 144}},  // lightgreen
+
+       {"Test"                           , "other",    "Test",       104 , 0, 0, 0, {240, 255, 255}},  // azure
+
+       /* End of list */
+       {NULL, NULL, NULL, 0, 0, 0, 0, {0, 0, 0}},
+};
+
+AlertInfo *alert_info_find(gchar *title)
+{
+       for (int i = 0; alert_info[i].title; i++)
+               if (g_str_has_prefix(title, alert_info[i].title))
+                       return &alert_info[i];
+       return NULL;
+}
diff --git a/src/plugins/alert-info.h b/src/plugins/alert-info.h
new file mode 100644 (file)
index 0000000..d74723f
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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/>.
+ */
+
+#ifndef __ALERT_INFO_H__
+#define __ALERT_INFO_H__
+
+#include <glib.h>
+#include <rsl.h>
+
+typedef struct {
+       char    *title;    // Title, "Tornado Warning"
+       char    *category; // Category, warning/watch/etc
+       char    *abbr;     // Abbreviation, for button label
+       int      prior;    // Priority, for county color picking
+       gboolean hidden;   // Show this alert type?
+       gboolean current;  // Are the currently alerts for this type?
+       gboolean ispoly;   // May contain a polygon warning area
+       guint8   color[3]; // Color to use for drawing alert
+} AlertInfo;
+
+extern AlertInfo alert_info[];
+
+AlertInfo *alert_info_find(gchar *title);
+
+#endif
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;
+}
diff --git a/src/plugins/alert.h b/src/plugins/alert.h
new file mode 100644 (file)
index 0000000..414fff1
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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/>.
+ */
+
+#ifndef __ALERT_H__
+#define __ALERT_H__
+
+#include <glib-object.h>
+#include <grits.h>
+
+#define GRITS_TYPE_PLUGIN_ALERT            (grits_plugin_alert_get_type ())
+#define GRITS_PLUGIN_ALERT(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj),   GRITS_TYPE_PLUGIN_ALERT, GritsPluginAlert))
+#define GRITS_IS_PLUGIN_ALERT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj),   GRITS_TYPE_PLUGIN_ALERT))
+#define GRITS_PLUGIN_ALERT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST   ((klass), GRITS_TYPE_PLUGIN_ALERT, GritsPluginAlertClass))
+#define GRITS_IS_PLUGIN_ALERT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE   ((klass), GRITS_TYPE_PLUGIN_ALERT))
+#define GRITS_PLUGIN_ALERT_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),   GRITS_TYPE_PLUGIN_ALERT, GritsPluginAlertClass))
+
+typedef struct _GritsPluginAlert      GritsPluginAlert;
+typedef struct _GritsPluginAlertClass GritsPluginAlertClass;
+
+struct _GritsPluginAlert {
+       GObject parent_instance;
+
+       /* instance members */
+       GritsViewer *viewer;
+       GritsPrefs  *prefs;
+       GtkWidget   *config;
+       GtkWidget   *details;
+
+       GritsHttp   *http;
+       guint        refresh_id;
+       guint        time_changed_id;
+       GThreadPool *threads;
+
+       GList       *msgs;
+       time_t       updated;
+       GTree       *counties;
+       GList       *states;
+};
+
+struct _GritsPluginAlertClass {
+       GObjectClass parent_class;
+};
+
+GType grits_plugin_alert_get_type();
+
+/* Methods */
+GritsPluginAlert *grits_plugin_alert_new(GritsViewer *viewer, GritsPrefs *prefs);
+
+#endif
+
index 66eb172a4c4431743c4b7fc265dd6764f5adee49..623fa34c0783db84a86a61520d20829a53edd47d 100644 (file)
@@ -211,7 +211,7 @@ void aweather_level2_draw(GritsObject *_level2, GritsOpenGL *opengl)
 
        /* Draw wsr88d */
        Sweep *sweep = level2->sweep;
-       glDisable(GL_ALPHA_TEST);
+       //glDisable(GL_ALPHA_TEST);
        glDisable(GL_CULL_FACE);
        glDisable(GL_LIGHTING);
        glEnable(GL_TEXTURE_2D);
@@ -324,7 +324,7 @@ void aweather_level2_set_iso(AWeatherLevel2 *level2, gfloat level)
                vol->disp = GRITS_VOLUME_SURFACE;
                GRITS_OBJECT(vol)->center = GRITS_OBJECT(level2)->center;
                grits_viewer_add(GRITS_OBJECT(level2)->viewer,
-                               GRITS_OBJECT(vol), GRITS_LEVEL_WORLD, TRUE);
+                               GRITS_OBJECT(vol), GRITS_LEVEL_WORLD+1, FALSE);
                level2->volume = vol;
        }
        if (ISO_MIN < level && level < ISO_MAX) {
index 66c8b7ed73793a5b32a4499ae449054e56db7c87..52d94c92d0ed1411c67abb3bafd8a4d255863609 100644 (file)
@@ -179,7 +179,7 @@ gpointer _site_update_thread(gpointer _site)
        }
        grits_object_hide(GRITS_OBJECT(site->level2), site->hidden);
        grits_viewer_add(site->viewer, GRITS_OBJECT(site->level2),
-                       GRITS_LEVEL_WORLD, TRUE);
+                       GRITS_LEVEL_WORLD+1, TRUE);
 
 out:
        g_idle_add(_site_update_end, site);