/*******************************************************************************
* 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<