#!/usr/bin/perl
# ==================================================================
# FINAL VERSION: CORRECT BUCHOLTZ RAYLEIGH & FIXED AIR MASS
# - Air Mass: Verified Correct (Solar Geometry Fixed)
# - Rayleigh: Updated to Bucholtz (1995) Analytical Formula
# ==================================================================
use strict;
use warnings;
# --- 1. ERROR HANDLING & HEADERS ---
my $headers_sent = 0;
BEGIN {
$SIG{__DIE__} = sub {
if (!$headers_sent) {
print "Content-type: text/html; charset=UTF-8\n\n";
$headers_sent = 1;
}
print "<h2>Script Error</h2><pre>$_[0]</pre>";
exit;
};
}
print "Content-type: text/html; charset=UTF-8\n\n";
$headers_sent = 1;
# --- 2. MATH CONSTANTS & HELPER FUNCTIONS ---
use constant PI => 3.14159265358979;
sub my_asin {
my $x = shift;
return 0 if $x == 0;
if ($x > 1) { $x = 1; }
if ($x < -1) { $x = -1; }
return atan2($x, sqrt(1 - $x * $x));
}
sub my_acos {
my $x = shift;
if ($x > 1) { $x = 1; }
if ($x < -1) { $x = -1; }
return atan2(sqrt(1 - $x * $x), $x);
}
sub my_fmod {
my ($x, $y) = @_;
return $x - (int($x / $y) * $y);
}
# --- 3. INPUT PARSING ---
sub get_form_data {
my %data;
my $buffer = "";
if (defined $ENV{'REQUEST_METHOD'} && $ENV{'REQUEST_METHOD'} eq 'POST') {
read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'} || 0);
} else {
$buffer = $ENV{'QUERY_STRING'} || "";
}
my @pairs = split(/&/, $buffer);
foreach my $pair (@pairs) {
my ($name, $value) = split(/=/, $pair);
$value =~ tr/+/ /;
$value =~ s/%(..)/pack("C", hex($1))/eg;
$value =~ s/^\s+|\s+$//g; # Trim whitespace
$data{$name} = $value;
}
return %data;
}
my %data = get_form_data();
my $PI = PI;
# --- 4. DEFAULTS ---
$ENV{TZ} = 'America/Los_Angeles';
my @timeData = localtime();
if (!defined($data{'presADA'}) || $data{'presADA'} eq "") {
$data{'presADA'} = 850.00;
$data{'timezoneADA'} = -7.00;
$data{'hourADA'} = $timeData[2];
$data{'minADA'} = $timeData[1];
$data{'secADA'} = $timeData[0];
$data{'monthADA'} = $timeData[4] + 1;
$data{'dayADA'} = $timeData[3];
$data{'yearADA'} = $timeData[5] + 1900;
$data{'latADA'} = 39.5411666;
$data{'lonADA'} = -119.81406;
$data{'wave'} = 532.00;
}
# Assign variables
my $pressure = $data{'presADA'};
my $timezonechosen = $data{'timezoneADA'};
my $hourchosen = $data{'hourADA'};
my $min = $data{'minADA'};
my $sec = $data{'secADA'};
my $mon = $data{'monthADA'};
my $daychosen = $data{'dayADA'};
my $yearchosen = $data{'yearADA'};
my $lat = $data{'latADA'};
my $lon = $data{'lonADA'};
my $wavelength = $data{'wave'};
# --- 5. VALIDATION ---
# Simple regex checks to ensure numeric inputs
my $errpressure = ($pressure =~ /[a-zA-Z]/ || $pressure < 10 || $pressure > 2000) ? '<span class="error">** Error **</span>' : '';
my $errtimezone = ($timezonechosen =~ /[a-zA-Z]/ || $timezonechosen < -12 || $timezonechosen > 12) ? '<span class="error">** Error **</span>' : '';
my $errhour = ($hourchosen =~ /[a-zA-Z]/ || $hourchosen < 0 || $hourchosen > 23) ? '<span class="error">** Error **</span>' : '';
my $errminute = ($min =~ /[a-zA-Z]/ || $min < 0 || $min > 59) ? '<span class="error">** Error **</span>' : '';
my $errsecond = ($sec =~ /[a-zA-Z]/ || $sec < 0 || $sec > 59) ? '<span class="error">** Error **</span>' : '';
my $errmonth = ($mon =~ /[a-zA-Z]/ || $mon < 1 || $mon > 12) ? '<span class="error">** Error **</span>' : '';
my $errday = ($daychosen =~ /[a-zA-Z]/ || $daychosen < 1 || $daychosen > 31) ? '<span class="error">** Error **</span>' : '';
my $erryear = ($yearchosen =~ /[a-zA-Z]/ || $yearchosen < 1900 || $yearchosen > 2200) ? '<span class="error">** Error **</span>' : '';
my $errlat = ($lat =~ /[a-zA-Z]/ || $lat < -90 || $lat > 90) ? '<span class="error">** Error **</span>' : '';
my $errlon = ($lon =~ /[a-zA-Z]/ || $lon < -180 || $lon > 180) ? '<span class="error">** Error **</span>' : '';
my $errwavelength = ($wavelength =~ /[a-zA-Z]/ || $wavelength < 300 || $wavelength > 1000) ? '<span class="error">** Error **</span>' : '';
my $has_errors = ($errpressure . $errtimezone . $errhour . $errminute . $errsecond . $errmonth . $errday . $erryear . $errlat . $errlon . $errwavelength) ne '';
# --- 6. CALCULATION SUBROUTINES ---
sub CalculateRayleigh {
my ($nm, $p) = @_;
# 1. Convert Wavelength (nm) to Microns
# Avoid division by zero
return 0 if ($nm == 0);
my $microns = $nm / 1000.0;
# 2. Bucholtz (1995) / Bodhaine et al (1999) formula constants
# Tau_standard = A * lambda^(-4) * (1 + B*lambda^(-2) + C*lambda^(-4))
# A = 0.008569, B = 0.0113, C = 0.00013
my $lam2 = $microns * $microns;
my $lam4 = $lam2 * $lam2;
# Calculate for Standard Atmosphere (1013.25 mb)
my $tau_std = (0.008569 / $lam4) * (1.0 + (0.0113 / $lam2) + (0.00013 / $lam4));
# 3. Scale for Actual Site Pressure
my $tau_actual = $tau_std * ($p / 1013.25);
return $tau_actual;
}
sub CalculateAirMass {
my ($lat_in, $lon_in, $tz_in, $yr, $mo, $da, $hr, $mi, $se) = @_;
my $Latitude = $lat_in * $PI / 180.0;
# Day of Year
my @days_cum = (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
my $DayOfYear = $da;
if ($mo > 1) { $DayOfYear = $da + $days_cum[$mo]; }
if (($yr % 4 == 0 && $yr % 100 != 0) || ($yr % 400 == 0)) {
if ($mo > 2) { $DayOfYear++; }
}
my $HourLocal = $hr + ($mi / 60.0) + ($se / 3600.0);
# Solar Declination
my $argSolarDeclination = ( ( 360.0 / 365.0 ) * ( $DayOfYear + 284.0 ) ) * $PI / 180.0;
my $SolarDeclination = my_asin( 0.3979 * sin($argSolarDeclination) );
# Equation of Time
my $argEquationOfTime = ( ( 360.0 / 365.0 ) * ( $DayOfYear - 81.0 ) ) * $PI / 180.0;
my $EquationOfTime = ( 9.87 * sin(2.0 * $argEquationOfTime) ) - ( 7.53 * cos($argEquationOfTime) ) - ( 1.5 * sin($argEquationOfTime) );
# Solar Time Correction (Fixed for West Negative Longitude)
my $SolarTimeCorrection = $EquationOfTime + ( 4.0 * $lon_in ) - ( 60.0 * $tz_in );
my $SolarTime = ( 60.0 * $HourLocal ) + $mi + ( $se / 60.0 ) + $SolarTimeCorrection;
# Hour Angle
my $HourAngle = my_fmod( ( ( $SolarTime / 4.0 ) - 180.0 ), 360.0 );
$HourAngle = $HourAngle * $PI / 180.0;
# Zenith Angle
my $CosZenith = ( sin($Latitude) * sin($SolarDeclination) ) + ( cos($Latitude) * cos($SolarDeclination) * cos($HourAngle) );
my $ZenithAngle = my_acos($CosZenith);
my $ZenithAngleDegrees = $ZenithAngle * 180.0 / $PI;
my $AirMass = 0.0;
if ($ZenithAngleDegrees <= 90.0) {
# Kasten and Young (1989)
$AirMass = 1.0 / ( $CosZenith + ( 0.50572 * ( 96.080 - $ZenithAngleDegrees ) ** ( -1.6364 ) ) );
}
return $AirMass;
}
# --- 7. HTML OUTPUT ---
my $results_html = "";
if (!$has_errors) {
my $tau_val = CalculateRayleigh($wavelength, $pressure);
my $am_val = CalculateAirMass($lat, $lon, $timezonechosen, $yearchosen, $mon, $daychosen, $hourchosen, $min, $sec);
my $tauTotal_val = $tau_val * $am_val ;
my $tau_disp = sprintf('%.4f', $tau_val);
my $tauTotal_disp = sprintf('%.4f',$tauTotal_val);
my $am_disp = sprintf('%.4f', $am_val);
$results_html = qq{
<table class="results-box">
<caption><h3>Calculated Results</h3></caption>
<tr><th>Air Mass, m<br><small>at chosen date and time (Kasten and Young 1989)</small></th><td><strong>$am_disp</strong></td></tr>
<tr><th>Rayleigh Scattering Optical Depth, <span class="greek-var-bold">τ</span><br><small>at Chosen Pressure/Wavelength (Bucholtz 1995)</small></th><td><strong>$tau_disp</strong></td></tr>
<tr><th>Total Rayleigh Scattering Optical Depth, <span class="greek-var-bold">τ</span><sub><small>total</small></sub> = m*<span class="greek-var-bold">τ</span></th><td><strong>$tauTotal_disp</strong></td></tr>
</table>
};
}
my $self_url = $ENV{SCRIPT_NAME} || 'AnalyzeSunPhotometerUNR_Rev.cgi';
print <<HTML;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Air Mass Calculator</title>
<style>
body { font-family: Arial, sans-serif; background: #FFFFF0; color: #000; margin: 20px; text-align: center; }
.container { max-width: 950px; margin: 0 auto; }
.results-box { border: 4px solid #336633; background: #F4E3C8; padding: 15px; margin: 20px auto; width: 80%; border-collapse: collapse; }
.results-box th, .results-box td { border: 1px solid #999; padding: 10px; background: #fff; text-align: left; }
.input-box { border: 4px solid #336699; background: #F4E3C8; margin: 20px auto; width: 100%; border-collapse: collapse; }
.input-table td { padding: 4px; }
.error { color: red; font-weight: bold; }
input[type="text"] { width: 120px; padding: 4px; }
input[type="submit"] { padding: 8px 16px; font-weight: bold; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
$results_html
<form method="POST" action="$self_url">
<table class="input-box">
<caption><h3>Sunphotometer Analysis Input</h3></caption>
<tr>
<td valign="top">
<table class="input-table">
<tr><td align="right">$errpressure Pressure (mb)</td><td><input type="text" name="presADA" value="$pressure"></td></tr>
<tr><td align="right">$errtimezone Time Zone</td><td><input type="text" name="timezoneADA" value="$timezonechosen"></td></tr>
<tr><td align="right">$errhour Hour (0-23)</td><td><input type="text" name="hourADA" value="$hourchosen"></td></tr>
<tr><td align="right">$errminute Minute (0-59)</td><td><input type="text" name="minADA" value="$min"></td></tr>
<tr><td align="right">$errsecond Second (0-59)</td><td><input type="text" name="secADA" value="$sec"></td></tr>
<tr><td align="right">$errmonth Month (1-12)</td><td><input type="text" name="monthADA" value="$mon"></td></tr>
<tr><td align="right">$errday Day (1-31)</td><td><input type="text" name="dayADA" value="$daychosen"></td></tr>
<tr><td align="right">$erryear Year</td><td><input type="text" name="yearADA" value="$yearchosen"></td></tr>
<tr><td align="right">$errlat Latitude</td><td><input type="text" name="latADA" value="$lat"></td></tr>
<tr><td align="right">$errlon Longitude</td><td><input type="text" name="lonADA" value="$lon"></td></tr>
<tr><td align="right">$errwavelength Wavelength (nm)</td><td><input type="text" name="wave" value="$wavelength"></td></tr>
<tr><td></td><td><br><input type="submit" value="CALCULATE"></td></tr>
</table>
</td>
<td align="center">
<div style="width:300px; height:300px; border:1px solid #ccc; display:flex; align-items:center; justify-content:center; background:white;">
<img src="sunPhotometerBasics.png" alt="Sun Photometer Diagram" style="max-width:100%; max-height:100%;">
</div>
</td>
</tr>
</table>
</form>
<div style="margin-top:30px; color:#666; font-size:0.8em;">
<iframe src="http://free.timeanddate.com/clock/i2oq821e/n599/fn6/fs18/fcff0/tc000/ftb/bas2/bat1/bacfff/pa8/tt0/tw1/th1/ta1/tb4" frameborder="0" width="242" height="64"></iframe>
<p><i>By W. Patrick Arnott and Ben Sumlin</i></p>
</div>
</div>
</body>
</html>
HTML
exit;