I've been working up some scripts that query the US national weather service api for various and sundry data. In the last thread, I posted a clunky-looking thing with
use WWW::Mechanize;
use HTML::TableExtract qw(tree);
, and marto simplified the whole business with Re: polishing up a json fetching script for weather data
where the above functionality was replaced with:
use Mojo::UserAgent;
I'd like to extend this by generalizing it to inputs of arbitrary lats and longs in the US as opposed to hard-coded to my location in Portland. Let's start by looking at a script, output first. It's longish, but important for people to see a sample of if they want to understand this. First, we find the stationID, gridX, gridY that correspond to a given ($lat, $long). Then we make a different query using these data, to find the forecast at this location.
Let's begin with the head and tail of the log:
2020/05/22 23:49:40 INFO ./3.6.pho.pl
2020/05/22 23:49:40 INFO 33.4 -112.1 112.1
2020/05/22 23:49:40 INFO PSR
2020/05/22 23:49:40 INFO 157 55
2020/05/22 23:49:40 INFO ==============
2020/05/22 23:49:40 INFO https://api.weather.gov/gridpoints/PSR/157,55
+/forecast/hourly
2020/05/22 23:49:40 INFO [
{
"2020-05-22T23:00:00-07:00" => {
"date-from-local" => "2020-05-22-23:00:00 PDT",
"forecast-string" => "Clear",
"forecast-temp-F" => 79,
"human-readable" => "Fri 11 PM",
"julian day" => "2458992.75",
"number" => 1
}
},
{
"2020-05-23T00:00:00-07:00" => {
"date-from-local" => "2020-05-23-00:00:00 PDT",
"forecast-string" => "Clear",
"forecast-temp-F" => 77,
"human-readable" => "Sat 12 AM",
"julian day" => "2458992.79166667",
"number" => 2
}
},
...
{
"2020-05-29T09:00:00-07:00" => {
"date-from-local" => "2020-05-29-09:00:00 PDT",
"forecast-string" => "Sunny",
"forecast-temp-F" => 94,
"human-readable" => "Fri 9 AM",
"julian day" => "2458999.16666667",
"number" => 155
}
},
{
"2020-05-29T10:00:00-07:00" => {
"date-from-local" => "2020-05-29-10:00:00 PDT",
"forecast-string" => "Sunny",
"forecast-temp-F" => 98,
"human-readable" => "Fri 10 AM",
"julian day" => "2458999.20833333",
"number" => 156
}
}
]
Source:
#!/usr/bin/env perl
use strict;
use warnings;
use DateTime::Format::Strptime;
use REST::Client;
use Data::Roundtrip qw/:all/;
use 5.016;
use Log::Log4perl;
use Data::Dump;
# get rid of old log
my $file = '/home/hogan/Documents/hogan/logs/4.log4perl.txt';
unlink $file or warn "Could not unlink $file: $!";
my $log_conf4 = "/home/hogan/Documents/hogan/logs/conf_files/4.conf";
Log::Log4perl::init($log_conf4); #info
my $logger = Log::Log4perl->get_logger();
$logger->info("$0");
## important...no trailing zeroes else 301 error
my $lat = 33.4;
my $long = -112.1;
my $west_long = -$long;
$logger->info("$lat $long $west_long");
# this is our fetcher, similar to LWP::UserAgent
# but better suited for this kind of web service: REST
my $rest = REST::Client->new() or die "failed to construct client";
# see examples in
# https://www.weather.gov/documentation/services-web-api
# set the host
$rest->setHost('https://api.weather.gov');
# https://api.weather.gov/points/{latitude},{longitude}
# https://api.weather.gov/points/45.4836356,-122.4170501
# and this is our query with lat,long specified above
#my $query = "gridpoints/TOP/$lat,$long/forecast";
#https://api.weather.gov/stations/PQR/observations/latest KTTD
my $query = "points/$lat,$long";
# make the request and check the response code, 200 is good
my $response = $rest->GET($query) or die "failed to GET($query)";
if ( $rest->responseCode() != 200 ) {
die "failed to GET("
. $rest->getHost()
. "/$query) with "
. $rest->responseCode();
}
# we get back JSON
my $jsonstr = $response->responseContent();
# convert JSON string to a perl variable
my $pv = json2perl($jsonstr);
if ( !defined $pv ) {
warn "something wrong with this alleged json : '$jsonstr'";
}
my $station = $pv->{'properties'}->{'cwa'};
my $gridX = $pv->{'properties'}->{'gridX'};
my $gridY = $pv->{'properties'}->{'gridY'};
$logger->info("$station");
$logger->info("$gridX $gridY");
# or print it all and examine it
#$logger->info(perl2dump($pv));
## use mojo now
use Mojo::UserAgent;
use open ':std', OUT => ':utf8';
use Mojo::Util qw(dumper);
my $url =
"https://api.weather.gov/gridpoints/$station/$gridX,$gridY/forecast/
+hourly";
$logger->info("==============");
$logger->info("$url");
# the API docs says you must identify yourself, please make this somet
+hing legit
my $name = '(example.com, contact@example.com)';
my $ua = Mojo::UserAgent->new;
$ua->transactor->name($name);
# get JSON response
my $forecasts = $ua->get($url)->res->json->{properties}->{periods};
use DateTime::Format::Strptime;
my $dateparser = DateTime::Format::Strptime->new(
# parses 2020-04-26T06:00:00-05:00,
# %F then literal T then %T then timezone offset
pattern => '%FT%T%z'
) or die "failed to DateTime::Format::Strptime->new()";
my @ordered;
my $refordered = \@ordered;
my $index = 0;
for my $aforecast (@$forecasts) {
my $number = $aforecast->{number};
# start and end times of the prediction, convert them to objects
my $start_time_str = $aforecast->{'startTime'};
say "start is $start_time_str";
my $start_time_dateobj = $dateparser->parse_datetime($start_time_str
+)
or die "parsing date '$start_time_str'.";
my $jd = $start_time_dateobj->jd;
$start_time_dateobj->set_time_zone('America/Los_Angeles');
my $st = $start_time_dateobj->strftime('%Y-%m-%d-%H:%M:%S %Z');
my $hour = $start_time_dateobj->hour_12();
my $which_m = $start_time_dateobj->am_or_pm();
my $abrv = $start_time_dateobj->day_abbr();
# sunny?
my $forecast_str = $aforecast->{'shortForecast'};
# temperature as a number, see later for the units
my $temp = $aforecast->{'temperature'};
# new scope for hash
my %parsed;
# store this record/prediction in our %parsed hash
# keyed on this:
my $key = $start_time_str;
$parsed{$key} = {
'human-readable' => "$abrv $hour $which_m",
'date-from-local' => $st,
'number' => $number,
'forecast-string' => $forecast_str,
'julian day' => $jd,
# we append temp unit to the key, e.g. 'F'
'forecast-temp-' . $aforecast->{'temperatureUnit'} => $temp,
};
my $refparsed = \%parsed;
$ordered[$index] = $refparsed;
#$logger->info( dumper $refparsed);
++$index;
}
$logger->info( dumper $refordered);
$logger->info("==============");
my $pturl1 = 'https://www.fourmilab.ch/yoursky/';
my $pturl2 =
'http://www.fourmilab.ch/cgi-bin/Yoursky?z=1&$lat&ns=North&lon=$west_l
+ong&ew=West';
#my $tx = $ua->post( $pturl2 => form => { });
__END__
This script shows my learning curve with different modules. If one gives me malformed json, I can see what the other does. Where am I going with this?
I would like to add the altitude and azimuth of the sun to all of weather forecasts. I was going to do it with
my $pturl2 =
'http://www.fourmilab.ch/cgi-bin/Yoursky?z=1&$lat&ns=North&lon=$west_l
+ong&ew=West';
#my $tx = $ua->post( $pturl2 => form => { utc => $dt->jd, date => '2'
+});
, but I didn't want to make 156 different queries *AND* I reasoned that there had to be a reasonable way to calculate this value a priori. I didn't have long to search on cpan before I found Astro::Coord::ECI::Sun. This script delivers something near proof of concept with the example provided:
$ ./1.1.astro.pl
./1.1.astro.pl
33.4 -112.1 112.1
Sun rise is Sat May 23 09:49:05 2020 UT
$ cat 1.1.astro.pl
#!/usr/bin/env perl
use strict;
use warnings;
use 5.016;
use Log::Log4perl;
# get rid of old log
my $file = '/home/hogan/Documents/hogan/logs/4.log4perl.txt';
unlink $file or warn "Could not unlink $file: $!";
my $log_conf4 = "/home/hogan/Documents/hogan/logs/conf_files/4.conf";
Log::Log4perl::init($log_conf4); #info
my $logger = Log::Log4perl->get_logger();
$logger->info("$0");
## im
my $deg_lat = 33.4;
my $deg_long = -112.1;
my $west_long = -$deg_long;
$logger->info("$deg_lat $deg_long $west_long");
use Astro::Coord::ECI;
use Astro::Coord::ECI::Sun;
use Astro::Coord::ECI::Utils qw{deg2rad};
# 1600 Pennsylvania Ave, Washington DC USA
# latitude 38.899 N, longitude 77.038 W,
# altitude 16.68 meters above sea level
my $lat = deg2rad (38.899); # Radians
my $long = deg2rad (-77.038); # Radians
my $alt = 16.68 / 1000; # Kilometers
my $sun = Astro::Coord::ECI::Sun->new ();
my $sta = Astro::Coord::ECI->
universal (time ())->
geodetic ($lat, $long, $alt);
my ($time, $rise) = $sta->next_elevation ($sun);
$logger->info("Sun @{[$rise ? 'rise' : 'set']} is ",
scalar gmtime $time, " UT\n");
__END__
$
What am I trying to do here? I would like to gather whatever relevant information I can find about the sun with latitude, longitude, elevation, and time given as a DataTime object. I see tantalizing options like this:
($point, $intens, $central) = $sun->magnitude ($theta, $omega);
Where am I stuck? I have time like this
my $start_time_str = $aforecast->{'startTime'};
say "start is $start_time_str";
my $start_time_dateobj = $dateparser->parse_datetime($start_time_str
+)
, and I need that to be simpatico with:
my $sun = Astro::Coord::ECI::Sun->universal ($time);
# Figure out if the Sun is up at the observer's location.
my ($azimuth, $elevation, $range) = $loc->azel ($sun);
print "The Sun is ", rad2deg ($elevation),
" degrees above the horizon.\n";
Thanks for your comment,