/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */ /* weather-metar.c - Weather server functions (METAR) * * 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 * . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #define MATEWEATHER_I_KNOW_THIS_IS_UNSTABLE #include "weather.h" #include "weather-priv.h" enum { TIME_RE, WIND_RE, VIS_RE, COND_RE, CLOUD_RE, TEMP_RE, PRES_RE, RE_NUM }; /* Return time of weather report as secs since epoch UTC */ static time_t make_time (gint utcDate, gint utcHour, gint utcMin) { const time_t now = time (NULL); struct tm tm; localtime_r (&now, &tm); /* If last reading took place just before midnight UTC on the * first, adjust the date downward to allow for the month * change-over. This ASSUMES that the reading won't be more than * 24 hrs old! */ if ((utcDate > tm.tm_mday) && (tm.tm_mday == 1)) { tm.tm_mday = 0; /* mktime knows this is the last day of the previous * month. */ } else { tm.tm_mday = utcDate; } tm.tm_hour = utcHour; tm.tm_min = utcMin; tm.tm_sec = 0; /* mktime() assumes value is local, not UTC. Use tm_gmtoff to compensate */ #ifdef HAVE_TM_TM_GMOFF return tm.tm_gmtoff + mktime (&tm); #elif defined HAVE_TIMEZONE return timezone + mktime (&tm); #endif } static void metar_tok_time (gchar *tokp, WeatherInfo *info) { gint day, hr, min; sscanf (tokp, "%2u%2u%2u", &day, &hr, &min); info->update = make_time (day, hr, min); } static void metar_tok_wind (gchar *tokp, WeatherInfo *info) { gchar sdir[4], sspd[4], sgust[4]; gint dir, spd = -1; gchar *gustp; size_t glen; strncpy (sdir, tokp, 3); sdir[3] = 0; dir = (!strcmp (sdir, "VRB")) ? -1 : atoi (sdir); memset (sspd, 0, sizeof (sspd)); glen = strspn (tokp + 3, CONST_DIGITS); strncpy (sspd, tokp + 3, glen); spd = atoi (sspd); tokp += glen + 3; gustp = strchr (tokp, 'G'); if (gustp) { memset (sgust, 0, sizeof (sgust)); glen = strspn (gustp + 1, CONST_DIGITS); strncpy (sgust, gustp + 1, glen); tokp = gustp + 1 + glen; } if (!strcmp (tokp, "MPS")) info->windspeed = WINDSPEED_MS_TO_KNOTS ((WeatherWindSpeed)spd); else info->windspeed = (WeatherWindSpeed)spd; if ((349 <= dir) || (dir <= 11)) info->wind = WIND_N; else if ((12 <= dir) && (dir <= 33)) info->wind = WIND_NNE; else if ((34 <= dir) && (dir <= 56)) info->wind = WIND_NE; else if ((57 <= dir) && (dir <= 78)) info->wind = WIND_ENE; else if ((79 <= dir) && (dir <= 101)) info->wind = WIND_E; else if ((102 <= dir) && (dir <= 123)) info->wind = WIND_ESE; else if ((124 <= dir) && (dir <= 146)) info->wind = WIND_SE; else if ((147 <= dir) && (dir <= 168)) info->wind = WIND_SSE; else if ((169 <= dir) && (dir <= 191)) info->wind = WIND_S; else if ((192 <= dir) && (dir <= 213)) info->wind = WIND_SSW; else if ((214 <= dir) && (dir <= 236)) info->wind = WIND_SW; else if ((237 <= dir) && (dir <= 258)) info->wind = WIND_WSW; else if ((259 <= dir) && (dir <= 281)) info->wind = WIND_W; else if ((282 <= dir) && (dir <= 303)) info->wind = WIND_WNW; else if ((304 <= dir) && (dir <= 326)) info->wind = WIND_NW; else if ((327 <= dir) && (dir <= 348)) info->wind = WIND_NNW; } static void metar_tok_vis (gchar *tokp, WeatherInfo *info) { gchar *pfrac, *pend, *psp; gchar sval[6]; gint num, den, val; memset (sval, 0, sizeof (sval)); if (!strcmp (tokp,"CAVOK")) { // "Ceiling And Visibility OK": visibility >= 10 KM info->visibility=10000. / VISIBILITY_SM_TO_M (1.); info->sky = SKY_CLEAR; } else if (0 != (pend = strstr (tokp, "SM"))) { // US observation: field ends with "SM" pfrac = strchr (tokp, '/'); if (pfrac) { if (*tokp == 'M') { info->visibility = 0.001; } else { num = (*(pfrac - 1) - '0'); strncpy (sval, pfrac + 1, pend - pfrac - 1); den = atoi (sval); info->visibility = ((WeatherVisibility)num / ((WeatherVisibility)den)); psp = strchr (tokp, ' '); if (psp) { *psp = '\0'; val = atoi (tokp); info->visibility += (WeatherVisibility)val; } } } else { strncpy (sval, tokp, pend - tokp); val = atoi (sval); info->visibility = (WeatherVisibility)val; } } else { // International observation: NNNN(DD NNNNDD)? // For now: use only the minimum visibility and ignore its direction strncpy (sval, tokp, strspn (tokp, CONST_DIGITS)); val = atoi (sval); info->visibility = (WeatherVisibility)val / VISIBILITY_SM_TO_M (1.); } } static void metar_tok_cloud (gchar *tokp, WeatherInfo *info) { gchar stype[4], salt[4]; strncpy (stype, tokp, 3); stype[3] = 0; if (strlen (tokp) == 6) { strncpy (salt, tokp + 3, 3); salt[3] = 0; } if (!strcmp (stype, "CLR")) { info->sky = SKY_CLEAR; } else if (!strcmp (stype, "SKC")) { info->sky = SKY_CLEAR; } else if (!strcmp (stype, "NSC")) { info->sky = SKY_CLEAR; } else if (!strcmp (stype, "BKN")) { info->sky = SKY_BROKEN; } else if (!strcmp (stype, "SCT")) { info->sky = SKY_SCATTERED; } else if (!strcmp (stype, "FEW")) { info->sky = SKY_FEW; } else if (!strcmp (stype, "OVC")) { info->sky = SKY_OVERCAST; } } static void metar_tok_pres (gchar *tokp, WeatherInfo *info) { if (*tokp == 'A') { gchar sintg[3], sfract[3]; gint intg, fract; strncpy (sintg, tokp + 1, 2); sintg[2] = 0; intg = atoi (sintg); strncpy (sfract, tokp + 3, 2); sfract[2] = 0; fract = atoi (sfract); info->pressure = (WeatherPressure)intg + (((WeatherPressure)fract)/100.0); } else { /* *tokp == 'Q' */ gchar spres[5]; gint pres; strncpy (spres, tokp + 1, 4); spres[4] = 0; pres = atoi (spres); info->pressure = PRESSURE_MBAR_TO_INCH ((WeatherPressure)pres); } } static void metar_tok_temp (gchar *tokp, WeatherInfo *info) { gchar *ptemp, *pdew, *psep; psep = strchr (tokp, '/'); *psep = 0; ptemp = tokp; pdew = psep + 1; info->temp = (*ptemp == 'M') ? TEMP_C_TO_F (-atoi (ptemp + 1)) : TEMP_C_TO_F (atoi (ptemp)); if (*pdew) { info->dew = (*pdew == 'M') ? TEMP_C_TO_F (-atoi (pdew + 1)) : TEMP_C_TO_F (atoi (pdew)); } else { info->dew = -1000.0; } } static void metar_tok_cond (gchar *tokp, WeatherInfo *info) { gchar squal[3], sphen[4]; gchar *pphen; if ((strlen (tokp) > 3) && ((*tokp == '+') || (*tokp == '-'))) ++tokp; /* FIX */ if ((*tokp == '+') || (*tokp == '-')) pphen = tokp + 1; else if (strlen (tokp) < 4) pphen = tokp; else pphen = tokp + 2; memset (squal, 0, sizeof (squal)); strncpy (squal, tokp, pphen - tokp); squal[pphen - tokp] = 0; memset (sphen, 0, sizeof (sphen)); strncpy (sphen, pphen, sizeof (sphen)); sphen[sizeof (sphen)-1] = '\0'; /* Defaults */ info->cond.qualifier = QUALIFIER_NONE; info->cond.phenomenon = PHENOMENON_NONE; info->cond.significant = FALSE; if (!strcmp (squal, "")) { info->cond.qualifier = QUALIFIER_MODERATE; } else if (!strcmp (squal, "-")) { info->cond.qualifier = QUALIFIER_LIGHT; } else if (!strcmp (squal, "+")) { info->cond.qualifier = QUALIFIER_HEAVY; } else if (!strcmp (squal, "VC")) { info->cond.qualifier = QUALIFIER_VICINITY; } else if (!strcmp (squal, "MI")) { info->cond.qualifier = QUALIFIER_SHALLOW; } else if (!strcmp (squal, "BC")) { info->cond.qualifier = QUALIFIER_PATCHES; } else if (!strcmp (squal, "PR")) { info->cond.qualifier = QUALIFIER_PARTIAL; } else if (!strcmp (squal, "TS")) { info->cond.qualifier = QUALIFIER_THUNDERSTORM; } else if (!strcmp (squal, "BL")) { info->cond.qualifier = QUALIFIER_BLOWING; } else if (!strcmp (squal, "SH")) { info->cond.qualifier = QUALIFIER_SHOWERS; } else if (!strcmp (squal, "DR")) { info->cond.qualifier = QUALIFIER_DRIFTING; } else if (!strcmp (squal, "FZ")) { info->cond.qualifier = QUALIFIER_FREEZING; } else { return; } if (!strcmp (sphen, "DZ")) { info->cond.phenomenon = PHENOMENON_DRIZZLE; } else if (!strcmp (sphen, "RA")) { info->cond.phenomenon = PHENOMENON_RAIN; } else if (!strcmp (sphen, "SN")) { info->cond.phenomenon = PHENOMENON_SNOW; } else if (!strcmp (sphen, "SG")) { info->cond.phenomenon = PHENOMENON_SNOW_GRAINS; } else if (!strcmp (sphen, "IC")) { info->cond.phenomenon = PHENOMENON_ICE_CRYSTALS; } else if (!strcmp (sphen, "PE")) { info->cond.phenomenon = PHENOMENON_ICE_PELLETS; } else if (!strcmp (sphen, "GR")) { info->cond.phenomenon = PHENOMENON_HAIL; } else if (!strcmp (sphen, "GS")) { info->cond.phenomenon = PHENOMENON_SMALL_HAIL; } else if (!strcmp (sphen, "UP")) { info->cond.phenomenon = PHENOMENON_UNKNOWN_PRECIPITATION; } else if (!strcmp (sphen, "BR")) { info->cond.phenomenon = PHENOMENON_MIST; } else if (!strcmp (sphen, "FG")) { info->cond.phenomenon = PHENOMENON_FOG; } else if (!strcmp (sphen, "FU")) { info->cond.phenomenon = PHENOMENON_SMOKE; } else if (!strcmp (sphen, "VA")) { info->cond.phenomenon = PHENOMENON_VOLCANIC_ASH; } else if (!strcmp (sphen, "SA")) { info->cond.phenomenon = PHENOMENON_SAND; } else if (!strcmp (sphen, "HZ")) { info->cond.phenomenon = PHENOMENON_HAZE; } else if (!strcmp (sphen, "PY")) { info->cond.phenomenon = PHENOMENON_SPRAY; } else if (!strcmp (sphen, "DU")) { info->cond.phenomenon = PHENOMENON_DUST; } else if (!strcmp (sphen, "SQ")) { info->cond.phenomenon = PHENOMENON_SQUALL; } else if (!strcmp (sphen, "SS")) { info->cond.phenomenon = PHENOMENON_SANDSTORM; } else if (!strcmp (sphen, "DS")) { info->cond.phenomenon = PHENOMENON_DUSTSTORM; } else if (!strcmp (sphen, "PO")) { info->cond.phenomenon = PHENOMENON_DUST_WHIRLS; } else if (!strcmp (sphen, "+FC")) { info->cond.phenomenon = PHENOMENON_TORNADO; } else if (!strcmp (sphen, "FC")) { info->cond.phenomenon = PHENOMENON_FUNNEL_CLOUD; } else { return; } if ((info->cond.qualifier != QUALIFIER_NONE) || (info->cond.phenomenon != PHENOMENON_NONE)) info->cond.significant = TRUE; } #define TIME_RE_STR "([0-9]{6})Z" #define WIND_RE_STR "(([0-9]{3})|VRB)([0-9]?[0-9]{2})(G[0-9]?[0-9]{2})?(KT|MPS)" #define VIS_RE_STR "((([0-9]?[0-9])|(M?([12] )?([1357]/1?[0-9])))SM)|" \ "([0-9]{4}(N|NE|E|SE|S|SW|W|NW( [0-9]{4}(N|NE|E|SE|S|SW|W|NW))?)?)|" \ "CAVOK" #define COND_RE_STR "(-|\\+)?(VC|MI|BC|PR|TS|BL|SH|DR|FZ)?(DZ|RA|SN|SG|IC|PE|GR|GS|UP|BR|FG|FU|VA|SA|HZ|PY|DU|SQ|SS|DS|PO|\\+?FC)" #define CLOUD_RE_STR "((CLR|BKN|SCT|FEW|OVC|SKC|NSC)([0-9]{3}|///)?(CB|TCU|///)?)" #define TEMP_RE_STR "(M?[0-9][0-9])/(M?(//|[0-9][0-9])?)" #define PRES_RE_STR "(A|Q)([0-9]{4})" /* POSIX regular expressions do not allow us to express "match whole words * only" in a simple way, so we have to wrap them all into * (^| )(...regex...)( |$) */ #define RE_PREFIX "(^| )(" #define RE_SUFFIX ")( |$)" static regex_t metar_re[RE_NUM]; static void (*metar_f[RE_NUM]) (gchar *tokp, WeatherInfo *info); static void metar_init_re (void) { static gboolean initialized = FALSE; if (initialized) return; initialized = TRUE; regcomp (&metar_re[TIME_RE], RE_PREFIX TIME_RE_STR RE_SUFFIX, REG_EXTENDED); regcomp (&metar_re[WIND_RE], RE_PREFIX WIND_RE_STR RE_SUFFIX, REG_EXTENDED); regcomp (&metar_re[VIS_RE], RE_PREFIX VIS_RE_STR RE_SUFFIX, REG_EXTENDED); regcomp (&metar_re[COND_RE], RE_PREFIX COND_RE_STR RE_SUFFIX, REG_EXTENDED); regcomp (&metar_re[CLOUD_RE], RE_PREFIX CLOUD_RE_STR RE_SUFFIX, REG_EXTENDED); regcomp (&metar_re[TEMP_RE], RE_PREFIX TEMP_RE_STR RE_SUFFIX, REG_EXTENDED); regcomp (&metar_re[PRES_RE], RE_PREFIX PRES_RE_STR RE_SUFFIX, REG_EXTENDED); metar_f[TIME_RE] = metar_tok_time; metar_f[WIND_RE] = metar_tok_wind; metar_f[VIS_RE] = metar_tok_vis; metar_f[COND_RE] = metar_tok_cond; metar_f[CLOUD_RE] = metar_tok_cloud; metar_f[TEMP_RE] = metar_tok_temp; metar_f[PRES_RE] = metar_tok_pres; } gboolean metar_parse (gchar *metar, WeatherInfo *info) { gchar *p; //gchar *rmk; gint i, i2; regmatch_t rm, rm2; gchar *tokp; g_return_val_if_fail (info != NULL, FALSE); g_return_val_if_fail (metar != NULL, FALSE); metar_init_re (); /* * Force parsing to end at "RMK" field. This prevents a subtle * problem when info within the remark happens to match an earlier state * and, as a result, throws off all the remaining expression */ if (0 != (p = strstr (metar, " RMK "))) { *p = '\0'; //rmk = p + 5; // uncomment this if RMK data becomes useful } p = metar; i = TIME_RE; while (*p) { i2 = RE_NUM; rm2.rm_so = strlen (p); rm2.rm_eo = rm2.rm_so; for (i = 0; i < RE_NUM && rm2.rm_so > 0; i++) { if (0 == regexec (&metar_re[i], p, 1, &rm, 0) && rm.rm_so < rm2.rm_so) { i2 = i; /* Skip leading and trailing space characters, if present. (the regular expressions include those characters to only get matches limited to whole words). */ if (p[rm.rm_so] == ' ') rm.rm_so++; if (p[rm.rm_eo - 1] == ' ') rm.rm_eo--; rm2.rm_so = rm.rm_so; rm2.rm_eo = rm.rm_eo; } } if (i2 != RE_NUM) { tokp = g_strndup (p + rm2.rm_so, rm2.rm_eo - rm2.rm_so); metar_f[i2] (tokp, info); g_free (tokp); } p += rm2.rm_eo; p += strspn (p, " "); } return TRUE; } static void metar_finish (SoupSession *session, SoupMessage *msg, gpointer data) { WeatherInfo *info = (WeatherInfo *)data; WeatherLocation *loc; const gchar *p, *endtag; gchar *searchkey, *metar; gboolean success = FALSE; g_return_if_fail (info != NULL); if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) { if (SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code)) info->network_error = TRUE; else { /* Translators: %d is an error code, and %s the error string */ g_warning (_("Failed to get METAR data: %d %s.\n"), msg->status_code, msg->reason_phrase); } request_done (info, FALSE); return; } loc = info->location; searchkey = g_strdup_printf ("%s", loc->code); p = strstr (msg->response_body->data, searchkey); g_free (searchkey); if (p) { p += WEATHER_LOCATION_CODE_LEN + 11; endtag = strstr (p, ""); if (endtag) metar = g_strndup (p, endtag - p); else metar = g_strdup (p); success = metar_parse (metar, info); g_free (metar); } else if (!strstr (msg->response_body->data, "aviationweather.gov")) { /* The response doesn't even seem to have come from NOAA... * most likely it is a wifi hotspot login page. Call that a * network error. */ info->network_error = TRUE; } info->valid = success; request_done (info, TRUE); } /* Read current conditions and fill in info structure */ void metar_start_open (WeatherInfo *info) { WeatherLocation *loc; SoupMessage *msg; g_return_if_fail (info != NULL); info->valid = info->network_error = FALSE; loc = info->location; if (loc == NULL) { g_warning (_("WeatherInfo missing location")); return; } msg = soup_form_request_new ( "GET", "https://www.aviationweather.gov/adds/dataserver_current/httpparam", "dataSource", "metars", "requestType", "retrieve", "format", "xml", "hoursBeforeNow", "3", "mostRecent", "true", "fields", "raw_text", "stationString", loc->code, NULL); soup_session_queue_message (info->session, msg, metar_finish, info); info->requests_pending++; }