#!/usr/bin/perl -w
# (C) Copyright 2010, met.no
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
# pod included at end of file
use strict;
use Getopt::Long;
use Pod::Usage qw(pod2usage);
use constant DEFAULT_TABLE_PATH => '/usr/local/lib/bufrtables';
my $BUFRDUMP = '/metno/local/bin/bufrdump';
# Parse command line options
my %option = ();
GetOptions(
\%option,
'help',
'tablepath=s', # Set BUFR table path
'filter=s', # Decode observations meeting criteria in <filter file> only
'param=s', # Decode the parameters in <parameter file> only
'csv', # Use CSV format for printing
'sort', # Sort on stationid (wmonr/call_sign/buoy_id)
'sort_on=s', # Sort on specified parameter
'transform=s', # Do the transformations in <transformation file>
'lon1=i',
'lat1=i',
'lon2=i',
'lat2=i',
) or pod2usage(-verbose => 0);
# User asked for help
pod2usage(-verbose => 1) if $option{help};
# Make sure there is at least one input file
pod2usage(-verbose => 0) unless @ARGV;
# --csv can only be used together with --param
pod2usage(-verbose => 0) if $option{csv} && !$option{param};
# --sort and --sort_on are exclusive
pod2usage(-verbose => 0) if $option{sort} && $option{sort_on};
# Prevent ECMWF software from printing table info
$ENV{PRINT_TABLE_NAMES} = 'false';
# Set BUFR table path environment variable used by bufrdump
if ($option{tablepath}) {
# Command line option --tablepath overrides all
$ENV{BUFR_TABLES} = $option{tablepath};
} elsif (!$ENV{BUFR_TABLES}) {
$ENV{BUFR_TABLES} = DEFAULT_TABLE_PATH;
}
# ECMWF software requires trailing '/' in bufrpath
$ENV{BUFR_TABLES} .= '/' if substr($ENV{BUFR_TABLES},-1) ne '/';
my $filter = $option{filter} ? "--filter $option{filter}" : '';
my $lon1 = $option{lon1} ? "--lon1 $option{lon1}" : '';
my $lat1 = $option{lat1} ? "--lat1 $option{lat1}" : '';
my $lon2 = $option{lon2} ? "--lon2 $option{lon2}" : '';
my $lat2 = $option{lat2} ? "--lat2 $option{lat2}" : '';
my $criteria_ref;
$criteria_ref = read_filter_file($option{filter}) if $filter;
my $param_file = $option{param} ? $option{param} : '';
my ($forced_params_ref, $params_ref);
($forced_params_ref, $params_ref)
= read_param_file($param_file) if $param_file;
my $csv = $option{csv} ? 1 : 0;
# First line in CSV should be the parameters
print join(',', @$params_ref) . "\n" if $csv;
my $transform_file = $option{transform} ? $option{transform} : 0;
my $transform_ref;
$transform_ref = read_transformation_file($transform_file) if $transform_file;
my $sort = $option{sort} ? 1 : 0;
my $sort_on = $option{sort_on} ? $option{sort_on} : '';
# What kind of sorting is required?
my $sort_by;
if ($sort_on) {
my @ascii_params = qw(call_sign icao_id obstime name type);
my $ascending_sort = 1;
# A minus sign appended to the sort parameter means descending sort
if ($sort_on =~ /-$/) {
$ascending_sort = 0;
chop $sort_on;
}
# Just in case someone adds a '+' to signify ascending sort
if ($sort_on =~ /[+]$/) {
$ascending_sort = 1;
chop $sort_on;
}
if (grep {$_ eq $sort_on} @ascii_params) {
if ($ascending_sort) {
$sort_by = sub {$a cmp $b};
} else {
$sort_by = sub {$b cmp $a};
}
} else {
if ($ascending_sort) {
$sort_by = sub {$a <=> $b};
} else {
$sort_by = sub {$b <=> $a};
}
}
}
# Loop for processing of BUFR input files
my %data_of = ();
foreach my $inputfname (@ARGV) {
# Dump the content of the BUFR file using the Fortran program $BUFRDUMP
my $dump = `$BUFRDUMP $filter $lon1 $lat1 $lon2 $lat2 $inputfname`;
die if $?; # Reason for bufrdump failing should have been printed to STDERR
# Then process the output from the dump
my @lines = split /\n/, $dump;
# Add an empty line to simplify processing
push @lines, '';
my $station = '';
# Skip first(blank) line
shift @lines;
my @lines_to_print;
my %msg; # Hash with parameter name as key, parameter value as value
LINE:while (defined(my $line = shift @lines)) {
# Each new message starts with a blank line
if ($line !~ /^\s*$/) {
# Build up the message to be (possibly) printed
my ($param, $value) = ($line =~ /^(.+)=\s*(.*)$/);
if ($transform_file && $transform_ref->{$param}) {
# Replace value with transformed value
my $transform = $transform_ref->{$param};
$transform =~ s/\$x/$value/g;
$value = eval $transform;
die "Couldn't parse following transform:\n"
. $transform . "\nReason:\n$@" if $@;
$line =~ s/=.*/=$value/;
}
$msg{$param} = $value;
push @lines_to_print, $line;
}
if ($line =~ /^\s*$/ or @lines == 0) {
# A full message has been completed. Should it be printed?
if ($filter && filter_obs(\%msg, $criteria_ref)) {
# Skip this message
} else {
# Print the message (or if --sort or --sort_on: save the message)
my $txt = '';
if ($param_file) {
# Print the params in @$params_ref if exists in
# message, in same order as in @$params_ref
foreach my $name (@$params_ref) {
if (exists $msg{$name}) {
$txt .= $csv ? $msg{$name} . ',' : "$name=$msg{$name}\n";
} elsif ($forced_params_ref->{$name}) {
$txt .= $csv ? '-32767,' : "$name=-32767\n";
} elsif ($csv) {
$txt .= ',';
}
}
} else {
foreach my $line2 (@lines_to_print) {
$line2 =~ s/=\s+/=/;
$txt .= $line2 . "\n";
}
}
chop $txt if $csv; # removes last ','
if ($txt) {
if ($sort) {
# Sort wmonr before call signs before buoy_id
if ($msg{wmonr}) {
$station = '00_' . $msg{wmonr};
} elsif ($msg{call_sign}) {
$station = '10_' . $msg{call_sign};
} elsif ($msg{buoy_id}) {
$station = '10_' . $msg{buoy_id};
} else {
# Skip observation if no station identification found
next LINE;
}
$data_of{$station} = exists $data_of{$station}
? $data_of{$station} . "$txt \n" : "$txt \n";
} elsif ($sort_on) {
my $val = exists $msg{$sort_on} ? $msg{$sort_on} : '';
$data_of{$val} = exists $data_of{$val}
? $data_of{$val} . "$txt \n" : "$txt \n";
} else {
# No sorting. We can print the line immediately
print $txt .= "\n" if $txt;
}
}
$txt = '';
} # Finished message
@lines_to_print = ();
%msg = ();
}
}
}
# If sorting requested, we cannot print before now
if ($sort) {
for (sort keys %data_of) {
print $data_of{$_};
}
} elsif ($sort_on) {
# Print observations with missing value for the sort parameter lastly
my $data_of_missing_value = $data_of{''} || '';
delete $data_of{''};
for (sort $sort_by keys %data_of) {
print $data_of{$_};
}
print $data_of_missing_value if $data_of_missing_value;
}
sub read_param_file {
my $parameter_file = shift;
open my $PARAM, '<', $parameter_file
or die "Cannot open $parameter_file: $!";
my %forced_params;
my @params;
# Read the parameters into @params, those preceded by an
# exclamation mark also into %forced_params, skipping blank lines
# and comment lines
while (my $name = <$PARAM>) {
$name =~ s/^\s+//;
$name =~ s/\s+$//;
next if !$name || $name =~ /^#/;
if ($name =~ /^!/) {
$name = substr $name, 1;
$forced_params{$name} = 1;
}
push @params, $name;
}
close $PARAM or die "Cannot close $parameter_file: $!";
return \%forced_params, \@params;
}
sub read_transformation_file {
my $transform_file = shift;
open my $TRANSFORM, '<', $transform_file
or die "Cannot open $transform_file: $!";
# Read in the transformations, skipping blank lines and comment
# lines
my %transform_of;
while (my $line = <$TRANSFORM>) {
$line =~ s/^\s+//;
$line =~ s/\s+$//;
next if !$line || $line =~ /^#/;
my ($param, $transform) = split /=/, $line, 2;
die "Invalid transformation in $transform_file:\n$line\n"
unless $transform;
$param =~ s/\s+$//;
$transform =~ s/^\s+//;
$transform_of{$param} = $transform;
}
return \%transform_of;
}
sub read_filter_file {
my $filter_file = shift;
my @allowed_operators =
('=',
'<',
'<=',
'>',
'>=',
'!=',
);
my @criteria;
open my $FILTER, '<', $filter_file
or die "Cannot open $filter_file: $!";
# Skip the criteria meant for Fortran parsing, i.e. proceed to
# first line following a blank line
while (<$FILTER>) {
last if $_ =~ /^\s*$/;
}
# Read the filter criteria meant for Perl parsing, skipping blank
# lines and comment lines
while (my $line = <$FILTER>) {
$line =~ s/^\s+//;
$line =~ s/\s+$//;
next if !$line || $line =~ /^#/;
push @criteria, $line;
}
# Check that the criteria are properly formatted
foreach my $criterium (@criteria) {
# Naked parameter is ok
next if $criterium =~ /^\w+$/;
my $op = (split / +/, $criterium)[1];
if (!defined($op) or grep(/[+*?\\]/, $op) or !grep(/^$op$/, @allowed_operators) ) {
print "Error in $filter_file, line $. is badly formatted"
. " or operator not supported:\n$criterium";
exit 1;
}
}
return \@criteria;
}
# Return true (1) if observation is to be filtered, i.e. does not
# comply with at least one of the <param> or <param> <operator>
# <value> filter criteria in filter file
sub filter_obs {
my $msg_ref = shift;
my $criteria_ref = shift;
my @ascii_params = qw(call_sign icao_id name obstime type);
foreach my $criterium (@$criteria_ref) {
my ($f_param, $f_operator, $f_value) = split / +/, $criterium, 3;
return 1 unless exists $msg_ref->{$f_param};
next if not defined $f_operator; # Naked parameter criterium. Parameter
# present, so criterium fulfilled
chomp $f_value;
if ($f_operator eq '=') {
if (grep {$_ eq $f_param} @ascii_params) {
$msg_ref->{$f_param} =~ s/\s*$//;
return 1 unless $msg_ref->{$f_param} eq $f_value;
} else {
return 1 unless $msg_ref->{$f_param} == $f_value;
}
} elsif ($f_operator eq '<') {
return 1 unless $msg_ref->{$f_param} < $f_value;
} elsif ($f_operator eq '<=') {
return 1 unless $msg_ref->{$f_param} <= $f_value;
} elsif ($f_operator eq '>') {
return 1 unless $msg_ref->{$f_param} > $f_value;
} elsif ($f_operator eq '>=') {
return 1 unless $msg_ref->{$f_param} >= $f_value;
} elsif ($f_operator eq '!=') {
if (grep {$_ eq $f_param} @ascii_params) {
$msg_ref->{$f_param} =~ s/\s*$//;
return 1 unless $msg_ref->{$f_param} ne $f_value;
} else {
return 1 unless $msg_ref->{$f_param} != $f_value;
}
}
}
# All filter conditions have been fullfilled
return 0;
}
=pod
=head1 SYNOPSIS
bufrdump.pl <bufr file(s)>
[--filter <filter file>]
[--param <parameter file> [--csv]]
[--sort | --sort_on <parameter>[-]]
[--transform <transformation file>]
[--lon1 x1]
[--lat1 y1]
[--lon2 x2]
[--lat2 x2]
[--tablepath <path to BUFR tables>]
[--help]
=head1 DESCRIPTION
Extracts BUFR messages from BUFR file(s) and prints section 4 as
"parameter=value" lines. Calls the Fortran program bufrdump
internally, so this program must be installed at the location set in
variable $BUFRDUMP in source code.
Execute without arguments for Usage, with option C<--help> for some
additional info. See also L</https://wiki.met.no/bufr.pm/start> for
examples of use.
=head1 OPTIONS
--filter <filter file>
Decode observations meeting criteria in <filter file> only
--param <parameter file> [--csv]
Print parameters in <parameter file> only, in same order
as they occur in <parameter file>. If --csv, the parameters
vill be printed using the CSV (comma separated values) format
--sort Sort the decoded observations on station identification;
first stations with wmonr, then stations with call sign,
then stations with buoy_id (others left out)
--sort_on <parameter>[-] Sort the decoded observations on increasing
values of <parameter>, or decreasing values if a '-'
follows the parameter name. Observations not containing
the parameter at all will be printed lastly. E.g.
--sort_on TA- will sort on decreasing temperatures
--transform <transformation file>
Do the transformations of parameter values listed in
<transformation file>
--lon1 x1 Decode observations with longitude >= x1 only
--lat1 y1 Decode observations with latitude >= y1 only
--lon2 x2 Decode observations with longitude <= x2 only
--lat2 y2 Decode observations with latitude <= y2 only
x1,y1,x2,y2 should be decimal degrees
--tablepath <path to BUFR tables>
Set path to BUFR tables (overrides ENV{BUFR_TABLES})
--help Print this Usage
Options may be abbreviated, e.g. --h or -h for --help.
To avoid having to use the C<--tablepath> option, you are adviced to
set the invironment variable BUFR_TABLES to the directory where your
BUFR tables are located (unless the default path provided by
bufrdump.pl works for you).
The lines in <parameter file> should be name of the parameters you
want to be printed. For example, if you want only station
identification and temperature to be printed for a BUFR SYNOP file,
the <parameter file> should look like this:
wmonr
call_sign
TA
If you want "parameter=value" to be printed also when value is missing
in BUFR message, precede the parameter name with an exclamation mark
(e.g. '!TA'). Missing values will then be displayed as -32767.
If --csv is used in conjunction with --param, all values will be
printed using the CSV format, with first line listing the parameters,
and with missing fields printed as -32767 if the parameter is marked
with '!' in parameter file. With the parameter file above, the listing
may for example start like
wmonr,call_sign,TA
01001,,-1.5
,LF5U,9.0
Using --filter will decode only those observations that meet at least
one of the BUFR descriptor criteria and all of the parameter criteria
in <filter file>, where the BUFR descriptor criteria should come first
in filter file followed by a blank line, then comes the parameter
criteria which should match <param> or <param> <operator> <value>
where operator is one of =, !=, <, <=, > and >=. An example filter
file is
D: 001001 I2.2
01
D: 001001 I2.2 001002 I3.3
03 895
06 252
D: 001011 A9
LF5U
type = Manned
NN != 8
TA >= 5
TA < 9.5
RR_24
which decodes all observations with block number 01, two other
specific wmo stations and one specific ship, being manned stations and
having cloud cover different from 8 (but NN must be part of the
message) and temperature between 5 and 9.5 degrees Celsius and
containing precipitation for last 24 hours. Comment lines starting
with # will be ignored.
Another example: the simple filter file (starting with a blank line!)
wmonr
will print only those observations containing a wmonr (skipping
ships).
The --transform option is provided mainly to be able to use other
units than what is default in bufrdump.pl. The transformation file
should list the transformations wanted, one per line as
<parameter> = <perl expression involving $x>
where $x is original value of the parameter.
For example, the following transformation file will display wind speed
FF and wind gust FG in knots instead of m/s, rounded to one decimal,
and cloud cover NN in % (instead of the default which is using WMO
code table 2700, roughly counting octas):
FF = sprintf("%.1f", $x*1.9438)
FG = sprintf("%.1f", $x*1.9438)
NN = int($x*12.5 + .5)
If --transform is combined with --filter, the filter criteria should
refer to the transformed values. E.g. if the above NN transform to %
is to be applied for sky not all covered by clouds, you should use NN
!= 100 instead of NN != 8 in filter file.
=head1 AUTHOR
Pål Sannes E<lt>pal.sannes@met.noE<gt>
=head1 COPYRIGHT
Copyright (C) 2010 met.no
=cut