#!/usr/bin/env perl =pod =head1 NAME SkewtTable.pm =head1 DESCRIPTION Modules for common functions for skewt. Subroutine ReadConfigFile should be called first in order to initialize the parameters =head1 DEPENDENCIES The formats of the list and config. files. =head1 HISTORY Created: 2016-07-10, Original Code. Based on IASI Get_L2_Metadata.pl - Oleg Roytburd (SGT Inc) 2023-05 Modified for NCCF. Support CGI functions only. =head1 SUBROUTINES =cut package SkewtTable; use warnings; use strict; use File::Basename; use Exporter; use Carp; use constant DEBUG => $ENV{PERLDEBUG}; BEGIN { if (DEBUG) { require Data::Dumper; Data::Dumper->import ('Dumper'); } else { sub Dumper {}; } } our @ISA = qw (Exporter); our @EXPORT = qw (GetIconName GetSkewtImagePath ReadConfigFile %Config CalculateTableParams ReadSoundingColors SortSkewt WEB_ROOT_DIR SKEWT_DIR IMAGES_DIR IMG_ID_FMT LogMsgTS LOG_INFO LOG_WARN LOG_ERROR LOG_DEBUG LOG_NEWLINE ); # Logging constants use constant { LOG_INFO => 0, LOG_WARN => 1, LOG_ERROR => 2, LOG_DEBUG => 3, LOG_NEWLINE => "\n". ' ' x29 }; use constant SECONDS_PER_DAY => 24 * 3600; use constant { # (DOCUMENT_ROOT env. var. is defined only if the script is called by a webserver) WEB_ROOT_DIR => '/nccfdata/Visualization01', # Webserver path to the skewt page elements SKEWT_DIR => '/cData/Atmosphere/Soundings/skewt/', # The images sub-directory IMAGES_DIR => 'images', # The subdirectory containing the sorted lists of sounding directories SORTED_DIR => 'sorted', }; # Constants for file locations use constant { SKEWT_CONFIG_FILE => WEB_ROOT_DIR. SKEWT_DIR. 'config.properties', SND_COLORS_FILE => WEB_ROOT_DIR. SKEWT_DIR. 'snd_colors.bin', }; # To to form image ID from its index: use constant IMG_ID_FMT => '%06d'; # Map parameters use constant MAP_PARAMS => qw ( lat_min lat_max lon_min lon_max num_rows num_cols bg_width bg_height grid_bg_x_min grid_bg_x_max grid_bg_y_min grid_bg_y_max format num_observations ); # Icon colors. Must match those in skewt_utils.py use constant COLORS => ( 'FF00FF', '0000FF', '0055FF', '00AAFF', '00FFFF', '00FFAA', '009933', '00FF00', 'AAFF00', 'FFFF00', 'FFD000', 'FFAA00', 'FF5500', 'FF0000', '000000', 'FFFFFF' ); # Externally available parameters our %Config; =head2 SortSkewt ($idx) Sort the sounding directories by the age of their soundings. $idx - grid index for the sounding Return: a list of directories, sorted by sounding modification time, the most recent first. =cut sub SortSkewt { my ($idx) = @_; my @sortedDirs; # Open the metadata file my $sortedDirsFile = WEB_ROOT_DIR. SKEWT_DIR. SORTED_DIR. '/g'. sprintf (IMG_ID_FMT, $idx); open (my $fh, "<", $sortedDirsFile) or do { warn qq (Error(s) opening sorted directories file "$sortedDirsFile": $!) if DEBUG; return \@sortedDirs; }; while (<$fh>) { push @sortedDirs, (split ',')[0]; } return \@sortedDirs; } =head2 GetSkewtImagePath ($idx, $dir) Generate the _webserver_ data path to an skewt image file based on its index. $idx - grid index for the sounding $dir - image directory, currently "D1" to "Dxx" (optional) Returns: image name only, or webserver (i.e. relative to the webpage root) path to image, if $dir is defined. =cut sub GetSkewtImagePath { my ($idx, $dir) = @_; my $imgName = 'g'. sprintf (IMG_ID_FMT, $idx); if (DEBUG) { warn '$imgName =', Dumper $imgName; warn '%Config =', Dumper \%Config; warn '$dir =', Dumper $dir; } if (defined $dir) { return SKEWT_DIR. IMAGES_DIR. "/$dir/$imgName.$Config{format}"; } else { return $imgName; } } # End GetSkewtImagePath =head2 ReadConfigFile ReadConfigFile ($filename); =over =item $filename (Map) configuration file - optional =back PURPOSE: Read a configuration file that consists of lines of the form KEYWORD=VALUE, and return a reference to a hash from keywords to their values. die() if the requested file cannot be read. Ignore lines that do not match the KEYWORD=VALUE pattern, and also ignore lines that start with the comment character '#'. HISTORY: Oleg Roytburd 2016-03-02 Original Code. Based on KeywordEqualsValueFile::read (by Eric Bosch, ca. 2011) =cut sub ReadConfigFile { my ($filename) = $_[0] || SKEWT_CONFIG_FILE; # Initialize the attributes hash: {param_name => param_value} # with parameter names only. @Config{@{[MAP_PARAMS]}} = undef; # Open the file for reading as filehandle $fh. The file handle will # automatically close when this subroutine exits and the variable # $fh goes out of scope. open (my $fh, "<", $filename) or do { warn qq (Error(s) opening configuration file "$filename": $!); return; }; while (my $line = <$fh>) { my ($keyword, $value) = ( $line =~ /^\s* # Scan past leading whitespace, if any ([^[:space:]\#].*?) # Don't allow lines starting # with '#'. Grab as few # characters, starting with a # non-whitespace character, as # are needed to make the regexp match. \s*=\s* # Scan past any whitespace immediately # preceding or after the equal sign. (.*?)\s* # Grab the value portion, ignoring any # trailing whitespace. $/x ); # Pattern finished next unless (defined $keyword); # Skip to next line if pattern match # failed. # Store values only for keywords already existing in the hash. $Config{$keyword} = $value if (exists $Config{$keyword}); } close $fh; return 1; } # End ReadConfigFile =head2 CalculateTableParams Collect and calculate parameters for the webpage table (or div tags, etc) =cut sub CalculateTableParams { my %p; $p{imgWidth} = $Config{bg_width}; $p{imgHeight} = $Config{bg_height}; $p{gridX0} = $Config{grid_bg_x_min}; $p{gridY0} = $Config{grid_bg_y_min}; $p{nRows} = $Config{num_rows}; $p{nCols} = $Config{num_cols}; # parameters added c. 7 November 2019 (G. Michalitsianos) $p{MinLon} = $Config{lon_min}; $p{MaxLon} = $Config{lon_max}; $p{MinLat} = $Config{lat_min}; $p{MaxLat} = $Config{lat_max}; # (0.5 is added to achieve the rounding effect): $p{colWidth} = int (($Config{grid_bg_x_max} - $Config{grid_bg_x_min}) / $p{nCols} + 0.5); $p{rowHeight} = int (($Config{grid_bg_y_max} - $Config{grid_bg_y_min}) / $p{nRows} + 0.5); return \%p; } =head2 LogMsgTS (@args) Print a time-stamped message to the log The 1-st argument C<$args[0]> could be the log file handle (SELECT'ed filehandle will be used by default) The next argument must be the message The last argument, if defined, indicates the message severity level. Examples: LogMsgTS ("Test") - prints "Info" message to the default output LogMsgTS ("Test2", LOG_ERROR) - prints "Error" message to the default output LogMsgTS (\*STEDRR, "Test3") - prints "Info" message to std. err LogMsgTS ($fh, "Test3") - prints "Info" message to $fh =cut sub LogMsgTS { my $sMsg = shift () or do { warn "Internal Error: @{[caller 0]}[3]: no arguments"; return; }; my $fh; if (ref $sMsg eq 'GLOB') { $fh = $sMsg; $sMsg = shift (); } # Message severity level my $level = shift; if (!defined $level || $level == LOG_INFO) { $level = " INFO"; } elsif ($level == LOG_WARN) { $level = " WARN"; } elsif ($level == LOG_DEBUG) { $level = "DEBUG"; } else { $level = "ERROR"; } my $scriptName = basename $0; my $sTime = substr gmtime, 4, 15; # Format gmtime output my $sEndMsg = "[$sTime UTC $level] $scriptName:\n$sMsg\n"; if ($fh) { print $fh ($sEndMsg); } else { print $sEndMsg; } return 1; } =head2 NumObservations @return the number of P-SkewT observations. It is read from the config. file Default = 10 =cut sub NumObservations { return $Config{num_observations} || 10; } =head2 SoundingDirs @return the list of sounding directories containing P-SkewT images, based on the defined number of observations, eg. "D1, D2, ... D10" =cut sub SoundingDirs { return map {"D$_"} (1 .. NumObservations ()); } =head2 ReadSoundingColors Read the sounding colors from the list file. =cut sub ReadSoundingColors { open my $in, '<:raw', SkewtTable::SND_COLORS_FILE () or confess (<