1
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
2
/* weather-yrno.c - Yr.no Weather service.
4
* Copyright 2012 Giovanni Campagna <scampa.giovanni@gmail.com>
6
* This program is free software; you can redistribute it and/or
7
* modify it under the terms of the GNU General Public License as
8
* published by the Free Software Foundation; either version 2 of the
9
* License, or (at your option) any later version.
11
* This program is distributed in the hope that it will be useful, but
12
* WITHOUT ANY WARRANTY; without even the implied warranty of
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
* General Public License for more details.
16
* You should have received a copy of the GNU General Public License
17
* along with this program; if not, see
18
* <http://www.gnu.org/licenses/>.
25
#define _GNU_SOURCE /* for strptime */
33
#include <libxml/parser.h>
34
#include <libxml/xpath.h>
35
#include <libxml/xpathInternals.h>
37
#define GWEATHER_I_KNOW_THIS_IS_UNSTABLE
38
#include "weather-priv.h"
40
#define XC(t) ((const xmlChar *)(t))
42
/* Reference for symbols at http://om.yr.no/forklaring/symbol/ */
45
GWeatherConditions condition;
47
{ GWEATHER_SKY_CLEAR, { FALSE, GWEATHER_PHENOMENON_NONE, GWEATHER_QUALIFIER_NONE } }, /* Sun / clear sky */
48
{ GWEATHER_SKY_BROKEN, { FALSE, GWEATHER_PHENOMENON_NONE, GWEATHER_QUALIFIER_NONE } }, /* Fair */
49
{ GWEATHER_SKY_SCATTERED, { FALSE, GWEATHER_PHENOMENON_NONE, GWEATHER_QUALIFIER_NONE } }, /* Partly cloudy */
50
{ GWEATHER_SKY_OVERCAST, { FALSE, GWEATHER_PHENOMENON_NONE, GWEATHER_QUALIFIER_NONE } }, /* Cloudy */
51
{ GWEATHER_SKY_BROKEN, { TRUE, GWEATHER_PHENOMENON_RAIN, GWEATHER_QUALIFIER_SHOWERS } }, /* Rain showers */
52
{ GWEATHER_SKY_BROKEN, { TRUE, GWEATHER_PHENOMENON_RAIN, GWEATHER_QUALIFIER_THUNDERSTORM } }, /* Rain showers with thunder */
53
{ GWEATHER_SKY_BROKEN, { TRUE, GWEATHER_PHENOMENON_ICE_PELLETS, GWEATHER_QUALIFIER_SHOWERS } }, /* Sleet showers */
54
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_SNOW, GWEATHER_QUALIFIER_SHOWERS } }, /* Snow showers */
55
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_RAIN, GWEATHER_QUALIFIER_NONE } }, /* Rain */
56
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_RAIN, GWEATHER_QUALIFIER_HEAVY } }, /* Heavy rain */
57
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_RAIN, GWEATHER_QUALIFIER_THUNDERSTORM } }, /* Rain and thunder */
58
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_ICE_PELLETS, GWEATHER_QUALIFIER_NONE } }, /* Sleet */
59
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_SNOW, GWEATHER_QUALIFIER_NONE } }, /* Snow */
60
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_SNOW, GWEATHER_QUALIFIER_THUNDERSTORM } }, /* Snow and thunder */
61
{ GWEATHER_SKY_CLEAR, { TRUE, GWEATHER_PHENOMENON_FOG, GWEATHER_QUALIFIER_NONE } }, /* Fog */
62
{ GWEATHER_SKY_BROKEN, { TRUE, GWEATHER_PHENOMENON_ICE_PELLETS, GWEATHER_QUALIFIER_THUNDERSTORM } }, /* Sleet showers and thunder */
63
{ GWEATHER_SKY_BROKEN, { TRUE, GWEATHER_PHENOMENON_SNOW, GWEATHER_QUALIFIER_THUNDERSTORM } }, /* Snow showers and thunder */
64
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_RAIN, GWEATHER_QUALIFIER_HEAVY } }, /* Rain and thunder */
65
{ GWEATHER_SKY_OVERCAST, { TRUE, GWEATHER_PHENOMENON_ICE_PELLETS, GWEATHER_QUALIFIER_HEAVY } } /* Sleet and thunder */
70
GWeatherWindDirection direction;
71
} wind_directions[] = {
72
{ "N", GWEATHER_WIND_N },
73
{ "NNE", GWEATHER_WIND_NNE },
74
{ "NE", GWEATHER_WIND_NE },
75
{ "ENE", GWEATHER_WIND_ENE },
76
{ "E", GWEATHER_WIND_E },
77
{ "ESE", GWEATHER_WIND_ESE },
78
{ "SE", GWEATHER_WIND_SE },
79
{ "SSE", GWEATHER_WIND_SSE },
80
{ "S", GWEATHER_WIND_S },
81
{ "SSW", GWEATHER_WIND_SSW },
82
{ "SW", GWEATHER_WIND_SW },
83
{ "WSW", GWEATHER_WIND_WSW },
84
{ "W", GWEATHER_WIND_W },
85
{ "WNW", GWEATHER_WIND_WNW },
86
{ "NW", GWEATHER_WIND_NW },
87
{ "NNW", GWEATHER_WIND_NNW },
91
date_to_time_t (const xmlChar *str, const char * tzid)
93
struct tm time = { 0 };
99
after = strptime ((const char*) str, "%Y-%m-%dT%T", &time);
101
g_warning ("Cannot parse date string \"%s\"", str);
108
tz = g_time_zone_new (tzid);
109
dt = g_date_time_new (tz,
117
rval = g_date_time_to_unix (dt);
119
g_time_zone_unref (tz);
120
g_date_time_unref (dt);
126
read_symbol (GWeatherInfo *info,
131
GWeatherInfoPrivate *priv = info->priv;
133
val = xmlGetProp (node, XC("number"));
135
symbol = strtol ((char*) val, NULL, 0) - 1;
136
if (symbol >= 0 && symbol < G_N_ELEMENTS (symbols)) {
138
priv->sky = symbols[symbol].sky;
139
priv->cond = symbols[symbol].condition;
144
read_wind_direction (GWeatherInfo *info,
150
val = xmlGetProp (node, XC("code"));
152
val = xmlGetProp (node, XC("name"));
156
for (i = 0; i < G_N_ELEMENTS (wind_directions); i++) {
157
if (strcmp ((char*) val, wind_directions[i].name) == 0) {
158
info->priv->wind = wind_directions[i].direction;
165
read_wind_speed (GWeatherInfo *info,
171
val = xmlGetProp (node, XC("mps"));
175
mps = g_ascii_strtod ((char*) val, NULL);
176
info->priv->windspeed = WINDSPEED_MS_TO_KNOTS (mps);
180
read_temperature (GWeatherInfo *info,
186
val = xmlGetProp (node, XC("value"));
190
celsius = g_ascii_strtod ((char*) val, NULL);
191
info->priv->temp = TEMP_C_TO_F (celsius);
195
read_pressure (GWeatherInfo *info,
201
val = xmlGetProp (node, XC("value"));
205
hpa = g_ascii_strtod ((char*) val, NULL);
206
info->priv->pressure = PRESSURE_MBAR_TO_INCH (hpa);
210
read_child_node (GWeatherInfo *info,
213
if (strcmp ((char*) node->name, "symbol") == 0)
214
read_symbol (info, node);
215
else if (strcmp ((char*) node->name, "windDirection") == 0)
216
read_wind_direction (info, node);
217
else if (strcmp ((char*) node->name, "windSpeed") == 0)
218
read_wind_speed (info, node);
219
else if (strcmp ((char*) node->name, "temperature") == 0)
220
read_temperature (info, node);
221
else if (strcmp ((char*) node->name, "pressure") == 0)
222
read_pressure (info, node);
226
fill_info_from_node (GWeatherInfo *info,
231
for (child = node->children; child != NULL; child = child->next) {
232
if (child->type == XML_ELEMENT_NODE)
233
read_child_node (info, child);
237
static GWeatherInfo *
238
make_info_from_node_old (GWeatherInfo *master_info,
242
GWeatherInfoPrivate *priv;
245
g_return_val_if_fail (node->type == XML_ELEMENT_NODE, NULL);
247
info = _gweather_info_new_clone (master_info);
250
val = xmlGetProp (node, XC("from"));
251
priv->current_time = priv->update = date_to_time_t (val, info->priv->location.tz_hint);
254
fill_info_from_node (info, node);
260
make_attribution_from_node (xmlNodePtr node)
266
url = xmlGetProp (node, XC("url"));
267
text = xmlGetProp (node, XC("text"));
269
/* Small hack to avoid linking the entire label, and to have
270
This is still compliant with the guidelines, as far as I
272
The label is a legal attribution and cannot be translated.
274
if (strcmp ((char*) text,
275
"Weather forecast from yr.no, delivered by the"
276
" Norwegian Meteorological Institute and the NRK") == 0)
277
res = g_strdup_printf ("Weather forecast from yr.no, delivered by"
278
" the <a href=\"%s\">Norwegian Meteorological"
279
" Institude and the NRK</a>", url);
281
res = g_strdup_printf ("<a href=\"%s\">%s</a>", url, text);
290
parse_forecast_xml_old (GWeatherInfo *master_info,
291
SoupMessageBody *body)
293
GWeatherInfoPrivate *priv;
295
xmlXPathContextPtr xpath_ctx;
296
xmlXPathObjectPtr xpath_result;
299
priv = master_info->priv;
301
doc = xmlParseMemory (body->data, body->length);
305
xpath_ctx = xmlXPathNewContext (doc);
306
xpath_result = xmlXPathEval (XC("/weatherdata/forecast/tabular/time"), xpath_ctx);
308
if (!xpath_result || xpath_result->type != XPATH_NODESET)
311
for (i = 0; i < xpath_result->nodesetval->nodeNr; i++) {
315
node = xpath_result->nodesetval->nodeTab[i];
316
info = make_info_from_node_old (master_info, node);
318
priv->forecast_list = g_slist_append (priv->forecast_list, info);
321
xmlXPathFreeObject (xpath_result);
323
xpath_result = xmlXPathEval (XC("/weatherdata/credit/link"), xpath_ctx);
324
if (!xpath_result || xpath_result->type != XPATH_NODESET)
327
priv->forecast_attribution = make_attribution_from_node (xpath_result->nodesetval->nodeTab[0]);
331
xmlXPathFreeObject (xpath_result);
332
xmlXPathFreeContext (xpath_ctx);
339
parse_forecast_xml_new (GWeatherInfo *master_info,
340
SoupMessageBody *body)
342
GWeatherInfoPrivate *priv;
344
xmlXPathContextPtr xpath_ctx;
345
xmlXPathObjectPtr xpath_result;
348
priv = master_info->priv;
350
doc = xmlParseMemory (body->data, body->length);
354
xpath_ctx = xmlXPathNewContext (doc);
355
xpath_result = xmlXPathEval (XC("/weatherdata/product/time"), xpath_ctx);
357
if (!xpath_result || xpath_result->type != XPATH_NODESET)
360
for (i = 0; i < xpath_result->nodesetval->nodeNr; i++) {
364
time_t from_time, to_time;
367
node = xpath_result->nodesetval->nodeTab[i];
369
val = xmlGetProp (node, XC("from"));
370
from_time = date_to_time_t (val, priv->location.tz_hint);
373
val = xmlGetProp (node, XC("to"));
374
to_time = date_to_time_t (val, priv->location.tz_hint);
377
/* New API has forecast in a list of "master" elements
378
with details (indicated by from==to) and "slave" elements
379
that hold only precipitation and symbol. For our purpose,
380
the master element is enough, except that we actually
381
want that symbol. So pick the symbol from the next element.
382
Additionally, compared to the old API the new API has one
383
<location> element inside each <time> element.
385
if (from_time == to_time) {
386
info = _gweather_info_new_clone (master_info);
387
info->priv->current_time = info->priv->update = from_time;
389
for (location = node->children;
390
location && location->type != XML_ELEMENT_NODE;
391
location = location->next);
393
fill_info_from_node (info, location);
395
if (i < xpath_result->nodesetval->nodeNr - 1) {
397
node = xpath_result->nodesetval->nodeTab[i];
399
for (location = node->children;
400
location && location->type != XML_ELEMENT_NODE;
401
location = location->next);
403
fill_info_from_node (info, location);
406
priv->forecast_list = g_slist_append (priv->forecast_list, info);
410
xmlXPathFreeObject (xpath_result);
412
/* The new (documented but not advertised) API is less strict in the
413
format of the attribution, and just requires a generic CC-BY compatible
414
attribution with a link to their service.
416
That's very nice of them!
418
priv->forecast_attribution = g_strdup(_("Weather data from the <a href=\"http://yr.no/\">Norwegian Meteorological Institute</a>"));
421
xmlXPathFreeContext (xpath_ctx);
426
yrno_finish_old (SoupSession *session,
430
GWeatherInfo *info = GWEATHER_INFO (user_data);
432
if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
433
/* forecast data is not really interesting anyway ;) */
434
g_message ("Failed to get Yr.no forecast data: %d %s\n",
435
msg->status_code, msg->reason_phrase);
436
_gweather_info_request_done (info);
440
parse_forecast_xml_old (info, msg->response_body);
441
_gweather_info_request_done (info);
445
yrno_start_open_old (GWeatherInfo *info)
447
GWeatherInfoPrivate *priv;
449
SoupMessage *message;
450
const char *country = NULL;
451
const char *adm_division = NULL;
452
char *city_name = NULL;
453
GWeatherLocation *glocation;
457
if (priv->forecast_type != GWEATHER_FORECAST_LIST)
460
glocation = priv->glocation;
462
if (glocation->level == GWEATHER_LOCATION_CITY)
463
city_name = glocation->name;
464
if (glocation->level == GWEATHER_LOCATION_ADM1 ||
465
glocation->level == GWEATHER_LOCATION_ADM2)
466
adm_division = glocation->name;
467
if (glocation->level == GWEATHER_LOCATION_COUNTRY)
468
country = glocation->name;
469
glocation = glocation->parent;
472
if (city_name == NULL || adm_division == NULL || country == NULL)
475
url = g_strdup_printf("http://yr.no/place/%s/%s/%s/forecast.xml", country, adm_division, city_name);
477
message = soup_message_new ("GET", url);
478
soup_session_queue_message (priv->session, message, yrno_finish_old, info);
480
priv->requests_pending++;
488
yrno_finish_new (SoupSession *session,
492
GWeatherInfo *info = GWEATHER_INFO (user_data);
494
if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
495
/* forecast data is not really interesting anyway ;) */
496
g_message ("Failed to get Yr.no forecast data: %d %s\n",
497
msg->status_code, msg->reason_phrase);
498
_gweather_info_request_done (info);
502
parse_forecast_xml_new (info, msg->response_body);
504
_gweather_info_request_done (info);
508
yrno_start_open_new (GWeatherInfo *info)
510
GWeatherInfoPrivate *priv;
512
SoupMessage *message;
513
WeatherLocation *loc;
514
gchar latstr[G_ASCII_DTOSTR_BUF_SIZE], lonstr[G_ASCII_DTOSTR_BUF_SIZE];
517
loc = &priv->location;
519
if (!loc->latlon_valid ||
520
priv->forecast_type != GWEATHER_FORECAST_LIST)
523
/* see the description here: http://api.yr.no/weatherapi/ */
525
g_ascii_dtostr (latstr, sizeof(latstr), RADIANS_TO_DEGREES (loc->latitude));
526
g_ascii_dtostr (lonstr, sizeof(lonstr), RADIANS_TO_DEGREES (loc->longitude));
528
url = g_strdup_printf("http://api.yr.no/weatherapi/locationforecast/1.8/?lat=%s;lon=%s", latstr, lonstr);
530
message = soup_message_new ("GET", url);
531
soup_session_queue_message (priv->session, message, yrno_finish_new, info);
533
priv->requests_pending++;
541
yrno_start_open (GWeatherInfo *info)
543
if (yrno_start_open_new (info))
546
return yrno_start_open_old (info);