Disclaimer and warning!

DISCLAIMER AND WARNING: A significant characteristic of these scripts is that all aspects of the forecast, at any point in time, are calculated/interpolated/etc, through software written by an amateur hobbyist with no training or expertise in weather forcasting or the display of weather information whatsoever. This means that the results could, at any time, be subtly or wildly wrong. You bear all risk for the use of and/or reliance on this software package. Read more about this, below. SO BEWARE!


This library is free software; you can use or redistribute it and/or modify it under the terms of version 2.1 of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License, AS LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left unchanged with all versions of this script, or collection of scripts and/or supporting items:

DISCLAIMER:



This script or collection of scripts and/or supporting items (hereafter called "this software package") 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 Lesser General Public License version 2.1 for more details. Beyond the lack of warranty listed above, it is required that any users of this software package accept and agree that the author of this software package, or any user of it, or entity providing all or portions of this software package within their weather package, or any other kind of package, is making no claim as to the accuracy of this information. The information, and forecast details this software package provides, and all calculations it performs, are the outputs of the work of an amateur hobbyist, the original author. Therefore, the outputs, return values, print outs, etc., of this software package are entirely experimental, and could very well be partially or wholly wrong, missing data, etc. You must view the outputs of this software package, or any package derived from it, as the unofficial hobby results an amateur hobbyist's work, and not that of a professional meteorologist. So, do not base personal decisions, or professional decisions, or really any decision at all, wholly or even the smallest bit partially, on the results, printouts, outputs, return values, etc., of this software package! If you cannot or are unwilling to accept full liability and responsibility for your use of this software package, or you cannot or are unwilling to indemnify the author, web hoster, etc. of this software package for any loss of any kind, that you or others that view or use data data, of any kind, produced by your use of this software pacakge, thend any and all rights for you to use it are completely and permanently revoked, and you must not use, leverage, call, reference, host, etc. this software package in any way, on any medium, or any machinery. You should have received a copy of the GNU Lesser General Public License version 2.1 along with this script or collection of scripts; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA, 02111-1307, USA


Architecture of the software

Each author of weather forecasting scripts has their own goals in writing their software, and their decisions as to how to structure their software are naturally guided by those goals. Here are some (not all!) examples of goals and the design decisions made in service to those goals:
  1. Some put in a great deal of focus on web page style and generation. With these, there's a lot of emphasis on CSS and the ability to customize it, the presentation of the material, etc. Sometimes, you'll find that the data collection from external websites (such as the National Weather Service (NWS)) is embedded in the code that generates the text and graphics of the actual web page. Some others might isolate the data collection as functons or separate include files.
  2. Some focus on being leveraged (included) by websites and scripts in a very clean, modular manner - making their code a fantastic reusable package leveraged by hundreds or thousands of website developers. In these packages, the data aquisition and processing might be separated from the website text and graphics generation by the use of separate files for each 'type of thing' (forecasts, alerts, satellite images, etc.), and some may make individual PHP files contain code for both data acquisition and presentation - which allows for easy use by others, who grab a single script for both acquisition and data presentation.
The primary script of this package, weather-parser.inc.php is designed to be entirely focused around 'data collection' because the primary goal of this author was to utilize the results of the data collection by scripts or programs that had nothing to do with web pages. Then, the author added forecast-to-html.inc.php and foreast.php to call that primary script and actually make HTML output that could be included in a web page... while other applications written by the author use the information for quite different purposes. Again, the value of a complete split between weather forecast and information obtainment on one hand, and display/use of that information, on other, is most seen in a series of small scripts that do things quite different than building a web page. These scripts do things such as send notices of very cold evenings as freeze alerts to both email addresses and phone numbers, or generate files to be read by Amazon Alexa skills.

Scripts included on this page


This is the primary script:
  1. weather-parser.inc.php is included (via 'require_once') in any PHP script that wants to obtain weather forecast (with alert information). The information is returned in a multi-dimensional PHP array, for the user of this script to use as it chooses. At the end of the script are a list of acknowledgements - thanking other weather scripters who work influenced the author, or who wrote scripts that are used by this script (or others in this collection.)
These are the scripts that use the weather forecast information for different purposes:
  1. forecast-to-html.inc.php Generates a bare-bones HTML-based 'table' of weather information suitable for presenting on a web page, by referencing and calling the code in weather-parser.inc.php. The only CSS is contained in HTML tags such as "span" and "tr" or "td", no exciting use of graphics. Its purpose is two-fold: First, to show how such forecast data could be displayed, and second, to show how it is retrieved from the results of using the weather-parser.inc.php script.
  2. forecast.php is an example of a very simple script that calls forecast-to-html.inc.php to obtain current HTML-formatted weather forecasts and alerts information. The author uses foreast.php in his own WeeWX-based weather web page. (as an aside, making Python and PHP play together was a tedious chore, but was required for the author's application. Thankfully, this two-language dance-step interaction is not part of the system presented here!)
  3. coldwarn.php takes the forecast information, and if the evening low is less than a specified value, sends text and email warnings out to folks.
  4. alexa.php takes the forecast information, and generates a series of text files to be read from your website by an Amazon Alexa skill.
  5. getnwsinfo.php is a script to help you obtain some of the information you will need to call 'obtain_forecast', an example of such a call is in the forecast.php script referenced above. The getnwsinfo.php script will obtain values you will need to give the 'obtain_forecast' function - just edit this script to contain your latitude and longitude, and run it from a web browser to see the results.
This are a few miscellaneous items:
  1. getweather.php is a script that allows a person to invoke your website to run these scripts to produce a table of the upcoming forecast, taking parameters from the URL to get the desired forecast.
  2. us-epa-realtime-single-sample-aqi.inc.php is a script that takes in various particulate matter (PM) readings and then produces AQI values for them. The caller can specify one of several calculations to use to produce the AQI.
  3. The GNU Lesser General Public License v2.1 Some packages used by these scripts use that license, which must be included, and these scripts use this license as well.

weather-parser.inc.php

<?php

// THIS CODE REQUIRES PHP 8.2 OR ABOVE. You can make it POSSIBLY run on PHP 7.0
// if you get rid of the 'readonly' keywords in the definitions of the private
// arrays in the WeatherParser class. Note that the code checks for the PHP
// version, so if you want to downgrade it to support PHP before V8.2, change
// the version check code.

ini_set('display_errors', 'On');
error_reporting(E_ALL | E_STRICT);

// OPEN SOURCE INFORMATION IS CONTAINED AT THE END OF THIS FILE!

//
// WEATHER PARSER
// --------------
//
// This is an experimental Data Extractor and Decoder of United States National
// Weather Service (NWS) Weather Forecasts and Alerts, which are used to also
// generate and format Human-Readable/Viewable Forecasts and Alerts.
//
// ---------------------------- USER BEWARE --------------------------------
//
// DISCLAIMER AND WARNING: A significant characteristic of this software
//                         package is that all aspects of the forecast and
//                         alert system are the results of the software
//                         programming work of an amateur hobbyist who has
//                         absolutely no training in meteorology, the use of
//                         NWS data to generate its output, or anything else
//                         concerning weather and alert information generation
//                         and dissemination! The forecasts and alerts
//                         generated by this software package are calculated
//                         from the raw data provided by the National Weather
//                         Service (NWS) NDFD XML/SOAP forecast feeds and NWS
//                         CAP XML/JSON alert feeds. Since this software
//                         package takes this raw NWS data and manipulates it,
//                         creates speculative results from it, and formats
//                         these speculations to produce a result, it is
//                         entirely possible that this experimental software
//                         package could generate results that at any time, may
//                         be correct, or totally wrong, or somewhere
//                         in-between. You should not rely on the results of
//                         this software package in any way, at any time, for
//                         any purpose, nor make any decisions at all based on
//                         the forecasts and alerts produced by this software
//                         package.
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//
// USAGE:
//
//  In your PHP file, do this to include this class:
//
//    require_once('weather-parser.inc.php');
//
//  Then to use it do the following:
//
// 1) Create a new instance of a Weather Parser object.
//
//    $weather_parser = New WeatherParser();
//
// 2) Call it, specifying parameters for usage. MOst parameters are required.
//    There are a few that have defaults if not provided. Those that are
//    optional will be marked as such and a default value is shown in this
//    list.
//
//  $forecast_result =
//   $weather_parser->obtain_forecast(array
//   ('latitude' => 37.263,
//    'longitude' => -122.003,
//    'placename' => 'Saratoga, CA',
//    'timezone' => 'America/Los_Angeles',
//    'nws-zone' => 'CAZ513',
//    'nws-county' => 'CAC085',
//    'nws-forecast-office' => 'MTR',
//    'avoid-percentage-icons' => false,    // OPTIONAL! Default: false
//    'show-duplicate-alerts' => false,     // OPTIONAL! Default: true
//    'ignore-cache' => false,              // OPTIONAL! Default: false
//    'forecast-source' => 'xml',           // OPTIONAL! Default: 'xml'
//    'alert-county-test-file' => '',       // OPTIONAL! Default: ''
//    'alert-zone-test-file' => '',         // OPTIONAL! Default: ''
//   );
//
// Special notes:
//
//     'latitude'
//     'longitude' is where you are at, in decimal degrees (such as 38.123).
//
//                 Note: latitudes north of the equator are positive values,
//                       south of equator are negative values. longitudes west
//                       of Greenwhich, UK are negative values, and east of it,
//                       positive values.
//
//     'placename' is whatever you want it to read.
//
//     'timezone' is a specification of the 'tz database timezone' (also known
//                as the 'zoneinfo timezone' for your station. For example,
//                people in San Jose, California are in the
//                "America/Los_Angeles" 'tz database/zoneinfo' timezone. You
//                can look up your tz timezone by going to this site:
//
//                   https://php.net/manual/en/timezones.php
//
//     'nws-zone' is the NWS forecast zone name for your desired location. If
//                you have a copy of the 'getnwsinfo.php' script, you can set
//                the latitude and longitude at the top of that script, run it
//                from a browser, and use the value for nws-zone provided by
//                it. If you don't have that script, you can find your NWS zone
//                by going to:
//
//                  https://api.weather.gov/points/latitude,longitude
//
//                The output will contain a 'forecastZone' - the value at the
//                end will be your zone. For example:
//
//                  https://api.weather.gov/points/37.26,-122
//
//                has 'CAZ516' at the end of 'forecastZone' and that's the zone
//                you would use for the latitude/longitude of 37.26 and -122.
//                The reason this system requires you to specify it, versus
//                looking it up itself, is to simply avoid a per-use call to
//                the government server, when the information tends not to
//                change unless your station changes location.
//
//     'nws-county' is the NWS forecast county name for your desired location.
//                  If you have a copy of the 'getnwsinfo.php' script, you can
//                  set the latitude and longitude at the top of that script,
//                  run it from a browser, and use the value for nws-county
//                  provided by it. If you don't have that script, you can
//                  find your NWS county by going to:
//
//                  https://api.weather.gov/points/latitude,longitude
//
//                  The output will contain a 'county' - the value at the end
//                  will be your county. For example:
//
//                    https://api.weather.gov/points/37.26,-122
//
//                  has 'CAC085' at the end of 'county' and that's the county
//                  you would use for the latitude/longitude of 37.26 and -122.
//                  The reason this system requires you to specify it, versus
//                  looking it up itself, is to simply avoid a per-use call to
//                  the government server, when the information tends not to
//                  change unless your station changes location.
//
//                  The county is ONLY used for obtaining addition alert
//                  information. Sometimes the zones are so small in size that
//                  they miss alerts given at the county-wide level, that you
//                  may be interested in. So if you want those, specify this.
//                  You must always give a value to this parameter, but if you
//                  make it an empty string, then county-wide alerts that don't
//                  apply to your local zone will not be listed.
//
//     'nws-forecast-office'
//
//                  is the NWS forecast office three letter name for your
//                  desired location. If you have a copy of the
//                  'getnwsinfo.php' script, you can set the latitude and
//                  longitude at the top of that script, run it from a
//                  browser, and use the value for nws-forecast-office
//                  provided by it. If you don't have that script, you can
//                  find your NWS forecast office by going to:
//
//                    https://api.weather.gov/points/latitude,longitude
//
//                  The output will contain a 'forecast' field, and that field
//                  will be a URL. For example:
//
//                    https://api.weather.gov/points/37.26,-122
//
//                  Will return in th forecast field a URL of:
//
//                    https://api.weather.gov/gridpoints/MTR/97,90/forecast
//
//                  Note the three letters after 'gridpoints' - MTR. That would
//                  be the NWS forecast office for 37.26,-122
//
//                  The reason this system requires you to specify it, versus
//                  looking it up itself, is to simply avoid a per-use call to
//                  the government server, when the information tends not to
//                  change unless your station changes location.
//
//     'development-mode' if true, indicates that a developer is currently
//                  working on the weather system, so its results should
//                  definitely not be trusted at this time. This causes a
//                  warning message to be displayed using the default 'forecast
//                  to HTML' converter that we provide.
//
//     'logs' if true, creates log files during the run to be used by a
//            developer. Do not set this to true on a normal production system,
//            as per-use processing costs will be increased.
//
//     'ignore-cache' if true, will cause a new retrieval of forecast and alert
//                   data from the NWS servers and otherwise cause the cache to
//                   be ignored. Setting this to true when testing is fine.
//                   Keeping it set to true is a bad thing, as you would be
//                   adding load to the NWS severs, and this is not good.
//
//     'avoid-percentage-icons' is true or false. If true, this means that the
//                              package tries to avoid the use of icons that
//                              show precipitation percentages. If false, it
//                              doesn't try to do that.
//
//     'show-duplicate-alerts' Oftentimes, the National Weather Service will
//                             generate multiple active alerts with the same
//                             name and seveity, especially if they are
//                             extending an alert's duration. They will also
//                             often generate the same alert for both county
//                             and zone. To avoid creating a flood of repeated
//                             identically-named alerts as output from this
//                             function call, the software will instead only
//                             return one of each set of duplicates, choosing
//                             the one which has the end-date-time furthest in
//                             the future. If you like this behavior, set this
//                             parameter to 'false'. If you would instead
//                             prefer to receive every active alert, including
//                             duplicates, set this parameter to 'true'.
//
//     'forecast-source'       Chooses the NWS source for forecasts. There is
//                             a faster XML source, and a slower SOAP source.
//                             By default, the XML source will be used (or by
//                             specifying 'xml' for this parameter,, but
//                             you can force the SOAP source by specifying
//                             'soap'. Either way, if the primary chosen source
//                             is not available, the secondary source will be
//                             used.
//
//     'alert-county-test-file' If either or both are set to non-empty values,
//     'alert-zone-test-file'   they specify that the alerts and/or the zones
//                              are to be read in from these files, instead of
//                              being retrieved from the NWS. These are copies
//                              of 'cache' files for the alerts from a previous
//                              run of this software package. These options
//                              should be used when you wish to develop the
//                              reading and generating of HTML for the alerts
//                              during times when your current weather is nice
//                              and calm with no alerts present. THESE OPTIONS
//                              SHOULD ONLY BE SET WHILE YOU ARE DEVELOPING OR
//                              DEBUGGING YOUR SOFTWARE, AND SHOULD NEVER BE
//                              SET WHEN THE SOFTWARE PACKAGE IS AVAILABLE FOR
//                              THE PRODUCTION USE OF YOUR WEBSITE, AS YOU WILL
//                              OBVIOUS GET INCORRECT ALERT INFORMATION FROM
//                              THESE FILES, AND YOUR WEBSITE VISITORS SHOULD
//                              BE TOLD THIS INFORMATION IS BAD WHEN YOU SET
//                              EITHER OF THESE OPTIONS! When one or both of
//                              these options are set to non-empty values, and
//                              the file specified does exist and is readable,
//                              all alerts in this file will be accepted and
//                              treated as active, as 'start-valid-timestamp'
//                              and 'end-valid-timestamp' will be ignored. This
//                              is to allowed the continued use of these files.
//                              The location paths for these files will be
//                              'rooted' in the 'current working directory' of
//                              your website when running this software.
//
// 3) Verify the result is valid:
//
//   if ($forecast_result['result'] != 'OK') {
//     print '<span style="text-align:initial;"><br><xmp>WHOOPS: ';
//     print print_r($parameter_setup_result, true);
//     print '</xmp></span><br>';
//     return;
//   }
//
//
// 4) Use the information to your heart's content! If the
//    $forecast_result['result'] is 'OK' you will also be presented with the
//    following information:
//
//    'forecast-parameters' - Parameters of the forecast
//
//     An array with the following elements:
//
//       'latitude'               -> Provided latitude
//       'longitude'              -> Provided longitude
//       'placename'              -> Provided placename
//       'nws-zone'               -> Provided NWS zone
//       'nws-county'             -> Provided NWS county
//       'nws-forecast-office'    -> Provided NWS forecast office
//       'timezone'               -> Provided timezone
//       'avoid-percentage-icons' -> Provided value for this parameter (or, the
//                                   default value if none specified)
//       'show-duplicate-alerts'  -> Provided value for this parameter (or, the
//                                   default value if none specified)
//       'forecast-source'        -> Provided value for this parameter (or, the
//                                   default value if none specified)
//       'logs'                   -> Provided value for this parameter (or, the
//                                   default value if none specified)
//       'ignore-cache'           -> Provided value for this parameter (or, the
//                                   default value if none specified)
//       'development-mode'       -> Provided value for this parameter (or, the
//                                   default value if none specified)
//       'alert-county-test-file' -> Provided value for this parameter. If this
//                                   this is set to a non-empty value you MUST
//                                   NOT TRUST the alert readout! The default
//                                   value is ''.
//       'alert-zone-test-file'   -> Provided value for this parameter. If this
//                                   this is set to a non-empty value you MUST
//                                   NOT TRUST the alert readout! The default
//                                   value is ''.
//
//    'timing-information' -   Information about how long portions of forecast
//                             forecast generation took to run. This is a two
//                             dimensional array, with each row containing:
//
//      'thing-timed' - Name of the thing timed
//      'time-taken'  - A float, in seconds, indicating how long that took
//
//    'forecast-information' - Information about the generated forecast
//
//      'generated-on'              -> NWS forecast generation time
//      'disclaimer-text'           -> Programmer's disclaimer text
//      'nws-disclaimer-url'        -> NWS disclaimer URL
//      'nws-more-info-url'         -> NWS more info URL
//      'forecast-feed-source-info' -> Information about the source of the
//                                     forecasts
//      'alert-feed-source-info'    -> Information about the source of the
//                                     alerts
//
//    'alerts'
//
//     This is an array of alerts, indexed from zero. Each 'row' contains the
//     following elements. If there are no alerts, then there are no elements.
//
//       'id'                 -> NWS event ID.
//       'event'              -> Event type
//       'effective'          -> When does the alert start being valid?
//       'expires'            -> When does the alert stop being valid?
//       'sent'               -> When was this alert sent (updated)?
//       'onset'              -> When will the event in the alert begin?
//                               (Often, but not always, the same as
//                               'effective')
//       'ends'               -> When will the event in the alert end?
//                               (Often, but not always, the same as
//                               'expires')
//       'title'              -> Combination of alert severity, event, and
//                               action modifiers
//       'phenominon-color'   -> The suggested border color for this type of
//                               alert phenomenon. Based off the NWS hazards
//                               map color choices indicating areas of hazard
//       'alert-border-color' -> suggested border color for the alert
//       'textcolor'          -> The suggested text color for this type of
//                               alert event (based on the phenominon-color.)
//       'priority'           -> The suggested priority level for this type of
//                               alert event. Lower priority numbers should be
//                               listed first
//       'NWSheadline'        -> THE ALL UPPER CASE NWS HEADLINE FOR THIS ALERT
//                               NOTE that 'NWS' in 'NWSheadline' is in
//                               uppercase here! If no headline exists, this
//                               field is not set
//       'response-detail'    -> Gives more information on the response code
//       'status'             -> ...various items as defined by NWS...
//       'description'        ->
//       'instruction'        ->
//       'response'           ->
//       'message-type'       ->
//       'category'           ->
//       'urgency'            ->
//       'severity'           ->
//       'certainty'          ->
//       'areaDesc'           -> (Notice how this is capitalized!)
//       'scope'              ->
//       'vtec'               -> (ONLY if VTEC was given in alert; do not
//                                assume it is always here)
//
//    'alerts-map' - Information on reading the alert map
//
//     This consists of one singular entity, then an array of specific items.
//     Use this to build an alerts map.
//
//       'url'                 -> URL to a map of items in the alert. If
//                                empty, it is not given, and the rest of this
//                                data block should be ignored. If it is not
//                                given, then 'map-keys' will not even be set
//                                to a value.
//       'keys'                -> Array of relevant to us map keys. Indexed
//                                with an integer, and then elements of each
//                                row are given.
//          'event'            -> Event name for this alert.
//          'color'            -> Map color for this alert.
//
//    'forecast-details' - Forecast details
//
//     This is an array of forecasts, indexed from zero. Each 'row'
//     contains the following elements. If there are no forecasts,
//     then there are no elements.
//
//       'period-name'     -> name of period 'Today' 'Tonight' 'Wednesday'
//       'weather-summary' -> text of weather summary (1-3 short words)
//       'conditions-text' -> Program-generated weather condition text. This
//                            will be 'null' if there is no conditions-text.
//
//       'temperature-text'  -> 'max' or 'min'
//       'temperature-units' -> units for temperature
//       'low-temperature'   -> low temperature, if appropriate
//       'high-temperature'  -> high temperature, if appropriate
//
//       'probability-of-precipitation-percent' -> probability of precipitation
//                                                 as a percent
//
//       'rain-amount'        -> rain amount
//       'rain-units'         -> rain units
//       'snow-amount'        -> snow amount
//       'snow-units'         -> snow units
//       'precipitation-text' -> human-friendly precipitation information
//
//       'ice-accumulation-amount'       -> ice accumulation amount
//       'ice-accumulation-units'        -> ice accumulation units
//       'ice-accumulation-text-summary' -> human-friendly ice accumulation
//                                          information
//       'ice-accumulation-text-detail'  -> 'techy' ice accumulation
//                                          information
//
//       'cloud-low'          -> low percent of cloud coverage
//       'cloud-high'         -> high percent of cloud coverage
//       'cloud-start'        -> percent of cloud coverage at earliest time in
//                               the reporting period
//       'cloud-end'          -> percent of cloud coverage at latest time in
//                               the reporting period
//       'cloud-text-summary' -> text of cloud cover in 'human' form
//       'cloud-text-detail'  -> text of cloud cover in 'techy' form
//
//       'conditions-icon' -> icon to use from NWS set (just the filename part,
//                            not any other part of the path)
//
//       'humidity-low'          -> relative humidity low
//       'humidity-high'         -> relative humidity high
//       'humidity-start'        -> percent of humidity at earliest time in the
//                                  reporting period
//       'humidity-end'          -> percent of humidity at latest time in the
//                                  reporting period
//       'humidity-text-detail'  -> humidity text in techy detail
//       'humidity-text-summary' -> humidity text in 'human' summary
//
//       'wind-sustained-low'          -> lower of range of sustained wind
//       'wind-sustained-high'         -> higher of range of sustained wind
//       'wind-sustained-start'        -> sustained wind at earliest known time
//                                        within the reporting period
//       'wind-sustained-end'          -> sustained wind at latest known time
//                                        within the reporting period
//       'wind-speed-units'            -> wind speed units
//       'wind-gust-low'               -> lower of range of wind gust
//       'wind-gust-high'              -> higher of range of wind gust
//       'wind-gust-start'             -> gust wind at earliest known time
//                                        within the reporting period
//       'wind-gust-end'               -> gust wind at latest known time within
//                                        the reporting period
//       'wind-direction-units'        -> wind direction units
//       'wind-direction-raw-first'    -> degrees true north wind
//       'wind-direction-first'        -> wind direction first, compass
//       'wind-direction-raw-second'   -> degrees true north wind
//       'wind-direction-second'       -> wind direction second, compass
//       'wind-direction-start'        -> wind direction at earliest known time
//                                        within the reporting period
//       'wind-direction-end'          -> wind direction at latest known time
//                                        within the reporting period
//       'wind-text'                   -> human friendly text of wind forecast
//       'wind-sustained-beaufort-scale-name' ->
//                                        sustained wind 'beaufort-scale' name
//       'wind-gust-beaufort-scale-name' -> wind gust 'beaufort-scale' name
//
//       'generated-forecast' -> human friendly forecast built from the above
//                               NWS raw data - NOT the officially written NWS
//                               forecast!
//
//       'generated-short-forecast' -> human friendly short forecast built from
//                                     the above data. This skips wind speed if
//                                     'higher 'is less than 15 MPH, and will
//                                     skip cloud conditions, using
//                                     weather-summary instead.
//
//       'generated-forecast-computer-speak' -> This is 'generated-forecast'
//                                              re-written in a way to make it
//                                              easier for a computer to
//                                              read it aloud with
//                                              text-to-speech synthesis.
//
//       'generated-short-forecast-computer-speak' -> This is the above short
//                                              forecast re-written in a way
//                                              to make it easier for a
//                                              computer to read it aloud with
//                                              text-to-speech synthesis.
//

//
// Required scripts
//

require_once('xmlize/xmlize-php5.inc.php');

require_once('nusoap/nusoap.php');

//
// Constants
//

// Gives names to client parameters
define('WEATHER_PARSER_CP_LATITUDE',               'latitude');
define('WEATHER_PARSER_CP_LONGITUDE',              'longitude');
define('WEATHER_PARSER_CP_PLACENAME',              'placename');
define('WEATHER_PARSER_CP_NWS_ZONE',               'nws-zone');
define('WEATHER_PARSER_CP_NWS_COUNTY',             'nws-county');
define('WEATHER_PARSER_CP_NWS_FORECAST_OFFICE',    'nws-forecast-office');
define('WEATHER_PARSER_CP_TIMEZONE',               'timezone');
define('WEATHER_PARSER_CP_IGNORE_CACHE',           'ignore-cache');
define('WEATHER_PARSER_CP_LOGS',                   'logs');
define('WEATHER_PARSER_CP_DEVELOPMENT_MODE',       'development-mode');
define('WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS', 'avoid-percentage-icons');
define('WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS',  'show-duplicate-alerts');
define('WEATHER_PARSER_CP_FORECAST_SOURCE',        'forecast-source');
define('WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE', 'alert-county-test-file');
define('WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE',   'alert-zone-test-file');

// These are based off of the server root path, not the location of this
// script!
define('WEATHER_PARSER_CACHE_DIRECTORY',
       'private_extensions/weather_parser/cache/');
define('WEATHER_PARSER_LOGS_DIRECTORY',
       'private_extensions/weather_parser/logs/');
define('WEATHER_PARSER_TIMINGS_DIRECTORY',
       'private_extensions/weather_parser/timings/');
define('WEATHER_PARSER_ICON_DIRECTORY',
       'private_extensions/weather_parser/icons/');

define('WEATHER_PARSER_ALERT_IMAGES_SUFFIX',           '.gif');

define('WEATHER_PARSER_EMPTY_ALERT_ENCODED_STRING', 's:0:"";');

define('WEATHER_PARSER_ALERT_CACHE_SUFFIX',      'wp-a-cache');
define('WEATHER_PARSER_FORECAST_CACHE_SUFFIX',   'wp-f-cache');
define('WEATHER_PARSER_MAX_FORECAST_AGE',                 120);
define('WEATHER_PARSER_MAX_ALERT_AGE',                     10);

// This should be understood precisely: The contents are pruned at the START of
// forcast generation, not at the end! Therefore, the information directories
// will contain at most, say 20 (if that is what this is set to) files at the
// start of the forecast generation, and then add in the new files created by
// the current forecast generation. So... if the forecast generation causes
// four files to be dumped, then after the forecast generation is over, there
// will be 24 files in the information dump directories.
//
// Both the forecast information dumps and the timing information dumps use
// this value to limit the number of retained files in their dump directories.
define('WEATHER_PARSER_MAX_FILES_IN_FORECAST_DUMP',        50);

define('SECONDS_PER_MINUTE',                               60);
define('DEGREES_IN_CIRCLE',                             360.0);
define('DEGREES_PER_COMPASS_ROSE_SEGMENT',               22.5);
define('HALF_OF_DEGREES_PER_COMPASS_ROSE_SEGMENT',      11.25);
define('ONE_EIGHTH_OF_ONE_HUNDRED',                      12.5);
define('MPH_PER_KNOT',                             1.15077945);

define('FORECAST_SOURCE_XML',                               0);
define('FORECAST_SOURCE_SOAP',                              1);


// Created so we get the full desktop version of a site, vs. a mobile version
define('WEATHER_PARSER_USER_AGENT_STRING',
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 " .
        "Firefox/67.0");

// We will try to get alerts this many times
define('WEATHER_PARSER_GET_ALERTS_MAX_ATTEMPTS', 2);

// We will sleep this many seconds between alert retrieval attempts
define('WEATHER_PARSER_GET_ALERTS_RETRY_SLEEP_SECONDS', 5);

if (!defined('PHP_VERSION_ID')) {
    $version = explode('.', PHP_VERSION);
    define('PHP_VERSION_ID', ($version[0] * 10000 +
                              $version[1] * 100 +
                              $version[2]));
}

// Version check!
if (PHP_VERSION_ID < 80200) {
  print '<xmp><br><h3>Your version of PHP is earlier than 8.2. Results may ' .
    'be strange.<br></xmp>';
}

class WeatherParser {

  // These are settable by the user
  private $_client_parameters;
  private $_forecast_source;

  // Date/Time this class was called
  private $_forecast_timestamp;

  // Per-call information storage
  private $_storage_zone_alerts;
  private $_storage_county_alerts;
  private $_storage_summary;
  private $_storage_detail;

  // Timing information
  private $_timing_information;

  // Obtained data files (stored in the forecast_information directory)
  private $_forecast_information_files;

  // Runtime (for debugging script speed)
  private $_script_run_time_start;

  // These are not settable by the user and are initialized by the
  // __construct method at class instance creation.
  private readonly array $_private_readonly_cache_max_ages;
  private readonly array $_private_readonly_compass_rose;
  private readonly array $_private_readonly_compass_rose_computer_speak;
  private readonly array $_private_readonly_compass_rose_abbreviations;
  private readonly array $_private_readonly_alert_action_decoder;
  private readonly array $_private_readonly_wind_speed_to_beaufort_scale;
  private readonly array $_private_readonly_cloud_summary_daytime;
  private readonly array $_private_readonly_cloud_summary_daytime_transitions;
  private readonly array $_private_readonly_cloud_summary_nighttime;
  private readonly array
    $_private_readonly_cloud_summary_nighttime_transitions;
  private readonly array $_private_readonly_coverage_translate;

  private readonly array $_private_readonly_alert_title_to_priority_and_colors;
  private readonly array $_private_readonly_alert_response_to_response_detail;
  private readonly array $_private_readonly_forecast_source_title;
  private readonly array $_private_readonly_forecast_summary_url;
  private readonly array $_private_readonly_forecast_detail_url;
  private readonly array $_private_readonly_forecast_backup_source;
  private readonly array $_private_readonly_alerts_backup_encoding;
  private readonly array $_private_readonly_alerts_encoding_name;
  private readonly array $_private_readonly_valid_forecast_sources;
  private readonly array $_private_readonly_valid_forecast_source_names;
  private readonly array $_private_readonly_tzlist;

  private readonly string $_private_readonly_alert_source_title;
  private readonly string $_private_readonly_alert_source_name;
  private readonly string $_private_readonly_alerts_headers;
  private readonly string $_private_readonly_alerts_url;
  private readonly string $_private_readonly_default_avoid_percentage_icons;
  private readonly string $_private_readonly_default_show_duplicate_alerts;
  private readonly string $_private_readonly_default_ignore_cache;
  private readonly string $_private_readonly_default_logs;
  private readonly string $_private_readonly_default_development_mode;
  private readonly string $_private_readonly_default_forecast_source;
  private readonly string $_private_readonly_default_alert_county_test_file;
  private readonly string $_private_readonly_default_alert_zone_test_file;
  private readonly string $_private_readonly_alerts_map_url;

  // _obtain_forecast_initialize
  //
  // Initialize obtain-forecast values
  //
  private function _obtain_forecast_initialize () {

    $this->_init_cpu_time();

    // Now we set up some individual variables to default values.
    $this->_storage_zone_alerts = null;
    $this->_storage_county_alerts = null;
    $this->_storage_summary = null;
    $this->_storage_detail = null;
    $this->_forecast_source = FORECAST_SOURCE_XML;

    // We default each of these to an array so timing information and forecast
    // information file names can be added to these arrays without having to
    // repeatedly check if these arrays have been initialized.
    $this->_forecast_information_files = array();
    $this->_timing_information = array();
  }

  // __construct
  //
  // Called when an instance of the class is created.
  //
  function __construct () {

    $this->_private_readonly_cache_max_ages =
      array(WEATHER_PARSER_ALERT_CACHE_SUFFIX =>
            WEATHER_PARSER_MAX_ALERT_AGE,
            WEATHER_PARSER_FORECAST_CACHE_SUFFIX =>
            WEATHER_PARSER_MAX_FORECAST_AGE);

 
    // The keys are in lowercase!
    $this->_private_readonly_alert_response_to_response_detail =
      array("shelter" => "take shelter in place or per the instruction",
            "evacuate" => "relocate as instructed in the instruction",
            "prepare" => "make preparations per the instruction",
            "execute" => "execute a pre-planned activity identified in " .
            "instruction",
            "avoid" => "avoid the subject event as per the instruction",
            "monitor" => "attend to information sources as described in " .
            "instruction",
            "assess" => "evaluate the information in this message",
            "allclear" => "the subject event no longer poses a threat or " .
            "concern and any follow-on action is described in instruction",
            "none" => "no action recommended beyond that given in " .
            "instruction");

    $this->_private_readonly_alert_title_to_priority_and_colors =
      // Alert name, priority, phenominon color, alert border color
      array('tsunami warning',                         1, 0xfd6347, 0xdd0000,
            'tornado warning',                         2, 0xff0000, 0xaa0000,
            'extreme wind warning',                    3, 0xff8c00, 0xdd0000,
            'severe thunderstorm warning',             4, 0xffa500, 0xbb1111,
            'flash flood warning',                     5, 0x8b0000, 0xdd0000,
            'flash flood statement',                   6, 0x8b0000, 0xff6600,
            // next line added in by hand
            'flash flood advisory',                    6, 0x8b0000, 0xff6600,
            'severe weather statement',                7, 0x00ffff, 0xff9933,
            'shelter in place warning',                8, 0xfa8072, 0xdd0000,
            'evacuation immediate',                    9, 0x7fff00, 0xeeaa00,
            'civil danger warning',                   10, 0xffb6c1, 0xdd0000,
            'nuclear power plant warning',            11, 0x4b0082, 0xdd0000,
            'radiological hazard warning',            12, 0x4b0082, 0xdd0000,
            'hazardous materials warning',            13, 0x4b0082, 0xdd0000,
            'fire warning',                           14, 0xa0522d, 0xdd0000,
            'civil emergency message',                15, 0xffb6c1, 0x009933,
            'law enforcement warning',                16, 0xc0c0c0, 0xdd0000,
            'storm surge warning',                    17, 0xb524f7, 0xdd0000,
            'hurricane force wind warning',           18, 0xcd5c5c, 0xdd0000,
            // hurricane wind warning added as variant that NWS sometimes uses
            'hurricane wind warning',                 18, 0xcd5c5c, 0xdd0000,
            'hurricane warning',                      19, 0xdc143c, 0xdd0000,
            'typhoon warning',                        20, 0xdc143c, 0xdd0000,
            'special marine warning',                 21, 0xffa500, 0xdd0000,
            'blizzard warning',                       22, 0xff4500, 0xdd0000,
            'snow squall warning',                    23, 0xc71585, 0xdd0000,
            // heavy snow warning is added in by hand; not part of NWS list
            'heavy snow warning',                     24, 0x8b008b, 0xdd0000,
            'ice storm warning',                      24, 0x8b008b, 0xdd0000,
            'winter storm warning',                   25, 0xff69b4, 0xdd0000,
            // next line added in by hand
            'winter weather warning',                 25, 0xff69b4, 0xdd0000,
            'high wind warning',                      26, 0xdaa520, 0xdd0000,
            'tropical storm warning',                 27, 0xb22222, 0xdd0000,
            'storm warning',                          28, 0x9400d3, 0xdd0000,
            'tsunami advisory',                       29, 0xd2691e, 0xff6600,
            'tsunami watch',                          30, 0xff00ff, 0xff9933,
            'avalanche warning',                      31, 0x1e90ff, 0xdd0000,
            'earthquake warning',                     32, 0x8b4513, 0xdd0000,
            'volcano warning',                        33, 0x2f4f4f, 0xdd0000,
            'ashfall warning',                        34, 0xa9a9a9, 0xdd0000,
            'coastal flood warning',                  35, 0x228b22, 0xdd0000,
            'lakeshore flood warning',                36, 0x228b22, 0xdd0000,
            'flood warning',                          37, 0x00ff00, 0xdd0000,
            // Next line added in as sometimes seen variant
            'river flood warning',                    37, 0x00ff00, 0xdd0000,
            'high surf warning',                      38, 0x228b22, 0xdd0000,
            'dust storm warning',                     39, 0xffe4c4, 0xdd0000,
            'blowing dust warning',                   40, 0xffe4c4, 0xdd0000,
            'lake effect snow warning',               41, 0x008b8b, 0xdd0000,
            'excessive heat warning',                 42, 0xc71585, 0xdd0000,
            'tornado watch',                          43, 0xffff00, 0xff9933,
            'severe thunderstorm watch',              44, 0xdb7093, 0xff3311,
            'flash flood watch',                      45, 0x2e8b57, 0xff9933,
            'gale warning',                           46, 0xdda0dd, 0xdd0000,
            'flood statement',                        47, 0x00ff00, 0xcc7700,
            // Next line added in by hand
            'river flood statement',                  47, 0x00ff00, 0xcc7700,
            'flash flood statement',                  47, 0x8b0000, 0xcc7700,
            'wind chill warning',                     48, 0xb0c4de, 0xdd0000,
            'extreme cold warning',                   49, 0x0000ff, 0xdd0000,
            'hard freeze warning',                    50, 0x9400d3, 0xdd0000,
            'freeze warning',                         51, 0x483d8b, 0xdd0000,
            'red flag warning',                       52, 0xff1493, 0xdd0000,
            'storm surge watch',                      53, 0xdb7ff7, 0xff9933,
            'hurricane watch',                        54, 0xff00ff, 0xff9933,
            'hurricane force wind watch',             55, 0x9932cc, 0xff9933,
            // next line added in by hand
            'hurricane wind watch',                   55, 0x9932cc, 0xff9933,
            'typhoon watch',                          56, 0xff00ff, 0xff9933,
            'tropical storm watch',                   57, 0xf08080, 0xff9933,
            'storm watch',                            58, 0xffe4b5, 0xff9933,
            'hurricane local statement',              59, 0xffe4b5, 0xcc7700,
            // Next line added in by hand
            'hurricane statement',                    59, 0xffe4b5, 0xcc7700,
            'typhoon local statement',                60, 0xffe4b5, 0xcc7700,
            // next line added in by hand
            'typhoon  statement',                     60, 0xffe4b5, 0xcc7700,
            // next line added in by hand
            'coastal hazard',                         60, 0x40e0d0, 0xcc7700,
            'tropical storm local statement',         61, 0xffe4b5, 0xffe4b5,
            'tropical depression local statement',    62, 0xffe4b5, 0xffe4b5,
            'avalanche advisory',                     63, 0xcd853f, 0xff6600,
            'winter weather advisory',                64, 0x7b68ee, 0xff6600,
            'wind chill advisory',                    65, 0xafeeee, 0xff6600,
            'heat advisory',                          66, 0xff7f50, 0xff6600,
            'urban and small stream flood advisory',  67, 0x00ff7f, 0xff6600,
            'small stream flood advisory',            68, 0x00ff7f, 0xff6600,
            'arroyo and small stream flood advisory', 69, 0x00ff7f, 0xff6600,
            'flood advisory',                         70, 0x00ff7f, 0xff6600,
            'hydrologic advisory',                    71, 0x00ff7f, 0xff6600,
            'lakeshore flood advisory',               72, 0x7cfc00, 0xff6600,
            'coastal flood advisory',                 73, 0x7cfc00, 0xff6600,
            'high surf advisory',                     74, 0xba55d3, 0xff6600,
            // Line below added in as NWS variant not in list
            'sleet warning',                          75, 0x00bfff, 0xdd0000,
            'heavy freezing spray warning',           75, 0x00bfff, 0xff6600,
            'dense fog advisory',                     76, 0x708090, 0xff6600,
            'dense smoke advisory',                   77, 0xf0e68c, 0xff6600,
            'small craft advisory',                   78, 0xd8bfd8, 0xff6600,
            'brisk wind advisory',                    79, 0xd8bfd8, 0xff6600,
            'hazardous seas warning',                 80, 0xd8bfd8, 0xdd0000,
            'dust advisory',                          81, 0xbdb76b, 0xff6600,
            'blowing dust advisory',                  82, 0xbdb76b, 0xff6600,
            // next line added in by hand
            'blowing snow advisory',                  82, 0x7b68ee, 0xff6600,
            // next line added in by hand
            'snow advisory',                          82, 0x7b68ee, 0xff6600,
            // next line added in by hand
            'snow and blowing snow advisory',         82, 0x7b68ee, 0xff6600,
            // Next line added in by hand
        'lake effect snow and blowing snow advisory', 82, 0x7b68ee, 0xff6600,
            // next line added in by hand
            'sleet advisory',                         82, 0x7b68ee, 0xff6600,
            'lake wind advisory',                     83, 0xd2b48c, 0xff6600,
            'wind advisory',                          84, 0xd2b48c, 0xff6600,
            'frost advisory',                         85, 0x6495ed, 0xff6600,
            'ashfall advisory',                       86, 0x696969, 0xff6600,
            'freezing fog advisory',                  87, 0x008080, 0xff6600,
            // next line added in by hand
            'freezing drizzle advisory',              87, 0x00bfff, 0xff6600,
            // next line added in by hand
            'freezing rain advisory',                 87, 0x008080, 0xff6600,
            'freezing spray advisory',                88, 0x00bfff, 0xff6600,
            'low water advisory',                     89, 0xa52a2a, 0xff6600,
            'local area emergency',                   90, 0xc0c0c0, 0x009933,
            'avalanche watch',                        91, 0xf4a460, 0xff9933,
            'blizzard watch',                         92, 0xadff2f, 0xff9933,
            'rip current statement',                  93, 0x40e0d0, 0xcc7700,
            'beach hazards statement',                94, 0x40e0d0, 0xcc7700,
            'gale watch',                             95, 0xffc0cb, 0xff9933,
            'winter storm watch',                     96, 0x4682b4, 0xff9933,
            // Next line added in by hand
            'winter weather watch',                   96, 0x4682b4, 0xff9933,
            'hazardous seas watch',                   97, 0x483d8b, 0xff9933,
            'heavy freezing spray watch',             98, 0xbc8f8f, 0xff9933,
            'coastal flood watch',                    99, 0x66cdaa, 0xff9933,
            'lakeshore flood watch',                 100, 0x66cdaa, 0xff9933,
            'flood watch',                           101, 0x2e8b57, 0xff9933,
            'high wind watch',                       102, 0xb8860b, 0xff9933,
            'excessive heat watch',                  103, 0x800000, 0xff9933,
            'extreme cold watch',                    104, 0x0000ff, 0xff9933,
            'wind chill watch',                      105, 0x5f9ea0, 0xff9933,
            'lake effect snow watch',                106, 0x87cefa, 0xff9933,
            // next line added in by hand
            'lake effect snow advisory',             106, 0x87cefa, 0xff6600,
            'hard freeze watch',                     107, 0x4169e1, 0xff9933,
            'freeze watch',                          108, 0x00ffff, 0xff9933,
            'fire weather watch',                    109, 0xffdead, 0xff9933,
            'extreme fire danger',                   110, 0xe9967a, 0xdd0000,
            '911 telephone outage',                  111, 0xc0c0c0, 0x3366cc,
            // Next line added in by hand. 
            'public information statement',          111, 0xc0c0c0, 0xcc7700,
            'coastal flood statement',               112, 0x6b8e23, 0xcc7700,
            'lakeshore flood statement',             113, 0x6b8e23, 0xcc7700,
            'special weather statement',             114, 0xffe4b5, 0xcc7700,
            // next line added in by hand
            'significant weather alert',             114, 0xffe4b5, 0xff9933,
            'marine weather statement',              115, 0xffdab9, 0xcc7700,
            'air quality alert',                     116, 0x808080, 0x0066cc,
            'air stagnation advisory',               117, 0x808080, 0x006600,
            'hazardous weather outlook',             118, 0xeee8aa, 0xff6600,
            'hydrologic outlook',                    119, 0x90ee90, 0xff6600,
            'short term forecast',                   120, 0x98fb98, 0x009933,
            'administrative message',                121, 0xc0c0c0, 0xcc7700,
            'test',                                  122, 0xf0ffff, 0xcc7700,
            'child abduction emergency',             123, 0xffffff, 0x009933,
            'blue alert',                            124, 0xffffff, 0xdd0000);

    $this->_private_readonly_compass_rose =
      array('north', 'northnortheast', 'northeast', 'eastnortheast',
            'east', 'eastsoutheast', 'southeast', 'southsoutheast',
            'south', 'southsouthwest', 'southwest', 'westsouthwest',
            'west', 'westnorthwest', 'northwest', 'northnorthwest');

    $this->_private_readonly_compass_rose_computer_speak =
      array('north', 'north-north-east', 'north-east', 'east-north-east',
            'east', 'east-south-east', 'south-east', 'south-south-east',
            'south', 'south-south-west', 'south-west', 'west-south-west',
            'west', 'west-north-west', 'north-west', 'north-north-west');

    $this->_private_readonly_compass_rose_abbreviations =
      array('N', 'NNE', 'NE', 'ENE',
            'E', 'ESE', 'SE', 'SSE',
            'S', 'SSW', 'SW', 'WSW',
            'W', 'WNW', 'NW', 'NNW');

    $this->_private_readonly_alert_action_decoder =
      array('NEW' => 'PNew',
            'CON' => 'PContinued',
            'EXT' => 'Sextended duration',
            'EXA' => 'Sextended area',
            'EXB' => 'Sextended duration and area',
            'UPG' => 'Supgraded!',
            'CAN' => 'PCancelled',
            'EXP' => 'PExpired',
            'COR' => 'Scorrection',
            'ROU' => 'PRoutine');

    $this->_private_readonly_wind_speed_to_beaufort_scale =
      array(0 => 'calm',  // 0
            3 => 'a light air', // 1
            7 => 'a light breeze', // 2
            12 => 'a gentle breeze', // 3
            18 => 'a moderate breeze', // 4
            24 => 'a fresh breeze', // 5
            31 => 'a strong breeze (small-craft advisory!)', // 6
            38 => 'a near gale (small-craft advisory!)', // 7
            46 => 'a gale-force wind (gale warning!)', // 8
            54 => 'a strong gale (gale warning!)', // 9
            63 => 'a storm-level gale (storm warning!)', // 10
            72 => 'a violent storm level wind (storm warning!)', // 11
            95 =>
            'at category 1 hurricane level (hurricane force wind warning!)',
            110 =>
            'at category 2 hurricane level (hurricane force wind warning!)',
            129 =>
            'at category 3 hurricane level (hurricane force wind warning!)',
            156 =>
            'at category 4 hurricane level (hurricane force wind warning!)',
            9999 => 'at category 5 hurricane level');

    // From 0/8 to 8/8 requires nine slots
    $this->_private_readonly_cloud_summary_daytime =
      array('sunny', 'mostly sunny', 'mostly sunny',
            'partly cloudy', 'partly cloudy', 'partly cloudy',
            'mostly cloudy', 'mostly cloudy', 'cloudy');

    // This handles transition names
    $this->_private_readonly_cloud_summary_daytime_transitions =
      array('sunny/mostly sunny' => 'with a few clouds later making it',
            'sunny/partly cloudy' => 'with incoming clouds making it',
            'sunny/mostly cloudy' =>
            'with overcast conditions arriving, becoming',
            'sunny/cloudy' => 'with overcast conditions arriving, becoming',
            'mostly sunny/sunny' => 'with any clouds clearing to become',
            'mostly sunny/partly cloudy' => 'with incoming clouds making it',
            'mostly sunny/mostly cloudy' =>
            'with overcast conditions arriving, becoming',
            'mostly sunny/cloudy' =>
            'with overcast conditions arriving, becoming',
            'partly cloudy/sunny' => 'with clouds clearing, to become',
            'partly cloudy/mostly sunny' =>
            'with some clouds clearing, making it',
            'partly cloudy/mostly cloudy' =>
            'with some increasing clouds, becoming',
            'partly cloudy/cloudy' =>
            'with full overcast conditions arriving, becoming',
            'mostly cloudy/sunny' => 'with clouds clearing, to become',
            'mostly cloudy/mostly sunny' =>
            'with most clouds clearing, making it',
            'mostly cloudy/partly cloudy' =>
            'with a bit of the overcast clearing, becoming',
            'mostly cloudy/cloudy' =>
            'with full overcast conditions arriving, becoming',
            'cloudy/sunny' => 'with clouds completely clearing, to become',
            'cloudy/mostly sunny' => 'with most clouds clearing, making it',
            'cloudy/partly cloudy' => 'with some overcast clearing, becoming',
            'cloudy/mostly cloudy' =>
            'with a bit of the cloudiness clearing, becoming');

    // From 0/8 to 8/8 requires nine slots
    $this->_private_readonly_cloud_summary_nighttime =
      array('clear', 'mostly clear', 'mostly clear',
            'partly cloudy', 'partly cloudy', 'partly cloudy',
            'mostly cloudy', 'mostly cloudy', 'cloudy');

    // This handles transition names
    $this->_private_readonly_cloud_summary_nighttime_transitions =
      array('clear/mostly clear' => 'with a few clouds later making it',
            'clear/partly cloudy' => 'with incoming clouds making it',
            'clear/mostly cloudy' =>
            'with overcast conditions arriving, becoming',
            'clear/cloudy' => 'with overcast conditions arriving, becoming',
            'mostly clear/clear' => 'with any clouds clearing to become',
            'mostly clear/partly cloudy' => 'with incoming clouds making it',
            'mostly clear/mostly cloudy' =>
            'with overcast conditions arriving, becoming',
            'mostly clear/cloudy' =>
            'with overcast conditions arriving, becoming',
            'partly cloudy/clear' => 'with clouds clearing, to become',
            'partly cloudy/mostly clear' =>
            'with some clouds clearing, making it',
            'partly cloudy/mostly cloudy' =>
            'with some increasing clouds, becoming',
            'partly cloudy/cloudy' =>
            'with full overcast conditions arriving, becoming',
            'mostly cloudy/clear' => 'with clouds clearing, to become',
            'mostly cloudy/mostly clear' =>
            'with most clouds clearing, making it',
            'mostly cloudy/partly cloudy' =>
            'with a bit of the overcast clearing, becoming',
            'mostly cloudy/cloudy' =>
            'with full overcast conditions arriving, becoming',
            'cloudy/clear' => 'with clouds completely clearing, to become',
            'cloudy/mostly clear' => 'with most clouds clearing, making it',
            'cloudy/partly cloudy' => 'with some overcast clearing, becoming',
            'cloudy/mostly cloudy' =>
            'with a bit of the cloudiness clearing, becoming');

    //
    // %t is weather-type (think: 'rain' or 'snow')
    //
    // %i is intensity, followed by a space unless intensity is 'none' or is
    //   missing, in which case, the replacement is the empty string. (think:
    //   'heavy' or 'light')
    //
    $this->_private_readonly_coverage_translate =
      array('slight chance' => 'a slight chance of %i%t',
            'chance' => 'a chance of %i%t',
            'likely' => '%i%t likely',
            'occasional' => 'occasional %i%t',
            'definitely' => '%i%t certain',
            'isolated' => 'isolated %i%t',
            'scattered' => 'scattered %i%t',
            'numerous' => 'numerous %i%t',
            'patchy' => 'patchy %i%t',
            'areas' => 'areas of %i%t',
            'widespread' => 'widespread %i%t',
            'periods of' => 'periods of %i%t',
            'frequent' => 'frequent %i%t',
            'intermittant' => 'intermittant %i%t');

    // Load in timezones.
    $tzout = array();
    $tza = timezone_abbreviations_list();
    foreach ($tza as $zone) {
      foreach ($zone as $this_item) {
        $tzout[$this_item['timezone_id']] = 1;
      }
    }
    $tzout[''] = null;
    ksort($tzout);
    $this->_private_readonly_tzlist = array_keys($tzout);

    // xml is the default
    $this->_private_readonly_valid_forecast_sources =
      array('xml' => FORECAST_SOURCE_XML,
            'soap' => FORECAST_SOURCE_SOAP);

    $this->_private_readonly_valid_forecast_source_names =
      array(FORECAST_SOURCE_XML => 'xml',
            FORECAST_SOURCE_SOAP => 'soap');

    $this->_private_readonly_forecast_source_title =
      array(FORECAST_SOURCE_XML =>
            "National Weather Service's NDFD XML Feed",
            FORECAST_SOURCE_SOAP =>
            "National Weather Service's NDFD SOAP Feed");

    $this->_private_readonly_alert_source_title =
      "National Weather Service's CAP v1.2 JSON Feed";

    $this->_private_readonly_alert_source_name =
      "capv1p2json";

    $this->_private_readonly_default_show_duplicate_alerts     =
      $this->_private_readonly_default_avoid_percentage_icons    =
      $this->_private_readonly_default_ignore_cache              =
      $this->_private_readonly_default_logs                      =
      $this->_private_readonly_default_development_mode          = false;

    $this->_private_readonly_default_forecast_source           = 'xml';

    $this->_private_readonly_default_alert_county_test_file    =
      $this->_private_readonly_default_alert_zone_test_file      = '';

    $this->_private_readonly_alerts_map_url =
      "https://www.weather.gov/wwamap/png/FORECAST_OFFICE.png";

    $this->_private_readonly_forecast_summary_url =
      array
      (FORECAST_SOURCE_XML =>
       "https://digital.mdl.nws.noaa.gov/xml/sample_products/" .
       "browser_interface/ndfdBrowserClientByDay.php?format=12+hourly" .
       "&numDays=7" .
       "&XMLformat=NDFD",
       FORECAST_SOURCE_SOAP =>
       "https://digital.weather.gov/xml/SOAP_server/ndfdXMLserver.php?wsdl");

    $this->_private_readonly_forecast_detail_url =
      array
      (FORECAST_SOURCE_XML =>
       "https://digital.mdl.nws.noaa.gov/xml/sample_products" .
       "/browser_interface/ndfdXMLclient.php",
       FORECAST_SOURCE_SOAP =>
       "https://digital.weather.gov/xml/SOAP_server/ndfdXMLserver.php?wsdl");

    // With alerts, when used, the value of %ZONEORCOUNTY% should be replaced
    // with the value of a zone or county (such as CAZ513 (zone) or CAC085
    // (county)).

    $this->_private_readonly_alerts_url =
      "https://api.weather.gov/alerts/active?zone=%ZONEORCOUNTY%";

    $this->_private_readonly_forecast_backup_source =
      array(FORECAST_SOURCE_XML => FORECAST_SOURCE_SOAP,
            FORECAST_SOURCE_SOAP => FORECAST_SOURCE_XML);

    $this->_private_readonly_alerts_headers =
      "Accept: application/geo+json";

    $this->_client_parameters =
      array_fill_keys
      (array(WEATHER_PARSER_CP_LATITUDE,
             WEATHER_PARSER_CP_LONGITUDE,
             WEATHER_PARSER_CP_PLACENAME,
             WEATHER_PARSER_CP_NWS_ZONE,
             WEATHER_PARSER_CP_NWS_COUNTY,
             WEATHER_PARSER_CP_NWS_FORECAST_OFFICE,
             WEATHER_PARSER_CP_TIMEZONE,
             WEATHER_PARSER_CP_IGNORE_CACHE,
             WEATHER_PARSER_CP_LOGS,
             WEATHER_PARSER_CP_DEVELOPMENT_MODE,
             WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS,
             WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS,
             WEATHER_PARSER_CP_FORECAST_SOURCE,
             WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE,
             WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE),
       null);

    // Clean this up as well
    $this->_obtain_forecast_initialize();
  }

  //
  // _debug_print
  //
  // Handy way to print debugging code.
  //
  // call as: $this->_debug_print(__LINE__, "what-to-print");
  //
  private function _debug_print ($line, $printthis) {

    print '<br><span style="text-align:initial;"><xmp><br> AT LINE ' . $line .
      ': ' . $printthis . '</xmp></span><br>';
  }

  // _cleaned_up_xmlize
  //
  // This deals with the fact the NWS can sometimes send us XML files that
  // contain << instead of <, >> instead of > and "" instead of ".
  //
  private function _cleaned_up_xmlize ($stuff) {

    return xmlize(str_replace('""', '"',
                              str_replace('>>', '>',
                                          str_replace('<<', '<', $stuff))));
  }

  // _isValidTimezoneId
  //
  // Return true if the timezone given is a valid one.
  //
  private function _isValidTimezoneId ($timezoneId) {

    // list of all valid timezones (last 4 are not included)
    return in_array($timezoneId, $this->_private_readonly_tzlist);
  }

  // _file_age_in_minutes
  //
  // Returns the age, in integer minutes.
  //
  // Calculation is essentially an integer-based-ceiling function of sorts:
  //   1) age_in_minutes = age_in_seconds / 60
  //   2) if (age_in_minutes * 60) < age_in_seconds, add 1 to age_in_minutes
  //
  // Returns < 0 if the file does not exist
  //
  private function _file_age_in_minutes ($filename) {

    if (!file_exists($filename)) {
      return -1;
    }
    if (is_file($filename) !== true) {
      return -1;
    }

    // Why do we do +1? Because if a file is 3 minutes and 2 seconds old,
    // it's over 3 minutes old - so return '4'
    $age_in_seconds = (time() - filemtime($filename));
    $age_in_minutes = $age_in_seconds / SECONDS_PER_MINUTE;
    if (($age_in_minutes * SECONDS_PER_MINUTE) < $age_in_seconds) {
      $age_in_minutes++;
    }
    return $age_in_minutes;
  }

  // _unlink_file_if_we_can
  //
  // Get rid of a file with the given name, if it exists and is a file and
  // we can write to the directory (delete files). This way we avoid error
  // messages.
  //
  private function _unlink_file_if_we_can ($filename) {

    if (file_exists($filename)) {
      if (is_file($filename)) {
        if (is_writable(dirname($filename))) {
          unlink($filename);
        }
      }
    }
  }

  // _init_cpu_time
  //
  // Start off the time check.
  //
  private function _init_cpu_time () {

    $this->_script_run_time_start = microtime(true);
  }

  // _print_cpu_time
  //
  // Print how long the code has run (CPU time)
  //
  // call as: $this->_print_cpu_time(__LINE__, "commentary");
  //
  private function _print_cpu_time ($line, $comment) {

    $this->_debug_print($line, "CPU Time: " .
                        (microtime(true) - $this->_script_run_time_start) .
                        " [" . $comment . "]");
  }

  // _is_cache_file_current_delete_if_not
  //
  // Returns true if file exists and is not too old. Returns false if file is
  // too old, or does not exist, and will also delete the file if it is too
  // old.
  //
  private function _is_cache_file_current_delete_if_not
    ($filename, $max_file_age_in_minutes) {

    // < 0 if file is not there.
    $file_age_in_minutes = $this->_file_age_in_minutes($filename);

    // File is not there
    if ($file_age_in_minutes < 0) {
      return false;
    }

    // If told to ignore cache, do so
    if ($this->_client_parameters[WEATHER_PARSER_CP_IGNORE_CACHE]) {
      $this->_unlink_file_if_we_can($filename);
      return false;
    }

    if ($file_age_in_minutes <= $max_file_age_in_minutes) {
      return true;
    }

    $this->_unlink_file_if_we_can($filename);
    return false;
  }

  // _cache_file_current
  //
  // Returns true iff the file is a recognized cache file, and  it is not too
  // old. Will also delete any old version of the requested file so the caller
  // doesn't need to think about that.
  //
  private function _cache_file_current ($filename) {
  
    $filename_parts = explode('.', $filename);
    $last_part = end($filename_parts);
    $cache_type = strtolower($last_part);
    
    if (array_key_exists($cache_type,
                         $this->_private_readonly_cache_max_ages) === false) {
      // Odd to end up here, but this is the right thing to do if we do!
      $this->_unlink_file_if_we_can($filename);
      return true;
    }
    return $this->_is_cache_file_current_delete_if_not
      ($filename, $this->_private_readonly_cache_max_ages[$cache_type]);
  }

  private function _compare_by_mtime ($thisone, $thatone) {

    return filemtime($thatone) - filemtime($thisone);
  }

  // _keep_directory_small
  //
  // Passed the parameter "$max_files". This removes files from the directory,
  // oldest first, until there are no more than "$max_files" left in the
  // directory.
  private function _keep_directory_small ($dirname, $max_files) {

    // Build a file list. Add a '/' if not there.
    $dirname .= ((substr($dirname, -1) != '/') ? ('/') : (''));

    $file_list = glob($dirname . "*");

    // Now, clean up the file list, so it contains only files.
    for ($i = 0; $i < count($file_list);) {
      $filename = $file_list[$i];
      if ((!file_exists($filename)) || (!is_file($filename))) {
        // Remove this file, and '$i' stays the same as it will now point
        // to the next file to look at, as the current one is gone.
        array_splice($file_list, $i, 1);
      } else {
        // Move to the next file
        $i++;
      }
    }

    // Now we have a list of only files. Yay! Sort it
    usort($file_list, [WeatherParser::class, "_compare_by_mtime"]);


    // Now, delete any entries in the file list that are beyond the max
    // file count.
    if (count($file_list) >= $max_files) {
      for ($i = $max_files; $i < count($file_list); $i++) {
        $this->_unlink_file_if_we_can($file_list[$i]);
      }
    }
  }

  // _clean_cache
  //
  // Removes files in the cache directory. If $force is not set to true,
  // will remove them only if they are too old to be used.
  //
  private function _clean_cache ($force) {

    if ($handle = opendir(WEATHER_PARSER_CACHE_DIRECTORY)) {
      while (false !== ($entry = readdir($handle))) {
        $filename = WEATHER_PARSER_CACHE_DIRECTORY . $entry;
        if (file_exists($filename)) {
          if (is_file($filename)) {
            $file_age_in_minutes = $this->_file_age_in_minutes($filename);
            if (($force === true) ||
                ($this->_cache_file_current($filename) === false)) {
             $this->_unlink_file_if_we_can($filename);
            }
          }
        }
      }
      closedir($handle);
    }
  }

  // _log_item
  //
  // Dump out a single forecast item
  //
  private function _log_item ($code, $filename, $data) {

    if ($this->_client_parameters[WEATHER_PARSER_CP_LOGS]) {
      $this->_unlink_file_if_we_can($filename);
      $logs_dir = WEATHER_PARSER_LOGS_DIRECTORY;
      $logs_dir .= ((substr($logs_dir, -1) != '/') ? ('/') : (''));

      // Clean out the logs directory
      $this->_keep_directory_small($logs_dir,
                                   WEATHER_PARSER_MAX_FILES_IN_FORECAST_DUMP);

      $filename = $logs_dir . $filename . '-' . $this->_forecast_timestamp;

      // Now, store the filename information in the array
      $this->_forecast_information_files[$code] = $filename;

      $file = fopen($filename, 'w');
      fwrite($file, $data);
      fclose($file);
    }
  }

  // _getalerts
  //
  // Retrieve the alerts from the NWS.
  //
  private function _getalerts ($zone_or_county, $alert_to_get) {

    for ($i = 0; $i < WEATHER_PARSER_GET_ALERTS_MAX_ATTEMPTS; $i++) {
      $curl_handle = curl_init();
      curl_setopt($curl_handle,
                  CURLOPT_USERAGENT, WEATHER_PARSER_USER_AGENT_STRING);
      $alert_url =
        str_replace("%ZONEORCOUNTY%", $alert_to_get,
                    $this->_private_readonly_alerts_url);

      $alerts_headers =  $this->_private_readonly_alerts_headers;

      curl_setopt($curl_handle, CURLOPT_URL, $alert_url);
      if ($alerts_headers != "") {
        curl_setopt($curl_handle, CURLOPT_HTTPHEADER, [$alerts_headers]);
        curl_setopt($curl_handle, CURLOPT_VERBOSE, true);
      }
      curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 3);
      curl_setopt($curl_handle, CURLOPT_TIMEOUT,        5);
      curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, false);
      $result = curl_exec($curl_handle);
      if (($result !== false) and
          (!preg_match('/Database Connection Issue/Ui', $result))) {
        $this->_log_item('alerts-' . $zone_or_county, $zone_or_county .
                         '-raw-alerts-' . 'test-' .
                         $this->_private_readonly_alert_source_name, $result);
        return array('result' => 'OK',
                     'detail' => $result);
      } else {
        sleep(WEATHER_PARSER_GET_ALERTS_RETRY_SLEEP_SECONDS);
      }
    }
    // Failure
    return array('result' => 'ERROR 1',
                 'detail' => 'cURL failure on getting alerts. Error: ' .
                 strval(curl_errno($curl_handle)) . ' ' .
                 curl_error($curl_handle));
  }

  // _getsummary_soap
  //
  // Retrieve forecast summaries from the NWS via the SOAP interface.
  //
  private function _getsummary_soap () {

    // Initialize array to hold constant parameters
    try {
      $dateTimeZoneUs =
      new DateTimeZone($this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE]);
    } catch(Exception $e) {
      return array("result" => "ERROR 2",
                   "detail" =>
                   "Unable to create DateTimeZone element for timezone [" .
                   $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE] .
                   "]");
    }
    $currentTimeUs = new DateTime('now', $dateTimeZoneUs);
    $currentDayString = $currentTimeUs->format('Y-m-d');
    // Define new object and specify location of wsdl file.
    $soapclient =
      new nusoap_client($this->_private_readonly_forecast_summary_url[
                        FORECAST_SOURCE_SOAP]);

    // Output any error conditions from creating the client
    $err = $soapclient->getError();
    if ($err) {
      return array('result' => 'ERROR 3',
                   'detail' => 'Constructor error with SOAP: ' . $err);
    }
    $soap_params = array('latitude' =>
                         $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE],
                         'longitude' =>
                         $this->_client_parameters
                         [WEATHER_PARSER_CP_LONGITUDE],
                         'startDate' => $currentDayString,
                         'numDays' => 7,
                         'Unit' => 'e',
                         'format' => '12 hourly');

    $result = $soapclient->call('NDFDgenByDay', $soap_params,
                                'uri:DWMLgenByDay',
                                'uri:DWMLgenByDay/NDFDgenByDay');

    //  Processes any SOAP fault information we get back from the server
    if ($soapclient->fault) {
      return array('result' => 'ERROR 4S',
                   'detail' => print_r($result, true));
    }
    $err = $soapclient->getError();
    if ($err) {
      return array('result' => 'ERROR 5S',
                   'detail' => $err);
    }
    $this->_log_item('forecast-summary', 'summary-soap', $result);
    return array('result' => 'OK',
                 'detail' => $result);
  }

  // _getsummary_xml
  //
  // Retrieve forecast summaries from the NWS via the XML interface.
  //
  private function _getsummary_xml () {

    global $traverse_array;

    // Initialize array to hold constant parameters
    try {
      $dateTimeZoneUs =
      new DateTimeZone($this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE]);
    } catch(Exception $e) {
      return array("result" => "ERROR 2",
                   "detail" =>
                   "Unable to create DateTimeZone element for timezone [" .
                   $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE] .
                   "]");
    }
    $currentTimeUs = new DateTime('now', $dateTimeZoneUs);
    $currentDayString = $currentTimeUs->format('Y-m-d');
    // Define new object and specify location of wsdl file.
    $curl_handle = curl_init();
    $curl_url = 
      $this->_private_readonly_forecast_summary_url[FORECAST_SOURCE_XML];
    $curl_url .=
      "&lat=" . $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE] .
      "&lon=" . $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE] .
      "&startDate=" . $currentDayString;
    curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl_handle, CURLOPT_URL, $curl_url);
    curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 3);
    curl_setopt($curl_handle, CURLOPT_TIMEOUT,        5);
    curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl_handle, CURLOPT_USERAGENT,
                WEATHER_PARSER_USER_AGENT_STRING);

    $result = curl_exec($curl_handle);
    if (($result === false) ||
        (preg_match('/Database Connection Issue/Ui', $result))) {
      return array('result' => 'ERROR 2.1XMLSUMMARY',
                   'detail' => 'Curl Error: ' .
                   strval(curl_errno($curl_handle)) . ' '.
                   curl_error($curl_handle));
    }
    $this->_log_item('forecast-summary', 'summary-xml', $result);
    return array('result' => 'OK',
                 'detail' => $result);
  }

  // _getdetail_soap
  //
  // Get detailed forecast information from the NWS via the SOAP interface.
  //
  private function _getdetail_soap () {

    try {
      $dateTimeZoneUs =
      new DateTimeZone($this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE]);
    } catch(Exception $e) {
      return array('result' => 'ERROR 6',
                   'detail' =>
                   "Unable to create DateTimeZone element for timezone [" .
                   $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE] .
                   "]");
    }
    $startTimeUs = new DateTime('now', $dateTimeZoneUs);
    $startTimeString = $startTimeUs->format(DATE_W3C);
    $endTimeUs = new DateTime('now', $dateTimeZoneUs);
    $endTimeUs = clone $startTimeUs;
    $endTimeUs->add(new DateInterval('P7D'));
    $endTimeString = $endTimeUs->format(DATE_W3C);

    // Define new object and specify location of wsdl file.
    $soapclient =
      new nusoap_client($this->_private_readonly_forecast_detail_url
                        [FORECAST_SOURCE_SOAP]);

    // Output any error conditions from creating the client
    $err = $soapclient->getError();
    if ($err) {
      return array('result' => 'ERROR 7',
                   'detail' => 'Constructor error with SOAP: ' . $err);
    }

    $parameters = array('latitude' =>
                        $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE],
                        'longitude' =>
                        $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE],
                        'product' => 'time-series',
                        'startTime' => $startTimeString,
                        'endTime' => $endTimeString,
                        'Unit'      => 'e',
                        'weatherParameters' =>
                        array('qpf'      => 'true',   // Liquid precip. amount
                              'snow'     => 'true',   // Snow amount
                              'iceaccum' => 'true',   // Ice accumulation
                              'sky'      => 'true',   // Cloud cover amount
                              'rh'       => 'true',   // relative humidity
                              'wspd'     => 'true',   // wind speed
                              'wdir'     => 'true',   // wind direction
                              'icons'    => 'true',   // Icons
                              'wgust'    => 'true')); // Wind Gust Level

    $result = $soapclient->call('NDFDgen', $parameters, 'uri:DWMLgen',
                                'uri:DWMLgen/NDFDgen');

    //  Processes any SOAP fault information we get back from the server
    if ($soapclient->fault) {
      return array('result' => 'ERROR 8',
                   'detail' => print_r($result, true));
    }
    $err = $soapclient->getError();
    if ($err) {
      return array('result' => 'ERROR 9',
                   'detail' => $err);
    }

    $this->_log_item('forecast-detail', 'detail-soap', $result);
    return array('result' => 'OK',
                 'detail' => $result);
  }

  // _getdetail_xml
  //
  // Retrieve forecast details from the NWS via the XML interface.
  //
  private function _getdetail_xml () {

    // Initialize array to hold constant parameters
    try {
      $dateTimeZoneUs =
      new DateTimeZone($this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE]);
    } catch(Exception $e) {
      return array("result" => "ERROR 2",
                   "detail" =>
                   "Unable to create DateTimeZone element for timezone [" .
                   $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE] .
                   "]");
    }
    try {
      $dateTimeZoneUs =
      new DateTimeZone("America/Los_Angeles");
    } catch(Exception $e) {
      return array('result' => 'ERROR 6',
                   'detail' =>
                   "Unable to create DateTimeZone element for timezone [" .
                   "America/Los_Angeles" .
                   "]");
    }
    $startTimeUs = new DateTime('now', $dateTimeZoneUs);
    $startTimeString = $startTimeUs->format(DATE_W3C);
    $endTimeUs = new DateTime('now', $dateTimeZoneUs);
    $endTimeUs = clone $startTimeUs;
    $endTimeUs->add(new DateInterval('P7D'));
    $endTimeString = $endTimeUs->format(DATE_W3C);

    // Define new object and specify location of wsdl file.
    $curl_handle = curl_init();

    $curl_url = 
      $this->_private_readonly_forecast_detail_url[FORECAST_SOURCE_XML];
    $curl_url .=
      "?product=time-series" .
      "&lat=" . $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE] .
      "&lon=" . $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE] .
      "&Unit=e" .
      "&qpf=qpf" .
      "&snow=snow" .
      "&iceaccum=iceaccum" .
      "&sky=sky" .
      "&rh=rh" .
      "&wspd=wspd" .
      "&wdir=wdir" .
      "&icons=icons" .
      "&wgust=wgust" .
      "&begin=" . $startTimeString .
      "&end=" . $endTimeString;
    curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl_handle, CURLOPT_URL, $curl_url);
    curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 3);
    curl_setopt($curl_handle, CURLOPT_TIMEOUT,        5);
    curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl_handle, CURLOPT_USERAGENT,
                WEATHER_PARSER_USER_AGENT_STRING);

    $result = curl_exec($curl_handle);
    if (($result === false) ||
        (preg_match('/Database Connection Issue/Ui', $result))) {
      return array('result' => 'ERROR 2.1XMLDETAIL',
                   'detail' => 'Curl Error: ' .
                   strval(curl_errno($curl_handle)) . ' ' .
                   curl_error($curl_handle));
    }
    $this->_log_item('forecast-detail', 'detail-xml', $result);
    return array('result' => 'OK',
                 'detail' => $result);
  }

  // _decode_alerts
  //
  // Keep the complexity of supporting multiple alert encoding types to a few
  // places to keep the code easier to maintain.
  //
  private function _decode_alerts ($encoded_alerts) {

      return
        (($encoded_alerts == WEATHER_PARSER_EMPTY_ALERT_ENCODED_STRING) ? '' :
         json_decode($encoded_alerts, true));
  }

  // _validate_alerts
  //
  // Do some most trivial checks on whether or not the alert information we
  // received is in a format this script understands.
  //
  private function _validate_alerts (&$forecasts) {


    if (empty($forecasts['zone_alerts']['title'])) {
      return array('result' => 'ERROR 21ZJSON',
                   'error-print-out' => 'alerts-zone',
                   'detail' =>
                   'Forecast zone alerts not in expected JSON format');
    }
    if (empty($forecasts['county_alerts']['title'])) {
      return array('result' => 'ERROR 21CJSON',
                   'error-print-out' => 'alerts-county',
                   'detail' =>
                   'Forecast county alerts not in expected JSON format');
    }
    return array('result' => 'OK');
  }

  // _loadalerts
  //
  // Load the alerts into this script. Try using the cache for them, if it
  // exists and is not too old, else read the information from the net. Reads
  // both zone and county alerts.
  //
  private function _loadalerts () {

    global $traverse_array;

    $alert_county_test_file = $alert_zone_test_file = '';
    if (!empty($this->_client_parameters
               [WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE])) {
      $alert_county_test_file = $this->_client_parameters
        [WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE];
    }

    if (!empty($this->_client_parameters
               [WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE])) {
      $alert_zone_test_file = $this->_client_parameters
        [WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE];
    }
    
    if ($alert_zone_test_file != '') {
      $zone_filename = $alert_zone_test_file;
    } else {
      $zone_filename = WEATHER_PARSER_CACHE_DIRECTORY .
        'weather-alerts-zone-' .
        $this->_client_parameters[WEATHER_PARSER_CP_NWS_ZONE] . '.' .
        $this->_private_readonly_alert_source_name . '.' .
        WEATHER_PARSER_ALERT_CACHE_SUFFIX;
    }
    $got_data_from_cache = false;
    if (($alert_zone_test_file != '') ||
        (($this->_cache_file_current($zone_filename)) &&
         (!$this->_client_parameters[WEATHER_PARSER_CP_IGNORE_CACHE]))) {
      try {
        $old_errors = error_reporting(0);
        $file = fopen($zone_filename, 'r');
        error_reporting($old_errors);
        if ($file !== false) {
          $this->_storage_zone_alerts =
            $this->_decode_alerts
            (unserialize(fread($file, filesize($zone_filename))));
          fclose($file);
          $got_data_from_cache = true;
          $this->_log_item('alerts-zone', 'cached-zone-alerts-' . 
                           $this->_private_readonly_alert_source_name,
                           print_r($this->_storage_zone_alerts, true));
        }
      } catch (Exception $e) {
        echo "<BR>In CATCH";
        $this->_storage_zone_alerts = null;
        $got_data_from_cache = false;
      }
    }
    if ($got_data_from_cache === false) {
      $temp_result =
        $this->_getalerts('zone', $this->_client_parameters
                          [WEATHER_PARSER_CP_NWS_ZONE]);
      if ($temp_result['result'] != 'OK') {
        return $temp_result;
      }
      $file = fopen($zone_filename, 'w');
      fwrite($file, serialize($temp_result['detail']));
      fclose($file);
      $this->_storage_zone_alerts =
        $this->_decode_alerts($temp_result['detail']);
    }
    $this->_log_item('alerts-zone', 'final-zone-alerts-' . 
                     $this->_private_readonly_alert_source_name,
                     print_r($this->_storage_zone_alerts, true));

    // Don't try to read counties if we don't have a county
    if (empty($this->_client_parameters[WEATHER_PARSER_CP_NWS_COUNTY])) {
      $this->_storage_county_alerts = '';
    } else {
      if ($alert_county_test_file != '') {
        $county_filename = $alert_county_test_file;
      } else {
        $county_filename = WEATHER_PARSER_CACHE_DIRECTORY .
          'weather-alerts-county-' .
          $this->_client_parameters[WEATHER_PARSER_CP_NWS_COUNTY] . '.' .
          $this->_private_readonly_alert_source_name . '.' .
          WEATHER_PARSER_ALERT_CACHE_SUFFIX;
      }
      $got_data_from_cache = false;
      if (($alert_county_test_file != '') ||
          ($this->_cache_file_current($county_filename))) {
        try {
          $old_errors = error_reporting(0);
          $file = fopen($county_filename, 'r');
          error_reporting($old_errors);
          if ($file !== false) {
            $this->_storage_county_alerts =
              $this->_decode_alerts
              (unserialize(fread($file, filesize($county_filename))));
            fclose($file);
            $got_data_from_cache = true;
            $this->_log_item('alerts-county', 'cached-county-alerts-' .
                             $this->_private_readonly_alert_source_name,
                             print_r($this->_storage_county_alerts, true));
          }
        } catch (Exception $e) {
          $this->_storage_county_alerts = null;
          $got_data_from_cache = false;
        }
      }
      if ($got_data_from_cache === false) {
        $temp_result =
          $this->_getalerts('county', $this->_client_parameters
                            [WEATHER_PARSER_CP_NWS_COUNTY]);
        if ($temp_result['result'] != 'OK') {
          return $temp_result;
        }
        $file = fopen($county_filename, 'w');
        fwrite($file, serialize($temp_result['detail']));
        fclose($file);
        $this->_storage_county_alerts =
          $this->_decode_alerts($temp_result['detail']);
      }
      $this->_log_item('alerts-county', 'final-county-alerts-' . 
                       $this->_private_readonly_alert_source_name,
                       print_r($this->_storage_county_alerts, true));
    }
    
    return array('result' => 'OK');
  }

  // _loadsummary
  //
  // Load the forecast summary into this script. Try using the cache for it,
  // if it exists and is not too old, else read the information from the net.
  //
  private function _loadsummary () {

    $summary_filename = WEATHER_PARSER_CACHE_DIRECTORY . 'weather-summary-' .
      $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE] . '-' .
      $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE] . '.' .
      $this->_private_readonly_valid_forecast_source_names
      [$this->_forecast_source] . '.' . WEATHER_PARSER_FORECAST_CACHE_SUFFIX;
   
    $got_data_from_cache = false;
    if ($this->_cache_file_current($summary_filename)) {
      try {
        $old_errors = error_reporting(0);
        $file = fopen($summary_filename, 'r');
        error_reporting($old_errors);
        if ($file !== false) {
          $this->_storage_summary =
            $this->_cleaned_up_xmlize
            (unserialize(fread($file, filesize($summary_filename))));
          fclose($file);
          $got_data_from_cache = true;
        }
      } catch (Exception $e) {
        $this->_storage_summary = null;
        $got_data_from_cache = false;
      }
    }
    if ($got_data_from_cache === false) {
      if ($this->_forecast_source == FORECAST_SOURCE_SOAP) {
        $temp_result = $this->_getsummary_soap();
      } else {
        $temp_result = $this->_getsummary_xml();
      }
      if ($temp_result['result'] != 'OK') {
        return $temp_result;
      }
      $file = fopen($summary_filename, 'w');
      fwrite($file, serialize($temp_result['detail']));
      fclose($file);
      $this->_storage_summary =
        $this->_cleaned_up_xmlize($temp_result['detail']);
    }
   
    return array('result' => 'OK');
  }

  // _loaddetail
  //
  // Load the forecast detail into this script. Try using the cache for it,
  // if it exists and is not too old, else read the information from the net.
  //
  private function _loaddetail () {

    $detail_filename = WEATHER_PARSER_CACHE_DIRECTORY . 'weather-detail-' .
      $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE] . '-' .
      $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE] . '.' .
      $this->_private_readonly_valid_forecast_source_names
      [$this->_forecast_source] . '.' . WEATHER_PARSER_FORECAST_CACHE_SUFFIX;

    $got_data_from_cache = false;
    if ($this->_cache_file_current($detail_filename)) {
      try {
        $old_errors = error_reporting(0);
        $file = fopen($detail_filename, 'r');
        error_reporting($old_errors);
        if ($file !== false) {
          $this->_storage_detail =
            $this->_cleaned_up_xmlize
            (unserialize(fread($file, filesize($detail_filename))));
          fclose($file);
          $got_data_from_cache = true;
        }
      } catch (Exception $e) {
        $this->_storage_detail = null;
        $got_data_from_cache = false;
      }
    }
    if ($got_data_from_cache === false) {
      if ($this->_forecast_source == FORECAST_SOURCE_SOAP) {
        $temp_result = $this->_getdetail_soap();
      } else { // XML
        $temp_result = $this->_getdetail_xml();
      }
    
      if ($temp_result['result'] != 'OK') {
        return $temp_result;
      }
      $file = fopen($detail_filename, 'w');
      fwrite($file, serialize($temp_result['detail']));
      fclose($file);
      $this->_storage_detail =
        $this->_cleaned_up_xmlize($temp_result['detail']);
    }
    return array('result' => 'OK');
  }

  // _loadweather
  //
  // A single function to call to load all the weather information of all types
  // into the script. Will load alerts, forecast summary, and forecast details.
  //
  private function _loadweather () {

    $tstart = microtime(true);
    $temp_result = $this->_loadsummary();
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "load-summary",
                     'time-taken' => ($tend - $tstart)));
    if ($temp_result['result'] != 'OK') {
      // Try the backup forecast mechanism
      $this->_forecast_source =
        $this->_private_readonly_forecast_backup_source
        [$this->_forecast_source];
      $tstart = microtime(true);
      $temp_result = $this->_loadsummary();
      $tend = microtime(true);
      array_push($this->_timing_information,
                 array('thing-timed' => "load-summary-backup",
                       'time-taken' => ($tend - $tstart)));
      if ($temp_result['result'] != 'OK') {
        return $temp_result;
      }
    }
    $tstart = microtime(true);
    $temp_result = $this->_loaddetail();
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "load-detail",
                     'time-taken' => ($tend - $tstart)));
    if ($temp_result['result'] != 'OK') {
      // Try the backup forecast mechanism
      $this->_forecast_source =
        $this->_private_readonly_forecast_backup_source
        [$this->_forecast_source];
      $tstart = microtime(true);
      $temp_result = $this->_loaddetail();
      $tend = microtime(true);
      array_push($this->_timing_information,
                 array('thing-timed' => "load-detail-backup",
                       'time-taken' => ($tend - $tstart)));
      if ($temp_result['result'] != 'OK') {
        return $temp_result;
      }
    }
    $tstart = microtime(true);
    $temp_result = $this->_loadalerts();
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "load-alerts",
                     'time-taken' => ($tend - $tstart)));
    if ($temp_result['result'] != 'OK') {
      return $temp_result;
    }
    return array('result' => 'OK');
  }

  // _dump_timing_information
  //
  // Write the timing results to the timing file.
  //
  private function _dump_timing_information () {

    if ($this->_client_parameters[WEATHER_PARSER_CP_LOGS]) {
      // First, create a time-stamp
      try {
        $dateTimeZoneUs =
          new DateTimeZone($this->_client_parameters
                           [WEATHER_PARSER_CP_TIMEZONE]);
      } catch(Exception $e) {
        return array("result" => "ERROR 2",
                     "detail" =>
                     "Unable to create DateTimeZone element for timezone [" .
                     $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE] .
                     "]");
      }
      $currentTimeUs = new DateTime('now', $dateTimeZoneUs);
      $currentTimeString = $currentTimeUs->format('Y-m-d.H:i:s');

      $this->_keep_directory_small(WEATHER_PARSER_TIMINGS_DIRECTORY, 20);

      $timings_dir = WEATHER_PARSER_TIMINGS_DIRECTORY;
      $timings_dir .= ((substr($timings_dir, -1) != '/') ? ('/') : (''));

      $file =
        fopen($timings_dir . 'timing-' . $currentTimeString, 'w');
      for ($i = 0; $i < count($this->_timing_information); $i++) {
        fwrite($file,
               sprintf("%s,%.7f\n",
                       $this->_timing_information[$i]['thing-timed'],
                       strval($this->_timing_information[$i]['time-taken'])));
      }
      fclose($file);
    }
  }

  // _dump_alerts
  //
  // Dump out the alerts
  //
  private function _dump_alerts ($filename) {

    global $traverse_array;

    // JSON Zone Alerts
    $filename = 'zone-alerts-json';
    $this->_log_item('alerts-zone', $filename,
                     json_encode($this->_storage_zone_alerts,
                                 JSON_PRETTY_PRINT));
    // JSON County Alerts
    $filename = 'county-alerts-json';
    $this->_log_item('alerts-county', $filename,
                     json_encode($this->_storage_county_alerts,
                                 JSON_PRETTY_PRINT));
  }

  // _dump_forecast_information
  //
  // Print out the weather forecast and alert information to the outputted web
  // page.
  //
  private function _dump_forecast_information () {

    global $traverse_array;

    // dump forecast summary
    $filename = 'summary-' .
      $this->_private_readonly_valid_forecast_source_names
      [$this->_forecast_source];
    traverse_xmlize($this->_storage_summary, 'summary_');
    $this->_log_item('forecast-summary', $filename,
                     implode('', $traverse_array));

    // dump forecast detail
    $filename = 'detail-' .
      $this->_private_readonly_valid_forecast_source_names
      [$this->_forecast_source];
    traverse_xmlize($this->_storage_detail, 'detail_');
    $this->_log_item('forecast-detail', $filename,
                     implode('', $traverse_array));

    // dump alerts
    $this->_dump_alerts($filename);
  }

  // _build_forecast_information
  //
  // Retrieve the alerts from the NWS.
  //
  private function _build_forecast_information () {

    return array('summary' => $this->_storage_summary,
                 'detail' => $this->_storage_detail,
                 'zone_alerts' => $this->_storage_zone_alerts,
                 'county_alerts' => $this->_storage_county_alerts);
  }

  // _get_wind_beaufort_scale_name
  //
  // Retrieves the beaufort scale name for the maximum wind speed specified.
  // People seem to like these.
  //
  private function _get_wind_beaufort_scale_name ($speed_max) {

    foreach ($this->_private_readonly_wind_speed_to_beaufort_scale as
             $maxspeed => $beaufort_scale_name) {
      if ($speed_max <= $maxspeed) {
        return $beaufort_scale_name;
      }
    }
    return "";
  }

  // _sample_within_valid_range
  //
  // returns true if valid_min, valid_max integer range overlaps sample_min,
  // sample_max. valid_min and valid_max can also be null, which allows more
  // samples
  //
  private function _sample_within_valid_range ($valid_min, $valid_max,
                                               $sample_min, $sample_max) {

    $initial_result =
      (
       //  1) If valid_min and valid_max are both null, then any sample works.
       (($valid_min == null) && ($valid_max == null)) ||

       //  2) If valid_min is null, and valid_max is not null, then sample_min
       //     less than or equal to valid_max works.
       (($valid_min == null) && ($valid_max != null) &&
        ($sample_min <= $valid_max)) ||

       //  3) If valid_min is not null, and valid_max is null, then sample_max
       //     greater than or equal to valid_min works.
       (($valid_min != null) && ($valid_max == null) &&
        ($sample_max >= $valid_min)));

    // If we passed this much, then just send it along, no restrictions
    if ($initial_result) {
      return $initial_result;
    }

    // See if there is overlap of some sort

    //  4) If both valids are not null, then verify there is some overlap
    $partial_result = (($valid_max >= $sample_min) &&
                       ($valid_min <= $sample_max));

    // If even that is false, just return
    if (!$partial_result) {
      return $partial_result;
    }

    // If the partial result is good, we want more than 50% of the sample to
    // straddle the valid range. If it is exactly 50%, we want it to count if
    // it is looking forward (if its max is greater than the valid max), as
    // weather forecasts looking forward are best.

    // In these cases, always take the sample:
    //  s-------------s
    //      v----v
    //
    //      s----s
    //  v-------------v
    $partial_result =
      ((($sample_min <= $valid_min) &&
        ($sample_max >= $valid_max)) ||
       (($valid_min <= $sample_min) &&
        ($valid_max >= $sample_max)));

    // If one of those, just accept it
    if ($partial_result) {
      return $partial_result;
    }

    // Overlap; now we need to compute % of overlap
    // and choose the biggest overlap as the reading
    // we will use

    // s------s
    //    v-----v
    // What percent is smax-vmin to total range of
    // smax-smin? If > 50%, great. If <=, then no
    // (Remember, we look forward, that breaks ties)
    if ($sample_min <= $valid_min) {
      return ((($sample_max - $valid_min) * 1000 /
               ($sample_max - $sample_min)) > 500);
    }
    //     s-----s
    // v------v
    // What percent is vmax-smin to total range of
    // vmax-vmin? If >= 50%, great. If <, then no
    // (Remember, we look forward, that breaks ties)
    return ((($valid_max - $sample_min) * 1000 /
             ($sample_max - $sample_min)) >= 500);
  }

  // _get_stats_between_times
  //
  // If timestamp_min or timestamp_max is not set, then there is no
  // minimum or maximum time as appropriate. Returns null on error.
  //
  // $times_array points to the start of our optimized times array.
  //
  // $parameters is the start of the list, as in:
  // If the array is:
  //  $detail_[dwml][//][data][0][#][parameters][0][#][wind-speed][0][#][value]
  //        [0][#] = '5'
  // Then parameters is
  //  $detail_[dwml][#][data][0][#][parameters][0][#][wind-speed][0];
  //
  private function _get_stats_between_times (&$parameters,
                                             $title,
                                             &$times_array,
                                             $timestamp_min = null,
                                             $timestamp_max = null,
                                             $nodes_visited = null) {

    $result = array();
    if ($nodes_visited == null) {
      $nodes_visited = array();
    }
    if (isset($parameters['@']['units'])) {
      $result['units'] = $parameters['@']['units'];
    } else {
      $result['units'] = null;
    }
    $result['total'] = 0;
    $result['min'] = null;
    $result['max'] = null;
    $result['count'] = 0;
    $result['average'] = null;
    $result['start'] = null;
    $result['end'] = null;
    $ftv = 0.0;
    // Get the associated time array
    $this_time_layout = $parameters['@']['time-layout'];
    if (empty($times_array[$this_time_layout])) {
      return array('result' => 'ERROR 13',
                   'error-print-out' => 'forecast-detail',
                   'detail' => "For forecast detail type [" . $title . "] " .
                   "Time layout [" . $this_time_layout .
                   "] not found in times array [" .
                   var_dump($times_array) . "] with " .
                   sizeof($times_array) . "values");
    }
    $this_time_array = $times_array[$this_time_layout];
    for ($i = 0; (isset($parameters['#']['value'][$i]['#'])); $i++) {
      if (($nodes_visited == null) || (!in_array($i, $nodes_visited))) {
        $sample_start = $this_time_array[$i]['start-valid-timestamp'];
        $sample_end = $this_time_array[$i]['end-valid-timestamp'];
        if ($this->_sample_within_valid_range($timestamp_min, $timestamp_max,
                                             $sample_start, $sample_end)) {
          $this_value = $parameters['#']['value'][$i]['#'];
          $ftv = floatval($this_value);
          $result['total'] = $result['total'] + $ftv;
          $result['count']++;
          if ($result['start'] == null) {
            $result['start'] = $ftv;
          }
          $result['end'] = $ftv;;
          if (($result['min'] == null) || ($ftv < $result['min'])) {
            $result['min'] = $ftv;
          }
          if (($result['max'] == null) || ($ftv > $result['max'])) {
            $result['max'] = $ftv;
          }
          array_push($nodes_visited, $i);
        }
      }
    }
    if ($result['count'] > 0) {
      $result['average'] = $result['total'] / $result['count'];
    }
    $result['nodes_visited'] = $nodes_visited;
    return array('result' => 'OK', 'detail' => $result);
  }

  // _parse_time_scales
  //
  // Extract the time scales and put them in a better format for our use to
  // make followon work faster and simpler.
  //
  private function _parse_time_scales (&$forecasts_raw_times) {

    $result_time_scales = array();
    for ($i = 0; (!empty($forecasts_raw_times[$i]['#']['layout-key'][0]['#']));
         $i++) {
      $this_layout_key = $forecasts_raw_times[$i]['#']['layout-key'][0]['#'];
      $this_layout_key_parts = explode('-', $this_layout_key);
      $this_layout_key_length = -1;
      foreach ($this_layout_key_parts as $this_portion) {
        if (substr($this_portion, 0, 1) == 'n') {
          $this_layout_key_length = (integer) substr($this_portion, 1);
          break;
        }
      }
      if ($this_layout_key_length < 0) {
        return array('result' => 'ERROR 14',
                     'error-print-out' => 'forecast-detail',
                     'detail' => 'Layout key of ' . $this_layout_key .
                     ' in forecast detail contains no n-part');
      }
      $previous_needs_end_time = false;
      $one_second = date_interval_create_from_date_string('1 second');
      $one_day = date_interval_create_from_date_string('1 day');
      $end_time_set_once = false;
      $one_valid_received = false;
      $last_good_j = -1;
      for ($j = 0; $j < $this_layout_key_length; $j++) {
        if (isset($forecasts_raw_times[$i]['#']['start-valid-time'][$j]['@']
                  ['period-name'])) {
          $result_time_scales[$this_layout_key][$j]['period-name'] =
            $forecasts_raw_times[$i]['#']['start-valid-time'][$j]['@']
            ['period-name'];
        } else {
          $result_time_scales[$this_layout_key][$j]['period-name'] = null;
        }
        if ((isset($forecasts_raw_times[$i]['#']['start-valid-time']
                   [$j]['#']))) {
          $last_good_j = $j;
          $one_valid_received = true;
          $this_start_time =
            DateTime::createFromFormat(DATE_W3C,
                                       $forecasts_raw_times[$i]['#']
                                       ['start-valid-time'][$j]['#']);
          $result_time_scales[$this_layout_key][$j]['start-valid-time'] =
            clone $this_start_time;
          $result_time_scales[$this_layout_key][$j]['start-valid-timestamp'] =
            $this_start_time->getTimestamp();

          if ($previous_needs_end_time) {
            $result_time_scales[$this_layout_key][($j - 1)]['end-valid-time'] =
              clone date_sub($this_start_time, $one_second);
            $result_time_scales[$this_layout_key][($j - 1)]
              ['end-valid-timestamp'] =
              $result_time_scales[$this_layout_key][($j - 1)]
              ['end-valid-time']->getTimestamp();
            $previous_needs_end_time = false;
            $end_time_set_once = true;
          }
          if (!empty($forecasts_raw_times[$i]['#']['end-valid-time'][$j]
                     ['#'])) {
            $this_end_time =
              DateTime::createFromFormat(DATE_W3C,
                                         $forecasts_raw_times[$i]['#']
                                         ['end-valid-time'][$j]['#']);
            $result_time_scales[$this_layout_key][$j]['end-valid-time'] =
            clone $this_end_time;
            $result_time_scales[$this_layout_key][$j]['end-valid-timestamp'] =
              $result_time_scales[$this_layout_key][$j]
              ['end-valid-time']->getTimestamp();
            $end_time_set_once = true;
            $previous_needs_end_time = false;
          } else {
            $previous_needs_end_time = true;
          }
        }
      }
      if (($previous_needs_end_time) && ($one_valid_received)) {
        if ($end_time_set_once) {
          // Okay - see what the last delta was - repeat that
          $last_start_time = clone
            $result_time_scales[$this_layout_key][($last_good_j - 1)]
            ['start-valid-time'];
          $last_end_time = clone
            $result_time_scales[$this_layout_key][($last_good_j - 1)]
            ['end-valid-time'];
          $last_delta =
            date_diff($last_start_time, $last_end_time);
          $this_start_time =
            clone $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['start-valid-time'];
          $end_valid_time_for_us = date_add($this_start_time, $last_delta);
          $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['end-valid-time'] =
            clone $end_valid_time_for_us;
          $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['end-valid-timestamp'] =
            $end_valid_time_for_us->getTimestamp();
        } else {
          // Ugh. Assume one day minus a second
          $this_start_time =
            clone $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['start-valid-time'];
          $temp_end_time = date_add($this_start_time, $one_day);
          $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['end-valid-time'] =
            clone date_sub($temp_end_time, $one_second);
          $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['end-valid-timestamp'] =
            $result_time_scales[$this_layout_key][($last_good_j - 0)]
            ['end-valid-time']->getTimestamp();
        }
      }
    }
    return array('result' => 'OK',
                 'detail' => $result_time_scales);
  }

  // _find_detail_forecast_section
  //
  // Returns a detail forecast section based on the measurement type given.
  // Returns 'OK'/Error as other functions here do.
  //
  // Takes in: '$forecast_category' and '$forecast_type' as both are needed.
  //           For example, snowfall amount is 'precipitation' and 'snow' for
  //           these two values. '$forecast_detail' is the searched array.
  //
  private function _find_detail_forecast_section (&$forecast_detail,
                                                  $forecast_category,
                                                  $forecast_type) {

    for ($i = 0;
         (isset($forecast_detail[$forecast_category][$i]['@']['type']));
         $i++) {
      if ($forecast_detail[$forecast_category][$i]['@']['type'] ==
          $forecast_type) {
        return array('result' => 'OK',
                     'detail' => $forecast_detail[$forecast_category][$i]);
      }
    }
    return array('result' => 'ERROR 16',
                 'error-print-out' => 'forecast-detail',
                 'detail' => 'Forecast category/type of [' .
                             $forecast_category . ']/[' . $forecast_type .
                             '] not found in forecast detail.');
  }

  // _find_weather_conditions_forecast_section
  //
  // Returns 'OK'/Error as other functions here do.
  //
  private function _find_weather_conditions_forecast_section
    (&$forecast_summary) {

    if (isset($forecast_summary['weather'][0]['#']['weather-conditions']
              [0]['#'])) {
      return array('result' => 'OK',
                   'detail' =>
                   $forecast_summary['weather'][0]['#']['weather-conditions'],
                   'time-layout' =>
                   $forecast_summary['weather'][0]['@']['time-layout']);
    } else {
      return array('result' => 'ERROR 17',
                   'error-print-out' => 'forecast-summary',
                   'detail' => 'Summary weather conditions not found');
    }
  }

  // _get_nws_weather
  //
  // Obtain a forecast from the National Weather Service
  //
  private function _get_nws_weather () {

    $tstart = microtime(true);
    $load_result = $this->_loadweather();
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "load-weather",
                     'time-taken' => ($tend - $tstart)));
    if ($load_result['result'] != 'OK') {
      return $load_result;
    }

    $tstart = microtime(true);
    $forecasts = $this->_build_forecast_information();
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "build-weather-arrays",
                     'time-taken' => ($tend - $tstart)));

    // These two checks really verify we received proper data. If not, then
    // we move on, and have error reporting, but don't update the files.
    if (!isset($forecasts['summary']['dwml']['#']['head'][0]['#']['product'][0]
               ['@']['srsName'])) {
      return array('result' => 'ERROR 19',
                   'error-print-out' => 'forecast-summary',
                   'detail' => 'Forecast summary not expected format');
    }
    if (!isset($forecasts['detail']['dwml']['#']['head'][0]['#']['product'][0]
               ['@']['srsName'])) {
      return array('result' => 'ERROR 20',
                   'error-print-out' => 'forecast-detail',
                   'detail' => 'Forecast detail not expected format');
    }

    $tstart = microtime(true);
    $alert_result = $this->_validate_alerts($forecasts);
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "validate-alerts",
                     'time-taken' => ($tend - $tstart)));
    if ($alert_result['result'] != 'OK') {
      return $alert_result;
    }

    // Here, we have read in the files and they meet basic sanity checks.
    // Update the forecast information in the files to the better format.
    $this->_dump_forecast_information();
    return array('result' => 'OK', 'detail' => $forecasts);
  }

  // _compass_wind_direction
  //
  // Return a compass wind direction. To make sense, we actually center each
  // direction 'range' on the cardinal direction. There are cardinals every
  // 22.5 degrees, a total of 16 in 360 degrees,. North is 0 degrees, then they
  // run clockwise. Since we want to center North on 0, we allow 11.25 degrees
  // in either direction of North to be considered North. To make for easy
  // indexing into an array of directions, we shift our incoming wind direction
  // up by 11.25 degrees (half the width), then if the result is >= 360, we
  // subract 360 from it. Then we can simply index into an array by looking at
  // the floor of new direction / 22.5.
  //
  private function _compass_wind_direction ($true_wind_direction) {

    // We add 11.25 to the total, and make it a float.
    $twd = ((float) $true_wind_direction) +
      HALF_OF_DEGREES_PER_COMPASS_ROSE_SEGMENT;

    // We then subtract 360 if it is >= to that.
    if ($twd >= DEGREES_IN_CIRCLE) {
      $twd -= DEGREES_IN_CIRCLE;
    }

    // Now we have [0-[22.5 for north, [22.5-[45.000 for NNE, etc.
    return $this->_private_readonly_compass_rose
      [((integer) floor($twd /
                        DEGREES_PER_COMPASS_ROSE_SEGMENT))];
  }

  // _produce_weather_conditions_summary_from_string
  //
  // Take weather type, intensity and coverage and produce a single nice
  // weather conditions summary.
  //
  private function _produce_weather_conditions_summary_from_string
    ($weather_type, $intensity, $coverage) {

    //
    // %t is weather-type
    // %i is intensity, followed by a space
    //   unless intensity is 'none' or is missing, in which case, the
    //   replacement is the empty string.
    //
    // Any of the above as uppercase means capitalize first letter of output.
    //

    if (isset($this->_private_readonly_coverage_translate
              [strtolower($coverage)])) {
      $result = $this->_private_readonly_coverage_translate
        [strtolower($coverage)];
      // Odd case - we don't know the coverage. Use the default pattern.
      if (strlen($result) < 1) {
        $result = strtolower($coverage) . ' %i%t';
      }
      if (strtolower($intensity) != 'none') {
        $result = str_replace('%i', $intensity . ' ', $result);
        $result = str_replace('%I', ucfirst($intensity) . ' ', $result);
      } else {
        $result = str_replace('%i', '', $result);
        $result = str_replace('%I', '', $result);
      }
      $result = str_replace('%t', $weather_type, $result);
      $result = str_replace('%T', ucfirst($weather_type), $result);
      return $result;
    }
    return null;
  }

  // _action_string_fixup
  //
  // Put the action string before or after the existing text, based on the
  // first character of the action string.
  //
  private function _action_string_fixup ($existing_text, $action_string) {

    $action_prefix = strtoupper(substr($action_string, 0, 1));
    $action_text = substr($action_string, 1);
    switch ($action_prefix) {
    case 'P':
      return $action_text . ' ' . strtolower($existing_text);
      break;
    case 'S':
      // Suffix. Add lowercase at end
      return $existing_text . ', ' . strtolower($action_text);
      break;
    default:
      // Best to use the ENTIRE string if neither P or S given
      return $existing_text . ' - ' . strtolower($action_string);
      break;
    }
  }

  // _cleanup_alert_fields
  //
  // This cleans up all alert fields that need it. We used to populate key
  // missing fields with empty values, but have decided it is the wrong thing
  // to do. It's better to create an error than it is to report some default
  // that misleads a reader of the information into thinking that all is well
  // with the alert system.
  private function _cleanup_alert_fields ($alert_fields) {

    $cleaned_alert_fields = array();
    foreach ($alert_fields as $this_parameter => $this_value) {
      $cleaned_alert_fields[$this_parameter] = trim($this_value);
    }
    return $cleaned_alert_fields;
  }

  // _parse_vtec
  //
  // Takes in parameters for an alert array, including the VTEC, then fills in
  // more fields of the array as a result.
  //
  private function _parse_vtec ($vtec_text, $alert_results) {

    $returned_results = $alert_results;

    // Example: "/O.CON.KMTR.FR.Y.0001.000000T0000Z-220225T1700Z/";
    //            0 111 2222 33 4 5555 6666666666666666666666666
    //            
    $results = array();
    $results = preg_split("/\./", $vtec_text);
    // Element 1 is the action
    $action_code = $results[1];
    $action_string = '';
    if (isset($this->_private_readonly_alert_action_decoder[$action_code])) {
      $action_string =
        $this->_private_readonly_alert_action_decoder[$action_code];
    }

    $title_and_alt_text = '';
    $event_name = $returned_results['event'];

    $severity_level = '';
    if (!empty($alert_results['severity'])) {
      if (strtolower($alert_results['severity']) != 'unknown') {
        $severity_level = $alert_results['severity'] . ' ';
        $event_name = strtolower($event_name);
      }
    }

    $vtec_significance = $results[4];
    $title_and_alt_text = $severity_level . $event_name;
    if ($action_string != '') {
      $title_and_alt_text =
        $this->_action_string_fixup($title_and_alt_text,
                                    $action_string);
    }
    $returned_results['title'] = $title_and_alt_text;
    // The start/end times are in element 6
    // Start time is element 0. End time is element 1.
    $returned_results['vtec'] = $vtec_text;
    return array('result' => 'OK',
                 'detail' => $returned_results);
  }

  // _parse_one_alert
  //
  // Read in a single alert via JSON, rooted at $this_event
  //
  private function _parse_one_alert ($this_id, $this_event) {

    $alert_results = array();

    $linkbit_parts = explode('/', $this_id);
    $alert_results['id'] = $linkbit_parts[count($linkbit_parts) - 1];
    $alert_results['event'] = $this_event['event'];
    $alert_results['description'] = $this_event['description'];
    $alert_results['instruction'] = $this_event['instruction'];
    $alert_results['response'] = $this_event['response'];
    $alert_results['response-detail'] = 
      ((!empty($this->_private_readonly_alert_response_to_response_detail
               [strtolower($this_event['response'])])) ?
       $this->_private_readonly_alert_response_to_response_detail
       [strtolower($this_event['response'])] : '');
    $alert_results['effective'] = $this_event['effective'];
    $alert_results['expires'] = $this_event['expires'];
    $alert_results['sent'] = $this_event['sent'];
    // Sadly, sometimes these are not provided. Work around it.
    $alert_results['onset'] =
      (isset($this_event['onset']) ?
       ($this_event['onset']) : ($alert_results['effective']));
    $alert_results['ends'] =
      (isset($this_event['ends']) ?
       ($this_event['ends']) : ($alert_results['expires']));
    $alert_results['status'] = $this_event['status'];
    $alert_results['message-type'] = $this_event['messageType'];
    $alert_results['category'] = $this_event['category'];
    $alert_results['urgency'] = $this_event['urgency'];
    $alert_results['severity'] = $this_event['severity'];
    $alert_results['certainty'] = $this_event['certainty'];
    $alert_results['areaDesc'] = $this_event['areaDesc'];
    $severity_level = '';
    $event_name = $alert_results['event'];
    if (isset($alert_results['severity'])) {
      if (strtolower($alert_results['severity']) != 'unknown') {
        $severity_level = $alert_results['severity'] . ' ';
        $event_name = strtolower($event_name);
      }
    }
    $alert_results['title'] = $severity_level . $event_name;

    // Put in things that are not part of VTEC but want to be in all returns
    // here
    if (!empty($this_event['parameters']['NWSheadline'][0])) {
      $alert_results['NWSheadline'] =
        $this_event['parameters']['NWSheadline'][0];
    }

    if (!empty($this_event['parameters']['VTEC'][0])) {
      // Example: "/O.CON.KMTR.FR.Y.0001.000000T0000Z-220225T1700Z/";
      $vtec_text = $this_event['parameters']['VTEC'][0];
      $vtec_results = $this->_parse_vtec($vtec_text, $alert_results);
      if ($vtec_results['result'] != 'OK') {
        return $vtec_results;
      }
      $alert_results = $vtec_results['detail'];
    }
    return array('result' => 'OK',
                 'detail' => $this->_cleanup_alert_fields($alert_results));
  }

  // Used internally.
  private function _alert_text_color_choice ($red, $green, $blue) {

    // For the sRGB/ITU-R BT.709 color space
    $relative_luminence =
      (0.2126 * ($red / 255.0)) + (0.7152 * ($green / 255.0)) +
      (0.0722 * ($blue / 255.0));
    if ($relative_luminence >= 0.5) {
      return 0x000000;
    } else {
      return 0xffffff;
    }
  }

  // _add_alert_avoid_duplicates
  //
  // Do the work necessary to avoid duplicate alerts - if we are configured to
  // do that!
  //
  private function _add_alert_avoid_duplicates (&$final_results, $this_result,
                                                $include_duplicates) {

    $alert_already_there = false;
    $alert_to_replace = -1;

    if ($include_duplicates === false) {
      $final_results_length = count($final_results);
      for ($idup = 0; ($idup < $final_results_length); ++$idup) {
        if ($final_results[$idup]['event'] == $this_result['event']) {
          if (empty($this_result['severity']) &&
              empty($final_results[$idup]['severity'])) {
            // Now, replace the current one with this one if this one is
            // a later duration
            $alert_already_there = true;
            if ($this_result['effective'] >
                $final_results[$idup]['effective']) {
              $alert_to_replace = $idup;
            }
            break;
          } else {
            if ((!empty($this_result['severity'])) &&
                (!empty($final_results[$idup]['severity']))) {
              if ($this_result['severity'] ==
                  $final_results[$idup]['severity']) {
                $alert_already_there = true;
                // Prefer the one with the effective date/time further in the
                // future.
                if ($this_result['effective'] >
                    $final_results[$idup]['effective']) {
                  $alert_to_replace = $idup;
                }
                break;
              }
            }
          }
        }
      }
    }
    $lcevent = strtolower($this_result['event']);
    if (($alert_already_there === false) ||
        ($alert_to_replace >= 0)) {
      $title_to_p_and_c_key =
        array_search
        ($lcevent,
         $this->_private_readonly_alert_title_to_priority_and_colors);
      if ($title_to_p_and_c_key === false) {
        // Choose defaults based on code from True/Challis atom-advisory script
        $this_result['priority'] = 0;
        $this_result['phenominon-color'] = 0x000000;
        if (strpos($lcevent, 'warning') !== false) {
          $this_result['alert-border-color'] = 0xdd0000;
        } elseif (strpos($lcevent, 'watch') !== false) {
          $this_result['alert-border-color'] = 0xff9933;
        } elseif ((strpos($lcevent, 'advisory' !== false)) ||
                  (strpos($lcevent, 'outlook' !== false))) {
          $this_result['alert-border-color'] = 0xff6600;
        } elseif (strpos($lcevent, 'statement') !== false) {
          $this_result['alert-border-color'] = 0xcc7700;
        } elseif (strpos($lcevent, 'air') !== false) {
          $this_result['alert-border-color'] = 0x0066cc;
        } elseif ((strpos($lcevent, 'short' !== false)) ||
                  (strpos($lcevent, 'emergency' !== false))) {
          $this_result['alert-border-color'] = 0x009933;
        } elseif (strpos($lcevent, 'outage') !== false) {
          $this_result['alert-border-color'] = 0x3366cc;
        } else {
          // Ick
          $this_result['color'] = 0x333333;
        }
      } else {
        $this_result['priority'] =
          $this->_private_readonly_alert_title_to_priority_and_colors
          [$title_to_p_and_c_key + 1];
        $this_result['phenominon-color'] =
          $this->_private_readonly_alert_title_to_priority_and_colors
          [$title_to_p_and_c_key + 2];
        $this_result['alert-border-color'] =
          $this->_private_readonly_alert_title_to_priority_and_colors
          [$title_to_p_and_c_key + 3];
      }
      $this_result['textcolor'] =
        $this->_alert_text_color_choice
        (($this_result['phenominon-color'] & 0xff0000) >> 16,
         ($this_result['phenominon-color'] & 0xff00) >> 8,
         ($this_result['phenominon-color'] & 0xff));
      if ($alert_to_replace >=0) {
        $final_results[$alert_to_replace] = $this_result;
      } else {
        array_push($final_results, $this_result);
      }
    }
  }

  // _parse_alerts
  //
  // Parse all the zone and county alerts we were able to retrieve.
  //
  private function _parse_alerts (&$forecasts, &$zone_alerts,
                                  &$county_alerts) {

    $zone_alerts = '';
    $county_alerts = '';
    $zone_alerts = $forecasts['zone_alerts'];
    $county_alerts = $forecasts['county_alerts'];

    // Special check for no alerts.
    $final_results = array();

    $include_duplicates =
      $this->_client_parameters[WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS];

    for ($zi = 0;
         (!empty($zone_alerts['features'][$zi]['id']));
         $zi++) {
      $result =
        $this->_parse_one_alert($zone_alerts['features'][$zi]['id'],
                                $zone_alerts['features'][$zi]['properties']);
      if ($result['result'] != 'OK') {
        return $result;
      }
      $this->_add_alert_avoid_duplicates($final_results, $result['detail'],
                                         $include_duplicates);
    }

    for ($ci = 0;
         (!empty($county_alerts['features'][$ci]['id']));
         $ci++) {
      $result =
        $this->_parse_one_alert($county_alerts['features'][$ci]['id'],
                                $county_alerts['features'][$ci]['properties']);
      if ($result['result'] != 'OK') {
        return $result;
      }
      $this->_add_alert_avoid_duplicates($final_results, $result['detail'],
                                         $include_duplicates);
    }
    return array('result' => 'OK', 'detail' => $final_results);
  }

  // _parse_forecast
  //
  // Put the forecast into a more useful format. Takes the parsed summary and
  // detail forecasts, and our time scales, and extracts the information we
  // need, adding information from the detail forecast as needed.
  //
  private function _parse_forecast (&$summary_info,
                                    &$summary_time_scales,
                                    &$detail_info,
                                    &$detail_time_scales) {

    $weather_conditions_section =
      $this->_find_weather_conditions_forecast_section($summary_info);
    if ($weather_conditions_section['result'] != 'OK') {
      return $weather_conditions_section;
    }
    $weather_summary_info = $weather_conditions_section['detail'];
    $weather_summary_time_layout = $weather_conditions_section['time-layout'];
    if (empty($summary_time_scales[$weather_summary_time_layout])) {
      return array('result' => 'ERROR 22',
                   'error-print-out' => 'forecast-summary',
                   'detail' =>
                   'Time layout [' . $this_time_layout . '] not found') ;
    }
    $this_time_array = $summary_time_scales[$weather_summary_time_layout];
    $final_results = array();
    for ($i = 0; (!empty($weather_summary_info[$i])); $i++) {
      $period_name = $this_time_array[$i]['period-name'];
      $start_valid_time = $this_time_array[$i]['start-valid-time'];
      $end_valid_time = $this_time_array[$i]['end-valid-time'];
      // We do this so we can avoid getting multiple results since the valid
      // ranges from the NWS go 6AM-6PM vs. 6AM-5:59:59PM. Doing this at both
      // ends ensures we do not get unwanted duplicate values.
      $start_valid_timestamp =
        $this_time_array[$i]['start-valid-timestamp'] + 1;
      $end_valid_timestamp = $this_time_array[$i]['end-valid-timestamp'] - 1;
      $final_results[$i]['period-name'] = $period_name;
      if (!empty($weather_summary_info[$i]['@']['weather-summary'])) {
        $final_results[$i]['weather-summary'] =
          $weather_summary_info[$i]['@']['weather-summary'];
        $final_results[$i]['conditions-text'] = '';
        for ($j = 0;
             (isset($weather_summary_info[$i]['#']['value'][$j]
                    ['@']['coverage']));
             $j++) {
          if (isset($weather_summary_info[$i]['#']['value'][$j]
                    ['@']['additive'])) {
            $additive = $weather_summary_info[$i]['#']['value'][$j]
              ['@']['additive'];
            if (isset($additive)) {
              if ($additive == 'and') {
                $additive = 'with';
              }
              $final_results[$i]['conditions-text'] .= ', ' .
                $additive . ' ';
            }
          }
          $final_results[$i]['conditions-text'] .=
            $this->_produce_weather_conditions_summary_from_string
            ($weather_summary_info[$i]['#']['value'][$j]['@']['weather-type'],
             $weather_summary_info[$i]['#']['value'][$j]['@']['intensity'],
             $weather_summary_info[$i]['#']['value'][$j]['@']['coverage']);
        }
        if (!empty($final_results[$i]['conditions-text'])) {
          $final_results[$i]['conditions-text'] =
            ucfirst($final_results[$i]['conditions-text']);
        }
      } else {
        // NOTHING is put in this entry
        $final_results[$i] = 'No data';
        continue;
      }
      $start_valid_hour = (integer) $start_valid_time->format('H');
      $end_valid_hour = (integer) $end_valid_time->format('H');
      $nighttime = (($start_valid_hour <= 3) || ($start_valid_hour >= 16));
      if ($nighttime) {
        $low_temp_section = $this->_find_detail_forecast_section($summary_info,
                                                                'temperature',
                                                                'minimum');
        if ($low_temp_section['result'] != 'OK') {
          return $low_temp_section;
        }
        $low_temp_stats =
          $this->_get_stats_between_times($low_temp_section['detail'],
                                          'low-temp',
                                          $summary_time_scales,
                                          $start_valid_timestamp,
                                          $end_valid_timestamp);
        if ($low_temp_stats['result'] != 'OK') {
          return $low_temp_stats;
        }
        if ($final_results[$i]['conditions-text'] == '') {
          $final_results[$i]['temperature-text'] = 'L';
        } else {
          $final_results[$i]['temperature-text'] = ', with a l';
        }
        $final_results[$i]['temperature-text'] .= 'ow of ' .
          $low_temp_stats['detail']['min'] . '*deg*.';
        $final_results[$i]['low-temperature'] =
          $low_temp_stats['detail']['min'];
        $final_results[$i]['temperature-units'] =
          $low_temp_stats['detail']['units'];
        $final_results[$i]['high-temperature'] = null;
      } else {
        $high_temp_section =
          $this->_find_detail_forecast_section($summary_info,
                                               'temperature',
                                               'maximum');
        if ($high_temp_section['result'] != 'OK') {
          return $high_temp_section;
        }
        $high_temp_stats =
          $this->_get_stats_between_times($high_temp_section['detail'],
                                          'high-temp',
                                          $summary_time_scales,
                                          $start_valid_timestamp,
                                          $end_valid_timestamp);
        if ($high_temp_stats['result'] != 'OK') {
          return $high_temp_stats;
        }
        if ($final_results[$i]['conditions-text'] == '') {
          $final_results[$i]['temperature-text'] = 'H';
        } else {
          $final_results[$i]['temperature-text'] = ', with a h';
        }
        $final_results[$i]['temperature-text'] .= 'igh of ' .
          $high_temp_stats['detail']['max'] . '*deg*.';
        $final_results[$i]['high-temperature'] =
          $high_temp_stats['detail']['max'];
        $final_results[$i]['temperature-units'] =
          $high_temp_stats['detail']['units'];
        $final_results[$i]['low-temperature'] = null;
      }

      // Percent of precipitation
      $pop_percent_section =
        $this->_find_detail_forecast_section($summary_info,
                                            'probability-of-precipitation',
                                             '12 hour');
      if ($pop_percent_section['result'] != 'OK') {
        return $pop_percent_section;
      }
      $pop_percent_stats =
        $this->_get_stats_between_times($pop_percent_section['detail'],
                                        'pop-percent',
                                        $summary_time_scales,
                                        $start_valid_timestamp,
                                        $end_valid_timestamp);
      if ($pop_percent_stats['result'] != 'OK') {
        return $pop_percent_stats;
      }
      $pop_percent =
        $final_results[$i]['probability-of-precipitation-percent'] =
        $pop_percent_stats['detail']['max'];

      // Rain
      $rain_section =
        $this->_find_detail_forecast_section($detail_info, 'precipitation',
                                             'liquid');
      if ($rain_section['result'] != 'OK') {
        return $rain_section;
      }
      $rain_stats =
        $this->_get_stats_between_times($rain_section['detail'],
                                        'rain',
                                        $detail_time_scales,
                                        $start_valid_timestamp,
                                        $end_valid_timestamp);
      if ($rain_stats['result'] != 'OK') {
        return $rain_stats;
      }
      $final_results[$i]['rain-amount'] = $rain_stats['detail']['total'];
      $final_results[$i]['rain-units'] = $rain_stats['detail']['units'];
      $final_results[$i]['rain-start'] = $rain_stats['detail']['start'];
      $final_results[$i]['rain-end'] = $rain_stats['detail']['end'];

      $rain_start = $rain_stats['detail']['start'];
      $rain_end = $rain_stats['detail']['end'];

      // Snow
      $snow_section =
        $this->_find_detail_forecast_section($detail_info, 'precipitation',
                                             'snow');
      if ($snow_section['result'] != 'OK') {
        $final_results[$i]['snow-amount'] = 0;
        $final_results[$i]['snow-units'] = 'inches';
        $final_results[$i]['snow-start'] = 0;
        $final_results[$i]['snow-end'] = 0;
        $snow_start = 0;
        $snow_end = 0;
      } else {
        $snow_stats =
          $this->_get_stats_between_times($snow_section['detail'],
                                          'snow',
                                          $detail_time_scales,
                                          $start_valid_timestamp,
                                          $end_valid_timestamp);
        if ($snow_stats['result'] != 'OK') {
          $final_results[$i]['snow-amount'] = 0;
          $final_results[$i]['snow-units'] = 'inches';
          $final_results[$i]['snow-start'] = 0;
          $final_results[$i]['snow-end'] = 0;
          $snow_start = 0;
          $snow_end = 0;
        } else {
          $final_results[$i]['snow-amount'] = $snow_stats['detail']['total'];
          $final_results[$i]['snow-units'] = $snow_stats['detail']['units'];
          $final_results[$i]['snow-start'] = $snow_stats['detail']['start'];
          $final_results[$i]['snow-end'] = $snow_stats['detail']['end'];
          $snow_start = $snow_stats['detail']['start'];
          $snow_end = $snow_stats['detail']['end'];
        }
      }

      // Now we build the precipitation-text from all the info above.
      // There are four possibilities.
      // Snow and rain totals both 0 - then report nothing
      // Rain total > 0, snow total 0 - report rain
      // Snow total > 0, rain total 0 - report snow
      // Both snow and rain > 0 - report mixed snow and rain
      if (($final_results[$i]['snow-amount'] <= 0) &&
          ($final_results[$i]['rain-amount'] <= 0)) {
        if ($final_results[$i]['probability-of-precipitation-percent'] > 0) {
          $final_results[$i]['precipitation-text'] =
            'Chance of precipitation ' . (($i == 0) ? 'is' : 'will be') .
            ' ' . $final_results[$i]['probability-of-precipitation-percent'] .
            '%.';
        } else {
          $final_results[$i]['precipitation-text'] = null;
        }
      } else if (($final_results[$i]['snow-amount'] <= 0) &&
                 ($final_results[$i]['rain-amount'] > 0)) {
        $rain_units = $final_results[$i]['rain-units'];
        if (($rain_units == 'inches') &&
            ($final_results[$i]['rain-amount'] == 1)) {
          $rain_units = 'inch';
        }
        $final_results[$i]['precipitation-text'] =
          'Expecting approximately ' . $final_results[$i]['rain-amount'] .
          ' ' . $rain_units . ' of rain';
        // If start >= (end * 2)
        if ($rain_start >= ($rain_end * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly in the ' .
            ($nighttime ? 'evening' : 'morning');
        } else if ($rain_end >= ($rain_start * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly ' .
            ($nighttime ? 'after midnight' :
             'in the afternoon');
        }
        $final_results[$i]['precipitation-text'] .= '.';
        if ($final_results[$i]['probability-of-precipitation-percent'] > 0) {
          $final_results[$i]['precipitation-text'] .=
            ' Chance of precipitation ' . (($i == 0) ? 'is' : 'will be') .
            ' ' . $final_results[$i]['probability-of-precipitation-percent'] .
            '%.';
        }
      } else if (($final_results[$i]['snow-amount'] > 0) &&
                 ($final_results[$i]['rain-amount'] <= 0)) {
        $snow_units = $final_results[$i]['snow-units'];
        if (($snow_units == 'inches') &&
            ($final_results[$i]['snow-amount'] == 1)) {
          $snow_units = 'inch';
        }
        $final_results[$i]['precipitation-text'] =
          'Expecting approximately ' . $final_results[$i]['snow-amount'] .
          ' ' . $snow_units . ' of snow';
        // If start >= (end * 2)
        if ($snow_start >= ($snow_end * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly in the ' .
            ($nighttime ? 'evening' : 'morning') ;
        } else if ($snow_end >= ($snow_start * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly ' .
            ($nighttime ? 'after midnight' :
             'in the afternoon');
        }
        $final_results[$i]['precipitation-text'] .= '.';
        if ($final_results[$i]['probability-of-precipitation-percent'] > 0) {
          $final_results[$i]['precipitation-text'] .=
            ' Chance of precipitation ' . (($i == 0) ? 'is' : 'will be') .
            ' ' . $final_results[$i]['probability-of-precipitation-percent'] .
            '%.';
        }
      } else {
        $rain_units = $final_results[$i]['rain-units'];
        if (($rain_units == 'inches') &&
            ($final_results[$i]['rain-amount'] == 1)) {
          $rain_units = 'inch';
        }
        $final_results[$i]['precipitation-text'] =
          'Expecting a mix of approximately ' .
          $final_results[$i]['rain-amount'] . ' ' .
          $rain_units . ' of rain';
        // If start >= (end * 2)
        if ($rain_start >= ($rain_end * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly in the ' .
            ($nighttime ? 'evening' : 'morning') ;
        } else if ($rain_end >= ($rain_start * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly ' .
            ($nighttime ? 'after midnight' :
             'in the afternoon');
        }
        $final_results[$i]['precipitation-text'] .= ', and ' .
        $snow_units = $final_results[$i]['snow-units'];
        if (($snow_units == 'inches') &&
            ($final_results[$i]['snow-amount'] == 1)) {
          $snow_units = 'inch';
        }
        $final_results[$i]['snow-amount'] . ' ' .
          $snow_units . ' of snow';
        // If start >= (end * 2)
        if ($snow_start >= ($snow_end * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly in the ' .
            ($nighttime ? 'evening' : 'morning') ;
        } else if ($snow_end >= ($snow_start * 2)) {
          $final_results[$i]['precipitation-text'] .=
            ', mostly ' .
            ($nighttime ? 'after midnight' :
             'in the afternoon');
        }
        $final_results[$i]['precipitation-text'] .= '.';
        if ($final_results[$i]['probability-of-precipitation-percent'] > 0) {
          $final_results[$i]['precipitation-text'] .=
            ' Chance of precipitation ' . (($i == 0) ? 'is' : 'will be') .
            ' ' . $final_results[$i]['probability-of-precipitation-percent'] .
            '%.';
        }
      }

      // Ice accumulation
      $ice_section =
        $this->_find_detail_forecast_section($detail_info, 'precipitation',
                                             'ice');
      $got_ice_info = false;
      if ($ice_section['result'] == 'OK') {
        $ice_stats =
          $this->_get_stats_between_times($ice_section['detail'],
                                          'ice',
                                          $detail_time_scales,
                                          $start_valid_timestamp,
                                          $end_valid_timestamp);
        if ($ice_stats['result'] == 'OK') {
          $got_ice_info = true;
          $final_results[$i]['ice-accumulation-amount'] =
            $ice_stats['detail']['total'];
          $final_results[$i]['ice-accumulation-units'] =
            $ice_stats['detail']['units'];
          $final_results[$i]['ice-accumulation-start'] =
            $ice_stats['detail']['start'];
          $final_results[$i]['ice-accumulation-end'] =
            $ice_stats['detail']['end'];
          $ice_start = $ice_stats['detail']['start'];
          $ice_end = $ice_stats['detail']['end'];
          if ($final_results[$i]['ice-accumulation-amount'] > 0) {
            $final_results[$i]['ice-accumulation-text-detail'] =
              'Ice accumulation of ' .
              $final_results[$i]['ice-accumulation-amount'] . ' ' .
              $final_results[$i]['ice-accumulation-units'] . '.';
            $final_results[$i]['ice-accumulation-text-summary'] =
              'Ice accumulation of ' .
              $final_results[$i]['ice-accumulation-amount'] . ' ' .
              $final_results[$i]['ice-accumulation-units'];
            if ($ice_start >= ($ice_end * 2)) {
              $final_results[$i]['ice-accumulation-text-summary'] .=
                ', primarily in the ' .
                ($nighttime ? 'evening' : 'morning') ;
            } else if ($ice_end >= ($ice_start * 2)) {
              $final_results[$i]['ice-accumulation-text-summary'] .=
                ', primarily ' .
                ($nighttime ? 'after midnight' :
                 'in the afternoon');
            }
            $final_results[$i]['ice-accumulation-text-summary'] .= '.';
          } else {
            $final_results[$i]['ice-accumulation-text-summary'] = '';
            $final_results[$i]['ice-accumulation-text-detail'] = '';
          }
        }
      }

      if ($got_ice_info == false) {
        $final_results[$i]['ice-accumulation-amount'] = 0;
        $final_results[$i]['ice-accumulation-units'] =
          $final_results[$i]['ice-accumulation-start'] =
          $final_results[$i]['ice-accumulation-end'] =
          $ice_start = $ice_end = "n/a";
        $final_results[$i]['ice-accumulation-text-summary'] = "";
        $final_results[$i]['ice-accumulation-text-detail'] = "";
      }

      // Cloud cover
      $cloud_percent_section =
        $this->_find_detail_forecast_section($detail_info, 'cloud-amount',
                                             'total');
      if ($cloud_percent_section['result'] != 'OK') {
        return $cloud_percent_section;
      }
      $cloud_percent_stats =
        $this->_get_stats_between_times($cloud_percent_section['detail'],
                                        'cloud-percent',
                                        $detail_time_scales,
                                        $start_valid_timestamp,
                                        $end_valid_timestamp);
      if ($cloud_percent_stats['result'] != 'OK') {
        return $cloud_percent_stats;
      }
      $final_results[$i]['cloud-low'] = $cloud_percent_stats['detail']['min'];
      $final_results[$i]['cloud-high'] = $cloud_percent_stats['detail']['max'];
      $final_results[$i]['cloud-start'] =
        $cloud_percent_stats['detail']['start'];
      $final_results[$i]['cloud-end'] = $cloud_percent_stats['detail']['end'];
      $final_results[$i]['cloud-text-detail'] = 'Cloud coverage ' .
        $final_results[$i]['cloud-low'];
      if ($final_results[$i]['cloud-low'] !=
          $final_results[$i]['cloud-high']) {
        $final_results[$i]['cloud-text-detail'] .= '-' .
          $final_results[$i]['cloud-high'];
      }
      $final_results[$i]['cloud-text-detail'] .= '% of forecast area.';
      $cloud_low_eighth =
        (integer) round($final_results[$i]['cloud-low'] /
                        ONE_EIGHTH_OF_ONE_HUNDRED);
      $cloud_high_eighth =
        (integer) round($final_results[$i]['cloud-high'] /
                        ONE_EIGHTH_OF_ONE_HUNDRED);
      $cloud_start_eighth =
        (integer) round($final_results[$i]['cloud-start'] /
                        ONE_EIGHTH_OF_ONE_HUNDRED);
      $cloud_end_eighth =
        (integer) round($final_results[$i]['cloud-end'] /
                        ONE_EIGHTH_OF_ONE_HUNDRED);
      if ($nighttime) {
        $cloud_start_summary =
          $this->_private_readonly_cloud_summary_nighttime
          [$cloud_start_eighth];
        $cloud_end_summary =
          $this->_private_readonly_cloud_summary_nighttime[$cloud_end_eighth];
      } else {
        $cloud_start_summary =
          $this->_private_readonly_cloud_summary_daytime[$cloud_start_eighth];
        $cloud_end_summary =
          $this->_private_readonly_cloud_summary_daytime[$cloud_end_eighth];
      }
      $combined_cloud_summary =
        $cloud_start_summary . '/' . $cloud_end_summary;
      if ($cloud_start_summary != $cloud_end_summary) {
        $transition_name = null;
        if ($nighttime) {
          $transition_name =
            $this->_private_readonly_cloud_summary_nighttime_transitions
            [$combined_cloud_summary];
        } else {
          $transition_name =
            $this->_private_readonly_cloud_summary_daytime_transitions
            [$combined_cloud_summary];
        }
        if (is_null($transition_name) || ($transition_name == '')) {
          $transition_name = 'becoming';
        }
        $final_results[$i]['cloud-text-summary'] =
          ucfirst($cloud_start_summary . ' in the ' .
                  ($nighttime ? 'evening' : 'morning') .
                  ', ' . $transition_name . ' ' .
                  $cloud_end_summary . ' ' .
                  ($nighttime ? 'after midnight' :
                   'by the afternoon') . '.');
      } else {
        $final_results[$i]['cloud-text-summary'] =
          ucfirst($cloud_start_summary .
                  ' through the ' . ($nighttime ? 'night' : 'day') . '.');
      }

      // Icon - get from SUMMARY
      $icon_section = $this->_find_detail_forecast_section($summary_info,
                                                          'conditions-icon',
                                                          'forecast-NWS');
      if ($icon_section['result'] != 'OK') {
        return $icon_section;
      }

      if (empty($icon_section['detail']['#']['icon-link'][$i]['#'])) {
        return array('result' => 'ERROR 23',
                     'error-print-out' => 'forecast-detail',
                     'detail' => 'No icon found for forecast #' . $i . '.');
      }
      $icon_name = $icon_section['detail']['#']['icon-link'][$i]['#'];
      if ($icon_name == '') {
        $full_our_icon_path = '';
      } else {
        $parsed_icon =
          parse_url($icon_section['detail']['#']['icon-link'][$i]['#']);
        $our_icon = $parsed_icon['path'];
        $path_parts = pathinfo($our_icon);
        $our_icon = $path_parts['basename'];

        // Strip leading '/' if any
        if (substr($our_icon, 0, 1) == '/') {
          $our_icon = substr($our_icon, 1);
        }
        $precip_value = filter_var($our_icon, FILTER_SANITIZE_NUMBER_INT);
        // Further sanitize to get the number
        if (empty($precip_value)) {
          $precip_value = 0;
        } else {
          if ($precip_value < 10) {
            $precip_value = 0;
          }
        }
        $period = '.';
        if ($precip_value > 0) {
          $trial_our_icon = str_replace(range(0,9), '', $our_icon);
          if ($this->_client_parameters
                     [WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS]) {
            // See if there is a non-rain one
            $full_trial_our_icon_path =
              WEATHER_PARSER_ICON_DIRECTORY . $trial_our_icon;
            if (is_readable($full_trial_our_icon_path)) {
              $our_icon = $trial_our_icon;
            }
          } else {
            // Try to get the correct percentage based on
            // our own ideas of precipitation.
            $closest_pop_multiple_of_10 = floor(round($pop_percent / 10) * 10);
            if ($closest_pop_multiple_of_10 > 0) {
              $to_string = $closest_pop_multiple_of_10 . '.';
              $replace_count = 1;
              $trial_our_icon =
                str_replace($period, $to_string, $trial_our_icon,
                            $replace_count);
            }
            $full_trial_our_icon_path =
              WEATHER_PARSER_ICON_DIRECTORY . $trial_our_icon;
            if (is_readable($full_trial_our_icon_path)) {
              $our_icon = $trial_our_icon;
            }
          }
        }
        $full_our_icon_path = WEATHER_PARSER_ICON_DIRECTORY . $our_icon;
        if (!(is_readable($full_our_icon_path))) {
          return array('result' => 'ERROR 24',
                       'detail' => 'Icon [' . $full_our_icon_path .
                                   '] not found');
        }
      }
      $final_results[$i]['conditions-icon'] =
        $full_our_icon_path;

      // humidity
      $humidity_percent_section =
        $this->_find_detail_forecast_section($detail_info, 'humidity',
                                             'relative');
      if ($humidity_percent_section['result'] != 'OK') {
        return $humidity_percent_section;
      }
      $humidity_percent_stats =
        $this->_get_stats_between_times($humidity_percent_section['detail'],
                                        'humidity',
                                        $detail_time_scales,
                                        $start_valid_timestamp,
                                        $end_valid_timestamp);
      if ($humidity_percent_stats['result'] != 'OK') {
        return $humidity_percent_stats;
      }
      if ((is_null($humidity_percent_stats['detail']['min'])) ||
          (is_null($humidity_percent_stats['detail']['max']))) {
        $final_results[$i]['humidity-low'] = null;
        $final_results[$i]['humidity-high'] = null;
        $final_results[$i]['humidity-start'] = null;
        $final_results[$i]['humidity-end'] = null;
        $final_results[$i]['humidity-text-detail'] =
          $final_results[$i]['humidity-text-summary'] = '';
      } else {
        $final_results[$i]['humidity-low'] =
          $humidity_percent_stats['detail']['min'];
        $final_results[$i]['humidity-high'] =
          $humidity_percent_stats['detail']['max'];
        $final_results[$i]['humidity-start'] =
          $humidity_percent_stats['detail']['start'];
        $final_results[$i]['humidity-end'] =
          $humidity_percent_stats['detail']['end'];
        $final_results[$i]['humidity-text-detail'] = 'Humidity ' .
          $final_results[$i]['humidity-low'];
        if ($final_results[$i]['humidity-low'] !=
            $final_results[$i]['humidity-high']) {
          $final_results[$i]['humidity-text-detail'] .= '-' .
            $final_results[$i]['humidity-high'];
        }
        $final_results[$i]['humidity-text-detail'] .= '%';
        if ($humidity_percent_stats['detail']['start'] !=
            $humidity_percent_stats['detail']['end']) {
          $final_results[$i]['humidity-text-summary'] =
            ucfirst('humidity ' .
                    $humidity_percent_stats['detail']['start'] . '% in the ' .
                    ($nighttime ? 'evening' : 'morning') . ', ' .
                    (($humidity_percent_stats['detail']['start'] >
                      $humidity_percent_stats['detail']['end']) ?
                     'lowering' : 'rising') . ' to ' .
                    $humidity_percent_stats['detail']['end'] . '% ' .
                    ($nighttime ? 'after midnight' : 'by the afternoon') .
                    '.');
        } else {
          $final_results[$i]['humidity-text-summary'] =
            ucfirst('humidity ' . $humidity_percent_stats['detail']['start'] .
                    '% through the ' . ($nighttime ? 'night' : 'day') . '.');
        }
      }

      // Wind direction
      $wind_direction_section =
        $this->_find_detail_forecast_section($detail_info, 'direction',
                                             'wind');
      if ($wind_direction_section['result'] != 'OK') {
        return $wind_direction_section;
      }
      $wind_direction_stats =
        $this->_get_stats_between_times($wind_direction_section['detail'],
                                        'wind-direction',
                                        $detail_time_scales,
                                        $start_valid_timestamp,
                                        $end_valid_timestamp);
      if ($wind_direction_stats['result'] != 'OK') {
        return $wind_direction_stats;
      }
      $wind_min = $wind_direction_stats['detail']['min'];
      $wind_max = $wind_direction_stats['detail']['max'];
      $final_results[$i]['wind-direction-units'] =
        $wind_direction_stats['detail']['units'];
      // By definition, max is always greater than min. If it is
      // greater than 180, we actually want to report the max
      // direction first, then the min, as the range.
      if (($wind_max - $wind_min) <= 180) {
        $first_wind_direction = $wind_min;
        $second_wind_direction = $wind_max;
      } else {
        $first_wind_direction = $wind_max;
        $second_wind_direction = $wind_min;
      }
      $final_results[$i]['wind-direction-start'] =
        $wind_direction_stats['detail']['start'];
      $final_results[$i]['wind-direction-end'] =
        $wind_direction_stats['detail']['end'];
      $final_results[$i]['wind-direction-raw-first'] = $first_wind_direction;
      $final_results[$i]['wind-direction-raw-second'] = $second_wind_direction;
      $final_results[$i]['wind-direction-first'] =
        $this->_compass_wind_direction($first_wind_direction);
      $final_results[$i]['wind-direction-second'] =
        $this->_compass_wind_direction($second_wind_direction);

      // Sustained Wind speed
      $wind_speed_sustained_section =
        $this->_find_detail_forecast_section($detail_info, 'wind-speed',
                                             'sustained');
      if ($wind_speed_sustained_section['result'] != 'OK') {
        return $wind_speed_sustained_section;
      }
      $wind_speed_sustained_stats =
        $this->_get_stats_between_times
        ($wind_speed_sustained_section['detail'], 'wind-speed-sustained',
         $detail_time_scales, $start_valid_timestamp, $end_valid_timestamp);
      if ($wind_speed_sustained_stats['result'] != 'OK') {
        return $wind_speed_sustained_stats;
      }
      if ((is_null($wind_speed_sustained_stats['detail']['min'])) ||
          (is_null($wind_speed_sustained_stats['detail']['max']))) {
        $final_results[$i]['wind-sustained-low'] = null;
        $final_results[$i]['wind-sustained-high'] = null;
        $final_results[$i]['wind-sustained-start'] = null;
        $final_results[$i]['wind-sustained-end'] = null;
        $final_results[$i]['wind-gust-low'] = null;
        $final_results[$i]['wind-gust-high'] = null;
        $final_results[$i]['wind-gust-start'] = null;
        $final_results[$i]['wind-gust-end'] = null;
        $final_results[$i]['wind-text'] =
          $final_results[$i]['wind-sustained-beaufort-scale-name'] =
          $final_results[$i]['wind-gust-beaufort-scale-name'] = '';
      } else {
        $final_results[$i]['wind-sustained-low'] =
          $wind_speed_sustained_stats['detail']['min'];
        $final_results[$i]['wind-sustained-high'] =
          $wind_speed_sustained_stats['detail']['max'];
        $final_results[$i]['wind-sustained-start'] =
          $wind_speed_sustained_stats['detail']['start'];
        $wind_sustained_start = $wind_speed_sustained_stats['detail']['start'];
        $final_results[$i]['wind-sustained-end'] =
          $wind_speed_sustained_stats['detail']['end'];
        $wind_sustained_end = $wind_speed_sustained_stats['detail']['end'];
        $final_results[$i]['wind-speed-units'] =
          $wind_speed_sustained_stats['detail']['units'];
        // Convert knots to mph
        if ($final_results[$i]['wind-speed-units'] == 'knots') {
          $final_results[$i]['wind-sustained-low'] =
            round($final_results[$i]['wind-sustained-low'] * MPH_PER_KNOT);
          $final_results[$i]['wind-sustained-high'] =
            round($final_results[$i]['wind-sustained-high'] * MPH_PER_KNOT);
          $final_results[$i]['wind-sustained-start'] =
            round($final_results[$i]['wind-sustained-start'] * MPH_PER_KNOT);
          $final_results[$i]['wind-sustained-end'] =
            round($final_results[$i]['wind-sustained-end'] * MPH_PER_KNOT);
          $final_results[$i]['wind-speed-units'] = 'mph';
        }

        // Gust Wind speed
        $wind_speed_gust_section =
          $this->_find_detail_forecast_section($detail_info, 'wind-speed',
                                               'gust');
        if ($wind_speed_gust_section['result'] != 'OK') {
          return $wind_speed_gust_section;
        }
        $wind_speed_gust_stats =
          $this->_get_stats_between_times($wind_speed_gust_section['detail'],
                                          'wind-speed-gust',
                                          $detail_time_scales,
                                          $start_valid_timestamp,
                                          $end_valid_timestamp);
        if ($wind_speed_gust_stats['result'] != 'OK') {
          return $wind_speed_gust_stats;
        }
        $final_results[$i]['wind-gust-low'] =
          $wind_speed_gust_stats['detail']['min'];
        $final_results[$i]['wind-gust-high'] =
          $wind_speed_gust_stats['detail']['max'];
        $final_results[$i]['wind-gust-start'] =
          $wind_speed_gust_stats['detail']['start'];
        $wind_gust_start = floatval($wind_speed_gust_stats['detail']['start']);
        $final_results[$i]['wind-gust-end'] =
          $wind_speed_gust_stats['detail']['end'];
        $wind_gust_end = floatval($wind_speed_gust_stats['detail']['end']);
        // Convert knots to mph
        if ($wind_speed_gust_stats['detail']['units'] == 'knots') {
          $final_results[$i]['wind-gust-low'] =
            round(floatval($final_results[$i]['wind-gust-low']) *
            MPH_PER_KNOT);
          $final_results[$i]['wind-gust-high'] =
            round(floatval($final_results[$i]['wind-gust-high']) *
            MPH_PER_KNOT);
          $final_results[$i]['wind-gust-start'] =
            round(floatval($final_results[$i]['wind-gust-start']) *
            MPH_PER_KNOT);
          $final_results[$i]['wind-gust-end'] =
            round(floatval($final_results[$i]['wind-gust-end']) *
            MPH_PER_KNOT);
        }
        // Build wind text
        $wind_text = 'Winds ';
        if ($i == 0) {
          $wind_text .= 'are ';
        } else {
          $wind_text .= 'will be ';
        }
        $sustained_wind_beaufort_scale_name =
          $this->_get_wind_beaufort_scale_name
          ($final_results[$i]['wind-sustained-high']);
        $final_results[$i]['wind-sustained-beaufort-scale-name'] =
          $sustained_wind_beaufort_scale_name;
        if ($sustained_wind_beaufort_scale_name != '') {
          $wind_text .= $sustained_wind_beaufort_scale_name . ', ';
        }
        $wind_text .= $final_results[$i]['wind-direction-first'];
        if ($final_results[$i]['wind-direction-first'] !=
            $final_results[$i]['wind-direction-second']) {
          $wind_text .= '-' . $final_results[$i]['wind-direction-second'];
        }
        $wind_text .= ' at ' .
          $final_results[$i]['wind-sustained-low'];

        if ($final_results[$i]['wind-sustained-high'] !=
            $final_results[$i]['wind-sustained-low']) {
          $wind_text .= '-' .
            $final_results[$i]['wind-sustained-high'];
        }
        $wind_text .= ' ' .
          $final_results[$i]['wind-speed-units'];
        if (($final_results[$i]['wind-gust-high'] > 0) &&
            (($final_results[$i]['wind-gust-low'] >
              $final_results[$i]['wind-sustained-low']) ||
             ($final_results[$i]['wind-gust-high'] >
              $final_results[$i]['wind-sustained-high']))) {
          $wind_text .=  ', gusting to ';
          $gusting_wind_beaufort_scale_name =
            $this->_get_wind_beaufort_scale_name
            ($final_results[$i]['wind-gust-high']);
          $final_results[$i]['wind-gust-beaufort-scale-name'] =
            $gusting_wind_beaufort_scale_name;
          if (($gusting_wind_beaufort_scale_name !=
               $sustained_wind_beaufort_scale_name) &&
              ($gusting_wind_beaufort_scale_name != '')) {
            $wind_text .= $gusting_wind_beaufort_scale_name . ', ';
          }
          $wind_text .= $final_results[$i]['wind-gust-low'];
          if ($final_results[$i]['wind-gust-high'] !=
              $final_results[$i]['wind-gust-low']) {
            $wind_text .= '-' .
              $final_results[$i]['wind-gust-high'];
          }
          $wind_text .= ' ' .
            $final_results[$i]['wind-speed-units'];
        }
        $wind_text .= '.';
        // If start >= (end * 2)
        if (($wind_sustained_start >= ($wind_sustained_end * 2)) and
            ($wind_gust_start >= ($wind_gust_end * 2))) {
          $wind_text .= ' Winds weaker in the ' .
            ($nighttime ? 'evening' : 'morning') . '.';
        } else if (($wind_sustained_end >= ($wind_sustained_start * 2)) and
                   ($wind_gust_end >= ($wind_gust_start * 2))) {
          $wind_text .= ' Winds stronger ' .
            ($nighttime ? 'after midnight' :
             'in the afternoon') . '.';
        } else {
          if ($wind_sustained_start >= ($wind_sustained_end * 2)) {
            $wind_text .= ' Sustained winds stronger in the ' .
              ($nighttime ? 'evening' : 'morning') . '.';
          } else if ($wind_sustained_end >= ($wind_sustained_start * 2)) {
            $wind_text .= ' Sustained winds stronger ' .
              ($nighttime ? 'after midnight' :
               'in the afternoon') . '.';
          }
          // If start >= (end * 2)
          if ($wind_gust_start >= ($wind_gust_end * 2)) {
            $wind_text .= ' Gusting winds stronger in the ' .
              ($nighttime ? 'evening' : 'morning') . '.';
          } else if ($wind_gust_end >= ($wind_gust_start * 2)) {
            $wind_text .= ' Gusting winds stronger ' .
              ($nighttime ? 'after midnight' :
               'in the afternoon') . '.';
          }
        }
        $final_results[$i]['wind-text'] = $wind_text;
      }

      // Build the overall computed forecast
      $final_results[$i]['generated-forecast'] =
        $final_results[$i]['generated-forecast-computer-speak'] =
        $final_results[$i]['generated-short-forecast'] =
        $final_results[$i]['generated-short-forecast-computer-speak'] = '';
      $did_one = false;
      if (isset($final_results[$i]['conditions-text'])) {
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
          $final_results[$i]['generated-short-forecast'] .= ' ';
          $final_results[$i]['generated-short-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-forecast'] .=
          $final_results[$i]['conditions-text'];
        $final_results[$i]['generated-forecast-computer-speak'] .=
          $final_results[$i]['conditions-text'];
        $final_results[$i]['generated-short-forecast'] .=
          $final_results[$i]['conditions-text'];
        $final_results[$i]['generated-short-forecast-computer-speak'] .=
          strtolower($final_results[$i]['conditions-text']);
      } else {
        if ($did_one) {
          $final_results[$i]['generated-short-forecast'] .= ' ';
          $final_results[$i]['generated-short-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-short-forecast'] .=
          $final_results[$i]['weather-summary'];
        $final_results[$i]['generated-short-forecast-computer-speak'] .=
          str_replace('/', ' and or ',
          strtolower($final_results[$i]['weather-summary'])) . ', with a ';
      }
      if (isset($final_results[$i]['temperature-text'])) {
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
          $final_results[$i]['generated-short-forecast'] .= ' ';
          $final_results[$i]['generated-short-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-forecast'] .=
          $final_results[$i]['temperature-text'];
        $final_results[$i]['generated-forecast-computer-speak'] .=
          $final_results[$i]['temperature-text'];
        $final_results[$i]['generated-short-forecast'] .=
          $final_results[$i]['temperature-text'];
        $final_results[$i]['generated-short-forecast-computer-speak'] .=
          strtolower($final_results[$i]['temperature-text']);
        $did_one = true;
      }
      if (isset($final_results[$i]['ice-accumulation-text-summary'])) {
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
          $final_results[$i]['generated-short-forecast'] .= ' ';
          $final_results[$i]['generated-short-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-forecast'] .=
          $final_results[$i]['ice-accumulation-text-summary'];
        $final_results[$i]['generated-forecast-computer-speak'] .=
          $final_results[$i]['ice-accumulation-text-summary'];
        $final_results[$i]['generated-short-forecast'] .=
          $final_results[$i]['ice-accumulation-text-summary'];
        $final_results[$i]['generated-short-forecast-computer-speak'] .=
          $final_results[$i]['ice-accumulation-text-summary'];
        $did_one = true;
      }
      if (isset($final_results[$i]['cloud-text-summary'])) {
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-forecast'] .=
          $final_results[$i]['cloud-text-summary'];
        $final_results[$i]['generated-forecast-computer-speak'] .=
          $final_results[$i]['cloud-text-summary'];
        $did_one = true;
      }
      if (isset($final_results[$i]['precipitation-text'])) {
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
          $final_results[$i]['generated-short-forecast'] .= ' ';
          $final_results[$i]['generated-short-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-forecast'] .=
          $final_results[$i]['precipitation-text'];
        $final_results[$i]['generated-forecast-computer-speak'] .=
          $final_results[$i]['precipitation-text'];
        $final_results[$i]['generated-short-forecast'] .=
          $final_results[$i]['precipitation-text'];
        $final_results[$i]['generated-short-forecast-computer-speak'] .=
          $final_results[$i]['precipitation-text'];
        $did_one = true;
      }
      if (isset($final_results[$i]['humidity-text-summary'])) {
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
        }
        $final_results[$i]['generated-forecast'] .=
          $final_results[$i]['humidity-text-summary'];
        $final_results[$i]['generated-forecast-computer-speak'] .=
          $final_results[$i]['humidity-text-summary'];
        $did_one = true;
      }
      if (isset($final_results[$i]['wind-text'])) {
        $doing_short = false;
        if ($did_one) {
          $final_results[$i]['generated-forecast'] .= ' ';
          $final_results[$i]['generated-forecast-computer-speak'] .= ' ';
          if (($final_results[$i]['wind-gust-high'] >= 15) ||
              ($final_results[$i]['wind-sustained-high'] >= 15)) {
            $final_results[$i]['generated-short-forecast'] .= ' ';
            $final_results[$i]['generated-short-forecast-computer-speak'] .=
              ' ';
            $doing_short = true;
          }
        }
        $final_results[$i]['generated-forecast'] .=
          str_replace($this->_private_readonly_compass_rose,
                      $this->_private_readonly_compass_rose_abbreviations,
                      $final_results[$i]['wind-text']);
        $final_results[$i]['generated-forecast-computer-speak'] .=
          str_replace($this->_private_readonly_compass_rose,
                      $this->_private_readonly_compass_rose_computer_speak,
                      str_replace('-', ' to ',
                                  $final_results[$i]['wind-text']));
        if ($doing_short) {
          $final_results[$i]['generated-short-forecast'] .=
            str_replace($this->_private_readonly_compass_rose,
                        $this->_private_readonly_compass_rose_abbreviations,
                        $final_results[$i]['wind-text']);
          $final_results[$i]['generated-short-forecast-computer-speak'] .=
            str_replace($this->_private_readonly_compass_rose,
                        $this->_private_readonly_compass_rose_computer_speak,
                        str_replace('-', ' to ',
                                    $final_results[$i]['wind-text']));
        }
        $did_one = true;
      }
      if (!$did_one) {
        $final_results[$i]['generated-forecast-computer-speak'] =
          'No forecast available at this time; try again later.';
      }

      $final_results[$i]['generated-forecast'] =
        str_replace('*deg*', '', $final_results[$i]['generated-forecast']);
      $final_results[$i]['generated-short-forecast'] =
        str_replace('*deg*', '',
                    $final_results[$i]['generated-short-forecast']);

      $final_results[$i]['generated-forecast-computer-speak'] =
        str_replace('%', ' percent',
                    $final_results[$i]['generated-forecast-computer-speak']);
      $final_results[$i]['generated-forecast-computer-speak'] =
        str_replace('mph', ' miles per hour',
                    $final_results[$i]['generated-forecast-computer-speak']);
      $final_results[$i]['generated-forecast-computer-speak'] =
        str_replace('*deg*', ' degrees',
                    $final_results[$i]['generated-forecast-computer-speak']);

      $final_results[$i]['generated-short-forecast-computer-speak'] =
        str_replace('%', ' percent',
                    $final_results[$i]
                    ['generated-short-forecast-computer-speak']);
      $final_results[$i]['generated-short-forecast-computer-speak'] =
        str_replace('mph', ' miles per hour',
                    $final_results[$i]
                    ['generated-short-forecast-computer-speak']);
      $final_results[$i]['generated-short-forecast-computer-speak'] =
        str_replace('*deg*', ' degrees',
                    $final_results[$i]
                    ['generated-short-forecast-computer-speak']);
    }
    return array('result' => 'OK', 'detail' => $final_results);
  }

  // _parse_parameter
  //
  // Parse a single parameter given by the caller to this script
  //
  private function _parse_parameter ($parameter, $value) {

    // Default action
    $filter_result = $value;

    switch ($parameter) {

    case WEATHER_PARSER_CP_LATITUDE:
      $filter_result = filter_var($value, FILTER_VALIDATE_FLOAT);
      if (($filter_result === false) || ($filter_result > 90.0) ||
          ($filter_result < -90.0)) {
        return array('result' => 'ERROR 26',
                     'detail' =>
                     "'" . $parameter .
                     "' must be between -90.0 and 90.0, inclusive. You gave ["
                     . $value . "]");
      }
      break;

    case WEATHER_PARSER_CP_LONGITUDE:
      $filter_result = filter_var($value, FILTER_VALIDATE_FLOAT);
      if (($filter_result === false) || ($filter_result > 180.0) ||
          ($filter_result < -180.0)) {
        return array('result' => 'ERROR 27',
                     'detail' =>
                     "'" . $parameter .
                     "' must be between -180.0 and 180.0, inclusive. You " .
                     "gave [" . $value . "]");
      }
      break;

    case WEATHER_PARSER_CP_PLACENAME:
      $filter_result = htmlspecialchars($value);
      if ($filter_result === false) {
        return array('result' => 'ERROR 28',
                     'detail' => "Poorly formed '" . $parameter .
                     "' given - it could not be sanitized.");
      }
      break;

    case WEATHER_PARSER_CP_NWS_ZONE:
      $filter_result = strtoupper(trim($value));
      $filter_result =
        filter_var($filter_result, FILTER_VALIDATE_REGEXP,
                   array('options' =>
                         array('regexp' => '/^[a-zA-Z]{2}[zZ]\d{3}$/')));
      if ($filter_result === false) {
        return array('result' => 'ERROR 29Z',
                     'detail' => "Poorly formatted '" . $parameter .
                     "' given. Should be of form XXZNNN where 'X' indicates " .
                     "a letter A-Z, inclusive, 'Z' is the specific letter " .
                     "Z, and NNN are digits 0-9, inclusive. You gave [" .
                     $value . "]");
      }
      break;

    case WEATHER_PARSER_CP_NWS_COUNTY:
      $filter_result = strtoupper(trim($value));
      if (!empty($filter_result)) {
        $filter_result =
          filter_var($filter_result, FILTER_VALIDATE_REGEXP,
                     array('options' =>
                           array('regexp' => '/^[a-zA-Z]{2}[cC]\d{3}$/')));
        if ($filter_result === false) {
          return array('result' => 'ERROR 29C',
                       'detail' => "Poorly formatted '" . $parameter .
                       "' given. Should be of form XXCNNN where 'X' " .
                       "indicates a letter A-Z, inclusive, 'C' is the " .
                       "specific letter C, and NNN are digits 0-9, " .
                       "inclusive. You gave [" . $value . "]");
        }
      }
      break;

    case WEATHER_PARSER_CP_NWS_FORECAST_OFFICE:
      $filter_result = strtoupper(trim($value));
      if (!empty($filter_result)) {
        $filter_result =
          filter_var($filter_result, FILTER_VALIDATE_REGEXP,
                     array('options' =>
                           array('regexp' => '/^[a-zA-Z]{3}$/')));
        if ($filter_result === false) {
          return array('result' => 'ERROR 29C',
                       'detail' => "Poorly formatted '" . $parameter .
                       "' given. Should be of form XXCNNN where 'X' " .
                       "indicates a letter A-Z, inclusive, 'C' is the " .
                       "specific letter C, and NNN are digits 0-9, " .
                       "inclusive. You gave [" . $value . "]");
        }
      }
      break;

    case WEATHER_PARSER_CP_TIMEZONE:
      if (!$this->_isValidTimezoneId($value)) {
        return array('result' => 'ERROR 30',
                     'detail' => "Unkown '" . $parameter .
                     "' given [" . $value . "]");
      }
      break;

    case WEATHER_PARSER_CP_IGNORE_CACHE:
    case WEATHER_PARSER_CP_LOGS:
    case WEATHER_PARSER_CP_DEVELOPMENT_MODE:
    case WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS:
    case WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS:
      if (!is_bool($value)) {
        $filter_result = filter_var($value, FILTER_VALIDATE_BOOLEAN,
                                    FILTER_NULL_ON_FAILURE);
        if ($filter_result === false) {
          return array('result' => 'ERROR 33',
                       'detail' => "need true/false value for the '" .
                       $parameter . "' parameter. You gave [" . $value . "]");
        }
      } else {
        $filter_result = $value;
      }
      break;

    case WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE:
    case WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE:
      $value = trim($value);
      if (file_exists($value)) {
        if (is_file($value)) {
          if (is_readable($value)) {
            $filter_result = $value;
            break;
          }
        }
      }
      return array('result' => 'ERROR 31',
                   'detail' =>
                   "'" . $parameter .
                   "' must be a file in the file tree of the weather " .
                   "webpage and must be readable. You gave [" . $value . "]");
      break;

    case WEATHER_PARSER_CP_FORECAST_SOURCE:
      $forecast_source = strtolower(trim($value));
      if (!isset($this->_private_readonly_valid_forecast_sources
                 [$forecast_source])) {
        $forecast_source_names = "'" .
          implode("', '",
                  array_keys
                  ($this->_private_readonly_valid_forecast_sources)) . "'";
        return array('result' => 'ERROR 43',
                     'detail' => "Invalid forecast source. Must be one of [" .
                     $forecast_source_names . "]. You gave [" . $value . "]");
      }
      $this->_forecast_source =
        $this->_private_readonly_valid_forecast_sources[$forecast_source];
      break;

    default:
      return array("result" => "ERROR 40",
                   "detail" => "Unknown parameter [" . $parameter . "]");
      break;

    }

    // If we got this far, set the proper element in the parameters array
    // to the filter result.
    $this->_client_parameters[$parameter] = $filter_result;
    return array('result' => 'OK');
  }

  // _parse_all_parameters
  //
  // Loop through the parameters passed to this script, and parse them one by
  // one by calling $this->_parse_parameter for each.
  //
  private function _parse_all_parameters ($data) {

    foreach ($data as $this_parameter => $this_value) {
      $current_result =
        $this->_parse_parameter($this_parameter, $this_value);
      if ($current_result['result'] != 'OK') {
        return $current_result;
      }
    }
    $error_string = '';

    if (is_null($this->_client_parameters
                [WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS])) {
      $this->_client_parameters[WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS] =
        $this->_private_readonly_default_avoid_percentage_icons;
    }

    if (is_null($this->_client_parameters
                [WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS])) {
      $this->_client_parameters[WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS] =
        $this->_private_readonly_default_show_duplicate_alerts;
    }

    if (is_null($this->_client_parameters
                [WEATHER_PARSER_CP_IGNORE_CACHE])) {
      $this->_client_parameters[WEATHER_PARSER_CP_IGNORE_CACHE] =
        $this->_private_readonly_default_ignore_cache;
    }

    if (is_null($this->_client_parameters
                [WEATHER_PARSER_CP_LOGS])) {
      $this->_client_parameters[WEATHER_PARSER_CP_LOGS] =
        $this->_private_readonly_default_logs;
    }

    if (is_null($this->_client_parameters
                [WEATHER_PARSER_CP_DEVELOPMENT_MODE])) {
      $this->_client_parameters[WEATHER_PARSER_CP_DEVELOPMENT_MODE] =
        $this->_private_readonly_default_development_mode;
    }

    if (is_null($this->_client_parameters
                [WEATHER_PARSER_CP_FORECAST_SOURCE])) {
      $this->_client_parameters[WEATHER_PARSER_CP_FORECAST_SOURCE] =
        $this->_private_readonly_default_forecast_source;
    }

    if (is_null
        ($this->_client_parameters[WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE])) {
      $this->_client_parameters[WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE] =
        $this->_private_readonly_default_alert_zone_test_file;
    }

    if (is_null
        ($this->_client_parameters
         [WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE])) {
      $this->_client_parameters[WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE] =
        $this->_private_readonly_default_alert_county_test_file;
    }

    foreach ($this->_client_parameters as $this_parameter => $this_value) {
      if (is_null($this->_client_parameters[$this_parameter])) {
        $error_string .= "'" . $this_parameter . "' not properly set...";
      }
    }
    if ($error_string != '') {
      return array('result' => 'ERROR 25',
                   'detail' => 'Forecast parameters not properly set: ' .
                   $error_string);
    }
    return array('result' => 'OK');
  }

  // _obtain_forecast_work
  //
  // Do the work that would be expected of _obtain_forecast, but we nest it
  // one level deeper into this function, so we can have code to process the
  // results of any step we take without cluttering the main '_obtain_forecast'
  // code.
  //
  private function _obtain_forecast_work ($parameters) {

    if (!is_array($parameters)) {
      return array('result' => 'ERROR 41',
                   'error_type' => 'parameter parsing',
                   'detail' =>
                   'Need to pass array of param/value pairs to ' .
                   'obtain_forecast');
    }
    $tstart = microtime(true);
    $parameter_check_result = $this->_parse_all_parameters($parameters);
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "parameter-check",
                     'time-taken' => ($tend - $tstart)));
                                    
    if ($parameter_check_result['result'] != 'OK') {
      $parameter_check_result['error_type'] = 'parameter parsing';
      return $parameter_check_result;
    }

    // First, create a time-stamp
    try {
      $dateTimeZoneUs =
      new DateTimeZone($this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE]);
    } catch(Exception $e) {
      return array("result" => "ERROR 42",
                   "detail" =>
                   "Unable to create DateTimeZone element for timezone [" .
                   $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE] .
                   "]");
    }
    $currentTimeUs = new DateTime('now', $dateTimeZoneUs);

    $this->_forecast_timestamp = $currentTimeUs->format('Y-m-d.H:i:s');

    // No longer needed. Free memory
    $parameter_check_result = null;

    $tstart = microtime(true);
    $forecast_result = $this->_get_nws_weather();
    $tend = microtime(true);
    array_push($this->_timing_information,
               array('thing-timed' => "get-nws-weather",
                     'time-taken' => ($tend - $tstart)));
    if ($forecast_result['result'] != 'OK') {
      $forecast_result['error_type'] = 'forecast generation';
      return $forecast_result;
    }
    $forecasts = $forecast_result['detail'];

    // No longer needed. Free memory
    $forecast_result = null;

    $our_result = array();

    // We base our forecasts off the summary grid times. We need to take data
    // from the detail grid to enhance that forecast.

    // This is done in multiple steps. First, we get the time scales for both
    // summary and detail extracted in ways that make working with them much
    // more efficient in the next steps.
    $summary_time_scales_temp =
      $this->_parse_time_scales($forecasts['summary']['dwml']['#']['data'][0]
                               ['#']['time-layout']);
    if ($summary_time_scales_temp['result'] != 'OK') {
      $summary_time_scales_temp['error_type'] = 'forecast generation';
      return $summary_time_scales_temp;
    }
    $summary_time_scales = $summary_time_scales_temp['detail'];
    $detail_time_scales_temp =
      $this->_parse_time_scales($forecasts['detail']['dwml']['#']['data'][0]
                               ['#']['time-layout']);
    if ($detail_time_scales_temp['result'] != 'OK') {
      $detail_time_scales_temp['error_type'] = 'forecast generation';
      return $detail_time_scales_temp;
    }
    $detail_time_scales = $detail_time_scales_temp['detail'];
    // Now let's extract the summary and detail information into an array that
    // we can work with.
    $parsed_forecast_array =
      $this->_parse_forecast($forecasts['summary']['dwml']['#']['data'][0]
                             ['#']['parameters'][0]['#'],
                             $summary_time_scales,
                             $forecasts['detail']['dwml']['#']['data'][0]
                             ['#']['parameters'][0]['#'],
                             $detail_time_scales);

    if ($parsed_forecast_array['result'] != 'OK') {
      $parsed_forecast_array['error_type'] = 'forecast generation';
      return $parsed_forecast_array;
    }
    $parsed_forecast = $parsed_forecast_array['detail'];

    // No longer needed. Free memory
    $parsed_forecast_array = null;

    // Empty the cache if requested.
    if ($this->_client_parameters[WEATHER_PARSER_CP_IGNORE_CACHE]) {
      $this->_clean_cache(true);
    }

    // Now process the alerts.
    $zone_alerts = array();
    $county_alerts = array();
    $parsed_alerts_array =
      $this->_parse_alerts($forecasts, $zone_alerts, $county_alerts);
    if ($parsed_alerts_array['result'] != 'OK') {
      $parsed_alerts_array['error_type'] = 'forecast generation';
      return $parsed_alerts_array;
    }

    $parsed_alerts = $parsed_alerts_array['detail'];

    // No longer needed. Free memory
    $parsed_alerts_array = null;

    // Now, add in the map information.
    $alerts_map = array();

    $alerts_map['url'] = '';
    if ((!empty($parsed_alerts)) && (count($parsed_alerts) > 0)) {
      $didone = false;
      for ($i = $j = 0; (!empty($parsed_alerts[$i]['event'])); $i++) {
        $alert_map_event = $parsed_alerts[$i]['event'];
        $alert_map_event_lc = strtolower($alert_map_event);
        $already_there = false;
        for ($k = 0; ((!empty($alerts_map['keys'][$k]['event'])) &&
                      ($already_there === false)); $k++) {
          if (strtolower($alerts_map['keys'][$k]['event']) ==
              $alert_map_event_lc) {
            $already_there = true;
          }
        }

        if ($already_there === false) {

          $alert_map_color = '';
          $title_to_p_and_c_key =
            array_search
            ($alert_map_event_lc,
             $this->_private_readonly_alert_title_to_priority_and_colors);
          if ($title_to_p_and_c_key !== false) {
            $alert_map_color =
              $this->_private_readonly_alert_title_to_priority_and_colors
              [$title_to_p_and_c_key + 2];
          }
          if (isset($alert_map_color)) {
            $alerts_map['keys'][$j]['event'] = $parsed_alerts[$i]['event'];
            $alerts_map['keys'][$j]['color'] = $alert_map_color;
            $j++;
            if ($didone === false) {
              $didone = true;
              $alerts_map['url'] =
                str_replace('FORECAST_OFFICE',
                            strtolower
                            ($this->_client_parameters
                             [WEATHER_PARSER_CP_NWS_FORECAST_OFFICE]),
                            $this->_private_readonly_alerts_map_url);
            }
          }
        }
      }
    }

    $forecast_parameters = array();
    $forecast_parameters[WEATHER_PARSER_CP_PLACENAME] =
      $this->_client_parameters[WEATHER_PARSER_CP_PLACENAME];
    $forecast_parameters[WEATHER_PARSER_CP_LATITUDE] =
      $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE];
    $forecast_parameters[WEATHER_PARSER_CP_LONGITUDE] =
      $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE];
    $forecast_parameters[WEATHER_PARSER_CP_NWS_ZONE] =
      $this->_client_parameters[WEATHER_PARSER_CP_NWS_ZONE];
    $forecast_parameters[WEATHER_PARSER_CP_NWS_COUNTY] =
      $this->_client_parameters[WEATHER_PARSER_CP_NWS_COUNTY];
    $forecast_parameters[WEATHER_PARSER_CP_NWS_FORECAST_OFFICE] =
      $this->_client_parameters[WEATHER_PARSER_CP_NWS_FORECAST_OFFICE];
    $forecast_parameters[WEATHER_PARSER_CP_TIMEZONE] =
      $this->_client_parameters[WEATHER_PARSER_CP_TIMEZONE];
    $forecast_parameters[WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS] =
      $this->_client_parameters[WEATHER_PARSER_CP_AVOID_PERCENTAGE_ICONS];
    $forecast_parameters[WEATHER_PARSER_CP_DEVELOPMENT_MODE] =
      $this->_client_parameters[WEATHER_PARSER_CP_DEVELOPMENT_MODE];
    $forecast_parameters[WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS] =
      $this->_client_parameters[WEATHER_PARSER_CP_SHOW_DUPLICATE_ALERTS];
    $forecast_parameters[WEATHER_PARSER_CP_FORECAST_SOURCE] =
      $this->_client_parameters[WEATHER_PARSER_CP_FORECAST_SOURCE];
    $forecast_parameters[WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE] =
      $this->_client_parameters[WEATHER_PARSER_CP_ALERT_ZONE_TEST_FILE];
    $forecast_parameters[WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE] =
      $this->_client_parameters[WEATHER_PARSER_CP_ALERT_COUNTY_TEST_FILE];

    $forecast_information = array();
    $forecast_information['generated-on'] =
      $forecasts['summary']['dwml']['#']['head'][0]['#']['product'][0]['#']
        ['creation-date'][0]['#'];
    $forecast_information['programmer-disclaimer-text'] =
      'The user and/or reader assumes the entire risk related to its use of ' .
      'this data. This code is providing this data "as is," and the author ' .
      'of the code, or the hoster of this script, disclaims any and all ' .
      'warranties, whether express or implied, including (without ' .
      'limitation) any implied warranties of merchantability or fitness for ' .
      'a particular purpose. In no event will the programmer of this code, ' .
      'or the owners or staff of the website hosting it, be liable to you ' .
      'or to any third or other party for any direct, indirect, incidental, ' .
      'consequential, special or exemplary damages or lost profit resulting ' .
      'from any use or misuse of this data. This service is provided "as ' .
      'is"- as a hobby in programming; it could very well contain partially ' .
      'and/or completely inaccurate data. You must view and treat the ' .
      'outputs of this software package as the output of an amateur ' .
      'hobbyist, and not base any decisions on them.';
    $forecast_information['nws-disclaimer-url'] =
      $forecasts['summary']['dwml']['#']['head']
      [0]['#']['source'][0]['#']['disclaimer'][0]['#'];
    $forecast_information['alert-feed-source-info'] =
      $this->_private_readonly_alert_source_title;
    $forecast_information['forecast-feed-source-info'] =
      $this->_private_readonly_forecast_source_title
      [$this->_forecast_source];
    $forecast_information['nws-more-info-url'] =
      "https://forecast.weather.gov/MapClick.php?lat=" .
      $this->_client_parameters[WEATHER_PARSER_CP_LATITUDE] .
      "&lon=" .
      $this->_client_parameters[WEATHER_PARSER_CP_LONGITUDE] .
      "&FcstType=textr";

    // No longer needed. Free memory
    $forecasts = null;

    $this->_clean_cache(false);

    $this->_dump_timing_information();

    return array('result' => 'OK',
                 'error_type' => '',
                 'forecast-parameters' => $forecast_parameters,
                 'forecast-information' => $forecast_information,
                 'forecast-details' => $parsed_forecast,
                 'timing-information' => $this->_timing_information,
                 'alerts' => $parsed_alerts,
                 'alerts-map' => $alerts_map);
  }

  // _obtain_forecast
  //
  // Obtain an array containing details of NWS forecast, along with any alerts
  // that may exist for the location specified.
  //
  public function obtain_forecast ($parameters) {

    global $traverse_array;

    $this->_obtain_forecast_initialize();

    $result = $this->_obtain_forecast_work($parameters);

    if ($result['result'] != 'OK') {
      if ((isset($result['error_type'])) ||
          ($result['error_type'] != 'parameter parsing')) {
        print '<span style="text-align:initial;"><xmp><br>execution error! ' .
          "Note this output, then the nature of the error in the\n";
        print '\'WHOOPS\' section given below<br><br>';
        print 'Client Parameters = ';
        print print_r($this->_client_parameters, true);
        print '</xmp></span>';
        $this->_clean_cache(true);
      }
      if (isset($result['error-print-out'])) {
        print '<BR><span style="text-align:initial;"><xmp>FIF array: ';
        print print_r($this->_forecast_information_files, true);
        print "<BR></xmp></span>";
        $epo = $result['error-print-out'];
        if ($epo != '') {
          print '<span style="text-align:initial;"><xmp><br>';
          if (!empty($this->_forecast_information_files[$epo])) {
            $filename = $this->_forecast_information_files[$epo];
            if ($filename == "") {
              print "ERROR PRINT OUT REQUESTED NOT SET!  [" . $epo . "]<br>\n";
            } else {
              if (!file_exists($filename)) {
                print "ERROR PRINT OUT REQUESTED DOESN'T EXIST! YOU MAY " .
                  "NEED TO SET BOTH THE '" .
                  WEATER_PARSER_CP_DEVELOPMENT_MODE .
                  "' AND '" . WEATHER_PARSER_CP_LOGS .
                  "'PARAMETERS TO TRUE AND RUN YOUR QUERY AGAIN. ERROR: [" .
                  $epo . "]<br>\n";
              } else if (is_file($filename) === true) {
                if (is_readable($filename)) {
                  print "<h4>Contents of file: [" . $filename . "]</h4><BR>";
                  print "<BR><XMP>";
                  print file_get_contents($filename);
                  print "</XMP><BR>";
                } else {
                  print "ERROR PRINT OUT REQUESTED ISN'T READABLE!  [" . $epo .
                    "]";
                }
              } else {
                print "ERROR PRINT OUT REQUESTED ISN'T A FILE!  [" . $epo .
                  "]";
              }
            }
          } else {
            print "CANNOT FIND INFORMATION FOR ERROR PRINT OUT [" . $epo . "]";
          }
          print "<br></xmp></span>\n";
        }
      }
    }

    $this->_log_item('returned-parameters', 'full-list',
                     print_r($result, TRUE));
    return $result;
  }
}

// ======================== OPEN SOURCE INFORMATION ===========================

// ------------------------------- INTRODUCTION -------------------------------
//
// What follows is a list of this software package's uses of open source,
// giving the credits, thank-yous, and attributions that are best understood
// to be required or appropriate for this software package's leveraging of
// others' software. If any credits, thank-yous or attributions are missing,
// please contact the author of this software package at the email address
// give at the top of this script, so the omission can be rectified.
//

// --------------------------------- XMLIZE -------------------------------
//
// This system utilizes the xmlize utility written by Hans Anderson.
//
// License terms:
//
// xmlize() is by Hans Anderson, me@hansanderson.com
//
// Ye Ole "Feel Free To Use it However" License [PHP, BSD, GPL]. some code in
// xml_depth is based on code written by other PHPers as well as one Perl
// script.  Poor programming practice and organization on my part is to blame
// for the credit these people aren't receiving. None of the code was
// copyrighted, though.
//

// -------------------------- NUSOAP -------------------------------
//
// This system utilizes the NuSOAP package for reading SOAP data feeds,
// licensed under GPL v2.1 and later, by NuSphere Corporation.
//
// License terms:
//
// NuSOAP - Web Services Toolkit for PHP
//
// Copyright (c) 2002 NuSphere Corporation
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by the
// Free Software Foundation; either version 2.1 of the License, or (at your
// option) any later version.
//
// This library 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 Lesser General Public
// License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation,
// Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// The NuSOAP project home is:
//
// http://sourceforge.net/projects/nusoap/
//
// The primary support for NuSOAP is the Help forum on the project home page.
//
// If you have any questions or comments, please email:
//
// Dietrich Ayala
// dietrich@ganx4.com
// http://dietrich.ganx4.com/nusoap
//
// NuSphere Corporation
// http://www.nusphere.com

// ------------------- THANK YOU TO JOHNANDJANE.ORG ----------------
//
// Thanks to JaneAndJohn.org for PHP-based RSS weather-feed code used in a
// prior iteration of this script.
//

// ----------------------- THANK YOU TO KEN TRUE ---------------------------
//
// Thanks to Ken True at saratoga-weather.org for pointers to good warning
// scripts, used in a prior iteration, and for being a great overall resource
// for scripting weather websites. Also, the idea to ignore certificate errors
// from the NOAA/NWS website comes from these scripts.
//


// ---------------- THANK YOU TO KEN TRUE AND MIKE CHALLIS ----------------
//
// Licensing for Ken True/Mike Challis script that offers the array mapping
// alert event to color for the background and a clever way to handle alert
// events that aren't in the table:
//
// "You are free to use and modify the code"
//
// This php code provided "as is", and Ken True, Mike Challis disclaims any and
// all warranties, whether express or implied, including (without limitation)
// any implied warranties of merchantability or fitness for a particular
// purpose.
//

// ----------- THANK YOU TO PXWEATHER BY JONATHAN M. ABBETT --------------
//
// The idea to use and how to properly invoke the xmlize library to extract
// information from the NWS comes from PXWeather.
//
// pxweather.abbett.org (PXWeather) by Jonathan M. Abbett.
//

// --------------- THANK YOU TO TOM AT CARTERLAKE.ORG --------------------
//
// Thanks to Tom at carterlake.org for a script that caused me to want to learn
// how to obtain the raw data of a National Weather Service forecast, that
// triggered my building my own set of scripts. He has also created another
// great overall resource for scripting weather websites.
//

// ------- THANK YOU TO RICK "CURLY" AT THE WXFORUM.NET FORUM ------------
//
// The URL used to get the alerts, and the idea to use cURL to get them (as
// well as using cURL to get any other JSON/XML data), comes from the "NWS
// Alerts" PHP package by Rick "Curly" at the wxforum.net web site.

// ----- THANK YOU TO THE UNITED STATES NATIONAL WEATHER SERVICE (NWS) ------
//
// The raw data on which the forecasts are built, and the icons for these
// forecasts, come from the United States' National Weather Service.
//

?>

forecast-to-html.inc.php

<?php

//
// FORECAST_TO_HTML
// ----------------
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder.
//
// This script takes the forecast output and converts it to an HTML format that
// the author of this script finds pleasing. This is something that can be used
// as an 'example' for anyone else who wants to generate different outputs than
// this one.
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

ini_set('display_errors', 'On');

define('WP_OUR_DATETIME_FORMAT', "D, d M g:i a");

error_reporting(E_ALL | E_STRICT);

// print_line
//
// Print out the string with a suffixed newline character.
//
function print_line ($str) {

  print $str . "\n";
}

function alert_text_color_choice ($red, $green, $blue) {

  // For the sRGB/ITU-R BT.709 color space
  $relative_luminence =
    (0.2126 * ($red / 255.0)) + (0.7152 * ($green / 255.0)) +
    (0.0722 * ($blue / 255.0));
  if ($relative_luminence >= 0.5) {
    return 0x000000;
  } else {
    return 0xffffff;
  }
}

function give_local_time ($timestr, $format, $tz) {

  $utc_time = new DateTime($timestr);
  $local_time = clone $utc_time;
  $local_time->setTimezone(new DateTimeZone($tz));
  $abbreviation_tz = $local_time->format('T');
  $tz_name_wanted = $abbreviation_tz;
  $filter_result = filter_var($abbreviation_tz, FILTER_VALIDATE_REGEXP,
                              array('options' =>
                                    array('regexp' =>
                                          '/^[\+\-][0-9]+$/')));;
  if ($filter_result !== false) {
    $tz_name_wanted = $local_time->format('e');
  }
  return $local_time->format($format) . " (" . $tz_name_wanted . ")";
}        


// forecast_to_html
//
// Collect the current foreast and print it out. The parameters to pass to
// the forecast weather parser are given as $inputparams
//
function forecast_to_html ($inputparams) {

  // We do not want to print the array of weather information; this is
  // a debugging tool.
  $print_array = false;

  // Collect the forecast.

  // First, grab an instance of the weather_parser object. We invoke methods in
  // this to obtain the forecast.
  $weather_parser = New WeatherParser();

  // Obtain our weather forecast.
  $forecast_result =
    $weather_parser->obtain_forecast($inputparams);

  // If there was an error, print it out and stop.
  if ($forecast_result['result'] != 'OK') {
    print '<span style="text-align:initial;">';
    print '<br><pre>WHOOPS: ';
    print print_r($forecast_result, true);
    print '</pre><br><pre>PARAMS: ';
    print print_r($inputparams, true);
    print '</pre><br></span>';
    return;
  }

  // No error. The forecast is in reasonable shape. Extract some portions of
  // it that we want, and then dump the larger array.
  $forecast_parameters = $forecast_result['forecast-parameters'];
  $forecast_information = $forecast_result['forecast-information'];
  $forecast_details = $forecast_result['forecast-details'];
  $timing_information = $forecast_result['timing-information'];
  $alerts = $forecast_result['alerts'];
  $alerts_map = $forecast_result['alerts-map'];
  
  // No longer needed - dump it.
  $forecast_result = null;

  $development_mode =
    ((!empty($forecast_parameters['alert-zone-test-file'])) ||
     (!empty($forecast_parameters['alert-county-test-file'])) ||
     (!empty($forecast_parameters['development-mode']))) ? (true) : (false);

  // We obtain information about the browser via browscap. If it is not there, 
  // we assume a mobile device is in use.
  //
  // If we are invoked from the command line, we determine if we are in a
  // browser by looking at the first parameter.
  $is_mobile = true;
  if (PHP_SAPI == 'cli') {
    global $argc;
    global $argv;
    if ($argc > 1) {
      for ($i = 1; $i < $argc; $i++) {
        $modeoption = strtolower($argv[$i]);
        if ($modeoption == '--desktopmode') {
          $is_mobile = false;
        } else if ($modeoption == '--mobilemode') {
          $is_mobile = true;
        }
      }
    }
  } else {
    // In a browser
    $bcap = get_cfg_var('browscap');
    if ($bcap !== false) {
      if (is_readable($bcap) === true) {
        $browser = get_browser(null, true);
        if ($browser !== false) {
          if (!empty($browser['ismobiledevice'])) {
            if($browser['ismobiledevice']) { 
              $is_mobile = true;
            } else {
              $is_mobile = false;
            }
          } else {
            $is_mobile = false;
          }
        }
      }
    }
  }

  if ($development_mode === true) {
    print_line('<table><tr><td style="border:10px solid red;">' .
               '<br><h3 style="color:#FF0000;text-align:center;">THIS ' .
               'WEATHER WEBSITE IS BEING DEBUGGED! NONE OF THE INFORMATION ' .
               'DISPLAYED HERE IS TO BE TRUSTED AT THIS TIME!</h3></td></tr>' .
               '</table>');
  }

  // Get a time structure in our time zone.
  // Initialize array to hold constant parameters
  try {
    $dateTimeZoneUs =
      new DateTimeZone($forecast_parameters['timezone']);
  } catch(Exception $e) {
    return array("result" => "ERROR 2",
                 "detail" =>
                 "Unable to create DateTimeZone element for timezone [" .
                 $forecast_parameters['timezone'] . "]");
  }

  // Print out any alerts first. The quick way to check is to see
  // if there is a first 'event' entry.

  // We like to sort our alerts by priority. Some priorities are not known.
  // These have priority '0' so will be put at the top of the list (which is
  // good.)

  if (!empty($alerts[0]['event'])) {
    $priority_keys = array_column($alerts, 'priority');
    array_multisort($priority_keys, SORT_ASC, $alerts);
    $priority_keys = null;
    $alert_types =
      array(array('singular' => 'evacuation',
                  'plural' => 'evacuations',
                  'num' => 0),
            array('singular' => 'emergency',
                  'plural' => 'emergencies',
                  'num' => 0),
            array('singular' => 'warning',
                  'plural' => 'warnings',
                  'num' => 0),
            array('singular' => 'danger',
                  'plural' => 'dangers',
                  'num' => 0),
            array('singular' => 'outage',
                  'plural' => 'outages',
                  'num' => 0),
            array('singular' => 'watch',
                  'plural' => 'watches',
                  'num' => 0),
            array('singular' => 'hazard',
                  'plural' => 'hazards',
                  'num' => 0),
            array('singular' => 'alert',
                  'plural' => 'alerts',
                  'num' => 0),
            array('singular' => 'advisory',
                  'plural' => 'advisories',
                  'num' => 0),
            array('singular' => 'outlook',
                  'plural' => 'outlooks',
                  'num' => 0),
            array('singular' => 'message',
                  'plural' => 'messages',
                  'num' => 0),
            array('singular' => 'statement',
                  'plural' => 'statements',
                  'num' => 0),
            array('singular' => 'forecast',
                  'plural' => 'forecasts',
                  'num' => 0),
            array('singular' => 'test',
                  'plural' => 'tests',
                  'num' => 0),
            array('singular' => 'note',
                  'plural' => 'notes',
                  'num' => 0));
               
    // We go through this to determine what kind of events we have. All of this
    // work just to make the first line of the advisory output 'pretty'!

    $alert_total = 0;
    $different_alert_types = 0;

    for ($j = 0; (!empty($alerts[$j]['event'])); $j++) {
      $found_prior_use = false;
      for ($k = 0; ($k < $j); $k++) {
        if (!empty($alerts[$k]['event'])) {
          if (!strcmp(trim($alerts[$k]['id']),
                      trim($alerts[$j]['id']))) {
            $found_prior_use = true;
            break;
          }
        }
      }

      if ($found_prior_use == false) {
        $alert_total++;
        $this_event = strtolower($alerts[$j]['event']);
        $found_it = false;
        foreach ($alert_types as &$alert_type) {
          if (strpos($this_event, $alert_type['singular']) !== false) {
            if ($alert_type['num'] == 0) {
              $different_alert_types++;
            }
            $alert_type['num']++;
            $found_it = true;
            break;
          }
        }
        // ALWAYS follow a foreach ($a as $b) with an 'unset($b)'!
        unset($alert_type);

        if ($found_it === false) {
          $atype = null;
          foreach ($alert_types as &$atype) {
            if ($atype['singular'] == 'note') {
              if ($atype['num'] == 0) {
                $different_alert_types++;
              }
              $atype['num']++;
              break;
            }
          }
        }
        // ALWAYS follow a foreach ($a as $b) with an unset($b)!
        unset($atype);
      }
    }

    $type_text = "";
    $alert_types_printed = 0;

    // We use a serial comma for 3 or more items.
    //
    //   See: https://en.wikipedia.org/wiki/Serial_comma
    //
    // We know the number of alert types we have in '$different_alert_types'
    //
    // 1 alert type:          Alerttype1
    // 2 alert types:         Alerttype1 and Alerttype2
    // 3 or more alert types: Alerttype1, ..., AlertTypeN-1, and AlertTypeN
    //
    // The algorithm to do this is summarized by keeping track of how many
    // alert types have been printed in "$alert_types_printed."
    //
    foreach ($alert_types as $alert_type) {
      $num = 0;
      if (!empty($alert_type['num'])) {
        $num = $alert_type['num'];
      }
      if ($num > 0) {
        $alert_types_printed++;
        $alert_name = $alert_type['singular'];
        if ($num > 1) {
          $alert_name = $alert_type['plural'];
        }
        $alert_name = ucfirst($alert_name);
        if ($alert_types_printed <= 1) {
          $type_text .= $alert_name;
        } else if ($alert_types_printed < $different_alert_types) {
          $type_text .= ", " . $alert_name;
        } else {
          if ($alert_types_printed > 2) {
            $type_text .= ",";
          }
          $type_text .= " and " . $alert_name;
        }
      }
    }

    print_line('<br><h2 style="color:#FF0000;padding-bottom=10px;">Active ' .
               $type_text . '</h2>');

    print_line('<h5>To obtain more information about an alert, ' .
               'click on the box containing the alert name</h5>');

    print_line("<style>");
    print_line("details > summary { list-style: none;}");
    // chrome, edge, and firefox
    print_line("details > summary::marker,");
    // safari
    print_line("details > summary::-webkit-details-marker {display: none;}");
    print_line("</style>");

    $top_alert_box_padding = "3";
    $bottom_alert_box_padding = "5";
    if ($is_mobile) {
      $top_alert_box_padding = "8";
      $bottom_alert_box_padding = "10";
    }

    // Print out each alert.
    for ($j = 0; (!empty($alerts[$j]['event'])); $j++) {
      $found_prior_use = false;
      for ($k = 0; ($k < $j); $k++) {
        if (!empty($alerts[$k]['event'])) {
          if (!strcmp(trim($alerts[$k]['id']),
                      trim($alerts[$j]['id']))) {
            $found_prior_use = true;
            break;
          }
        }
      }
      if ($found_prior_use === false) {
        $title_and_alt_text = $alerts[$j]['title'];
        $phenominoncolor = $alerts[$j]['phenominon-color'];
        $txtcolor = $alerts[$j]['textcolor'];
        $bordercolor = $alerts[$j]['alert-border-color'];
        print_line('<details style="margin-top:30px;margin-bottom:30px">');
        print_line('<summary><b>');
        print_line('<span style="padding-left:10px;padding-right:10px' .
                   ';background-color:#' . sprintf("%06X", $phenominoncolor) .
                   ';padding-top:' . $top_alert_box_padding . 'px'.
                   ';padding-bottom:' . $bottom_alert_box_padding . 'px' .
                   ';border-width:7px;border-style:solid;border-color:#' .
                   sprintf("%06X", $bordercolor) . ';color:#' .
                   sprintf("%06X", $txtcolor) . ';cursor: zoom-in;"><u>' .
                   $title_and_alt_text . '</u></span></b>');
        if (!empty($alerts[$j]['NWSheadline'])) {
          print_line('<h4 style="margin-bottom:0px;">' .
                     $alerts[$j]['NWSheadline'] . '</h4>');
        }
        $redbg = (($phenominoncolor & 0xff0000) >> 16);
        $greenbg = (($phenominoncolor & 0xff00) >> 8);
        $bluebg = ($phenominoncolor & 0xff);

        // Now mix in the opacity
        $opacity = 0.10;
        $redbg =
          ceil(255.0 - $opacity * (255.0 - floatval($redbg)));
        $greenbg =
          ceil(255.0 - $opacity * (255.0 - floatval($greenbg)));
        $bluebg =
          ceil(255.0 - $opacity * (255.0 - floatval($bluebg)));
        $tablebgcolor = ($redbg << 16) + ($greenbg << 8) + $bluebg;
        $tabletxtcolor = alert_text_color_choice($redbg, $greenbg, $bluebg);
        // Now we take that, and compute the proper text color...
        print_line('</summary>');
        print_line('&nbsp;');
        print_line('<table style="border-style:solid' .
                   ';border-collapse:collapse' .
                   ';background-color:#' . sprintf('%06X', $tablebgcolor) .
                   ';border-color:#' . sprintf('%06X', $phenominoncolor) .
                   ';color:#' . sprintf('%06X', $tabletxtcolor) .
                   ';margin-top:0px;margin-bottom:0px;" align="center">');
        $our_tz = $forecast_parameters['timezone'];
        print_line('<tr valign="center" align="center" ' .
                   'style="height:10px;"><td colspan=3>');
        print_line('<b><span style="color:#3d6c87">To ' .
                   'collapse this detailed alert information, click on the ' .
                   'box above containing the alert name</span></b>');
        print_line('</td></tr>');

        print_line('<tr valign ="center" align="center">' .
                   '<td style="width:33%;border-top:2px solid back;">');
        print_line("<b>Severity</b><br>" . $alerts[$j]['severity']);
        print_line('</td>');
        print_line('<td style="width:33%;">');
        print_line("<b>Urgency</b><br>" . $alerts[$j]['urgency']);
        print_line('</td>');
        print_line('<td style="width:33%;">');
        print_line("<b>Certainty</b><br>" . $alerts[$j]['certainty'] .
                   "<BR>");
        print_line('</td></tr>');

        print_line('<tr valign ="center" align="center">');
        $effective_datetime = give_local_time($alerts[$j]['effective'],
                                              WP_OUR_DATETIME_FORMAT, $our_tz);
        print_line('<td style="width:33%;border-left:2px solid black' .
                   ';border-top:2px solid black' .
                   ';border-bottom:2px solid black">');
        print_line("<b>Alert Effective</b><br>" . $effective_datetime);
        print_line('</td>');

        $expires_datetime = give_local_time($alerts[$j]['expires'],
                                            WP_OUR_DATETIME_FORMAT, $our_tz);
        print_line('<td style="width:33%;border-right:2px solid black' .
                   ';border-top:2px solid black' .
                   ';border-bottom:2px solid black">');
        print_line("<b>Alert Expires</b><br>" . $expires_datetime);
        print_line('</td>');

        $sent_datetime = give_local_time($alerts[$j]['sent'],
                                         WP_OUR_DATETIME_FORMAT, $our_tz);
        print_line('<td style="width:33%;border-top:2px solid black" ' .
                   'rowspan=2>');
        print_line("<b>Sent by NWS On</b><br>" . $sent_datetime);
        print_line('</td></tr>');
        
        print_line('<tr valign ="center"');
        print_line('align="center"');
        print_line('style="border-bottom:2px solid black;">');
        $onset_datetime =
          give_local_time
          ($alerts[$j]['onset'], WP_OUR_DATETIME_FORMAT, $our_tz);
        print_line('<td style="width:33%;border-left:2px solid black' .
                   ';border-top:2px solid black' .
                   ';border-bottom:2px solid black">');
        print_line("<b>Event Onset</b><br>" . $onset_datetime);
        print_line('</td>');

        $ends_datetime =
          give_local_time
          ($alerts[$j]['ends'], WP_OUR_DATETIME_FORMAT, $our_tz);
        print_line('<td style="width:33%;border-right:2px solid black' .
                   ';border-top:2px solid black' .
                   ';border-bottom:2px solid black">');
        print_line("<b>Event Ends</b><br>" . $ends_datetime);
        print_line('</td></tr>');

        print_line('<tr valign ="center" align="center" ' .
                   'style="height:30px;"><td colspan=3>');
        print_line('<b><i>Description</i></b>');
        print_line('</td></tr>');

        print_line('<tr valign ="top" align="left"><td colspan=3>');
        print_line('<div style=' .
                   '"text-align:initial;margin-left:30px;margin-right:30px">');

        $desc = $alerts[$j]['description'];

        // This is an incredibly ugly kludge. My apologies.
        $desc = str_replace("\n\n* ", '<BR><P style="text-indent:-15px;">* ',
                            $desc);
        if (substr($desc, 0, 2) == "* ") {
          $desc = '<P style="text-indent:-15px;">' . $desc;
        }
        $desc = str_replace("\n\n", "<BR><P>", $desc);
        print_line('<pre style="white-space:normal;">' .
                   $desc . "</pre><BR></div>");
        print_line('</td></tr>');

        print_line('<tr valign="bottom" align="center" ' .
                   'style="margin-top:5px;"><td colspan=3 ' .
                   'style="height:10px;' .
                   'border-top:2px solid black;color:#3d6c87;">');
        print_line("<b>According to the National Weather Service, your " .
                   "response to this alert should be to ");
        if (!empty($alerts[$j]['response-detail'])) {
          print_line("<i>" . $alerts[$j]['response-detail'] . "</i>.");
        } else {
          print_line("<i>" . $alerts[$j]['response'] . "</i>.");
        }
        print_line('</b></td></tr>');

        print_line('<tr valign ="top" align="center">' .
                   '<td colspan=3>');
        print_line("<br><b><i>Instruction</i></b>");
        print_line('</b></td></tr>');

        print_line('<tr valign ="top" align="left"><td colspan=3>');
        print_line('<div style=' .
                   '"text-align:initial;margin-left:30px;margin-right:30px">');
        $inst = $alerts[$j]['instruction'];
        $inst = str_replace("\n\n", "<BR><P>", $inst);
        print_line('<pre style="white-space:normal;">' . $inst .
                   "</pre><BR></div>");
        print_line('</td></tr>');

        print_line('</table>');
        print_line('</details>');
      }
    }

    $nws_fo_url = "https://www.weather.gov/" .
      $forecast_parameters['nws-forecast-office'];
      
    $printed_map_table = false;

    if (!empty($alerts_map['url'])) {
      // Compute number of rows
      $display_rows = 15;
      $rowcount = count($alerts_map['keys']);
      if ($rowcount > 0) {
        print_line('<h5>National Weather Service alerts map for your ' .
                   'zone/county. The color key table only shows the keys ' .
                   'for alerts that are active in your specified county or ' .
                   'zone and only shows one alert for each spot on the map, ' .
                   'even when more than one alert may be active at that ' .
                   'spot. According to the NWS, when there are multiple ' .
                   "alerts for a single spot, 'only the most significant " .
                   "threat to life or property is displayed on the map.'" .
                   "</h5>");
        $map_url = $alerts_map['url'];
        $printed_map_table = true;
        print_line('<table align="center" ' .
                   'style="border: 1px solid black;' .
                   'width=100%;' .
                   'empty-cells:show;' .
                   'border-collapse: collapse;">');
                   
        $dim_factor = $display_rows * 10;
        print_line('<tr><td style="width:50%'. $dim_factor . ';height=' .
                   $dim_factor . ';border: 1px solid black;" rowspan=' .
                   strval($display_rows) . '>');
        print_line('<A HREF=' . $nws_fo_url . '><IMG SRC="' . $map_url .
                   '"></A>');
        print_line('</td>');
        // How many rows of alerts do we allow for
        // How many rows left over?
        $leftoverrows = max(($display_rows - $rowcount), 0);

        $border_params = '';
                           
        $first_data_row = floor($leftoverrows /2);

        // Distribute empty rows evenly above and below the ones we list
     
        for ($i = 0; ($i < $display_rows); $i++) {
          // The first row's <TR> was already given
          if ($i > 0) {
            print_line('<tr>');
          }
          // Empty row above or below the data rows
          // Example: First data row 2, and 5 items. Fill in 2, 3, 4, 5, 6
          //  (($i < 2) || ($i >= (2 + 5))
          if (($i < $first_data_row) ||
              ($i >= ($first_data_row + $rowcount))) {
            print_line('<td colspan=4 style="width:50%;height:10;' .
                       $border_params . '">&nbsp;</td>');
          } else {
            // This row has data!
            print_line('<td style="width:5%;' . $border_params .
                       '"></td>' .
                       '<td style="width:10%;" valign="center">' .
                       '<div style="display:inline-block; width:20px;' .
                       'height:12px; background-color:#' .
                       sprintf('%06X',
                               $alerts_map['keys'][$i - $first_data_row]
                               ['color']) .
                       ';"></div>' .
                       // 'border-width:7px;border-style:solid;' .
                       //                       'border-color:white;' .
                       //'background-color:#' .
                       //'empty-cells:show;' .
                       //                       'border: 5px solid white;' .
                       '</td>' .
                       '<td style="width:5%;' . $border_params . '"></td>' .
                       '<td valign="center" align="left" ' .
                       'style="width:30%' .
                       $border_params . '">' .
                       $alerts_map['keys'][$i - $first_data_row]['event'] .
                       '</td>');
          }
          print_line('</tr>');
        }
        print_line('</table>');
      }
    }
                 
    if ($printed_map_table === false) {
      print_line('<hr align="center" width="10%">');
    }
    print_line('<h5>You may also obtain far more information about ' .
               'th' . (($alert_total == 1) ? 'is' : 'ese') . ' weather alert' .
               (($alert_total == 1) ? '' : 's') . ' by clicking on this ' .
               'link to <BR><A HREF="' . $nws_fo_url . '">the National ' .
               "Weather Service Forecast Office's report</A> for your " .
               'forecast region.</h5>');

    print_line('<h5 style="padding-top=0px;">(Alerts are from the ' .
               $forecast_information['alert-feed-source-info'] . ')</h5>');
    print_line('<br>');
    print_line('<hr>');
  }
  // Alerts handled. Now, print out the forecast.
  print_line('<table style="border: 0" align="center">');
  print_line(' <tr align="center">');
  print_line('  <td>');
  print_line('  <h2>Weather Forecast</h2>');
  print_line('  </td></tr>');
  print_line(' <tr align="center">');
  print_line('  <td>');
  print_line('   <b><span style="color: #FF0000;">');
  print_line('Forecast Created by Experimental Software Written By an ' .
             'Amateur With No Guarantee or Warranty or Promise of Accuracy ' .
             'Provided or Implied! Use at Your Own Risk! You Assume All ' .
             'Risk and Liability If You Use This Information in Any Way, ' .
             'For Any Purpose Whatsoever! See the Disclaimer Given Above!');
  print_line('</span></b>');
  print_line('  </td>');
  print_line(' </tr>');
  print_line(' <tr align="center"><td>');
  $latval = floatval($forecast_parameters['latitude']);
  $longval = floatval($forecast_parameters['longitude']);

  print_line(' <tr align="center">');
  print_line('  <td>');
  print_line('   <span style="color:#00bb00;"><i>raw forecast data ' .
             'generated on</i><br>');
  $utc_generation_time =
    DateTime::createFromFormat(DATE_W3C,
                               $forecast_information['generated-on']);
  $local_generation_time = clone $utc_generation_time;
  $local_generation_time->setTimezone(new DateTimeZone($forecast_parameters
                                                       ['timezone']));
  $abbreviation_tz = $local_generation_time->format('T');
  $tz_name_wanted = $abbreviation_tz;
  $filter_result = filter_var($abbreviation_tz, FILTER_VALIDATE_REGEXP,
                              array('options' =>
                                    array('regexp' => '/^[\+\-][0-9]+$/')));;
  if ($filter_result !== false) {
    $tz_name_wanted = $local_generation_time->format('e');
  }
  print_line($local_generation_time->format(DATE_RFC1123) . " (" .
             $tz_name_wanted . ")</span>");
  print_line('  </td>');
  print_line(' </tr>');
  print_line(' <tr align="center">');
  print_line('  <td>');
  print_line('   <br><A HREF=');
  print_line('"' . $forecast_information['nws-more-info-url'] . '">');
  print_line("Click here for an official National Weather Service " .
             "forecast</A>!");
  print_line('<A HREF="' . $forecast_information['nws-disclaimer-url'] . '"' .
             ">NWS Disclaimer</A><br></center>");
  print_line('  </td>');
  print_line(' </tr>');
  print_line(' <tr>');
  print_line('  <td align="center">&nbsp;');
  print_line('   <table border="0" cellpadding="0" ' .
             'cellspacing="0">');
  print_line('  <tr valign ="top" align="center">');

  // We only care about the first few entries for our summary row of forecast
  // tiles. 7 for mobile devices, 8 for desktop.
  for ($i = 0; $i < ($is_mobile ? 7 : 8); $i++) {

    // If not an array, that means there is no detail for this time period, so
    // skip it. If not an array, skip this entry
    if ((!array_key_exists($i, $forecast_details)) ||
        (!is_array($forecast_details[$i]))) {
      continue;
    }

    print_line('   <td style="width: 5%;">');
    print_line('    <span style="font-size: 8pt;"><b>');

    $explode_period_name = explode(' ', $forecast_details[$i]['period-name']);

    // If the period name has more than two words, just print out the first
    // two. Readers of the page will still figure it out.
    if (count($explode_period_name) < 2) {
      print_line('&nbsp;<br>' . $explode_period_name[0]);
    } else {
      print_line($explode_period_name[0] . '<br>' . $explode_period_name[1]);
    }
    // Sometimes there is no conditions text, in which case we revert to
    // printing out the weather summary.
    $title_and_alt_text = $forecast_details[$i]['conditions-text'];
    if (empty($forecast_details[$i]['conditions-text'])) {
      $title_and_alt_text = $forecast_details[$i]['weather-summary'];
    }
    print_line('<br></b></span>');
    print_line('<img src="' . $forecast_details[$i]['conditions-icon'] .
              '" width="55" height="58" title="' . $title_and_alt_text .
              '" alt="' . $title_and_alt_text . '"><br>');

    // Print out the temperature - high or low. Typically, there will be only
    // one. If both, prefer high.
    if (!empty($forecast_details[$i]['high-temperature'])) {
      print_line('Hi <span style="color: #FF0000;">' .
                $forecast_details[$i]['high-temperature']);
    } else {
      print_line('Lo <span style="color: #0033CC;">' .
                $forecast_details[$i]['low-temperature']);
    }
    print_line(' &deg;' .
              strtoupper(substr($forecast_details[$i]
                                ['temperature-units'], 0, 1)) .
              '</span><br>');

    // Precipitation, if any
    print_line('&nbsp;Precp&nbsp;<span style="color: #00AA00;">' .
              $forecast_details[$i]['probability-of-precipitation-percent'] .
              '%</b>&nbsp;</span><br>');
    print_line('    <span style="font-size: 8pt;">');
    print_line(str_replace(' ', '<br>',
                          $forecast_details[$i]['weather-summary']));
    print_line('</span>');
    print_line('<br></td>');
  }

  print_line('</tr></table></td></tr></table>');
  print_line('<p>');

  // We've finished printing out the forecast tiles. Now give the details.
  $did_row = false;
  // Print up to 14 of these - a week ahead.
  for ($i = 0; $i < 14; $i++) {
    // If not an array, skip this entry
    if ((!array_key_exists($i, $forecast_details)) ||
        (!is_array($forecast_details[$i]))) {
      continue;
    }
    if ($did_row === false) {
      print_line('<table style="border: 0">');
      $did_row = true;
    }
    print_line(' <tr valign ="top" align="left">');
    print_line('  <td style="width: 20%;"><b>' .
              $forecast_details[$i]['period-name'] .
              '</b><br>&nbsp;<br></td>');
    print_line('  <td style="width: 80%;">' .
              $forecast_details[$i]['generated-forecast'] .
              '</td>');
    print_line(' </tr>');
  }
  if ($did_row !== false) {
    print_line('</table>');
  }
  print_line('<br><hr align="center" width="30%">');
  print_line('<table style="border: 0" align="center">');
  print_line(' <tr align="center"><td>');
  $experimental_info =
    '<i>The above Experimental Forecast used the ' .
    $forecast_information['forecast-feed-source-info'] . '<br> for ' .
    $forecast_parameters['placename'];
  $experimental_info .=
    "<br>("
    . abs($latval)
    . "&deg;"
    . (($latval > 0) ? 'N' : (($latval < 0) ? 'S' : ""))
    . ", "
    . abs($longval)
    . "&deg;"
    . (($longval > 0) ? 'E' : (($longval < 0) ? 'W' : ""))
    . ", "
    . ' NWS Zone '
    . $forecast_parameters['nws-zone'];
  if (strlen($forecast_parameters['nws-county']) > 0) {
    $experimental_info .= ', NWS County '
      . $forecast_parameters['nws-county'];
  }
  print_line($experimental_info . ')</i>');
  print_line('  </td>');
  print_line(' </tr>');
  print_line('</table><br><hr><br>');
}

?>

forecast.php

<?php

//
// FORECAST
// --------
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

ini_set('display_errors', 'On');
error_reporting(E_ALL | E_STRICT);

// auto refresh every 5 minutes
print '<meta http-equiv="refresh" content="300">';

require_once('weather-parser.inc.php');
require_once('forecast-to-html.inc.php');

// Call our function.
//
// 'nws-county' can be the empty string ('') if you only want alerts for your
// zone, as opposed to the entire county.
forecast_to_html
 (array('latitude' => 37.263,
        'longitude' => -122.003,
        'placename' => 'Saratoga, CA',
        'nws-zone' => 'CAZ513',
        'nws-county' => 'CAC085', 
        'nws-forecast-office' => 'MTR',
        'timezone' => 'America/Los_Angeles',
        'avoid-percentage-icons' => false,
        'ignore-cache' => false,
//        'development-mode' => true,
        'logs' => true,
        'forecast-source' => 'xml',
//        'alert-zone-test-file' => 'private_extensions/development/bigstorm-zone.txt',
//        'alert-county-test-file' => 'private_extensions/development/bigstorm-county.txt',
        'show-duplicate-alerts' => false));
?>

getweather.php

<?php

//
// GETWEATHER
// ----------
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder. This script provides an ability to get
// the forecast information via a URL.
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

// Example:
// https://westvi.com/weather/getweather.php?latitude=37.263&longitude=-122.003&placename=Saratoga,%20CA&nws-zone=CAZ513&nws-forecast-office=MTR&nws-county=CAC085&timezone=America%2FLos_Angeles

function our_error ($why) {

  print "<BR>Not processing request due to: " . $why . ".<BR>";
  die();
}

$url = parse_url($_SERVER["REQUEST_URI"]);
$url_params = [];
parse_str($url['query'], $url_params);

$defaults = [];
$defaults['latitude'] = '999.0';
$defaults['longitude'] = '999.0';
$defaults['placename'] = 'NOT SET';
$defaults['timezone'] = 'NOT SET';
$defaults['nws-zone'] = 'NOT SET';
$defaults['nws-county'] = 'NOT SET';
$defaults['nws-forecast-office'] = 'NOT SET';
$defaults['reset-cache'] = false;
$defaults['avoid-percentage-icons'] = false;
$defaults['show-duplicate-alerts'] = false;

$params_allowed_to_change = array('latitude', 'longitude', 'placename',
                                  'timezone', 'nws-zone', 'nws-county',
                                  'nws-forecast-office');

// Happens to be the same as 'params_allowed_to_change' right now, but it is
// possible at some future time that the two lists are different.
$params_must_be_given = array('latitude', 'longitude', 'placename',
                              'timezone', 'nws-zone', 'nws-county',
                              'nws-forecast-office');

// Lowercase all keyword names
$lckey_url_params = [];
foreach ($url_params as $keyname => $val) {
  $lckey_url_params[strtolower($keyname)] = $val;
}

// No params given so far
$url_params = [];

// Now, loop through and check for valid parameters complain if invalid seen
$bad_param = false;
foreach ($lckey_url_params as $keyname => $val) {
  if (!in_array($keyname, $params_allowed_to_change, true)) {
    print "Invalid parameter - [" . $keyname . "]<br>";
    $bad_param = true;
  } else {
    $array_location = array_search($keyname, $params_must_be_given, true);
    if ($array_location !== false) {
      unset($params_must_be_given[$array_location]);
    }
  }
}

if ($bad_param === true) {
  our_error("unknown parameter given");
}

// Complain if any must-be-given parameters are not given
if (!empty($params_must_be_given)) {
  $done_one = false;
  $text_output = "Missing these parameters that must be given:";
  foreach ($params_must_be_given as $val) {
    if ($done_one === true) {
      $text_output .= ",";
    } else {
      $done_one = true;
    }
    $text_output .= " " . $val;
  }
  print $text_output . ".";
}

$lckey_url_params['reset-cache'] =
  $lckey_url_params['avoid-percentage-icons'] =
  $lckey_url_params['show-duplicate-alerts'] = false;

require_once('private_extensions/weather_parser/weather-parser.inc.php');
require_once('private_extensions/weather_parser/collect-and-print-forecast.inc.php');

collect_and_print_forecast($lckey_url_params);

?>

coldwarn.php

<?php

// WHEN EDITING THIS FILE, MAKE THE EMAIL ADDRESSES AND TEXTING NUMBERS
// ANONYMOUS, AND SAVE A COPY IN
//
//  /home/weewx/weewx-data/public_html/private_extensions/extras
//

//
// COLDWARN
// --------
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder. This script simply looks at the coming
// night's forecast and sends email/text warnings if the forecast indicates the
// temperature may be approaching freezing. It is meant to be called as a daily
// 'cron' script.
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

chdir("/home/weewx/weewx-data/public_html");

require_once('private_extensions/weather_parser/weather-parser.inc.php');

  // Set to true for forcing email/texts even when its too warm to warn
  $debug_mode = false;
  $weather_parser = New WeatherParser();
  $forecast_result =
    $weather_parser->obtain_forecast
  (array('latitude' => 'NOT_SET',
         'longitude' => 'NOT_SET',
         'placename' => 'Los Angeles, CA',
         'nws-zone' => 'CAZ000',
         'nws-county' => 'CAC000',
         'nws-forecast-office' => 'MTR',
         'timezone' => 'America/Los_Angeles',
         'show-duplicate-alerts' => false));

  if ($forecast_result['result'] != 'OK') {
    mail('joelfran@westvi.com', 'Home Freeze Warning Script Failure!',
         $forecast_result['result']);
  } else {
    $forecast_details = $forecast_result['forecast-details'];
    // Find the first low temperature; use that.
    for ($i = 0; $i < 9; $i++) {
      if (is_array($forecast_details[$i]) &&
          isset($forecast_details[$i]['low-temperature'])) {
        $this_low = $forecast_details[$i]['low-temperature'];
        if (($this_low < 40) || ($debug_mode)) {
          $msg_to_send =
            'Low tonight forecast to be ' . $this_low .
            ', but our low is often less!';
          mail('4084386393@tmomail.net', 'FREEZE WARNING!', $msg_to_send);
          sleep(5);
          mail('4089102046@tmomail.net', 'FREEZE WARNING!', $msg_to_send);
          sleep(5);
          mail('joelfran@westvi.com', 'FREEZE WARNING!', $msg_to_send);
        }
        break;
      }
    }
  }
?>

getnwsinfo.php

<?php

$latitude = "0.000";
$longitude = "0.000"; // Use a negative value for 'west' longitudes.

//
// EDIT THIS SCRIPT'S VALUE FOR LATITUDE AND LONGITUDE, ABOVE, AND THEN RUN IT
// FROM A BROWSER.
//

//
// GETNWSINFO
// ----------
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder. This script simply computes parameters
// for calling obtain_forecast based on inputted latitude and longitude values.
// Edit the values for latitude and longitude, above, and run the script from a
// web browser.
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

define('GETNWSINFO_USER_AGENT_STRING',
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 " .
        "Firefox/67.0");

$curl_handle = curl_init();
curl_setopt($curl_handle,
            CURLOPT_USERAGENT, GETNWSINFO_USER_AGENT_STRING);
        
$points_url = "https://api.weather.gov/points/" . $latitude . "," . $longitude;
curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($curl_handle, CURLOPT_URL, $points_url);
curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($curl_handle, CURLOPT_TIMEOUT,        5);
curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($curl_handle);
if (($result === false) ||
    (preg_match('/Database Connection Issue/Ui', $result))) {
  echo "Failure: Curl errno = " . strval(curl_errno($curl_handle)) . ' [' .
    curl_error($curl_handle) . ']';
} else {
  $json_result = json_decode($result, true);
  $pieces = explode('/', $json_result['properties']['forecastZone']);
  $zoneinfo = $pieces[count($pieces) - 1];
  $pieces = explode('/', $json_result['properties']['county']);
  $countyinfo = $pieces[count($pieces) - 1];
  $pieces = explode('/', $json_result['properties']['forecastOffice']);
  $forecastofficeinfo = $pieces[count($pieces) - 1];
  echo "<br><br>Some NWS-specific parameters for calling forecast_to_html " .
    "or obtain_forecast:<br>";
  echo "'latitude' => '" . $latitude . "',<br>";
  echo "'longitude' => '" . $longitude . "',<br>";
  echo "'nws-zone' => '" . $zoneinfo . "',<br>";
  echo "'nws-county' => '" . $countyinfo . "',<br>";
  echo "'nws-forecast-office' => '" . $forecastofficeinfo . "',<br><br>";
}

alexa.php

<?php

//
// ALEXA
// -----
//
// Part of a package that is a NWS Weather Forecast and Alerts Data Extractor
// and Human-Readable Forecast Builder. This script takes forecast information
// and builds a series of text files that can be retrieved by an Alexa skill,
// to be read aloud to its user.
//
// Copyright (C) 2012-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

// ------------------------------- PARAMETERS --------------------------------

define('DEBUG', false);
define('DEBUG_ALERTS', false);
define('DEBUG_TEST_NO_FILE_WRITES', false);
define('REPORT_AIR_QUALITY', false);

define('LATITUDE', 99.999);
define('LONGITUDE', 99.999);
define('NWS_ZONE', 'CAZ999');
define('NWS_COUNTY', 'CAC999');
define('NWS_FORECAST_OFFICE', 'ZZZ');
define('CITY_NAME', 'Hometown');
define('STATE_NAME', 'CA');
define('TIMEZONE', 'America/Los_Angeles');
define('SHOW_DUPLICATE_ALERTS', false);

define('WEEWX_OUTPUT_ROOT',
       '/usr/share/httpd/htdocs/westvi.com/weather/');
define('PLACE_NAME', CITY_NAME . ', ' . STATE_NAME);

define('SCRIPTS_ROOT', WEEWX_OUTPUT_ROOT . 'private_extensions/');

define('ALEXA_FILES_ROOT', SCRIPTS_ROOT . 'alexa/');

require_once(SCRIPTS_ROOT .
             'aqi/us/epa/us-epa-realtime-single-sample-aqi.inc.php');
require_once(SCRIPTS_ROOT .
             'weather_parser/weather-parser.inc.php');

// --------------------- GLOBAL CONSTANTS INITIALIZATION ----------------------

define('NUMBER_STRINGS',
       array("--", "one", "two", "three", "four", "five", "six", "seven",
             "eight", "nine", "ten", "eleven", "twelve", "thirteen",
             "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
             "nineteen", "twenty", "twenty one", "twenty two", "twenty three",
             "twenty four", "twenty five", "twenty six", "twenty seven",
             "twenty eight", "twenty nine", "thirty", "thirty one",
             "thirty two", "thirty three", "thirty four", "thirty five",
             "thirty six", "thirty seven", "thirty eight", "thirty nine",
             "forty", "forty one", "forty two", "forty three", "forty four",
             "forty five", "forty six", "forty seven", "forty eight",
             "forty nine", "fifty", "fifty one", "fifty two", "fifty three",
             "fifty four", "fifty five", "fifty six", "fifty seven",
             "fifty eight", "fifty nine", "sixty", "sixty one", "sixty two",
             "sixty three", "sixty four", "sixty five", "sixty six",
             "sixty seven", "sixty eight", "sixty nine", "seventy",
             "seventy one", "seventy two", "seventy three", "seventy four",
             "seventy five", "seventy six", "seventy seven", "seventy eight",
             "seventy nine", "eighty", "eighty one", "eighty two",
             "eighty three", "eighty four", "eighty five", "eighty six",
             "eighty seven", "eighty eight", "eighty nine", "ninety",
             "ninety one", "ninety two", "ninety three", "ninety four",
             "ninety five", "ninety six", "ninety seven", "ninety eight",
             "ninety nine"));

define('MONTH_NAME_LIST',
       array("-", "january", "february", "march", "april", "may", "june",
             "july", "august", "september", "october", "november",
             "december"));

define('WEEKDAY_NAME_LIST',
       array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
             'saturday'));

define('DAY_NAME_LIST',
       array("-", "first", "second", "third", "fourth", "fifth", "sixth",
             "seventh", "eighth", "ninth", "tenth", "eleventh", "twelvth",
             "thirteenth", "fourteenth", "fifteenth", "sixteenth",
             "seventeenth", "eighteenth", "nineteenth", "twentieth",
             "twenty first", "twenty second", "twenty third", "twenty fourth",
             "twenty fifth", "twenty sixth", "twenty seventh", "twenty eighth",
             "twenty ninth", "thirtyieth", "thirty first"));

define('STANDARD_FORECAST_PERIOD_NAMES',
       array('today', 'tonight', 'tomorrow', 'tomorrow night', 'later today',
             'this evening', 'this morning', 'this afternoon', 'later tonight',
             'later tomorrow'));

define('WEEKDAYS_PERIOD_NAMES',
       array_merge(WEEKDAY_NAME_LIST,
                   array('sunday night', 'monday night', 'tuesday night',
                         'wednesday night', 'thursday night', 'friday night',
                         'saturday night')));

define('WELL_HAVE_ARRAY',
       array("we'll have", "we'll see", "we're going to have",
             "we'll experience", "expect", "anticipate"));

// ---------------------- OBTAIN FORECAST INFORMATION ----------------------

$alerts_text = '';
$alerts = '';
$total_alert_count = 0;
$weather_parser = New WeatherParser();

$original_dir = getcwd();

chdir("/home/weewx/weewx-data/public_html");

$forecast_result =
  $weather_parser->obtain_forecast
  (array('latitude' => LATITUDE,
         'longitude' => LONGITUDE,
         'placename' => PLACE_NAME,
         'nws-zone' => NWS_ZONE,
         'nws-county' => NWS_COUNTY,
         'nws-forecast-office' => NWS_FORECAST_OFFICE,
         'timezone' => TIMEZONE,
         'show-duplicate-alerts' => SHOW_DUPLICATE_ALERTS));

// For debugging:
// print_r($forecast_result);

chdir($original_dir);

// ----------------------------- FUNCTIONS ----------------------------------

function wf_truncate ($input_num, $input_precision) {

  $input_float = floatval($input_num);
  $was_negative = false;
  if ($input_float < 0.0) {
    $was_negative = true;
    $input_float = abs($input_float);
  }
  $powval = pow(10.0, $input_precision);
  $truncated = floatval(floor($input_float * $powval)) / $powval;
  if ($was_negative == true) {
    $truncated = -1.0 * $truncated;
  }
  return $truncated;
}

function wf_num_to_words_helper ($x) {

  $f = new NumberFormatter("en", NumberFormatter::SPELLOUT);
  $f->setTextAttribute(NumberFormatter::DEFAULT_RULESET,
                       "%spellout-numbering-verbose");
  $y = $f->format($x);
  $f = null;
  return($y);
}

function wf_num_to_words ($input_num, $input_precision, $singular_suffix,
                          $plural_suffix) {

  // Get the proper precision
  $input_float = wf_truncate($input_num, $input_precision);
  $result = wf_num_to_words_helper($input_float);
  if ($input_float == 1.0) {
    $result .= " " . $singlar_suffix;
  } else {
    $result .= " " . $plural_suffix;
  }
  return $result;
}

function wf_lowercase_first_letter ($input_str) {

  return strtolower(substr($input_str, 0, 1)) . substr($input_str, 1);
}

function current_date_time_string () {

  $cd = new DateTime();

  $time_str = $date_str = "";
  $year_num = intval($cd->format('Y'));
  $month_num = intval($cd->format('m'));
  $day_num = intval($cd->format('d'));
  $hour_num = intval($cd->format('H'));
  $minute_num = intval($cd->format('i'));
  $second_num = intval($cd->format('s'));
  $dow_num = intval($cd->format('w'));

  // Handle special cases
  if ($hour_num === 0) {
    $time_str = "oh";
  } else {
    $time_str = NUMBER_STRINGS[$hour_num];
  }
  $time_str .= " hundred hours";
  if ($minute_num !== 0) {
    $time_str .= " and " . NUMBER_STRINGS[$minute_num] .
      (($minute_num === 1) ? " minute" : " minutes");
  }
  if ($second_num !== 0) {
    $time_str .= " and " . NUMBER_STRINGS[$second_num] .
      (($second_num === 1) ? " second" : " seconds");
  }

  // We have the time. Now get the day of week
  $time_str .= " on " . WEEKDAY_NAME_LIST[$dow_num];

  // Add in the month
  $time_str .= ", " . MONTH_NAME_LIST[$month_num];

  // Add in the day
  $time_str .= " " . DAY_NAME_LIST[$day_num];

  // Add in the year
  $century_num = intdiv($year_num, 100);
  $time_str .= ", " . NUMBER_STRINGS[$century_num];
  $year_in_century_num = $year_num % 100;
  if ($year_in_century_num === 0) {
    $time_str .= " hundred";
  } else {
    if ($year_in_century_num < 10) {
      $time_str .= " oh";
    }
    $time_str .= " " . NUMBER_STRINGS[$year_in_century_num];
  }
  $cd = null;
  return $time_str;
}

function create_status_text ($prefix_text) {

  return $prefix_text . " at " . current_date_time_string();
}

function better_period_name ($input_pn, $is_nighttime, $i) {

  // Make it lowercase.
  $ipn = strtolower($input_pn);
  $ipn_words = explode(' ', $ipn);
  $ipn_first_word = $ipn_words[0];

  // First, check out the initial entry is not this weekday - if so,
  // just call this field 'right now'
  if ($i == 0) {
    $this_weekday_number = array_search($ipn, WEEKDAY_NAME_LIST);
    if ($this_weekday_number !== false) {
      $cd = new DateTime();
      $current_weekday_number = intval($cd->format('w'));
      $cd = null;
      if ($current_weekday_number != $this_weekday_number) {
        return 'right now';
      }
    }
  }

  // Now, we want to say the entry is either 'Today', 'Tonight' or
  // 'Tomorrow' or 'Tomorrow Night' if the entry begins with a weekday
  if (in_array($ipn, WEEKDAYS_PERIOD_NAMES)) {
    if ($is_nighttime) {
      switch ($i) {
      case 0:
      case 1:
        return "Tonight";
      case 2:
        return "Tomorrow night";
      default:
        return "On " . $input_pn;
      }
    } else {
      switch ($i) {
      case 0:
        return "Today";
      case 1:
      case 2:
        return "Tomorrow";
      default:
        return "On " . $input_pn;
      }
    }
  } else {
    if (in_array($ipn, STANDARD_FORECAST_PERIOD_NAMES)) {
      return $input_pn;
    } else {
      return "On " . $input_pn;
    }
  }
}

function wf_get_alerts () {

  GLOBAL $alerts_text;
  GLOBAL $alerts;
  GLOBAL $total_alert_count;
  GLOBAL $forecast_result;

  $alerts_text = '';
  if (isset($forecast_result['alerts'])) {
    $alerts = $forecast_result['alerts'];
  } else {
    $alerts = null;
  }
  $total_alert_count = 0;
  $spoken_alert_count = 1;

  // A test alert to test alert usage.
  if (DEBUG_ALERTS) {
    $alerts[0]['title'] = 'Alert Debugging Title. This is a test. There is ' .
      'no real alert.';
    $alerts[0]['link'] = 'Alert Link';
    $alerts[0]['event'] = 'Alert Event';
    $alerts[0]['severity'] = 'Alert Severity';
  }

  if (isset($alerts[0]['title'])) {

    $alerts_text_singular = "There is an active national weather service " .
      "alert! Go to a reliable source for weather information to learn more " .
      "details about this, but as a summary, here is the alert: ";

    $alerts_text_plural =
      "There are active national weather service alerts! Go to a reliable " .
      "source for weather information to learn more details about these, " .
      "but as a summary, here are the alerts: ";

    // Loop through the alerts getting total unique count
    for ($j = 0; isset($alerts[$j]['title']); $j++) {
      $found_prior_use = false;
      for ($k = 0; (($k < $j) && ($found_prior_use == false)); $k++) {
        if (isset($alerts[$k]['title'])) {
          if (!strcmp(trim($alerts[$k]['link']),
                      trim($alerts[$j]['link']))) {
            $found_prior_use = true;
          }
        }
      }
      if ($found_prior_use == false) {
        $total_alert_count++;
      }
    }

    // If no alerts, just return
    if ($total_alert_count <= 0) {
      return;
    }

    // Set alert text to proper prefix
    $alerts_text = ($total_alert_count == 1) ?
      $alerts_text_singular : $alerts_text_plural;

    for ($j = 0; isset($alerts[$j]['title']); $j++) {
      $found_prior_use = false;
      for ($k = 0; (($k < $j) && ($found_prior_use == false)); $k++) {
        if (isset($alerts[$k]['title'])) {
          if (!strcmp(trim($alerts[$k]['link']),
                      trim($alerts[$j]['link']))) {
            $found_prior_use = true;
          }
        }
      }
      if ($found_prior_use == false) {
        $severity_level = '';
        $title_text = $alerts[$j]['title'];
        $event_name = $alerts[$j]['event'];
        $headline = '';
        if (isset($alerts[$j]['NWSheadline'])) {
          $headline = $alerts[$j]['NWSheadline'];
        }
        if (isset($alerts[$j]['severity'])) {
          if (strtolower($alerts[$j]['severity']) != 'unknown') {
            $severity_level = $alerts[$j]['severity'] . ' ';
            $event_name = strtolower($event_name);
          }
        }
        if ($total_alert_count > 1) {
          $alerts_text .= "Alert number " . $spoken_alert_count++ . " of " .
            $total_alert_count . ": ";
        }
//        $alerts_text .= $severity_level . $event_name . ". ";
        $alerts_text .= $title_text . ". ";
        if ($headline != '') {
          $alerts_text .= $headline . ". ";
        }
      }
    }
  }
  if ($total_alert_count > 1) {
    $alerts_text .= " This is the end of the alert list...";
  }
}

function wf_produce_file ($file_root_name, $text_name, $file_text,
                          $incoming_array, $give_alerts,
                          $give_verbose_alerts) {

  GLOBAL $total_alert_count;
  GLOBAL $alerts_text;

  if (DEBUG_TEST_NO_FILE_WRITES) {
    return;
  }

  $filename = ALEXA_FILES_ROOT . $file_root_name . '-' .
    NWS_ZONE . '.txt';
  $file = fopen($filename, 'w');
  if ($file !== false) {
    $full_file_text = '';
    $file_alert_text = '';
    if ($give_alerts) {
      if ($total_alert_count > 0) {
        if ($give_verbose_alerts !== false) {
          $file_alert_text =
            "Before getting to the $text_name, please note that $alerts_text Now on to the $text_name : ";
        } else {
          $file_alert_text = $alerts_text;
        }
      } else {
        if ($give_verbose_alerts === false) {
          $file_alert_text =
            "There are no known active national weather service alerts at the time this report was generated.";
        }
      }
    }
    $full_file_text .= $file_text;
    if (empty($incoming_array)) {
      $file_info = array();
    } else {
      $file_info = $incoming_array;
    }
    $file_info['alert_speech'] = $file_alert_text;
    $file_info['speech'] = $full_file_text;
    $file_info['timestamp'] = gmdate("Y-m-d\TH:i:s\Z");
    fwrite($file, json_encode($file_info));
    fflush($file);
    fclose($file);
    if (DEBUG === true) {
      echo $full_file_text . "\n";
    }
  }
}

// ------------------------------- MAIN CODE --------------------------------

// ---------------------- INDICATE BEGINNING STATUS -------------------------

wf_produce_file('status', 'status',
                create_status_text("Began producing information"), array(),
                false, false);

wf_get_alerts();

// ----------------------------- TEMPERATURE --------------------------------

$temperature_text =
  'Unable to procure ' . CITY_NAME . ' temperature information; sorry!';

if ($forecast_result['result'] == 'OK') {
  $forecast_details = $forecast_result['forecast-details'];
  $filename = WEEWX_OUTPUT_ROOT . "outsidetemp.txt";
  if ((file_exists($filename)) && (filemtime($filename) > (time() - 900))) {
    $outside_temp_file = fopen($filename, 'r');
    if ($outside_temp_file !== false) {
      $temptext = fread($outside_temp_file, filesize($filename));
      fclose($outside_temp_file);
      preg_match_all('!\d+\.*\d*!', $temptext, $temp_matches);
      if (is_array($temp_matches)) {
        $current_outside_temp_array = $temp_matches[0];
        if (is_array($current_outside_temp_array)) {
          $current_outside_temp =
            round((float) $current_outside_temp_array[0]);
          $degree_text = ' degree';
          if (abs($current_outside_temp) != 1) {
            $degree_text .= 's';
          }
          $temperature_text = 'The current temperature in ' . CITY_NAME .
            ' is ' . $current_outside_temp . $degree_text . ' Farenheit.';
          for ($i = 0; $i <= 2; $i++) {
            if (is_array($forecast_details[$i]) &&
                (isset($forecast_details[$i]['low-temperature']) ||
                 isset($forecast_details[$i]['high-temperature']))) {
              $temperature_text .= " " .
                better_period_name
                ($forecast_details[$i]['period-name'],
                 isset($forecast_details[$i]['low-temperature']), $i) .
                ', ' . WELL_HAVE_ARRAY[array_rand(WELL_HAVE_ARRAY)] .
                ' a ';
              if (isset($forecast_details[$i]['low-temperature'])) {
                $temperature_text .= 'low of ';
                $low_temp =
                  (float) $forecast_details[$i]['low-temperature'];
                if ($i == 0) {
                  if ($low_temp > $current_outside_temp) {
                    $low_temp = $current_outside_temp;
                  }
                }
                $temperature_text .= $low_temp;
              } else {
                $temperature_text .= 'high of ';
                $high_temp =
                  (float) $forecast_details[$i]['high-temperature'];
                if ($i == 0) {
                  if ($high_temp < $current_outside_temp) {
                    $high_temp = $current_outside_temp;
                  }
                }
                $temperature_text .= $high_temp;
              }
              $temperature_text .= '.';
            }
          }
        }
      }
    }
  }
}

wf_produce_file('temperature', 'temperature', $temperature_text, array(),
                true, true);

// ------------------------------ ALERTS -----------------------------------

wf_produce_file('alerts', 'alerts', '', array(), true, false);

// ------------------------- GARAGE TEMPERATURE ----------------------------

$garage_text =
  'Unable to procure garage temperature information; sorry!';

$filename = WEEWX_OUTPUT_ROOT . "garagetemp.txt";
if ((file_exists($filename)) && (filemtime($filename) > (time() - 900))) {
  $garage_temp_file = fopen($filename, 'r');
  if ($garage_temp_file !== false) {
    $temptext = fread($garage_temp_file, filesize($filename));
    fclose($garage_temp_file);
    preg_match_all('!\d+\.*\d*!', $temptext, $temp_matches);
    if (is_array($temp_matches)) {
      $current_garage_temp_array = $temp_matches[0];
      if (is_array($current_garage_temp_array)) {
        $current_garage_temp =
          round((float) $current_garage_temp_array[0]);
        $degree_text = ' degree';
        if (abs($current_garage_temp) != 1) {
          $degree_text .= 's';
        }
        $garage_text = 'The current garage temperature is ' .
          $current_garage_temp . $degree_text . ' Farenheit.';
      }
    }
  }
}

wf_produce_file('garage', 'garage temperature', $garage_text, array(), true,
                true);

// ---------------------- FORECAST AND FULL FORECAST -----------------------

// Now, get the full forecast information.
$full_forecast_text = 'Unable to procure ' . CITY_NAME .
  ' full forecast information; sorry!';

$forecast_text = 'Unable to procudure ' . CITY_NAME .
  ' forecast information; sorry!';

if ($forecast_result['result'] == 'OK') {
  $forecast_details = $forecast_result['forecast-details'];
  for ($i = 0; $i <= 2; $i++) {
    if (is_array($forecast_details[$i])) {
      if ($i == 0) {
        $full_forecast_text = $forecast_text = '';
      } else {
        $full_forecast_text .= ' ';
        $forecast_text .= ' ';
      }
      $full_forecast_text .=
        better_period_name
        ($forecast_details[$i]['period-name'],
         isset($forecast_details[$i]['low-temperature']), $i) . ' ' .
        WELL_HAVE_ARRAY[array_rand(WELL_HAVE_ARRAY)] . ' a ' .
        wf_lowercase_first_letter($forecast_details[$i]
                                  ['generated-forecast-computer-speak']);
      $forecast_text .=
        better_period_name
        ($forecast_details[$i]['period-name'],
         isset($forecast_details[$i]['low-temperature']), $i) . ', ' .
        wf_lowercase_first_letter($forecast_details[$i]
                                  ['generated-short-forecast-computer-speak']);
    }
  }
}

wf_produce_file('full-forecast', 'full forecast', $full_forecast_text,
                array(), true, true);

wf_produce_file('forecast', 'forecast', $forecast_text, array(), true, true);

// ------------------------------ RAINFALL ---------------------------------

// Now, get the rainfall information
$rain_text =
  "Unable to obtain current or historic rainfall information; sorry!";

$filename = WEEWX_OUTPUT_ROOT . "rain.txt";
if ((file_exists($filename)) && (filemtime($filename) > (time() - 900))) {
  $rain_file = fopen($filename, 'r');
  if ($rain_file !== false) {
    $rain_line = fgets($rain_file, filesize($filename));
    fclose($rain_file);
    $rain_line = str_replace('\n', '', $rain_line);
    $rain_line = str_replace('\r', '', $rain_line);
    $rain_info = explode(';', $rain_line);
    if (count($rain_info) >= 5) {
      $rain_info_current_rate = (float) $rain_info[0];
      $rain_info_rain_today = (float) $rain_info[1];
      $rain_info_rain_past_seven_days = (float) $rain_info[2];
      $rain_info_rain_since_start_of_rain_year = (float) $rain_info[3];
      $rain_info_rain_year_month_start = (integer) $rain_info[4];
      $rain_text = "";
      if ($rain_info_current_rate <= 0.0) {
        $rain_text .= "It is not currently raining. ";
      } else {
        $rain_text .= "The current rain rate is " .
          wf_num_to_words($rain_info_current_rate, 2, "inch per hour",
                          "inches per hour") . " ";
      }
      if ($rain_info_rain_today <= 0.0) {
        $rain_text .= "We've had no rain today. ";
      } else {
        $rain_text .= "We've had " .
          wf_num_to_words($rain_info_rain_today, 2, "inch",
                          "inches") . " of rain today. ";
      }
      if ($rain_info_rain_past_seven_days <= 0.0) {
        $rain_text .= "We've had no rain in the past week. ";
      } else {
        $rain_text .= "We've had " .
          wf_num_to_words($rain_info_rain_past_seven_days, 2, "inch",
                          "inches") . " of rain in the past week. ";
      }
      if ($rain_info_rain_since_start_of_rain_year <= 0.0) {
        $rain_text .= "We've had no rain since " .
          MONTH_NAME_LIST[$rain_info_rain_year_month_start] .
          " first, which is the start of our rain year. ";
      } else {
        $rain_text .= "We've had " .
          wf_num_to_words($rain_info_rain_since_start_of_rain_year, 2, "inch",
                          "inches") . " of rain since " .
          MONTH_NAME_LIST[$rain_info_rain_year_month_start] .
          " first, which is the start of our rain year. ";
      }
    }
  }
}

wf_produce_file('rain', 'rainfall information', $rain_text, array(), true,
                true);

// ------------------------------ AIR QUALITY --------------------------------

// Now, get the air quality information
$aqi_text =
  "Unable to obtain our house location's air quality information; sorry!";
$our_aqi = -1;
$aqi_category = "??";
$aqi_category_alexa = "??";
$aqi_display_blended_color = "#ffffff";
$aqi_display_text_blended_color = "#000000";
$filename = WEEWX_OUTPUT_ROOT . "pollutants.txt";
if ((REPORT_AIR_QUALITY) && (file_exists($filename)) &&
    (filemtime($filename) > (time() - 900))) {
  $pollutants_file = fopen($filename, 'r');
  if ($pollutants_file !== false) {
    $pollutant_line = fgets($pollutants_file, filesize($filename));
    fclose($pollutants_file);
    $pollutant_line = str_replace('\n', '', $pollutant_line);
    $pollutant_line = str_replace('\r', '', $pollutant_line);
    $pollutants = explode(';', $pollutant_line);
    if (count($pollutants) >= 5) {
      $pollutants_has_data = strtolower($pollutants[0]);
      $pm2_5 = $pollutants[1];
      $pm10_0 = (float) $pollutants[2];
      $out_humidity = (float) $pollutants[3];
      $pm2_5_conversion_type = $pollutants[4];
      if ($pollutants_has_data === "true") {
        $aqi_result =
          compute_and_retrieve_realtime_single_sample_us_epa_aqi_estimate
          (array('pm_2_5_realtime_single_sample' => $pm2_5,
                 'pm_10_0_realtime_single_sample' => $pm10_0,
                 'out_humidity' => $out_humidity,
                 'pm2_5_pa_conversion_type' => $pm2_5_conversion_type));
        if ($aqi_result['result'] == "OK") {
          $our_aqi = $aqi_result['aqi-realtime-single-sample-us-epa'];
          $aqi_category =
            $aqi_result['aqi-realtime-single-sample-us-epa-display-category'];
          $aqi_category_alexa =
            $aqi_result
            ['aqi-realtime-single-sample-us-epa-display-category-alexa'];
          $aqi_detail =
            $aqi_result['aqi-realtime-single-sample-us-epa-display-detail'];
          $aqi_text = "The current air quality index is " .
            $our_aqi . ", a level that is considered " .
            strtolower($aqi_category) . ". " . $aqi_detail;
          $aqi_display_blended_color =
            $aqi_result
            ['aqi-realtime-single-sample-us-epa-display-blended-color'];
          $aqi_display_text_blended_color =
            $aqi_result
            ['aqi-realtime-single-sample-us-epa-display-text-blended-color'];
        }
      }
    }
  }
}

$aqi_info = array('aqi_category' => $aqi_category,
                  'aqi_category_alexa' => $aqi_category_alexa,
                  'aqi_display_blended_color' =>
                  $aqi_display_blended_color,
                  'aqi_display_text_blended_color' =>
                  $aqi_display_text_blended_color,
                  'aqi_value' => (($our_aqi >= 0) ? $our_aqi : "??"));

wf_produce_file('aqi', 'air quality index', $aqi_text, $aqi_info, true, true);

// ---------------------------- SUN RISE AND SET ----------------------------

$sunriseset_text = '';
$todaydt = new DateTime('today');
$todaydts = $todaydt->format('Y-m-d');
$todaytime = strtotime($todaydts);
$tomorrowdt = new DateTime('tomorrow');
$tomorrowdts = $tomorrowdt->format('Y-m-d');
$tomorrowtime = strtotime($tomorrowdts);
date_default_timezone_set(TIMEZONE);
$utc_offset = date('Z') / 3600;
$today_sun_info = date_sun_info($todaytime, LATITUDE, LONGITUDE);
$todaysunrise = date("G:i", $today_sun_info["sunrise"]);
$todaysunset = date("G:i", $today_sun_info["sunset"]);
$tomorrow_sun_info = date_sun_info($tomorrowtime, LATITUDE, LONGITUDE);
$tomorrowsunrise = date("G:i", $tomorrow_sun_info["sunrise"]);
$tomorrowsunset = date("G:i", $tomorrow_sun_info["sunset"]);
$packedstring =
  $todaysunrise . ':x:' . $todaysunset . ':x:' .
  $tomorrowsunrise . ':x:' . $tomorrowsunset . ':x';
$localtime_info = localtime(time(), true);
$current_hour = $localtime_info["tm_hour"];
$current_min = $localtime_info["tm_min"];
$unpackedstring = explode(":", $packedstring);
for ($i = 0; $i < 11; $i++) {
  if (($i + 1) % 3) {
    $unpackedstring[$i] = intval($unpackedstring[$i]);
  }
}

// We are either before sunrise today, before sunset today, or after both
$start_offset = 0;
if (($current_hour > $unpackedstring[0]) ||
    (($current_hour == $unpackedstring[0]) &&
     ($current_min >= $unpackedstring[1]))) {
  $start_offset = 3;
  if (($current_hour > $unpackedstring[3]) ||
      (($current_hour == $unpackedstring[3]) &&
       ($current_min >= $unpackedstring[4]))) {
    $start_offset = 6;
  }
}
// Convert hours to 12 hour format
// Convert 0 minutes to 'oh clock'
// Convert 1-9 minutes to 'oh <number>'
for ($i = 0; $i <= 9; $i += 3) {
  if ($unpackedstring[$i + 1] == 0) {
    $unpackedstring[$i + 1] = ' oh clock';
  } else if ($unpackedstring[$i + 1] < 10) {
    $unpackedstring[$i + 1] = ' oh ' . strval($unpackedstring[$i + 1]);
  }
  if ($unpackedstring[$i] < 12) {
    $unpackedstring[$i + 2] = ' a. m.';
    if ($unpackedstring[$i] == 0) {
      $unpackedstring[$i] = 12;
    }
  } else {
    $unpackedstring[$i + 2] = ' p. m.';
    if ($unpackedstring[$i] > 12) {
      $unpackedstring[$i] = $unpackedstring[$i] - 12;
    }
  }
}
for ($i = 0; $i < 2; $i++) {
  if ($i > 0) {
    $sunriseset_text .= '. ';
  }
  $doing_sunrise = (($start_offset % 6) == 0);
  $doing_today = ($start_offset < 6);
  if ($doing_sunrise) {
    $sunriseset_text .= ($doing_today ? 'This' : 'Tomorrow') .
      ' morning, sunrise will be at ';
    $sunriseset_text .=
      $unpackedstring[$start_offset] . ' ' .
      $unpackedstring[$start_offset + 1] .
      $unpackedstring[$start_offset + 2];
    $doing_sunrise = 0;
  } else {
    $sunriseset_text .= ($doing_today ? 'This' : 'Tomorrow') .
      ' evening, sunset will be at ';
    $sunriseset_text .=
      $unpackedstring[$start_offset] . ' ' .
      $unpackedstring[$start_offset + 1] .
      $unpackedstring[$start_offset + 2];
    $doing_sunrise = 1;
  }
  $start_offset += 3;
}

wf_produce_file('sunriseset', 'sun rise and set times', $sunriseset_text,
                array(), true, true);

// --------------------- INDICATE COMPLETION STATUS -------------------------

wf_produce_file('status', 'status',
                create_status_text("Completed producing information"),
                array(), true, true);

?>

us-epa-realtime-single-sample-aqi.inc.php

<?php

ini_set('display_errors', 'On');
error_reporting(E_ALL | E_STRICT);

// "Single-sample/instant" AQI Calculator Based on US-EPA formulae
//
// This code uses information from the United States EPA for the formula for
// calculating an AQI value for various pollutant types. So far, it just
// implements this for PM2.5 and PM10.0 particulate counts.
//
// CAVEAT: This system just computes an AQI for the inputted  pollution
//         measurements to the one call to this function. This is NOT a valid
//         way to compute an 'official' AQI according to the US EPA, which
//         wants to compute an AQI from a series series of measurements.
//         Therefore, this package should not be viewed as producing a truly
//         valid AQI value!
//
// Copyright (C) 2019-2024 Joel P. Bion <jpbion_at_westvi_dot_com>
//
// This library is free software; you can use or redistribute it and/or modify
// it under the terms of version 2.1 of the GNU Lesser General Public License
// as published by the Free Software Foundation; version 2.1 of the License, AS
// LONG AS YOU AGREE TO ALL OF ITS TERMS, AND ALSO AGREE TO AND UNDERSTAND THE
// TERMS OF THE DISCLAIMER LISTED BELOW. The following disclaimer must be left
// unchanged with all versions of this script, or collection of scripts and/or
// supporting items:
//
//  DISCLAIMER:
//
//  This script or collection of scripts and/or supporting items (hereafter
//  called "this software package") 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 Lesser
//  General Public License version 2.1 for more details. Beyond the lack of
//  warranty listed above, it is required that any users of this software
//  package accept and agree that the author of this software package, or any
//  user of it, or entity providing all or portions of this software package
//  within their weather package, or any other kind of package, is making no
//  claim as to the accuracy of this information. The information, and
//  forecast details this software package provides, and all calculations it
//  performs, are the outputs of the work of an amateur hobbyist, the original
//  author. Therefore, the outputs, return values, print outs, etc., of this
//  software package are entirely experimental, and could very well be
//  partially or wholly wrong, missing data, etc. You must view the outputs of
//  this software package, or any package derived from it, as the unofficial
//  hobby results an amateur hobbyist's work, and not that of a professional
//  meteorologist. So, do not base personal decisions, or professional
//  decisions, or really any decision at all, wholly or even the smallest bit
//  partially, on the results, printouts, outputs, return values, etc., of this
//  software package! If you cannot or are unwilling to accept full liability
//  and responsibility for your use of this software package, or you cannot or
//  are unwilling to indemnify the author, web hoster, etc. of this software
//  package for any loss of any kind, that you or others that view or use data
//  data, of any kind, produced by your use of this software pacakge, thend any
//  and all rights for you to use it are completely and permanently revoked,
//  and you must not use, leverage, call, reference, host, etc. this software
//  package in any way, on any medium, or any machinery. You should have
//  received a copy of the GNU Lesser General Public License version 2.1 along
//  with this script or collection of scripts; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA,
//  02111-1307, USA
//

//
// USAGE:
//
//  In your PHP file, do this to include this functionality:
//
//    require_once('us-epa-realtime-single-sample-aqi.php');
//
//  Then to use it do the following:
//
// 1) Call it with:
//
//    $us_epa_realtime_single_sample_aqi_estimate =
//      compute_and_retrieve_realtime_single_sample_us_epa_aqi_estimate
//       (array('pm_2_5_realtime_single_sample'  => 1.23,
//              'pm_10_0_realtime_single_sample' => 10.5,
//              'out_humidity'                   => 92.0,
//              'pm2_5_pa_conversion_type'       => 'pa-usepa'));
//
//
//    If you have one of the 2.5 or 10_0 realtime single-sample values, but not
//    both, there is no need to pass in any value for the one you don't have.
//
//    If you don't have either the 2_5 or 10_0 realtime single-sample values,
//    an error is returned.
//
//    'out_humidity' must be set if the conversion type is pa-usepa; it does
//    not need to be given for any other conversion type. It should be given in
//    the range 0.0 - 100.0, inclusive. In other words, a 92% humidity should
//    be given as 92.0, not 0.92.
//
//    If 'pm2_5_pa_conversion_type' is not set, the unconverted raw sensor data
//    is used.
//
//    If 'pm2_5_pa_conversion_type' is set, it must be one of the following:
//
//        pa-raw       - Use the PurpleAir PM values directly. Do not convert
//                       them. This is the default conversion type, and is used
//                       when this parameter is not specified.
//
//        pa-usepa     - Use the PurpleAir conversions given by the US-EPA. If
//                       input PM2.5 is > 250, then do not convert. If it is <=
//                       250, then convert using:
//
//                        New = (0.52 * Orig) - (0.085 * out_humidity) + 5.71
//
//        pa-woodsmoke - Australian-supplied conversion using:
//
//                        New = (0.55 * Orig) + 0.53
//
//        pa-aqandu    - From the University of Utah, Salt Lake City, using:
//
//                        New = (0.778 * Orig) + 2.65
//
//        pa-lrapa     - From the Lane Regional Air Protection Agency, using:
//
//                        New = (0.5 * Orig) - 0.66
//
// 2) Verify the function returned decent values
//
//    Look at the value of the $result['result']. If it's "OK", then you have
//    valid return parameters. If it's "ERROR", then the $result['detail']
//    contains the text of the error, and nothing can be assumed as valid in
//    any other data returned by this function.
//
// 3) If the function returned a result of 'OK', use the given values in the
//    returned  array. Note that some values may not be included if a valid
//    value for the item could not be computed!
//
//    'aqi-realtime-single-sample-us-epa'
//
//       This is the resultant AQI value. It is the maximum of the AQIs
//       calculated for any of the inputted pollutant measurements
//
//    'aqi-pm2-5-realtime-single-sample-us-epa'
//
//       This is the resultant AQI value for the input PM2.5 pollutant
//       measurement. This will be set to null if no PM2.5 measurement was
//       given as input.
//
//    'aqi-pm2-5-truncated-concentration-float'
//
//       This is the incoming PM2.5 pollutant, truncated to the point the US
//       EPA wants it to be. This will be set to null if no PM2.5 measurement
//       was given as input.
//
//    'aqi-pm2-5-pa-conversion-type'
//
//       This is the 'standardized' form of the pm2_5_pa_conversion_type
//       parameter.
//
//    'aqi-pm2-5-pa-conversion-detail'
//
//       This is the detailed description of the pm2_5_pa_conversion_type
//
//    'aqi-pm10-0-realtime-single-sample-us-epa'
//
//       This is the resultant AQI value for the input PM2.5 pollutant
//       measurement. This will be set to null if no PM10 measurement was
//       given as input.
//
//    'aqi-pm10-0-truncated-concentration-float'
//
//       This is the incoming PM2.5 pollutant, truncated to the point the US
//       EPA wants it to be. This will be set to null if no PM10 measurement
//       was given as input.
//
//    'aqi-realtime-single-sample-us-epa-display-category'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-category'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-category'
//
//       The display category of the resultant combined AQI value, the AQI
//       value for just the PM2.5 pollutant, and the AQI value for just the
//       PM10 pollutant. This will be a text field such as GOOD, MODERATE,
//       UNHEALTHY FOR SENSITIVE, etc.
//
//    'aqi-realtime-single-sample-us-epa-display-category-alexa'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-category-alexa'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-category-alexa'
//
//       The display category of the resultant combined AQI value, the AQI
//       value for just the PM2.5 pollutant, and the AQI value for just the
//       PM10 pollutant. This string also contains breaks to allow for easy
//       displays on an alexa device. This will be a text field such as GOOD,
//       MODERATE, "UNHEALTHY FOR<br>SENSITIVE", etc.
//
//    'aqi-realtime-single-sample-us-epa-display-detail'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-detail'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-detail'
//
//       Text giving detail of the AQI category of the resultant combined AQI
//       value, the AQI value for just the PM2.5 pollutant, and the AQI value
//       for just the PM2.5 pollutant.
//
//    'aqi-realtime-single-sample-us-epa-display-color'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-color'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-color'
//
//       A #rrggbb result, giving an rgb value for the color assigned by the US
//       EPA to the resultant AQI, the AQI value for just the PM2.5 pollutant,
//       and the AQI value for just the PM10 pollutant. Note that this result
//       uses the same color for ALL AQI values within the resultant category's
//       range. So, (current as of June 2019) an AQI of 51 and an AQI of 99 get
//       the very same color.
//
//    'aqi-realtime-single-sample-us-epa-display-blended-color'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-blended-color'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-blended-color'
//
//       A #rrggbb result, giving an rgb value for the AQI color that is a
//       blend between the resultant category's US EPA assigned official color
//       and the next category's US EPA assigned official color. This is given
//       for the resultant combined AQI, the resultant PM2.5-only AQI, and the
//       resultant PM10 AQI. As an example (current as of June 2019), an AQI of
//       '1' has a blended color that looks quite green (the US EPA color for
//       the AQI range 0-49), while an AQI of '49,' although still considered
//       in the first 'green' category, will have a blended color that is
//       almost completely yellow, the color of the second AQI category with
//       values 50-99. And an AQI of 25 would get a yellow/green color
//       somewhere midway between green and yellow. The author of this script
//       prefers using this blended color, because, without the blending, two
//       very close AQI readings right at a color boundary will have quite
//       different colors, causing casual observers to mistakenly think a drop
//       or raise of a couple of AQI points made a huge difference, when they
//       don't.
//
//    'aqi-realtime-single-sample-us-epa-display-text-color'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-text-color'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-text-color'
//
//       A #rrggbb result, giving a good choice of text color for any text that
//       you want to place over a field that is set to the AQI color given
//       above as its background. This is meant to be used with the
//       'aqi-realtime-single-sample-us-epa-display-color' result.
//
//    'aqi-realtime-single-sample-us-epa-display-text-blended-color'
//    'aqi-pm2-5-realtime-single-sample-us-epa-display-text-blended-color'
//    'aqi-pm10-0-realtime-single-sample-us-epa-display-text-blended-color'
//
//       A #rrggbb result, giving a good choice of text color for any text that
//       you want to place over a field that is set to the blended AQI color as
//       its background. This is meant to be used with the
//       'aqi-realtime-single-sample-us-epa-display-blended-color' result.
//
//
// 4) There is a second function call, rarely used but handy in certain
//    circumstances when we need this information.
//
//    $aqi_info =
//      retrieve_aqi_info(array('aqi' => 1));
//
//    Verify the function returned decent values by looking at the value of
//    $aqi_info['result']. If it's "OK", then you have valid return parameters.
//    If it's "ERROR", then the $result['detail'] contains the text of the
//    error, and nothing can be assumed as valid in any other data returned by
//    this function.
//
//    The returned values that you get with an 'OK' result will be:
// 
//    'aqi'
//
//       The AQI you provided as input.
//
//    'aqi-display-category'
//
//       The display category corresponding to the input AQI value. This will
//       be a text field such as GOOD, MODERATE, UNHEALTHY FOR SENSITIVE, etc.
//
//    'aqi-display-category-alexa'
//
//       The display category corresponding to the input AQI value with line
//       breaks for Alexa. This will be a text field such as GOOD, MODERATE,
//       "UNHEALTHY FOR<br>SENSITIVE", etc.
//
//    'aqi-display-detail'
//
//       Text giving detail of the AQI category for the input AQI value.
//
//    'aqi-display-color'
//
//       A #rrggbb result, giving the rgb values for the color assigned by the
//       US EPA to the input AQI value.
//
//    'aqi-display-blended-color'
//
//       A #rrggbb result, giving an rgb value for the AQI color that is a
//       blend between the resultant category's US EPA assigned official color
//       and the next category's US EPA assigned official color. This is given
//       for the input AQI value. As an example (current as of June 2019), an
//       AQI of '1' has a blended color that looks quite green (the US EPA
//       color for the AQI range 0-49), while an AQI of '49,' although still
//       considered in the first 'green' category, will have a blended color
//       that is almost completely yellow, the color of the second AQI category
//       with values 50-99. And an AQI of 25 would get a yellow/green color
//       somewhere midway between green and yellow. The author of this script
//       prefers using this blended color, because, without the blending, two
//       very close AQI readings right at a color boundary will have quite
//       different colors, causing casual observers to mistakenly think a drop
//       or raise of a couple of AQI points made a huge difference, when they
//       don't.
//
//    'aqi-display-text-color'
//
//       A hex string (such as "#000000" or "#ffffff") giving a good choice of
//       text color for any text that you want to place over a field that is
//       set to the AQI color given above as its background. This is meant t
//       be used with the 'aqi-display-color' result.
//
//    'aqi-display-text-blended-color'
//
//       An #rrggbb result (such as "#000000" or "#ffffff") giving a good
//       choice of text color for any text that you want to place over a field
//       that is set to the blended AQI color as its background. This is meant
//       to be used with the 'aqi-display-blended-color' result.
//
//
// 5) There is a third function call, rarely used but handy in certain
//    circumstances when we need this information.
//
//    $conversion_type_info =
//      retrieve_pm2_5_aqi_conversion_type_info(
//        array('conversion-name' => 'str'));
//
//    Verify the function returned decent values by looking at the value of
//    $conversion_type_info['result']. If it's "OK", then you have valid return
//    parameters. If it's "ERROR", then the $result['detail'] contains the text
//    of the error, and nothing can be assumed as valid in any other data
//    returned by this function.
//
//    The returned values that you get with an 'OK' result will be:
// 
//    'conversion-name'
//
//       The conversion name you provided as input, but in all lower case and
//       with a string trim performed.
//
//    'conversion-detail'
//
//       This provides a human-readable short detail string describing the
//       conversion.
//

// Conversion types information

$global_aqi_pm2_5_pa_conversion_types =
  array(array('name' => 'pa-raw',
              'detail' => 'no PM2.5 conversion algorithm'),
        array('name' => 'pa-usepa',
              'detail' => 'US-EPA PM2.5 conversion algorithm'),
        array('name' => 'pa-woodsmoke',
              'detail' => 'Australian PM2.5 conversion algorithm'),
        array('name' => 'pa-aqandu',
              'detail' => 'University of Utah PM2.5 conversion algorithm'),
        array('name' => 'pa-lrapa',
              'detail' => 'Lane Regional Air Protection Agency PM2.5 ' .
                          'conversion algorithm'));

function retrieve_pm2_5_aqi_conversion_type_info ($params) {

  global $global_aqi_pm2_5_pa_conversion_types;

  // Convert incoming name 
  if (!isset($params['conversion-name'])) {
    return array('result' => 'ERROR',
                 'detail' =>
                 'No PM conversion name given, not even default empty string');
  }
  $incoming_name = $params['conversion-name'];
  if ((!isset($incoming_name)) ||
      ($incoming_name === '')) {
    $incoming_name = 'pa-raw';
  } else {
    $incoming_name = strtolower(trim($incoming_name));
  }
  $key =
    array_search($incoming_name,
                 array_column($global_aqi_pm2_5_pa_conversion_types, 'name'));
  if (isset($key) && ($key !== false)) {
    return
      array('result' => 'OK',
            'detail' => '',
            'conversion-name' => $incoming_name,
            'conversion-detail' =>
            $global_aqi_pm2_5_pa_conversion_types[$key]['detail']);
  }
  return
    array('result' => 'ERROR',
          'detail' => "Unknown PM conversion name '" . $incoming_name . "'");
}

// Used internally.
function aqi_bw_text_choice ($red, $green, $blue) {

  // For the sRGB/ITU-R BT.709 color space
  $relative_luminence =
    (0.2126 * ($red / 255.0)) + (0.7152 * ($green / 255.0)) +
    (0.0722 * ($blue / 255.0));
  if ($relative_luminence >= 0.5) {
    return "#000000";
  } else {
    return "#ffffff";
  }
}

function
compute_single_sample_us_epa_aqi_estimate_display_parameters ($input_aqi) {

  $pm2_5_10_0_aqi_display_array_length = 6;

  $pm2_5_10_0_aqi_display_breakpoints_low_values =
    array(0, 51, 101, 151, 201, 301);

  $pm2_5_10_0_aqi_display_breakpoints_high_values =
    array(50, 100, 150, 200, 300, 999);

  $pm2_5_10_0_aqi_display_background_colors =
    array(104, 225, 67, // green
          255, 255, 85, // yellow
          239, 133, 51, // orange
          234, 51, 36,  // red
          140, 26, 75,  // purple
          115, 20, 37); // maroon

  $pm2_5_10_0_aqi_display_categories =
    array("GOOD", "MODERATE", "UNHEALTHY FOR SENSITIVE GROUPS",
          "UNHEALTHY", "VERY UNHEALTHY", "HAZARDOUS");

  $pm2_5_10_0_aqi_display_categories_alexa =
    array("GOOD", "MODERATE", "UNHEALTHY FOR<br>SENSITIVE<br>GROUPS",
          "UNHEALTHY", "VERY<br>UNHEALTHY", "HAZARDOUS");

  $pm2_5_10_0_aqi_display_details =
    array("Air quality is considered satisfactory, and air pollution poses " .
          "little or no risk.",
          "Air quality is acceptable; however, for some pollutants there " .
          "may be a moderate health concern for a very small number of " .
          "people who are unusually sensitive to air pollution. Active " .
          "children and adults, and people with respiratory disease, such " .
          "as asthma, should limit prolonged outdoor exertion.",
          "Members of sensitive groups may experience health effects. The " .
          "general public is not likely to be affected. Active children and " .
          "adults, and people with respiratory disease, such as asthma, " .
          "should limit prolonged outdoor exertion.",
          "Everyone may begin to experience health effects; members of " .
          "sensitive groups may experience more serious health effects. " .
          "Active children and adults, and people with respiratory disease, " .
          "such as asthma, should avoid prolonged outdoor exertion; " .
          "everyone else, especially children, should limit prolonged " .
          "outdoor exertion.",
          "Health alert: everyone may experience more serious health " .
          "effects. Active children and adults, and people with respiratory " .
          "disease, such as asthma, should avoid all outdoor exertion; " .
          "everyone else, especially children, should limit outdoor exertion.",
          "Health warnings of emergency conditions. The entire population " .
          "is more likely to be affected. Everyone should avoid all outdoor " .
          "exertion.");

  $aqi_display_offset = $pm2_5_10_0_aqi_display_array_length - 1;
  $i = 0;
  for (; $i < $pm2_5_10_0_aqi_display_array_length; $i++) {
    if ($input_aqi <=
      $pm2_5_10_0_aqi_display_breakpoints_high_values[$i]) {
      $aqi_display_offset = $i;
      break;
    }
  }

  $aqi_display_offset_next = $aqi_display_offset + 1;
  if ($aqi_display_offset_next >= $pm2_5_10_0_aqi_display_array_length) {
    $aqi_display_offset_next = $aqi_display_offset;
  }
  $aqi_display_category =
    $pm2_5_10_0_aqi_display_categories[$aqi_display_offset];
  $aqi_display_detail = $pm2_5_10_0_aqi_display_details[$aqi_display_offset];
  $low_colors =
    array_slice($pm2_5_10_0_aqi_display_background_colors,
                $aqi_display_offset * 3, 3);
  $high_colors =
    array_slice($pm2_5_10_0_aqi_display_background_colors,
                $aqi_display_offset_next * 3, 3);
  $aqi_background_color = $low_colors;
  if ($aqi_display_offset != $aqi_display_offset_next) {
    $low_aqi =
      $pm2_5_10_0_aqi_display_breakpoints_low_values[$aqi_display_offset];
    $high_aqi =
      $pm2_5_10_0_aqi_display_breakpoints_high_values[$aqi_display_offset];

    $i = 0;
    for (; $i < 3; $i += 1) {
      $aqi_background_color[$i] =
        intval((($input_aqi - $low_aqi) *
                ($high_colors[$i] - $low_colors[$i]) /
                ($high_aqi - $low_aqi)) + $low_colors[$i]);
    }
  }
  $aqi_display_blended_color =
    sprintf("#%02x%02x%02x", $aqi_background_color[0],
            $aqi_background_color[1], $aqi_background_color[2]);
  $aqi_display_color = sprintf("#%02x%02x%02x", $low_colors[0],
                               $low_colors[1], $low_colors[2]);
  $aqi_display_text_blended_color =
    aqi_bw_text_choice($aqi_background_color[0], $aqi_background_color[1],
                       $aqi_background_color[2]);
  $aqi_display_text_color =
    aqi_bw_text_choice($low_colors[0], $low_colors[1], $low_colors[2]);

  return
    array('aqi' => $input_aqi,
          'aqi-display-category' =>
            $pm2_5_10_0_aqi_display_categories[$aqi_display_offset],
          'aqi-display-category-alexa' =>
            $pm2_5_10_0_aqi_display_categories_alexa[$aqi_display_offset],
      'aqi-display-detail' =>
            $pm2_5_10_0_aqi_display_details[$aqi_display_offset],
      'aqi-display-color' => $aqi_display_color,
      'aqi-display-blended-color' => $aqi_display_blended_color,
      'aqi-display-text-color' => $aqi_display_text_color,
      'aqi-display-text-blended-color' => $aqi_display_text_blended_color);
}

function retrieve_aqi_info ($incoming_aqi) {

  $int_aqi = (int) $incoming_aqi['aqi'];

  if ($int_aqi < 0) {
    return array('result' => 'ERROR',
                 'detail' =>
                 'Negative AQI values are not defined.');
  } else if ($int_aqi > 99999) {
    return array('result' => 'ERROR',
                 'detail' =>
                 'Provided AQI value is too large.');
  }
  $result =
    compute_single_sample_us_epa_aqi_estimate_display_parameters($int_aqi);
  $result['result'] = 'OK';
  $result['detail'] = '';
  return $result;
}
  
function
compute_and_retrieve_realtime_single_sample_us_epa_aqi_estimate
($input_pollutant_measurements) {

  // The breakpoint values are from the EPA website on May 24, 2019
  // https://aqs.epa.gov/aqsweb/documents/codetables/aqi_breakpoints.html
  $pm2_5_10_0_breakpoints_array_length = 8;
  $pm2_5_digits_after_decimal = 1;
  $pm2_5_breakpoints_concentration_low_values =
    array(0.0, 12.1, 35.5, 55.5, 150.5, 250.5, 350.5, 500.5);
  $pm2_5_breakpoints_concentration_high_values =
    array(12.0, 35.4, 55.4, 150.4, 250.4, 350.4, 500.4, 99999.9);

  $pm10_0_digits_after_decimal = 0;
  $pm10_0_breakpoints_concentration_low_values =
    array(0.0, 55.0, 155.0, 255.0, 355.0, 425.0, 505.0, 605.0);
  $pm10_0_breakpoints_concentration_high_values =
    array(54.0, 154.0, 254.0, 354.0, 424.0, 504.0, 604.0, 99999.0);

  $pm2_5_10_0_breakpoints_aqi_low_values =
    array(0.0, 51.0, 101.0, 151.0, 201.0, 301.0, 401.0, 501.0);
  $pm2_5_10_0_breakpoints_aqi_high_values =
    array(50.0, 100.0, 150.0, 200.0, 300.0, 400.0, 500.0, 999.0);

  // Compute pm2.5 AQI

  $pm2_5_truncated_aqi = $pm2_5_truncated_concentration_float = NULL;

  $out_humidity_float = -1.0;
  $got_valid_out_humidity = false;
  if (isset($input_pollutant_measurements['out_humidity'])) {
    $out_humidity_string =
      $input_pollutant_measurements['out_humidity'];
    if (isset($out_humidity_string)) {
      $out_humidity_float = (float) $out_humidity_string;
      if (($out_humidity_float >= 0.0) && ($out_humidity_float <= 100.0)) {
        $got_valid_out_humidity = true;
      }
    }
  }

  // Get the conversion type, if any
  if (isset($input_pollutant_measurements['pm2_5_pa_conversion_type'])) {
    $requested_conversion_type =
      $input_pollutant_measurements['pm2_5_pa_conversion_type'];
  } else {
    $requested_conversion_type = '';
  }

  $conversion_type_info =
    retrieve_pm2_5_aqi_conversion_type_info
    (array('conversion-name' => $requested_conversion_type));
  if ($conversion_type_info['result'] != 'OK') {
    return $conversion_type_info;
  }

  $requested_conversion_type = $conversion_type_info['conversion-name'];
  $requested_conversion_detail = $conversion_type_info['conversion-detail'];

  // Special case: If conversion type is pa-usepa, complain if a valid
  // humidity is not given.
  if (($requested_conversion_type == "pa-usepa") &&
      (!$got_valid_out_humidity)) {
    return array('result' => 'ERROR',
                 'detail' =>
                 'Requested pa-usepa PM2.5 value conversion, but no outside ' .
                 'humidity given. This creates an impossible situation. No ' .
                 'AQI can be calculated.');
  }

  $got_one_valid_pollutant = false;
  if (isset($input_pollutant_measurements['pm_2_5_realtime_single_sample'])) {
    $pm2_5_pollutant_concentration_string =
      $input_pollutant_measurements['pm_2_5_realtime_single_sample'];
    $got_one_valid_pollutant = true;
    $pm2_5_truncated_concentration_string =
      $pm2_5_pollutant_concentration_string;
    $decimal_point = strpos($pm2_5_truncated_concentration_string, '.');
    if ($decimal_point !== false) {
      $pm2_5_truncated_concentration_string =
        ($pm2_5_digits_after_decimal > 0) ?
        substr($pm2_5_truncated_concentration_string, 0,
               $decimal_point + 1 + $pm2_5_digits_after_decimal) :
        (($decimal_point <= 0) ? "0" :
         substr($pm2_5_truncated_concentration_string, 0,
                $decimal_point));
    }
    $pm2_5_truncated_concentration_float =
      (float) $pm2_5_truncated_concentration_string;

    // We now have the computed pm2_5 value. Now we have to modify it
    // based on the input conversion type.

    switch ($requested_conversion_type) {
    case 'pa-usepa':
      if ($pm2_5_truncated_concentration_float <= 250.0) {
        $pm2_5_truncated_concentration_float =
          (0.52 * $pm2_5_truncated_concentration_float) -
          (0.085 * $out_humidity_float) + 5.71;
        if ($pm2_5_truncated_concentration_float < 0.0) {
          $pm2_5_truncated_concentration_float = 0.0;
        }
      }
      break;
    case 'pa-woodsmoke':
      $pm2_5_truncated_concentration_float =
        (0.55 * $pm2_5_truncated_concentration_float) + 0.53;
      if ($pm2_5_truncated_concentration_float < 0.0) {
        $pm2_5_truncated_concentration_float = 0.0;
      }
      break;
    case 'pa-aqandu':
      $pm2_5_truncated_concentration_float =
        (0.778 * $pm2_5_truncated_concentration_float) + 2.65;
      if ($pm2_5_truncated_concentration_float < 0.0) {
        $pm2_5_truncated_concentration_float = 0.0;
      }
      break;
    case 'pa-lrapa':
      $pm2_5_truncated_concentration_float =
        (0.5 * $pm2_5_truncated_concentration_float) - 0.66;
      if ($pm2_5_truncated_concentration_float < 0.0) {
        $pm2_5_truncated_concentration_float = 0.0;
      }
      break;
    case 'pa-raw':
    default:
      break;
    }
    $pm2_5_offset = $pm2_5_10_0_breakpoints_array_length - 1;
    $i = 0;
    for (; $i < $pm2_5_10_0_breakpoints_array_length; $i++) {
      if ($pm2_5_truncated_concentration_float <=
          $pm2_5_breakpoints_concentration_high_values[$i]) {
        $pm2_5_offset = $i;
        break;
      }
    }

    $pm2_5_pollutant_aqi =
      ((($pm2_5_10_0_breakpoints_aqi_high_values[$pm2_5_offset] -
         $pm2_5_10_0_breakpoints_aqi_low_values[$pm2_5_offset]) /
        ($pm2_5_breakpoints_concentration_high_values[$pm2_5_offset] -
         $pm2_5_breakpoints_concentration_low_values[$pm2_5_offset])) *
       ($pm2_5_truncated_concentration_float -
        $pm2_5_breakpoints_concentration_low_values[$pm2_5_offset])) +
      $pm2_5_10_0_breakpoints_aqi_low_values[$pm2_5_offset];

    $pm2_5_truncated_aqi = round($pm2_5_pollutant_aqi);
  }

  // Compute pm10 AQI

  $pm10_0_truncated_aqi = $pm10_0_truncated_concentration_float = NULL;
  if (isset($input_pollutant_measurements['pm_10_0_realtime_single_sample'])) {
    $pm10_0_pollutant_concentration_string =
      $input_pollutant_measurements['pm_10_0_realtime_single_sample'];
    $got_one_valid_pollutant = true;
    $pm10_0_truncated_concentration_string =
      $pm10_0_pollutant_concentration_string;
    $decimal_point = strpos($pm10_0_truncated_concentration_string, '.');
    if ($decimal_point !== false) {
      $pm10_0_truncated_concentration_string =
        ($pm10_0_digits_after_decimal > 0) ?
        substr($pm10_0_truncated_concentration_string, 0,
               $decimal_point + 1 + $pm10_0_digits_after_decimal) :
        (($decimal_point <= 0) ? "0" :
         substr($pm10_0_truncated_concentration_string, 0,
                $decimal_point));
    }
    $pm10_0_truncated_concentration_float =
      (float) $pm10_0_truncated_concentration_string;

    $pm10_0_offset = $pm2_5_10_0_breakpoints_array_length - 1;
    $i = 0;
    for (; $i < $pm2_5_10_0_breakpoints_array_length; $i++) {
      if ($pm10_0_truncated_concentration_float <=
          $pm10_0_breakpoints_concentration_high_values[$i]) {
        $pm10_0_offset = $i;
        break;
      }
    }

    $pm10_0_pollutant_aqi =
      ((($pm2_5_10_0_breakpoints_aqi_high_values[$pm2_5_offset] -
         $pm2_5_10_0_breakpoints_aqi_low_values[$pm2_5_offset]) /
        ($pm10_0_breakpoints_concentration_high_values[$pm10_0_offset] -
         $pm10_0_breakpoints_concentration_low_values[$pm10_0_offset])) *
       ($pm10_0_truncated_concentration_float -
        $pm10_0_breakpoints_concentration_low_values[$pm10_0_offset])) +
      $pm2_5_10_0_breakpoints_aqi_low_values[$pm10_0_offset];

    $pm10_0_truncated_aqi = round($pm10_0_pollutant_aqi);
  }
  if ($got_one_valid_pollutant == false) {
    return array('result' => 'ERROR',
                 'detail' =>
                 'No pollutant measurement given as input, so no AQI can be ' .
                 'calculated.');
  }

  // Determine which AQI we will use - always use the maximum
  $truncated_aqi = max($pm2_5_truncated_aqi, $pm10_0_truncated_aqi);

  // Get display information for the chosen AQI, and each of the
  // constituant AQIs.

  $combined_aqi_display_result =
    compute_single_sample_us_epa_aqi_estimate_display_parameters
     ($truncated_aqi);

  $pm2_5_aqi_display_result =
    compute_single_sample_us_epa_aqi_estimate_display_parameters
      ($pm2_5_truncated_aqi);

  $pm10_0_aqi_display_result =
    compute_single_sample_us_epa_aqi_estimate_display_parameters
      ($pm10_0_truncated_aqi);

  return
    array(
      'result' => 'OK',
      'aqi-realtime-single-sample-us-epa' => $truncated_aqi,
      'aqi-pm2-5-realtime-single-sample-us-epa' => $pm2_5_truncated_aqi,
      'aqi-pm2-5-truncated-concentration-float' =>
      $pm2_5_truncated_concentration_float,
      'aqi-pm2-5-pa-conversion-type' => $requested_conversion_type,
      'aqi-pm2-5-pa-conversion-detail' => $requested_conversion_detail,
      'aqi-pm10-0-realtime-single-sample-us-epa' => $pm10_0_truncated_aqi,
      'aqi-pm10-0-truncated-concentration-float' =>
      $pm10_0_truncated_concentration_float,

      'aqi-realtime-single-sample-us-epa-display-category' =>
      $combined_aqi_display_result['aqi-display-category'],
      'aqi-realtime-single-sample-us-epa-display-category-alexa' =>
      $combined_aqi_display_result['aqi-display-category-alexa'],
      'aqi-realtime-single-sample-us-epa-display-detail' =>
      $combined_aqi_display_result['aqi-display-detail'],
      'aqi-realtime-single-sample-us-epa-display-color' =>
      $combined_aqi_display_result['aqi-display-color'],
      'aqi-realtime-single-sample-us-epa-display-blended-color' =>
      $combined_aqi_display_result['aqi-display-blended-color'],
      'aqi-realtime-single-sample-us-epa-display-text-color' =>
      $combined_aqi_display_result['aqi-display-text-color'],
      'aqi-realtime-single-sample-us-epa-display-text-blended-color' =>
      $combined_aqi_display_result['aqi-display-text-blended-color'],

      'aqi-pm2-5-realtime-single-sample-us-epa-display-category' =>
      $pm2_5_aqi_display_result['aqi-display-category'],
      'aqi-pm2-5-realtime-single-sample-us-epa-display-category-alexa' =>
      $pm2_5_aqi_display_result['aqi-display-category-alexa'],
      'aqi-pm2-5-realtime-single-sample-us-epa-display-detail' =>
      $pm2_5_aqi_display_result['aqi-display-detail'],
      'aqi-pm2-5-realtime-single-sample-us-epa-display-color' =>
      $pm2_5_aqi_display_result['aqi-display-color'],
      'aqi-pm2-5-realtime-single-sample-us-epa-display-blended-color' =>
      $pm2_5_aqi_display_result['aqi-display-blended-color'],
      'aqi-pm2-5-realtime-single-sample-us-epa-display-text-color' =>
      $pm2_5_aqi_display_result['aqi-display-text-color'],
      'aqi-pm2-5-realtime-single-sample-us-epa-display-text-blended-color' =>
      $pm2_5_aqi_display_result['aqi-display-text-blended-color'],

      'aqi-pm10-0-realtime-single-sample-us-epa-display-category' =>
      $pm10_0_aqi_display_result['aqi-display-category'],
      'aqi-pm10-0-realtime-single-sample-us-epa-display-category-alexa' =>
      $pm10_0_aqi_display_result['aqi-display-category-alexa'],
      'aqi-pm10-0-realtime-single-sample-us-epa-display-detail' =>
      $pm10_0_aqi_display_result['aqi-display-detail'],
      'aqi-pm10-0-realtime-single-sample-us-epa-display-color' =>
      $pm10_0_aqi_display_result['aqi-display-color'],
      'aqi-pm10-0-realtime-single-sample-us-epa-display-blended-color' =>
      $pm10_0_aqi_display_result['aqi-display-blended-color'],
      'aqi-pm10-0-realtime-single-sample-us-epa-display-text-color' =>
      $pm10_0_aqi_display_result['aqi-display-text-color'],
      'aqi-pm10-0-realtime-single-sample-us-epa-display-text-blended-color' =>
      $pm10_0_aqi_display_result['aqi-display-text-blended-color']);
}

?>

GNU Lesser General Public License v2.1, referenced by many packages

                  GNU LESSER GENERAL PUBLIC LICENSE
                       Version 2.1, February 1999

 Copyright (C) 1991, 1999 Free Software Foundation, Inc.
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

[This is the first released version of the Lesser GPL.  It also counts
 as the successor of the GNU Library Public License, version 2, hence
 the version number 2.1.]

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.

  This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it.  You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.

  When we speak of free software, we are referring to freedom of use,
not price.  Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.

  To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights.  These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.

  For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you.  You must make sure that they, too, receive or can get the source
code.  If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it.  And you must show them these terms so they know their rights.

  We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.

  To protect each distributor, we want to make it very clear that
there is no warranty for the free library.  Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.

  Finally, software patents pose a constant threat to the existence of
any free program.  We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder.  Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.

  Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License.  This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License.  We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.

  When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library.  The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom.  The Lesser General
Public License permits more lax criteria for linking other code with
the library.

  We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License.  It also provides other free software developers Less
of an advantage over competing non-free programs.  These disadvantages
are the reason we use the ordinary General Public License for many
libraries.  However, the Lesser license provides advantages in certain
special circumstances.

  For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard.  To achieve this, non-free programs must be
allowed to use the library.  A more frequent case is that a free
library does the same job as widely used non-free libraries.  In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.

  In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software.  For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.

  Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.

  The precise terms and conditions for copying, distribution and
modification follow.  Pay close attention to the difference between a
"work based on the library" and a "work that uses the library".  The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.

                  GNU LESSER GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".

  A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.

  The "Library", below, refers to any such software library or work
which has been distributed under these terms.  A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language.  (Hereinafter, translation is
included without limitation in the term "modification".)

  "Source code" for a work means the preferred form of the work for
making modifications to it.  For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.

  Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it).  Whether that is true depends on what the Library does
and what the program that uses the Library does.

  1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.

  You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.

  2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) The modified work must itself be a software library.

    b) You must cause the files modified to carry prominent notices
    stating that you changed the files and the date of any change.

    c) You must cause the whole of the work to be licensed at no
    charge to all third parties under the terms of this License.

    d) If a facility in the modified Library refers to a function or a
    table of data to be supplied by an application program that uses
    the facility, other than as an argument passed when the facility
    is invoked, then you must make a good faith effort to ensure that,
    in the event an application does not supply such function or
    table, the facility still operates, and performs whatever part of
    its purpose remains meaningful.

    (For example, a function in a library to compute square roots has
    a purpose that is entirely well-defined independent of the
    application.  Therefore, Subsection 2d requires that any
    application-supplied function or table used by this function must
    be optional: if the application does not supply it, the square
    root function must still compute square roots.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.

In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library.  To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License.  (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.)  Do not make any other change in
these notices.

  Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.

  This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.

  4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.

  If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.

  5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library".  Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.

  However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library".  The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.

  When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library.  The
threshold for this to be true is not precisely defined by law.

  If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work.  (Executables containing this object code plus portions of the
Library will still fall under Section 6.)

  Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.

  6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.

  You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License.  You must supply a copy of this License.  If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License.  Also, you must do one
of these things:

    a) Accompany the work with the complete corresponding
    machine-readable source code for the Library including whatever
    changes were used in the work (which must be distributed under
    Sections 1 and 2 above); and, if the work is an executable linked
    with the Library, with the complete machine-readable "work that
    uses the Library", as object code and/or source code, so that the
    user can modify the Library and then relink to produce a modified
    executable containing the modified Library.  (It is understood
    that the user who changes the contents of definitions files in the
    Library will not necessarily be able to recompile the application
    to use the modified definitions.)

    b) Use a suitable shared library mechanism for linking with the
    Library.  A suitable mechanism is one that (1) uses at run time a
    copy of the library already present on the user's computer system,
    rather than copying library functions into the executable, and (2)
    will operate properly with a modified version of the library, if
    the user installs one, as long as the modified version is
    interface-compatible with the version that the work was made with.

    c) Accompany the work with a written offer, valid for at
    least three years, to give the same user the materials
    specified in Subsection 6a, above, for a charge no more
    than the cost of performing this distribution.

    d) If distribution of the work is made by offering access to copy
    from a designated place, offer equivalent access to copy the above
    specified materials from the same place.

    e) Verify that the user has already received a copy of these
    materials or that you have already sent this user a copy.

  For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it.  However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.

  It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system.  Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.

  7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:

    a) Accompany the combined library with a copy of the same work
    based on the Library, uncombined with any other library
    facilities.  This must be distributed under the terms of the
    Sections above.

    b) Give prominent notice with the combined library of the fact
    that part of it is a work based on the Library, and explaining
    where to find the accompanying uncombined form of the same work.

  8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License.  Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License.  However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.

  9. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Library or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.

  10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.

  11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all.  For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.

If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded.  In such case, this License incorporates the limitation as if
written in the body of this License.

  13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.

Each version is given a distinguishing version number.  If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation.  If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.

  14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission.  For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this.  Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.

                            NO WARRANTY

  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.

                     END OF TERMS AND CONDITIONS

           How to Apply These Terms to Your New Libraries

  If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change.  You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).

  To apply these terms, attach the following notices to the library.  It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.

    <one line to give the library's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

Also add information on how to contact you by electronic and paper mail.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the
  library `Frob' (a library for tweaking knobs) written by James Random Hacker.

  <signature of Ty Coon>, 1 April 1990
  Ty Coon, President of Vice

That's all there is to it!