/******************************************************************************* * File: localtemp.cpp * * Version: 0.4 * * Date: 20260809 * * Author: Gaspar Fernández (helyo@totaki.com) * * * * Copyright (C) 2008 Gaspar Fernández * * * * 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 . * * * ******************************************************************************** * Description: * It is part of a Window Maker dockapp called dwgo. This class reads weather * of a selected location from National Weather Service (http://weather.noaa.gov/) * and parses some information to display it formatted on screen. * I know it parses unnecesary information, but I'm planning to update dwgo * with some improvements that use all this information. * * Change History: * Date (D.M.Y) Author Modification * 08.05.2008 Gaspar Fernández Initial release * 31.10.2010 Gaspar Fernández Bug Corrections * 09.08.2026 Koudai Kudo Ported to JMA * ********************************************************************************/ #include "localtemp.h" #include "MySock.h" #include "dwgo.h" // Theme numbers using namespace std; /************************************************************* * Constructur: localtemp * ************************************************************* * Description: * * Initializes variables * * * * Input: * * char *metar - ICAO name locator (4 chars) * * char location_name - Amedas station code (5 digits) * * * * Change History: * * Date Author Modification * * * *************************************************************/ localtemp::localtemp(char *metar, char *location_name) { this->metar=metar; this->location_name=location_name; this->celsius=0; this->fahrenheit=0; this->loaded=false; this->theme=DEFAULT_THEME; } /************************************************************* * Method: set_temp * ************************************************************* * Description: * * Extract temperature information. In Celsius and * * Fahrenheit * * * * Input: * * string tmp_str - Temporary string containing * * temperature in Celsius and Fahrenheit * * * * Change History: * * Date Author Modification * * * *************************************************************/ void localtemp::set_temp(double temp_c) { this->celsius=(int)(temp_c+0.5); this->fahrenheit=(int)(temp_c*9.0/5.0+32.0+0.5); } /************************************************************* * Method: set_humidity * ************************************************************* * Description: * * Extract humidity information. * * * * Input: * * int hum - Relative humidity, from the "humidity" field * * of the Amedas JSON * * * * Change History: * * Date Author Modification * * * *************************************************************/ void localtemp::set_humidity(int hum) { this->humidity=hum; } /************************************************************* * Method: get_ob_info * ************************************************************* * Description: * * Works out which theme to use from the 10 minute * * precipitation figure and current temperature, since * * Amedas doesn't report general sky conditions the way * * METAR does. * * * * Input: * * double precip10m - 10 minute precipitation, in mm, as * * read from the "precipitation10m" field of the Amedas * * JSON (0 or negative means no precipitation reported) * * * * Change History: * * Date Author Modification * * * *************************************************************/ void localtemp::get_ob_info(double precip10m) { mInfo.CB=false; mInfo.fog=undef_fog; // Not reported by Amedas if (precip10m>0.0) { if (this->celsius<=2) mInfo.rain=snow; else mInfo.rain=rain; mInfo.sky=undef_sky; } else { mInfo.rain=undef_rain; mInfo.sky=clear; } // The most important for the display theme is rain, then fog and then sky conditions if (mInfo.rain!=undef_rain) { switch (mInfo.rain) { case rain: theme=RAINY_THEME; break; case snow: theme=SNOWY_THEME; break; default: break; } } else if (mInfo.sky!=undef_sky) { switch (mInfo.sky) { case clear : theme=CLEAR_THEME; break; default: break; } } else theme=DEFAULT_THEME; // Nothing significant found. } /************************************************************* * Method: extract_json_number * ************************************************************* * Description: * * Very small, purpose built JSON scraper. It looks for * * "key":[ inside the given text and parses the first number * * after it. Amedas values always look like "temp":[26.3,0] * * (value, quality flag), so we only need the number before * * the first comma or closing bracket. * * * * Input: * * string json - Text to search in (one HHMM observation * * block) * * string key - Field name, e.g. "temp" * * * * Output: * * double &value - Parsed number * * bool - true if the key was found and a number was read * * * * Change History: * * Date Author Modification * * * *************************************************************/ bool localtemp::extract_json_number(const string &json, const string &key, double &value) { string needle="\""+key+"\":["; size_t pos=json.find(needle); if (pos==string::npos) return false; pos+=needle.length(); size_t endpos=json.find_first_of(",]", pos); if (endpos==string::npos) return false; string numstr=json.substr(pos, endpos-pos); if (numstr.empty() || numstr=="null") return false; value=atof(numstr.data()); return true; } /************************************************************* * Method: fetch_latest_time * ************************************************************* * Description: * * Downloads AMEDAS_LATEST_URL, which returns a single * * ISO-8601 JST timestamp such as * * "2024-08-09T15:40:00+09:00", telling us which day's file * * to request and which HHMM block inside it is the most * * recent observation. * * It also converts that JST timestamp into a real UTC * * epoch (obs_utc), because dwgo.cpp's displaytemp() expects * * info_time to be a plain UTC instant: it later does * * "info_time + tm_diff" and feeds that into localtime() to * * show it in the viewer's own local time zone. JMA gives us * * JST (UTC+9) directly, so we build the calendar fields with * * timegm() (which treats a struct tm as UTC, unlike * * mktime() which would apply the *host*'s time zone) and * * then subtract 9 hours to land back on true UTC. * * * * Output: * * string &yyyymmdd - Date, e.g. "20240809" * * string &hhmm - Time, e.g. "1540" * * time_t &obs_utc - Observation instant as a UTC epoch * * bool - true if we could fetch and parse the timestamp * * * * Change History: * * Date Author Modification * * * *************************************************************/ bool localtemp::fetch_latest_time(string &yyyymmdd, string &hhmmss, string &block3h, time_t &obs_utc) { MySock *skt; HTTP_Request *http; struct tm jst; int hour, block; char blockbuf[3]; skt = new MySock((char*)AMEDAS_LATEST_URL); http=skt->GetHTTPData(); delete skt; if (http==NULL || http->status!=200 || http->data.length()<19) return false; // Expected format: YYYY-MM-DDTHH:MM:SS+09:00 string d=http->data; yyyymmdd=d.substr(0,4)+d.substr(5,2)+d.substr(8,2); hhmmss=d.substr(11,2)+d.substr(14,2)+d.substr(17,2); hour=atoi(d.substr(11,2).data()); block=(hour/3)*3; // Point files are split into 3 hour windows: 00,03,06,...,21 sprintf(blockbuf, "%02d", block); block3h=blockbuf; memset(&jst, 0, sizeof(jst)); jst.tm_year=atoi(d.substr(0,4).data())-1900; jst.tm_mon =atoi(d.substr(5,2).data())-1; jst.tm_mday=atoi(d.substr(8,2).data()); jst.tm_hour=hour; jst.tm_min =atoi(d.substr(14,2).data()); jst.tm_sec =atoi(d.substr(17,2).data()); obs_utc=timegm(&jst)-9*3600; // JST -> UTC (JMA has no DST, always UTC+9) return true; } /************************************************************* * Method: getInfo * ************************************************************* * Description: * * Fetch information and parse the file * * * * Output: * * True if we downloaded it right, false if not. * * * * Change History: * * Date Author Modification * * 20101031 Gaspar Fernández Bug in skt * *************************************************************/ bool localtemp::getInfo() { char *point_fetch; // Amedas point data URL MySock *skt; HTTP_Request *http; string yyyymmdd, hhmmss, block3h; time_t obs_utc; double temp_c, hum, precip10m; this->error=0; // No error this->loaded=false; if (!fetch_latest_time(yyyymmdd, hhmmss, block3h, obs_utc)) return false; point_fetch= (char*) malloc(strlen(AMEDAS_POINT_URL)+this->metar.length()+yyyymmdd.length()+block3h.length()+4); sprintf(point_fetch, AMEDAS_POINT_URL, this->metar.data(), yyyymmdd.data(), block3h.data()); skt = new MySock(point_fetch); verbsth(VERB_ASTTO, "Open connection: "); http=skt->GetHTTPData(); delete skt; // We don't need this anymore free(point_fetch); if (http!=NULL) { if (http->status==200) { time(&get_time); // We got the file at this moment verbsth(VERB_ASTTO, "Get Data: "); this->long_location=this->location_name; string fullkey=yyyymmdd+hhmmss; size_t blockpos=http->data.find("\""+fullkey+"\":"); string selected_key; for (size_t pos = 0; pos + 16 < http->data.size(); ++pos) { if (http->data[pos] != '"') continue; string candidate = http->data.substr(pos + 1, 14); bool digits = true; for (size_t i = 0; i < candidate.size(); ++i) { if (candidate[i] < '0' || candidate[i] > '9') { digits = false; break; } } if (!digits || http->data[pos + 15] != '"') continue; size_t colon = http->data.find(':', pos + 16); if (colon == string::npos) continue; size_t object_start = http->data.find_first_not_of(" \t\r\n", colon + 1); if (object_start == string::npos || http->data[object_start] != '{') continue; if (candidate <= fullkey && (selected_key.empty() || candidate > selected_key)) { selected_key = candidate; blockpos = pos; } } if (blockpos==string::npos) { blockpos=http->data.rfind("\":{"); if (blockpos!=string::npos) blockpos=http->data.rfind("\"", blockpos-1); } if (blockpos!=string::npos) { this->ob=http->data.substr(blockpos); if (extract_json_number(this->ob, "temp", temp_c)) this->set_temp(temp_c); if (extract_json_number(this->ob, "humidity", hum)) this->set_humidity((int)hum); if (!extract_json_number(this->ob, "precipitation10m", precip10m)) precip10m=0.0; this->info_time=obs_utc; // Actual observation instant, in UTC get_ob_info(precip10m); // Work out theme from precipitation/temperature this->loaded=true; verbsth(VERB_ASTTO, "Close connection: "); } else { this->error=1; verbsth(VERB_WARNING, "Couldn't find observation data in Amedas response."); } } else { this->error=1; // Not found // Print request cout<data<