You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1113 lines
38 KiB
1113 lines
38 KiB
#!/usr/bin/perl -w
|
|
########################################################################
|
|
#
|
|
# filename: metar2text
|
|
#
|
|
# description: A PERL program that will read a METAR from the STDIN and
|
|
# produce a human readable report. Multiple languages are
|
|
# supported by having all language dependent information
|
|
# in a seperate file. Thus that is the only files that must
|
|
# be changed to support a different language. The main
|
|
# engine contained here does not need to be modified.
|
|
#
|
|
# One advantage of this program over GEO:METAR is that
|
|
# program allows international information to be processed
|
|
# and this one provides a much more comprehensive report.
|
|
# If you want to create your own report, ALL of the
|
|
# of the decoded data is available
|
|
#
|
|
# formulas: Wind Chill - T = temp F; V = wind speed MPH
|
|
# Valid for V >= 3 and T <= 45
|
|
# WC = 35.74 + 0.6215T - 35.75(V^0.16) + 0.4275T(V^0.16)
|
|
#
|
|
# Heat Index - T = temp F; RH = Relative Humidity
|
|
# Valid for T >= 80 and RH >= 40
|
|
# HI = -42.379 + 2.0491523T + 10.14333127RH -0.22475541TRH
|
|
# - (6.83783 * 10^-3)T^2 - (5.48171 * 10^-2)RH^2
|
|
# + (1.22874 * 10^-3)T^2RH + (8.5282 * 10^-4)TRH^2
|
|
# - (1.99 * 10^-6)T^2RH^2
|
|
#
|
|
# Relative Humidity - T = temp C; Td = dew point C
|
|
# RH = (((112 - (0.1T) + Td) / (112 + (0.9T)))**8)*100.0
|
|
#
|
|
# history:
|
|
# 20040818 kc6hur Alpha release
|
|
# 20060903 kc6hur 0.10 New formulas for Relative Humidity,
|
|
# Wind Chill and added Heat Index. Provide
|
|
# better personalization of report.
|
|
#
|
|
# $Id: metar2text 31 2009-01-05 02:52:02Z $"
|
|
########################################################################
|
|
|
|
package METAR;
|
|
|
|
## Required Modules
|
|
|
|
use 5.005;
|
|
use strict;
|
|
use vars qw($AUTOLOAD $VERSION);
|
|
use Carp 'cluck';
|
|
use Date::Manip qw(ParseDate UnixDate);
|
|
use Time::Local;
|
|
# use exported CALL_SPACED variable (from wx_scripts.conf)
|
|
use Env qw(CALL_SPACED);
|
|
|
|
my $VERSION = '0.10';
|
|
|
|
##
|
|
## Language dependent data
|
|
##
|
|
|
|
########################################################################
|
|
## These tables contain all of the "words" used to generate the final
|
|
## printed report. In order to provide, international support, all that
|
|
## is necessary is to change the data in these tables.
|
|
########################################################################
|
|
|
|
my %_weather_types = (
|
|
MI => ' shallow',
|
|
PR => ' partial',
|
|
BC => ' patches',
|
|
DR => ' low drifting', ## drizzle
|
|
BL => ' blowing',
|
|
SH => ' shower(s)',
|
|
TS => ' thunderstorm',
|
|
FZ => ' freezing',
|
|
|
|
DZ => ' drizzle',
|
|
RA => ' rain',
|
|
SN => ' snow',
|
|
SG => ' snow grains',
|
|
IC => ' ice crystals',
|
|
PE => ' ice pellets',
|
|
GR => ' hail',
|
|
GS => ' small hail and or snow pellets',
|
|
UP => ' unknown',
|
|
|
|
BR => ' mist',
|
|
FG => ' fog',
|
|
FU => ' smoke',
|
|
VA => ' volcanic ash',
|
|
DU => ' widespread dust', ## dust
|
|
SA => ' sand',
|
|
HZ => ' haze',
|
|
PY => ' spray',
|
|
|
|
PO => ' well-developed dust sand whirls',
|
|
SQ => ' squalls',
|
|
FC => ' funnel cloud (tornado/waterspout)',
|
|
SS => ' sand storm',
|
|
DS => ' dust storm'
|
|
);
|
|
|
|
my %_cloud_condition = (
|
|
SKC => 'was clear',
|
|
CLR => 'was clear',
|
|
VV => 'was vertical visibility',
|
|
FEW => 'were a few',
|
|
SCT => 'were scattered',
|
|
BKN => 'were broken',
|
|
OVC => 'was overcast',
|
|
);
|
|
|
|
my %_cloud_coverage = (
|
|
SKC => '0',
|
|
CLR => '0',
|
|
VV => '8/8',
|
|
FEW => '1/8 - 2/8',
|
|
SCT => '3/8 - 4/8',
|
|
BKN => '5/7 - 7/8',
|
|
OVC => '8/8',
|
|
);
|
|
|
|
my @_wind_dir = (
|
|
'North',
|
|
'North Northeast',
|
|
'Northeast',
|
|
'East Northeast',
|
|
'East',
|
|
'East Southeast',
|
|
'Southeast',
|
|
'South Southeast',
|
|
'South',
|
|
'South Southwest',
|
|
'Southwest',
|
|
'West Southwest',
|
|
'West',
|
|
'West Northwest',
|
|
'Northwest',
|
|
'North Northwest',
|
|
'North');
|
|
|
|
my %_units = (
|
|
english_height => 'feet',
|
|
english_depth => 'inches',
|
|
english_distance => 'miles',
|
|
english_speed => 'miles per hour',
|
|
english_pressure => 'inches of mercury',
|
|
english_temperature => 'degrees',
|
|
metric_height => 'meters',
|
|
metric_depth => 'millimeters',
|
|
metric_distance => 'kilometers',
|
|
metric_speed => 'meters per second',
|
|
metric_pressure => 'hecto-pascals',
|
|
metric_temperature => 'celcius',
|
|
);
|
|
|
|
my %_strings = (
|
|
no_data => 'Sorry! There is no data available for %s.',
|
|
mm_inches => '%s %s',
|
|
precip_a_trace => 'a trace',
|
|
precip_there_was => ' There was %s of precipitation ',
|
|
sky_str_format1 => ' There %s at a height of %s %s',
|
|
sky_str_format2 => ', %s at a height of %s %s and %s at a height of %s %s',
|
|
sky_str_format3 => ' and %s at a height of %s %s',
|
|
sky_str_clear => ' The sky was clear',
|
|
sky_cavok => ' There were no clouds below %s.',
|
|
clouds => ' clouds',
|
|
clouds_cb => ' cumulonimbus clouds',
|
|
clouds_tcu => ' towering cumulus clouds',
|
|
visibility_format => ' The visibility was %s %s.',
|
|
wind_str_format1 => 'blowing at a speed of %s %s',
|
|
wind_str_format2 => ', with gusts to %s %s,',
|
|
wind_str_format3 => ' from %s',
|
|
wind_str_calm => 'calm',
|
|
wind_vrb_long => 'variable directions',
|
|
windchill => ', the windchill was %s %s',
|
|
rel_humidity => ' The relative humidity was %s%%.',
|
|
heat_index => ', the heat index was %s %s',
|
|
precip_last_hour => 'in the last hour. ',
|
|
precip_last_6_hours => 'in the last 3 to 6 hours. ',
|
|
precip_last_24_hours => 'in the last 24 hours. ',
|
|
precip_snow => ' There is %s %s of snow on the ground.',
|
|
temp_min_max_6_hours => ' The high and low temperatures over the last 6 hours were %s and %s %s.',
|
|
temp_max_6_hours => ' The high temperature over the last 6 hours was %s %s.',
|
|
temp_min_6_hours => ' The low temperature over the last 6 hours was %s %s.',
|
|
temp_min_max_24_hours => ' The high and low temperatures over the last 24 hours were %s and %s %s.',
|
|
runway_vis => ' The visibility for runway %s is %s meters (%s feet).',
|
|
runway_vis_min_max => ' The visibility for runway %s varies between %s meters (%s feet) and %s meters (%s feet).',
|
|
light => ' light',
|
|
moderate => ' moderate',
|
|
Heavy => ' heavy',
|
|
nearby => ' nearby',
|
|
current_weather => ' The weather condition was%s.',
|
|
barometric_pressure => ' and the pressure was %s %s',
|
|
pretty_print_format => 'This is the %s node weather report for the %s. At %s, the wind was %s. The temperature was %s %s%s%s.%s%s%s%s%s%s%s',
|
|
);
|
|
|
|
##
|
|
##
|
|
##
|
|
|
|
my $_weather_types_pat = join("|", keys(%_weather_types));
|
|
|
|
########################################################################
|
|
##
|
|
## DEFINE THE METAR METHODS
|
|
##
|
|
########################################################################
|
|
|
|
##
|
|
## Constructor.
|
|
##
|
|
|
|
sub new
|
|
{
|
|
my $this = shift;
|
|
my $class = ref($this) || $this;
|
|
my $self = {};
|
|
|
|
$self->{version} = $VERSION;
|
|
$self->{metar} = undef;
|
|
$self->{station} = undef;
|
|
$self->{type} = undef;
|
|
$self->{time} = undef;
|
|
$self->{mod} = undef;
|
|
$self->{wind_dir_deg} = undef;
|
|
$self->{wind_dir_txt} = undef;
|
|
$self->{wind_meters_per_second} = undef;
|
|
$self->{wind_knots} = undef;
|
|
$self->{wind_miles_per_hour} = undef;
|
|
$self->{wind_gust_meters_per_second} = undef;
|
|
$self->{wind_gust_knots} = undef;
|
|
$self->{wind_gust_miles_per_hour} = undef;
|
|
$self->{wind_var_beg} = undef;
|
|
$self->{wind_var_end} = undef;
|
|
$self->{visibility_miles} = undef;
|
|
$self->{visibility_km} = undef;
|
|
$self->{runway_nr} = undef;
|
|
$self->{runway_vis_ft} = undef;
|
|
$self->{runway_vis_meter} = undef;
|
|
$self->{runway_vis_ft_min} = undef;
|
|
$self->{runway_vis_meter_min} = undef;
|
|
$self->{runway_vis_ft_max} = undef;
|
|
$self->{runway_vis_meter_max} = undef;
|
|
$self->{weather} = undef;
|
|
$self->{cloud_layer1_condition} = undef;
|
|
$self->{cloud_layer1_coverage} = undef;
|
|
$self->{cloud_layer1_altitude_ft} = undef;
|
|
$self->{cloud_layer1_altitude_m} = undef;
|
|
$self->{cloud_layer2_condition} = undef;
|
|
$self->{cloud_layer2_coverage} = undef;
|
|
$self->{cloud_layer2_altitude_ft} = undef;
|
|
$self->{cloud_layer2_altitude_m} = undef;
|
|
$self->{cloud_layer3_condition} = undef;
|
|
$self->{cloud_layer3_coverage} = undef;
|
|
$self->{cloud_layer3_altitude_ft} = undef;
|
|
$self->{cloud_layer3_altitude_m} = undef;
|
|
$self->{dew_c} = undef;
|
|
$self->{dew_f} = undef;
|
|
$self->{altimeter_inhg} = undef;
|
|
$self->{altimeter_mmhg} = undef;
|
|
$self->{altimeter_hpa} = undef;
|
|
$self->{altimeter_atm} = undef;
|
|
$self->{temp_c} = undef;
|
|
$self->{temp_f} = undef;
|
|
$self->{temp_c_max6h} = undef;
|
|
$self->{temp_f_max6h} = undef;
|
|
$self->{temp_c_min6h} = undef;
|
|
$self->{temp_f_min6h} = undef;
|
|
$self->{temp_c_max24h} = undef;
|
|
$self->{temp_f_max24h} = undef;
|
|
$self->{temp_c_min24h} = undef;
|
|
$self->{temp_f_min24h} = undef;
|
|
$self->{precip_in} = undef;
|
|
$self->{precip_mm} = undef;
|
|
$self->{precip_6h_in} = undef;
|
|
$self->{precip_6h_mm} = undef;
|
|
$self->{precip_24h_in} = undef;
|
|
$self->{precip_24h_mm} = undef;
|
|
$self->{snow_in} = undef;
|
|
$self->{snow_mm} = undef;
|
|
$self->{rel_humidity} = undef;
|
|
$self->{humidex_c} = undef;
|
|
$self->{humidex_f} = undef;
|
|
$self->{windchill_f} = undef;
|
|
$self->{windchill_c} = undef;
|
|
$self->{heat_index_f} = undef;
|
|
$self->{heat_index_c} = undef;
|
|
#
|
|
# Set the value of units to 'english' or 'metric'
|
|
#
|
|
$self->{units} = 'english';
|
|
$self->{hrs24} = 0; # 0=12hr, 1=24hr
|
|
$self->{debug} = 0; # 0=nodebug, 1=debug
|
|
|
|
bless $self, $class;
|
|
return $self;
|
|
}
|
|
|
|
########################################################################
|
|
## Read the METAR, process it, load up the METAR object and return the
|
|
## original METAR
|
|
##
|
|
|
|
sub fetch_metar {
|
|
#
|
|
# Fetches a new METER from STDIN and parses it
|
|
# The new METAR is returned.
|
|
#
|
|
my $self = shift;
|
|
my $metar = '';
|
|
|
|
while (<STDIN>) {
|
|
chomp;
|
|
$metar = $_ if (/^([A-Z]{4,4}) /i);
|
|
}
|
|
$self->{metar} = $metar;
|
|
_process($self);
|
|
|
|
return $self->{metar};
|
|
}
|
|
|
|
########################################################################
|
|
## Autoload for access methods to stuff in %fields hash.
|
|
##
|
|
|
|
sub AUTOLOAD {
|
|
my $self = shift;
|
|
|
|
if (not ref $self) {
|
|
cluck "bad AUTOLOAD for obj [$self]";
|
|
}
|
|
|
|
if ($AUTOLOAD =~ /.*::(.*)/) {
|
|
my $key = $1;
|
|
|
|
## Check for the items...
|
|
|
|
if (exists $self->{$key}) {
|
|
return $self->{$key};
|
|
} else {
|
|
return undef;
|
|
}
|
|
} else {
|
|
warn "strange AUTOLOAD problem!";
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
########################################################################
|
|
## Get current version number.
|
|
##
|
|
|
|
sub version {
|
|
my $self = shift;
|
|
return $self->{version};
|
|
}
|
|
|
|
|
|
########################################################################
|
|
## Get site id
|
|
##
|
|
|
|
sub getstnid {
|
|
my $self = shift;
|
|
return $self->{site};
|
|
}
|
|
|
|
########################################################################
|
|
## Process the METAR.
|
|
##
|
|
## This function decodes a raw METAR.
|
|
##
|
|
sub _process
|
|
{
|
|
my $self = shift;
|
|
my $temp_visibility_miles = '';
|
|
my $cloud_layers = 0;
|
|
my $remarks = 0;
|
|
|
|
## Assume standard report by default
|
|
|
|
$self->{type} = "METAR";
|
|
print STDERR "DEBUG: metar $self->{metar}\n" if $self->{debug};
|
|
|
|
foreach (split(/\s+/, $self->{metar})) {
|
|
|
|
print STDERR "DEBUG: token [$_]..." if $self->{debug};
|
|
|
|
if (! $remarks) {
|
|
##
|
|
## Type of Report
|
|
##
|
|
|
|
if (/^(METAR|SPECI)$/i) {
|
|
print STDERR "Type of Report.\n" if $self->{debug};
|
|
$self->{type} = $1;
|
|
}
|
|
|
|
##
|
|
## Station Identifier
|
|
##
|
|
## Changed to check all 4 letters. Used to required K as the
|
|
## first letter but this would break the program when used
|
|
## to process data for non-USA weather.
|
|
##
|
|
elsif ((! defined $self->{site}) && (/^([A-Z]{4,4})$/i)) {
|
|
print STDERR "Station Identifier.\n" if $self->{debug};
|
|
$self->{site} = $1;
|
|
}
|
|
|
|
##
|
|
## Date and Time of Report
|
|
##
|
|
elsif (/^(\d{2})(\d{2})(\d{2})Z$/i) {
|
|
print STDERR "Date and Time of Report.\n" if $self->{debug};
|
|
$self->{time} = timegm("00",$3,$2,$1,(gmtime)[4],(gmtime)[5]+1900);
|
|
}
|
|
|
|
##
|
|
## Report Modifier
|
|
##
|
|
elsif (/^(AUTO|COR|RTD|CC[A-Z]|RR[A-Z])$/i) {
|
|
print STDERR "Report Modifier.\n" if $self->{debug};
|
|
$self->{report_mod} = $1;
|
|
}
|
|
|
|
##
|
|
## Wind Group
|
|
##
|
|
elsif (/^(\d{2,3}|VRB)(\d{2,3})(G(\d{2,3}))?(KT|MPS|KMH)$/i) {
|
|
print STDERR "Wind Group.\n" if $self->{debug};
|
|
my $windunit = $5;
|
|
#
|
|
# now get the actual values
|
|
#
|
|
if ( $1 eq 'VRB' ) {
|
|
$self->{wind_deg} = $_strings{wind_vrb_long};
|
|
$self->{wind_dir_text} = $_strings{wind_vrb_long};
|
|
} else {
|
|
$self->{wind_deg} = $1;
|
|
$self->{wind_dir_text} = $_wind_dir[int(($1/22.5)+0.5)];
|
|
}
|
|
($self->{wind_meters_per_second},
|
|
$self->{wind_knots},
|
|
$self->{wind_miles_per_hour}) = conv_speed($2, $windunit);
|
|
if (defined $3) {
|
|
#
|
|
# We have a report with information about gusting.
|
|
#
|
|
($self->{wind_gust_meters_per_second},
|
|
$self->{wind_gust_knots},
|
|
$self->{wind_gust_miles_per_hour}) = conv_speed($4, $windunit);
|
|
}
|
|
}
|
|
|
|
##
|
|
## Wind Group - Variable Wind Direction
|
|
##
|
|
elsif (/^(\d{3})V(\d{3})$/i) {
|
|
print STDERR "Wind Group - Variable Wind Direction.\n" if $self->{debug};
|
|
$self->{wind_var_beg} = $1;
|
|
$self->{wind_var_end} = $2;
|
|
}
|
|
|
|
##
|
|
## Visibility Group
|
|
## in meters (4 digits only)
|
|
##
|
|
elsif (/^(\d{4})$/i) {
|
|
if ( $1 == "9999" ) {
|
|
#
|
|
# Visibility Group Special Case
|
|
#
|
|
print STDERR "Visibility Group (>10 km).\n" if $self->{debug};
|
|
$self->{visibility_miles} = '>6.2';
|
|
$self->{visibility_km} = '>10';
|
|
} else {
|
|
#
|
|
# Visibility Group Normal Case
|
|
print STDERR "Visibility Group (meters).\n" if $self->{debug};
|
|
$self->{visibility_km} = $1 / 1000;
|
|
$self->{visibility_miles} = $self->{visibility_km} / 1.609344;
|
|
}
|
|
}
|
|
|
|
##
|
|
## Visibility Group - Whole Miles
|
|
## Save this token so that it can be used with Visibility Group below.
|
|
##
|
|
elsif (/^M?\d$/i) {
|
|
print STDERR "Visibility Group (Whole miles).\n" if $self->{debug};
|
|
$temp_visibility_miles = $_;
|
|
}
|
|
|
|
##
|
|
## Visibility Group - Whole or Fractional Miles
|
|
##
|
|
## 2 3 4
|
|
elsif (/^((\d)(\/))?(\d*)SM$/i) {
|
|
print STDERR "Visibility Group (Whole/Fractional miles).\n" if $self->{debug};
|
|
|
|
my $vis_miles = ((defined $3) && ($3 eq '/')) ? $2 / $4 : $4;
|
|
|
|
if (($temp_visibility_miles)[0] eq 'M') {
|
|
$self->{visibility_miles} = '<'.$vis_miles;
|
|
$self->{visibility_km} = '<'.($vis_miles * 1.609344);
|
|
} else {
|
|
$self->{visibility_miles} = $vis_miles;
|
|
$self->{visibility_km} = $vis_miles * 1.609344;
|
|
}
|
|
}
|
|
|
|
##
|
|
## CAVOK: used when the visibility is greater than 10 km,
|
|
## the lowest cloud-base is at 5000 and there is no
|
|
## significant weather.
|
|
##
|
|
elsif (/^CAVOK$/i) {
|
|
print STDERR "is visibility information.\n" if $self->{debug};
|
|
$self->{visibility_km} = '>10';
|
|
$self->{visibility_miles} = '>6.2';
|
|
$self->{cloud_layer1_condition} = 'CAVOK';
|
|
}
|
|
|
|
##
|
|
## Runway Visual Range Group
|
|
##
|
|
elsif (/^R(\d{2}[RLC]?)\/([MP]?\d{4})V(P?\d{4})FT$/i) {
|
|
print STDERR "is runway visual information.\n" if $self->{debug};
|
|
}
|
|
|
|
##
|
|
## Present Weather Group
|
|
##
|
|
elsif (/^(-|\+|VC)?($_weather_types_pat)+$/i) {
|
|
print STDERR "Present Weather Group.\n" if $self->{debug};
|
|
my $part = $_;
|
|
if ($part =~ /-/) {
|
|
$self->{weather} .= $_strings{light};
|
|
$part = substr($part, 1);
|
|
} elsif ($part =~ /\+/) {
|
|
$self->{weather} .= $_strings{heavy};
|
|
} elsif (substr($part, 0, 2) =~ /VC/) {
|
|
$self->{weather} .= $_strings{nearby};
|
|
$part = substr($part, 2);
|
|
} else {
|
|
$self->{weather} .= $_strings{moderate};
|
|
}
|
|
|
|
while (my $bite=substr($part, 0, 2)) {
|
|
$self->{weather} .= $_weather_types{$bite};
|
|
$part = substr($part, 2);
|
|
}
|
|
}
|
|
|
|
##
|
|
## Sky Condition Group
|
|
## There can be up to three of these groups
|
|
##
|
|
elsif (/^(CLR|SKC)$/i) {
|
|
print STDERR "Sky Condition Group.\n" if $self->{debug};
|
|
$cloud_layers++;
|
|
|
|
$self->{'cloud_layer'.$cloud_layers.'_condition'} = $1;
|
|
$self->{'cloud_layer'.$cloud_layers.'_coverage'} = $1;
|
|
}
|
|
|
|
##
|
|
## Sky Condition Group
|
|
## We found a(nother) cloud-layer group. There can be up to
|
|
## thrre of these groups.
|
|
elsif (/^(VV|FEW|SCT|BKN|OVC)(\d{3})(CB|TCU)?$/i) {
|
|
print STDERR "Sky Condition Group.\n" if $self->{debug};
|
|
$cloud_layers++;
|
|
if ((defined $3) && ($3 eq 'CB')) {
|
|
$self->{'cloud_layer'.$cloud_layers.'_condition'} = $_cloud_condition{$1} . $_strings{clouds_cb};
|
|
} elsif ((defined $3) && ($3 eq 'TCU')) {
|
|
$self->{'cloud_layer'.$cloud_layers.'_condition'} = $_cloud_condition{$1} . $_strings{clouds_tcu};
|
|
} else {
|
|
$self->{'cloud_layer'.$cloud_layers.'_condition'} = $_cloud_condition{$1} . (($1 eq 'OVC') ? '' : $_strings{clouds});
|
|
}
|
|
$self->{'cloud_layer'.$cloud_layers.'_coverage'} = $_cloud_coverage{$1};
|
|
$self->{'cloud_layer'.$cloud_layers.'_altitude_ft'} = $2 * 100;
|
|
$self->{'cloud_layer'.$cloud_layers.'_altitude_m'} = $2 * 30.48;
|
|
}
|
|
|
|
##
|
|
## Temperature/Dew Point Group
|
|
##
|
|
elsif (/^(M?\d{1,2})\/(M?\d{1,2})?$/i) {
|
|
print STDERR "Temperature/Dew Point Group.\n" if $self->{debug};
|
|
my $temp_c = $1;
|
|
my $dew_c = $2 if (defined $2);
|
|
$temp_c =~ s/^M/-/;
|
|
$dew_c =~ s/^M/-/ if (defined $dew_c);
|
|
($self->{temp_c}, $self->{temp_f}) = conv_temp($temp_c);
|
|
($self->{dew_c}, $self->{dew_f}) = conv_temp($dew_c) if (defined $dew_c);
|
|
}
|
|
|
|
##
|
|
## Altimeter
|
|
##
|
|
elsif (/^A(\d{4})$/i) {
|
|
print STDERR "Altimeter.\n" if $self->{debug};
|
|
$self->{altimeter_inhg} = $1 / 100;
|
|
$self->{altimeter_mmhg} = $1 * 0.254;
|
|
$self->{altimeter_hpa} = $1 * 0.33863881578947;
|
|
$self->{altimeter_atm} = $1 * 3.3421052631579e-4;
|
|
}
|
|
|
|
##
|
|
## Remarks
|
|
##
|
|
elsif (/^RMK$/i) {
|
|
print STDERR "Begin Remarks Section.\n" if $self->{debug};
|
|
$remarks = 1;
|
|
}
|
|
|
|
##
|
|
## Unknown Token
|
|
##
|
|
else {
|
|
print STDERR "Unknown Token.\n" if $self->{debug};
|
|
}
|
|
|
|
} else {
|
|
##
|
|
## Now processing REMarks
|
|
##
|
|
print STDERR "RMK..." if $self->{debug};
|
|
##
|
|
## Hourly Temperature and Dew Point
|
|
##
|
|
if (/^T(\d{4})(\d{4})?$/i) {
|
|
print STDERR "Hourly Temperature and Dew Point.\n" if $self->{debug};
|
|
($self->{temp_c}, $self->{temp_f}) = conv_coded_temp($1);
|
|
($self->{dew_c}, $self->{dew_f}) = conv_coded_temp($2) if (defined $2);
|
|
}
|
|
|
|
##
|
|
## 6-Hourly Maximum Temperature
|
|
##
|
|
elsif (/^1(\d{4})$/i) {
|
|
print STDERR "6-Hourly Maximum Temperature.\n" if $self->{debug};
|
|
($self->{temp_max6h_c}, $self->{temp_max6h_f}) = conv_coded_temp($1);
|
|
}
|
|
|
|
##
|
|
## 6-Hourly Minimum Temperature
|
|
##
|
|
elsif (/^2(\d{4})$/i) {
|
|
print STDERR "6-Hourly Minimum Temperature.\n" if $self->{debug};
|
|
($self->{temp_min6h_c}, $self->{temp_min6h_f}) = conv_coded_temp($1);
|
|
}
|
|
|
|
##
|
|
## 24-Hour Maximum and Minimum Temperature
|
|
##
|
|
elsif (/^4(\d{4})(\d{4})$/i) {
|
|
print STDERR "24-Hour Maximum and Minimum Temperature.\n" if $self->{debug};
|
|
($self->{temp_max24h_c}, $self->{temp_max24h_f}) = conv_coded_temp($1);
|
|
($self->{temp_min24h_c}, $self->{temp_min24h_f}) = conv_coded_temp($2);
|
|
}
|
|
|
|
##
|
|
## 3-Hourly Pressure Tendancy
|
|
##
|
|
elsif (/^5(\d{4})$/i) {
|
|
print STDERR "3-Hourly Pressure Tendancy.\n" if $self->{debug};
|
|
}
|
|
|
|
##
|
|
## Hourly Precipitation Amount
|
|
## (stored as inches)
|
|
##
|
|
elsif (/^P(\d{4})$/i) {
|
|
print STDERR "Hourly Precipitation Amount.\n" if $self->{debug};
|
|
$self->{precip_in} = $1 / 100;
|
|
$self->{precip_mm} = $1 * 0.254;
|
|
}
|
|
|
|
##
|
|
## 3- and 6-Hour Precipitation Amount
|
|
## (store as inches)
|
|
##
|
|
elsif (/^6(\d{4})$/i) {
|
|
print STDERR "3- and 6-Hour Precipitation Amount.\n" if $self->{debug};
|
|
$self->{precip_6h_in} = $1 / 100;
|
|
$self->{precip_6h_mm} = $1 * 0.254;
|
|
}
|
|
|
|
##
|
|
## 24-Hour Precipitation Amount
|
|
## (store as inches)
|
|
##
|
|
elsif (/^7(\d{4})$/i) {
|
|
print STDERR "24-Hour Precipitation Amount.\n" if $self->{debug};
|
|
$self->{precip_24h_in} = $1 / 100;
|
|
$self->{precip_24h_mm} = $1 * 0.254;
|
|
}
|
|
|
|
##
|
|
## Snow Depth on Ground
|
|
##
|
|
elsif (/^4\/(\d{3})$/i) {
|
|
print STDERR "Snow Depth on Ground.\n" if $self->{debug};
|
|
$self->{snow_in} = $1;
|
|
$self->{snow_mm} = $1 * 25.4;
|
|
}
|
|
|
|
##
|
|
## Type of Automated Station
|
|
##
|
|
elsif (/^AO(1|2)$/i) {
|
|
print STDERR "Type of Automated Station.\n" if $self->{debug};
|
|
}
|
|
|
|
##
|
|
## SEA Level Pressure
|
|
##
|
|
elsif (/^SLP(\d{3}|NO)/i) {
|
|
print STDERR "Sea Level Pressure.\n" if $self->{debug};
|
|
}
|
|
|
|
##
|
|
## unknown.
|
|
##
|
|
else {
|
|
print STDERR "is unknown token.\n" if $self->{debug};
|
|
}
|
|
}
|
|
}
|
|
##
|
|
## All done processing the data in the METAR
|
|
##
|
|
## Process Derived Values
|
|
##
|
|
|
|
##
|
|
## Relative Humidity
|
|
##
|
|
if (defined $self->{dew_c}) {
|
|
$self->{rel_humidity} = (((112 - (0.1 * $self->{temp_c}) + $self->{dew_c}) / ((112 + (0.9 * $self->{temp_c}))))**8)*100.0;
|
|
}
|
|
|
|
##
|
|
## Heat Index
|
|
##
|
|
if ((defined $self->{rel_humidity}) && (defined $self->{temp_f}) && ($self->{temp_f} >= 80) && ($self->{rel_humidity} >= 40)) {
|
|
my $T = $self->{temp_f};
|
|
my $R = $self->{rel_humidity};
|
|
$self->{heat_index_f} = -42.379 + (2.0491423 * $T) + (10.14333127 * $R) - (0.22475541 * $T * $R) - ((6.83783 * (10 ** -3)) * ($T ** 2)) - ((5.48171 * (10 ** -2)) * ($R ** 2)) + ((1.22874 * (10 ** -3)) * ($T ** 2) * $R) + ((8.5282 * (10 ** -4)) * $T * ($R ** 2)) - ((1.99 * (10 ** -6)) * ($T ** 2) * ($R ** 2));
|
|
$self->{heat_index_c} = ($self->{heat_index_f} - 32) * 0.556;
|
|
}
|
|
|
|
##
|
|
## Windchill
|
|
##
|
|
if ((defined $self->{temp_f}) && ($self->{temp_f} <= 45) && ($self->{wind_miles_per_hour} >= 3)) {
|
|
my $wf = $self->{wind_miles_per_hour} ** 0.16;
|
|
$self->{windchill_f} = 35.74 + (0.6215 * $self->{temp_f}) - (35.75 * $wf) + (0.4275 * $self->{temp_f} * $wf);
|
|
$self->{windchill_c} = ($self->{windchill_f} - 32) * 0.556;
|
|
}
|
|
|
|
}
|
|
|
|
########################################################################
|
|
## Translate the precipitation values. If there there was precipitation
|
|
## but the amount was unmeasurable, then report "trace" for amount.
|
|
##
|
|
sub pretty_print_precip {
|
|
my $self = shift;
|
|
my $precip_mm = shift;
|
|
my $precip_in = shift;
|
|
|
|
print STDERR "DEBUG: Translate precipitation: \"$precip_mm\" mm, \"$precip_in\" in.\n" if $self->{debug};
|
|
my $amount = (($precip_mm > 0) ? sprintf($_strings{mm_inches},
|
|
(($self->{units} eq 'metric') ? $precip_mm : $precip_in),
|
|
$_units{$self->{units}.'_depth'})
|
|
: $_strings{precip_a_trace});
|
|
|
|
return sprintf($_strings{precip_there_was}, $amount);
|
|
}
|
|
|
|
########################################################################
|
|
## Returns a string with a "human readable" text which is a translation
|
|
## of the data contained in the METAR.
|
|
##
|
|
sub pretty_print {
|
|
|
|
my $self = shift;
|
|
my $location_str = shift;
|
|
|
|
if (!$self->{metar}) {
|
|
return sprintf($_strings{no_data}, $location_str);
|
|
}
|
|
|
|
##
|
|
## Time
|
|
##
|
|
my $minutes_old = int((time() - $self->{time}) / 60 + 0.5);
|
|
my $min = (localtime($self->{time}))[1];
|
|
my $hrs = (localtime($self->{time}))[2];
|
|
if ((! $self->{hrs24}) && ($hrs > 12)) {
|
|
$hrs -= 12;
|
|
}
|
|
my $lcltime_str = sprintf("%02s:%02s",$hrs,$min);
|
|
|
|
##
|
|
## Barometric Pressure
|
|
##
|
|
my $pressure_str = '';
|
|
if (defined $self->{altimeter_inhg}) {
|
|
$pressure_str = sprintf($_strings{barometric_pressure},
|
|
(($self->{units} eq 'metric') ? int($self->{altimeter_hpa} * 100 + 0.5) / 100
|
|
: $self->{altimeter_inhg}),
|
|
$_units{$self->{units}.'_pressure'});
|
|
}
|
|
|
|
##
|
|
## Cloud Layers
|
|
##
|
|
my $sky_str;
|
|
if ((defined $self->{cloud_layer1_condition}) && ($self->{cloud_layer1_condition} =~ /CAVOK/)) {
|
|
$sky_str = sprintf($_strings{sky_cavok}, (($self->{units} eq 'metric') ? "1,525 meters" : "5,000 feet"));
|
|
} elsif (defined $self->{cloud_layer1_altitude_ft}) {
|
|
$sky_str = sprintf($_strings{sky_str_format1},
|
|
$self->{cloud_layer1_condition},
|
|
(($self->{units} eq 'metric') ? int($self->{cloud_layer1_altitude_m} + 0.5)
|
|
: $self->{cloud_layer1_altitude_ft}),
|
|
$_units{$self->{units}.'_height'});
|
|
if (defined $self->{cloud_layer2_altitude_ft}) {
|
|
if (defined $self->{cloud_layer3_altitude_ft}) {
|
|
$sky_str .= sprintf($_strings{sky_str_format2},
|
|
$self->{cloud_layer2_condition},
|
|
(($self->{units} eq 'metric') ? int($self->{cloud_layer2_altitude_m} + 0.5)
|
|
: $self->{cloud_layer2_altitude_ft}),
|
|
$_units{$self->{units}.'_height'},
|
|
$self->{cloud_layer3_condition},
|
|
(($self->{units} eq 'metric') ? int($self->{cloud_layer3_altitude_m} + 0.5)
|
|
: $self->{cloud_layer3_altitude_ft}),
|
|
$_units{$self->{units}.'_height'});
|
|
} else {
|
|
$sky_str .= sprintf($_strings{sky_str_format3},
|
|
$self->{cloud_layer2_condition},
|
|
(($self->{units} eq 'metric') ? int($self->{cloud_layer2_altitude_m} + 0.5)
|
|
: $self->{cloud_layer2_altitude_ft}),
|
|
$_units{$self->{units}.'_height'});
|
|
}
|
|
}
|
|
} else {
|
|
$sky_str = $_strings{sky_str_clear};
|
|
}
|
|
$sky_str .= '.';
|
|
|
|
##
|
|
## Visibility
|
|
##
|
|
my $visibility_str = '';
|
|
if (defined $self->{visibility_miles}) {
|
|
if ($self->{visibility_miles} =~ /^>(.+)/) {
|
|
$visibility_str = sprintf($_strings{visibility_format},
|
|
($self->{units} eq 'metric') ? $self->{visibility_km}
|
|
: $self->{visibility_miles},
|
|
$_units{$self->{units}.'_distance'});
|
|
} else {
|
|
$visibility_str = sprintf($_strings{visibility_format},
|
|
int((($self->{units} eq 'metric') ? $self->{visibility_km}
|
|
: $self->{visibility_miles}) + 0.5),
|
|
$_units{$self->{units}.'_distance'});
|
|
}
|
|
}
|
|
|
|
##
|
|
## Wind
|
|
##
|
|
my $wind_str;
|
|
if ((defined $self->{wind_meters_per_second}) && ($self->{wind_meters_per_second} > 0)) {
|
|
$wind_str = sprintf($_strings{wind_str_format1},
|
|
int((($self->{units} eq 'metric') ? $self->{wind_meters_per_second}
|
|
: $self->{wind_miles_per_hour}) + 0.5),
|
|
$_units{$self->{units}.'_speed'});
|
|
if ((defined $self->{wind_gust_meters_per_second}) && ($self->{wind_gust_meters_per_second} > 0)) {
|
|
$wind_str .= sprintf($_strings{wind_str_format2},
|
|
int((($self->{units} eq 'metric') ? $self->{wind_gust_meters_per_second}
|
|
: $self->{wind_gust_miles_per_hour}) + 0.5),
|
|
$_units{$self->{units}.'_speed'});
|
|
}
|
|
$wind_str .= sprintf($_strings{wind_str_format3}, $self->{wind_dir_text});
|
|
} else {
|
|
$wind_str = $_strings{wind_str_calm};
|
|
}
|
|
|
|
##
|
|
## Windchill
|
|
##
|
|
my $feels_like_str = '';
|
|
if (defined $self->{windchill_c}) {
|
|
$feels_like_str = sprintf($_strings{windchill},
|
|
int((($self->{units} eq 'metric') ? $self->{windchill_c}
|
|
: $self->{windchill_f}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'});
|
|
} elsif (defined $self->{heat_index_f}) {
|
|
$feels_like_str = sprintf($_strings{heat_index},
|
|
int((($self->{units} eq 'english') ? $self->{heat_index_f}
|
|
: $self->{heat_index_c}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'});
|
|
}
|
|
|
|
##
|
|
## Relative Humidity
|
|
##
|
|
my $rel_humidity_str = (defined $self->{rel_humidity}) ? sprintf($_strings{rel_humidity},
|
|
int($self->{rel_humidity} + 0.5))
|
|
: '';
|
|
|
|
##
|
|
## Precipitation
|
|
##
|
|
my $precip_str = '';
|
|
if (defined $self->{precip_in}) {
|
|
$precip_str = pretty_print_precip($self, int($self->{precip_mm} * 100 + 0.5) / 100, int($self->{precip_in} * 100 + 0.5) / 100) .
|
|
$_strings{precip_last_hour};
|
|
}
|
|
if (defined $self->{precip_6h_in}) {
|
|
$precip_str .= pretty_print_precip($self, int($self->{precip_6h_mm} * 100 + 0.5) / 100, int($self->{precip_6h_in} * 100 + 0.5) / 100) .
|
|
$_strings{precip_last_6_hours};
|
|
}
|
|
if (defined $self->{precip_24h_in}) {
|
|
$precip_str .= pretty_print_precip($self, int($self->{precip_24h_mm} * 100 + 0.5) / 100, int($self->{precip_24h_in} * 100 + 0.5) / 100) .
|
|
$_strings{precip_last_24_hours};
|
|
}
|
|
if (defined $self->{snow_in}) {
|
|
$precip_str .= sprintf($_strings{precip_snow},
|
|
int((($self->{units} eq 'metric') ? $self->{snow_mm}
|
|
: $self->{snow_in}) + 0.5),
|
|
$_units{$self->{units}.'_depth'});
|
|
}
|
|
|
|
##
|
|
## Min and Max Temperatures
|
|
##
|
|
my $temp_str = '';
|
|
if ((defined $self->{temp_max6h_c}) && (defined $self->{temp_min6h_c})) {
|
|
$temp_str .= sprintf($_strings{temp_min_max_6_hours},
|
|
int((($self->{units} eq 'metric') ? $self->{temp_max6h_c}
|
|
: $self->{temp_max6h_f}) + 0.5),
|
|
int((($self->{units} eq 'metric') ? $self->{temp_min6h_c}
|
|
: $self->{temp_min6h_f}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'});
|
|
} else {
|
|
if (defined $self->{temp_max6h_c}) {
|
|
$temp_str .= sprintf($_strings{temp_max_6_hours},
|
|
int((($self->{units} eq 'metric') ? $self->{temp_max6h_c}
|
|
: $self->{temp_max6h_f}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'});
|
|
}
|
|
if (defined $self->{temp_min6h_c}) {
|
|
$temp_str .= sprintf($_strings{temp_min_6_hours},
|
|
int((($self->{units} eq 'metric') ? $self->{temp_max6h_c}
|
|
: $self->{temp_min6h_f}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'});
|
|
}
|
|
}
|
|
if (defined $self->{temp_max24h_c}) {
|
|
$temp_str .= sprintf($_strings{temp_min_max_24_hours},
|
|
int((($self->{units} eq 'metric') ? $self->{temp_max24h_c}
|
|
: $self->{temp_max24h_f}) + 0.5),
|
|
int((($self->{units} eq 'metric') ? $self->{temp_min24h_c}
|
|
: $self->{temp_min24h_f}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'});
|
|
}
|
|
|
|
##
|
|
## Runway information
|
|
##
|
|
my $runway_str = '';
|
|
if (defined $self->{runway_vis_meter}) {
|
|
$runway_str = sprintf($_strings{runway_vis},
|
|
$self->{runway_nr},
|
|
$self->{runway_vis_meter},
|
|
$self->{runway_vis_ft});
|
|
}
|
|
if (defined $self->{runway_vis_min_meter}) {
|
|
$runway_str .= sprintf($_strings{runway_vis_min_max},
|
|
$self->{runway_nr},
|
|
$self->{runway_vis_min_meter},
|
|
$self->{runway_vis_min_ft},
|
|
$self->{runway_vis_max_meter},
|
|
$self->{runway_vis_max_ft});
|
|
}
|
|
|
|
##
|
|
## Current Weather
|
|
##
|
|
my $weather_str = '';
|
|
if (defined $self->{weather}) {
|
|
$weather_str = sprintf($_strings{current_weather}, $self->{weather});
|
|
}
|
|
|
|
return sprintf($_strings{pretty_print_format},
|
|
$ENV{'CALL_SPACED'},
|
|
$location_str,
|
|
$lcltime_str,
|
|
$wind_str,
|
|
int((($self->{units} eq 'metric') ? $self->{temp_c} : $self->{temp_f}) + 0.5),
|
|
$_units{$self->{units}.'_temperature'},
|
|
$feels_like_str,
|
|
$pressure_str,
|
|
$rel_humidity_str,
|
|
$sky_str,
|
|
$visibility_str,
|
|
$runway_str,
|
|
$weather_str,
|
|
$precip_str,
|
|
$temp_str);
|
|
}
|
|
|
|
########################################################################
|
|
## Helper function to convert speed based on unit.
|
|
## Passed: speed and unit
|
|
## Returns: list of MPS,KTS,MPH
|
|
##
|
|
sub conv_speed {
|
|
my $speed = shift;
|
|
my $units = shift;
|
|
|
|
my $kts = '';
|
|
my $mps = '';
|
|
my $mph = '';
|
|
|
|
if ($units =~ /KT/) {
|
|
$kts = $speed;
|
|
$mps = $speed * 0.51444;
|
|
$mph = $speed * 1.1507695060844667;
|
|
}
|
|
elsif ($units =~ /MPS/) {
|
|
$mps = $speed;
|
|
$kts = $speed / 0.51444;
|
|
$mph = $kts * 1.1507695060844667;
|
|
}
|
|
elsif ($units =~ /KMH/) {
|
|
$mps = $speed * 1000 / 3600;
|
|
$kts = $mps / 0.51444;
|
|
$mph = $kts * 1.1507695060844667;
|
|
}
|
|
return $mps, $kts, $mph;
|
|
}
|
|
|
|
########################################################################
|
|
## Helper function to convert temperature.
|
|
## Passed: temp_Celcius
|
|
## Returns: list of temp_Celsius and temp_Farenheit
|
|
##
|
|
sub conv_temp {
|
|
my $temp_c = shift;
|
|
my $temp_f = ($temp_c * (9/5)) + 32;
|
|
return $temp_c, $temp_f;
|
|
}
|
|
|
|
########################################################################
|
|
## Helper function to convert coded temperatures.
|
|
## Passed: coded temperature
|
|
## Returns: list of temp_Celcius and temp_Faenheit
|
|
##
|
|
sub conv_coded_temp {
|
|
my $temp = shift;
|
|
$temp =~ s/^1/-/;
|
|
my$temp_c = $temp / 10;
|
|
my $temp_f = $temp_c * (9/5) + 32;
|
|
return $temp_c, $temp_f;
|
|
}
|
|
|
|
########################################################################
|
|
########################################################################
|
|
##
|
|
## MAIN PROGRAM
|
|
##
|
|
my $m = new METAR;
|
|
|
|
$m->fetch_metar(); # Read METAR from STDIN and process it
|
|
|
|
my $site = $m->getstnid(); # Retrieve the station ID (ICAO)
|
|
|
|
if ($site) {
|
|
|
|
if (! open ICAO, "< /home/irlp/custom/nsd_cccc.txt") {
|
|
print STDERR "Unable to open file nsd_cccc.txt\n";
|
|
exit 0;
|
|
}
|
|
my ($icao,$blkno,$stnno,$location,$state,$country,$WMOREG,$stnlat,$stnlon,$ualat,$ualon,$stnelv,$uaelv,$RBSN);
|
|
my $found_ICAO = 0;
|
|
while (<ICAO>) { # Find the Station Name
|
|
chomp;
|
|
($icao,$blkno,$stnno,$location,$state,$country,$WMOREG,$stnlat,$stnlon,$ualat,$ualon,$stnelv,$uaelv,$RBSN) = split /\;/;
|
|
if ($icao =~ /$site/) {
|
|
$found_ICAO = 1;
|
|
last;
|
|
}
|
|
}
|
|
close ICAO;
|
|
exit 0 if (! $found_ICAO);
|
|
|
|
$location =~ s/.*?,\s//;
|
|
|
|
print $m->pretty_print($location) . "\n"; # Display METAR in natural language
|
|
} else {
|
|
print "Data for requested station is not available.\n";
|
|
}
|
|
exit 1;
|
|
|