[libgweather: 1/2] Add NWS (USA) backend
- From: Emmanuele Bassi <ebassi src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libgweather: 1/2] Add NWS (USA) backend
- Date: Mon, 22 Aug 2022 18:06:04 +0000 (UTC)
commit d7f7b1dfbab74e358bed9afbda3de30dc43913ed
Author: Ryan Hendrickson <ryan hendrickson alum mit edu>
Date: Tue May 31 17:02:21 2022 -0400
Add NWS (USA) backend
.gitlab-ci.yml | 5 +
libgweather/gweather-info.c | 5 +
libgweather/gweather-info.h | 6 +-
libgweather/gweather-private.h | 3 +
libgweather/meson.build | 2 +
libgweather/tools/test_weather.c | 2 +
libgweather/weather-nws.c | 1082 ++++++++++++++++++++++++++++++++++++++
7 files changed, 1103 insertions(+), 2 deletions(-)
---
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 70ba4e6f..125cb7e2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -109,6 +109,7 @@ fedora-x86_64:
git
gobject-introspection-devel
itstool
+ json-glib-devel
libsoup-devel
libxml2-devel
ninja-build
@@ -197,6 +198,7 @@ static-scan:
git
gobject-introspection-devel
itstool
+ json-glib-devel
libsoup-devel
libxml2-devel
ninja-build
@@ -232,6 +234,7 @@ asan-build:
git
gobject-introspection-devel
itstool
+ json-glib-devel
libasan
libsoup-devel
libxml2-devel
@@ -268,6 +271,7 @@ coverage:
git
gobject-introspection-devel
itstool
+ json-glib-devel
lcov
libsoup-devel
libxml2-devel
@@ -310,6 +314,7 @@ reference:
git
gobject-introspection-devel
itstool
+ json-glib-devel
libsoup-devel
libxml2-devel
ninja-build
diff --git a/libgweather/gweather-info.c b/libgweather/gweather-info.c
index 694f609e..d33d3905 100644
--- a/libgweather/gweather-info.c
+++ b/libgweather/gweather-info.c
@@ -700,6 +700,11 @@ gweather_info_update (GWeatherInfo *info)
ok = FALSE;
/* Try national forecast services first */
+ if (info->providers & GWEATHER_PROVIDER_NWS)
+ ok = nws_start_open (info);
+ if (ok)
+ return;
+
if (info->providers & GWEATHER_PROVIDER_IWIN)
ok = iwin_start_open (info);
if (ok)
diff --git a/libgweather/gweather-info.h b/libgweather/gweather-info.h
index ddf89d96..8102cc44 100644
--- a/libgweather/gweather-info.h
+++ b/libgweather/gweather-info.h
@@ -20,9 +20,10 @@ G_BEGIN_DECLS
* GWeatherProvider:
* @GWEATHER_PROVIDER_NONE: no provider, no weather information available
* @GWEATHER_PROVIDER_METAR: METAR office, providing current conditions worldwide
- * @GWEATHER_PROVIDER_IWIN: US weather office, providing 7 days of forecast
+ * @GWEATHER_PROVIDER_IWIN: US weather office (old API), providing 7 days of forecast
* @GWEATHER_PROVIDER_MET_NO: MET.no service, worldwide but requires attribution and a subscription to the
[API users mailing-list](https://lists.met.no/mailman/listinfo/api-users).
* @GWEATHER_PROVIDER_OWM: OpenWeatherMap, worldwide and possibly more reliable, but requires attribution
and is limited in the number of queries
+ * @GWEATHER_PROVIDER_NWS: US weather office (new API), providing 7 days of hourly forecast (available since
4.2)
* @GWEATHER_PROVIDER_ALL: enable all available providers
*/
typedef enum { /*< flags, underscore_name=gweather_provider >*/
@@ -31,7 +32,8 @@ typedef enum { /*< flags, underscore_name=gweather_provider >*/
GWEATHER_PROVIDER_IWIN = 1 << 2,
GWEATHER_PROVIDER_MET_NO = 1 << 3,
GWEATHER_PROVIDER_OWM = 1 << 4,
- GWEATHER_PROVIDER_ALL = (GWEATHER_PROVIDER_METAR | GWEATHER_PROVIDER_IWIN | GWEATHER_PROVIDER_MET_NO |
GWEATHER_PROVIDER_OWM)
+ GWEATHER_PROVIDER_NWS = 1 << 5,
+ GWEATHER_PROVIDER_ALL = (GWEATHER_PROVIDER_METAR | GWEATHER_PROVIDER_IWIN | GWEATHER_PROVIDER_MET_NO |
GWEATHER_PROVIDER_OWM | GWEATHER_PROVIDER_NWS)
} GWeatherProvider;
#define GWEATHER_TYPE_INFO (gweather_info_get_type ())
diff --git a/libgweather/gweather-private.h b/libgweather/gweather-private.h
index d2ba299c..ffe4464a 100644
--- a/libgweather/gweather-private.h
+++ b/libgweather/gweather-private.h
@@ -210,6 +210,9 @@ metno_start_open (GWeatherInfo *info);
gboolean
owm_start_open (GWeatherInfo *info);
+gboolean
+nws_start_open (GWeatherInfo *info);
+
gboolean
metar_parse (char *metar,
GWeatherInfo *info);
diff --git a/libgweather/meson.build b/libgweather/meson.build
index 22738e67..fac4efff 100644
--- a/libgweather/meson.build
+++ b/libgweather/meson.build
@@ -122,6 +122,7 @@ deps_libgweather = [
libsoup_dep,
dependency('libxml-2.0', version: libxml_req_version),
geocode_glib_dep,
+ dependency('json-glib-1.0'),
c_compiler.find_library('m', required: false),
]
@@ -154,6 +155,7 @@ gweather_priv_sources = [
'weather-iwin.c',
'weather-metno.c',
'weather-owm.c',
+ 'weather-nws.c',
'weather-sun.c',
'weather-moon.c',
]
diff --git a/libgweather/tools/test_weather.c b/libgweather/tools/test_weather.c
index 9518bc54..349ae7a0 100644
--- a/libgweather/tools/test_weather.c
+++ b/libgweather/tools/test_weather.c
@@ -116,6 +116,8 @@ set_providers (GWeatherInfo *info)
ADD_PROVIDER_STR ("MET_NO");
if (providers & GWEATHER_PROVIDER_OWM)
ADD_PROVIDER_STR ("OWM");
+ if (providers & GWEATHER_PROVIDER_NWS)
+ ADD_PROVIDER_STR ("NWS");
if (providers == GWEATHER_PROVIDER_NONE) {
g_string_free (s, TRUE);
g_warning ("No providers enabled, failing");
diff --git a/libgweather/weather-nws.c b/libgweather/weather-nws.c
new file mode 100644
index 00000000..2a7238b9
--- /dev/null
+++ b/libgweather/weather-nws.c
@@ -0,0 +1,1082 @@
+/* weather-nws.c - National Weather Service (USA)
+ *
+ * SPDX-FileCopyrightText: The GWeather authors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "config.h"
+
+#include "gweather-private.h"
+
+#include <stdio.h>
+
+#include <glib.h>
+
+#include <json-glib/json-glib.h>
+
+#define JSON_OBJECT_RETURN_UNLESS_MEMBER_EXISTS(in, name, tmpl, tmpl_suffix, ...) \
+ if (!json_object_has_member (in, name)) { \
+ g_warning ("Member `" #tmpl "` does not exist" #tmpl_suffix, __VA_ARGS__); \
+ return; \
+ }
+
+#define JSON_OBJECT_GET_MEMBER_OR_RETURN(article, type, in, name, out, tmpl, tmpl_suffix, ...) \
+ JSON_OBJECT_RETURN_UNLESS_MEMBER_EXISTS (in, name, tmpl, tmpl_suffix, __VA_ARGS__) \
+ out = json_object_get_##type##_member (in, name); \
+ if (out == NULL) { \
+ g_warning ("Value at `" #tmpl "` is not " article " " #type #tmpl_suffix, __VA_ARGS__); \
+ return; \
+ }
+
+#define JSON_OBJECT_GET_ARRAY_MEMBER_OR_RETURN(in, name, out, tmpl, tmpl_suffix, ...) \
+ JSON_OBJECT_GET_MEMBER_OR_RETURN ("an", array, in, name, out, tmpl, tmpl_suffix, __VA_ARGS__)
+#define JSON_OBJECT_GET_OBJECT_MEMBER_OR_RETURN(in, name, out, tmpl, tmpl_suffix, ...) \
+ JSON_OBJECT_GET_MEMBER_OR_RETURN ("an", object, in, name, out, tmpl, tmpl_suffix, __VA_ARGS__)
+#define JSON_OBJECT_GET_STRING_MEMBER_OR_RETURN(in, name, out, tmpl, tmpl_suffix, ...) \
+ JSON_OBJECT_GET_MEMBER_OR_RETURN ("a", string, in, name, out, tmpl, tmpl_suffix, __VA_ARGS__)
+
+#define JSON_ARRAY_GET_OBJECT_ELEMENT_OR_RETURN(in, index, out, tmpl, ...) \
+ out = json_array_get_object_element (in, index); \
+ if (out == NULL) { \
+ g_warning ("Value at `" #tmpl "` is not an object", __VA_ARGS__); \
+ return; \
+ }
+
+typedef struct _TimePair
+{
+ time_t start;
+ time_t end;
+} TimePair;
+
+/* The documented values for properties.weather.values.*.value.*.weather in the
+ * responses generated by the /gridpoints endpoint.
+ *
+ * See https://www.weather.gov/documentation/services-web-api#/default/gridpoint
+ */
+typedef enum
+{
+ NWS_WEATHER_NULL,
+
+ NWS_WEATHER_BLOWING_DUST,
+ NWS_WEATHER_BLOWING_SAND,
+ NWS_WEATHER_BLOWING_SNOW,
+ NWS_WEATHER_DRIZZLE,
+ NWS_WEATHER_FOG,
+ NWS_WEATHER_FREEZING_DRIZZLE,
+ NWS_WEATHER_FREEZING_FOG,
+ NWS_WEATHER_FREEZING_RAIN,
+ NWS_WEATHER_FREEZING_SPRAY,
+ NWS_WEATHER_FROST,
+ NWS_WEATHER_HAIL,
+ NWS_WEATHER_HAZE,
+ NWS_WEATHER_ICE_CRYSTALS,
+ NWS_WEATHER_ICE_FOG,
+ NWS_WEATHER_RAIN,
+ NWS_WEATHER_RAIN_SHOWERS,
+ NWS_WEATHER_SLEET,
+ NWS_WEATHER_SMOKE,
+ NWS_WEATHER_SNOW,
+ NWS_WEATHER_SNOW_SHOWERS,
+ NWS_WEATHER_THUNDERSTORMS,
+ NWS_WEATHER_VOLCANIC_ASH,
+ NWS_WEATHER_WATER_SPOUTS,
+
+ NWS_WEATHER_UNRECOGNIZED
+} NwsWeather;
+
+static NwsWeather
+parse_nws_weather (const gchar *str)
+{
+ if (str == NULL)
+ return NWS_WEATHER_NULL;
+
+ switch (str[0]) {
+ case 'b':
+ if (strncmp (str + 1, "lowing_", 7))
+ break;
+ switch (str[8]) {
+ case 'd':
+ if (strcmp (str + 9, "ust"))
+ break;
+ return NWS_WEATHER_BLOWING_DUST;
+ case 's':
+ switch (str[9]) {
+ case 'a':
+ if (strcmp (str + 10, "nd"))
+ break;
+ return NWS_WEATHER_BLOWING_SAND;
+ case 'n':
+ if (strcmp (str + 10, "ow"))
+ break;
+ return NWS_WEATHER_BLOWING_SNOW;
+ }
+ break;
+ }
+ break;
+ case 'd':
+ if (strcmp (str + 1, "rizzle"))
+ break;
+ return NWS_WEATHER_DRIZZLE;
+ case 'f':
+ switch (str[1]) {
+ case 'o':
+ if (strcmp (str + 2, "g"))
+ break;
+ return NWS_WEATHER_FOG;
+ case 'r':
+ switch (str[2]) {
+ case 'e':
+ if (strncmp (str + 3, "ezing_", 6))
+ break;
+ switch (str[9]) {
+ case 'd':
+ if (strcmp (str + 10, "rizzle"))
+ break;
+ return NWS_WEATHER_FREEZING_DRIZZLE;
+ case 'f':
+ if (strcmp (str + 10, "og"))
+ break;
+ return NWS_WEATHER_FREEZING_FOG;
+ case 'r':
+ if (strcmp (str + 10, "ain"))
+ break;
+ return NWS_WEATHER_FREEZING_RAIN;
+ case 's':
+ if (strcmp (str + 10, "pray"))
+ break;
+ return NWS_WEATHER_FREEZING_SPRAY;
+ }
+ break;
+ case 'o':
+ if (strcmp (str + 3, "st"))
+ break;
+ return NWS_WEATHER_FROST;
+ }
+ break;
+ }
+ break;
+ case 'h':
+ if (str[1] != 'a')
+ break;
+ switch (str[2]) {
+ case 'i':
+ if (strcmp (str + 3, "l"))
+ break;
+ return NWS_WEATHER_HAIL;
+ case 'z':
+ if (strcmp (str + 3, "e"))
+ break;
+ return NWS_WEATHER_HAZE;
+ }
+ break;
+ case 'i':
+ if (strncmp (str + 1, "ce_", 3))
+ break;
+ switch (str[4]) {
+ case 'c':
+ if (strcmp (str + 5, "rystals"))
+ break;
+ return NWS_WEATHER_ICE_CRYSTALS;
+ case 'f':
+ if (strcmp (str + 5, "og"))
+ break;
+ return NWS_WEATHER_ICE_FOG;
+ }
+ break;
+ case 'r':
+ if (strncmp (str + 1, "ain", 3))
+ break;
+ switch (str[4]) {
+ case '\0':
+ return NWS_WEATHER_RAIN;
+ case '_':
+ if (strcmp (str + 5, "showers"))
+ break;
+ return NWS_WEATHER_RAIN_SHOWERS;
+ }
+ break;
+ case 's':
+ switch (str[1]) {
+ case 'l':
+ if (strcmp (str + 2, "eet"))
+ break;
+ return NWS_WEATHER_SLEET;
+ case 'm':
+ if (strcmp (str + 2, "oke"))
+ break;
+ return NWS_WEATHER_SMOKE;
+ case 'n':
+ if (strncmp (str + 2, "ow", 2))
+ break;
+ switch (str[4]) {
+ case '\0':
+ return NWS_WEATHER_SNOW;
+ case '_':
+ if (strcmp (str + 5, "showers"))
+ break;
+ return NWS_WEATHER_SNOW_SHOWERS;
+ }
+ break;
+ }
+ break;
+ case 't':
+ if (strcmp (str + 1, "hunderstorms"))
+ break;
+ return NWS_WEATHER_THUNDERSTORMS;
+ case 'v':
+ if (strcmp (str + 1, "olcanic_ash"))
+ break;
+ return NWS_WEATHER_VOLCANIC_ASH;
+ case 'w':
+ if (strcmp (str + 1, "ater_spouts"))
+ break;
+ return NWS_WEATHER_WATER_SPOUTS;
+ }
+ return NWS_WEATHER_UNRECOGNIZED;
+}
+
+/**
+ * times_from_iso8601_interval:
+ *
+ * A very incomplete parser for the ISO 8601 format for time intervals like
+ * 2022-01-13T18:00:00Z/PT6H. Doesn't support a lot of the more advanced
+ * features of the standard. Replacing this with a function in some other
+ * library would be a great idea.
+ *
+ * Return value: a pair of time_t values representing the start and the end of
+ * the interval.
+ **/
+static TimePair
+times_from_iso8601_interval (const gchar *str)
+{
+ TimePair ret = { 0, 0 };
+ const gchar *sep;
+ gchar *date_part;
+ g_autoptr (GDateTime) dt_start = NULL;
+ g_autoptr (GDateTime) dt_end = NULL;
+
+ sep = strchr (str, '/');
+ if (sep == NULL)
+ return ret;
+
+ date_part = g_strndup (str, sep - str);
+ dt_start = g_date_time_new_from_iso8601 (date_part, NULL);
+ g_free (date_part);
+ ret.start = g_date_time_to_unix (dt_start);
+
+ if (*(sep + 1) == 'P') {
+ gint years = 0, months = 0, days = 0, hours = 0, minutes = 0, seconds = 0;
+ gboolean in_date = TRUE;
+ for (const gchar *ptr = sep + 2; *ptr != '\0';) {
+ if (*ptr == 'T') {
+ in_date = FALSE;
+ ptr++;
+ } else {
+ int num, advanced;
+ gchar field;
+ sscanf (ptr, "%d%c%n", &num, &field, &advanced);
+ if (in_date) {
+ switch (field) {
+ case 'Y':
+ years = num;
+ break;
+ case 'M':
+ months = num;
+ break;
+ case 'D':
+ days = num;
+ break;
+ default:
+ ret.end = ret.start;
+ return ret;
+ }
+ } else {
+ switch (field) {
+ case 'H':
+ hours = num;
+ break;
+ case 'M':
+ minutes = num;
+ break;
+ case 'S':
+ seconds = num;
+ break;
+ default:
+ ret.end = ret.start;
+ return ret;
+ }
+ }
+ ptr += advanced;
+ }
+ }
+ dt_end = g_date_time_add_full (dt_start, years, months, days, hours, minutes, (gdouble) seconds);
+ } else {
+ dt_end = g_date_time_new_from_iso8601 (sep + 1, NULL);
+ }
+
+ ret.end = g_date_time_to_unix (dt_end);
+ return ret;
+}
+
+static gboolean
+json_array_contains_string (JsonArray *arr, const gchar *str)
+{
+ guint len;
+ const gchar *str2;
+
+ len = json_array_get_length (arr);
+ for (guint i = 0; i < len; i++) {
+ str2 = json_array_get_string_element (arr, i);
+ if (str2 != NULL && strcmp (str, str2) == 0) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static SoupMessage *
+nws_new_request (GWeatherInfo *info, const gchar *url)
+{
+ SoupMessage *message;
+ SoupMessageHeaders *headers;
+
+ message = soup_message_new ("GET", url);
+ _gweather_info_begin_request (info, message);
+#if SOUP_CHECK_VERSION(2, 99, 2)
+ headers = soup_message_get_request_headers (message);
+#else
+ headers = message->request_headers;
+#endif
+ soup_message_headers_append (headers, "Accept", "application/geo+json");
+
+ return message;
+}
+
+typedef void (*ValueReader) (GWeatherInfo *, JsonNode *);
+
+static void
+read_interval_values (GSList *forecast_list,
+ JsonObject *obj,
+ gchar *property_name,
+ ValueReader (*select_read_value) (const gchar *),
+ void (*copy_value) (GWeatherInfo *, GWeatherInfo *))
+{
+ JsonObject *prop;
+ JsonArray *arr;
+ const gchar *uom;
+ const gchar *valid_time;
+ ValueReader read_value;
+ guint len;
+ JsonObject *datum;
+ TimePair range;
+ guint i = 0;
+ GWeatherInfo *prev = NULL;
+ GWeatherInfo *info;
+
+ JSON_OBJECT_GET_OBJECT_MEMBER_OR_RETURN (obj, property_name, prop, "%s", "", property_name)
+
+ JSON_OBJECT_GET_ARRAY_MEMBER_OR_RETURN (prop, "values", arr, "%s.values", "", property_name)
+
+ uom = NULL;
+ if (json_object_has_member (prop, "uom")) {
+ uom = json_object_get_string_member (prop, "uom");
+ }
+
+ read_value = (*select_read_value) (uom);
+ len = json_array_get_length (arr);
+ if (len > 0) {
+ JSON_ARRAY_GET_OBJECT_ELEMENT_OR_RETURN (arr, i, datum, "%s.values[%d]", property_name, i)
+
+ JSON_OBJECT_GET_STRING_MEMBER_OR_RETURN (datum, "validTime", valid_time, "%s.values[%d].validTime",
"", property_name, i)
+
+ range = times_from_iso8601_interval (valid_time);
+ for (GSList *slist = forecast_list; slist != NULL; slist = slist->next) {
+ info = slist->data;
+ while (info->current_time >= range.end) {
+ i++;
+ if (i >= len) {
+ return;
+ }
+ JSON_ARRAY_GET_OBJECT_ELEMENT_OR_RETURN (arr, i, datum, "%s.values[%d]", property_name, i)
+
+ JSON_OBJECT_GET_STRING_MEMBER_OR_RETURN (datum, "validTime", valid_time,
"%s.values[%d].validTime", "", property_name, i)
+
+ range = times_from_iso8601_interval (valid_time);
+ prev = NULL;
+ }
+ if (info->current_time >= range.start) {
+ if (prev == NULL) {
+ JSON_OBJECT_RETURN_UNLESS_MEMBER_EXISTS (datum, "value", "%s.values[%d].value", "",
property_name, i)
+ (*read_value) (info, json_object_get_member (datum, "value"));
+ } else {
+ (*copy_value) (info, prev);
+ }
+ prev = info;
+ }
+ }
+ }
+}
+
+static void
+read_temperature_f (GWeatherInfo *info, JsonNode *node)
+{
+ info->temp = json_node_get_double (node);
+}
+
+static void
+read_temperature_c (GWeatherInfo *info, JsonNode *node)
+{
+ info->temp = TEMP_C_TO_F (json_node_get_double (node));
+}
+
+static ValueReader
+select_read_temperature (const gchar *uom)
+{
+ if (uom != NULL && strcmp (uom, "wmoUnit:degC") == 0) {
+ return read_temperature_c;
+ }
+ return read_temperature_f;
+}
+
+static void
+copy_temperature (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->temp = prev->temp;
+}
+
+static void
+read_dew_f (GWeatherInfo *info, JsonNode *node)
+{
+ info->dew = json_node_get_double (node);
+}
+
+static void
+read_dew_c (GWeatherInfo *info, JsonNode *node)
+{
+ info->dew = TEMP_C_TO_F (json_node_get_double (node));
+}
+
+static ValueReader
+select_read_dew (const gchar *uom)
+{
+ if (uom != NULL && strcmp (uom, "wmoUnit:degC") == 0) {
+ return read_dew_c;
+ }
+ return read_dew_f;
+}
+
+static void
+copy_dew (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->dew = prev->dew;
+}
+
+static void
+read_weather (GWeatherInfo *info, JsonNode *node)
+{
+ GWeatherConditions conditions = { FALSE, GWEATHER_PHENOMENON_NONE, GWEATHER_QUALIFIER_NONE };
+ JsonArray *arr;
+ guint len;
+ JsonObject *obj;
+ NwsWeather weather;
+ const gchar *coverage;
+ const gchar *intensity;
+ GWeatherConditionQualifier intensity_qualifier = GWEATHER_QUALIFIER_NONE;
+ JsonArray *attributes;
+
+ if (!JSON_NODE_HOLDS_ARRAY (node)) {
+ g_warning ("Value is not an array");
+ return;
+ }
+ arr = json_node_get_array (node);
+ len = json_array_get_length (arr);
+
+ // Deeply imperfect, but: for the first element in the array that produces
+ // a coherent GWeatherConditions, use it. If there's another element in the
+ // array with broader coverage, higher intensity, or is just more dramatic
+ // (tornadoes!), too bad.
+ //
+ // A smarter approach might take such things into account, and attempt to
+ // report on the most overall significant weather phenomenon based on some
+ // prioritization of entries in this array, or perhaps merge them in some
+ // way.
+ for (guint i = 0; i < len; i++) {
+ JSON_ARRAY_GET_OBJECT_ELEMENT_OR_RETURN (arr, i, obj, "[%d]", i)
+
+ // Can be NULL; parse_nws_weather maps NULL to NWS_WEATHER_NULL
+ JSON_OBJECT_RETURN_UNLESS_MEMBER_EXISTS (obj, "weather", "[%d].weather", "", i)
+ weather = parse_nws_weather (json_object_get_string_member (obj, "weather"));
+
+ // NULL or one of: areas, brief, chance, definite, few, frequent,
+ // intermittent, isolated, likely, numerous, occasional, patchy,
+ // periods, scattered, slight_chance, widespread
+ JSON_OBJECT_RETURN_UNLESS_MEMBER_EXISTS (obj, "coverage", "[%d].coverage", "", i)
+ coverage = json_object_get_string_member (obj, "coverage");
+ if (coverage != NULL) {
+ // We don't want to use weather conditions if they come with a
+ // coverage keyword that indicates the conditions are less likely
+ // than not to come to pass. The values chosen to be excluded are
+ // taken from the table here:
+ // https://vlab.noaa.gov/web/mdl/weather-type-definitions
+ if (strcmp (coverage, "slight_chance") == 0 ||
+ strcmp (coverage, "chance") == 0 ||
+ strcmp (coverage, "isolated") == 0 ||
+ strcmp (coverage, "scattered") == 0) {
+ continue;
+ }
+ }
+
+ // NULL or one of: very_light, light, moderate, heavy
+ JSON_OBJECT_RETURN_UNLESS_MEMBER_EXISTS (obj, "intensity", "[%d].intensity", "", i)
+ intensity = json_object_get_string_member (obj, "intensity");
+ if (intensity != NULL) {
+ if (strcmp (intensity, "very_light") == 0 || strcmp (intensity, "light") == 0) {
+ intensity_qualifier = GWEATHER_QUALIFIER_LIGHT;
+ } else if (strcmp (intensity, "moderate") == 0) {
+ intensity_qualifier = GWEATHER_QUALIFIER_MODERATE;
+ } else if (strcmp (intensity, "heavy") == 0) {
+ intensity_qualifier = GWEATHER_QUALIFIER_HEAVY;
+ }
+ }
+
+ // Array of damaging_wind, dry_thunderstorms, flooding, gusty_wind, heavy_rain, large_hail,
small_hail, tornadoes
+ JSON_OBJECT_GET_ARRAY_MEMBER_OR_RETURN (obj, "attributes", attributes, "[%d].attributes", "", i)
+
+ switch (weather) {
+ case NWS_WEATHER_FREEZING_DRIZZLE:
+ case NWS_WEATHER_DRIZZLE:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_DRIZZLE;
+ if (weather == NWS_WEATHER_FREEZING_DRIZZLE) {
+ conditions.qualifier = GWEATHER_QUALIFIER_FREEZING;
+ } else if (intensity_qualifier != GWEATHER_QUALIFIER_NONE) {
+ conditions.qualifier = intensity_qualifier;
+ }
+ break;
+ case NWS_WEATHER_FREEZING_RAIN:
+ case NWS_WEATHER_RAIN:
+ case NWS_WEATHER_RAIN_SHOWERS:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_RAIN;
+ if (weather == NWS_WEATHER_FREEZING_RAIN) {
+ conditions.qualifier = GWEATHER_QUALIFIER_FREEZING;
+ } else if (weather == NWS_WEATHER_RAIN_SHOWERS) {
+ conditions.qualifier = GWEATHER_QUALIFIER_SHOWERS;
+ } else if (intensity_qualifier != GWEATHER_QUALIFIER_NONE) {
+ conditions.qualifier = intensity_qualifier;
+ }
+ break;
+ case NWS_WEATHER_BLOWING_SNOW:
+ case NWS_WEATHER_SNOW:
+ case NWS_WEATHER_SNOW_SHOWERS:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_SNOW;
+ if (weather == NWS_WEATHER_BLOWING_SNOW) {
+ conditions.qualifier = GWEATHER_QUALIFIER_BLOWING;
+ } else if (weather == NWS_WEATHER_SNOW_SHOWERS) {
+ conditions.qualifier = GWEATHER_QUALIFIER_SHOWERS;
+ } else if (intensity_qualifier != GWEATHER_QUALIFIER_NONE) {
+ conditions.qualifier = intensity_qualifier;
+ }
+ break;
+ case NWS_WEATHER_ICE_CRYSTALS:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_ICE_CRYSTALS;
+ break;
+ case NWS_WEATHER_SLEET:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_ICE_PELLETS;
+ if (intensity_qualifier != GWEATHER_QUALIFIER_NONE) {
+ conditions.qualifier = intensity_qualifier;
+ }
+ break;
+ case NWS_WEATHER_HAIL:
+ conditions.significant = TRUE;
+ if (json_array_contains_string (attributes, "small_hail")) {
+ conditions.phenomenon = GWEATHER_PHENOMENON_SMALL_HAIL;
+ } else {
+ conditions.phenomenon = GWEATHER_PHENOMENON_HAIL;
+ }
+ break;
+ case NWS_WEATHER_FOG:
+ case NWS_WEATHER_FREEZING_FOG:
+ case NWS_WEATHER_ICE_FOG:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_FOG;
+ if (weather != NWS_WEATHER_FOG) {
+ conditions.qualifier = GWEATHER_QUALIFIER_FREEZING;
+ } else if (strcmp (coverage, "areas") == 0) {
+ conditions.qualifier = GWEATHER_QUALIFIER_PARTIAL;
+ } else if (strcmp (coverage, "patchy") == 0) {
+ conditions.qualifier = GWEATHER_QUALIFIER_PATCHES;
+ }
+ break;
+ case NWS_WEATHER_SMOKE:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_SMOKE;
+ break;
+ case NWS_WEATHER_VOLCANIC_ASH:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_VOLCANIC_ASH;
+ break;
+ case NWS_WEATHER_BLOWING_SAND:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_SAND;
+ conditions.qualifier = GWEATHER_QUALIFIER_BLOWING;
+ break;
+ case NWS_WEATHER_HAZE:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_HAZE;
+ break;
+ case NWS_WEATHER_FREEZING_SPRAY:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_SPRAY;
+ conditions.qualifier = GWEATHER_QUALIFIER_FREEZING;
+ break;
+ case NWS_WEATHER_BLOWING_DUST:
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_DUST;
+ conditions.qualifier = GWEATHER_QUALIFIER_BLOWING;
+ break;
+ case NWS_WEATHER_THUNDERSTORMS:
+ conditions.significant = TRUE;
+ conditions.qualifier = GWEATHER_QUALIFIER_THUNDERSTORM;
+ break;
+ case NWS_WEATHER_NULL:
+ case NWS_WEATHER_FROST:
+ case NWS_WEATHER_WATER_SPOUTS:
+ case NWS_WEATHER_UNRECOGNIZED:
+ break;
+ }
+ if (conditions.phenomenon == GWEATHER_PHENOMENON_NONE) {
+ if (json_array_contains_string (attributes, "tornadoes")) {
+ conditions.significant = TRUE;
+ conditions.phenomenon = GWEATHER_PHENOMENON_TORNADO;
+ }
+ }
+
+ if (conditions.significant) {
+ break;
+ }
+ }
+ info->cond = conditions;
+}
+
+static ValueReader
+select_read_weather (const gchar *uom)
+{
+ return read_weather;
+}
+
+static void
+copy_weather (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->cond = prev->cond;
+}
+
+static void
+read_sky (GWeatherInfo *info, JsonNode *node)
+{
+ gdouble pct = json_node_get_double (node);
+ if (pct >= 0 && pct <= 100) {
+ if (pct < 12.5) {
+ info->sky = GWEATHER_SKY_CLEAR;
+ } else if (pct < 37.5) {
+ info->sky = GWEATHER_SKY_BROKEN;
+ } else if (pct < 62.5) {
+ info->sky = GWEATHER_SKY_SCATTERED;
+ } else if (pct < 87.5) {
+ info->sky = GWEATHER_SKY_FEW;
+ } else {
+ info->sky = GWEATHER_SKY_OVERCAST;
+ }
+ }
+}
+
+static ValueReader
+select_read_sky (const gchar *uom)
+{
+ return read_sky;
+}
+
+static void
+copy_sky (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->sky = prev->sky;
+}
+
+static void
+read_winddir (GWeatherInfo *info, JsonNode *node)
+{
+ gdouble wind = json_node_get_double (node);
+ if (wind >= 0 && wind < 360) {
+ if (wind >= 348.75) {
+ info->wind = GWEATHER_WIND_N;
+ } else {
+ info->wind = GWEATHER_WIND_N + (int) ((wind + 11.25) / 22.5);
+ }
+ }
+}
+
+static ValueReader
+select_read_winddir (const gchar *uom)
+{
+ return read_winddir;
+}
+
+static void
+copy_winddir (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->wind = prev->wind;
+}
+
+static void
+read_windspeed (GWeatherInfo *info, JsonNode *node)
+{
+ gdouble windspeed_kph = json_node_get_double (node);
+ info->windspeed = WINDSPEED_MS_TO_KNOTS (windspeed_kph / 3.6);
+}
+
+static ValueReader
+select_read_windspeed (const gchar *uom)
+{
+ return read_windspeed;
+}
+
+static void
+copy_windspeed (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->windspeed = prev->windspeed;
+}
+
+static void
+read_humidity (GWeatherInfo *info, JsonNode *node)
+{
+ info->humidity = json_node_get_double (node);
+ info->hasHumidity = TRUE;
+}
+
+static ValueReader
+select_read_humidity (const gchar *uom)
+{
+ return read_humidity;
+}
+
+static void
+copy_humidity (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->humidity = prev->humidity;
+ info->hasHumidity = prev->hasHumidity;
+}
+
+static void
+read_visibility_m (GWeatherInfo *info, JsonNode *node)
+{
+ info->visibility = json_node_get_double (node) / VISIBILITY_SM_TO_M (1.);
+}
+
+static void
+read_visibility_sm (GWeatherInfo *info, JsonNode *node)
+{
+ info->visibility = json_node_get_double (node);
+}
+
+static ValueReader
+select_read_visibility (const gchar *uom)
+{
+ if (strcmp (uom, "wmoUnit:m") == 0) {
+ return read_visibility_m;
+ }
+ return read_visibility_sm;
+}
+
+static void
+copy_visibility (GWeatherInfo *info, GWeatherInfo *prev)
+{
+ info->visibility = prev->visibility;
+}
+
+static void
+nws_finish_forecast_common (GWeatherInfo *info,
+ const char *content,
+ gsize body_size)
+{
+ WeatherLocation *loc;
+ JsonNode *root;
+ JsonObject *obj;
+ const char *valid_times;
+ TimePair range;
+ GWeatherInfo *info2;
+ guint num_forecasts = 0;
+
+ loc = &info->location;
+ g_debug ("nws gridpoint data for %lf, %lf", loc->latitude, loc->longitude);
+ g_debug ("%s", content);
+
+ g_autoptr (JsonParser) parser = json_parser_new ();
+ g_autoptr (GError) error = NULL;
+ if (!json_parser_load_from_data (parser, content, body_size, &error)) {
+ g_warning ("Failed to parse response from weather.gov: %s", error->message);
+ return;
+ }
+
+ root = json_parser_get_root (parser);
+ if (!JSON_NODE_HOLDS_OBJECT (root)) {
+ g_warning ("Response from weather.gov is not an object: %s", content);
+ return;
+ }
+ obj = json_node_get_object (root);
+
+ JSON_OBJECT_GET_OBJECT_MEMBER_OR_RETURN (obj, "properties", obj, "properties", ": %s", content)
+
+ /* The gridpoints API uses an encoding in which each weather variable
+ * is an array of values tagged with time *intervals*. So far, all of
+ * the intervals I've seen have been multiples of an hour, although
+ * this isn't guaranteed by the API specification. The intervals are
+ * *not* uniform, and different weather variables are often broken into
+ * different time intervals (based, perhaps, on whether there is an
+ * actual change forecasted from one hour to the next).
+ *
+ * So, to decode this, we create one forecast info object for every
+ * hour in the overall response's validTimes range, and then repeatedly
+ * iterate over the list of infos, populating the info object one
+ * variable at a time.
+ */
+
+ JSON_OBJECT_GET_STRING_MEMBER_OR_RETURN (obj, "validTimes", valid_times, "properties.validTimes", ":
%s", content)
+
+ range = times_from_iso8601_interval (valid_times);
+ // POSIX time doesn't care about leap seconds, neither does GLib, so
+ // whatever, an hour is always 3600 seconds.
+ for (time_t t = range.start; t < range.end; t += 3600) {
+ info2 = _gweather_info_new_clone (info);
+ info2->valid = TRUE;
+ info2->current_time = info2->update = t;
+ info->forecast_list = g_slist_prepend (info->forecast_list, info2);
+ num_forecasts++;
+ }
+ info->forecast_list = g_slist_reverse (info->forecast_list);
+
+ read_interval_values (info->forecast_list, obj, "weather", select_read_weather, copy_weather);
+ read_interval_values (info->forecast_list, obj, "temperature", select_read_temperature,
copy_temperature);
+ read_interval_values (info->forecast_list, obj, "dewpoint", select_read_dew, copy_dew);
+ read_interval_values (info->forecast_list, obj, "skyCover", select_read_sky, copy_sky);
+ read_interval_values (info->forecast_list, obj, "windDirection", select_read_winddir, copy_winddir);
+ read_interval_values (info->forecast_list, obj, "windSpeed", select_read_windspeed, copy_windspeed);
+ read_interval_values (info->forecast_list, obj, "relativeHumidity", select_read_humidity, copy_humidity);
+ read_interval_values (info->forecast_list, obj, "visibility", select_read_visibility, copy_visibility);
+
+ g_debug ("nws generated %d forecast infos", num_forecasts);
+ if (!info->valid)
+ info->valid = (num_forecasts > 0);
+}
+
+#if SOUP_CHECK_VERSION(2, 99, 2)
+static void
+nws_finish_forecast (GObject *source,
+ GAsyncResult *result,
+ gpointer data)
+{
+ GWeatherInfo *info;
+ SoupSession *session = SOUP_SESSION (source);
+ SoupMessage *msg = soup_session_get_async_result_message (session, result);
+ GBytes *body;
+ GError *error = NULL;
+ const char *content;
+ gsize body_size;
+
+ body = soup_session_send_and_read_finish (session, result, &error);
+
+ if (!body) {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_debug ("Failed to get weather.gov gridpoint data: %s", error->message);
+ return;
+ }
+ g_message ("Failed to get weather.gov gridpoint data: %s", error->message);
+ g_clear_error (&error);
+ _gweather_info_request_done (data, msg);
+ return;
+ } else if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) {
+ g_message ("Failed to get weather.gov gridpoint data: [status: %d] %s",
+ soup_message_get_status (msg),
+ soup_message_get_reason_phrase (msg));
+ _gweather_info_request_done (data, msg);
+ return;
+ }
+
+ content = g_bytes_get_data (body, &body_size);
+
+ info = data;
+
+ nws_finish_forecast_common (info, content, body_size);
+
+ g_bytes_unref (body);
+ _gweather_info_request_done (info, msg);
+}
+#else
+static void
+nws_finish_forecast (SoupSession *session,
+ SoupMessage *msg,
+ gpointer user_data)
+{
+ GWeatherInfo *info;
+
+ info = user_data;
+
+ if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
+ if (msg->status_code == SOUP_STATUS_CANCELLED) {
+ g_debug ("Failed to get weather.gov gridpoint data: %s",
+ msg->reason_phrase);
+ return;
+ }
+ g_debug ("Failed to get weather.gov gridpoint data: [status: %d]: %s",
+ msg->status_code,
+ msg->reason_phrase);
+ } else {
+ nws_finish_forecast_common (info, msg->response_body->data, msg->response_body->length);
+ }
+
+ _gweather_info_request_done (info, msg);
+}
+#endif
+
+static void
+nws_finish_new_common (GWeatherInfo *info,
+ const char *content,
+ gsize body_size)
+{
+ WeatherLocation *loc;
+ JsonNode *root;
+ JsonObject *obj;
+ const gchar *url;
+ SoupMessage *msg;
+
+ loc = &info->location;
+ g_debug ("nws data for %lf, %lf", loc->latitude, loc->longitude);
+ g_debug ("%s", content);
+
+ g_autoptr (JsonParser) parser = json_parser_new ();
+ g_autoptr (GError) error = NULL;
+ if (!json_parser_load_from_data (parser, content, body_size, &error)) {
+ g_warning ("Failed to parse response from weather.gov: %s", error->message);
+ return;
+ }
+
+ root = json_parser_get_root (parser);
+ if (!JSON_NODE_HOLDS_OBJECT (root)) {
+ g_warning ("Response from weather.gov is not an object: %s", content);
+ return;
+ }
+ obj = json_node_get_object (root);
+
+ JSON_OBJECT_GET_OBJECT_MEMBER_OR_RETURN (obj, "properties", obj, "properties", ": %s", content)
+
+ // The endpoint at forecastGridData offers a superset of the
+ // information available from the other endpoints, albeit in a more
+ // complex format. Perhaps at some future date, the friendlier
+ // forecastHourly endpoint will be sufficient.
+ JSON_OBJECT_GET_STRING_MEMBER_OR_RETURN (obj, "forecastGridData", url, "properties.forecastGridData", ":
%s", content)
+
+ msg = nws_new_request (info, url);
+ _gweather_info_queue_request (info, msg, nws_finish_forecast);
+}
+
+#if SOUP_CHECK_VERSION(2, 99, 2)
+static void
+nws_finish_new (GObject *source,
+ GAsyncResult *result,
+ gpointer data)
+{
+ GWeatherInfo *info;
+ SoupSession *session = SOUP_SESSION (source);
+ SoupMessage *msg = soup_session_get_async_result_message (session, result);
+ GBytes *body;
+ GError *error = NULL;
+ const char *content;
+ gsize body_size;
+
+ body = soup_session_send_and_read_finish (session, result, &error);
+
+ if (!body) {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_debug ("Failed to get weather.gov point data: %s", error->message);
+ return;
+ }
+ g_message ("Failed to get weather.gov point data: %s", error->message);
+ g_clear_error (&error);
+ _gweather_info_request_done (data, msg);
+ return;
+ } else if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) {
+ g_message ("Failed to get weather.gov point data: [status: %d] %s",
+ soup_message_get_status (msg),
+ soup_message_get_reason_phrase (msg));
+ _gweather_info_request_done (data, msg);
+ return;
+ }
+
+ content = g_bytes_get_data (body, &body_size);
+
+ info = data;
+
+ nws_finish_new_common (info, content, body_size);
+
+ g_bytes_unref (body);
+ _gweather_info_request_done (info, msg);
+}
+#else
+static void
+nws_finish_new (SoupSession *session,
+ SoupMessage *msg,
+ gpointer user_data)
+{
+ GWeatherInfo *info;
+
+ info = user_data;
+
+ if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
+ if (msg->status_code == SOUP_STATUS_CANCELLED) {
+ g_debug ("Failed to get weather.gov point data: %s",
+ msg->reason_phrase);
+ return;
+ }
+ g_debug ("Failed to get weather.gov point data: [status: %d]: %s",
+ msg->status_code,
+ msg->reason_phrase);
+ } else {
+ nws_finish_new_common (info, msg->response_body->data, msg->response_body->length);
+ }
+
+ _gweather_info_request_done (info, msg);
+}
+#endif
+
+gboolean
+nws_start_open (GWeatherInfo *info)
+{
+ gchar *url;
+ SoupMessage *message;
+ WeatherLocation *loc;
+ g_autofree gchar *latstr = NULL;
+ g_autofree gchar *lonstr = NULL;
+
+ loc = &info->location;
+
+ if (!loc->latlon_valid)
+ return FALSE;
+
+ /* see the description here: https://www.weather.gov/documentation/services-web-api */
+
+ latstr = _radians_to_degrees_str (loc->latitude);
+ lonstr = _radians_to_degrees_str (loc->longitude);
+
+ url = g_strdup_printf ("https://api.weather.gov/points/%s%%2C%s", latstr, lonstr);
+ g_debug ("nws_start_open, requesting: %s", url);
+
+ message = nws_new_request (info, url);
+ _gweather_info_queue_request (info, message, nws_finish_new);
+
+ g_free (url);
+
+ return TRUE;
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]