summaryrefslogtreecommitdiff
path: root/libmateweather/weather-iwin.c
diff options
context:
space:
mode:
Diffstat (limited to 'libmateweather/weather-iwin.c')
-rw-r--r--libmateweather/weather-iwin.c475
1 files changed, 475 insertions, 0 deletions
diff --git a/libmateweather/weather-iwin.c b/libmateweather/weather-iwin.c
new file mode 100644
index 0000000..e098354
--- /dev/null
+++ b/libmateweather/weather-iwin.c
@@ -0,0 +1,475 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* weather-iwin.c - US National Weather Service IWIN forecast source
+ *
+ * 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 2 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/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <libxml/parser.h>
+
+#define MATEWEATHER_I_KNOW_THIS_IS_UNSTABLE
+#include "weather.h"
+#include "weather-priv.h"
+
+/**
+ * Humans don't deal well with .MONDAY...SUNNY AND BLAH BLAH.TUESDAY...THEN THIS AND THAT.WEDNESDAY...RAINY BLAH BLAH.
+ * This function makes it easier to read.
+ */
+static gchar *
+formatWeatherMsg (gchar *forecast)
+{
+ gchar *ptr = forecast;
+ gchar *startLine = NULL;
+
+ while (0 != *ptr) {
+ if (ptr[0] == '\n' && ptr[1] == '.') {
+ /* This removes the preamble by shifting the relevant data
+ * down to the start of the buffer. */
+ if (NULL == startLine) {
+ memmove (forecast, ptr, strlen (ptr) + 1);
+ ptr = forecast;
+ ptr[0] = ' ';
+ }
+ ptr[1] = '\n';
+ ptr += 2;
+ startLine = ptr;
+ } else if (ptr[0] == '.' && ptr[1] == '.' && ptr[2] == '.' && NULL != startLine) {
+ memmove (startLine + 2, startLine, (ptr - startLine) * sizeof (gchar));
+ startLine[0] = ' ';
+ startLine[1] = '\n';
+ ptr[2] = '\n';
+
+ ptr += 3;
+
+ } else if (ptr[0] == '$' && ptr[1] == '$') {
+ ptr[0] = ptr[1] = ' ';
+
+ } else {
+ ptr++;
+ }
+ }
+
+ return forecast;
+}
+
+static gboolean
+hasAttr (xmlNode *node, const char *attr_name, const char *attr_value)
+{
+ xmlChar *attr;
+ gboolean res = FALSE;
+
+ if (!node)
+ return res;
+
+ attr = xmlGetProp (node, (const xmlChar *) attr_name);
+
+ if (!attr)
+ return res;
+
+ res = g_str_equal ((const char *)attr, attr_value);
+
+ xmlFree (attr);
+
+ return res;
+}
+
+static GSList *
+parseForecastXml (const char *buff, WeatherInfo *master_info)
+{
+ GSList *res = NULL;
+ xmlDocPtr doc;
+ xmlNode *root, *node;
+
+ g_return_val_if_fail (master_info != NULL, NULL);
+
+ if (!buff || !*buff)
+ return NULL;
+
+ #define XC (const xmlChar *)
+ #define isElem(_node,_name) g_str_equal ((const char *)_node->name, _name)
+
+ doc = xmlParseMemory (buff, strlen (buff));
+ if (!doc)
+ return NULL;
+
+ /* Description at http://www.weather.gov/mdl/XML/Design/MDL_XML_Design.pdf */
+ root = xmlDocGetRootElement (doc);
+ for (node = root->xmlChildrenNode; node; node = node->next) {
+ if (node->name == NULL || node->type != XML_ELEMENT_NODE)
+ continue;
+
+ if (isElem (node, "data")) {
+ xmlNode *n;
+ char *time_layout = NULL;
+ time_t update_times[7] = {0};
+
+ for (n = node->children; n; n = n->next) {
+ if (!n->name)
+ continue;
+
+ if (isElem (n, "time-layout")) {
+ if (!time_layout && hasAttr (n, "summarization", "24hourly")) {
+ xmlNode *c;
+ int count = 0;
+
+ for (c = n->children; c && (count < 7 || !time_layout); c = c->next) {
+ if (c->name && !time_layout && isElem (c, "layout-key")) {
+ xmlChar *val = xmlNodeGetContent (c);
+
+ if (val) {
+ time_layout = g_strdup ((const char *)val);
+ xmlFree (val);
+ }
+ } else if (c->name && isElem (c, "start-valid-time")) {
+ xmlChar *val = xmlNodeGetContent (c);
+
+ if (val) {
+ GTimeVal tv;
+
+ if (g_time_val_from_iso8601 ((const char *)val, &tv)) {
+ update_times[count] = tv.tv_sec;
+ } else {
+ update_times[count] = 0;
+ }
+
+ count++;
+
+ xmlFree (val);
+ }
+ }
+ }
+
+ if (count != 7) {
+ /* There can be more than one time-layout element, the other
+ with only few children, which is not the one to use. */
+ g_free (time_layout);
+ time_layout = NULL;
+ }
+ }
+ } else if (isElem (n, "parameters")) {
+ xmlNode *p;
+
+ /* time-layout should be always before parameters */
+ if (!time_layout)
+ break;
+
+ if (!res) {
+ int i;
+
+ for (i = 0; i < 7; i++) {
+ WeatherInfo *nfo = weather_info_clone (master_info);
+
+ if (nfo) {
+ nfo->valid = FALSE;
+ nfo->forecast_type = FORECAST_ZONE;
+ nfo->update = update_times [i];
+ nfo->sky = -1;
+ nfo->temperature_unit = TEMP_UNIT_FAHRENHEIT;
+ nfo->temp = -1000.0;
+ nfo->temp_min = -1000.0;
+ nfo->temp_max = -1000.0;
+ nfo->tempMinMaxValid = FALSE;
+ nfo->cond.significant = FALSE;
+ nfo->cond.phenomenon = PHENOMENON_NONE;
+ nfo->cond.qualifier = QUALIFIER_NONE;
+ nfo->dew = -1000.0;
+ nfo->wind = -1;
+ nfo->windspeed = -1;
+ nfo->pressure = -1.0;
+ nfo->visibility = -1.0;
+ nfo->sunriseValid = FALSE;
+ nfo->sunsetValid = FALSE;
+ nfo->sunrise = 0;
+ nfo->sunset = 0;
+ g_free (nfo->forecast);
+ nfo->forecast = NULL;
+ nfo->session = NULL;
+ nfo->requests_pending = 0;
+ nfo->finish_cb = NULL;
+ nfo->cb_data = NULL;
+ res = g_slist_append (res, nfo);
+ }
+ }
+ }
+
+ for (p = n->children; p; p = p->next) {
+ if (p->name && isElem (p, "temperature") && hasAttr (p, "time-layout", time_layout)) {
+ xmlNode *c;
+ GSList *at = res;
+ gboolean is_max = hasAttr (p, "type", "maximum");
+
+ if (!is_max && !hasAttr (p, "type", "minimum"))
+ break;
+
+ for (c = p->children; c && at; c = c->next) {
+ if (isElem (c, "value")) {
+ WeatherInfo *nfo = (WeatherInfo *)at->data;
+ xmlChar *val = xmlNodeGetContent (c);
+
+ /* can pass some values as <value xsi:nil="true"/> */
+ if (!val || !*val) {
+ if (is_max)
+ nfo->temp_max = nfo->temp_min;
+ else
+ nfo->temp_min = nfo->temp_max;
+ } else {
+ if (is_max)
+ nfo->temp_max = atof ((const char *)val);
+ else
+ nfo->temp_min = atof ((const char *)val);
+ }
+
+ if (val)
+ xmlFree (val);
+
+ nfo->tempMinMaxValid = nfo->tempMinMaxValid || (nfo->temp_max > -999.0 && nfo->temp_min > -999.0);
+ nfo->valid = nfo->tempMinMaxValid;
+
+ at = at->next;
+ }
+ }
+ } else if (p->name && isElem (p, "weather") && hasAttr (p, "time-layout", time_layout)) {
+ xmlNode *c;
+ GSList *at = res;
+
+ for (c = p->children; c && at; c = c->next) {
+ if (c->name && isElem (c, "weather-conditions")) {
+ WeatherInfo *nfo = at->data;
+ xmlChar *val = xmlGetProp (c, XC "weather-summary");
+
+ if (val && nfo) {
+ /* Checking from top to bottom, if 'value' contains 'name', then that win,
+ thus put longer (more precise) values to the top. */
+ int i;
+ struct _ph_list {
+ const char *name;
+ WeatherConditionPhenomenon ph;
+ } ph_list[] = {
+ { "Ice Crystals", PHENOMENON_ICE_CRYSTALS } ,
+ { "Volcanic Ash", PHENOMENON_VOLCANIC_ASH } ,
+ { "Blowing Sand", PHENOMENON_SANDSTORM } ,
+ { "Blowing Dust", PHENOMENON_DUSTSTORM } ,
+ { "Blowing Snow", PHENOMENON_FUNNEL_CLOUD } ,
+ { "Drizzle", PHENOMENON_DRIZZLE } ,
+ { "Rain", PHENOMENON_RAIN } ,
+ { "Snow", PHENOMENON_SNOW } ,
+ { "Fog", PHENOMENON_FOG } ,
+ { "Smoke", PHENOMENON_SMOKE } ,
+ { "Sand", PHENOMENON_SAND } ,
+ { "Haze", PHENOMENON_HAZE } ,
+ { "Dust", PHENOMENON_DUST } /*,
+ { "", PHENOMENON_SNOW_GRAINS } ,
+ { "", PHENOMENON_ICE_PELLETS } ,
+ { "", PHENOMENON_HAIL } ,
+ { "", PHENOMENON_SMALL_HAIL } ,
+ { "", PHENOMENON_UNKNOWN_PRECIPITATION } ,
+ { "", PHENOMENON_MIST } ,
+ { "", PHENOMENON_SPRAY } ,
+ { "", PHENOMENON_SQUALL } ,
+ { "", PHENOMENON_TORNADO } ,
+ { "", PHENOMENON_DUST_WHIRLS } */
+ };
+ struct _sky_list {
+ const char *name;
+ WeatherSky sky;
+ } sky_list[] = {
+ { "Mostly Sunny", SKY_BROKEN } ,
+ { "Mostly Clear", SKY_BROKEN } ,
+ { "Partly Cloudy", SKY_SCATTERED } ,
+ { "Mostly Cloudy", SKY_FEW } ,
+ { "Sunny", SKY_CLEAR } ,
+ { "Clear", SKY_CLEAR } ,
+ { "Cloudy", SKY_OVERCAST } ,
+ { "Clouds", SKY_SCATTERED } ,
+ { "Rain", SKY_SCATTERED } ,
+ { "Snow", SKY_SCATTERED }
+ };
+
+ nfo->valid = TRUE;
+ g_free (nfo->forecast);
+ nfo->forecast = g_strdup ((const char *)val);
+
+ for (i = 0; i < G_N_ELEMENTS (ph_list); i++) {
+ if (strstr ((const char *)val, ph_list [i].name)) {
+ nfo->cond.phenomenon = ph_list [i].ph;
+ break;
+ }
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (sky_list); i++) {
+ if (strstr ((const char *)val, sky_list [i].name)) {
+ nfo->sky = sky_list [i].sky;
+ break;
+ }
+ }
+ }
+
+ if (val)
+ xmlFree (val);
+
+ at = at->next;
+ }
+ }
+ }
+ }
+
+ if (res) {
+ gboolean have_any = FALSE;
+ GSList *r;
+
+ /* Remove invalid forecast data from the list.
+ They should be all valid or all invalid. */
+ for (r = res; r; r = r->next) {
+ WeatherInfo *nfo = r->data;
+
+ if (!nfo || !nfo->valid) {
+ if (r->data)
+ weather_info_free (r->data);
+
+ r->data = NULL;
+ } else {
+ have_any = TRUE;
+
+ if (nfo->tempMinMaxValid)
+ nfo->temp = (nfo->temp_min + nfo->temp_max) / 2.0;
+ }
+ }
+
+ if (!have_any) {
+ /* data members are freed already */
+ g_slist_free (res);
+ res = NULL;
+ }
+ }
+
+ break;
+ }
+ }
+
+ g_free (time_layout);
+
+ /* stop seeking XML */
+ break;
+ }
+ }
+ xmlFreeDoc (doc);
+
+ #undef XC
+ #undef isElem
+
+ return res;
+}
+
+static void
+iwin_finish (SoupSession *session, SoupMessage *msg, gpointer data)
+{
+ WeatherInfo *info = (WeatherInfo *)data;
+
+ g_return_if_fail (info != NULL);
+
+ if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
+ /* forecast data is not really interesting anyway ;) */
+ g_warning ("Failed to get IWIN forecast data: %d %s\n",
+ msg->status_code, msg->reason_phrase);
+ request_done (info, FALSE);
+ return;
+ }
+
+ if (info->forecast_type == FORECAST_LIST)
+ info->forecast_list = parseForecastXml (msg->response_body->data, info);
+ else
+ info->forecast = formatWeatherMsg (g_strdup (msg->response_body->data));
+
+ request_done (info, TRUE);
+}
+
+/* Get forecast into newly alloc'ed string */
+void
+iwin_start_open (WeatherInfo *info)
+{
+ gchar *url, *state, *zone;
+ WeatherLocation *loc;
+ SoupMessage *msg;
+
+ g_return_if_fail (info != NULL);
+ loc = info->location;
+ g_return_if_fail (loc != NULL);
+
+ if (loc->zone[0] == '-' && (info->forecast_type != FORECAST_LIST || !loc->latlon_valid))
+ return;
+
+ if (info->forecast) {
+ g_free (info->forecast);
+ info->forecast = NULL;
+ }
+
+ free_forecast_list (info);
+
+ if (info->forecast_type == FORECAST_LIST) {
+ /* see the description here: http://www.weather.gov/forecasts/xml/ */
+ if (loc->latlon_valid) {
+ struct tm tm;
+ time_t now = time (NULL);
+
+ localtime_r (&now, &tm);
+
+ url = g_strdup_printf ("http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?&lat=%.02f&lon=%.02f&format=24+hourly&startDate=%04d-%02d-%02d&numDays=7",
+ RADIANS_TO_DEGREES (loc->latitude), RADIANS_TO_DEGREES (loc->longitude), 1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday);
+
+ msg = soup_message_new ("GET", url);
+ g_free (url);
+ soup_session_queue_message (info->session, msg, iwin_finish, info);
+
+ info->requests_pending++;
+ }
+
+ return;
+ }
+
+ if (loc->zone[0] == ':') {
+ /* Met Office Region Names */
+ metoffice_start_open (info);
+ return;
+ } else if (loc->zone[0] == '@') {
+ /* Australian BOM forecasts */
+ bom_start_open (info);
+ return;
+ }
+
+ /* The zone for Pittsburgh (for example) is given as PAZ021 in the locations
+ ** file (the PA stands for the state pennsylvania). The url used wants the state
+ ** as pa, and the zone as lower case paz021.
+ */
+ zone = g_ascii_strdown (loc->zone, -1);
+ state = g_strndup (zone, 2);
+
+ url = g_strdup_printf ("http://weather.noaa.gov/pub/data/forecasts/zone/%s/%s.txt", state, zone);
+
+ g_free (zone);
+ g_free (state);
+
+ msg = soup_message_new ("GET", url);
+ g_free (url);
+ soup_session_queue_message (info->session, msg, iwin_finish, info);
+
+ info->requests_pending++;
+}