/*!
 * THIS VERSION CUSTOMISED FROM CUMULUS MX DEFAULT WEB SITE
 * Last Modified: 2023/09/02 17:15:14
 *
 * A starter gauges page for Cumulus and Weather Display, based
 * on the JavaScript SteelSeries gauges by Gerrit Grunwald.
 *
 * Created by Mark Crossley, July 2011
 *  see scriptVer below for latest release
 *
 * Released under GNU GENERAL PUBLIC LICENSE, Version 2, June 1991
 * See the enclosed License file
 *
 * File encoding = UTF-8
 *
 */

/* exported gauges */

/*!
* Tiny Pub/Sub - v0.7.0 - 2013-01-29
* https://github.com/cowboy/jquery-tiny-pubsub
* Copyright (c) 2013 "Cowboy" Ben Alman; Licensed MIT
*/
(function ($) {
    'use strict';
    var o = $({});
    $.subscribe = function () {o.on.apply(o, arguments);};
    $.unsubscribe = function () {o.off.apply(o, arguments);};
    $.publish = function () {o.trigger.apply(o, arguments);};
}(jQuery));

var gauges;
gauges = (function () {
    'use strict';
    console.log( "Gauges: dataPath - " + dataPath);
    var strings = LANG.EN,         // Set to your default language. Store all the strings in one object
        config = {
            // Script configuration parameters you may want to 'tweak'
            scriptVer          : '2.7.7',
            weatherProgram     : 0,                      // Set 0=Cumulus, 1=Weather Display, 2=VWS, 3=WeatherCat, 4=Meteobridge, 5=WView, 6=WeeWX, 7=WLCOM
            imgPathURL         : './images/',            // *** Change this to the relative path for your 'Trend' graph images
            oldGauges          : 'gauges.htm',           // *** Change this to the relative path for your 'old' gauges page.
            realtimeInterval   : 10,                     // *** Download data interval, set to your realtime data update interval in seconds
            longPoll           : false,                  // if enabled, use long polling and PHP generated data !!only enable if you understand how this is implemented!!
            gaugeMobileScaling : 0.85,                   // scaling factor to apply when displaying the gauges mobile devices, set to 1 to disable (default 0.85)
            graphUpdateTime    : 15,                     // period of pop-up data graph refresh, in minutes (default 15)
            stationTimeout     : 3,                      // period of no data change before we declare the station off-line, in minutes (default 3)
            pageUpdateLimit    : 0,                      // period after which the page stops automatically updating, in minutes (default 20),
                                                         // - set to 0 (zero) to disable this feature
            pageUpdatePswd     : 'its-me',               // password to over ride the page updates time-out, do not set to blank even if you do not use a password - http://<URL>&pageUpdate=its-me
            digitalFont        : false,                  // Font control for the gauges & timer
            digitalForecast    : false,                  // Font control for the status display, set this to false for languages that use accented characters in the forecasts
            showPopupData      : true,                   // Pop-up data displayed
            showPopupGraphs    : false,                  // If pop-up data is displayed, show the graphs?
            mobileShowGraphs   : false,                  // If false, on a mobile/narrow display, always disable the graphs
            showWindVariation  : true,                   // Show variation in wind direction over the last 10 minutes on the direction gauge
            showWindMetar      : false,                  // Show the METAR substring for wind speed/direction over the last 10 minutes on the direction gauge popup
            showIndoorTempHum  : true,                   // Show the indoor temperature/humidity options
            showCloudGauge     : true,                   // Display the Cloud Base gauge
            showUvGauge        : true,                   // Display the UV Index gauge
            showSolarGauge     : true,                   // Display the Solar gauge
            showSunshineLed    : true,                   // Show 'sun shining now' LED on solar gauge
            showRoseGauge      : true,                   // Show the optional Wind Rose gauge
            showRoseGaugeOdo   : true,                   // Show the optional Wind Rose gauge wind run Odometer
            showRoseOnDirGauge : true,                   // Show the rose data as sectors on the direction gauge
            showGaugeShadow    : true,                   // Show a drop shadow outside the gauges
            roundCloudbaseVal  : true,                   // Round the value shown on the cloud base gauge to make it easier to read
                                                         // The realtime files should be absolute paths, "/xxx.txt" refers to the public root of your web server
            realTimeUrlLongPoll: 'realtimegauges-longpoll.php',     // *** ALL Users: If using long polling, change to your location of the PHP long poll realtime file ***
                                                                    // *** the supplied file is for Cumulus only
            realTimeUrlCumulus : dataPath + 'realtimegauges.txt',     // *** Cumulus Users: Change to your location of the realtime file ***
            realTimeUrlWD      : 'customclientraw.txt',    // *** WD Users: Change to your location of the ccr file ***
            realTimeUrlVWS     : 'steelseriesVWSjson.php', // *** VWS Users: Change to your location of the JSON script generator ***
            realTimeUrlWC      : 'realtimegaugesWC.txt',   // *** WeatherCat Users: Change to your location of the JSON script generator ***
            realTimeUrlMB      : 'MBrealtimegauges.txt',   // *** Meteobridge Users: Change to the location of the JSON file
            realTimeUrlWView   : 'customclientraw.txt',    // *** WView Users: Change to your location of the customclientraw.txt file ***
            realTimeUrlWeewx   : 'gauge-data.txt',         // *** WeeWX Users: Change to your location of the gauge data file ***
            realTimeUrlWLCOM   : 'WLrealtimegauges.php',   // *** WLCOM Users: change to location of WLCOMtags.php file ***
            useCookies         : true,                   // Persistently store user preferences in a cookie?
            tipImages          : [],
            dashboardMode      : false,                  // Used by Cumulus MX dashboard - SET TO FALSE OTHERWISE
            dewDisplayType     : 'dew'                   // Initial 'scale' to display on the 'dew point' gauge.
                                                         // 'dew' - Dewpoint
                                                         // 'app' - Apparent temperature
                                                         // 'wnd' - Wind Chill
                                                         // 'feel' - Feels Like
                                                         // 'hea' - Heat Index
                                                         // 'hum' - Humidex
        },

        // Gauge global look'n'feel settings
        gaugeGlobals = {
            minMaxArea            : 'rgba(212,132,134,0.3)', // area sector for today's max/min. (red, green, blue, transparency)
            windAvgArea           : 'rgba(132,212,134,0.3)',
            windVariationSector   : 'rgba(120,200,120,0.7)', // only used when rose data is shown on direction gauge
            frameDesign           : steelseries.FrameDesign.TILTED_GRAY,
            background            : steelseries.BackgroundColor.BEIGE,
            foreground            : steelseries.ForegroundType.TYPE1,
            pointer               : steelseries.PointerType.TYPE8,
            pointerColour         : steelseries.ColorDef.RED,
            dirAvgPointer         : steelseries.PointerType.TYPE8,
            dirAvgPointerColour   : steelseries.ColorDef.BLUE,
            gaugeType             : steelseries.GaugeType.TYPE4,
            lcdColour             : steelseries.LcdColor.STANDARD,
            knob                  : steelseries.KnobType.STANDARD_KNOB,
            knobStyle             : steelseries.KnobStyle.SILVER,
            labelFormat           : steelseries.LabelNumberFormat.STANDARD,
            tickLabelOrientation  : steelseries.TickLabelOrientation.HORIZONTAL, // was .NORMAL up to v1.6.4
            rainUseSectionColours : false,                                       // Only one of these colour options should be true
            rainUseGradientColours: true,                                       // Set both to false to use the pointer colour
            tempTrendVisible      : true,
            pressureTrendVisible  : true,
            uvLcdDecimals         : 0,
            // sunshine threshold values
            sunshineThreshold     : 50,    // the value in W/m² above which we can consider the Sun to be shining, *if* the current value exceeds...
            sunshineThresholdPct  : 75,    // the percentage of theoretical solar irradiance above which we consider the Sun to be shining
            // default gauge ranges - before auto-scaling/ranging
            tempScaleDefMinC      : -20,
            tempScaleDefMaxC      : 40,
            tempScaleDefMinF      : 0,
            tempScaleDefMaxF      : 100,
            baroScaleDefMinhPa    : 990,
            baroScaleDefMaxhPa    : 1030,
            baroScaleDefMinkPa    : 99,
            baroScaleDefMaxkPa    : 103,
            baroScaleDefMininHg   : 29.2,
            baroScaleDefMaxinHg   : 30.4,
            windScaleDefMaxMph    : 20,
            windScaleDefMaxKts    : 20,
            windScaleDefMaxMs     : 10,
            windScaleDefMaxKmh    : 30,
            rainScaleDefMaxmm     : 10,
            rainScaleDefMaxIn     : 0.5,
            rainRateScaleDefMaxmm : 10,
            rainRateScaleDefMaxIn : 0.5,
            uvScaleDefMax         : 10,             // Northern Europe may be lower - max. value recorded in the UK is 8, so use a scale of 10 for UK
            solarGaugeScaleMax    : 1000,           // Max value to be shown on the solar gauge - theoretical max without atmosphere ~ 1374 W/m²
                                                    // - but Davis stations can read up to 1800, use 1000 for Northern Europe?
            cloudScaleDefMaxft    : 3000,
            cloudScaleDefMaxm     : 1000,
            shadowColour          : 'rgba(0, 0, 0, 0.3)'  // Colour to use for gauge shadows - default 30% transparent black
        },

        commonParams = {
            // Common parameters for all the SteelSeries gauges
            fullScaleDeflectionTime: 4,             // Bigger numbers (seconds) slow the gauge pointer movements more
            gaugeType              : gaugeGlobals.gaugeType,
            minValue               : 0,
            niceScale              : true,
            ledVisible             : false,
            frameDesign            : gaugeGlobals.frameDesign,
            backgroundColor        : gaugeGlobals.background,
            foregroundType         : gaugeGlobals.foreground,
            pointerType            : gaugeGlobals.pointer,
            pointerColor           : gaugeGlobals.pointerColour,
            knobType               : gaugeGlobals.knob,
            knobStyle              : gaugeGlobals.knobStyle,
            lcdColor               : gaugeGlobals.lcdColour,
            lcdDecimals            : 1,
            digitalFont            : config.digitalFont,
            tickLabelOrientation   : gaugeGlobals.tickLabelOrientation,
            labelNumberFormat      : gaugeGlobals.labelFormat
        },
        firstRun = true,            // Used to set-up units & scales etc
        userUnitsSet = false,       // Tracks if the display units have been set by a user preference
        data = {},                  // Stores all the values from realtime.txt
        tickTockInterval,           // The 1s clock interval timer

                                    // ajaxDelay, used by long polling, the delay between getting a response and queueing the next request, as the default PHP
                                    // script runtime timout is 30 seconds, we do not want the PHP task to run for more than 20 seconds. So queue the next
                                    // request half of the realtime interval, or 20 seconds before it is due, which ever is the larger.
        ajaxDelay = config.longPoll ? Math.max(config.realtimeInterval - 20, 0) : config.realtimeInterval,
        downloadTimer,              // Stores a reference to the ajax download setTimout() timer
        timestamp = 0,              // the timestamp of last data update on the server.
        jqXHR = null,               // handle to the jQuery web request
        displayUnits = null,        // Stores the display units cookie settings
        sampleDate,
        realtimeVer,                // minimum version of the realtime JSON file required
        programLink = [
            '<a href="https://cumuluswiki.wxforum.net/a/Software" target="_blank" rel="noopener">Cumulus</a>',
            '<a href="//www.weather-display.com/" target="_blank" rel="noopener">Weather Display</a>',
            '<a href="//www.ambientweather.com/virtualstation.html" target="_blank" rel="noopener">Virtual Weather Station</a>',
            '<a href="//trixology.com/weathercat/" target="_blank" rel="noopener">WeatherCat</a>',
            '<a href="//www.meteobridge.com/" target="_blank rel="noopener"">Meteobridge</a>',
            '<a href="//www.wviewweather.com/" target="_blank" rel="noopener">Wview</a>',
            '<a href="//www.weewx.com/" target="_blank" rel="noopener">weewx</a>',
            '<a href="//weatherlink.com/" target="_blank" rel="noopener">Weatherlink.com</a>'
        ],

        ledIndicator, statusScroller, statusTimer,

        gaugeTemp, gaugeDew, gaugeRain, gaugeRRate,
        gaugeHum, gaugeBaro, gaugeWind, gaugeDir,
        gaugeUV, gaugeSolar, gaugeCloud, gaugeRose,

        /* _imgBackground,    // Uncomment if using a background image on the gauges */

        // ==================================================================================================================
        // Nothing below this line needs to be modified for the gauges as supplied
        // - unless you really know what you are doing
        // - but remember, if you break it, it's up to you to fix it ;-)
        // ==================================================================================================================

        //
        // init() Called when the document is ready, pre-draws the Status Display then calls
        // the first Ajax fetch of realtimegauges.txt. First draw of the gauges now deferred until
        // the Ajax data is available as a 'speed up'.
        //
        init = function (dashboard) {
            // Cumulus, Weather Display, VWS, WeatherCat?
            switch (config.weatherProgram) {
            case 0:
                // Cumulus
                realtimeVer = 12;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlCumulus;
                // the trend images to be used for the pop-up data, used in conjunction with config.imgPathURL
                // by default this is configured for the Cumulus 'standard' web site
                // ** If you specify one image in a sub-array, then you MUST provide images for all the other sub-elements
                config.tipImgs = [                                  // config.tipImgs for Cumulus users using the 'default' weather site
                    ['temp.png', 'intemp.png'],                     // Temperature: outdoor, indoor
                    // Temperature: dewpoint, apparent, windChill, heatIndex, humidex
                    ['temp.png', 'temp.png', 'temp.png', 'temp.png', 'temp.png'],
                    'raint.png',                                    // Rainfall
                    'rain.png',                                     // Rainfall rate
                    ['hum.png', 'hum.png'],                         // Humidity: outdoor, indoor
                    'press.png',                                    // Pressure
                    'wind.png',                                     // Wind speed
                    'windd.png',                                    // Wind direction
                    (config.showUvGauge ? 'uv.png' : null),         // UV graph if UV sensor is present | =null if no UV sensor
                    (config.showSolarGauge ? 'solar.png' : null),   // Solar rad graph if Solar sensor is present | Solar =null if no Solar sensor
                    (config.showRoseGauge ? 'windd.png' : null),    // Wind direction if Rose is enabled | =null if Rose is disabled
                    (config.showCloudGauge ? 'press.png' : null)    // Pressure for cloud height | =null if Cloud Height is disabled
                ];
                break;
            case 1:
                // Weather Display
                realtimeVer = 14;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlWD;
                config.tipImgs = [                                      // config.tipImgs for Weather Display users with wxgraph
                    ['temp+hum_24hr.php', 'indoor_temp_24hr.php'],      // Temperature: outdoor, indoor
                    // Temperature: dewpnt, apparent, windChill, HeatIndx, humidex
                    ['temp+dew+hum_1hr.php', 'temp+dew+hum_1hr.php', 'temp+dew+hum_1hr.php', 'temp+dew+hum_1hr.php', 'temp+dew+hum_1hr.php'],
                    'rain_24hr.php',                                    // Rainfall
                    'rain_1hr.php',                                     // Rainfall rate
                    ['humidity_1hr.php', 'humidity_7days.php'],         // Humidity: outdoor, indoor
                    'baro_24hr.php',                                    // Pressure
                    'windgust_1hr.php',                                 // Wind speed
                    'winddir_24hr.php',                                 // Wind direction
                    (config.showUvGauge ? 'uv_24hr.php' : null),        // UV graph if UV sensor is present | =null if no UV sensor
                    (config.showSolarGauge ? 'solar_24hr.php' : null),  // Solar rad graph if Solar sensor is present | Solar =null if no Solar sensor
                    (config.showRoseGauge ? 'winddir_24hr.php' : null), // Wind direction if Rose is enabled | =null if Rose is disabled
                    (config.showCloudGauge ? 'baro_24hr.php' : null)    // Pressure for cloud height | =null if Cloud Height is disabled
                ];
                // WD useer generally use wxgraphs - tweak the CSS to accomadate the different aspect ratio
                $('.tipimg').css({
                    width : '360px',
                    height: '260px'});
                break;
            case 2:
                // WVS
                realtimeVer = 11;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlVWS;
                config.showRoseGauge = false; // no wind rose data from VWS
                config.showCloudGauge = false;
                config.tipImgs = [                                  // config.tipImgs for VWS users
                    ['vws742.jpg', 'vws741.jpg'],                   // Temperature: outdoor, indoor
                    // Temperature: dewpnt, apparent, windChill, HeatIndx, humidex
                    ['vws757.jpg', 'vws762.jpg', 'vws754.jpg', 'vws756.jpg', null],
                    'vws744.jpg',                                   // Rainfall
                    'vws859.jpg',                                   // Rainfall rate
                    ['vws740.jpg', 'vws739.jpg'],                   // Humidity: outdoor, indoor
                    'vws758.jpg',                                   // Pressure
                    'vws737.jpg',                                   // Wind speed
                    'vws736.jpg',                                   // Wind direction
                    (config.showUvGauge ? 'vws752.jpg' : null),     // UV graph if UV sensor is present | =null if no UV sensor
                    (config.showSolarGauge ? 'vws753.jpg' : null),  // Solar rad graph if Solar sensor is present | Solar =null if no Solar sensor
                    (config.showRoseGauge ? 'vws736.jpg' : null),   // Wind direction if Rose is enabled | =null if Rose is disabled
                    (config.showCloudGauge ? 'vws758.jpg' : null)   // Pressure for cloud height | =null if Cloud Height is disabled
                ];
                break;
            case 3:
                // WeatherCat
                realtimeVer = 14;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlWC;
                config.tipImgs = [                                       // config.tipImgs - WeatherCat users using the 'default' weather site
                    ['temperature1.jpg', 'tempin1.jpg'],                 // Temperature: outdoor, indoor
                    // Temperature: dewpoint, apparent, windChill, heatIndex, humidex
                    ['dewpoint1.jpg', 'temperature1.jpg', 'windchill1.jpg', 'heatindex1.jpg', 'temperature1.jpg'],
                    'precipitationc1.jpg',                               // Rainfall
                    'precipitation1.jpg',                                // Rainfall rate
                    ['rh1.jpg', 'rhin1.jpg'],                            // Humidity: outdoor, indoor
                    'pressure1.jpg',                                     // Pressure
                    'windspeed1.jpg',                                    // Wind speed
                    'winddirection1.jpg',                                // Wind direction
                    (config.showUvGauge ? 'uv1.jpg' : null),             // UV graph if UV sensor is present | =null if no UV sensor
                    (config.showSolarGauge ? 'solarrad1.jpg' : null),    // Solar rad graph if Solar sensor is present | Solar =null if no Solar sensor
                    (config.showRoseGauge ? 'winddirection1.jpg' : null), // Wind direction if Rose is enabled | =null if Rose is disabled
                    (config.showCloudGauge ? 'cloudbase1.jpg' : null)     // Pressure for cloud height | =null if Cloud Height is disabled
                ];
                break;
            case 4:
                // Meteobridge
                realtimeVer = 10;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlMB;
                config.showPopupGraphs = false;        // config.tipImgs - no Meteobridge images available
                config.showRoseGauge = false;          // no windrose data from MB
                config.showCloudGauge = false;
                config.tipImgs = null;                 // config.tipImgs - no Meteobridge images available
                config.showWindVariation = false;      // no wind variation data from MB
                break;
            case 5:
                // WView
                realtimeVer = 11;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlWView;
                config.showSunshineLed = false;     // WView does not provide the current theoretical solar max required to determine sunshine
                config.showWindVariation = false;   // no wind variation from WView
                config.showRoseGauge = false;       // No wind rose array from WView by default - there is a user supplied modification to enable it though.
                config.showCloudGauge = false;
                config.tipImgs = [                                      // config.tipImgs for WView users
                    ['tempdaycomp.png', 'indoor_temp.png'],             // Temperature: outdoor, indoor
                    // Temperature: dewpnt, apparent, windChill, HeatIndx, humidex
                    ['tempdaycomp.png', 'tempdaycomp.png', 'heatchillcomp.png', 'heatchillcomp.png', 'tempdaycomp.png'],
                    'rainday.png',                                      // Rainfall
                    'rainrate.png',                                     // Rainfall rate
                    ['humidday.png', 'humidday.png'],                   // Humidity: outdoor, indoor
                    'baromday.png',                                     // Pressure
                    'wspeeddaycomp.png',                                // Wind speed
                    'wdirday.png',                                      // Wind direction
                    (config.showUvGauge ? 'uvday.png' : null),          // UV graph if UV sensor is present | =null if no UV sensor
                    (config.showSolarGauge ? 'solarday.png' : null),    // Solar rad graph if Solar sensor is present | Solar =null if no Solar sensor
                    (config.showRoseGauge ? 'windroseday.png' : null),  // Wind direction if Rose is enabled | =null if Rose is disabled
                    (config.showCloudGauge ? 'baromday.png' : null)     // Pressure for cloud height | =null if Cloud Height is disabled
                ];
                break;
            case 6:
                // weewx
                realtimeVer = 14;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.longPoll ? config.realTimeUrlLongPoll : config.realTimeUrlWeewx;
                config.showSunshineLed = true;
                config.showWindVariation = true;
                config.tipImgs = [                                      // config.tipImgs for weewx
                    ['dayinouttemp.png', 'dayinouttemp.png'],           // Temperature: outdoor, indoor
                    // Temperature: dewpnt, apparent, windChill, HeatIndx, humidex
                    ['dayouttemphum.png', 'dayouttemphum.png', 'dayouttemphum.png', 'dayouttemphum.png', 'dayouttemphum.png'],
                    'dayrain.png',                                      // Rainfall
                    'dayrainrate.png',                                  // Rainfall rate
                    ['dayinouthum.png', 'dayinouthum.png'],             // Humidity: outdoor, indoor
                    'daybarometer.png',                                 // Pressure
                    'daywind.png',                                      // Wind speed
                    'daywinddir.png',                                   // Wind direction
                    (config.showUvGauge ? 'dayuv.png' : null),          // UV graph if UV sensor is present | =null if no UV sensor
                    (config.showSolarGauge ? 'dayradiation.png' : null), // Solar rad graph if Solar sensor is present | Solar =null if no Solar sensor
                    (config.showRoseGauge ? 'daywindvec.png' : null),    // Wind direction if Rose is enabled | =null if Rose is disabled
                    (config.showCloudGauge ? 'daybarometer.png' : null)  // Pressure for cloud height | =null if Cloud Height is disabled
                ];
                break;
            case 7:
                // WLCOM
                realtimeVer = 10;   // minimum version of the realtime JSON file required
                config.realTimeURL = config.realTimeUrlWLCOM;
                config.showPopupGraphs = false;        // config.tipImgs - no WL images available
                config.showRoseGauge = false;          // no windrose data from WL
                config.showCloudGauge = false;
                config.tipImgs = null;                 // config.tipImgs - no WL images available
                config.showWindVariation = false;      // no wind variation data from WL
                break;
            default:
                // Who knows!
                realtimeVer = 0;   // minimum version of the realtime JSON file required
                config.realtimeURL = null;
                config.showPopupGraphs = false;
                config.tipImgs = null;                     // config.tipImgs - unknown
            }

            // Are we running on a phone device (or really low res screen)?
            if ($(window).width() < 480) {
                // Change the gauge scaling
                config.gaugeScaling = config.gaugeMobileScaling;
                // Switch off graphs?
                config.showPopupGraphs = config.mobileShowGraphs;
            } else {
                config.gaugeScaling = 1;
            }

            // Logo images are used to 'personalise' the gauge backgrounds
            // To add a logo to a gauge, add the parameter:
            //    params.customLayer = _imgBackground;
            // to the corresponding drawXxxx() function below.
            //
            // These are for demo only, to add them remove the comments around the following lines, and
            // the _imgBackground definition line above...
            /*
            _imgBackground = document.createElement('img');                 // small logo
            $(_imgBackground).attr('src', config.imgPathURL + 'logoSmall.png');
            */
            // End of logo images

            // Get the display units the user last used when they visited before - if present
            displayUnits = getCookie('units');
            // Set 'units' radio buttons to match preferred units
            if (displayUnits !== null) {
                // User wants specific units
                userUnitsSet = true;

                // temperature
                setRadioCheck('rad_unitsTemp', displayUnits.temp);
                data.tempunit = '°' + displayUnits.temp;
                // rain
                setRadioCheck('rad_unitsRain', displayUnits.rain);
                data.rainunit = displayUnits.rain;
                // pressure
                setRadioCheck('rad_unitsPress', displayUnits.press);
                data.pressunit = displayUnits.press;
                // wind
                setRadioCheck('rad_unitsWind', displayUnits.wind);
                data.windunit = displayUnits.wind;
                displayUnits.windrun = getWindrunUnits(data.windunit);
                // cloud base
                setRadioCheck('rad_unitsCloud', displayUnits.cloud);
                data.cloudunit = displayUnits.cloud;
            } else {
                // Set the defaults to metric )
                // DO NOT CHANGE THESE - THE SCRIPT DEPENDS ON THESE DEFAULTS
                // The units actually displayed will be read from the realtime.txt file, or from the users last visit cookie
                displayUnits = {
                    temp   : 'C',
                    rain   : 'mm',
                    press  : 'hPa',
                    wind   : 'km/h',
                    windrun: 'km',
                    cloud  : 'm'
                };

                data.tempunit = '°C';
                data.rainunit = 'mm';
                data.pressunit = 'hPa';
                data.windunit = 'km/h';
                data.cloudunit = 'm';
            }

            // enable popup data
            if (config.showPopupData) {
                ddimgtooltip.showTips = config.showPopupData;
            }

            if (config.showPopupGraphs) {
                // Note the number of array elements must match 'i' in ddimgtooltip.tiparray()
                ddimgtooltip.tiparray[0][0] = (config.tipImgs[0][0] === null ? null : '');
                ddimgtooltip.tiparray[1][0] = (config.tipImgs[1][0] === null ? null : '');
                ddimgtooltip.tiparray[2][0] = (config.tipImgs[2]    === null ? null : '');
                ddimgtooltip.tiparray[3][0] = (config.tipImgs[3]    === null ? null : '');
                ddimgtooltip.tiparray[4][0] = (config.tipImgs[4][0] === null ? null : '');
                ddimgtooltip.tiparray[5][0] = (config.tipImgs[5]    === null ? null : '');
                ddimgtooltip.tiparray[6][0] = (config.tipImgs[6]    === null ? null : '');
                ddimgtooltip.tiparray[7][0] = (config.tipImgs[7]    === null ? null : '');
                ddimgtooltip.tiparray[8][0] = (config.tipImgs[8]    === null ? null : '');
                ddimgtooltip.tiparray[9][0] = (config.tipImgs[9]    === null ? null : '');
                ddimgtooltip.tiparray[10][0] = (config.tipImgs[10]  === null ? null : '');
                ddimgtooltip.tiparray[11][0] = (config.tipImgs[11]  === null ? null : '');
            }

            // draw the status gadgets first, they will display any errors in the initial set-up
            ledIndicator = singleLed.getInstance();
            statusScroller = singleStatus.getInstance();
            statusTimer = singleTimer.getInstance();

            gaugeTemp = singleTemp.getInstance();
            // Export gaugeTemp.update() so it can be called from the HTML code
            if (gaugeTemp) {gauges.doTemp = gaugeTemp.update;}

            gaugeDew = singleDew.getInstance();
            // Export gaugeDew.update() so it can be called from the HTML code
            if (gaugeDew) {gauges.doDew = gaugeDew.update;}

            gaugeHum = singleHum.getInstance();
            // Export gaugeHum.update() so it can be called from the HTML code
            if (gaugeHum) {gauges.doHum = gaugeHum.update;}

            gaugeBaro = singleBaro.getInstance();

            gaugeWind = singleWind.getInstance();
            gaugeDir = singleDir.getInstance();

            gaugeRain = singleRain.getInstance();
            gaugeRRate = singleRRate.getInstance();

            // remove the UV gauge?
            if (!config.showUvGauge) {
                $('#canvas_uv').parent().remove();
            } else {
                gaugeUV = singleUV.getInstance();
            }

            // remove the Solar gauge?
            if (!config.showSolarGauge) {
                $('#canvas_solar').parent().remove();
            } else {
                gaugeSolar = singleSolar.getInstance();
            }

            // remove the Wind Rose?
            if (!config.showRoseGauge) {
                $('#canvas_rose').parent().remove();
            } else {
                gaugeRose = singleRose.getInstance();
            }

            // remove the cloud base gauge?
            if (!config.showCloudGauge) {
                $('#canvas_cloud').parent().remove();
                // and remove cloudbase unit selection options
                $('#cloud').parent().remove();
            } else {
                gaugeCloud = singleCloudBase.getInstance();
            }

            // Set the language
            changeLang(strings, false);

            if (!dashboard) {
                // Go do get the data!
                getRealtime();

                // start a timer to update the status time
                tickTockInterval = setInterval(
                    function () {
                        $.publish('gauges.clockTick', null);
                    },
                    1000);

                // start a timer to stop the page updates after the timeout period
                if (config.pageUpdateLimit > 0 && getUrlParam('pageUpdate') !== config.pageUpdatePswd) {
                    setTimeout(pageTimeout, config.pageUpdateLimit * 60 * 1000);
                }
            }
        },

        //
        // singleXXXX functions define a singleton for each of the gauges
        //

        //
        // Singleton for the LED Indicator
        //
        singleLed = (function () {
            var instance;   // Stores a reference to the Singleton
            var led;        // Stores a reference to the SS LED

            function init() {
                // create led indicator
                if ($('#canvas_led').length) {
                    led = new steelseries.Led(
                        'canvas_led', {
                            ledColor: steelseries.LedColor.GREEN_LED,
                            size    : $('#canvas_led').width()
                        });

                    setTitle(strings.led_title);
                }

                function setTitle(newTitle) {
                    $('#canvas_led').attr('title', newTitle);
                }

                function setLedColor(newColour) {
                    if (led) {
                        led.setLedColor(newColour);
                    }
                }

                function setLedOnOff(onState) {
                    if (led) {
                        led.setLedOnOff(onState);
                    }
                }

                function blink(blinkState) {
                    if (led) {
                        led.blink(blinkState);
                    }
                }

                return {
                    setTitle   : setTitle,
                    setLedColor: setLedColor,
                    setLedOnOff: setLedOnOff,
                    blink      : blink
                };
            }

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Status Scroller
        //
        singleStatus = (function () {
            var instance;   // Stores a reference to the Singleton
            var scroller;   // Stores a reference to the SS scrolling display

            function init() {
                // create forecast display
                if ($('#canvas_status').length) {
                    scroller = new steelseries.DisplaySingle(
                        'canvas_status', {
                            width            : $('#canvas_status').width(),
                            height           : $('#canvas_status').height(),
                            lcdColor         : gaugeGlobals.lcdColour,
                            unitStringVisible: false,
                            value            : strings.statusStr,
                            digitalFont      : config.digitalForecast,
                            valuesNumeric    : false,
                            autoScroll       : true,
                            alwaysScroll     : true
                        });
                }

                function setValue(newTxt) {
                    if (scroller) {
                        scroller.setValue(newTxt);
                    }
                }

                return {setText: setValue};
            }

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Status Timer
        //
        singleTimer = (function () {
            var instance,   // Stores a reference to the Singleton
                lcd,        // Stores a reference to the SS LED
                count = 1;

            function init() {
                function tick() {
                    if (lcd) {
                        lcd.setValue(count);
                        count += config.longPoll ? 1 : -1;
                    }
                }

                function reset(val) {
                    count = val;
                }

                function setValue(newVal) {
                    if (lcd) {
                        lcd.setValue(newVal);
                    }
                }

                // create timer display
                if ($('#canvas_timer').length) {
                    lcd = new steelseries.DisplaySingle(
                        'canvas_timer', {
                            width            : $('#canvas_timer').width(),
                            height           : $('#canvas_timer').height(),
                            lcdColor         : gaugeGlobals.lcdColour,
                            lcdDecimals      : 0,
                            unitString       : strings.timer,
                            unitStringVisible: true,
                            digitalFont      : config.digitalFont,
                            value            : count
                        });
                    // subscribe to data updates
                    $.subscribe('gauges.clockTick', tick);
                }

                return {
                    reset   : reset,
                    setValue: setValue
                };
            }

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Temperature Gauge
        //
        singleTemp = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define temperature gauge start values
                cache.sections = createTempSections(true);
                cache.areas = [];
                cache.minValue = gaugeGlobals.tempScaleDefMinC;
                cache.maxValue = gaugeGlobals.tempScaleDefMaxC;
                cache.title = strings.temp_title_out;
                cache.value = gaugeGlobals.tempScaleDefMinC + 0.0001;
                cache.maxMinVisible = false;
                cache.selected = 'out';

                // create temperature radial gauge
                if ($('#canvas_temp').length) {
                    params.size = Math.ceil($('#canvas_temp').width() * config.gaugeScaling);
                    params.section = cache.sections;
                    params.area = cache.areas;
                    params.minValue = cache.minValue;
                    params.maxValue = cache.maxValue;
                    params.thresholdVisible = false;
                    params.minMeasuredValueVisible = cache.maxMinVisible;
                    params.maxMeasuredValueVisible = cache.maxMinVisible;
                    params.titleString = cache.title;
                    params.unitString = data.tempunit;
                    params.trendVisible = gaugeGlobals.tempTrendVisible;
                    // params.customLayer = _imgBackground;      // uncomment to add a background image - See Logo Images above

                    ssGauge = new steelseries.Radial('canvas_temp', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_temp').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_temp').css(gaugeShadow(params.size));
                    }

                    // remove indoor temperature/humidity options?
                    if (!config.showIndoorTempHum) {
                        $('#rad_temp1').remove();
                        $('#lab_temp1').remove();
                        $('#rad_temp2').remove();
                        $('#lab_temp2').remove();
                        $('#rad_hum1').remove();
                        $('#lab_hum1').remove();
                        $('#rad_hum2').remove();
                        $('#lab_hum2').remove();
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var sel = cache.selected;

                    // Argument length === 1 when called from radio input
                    // Argument length === 2 when called from event handler
                    if (arguments.length === 1) {
                        sel = arguments[0].value;
                    }

                    // if rad isn't specified, just use existing value
                    var t1, scaleStep, tip;

                    cache.minValue = data.tempunit[1] === 'C' ? gaugeGlobals.tempScaleDefMinC : gaugeGlobals.tempScaleDefMinF;
                    cache.maxValue = data.tempunit[1] === 'C' ? gaugeGlobals.tempScaleDefMaxC : gaugeGlobals.tempScaleDefMaxF;

                    if (sel === 'out') {
                        cache.low = extractDecimal(data.tempTL);
                        cache.high = extractDecimal(data.tempTH);
                        cache.lowScale = getMinTemp(cache.minValue);
                        cache.highScale = getMaxTemp(cache.maxValue);
                        cache.value = extractDecimal(data.temp);
                        cache.title = strings.temp_title_out;
                        cache.loc = strings.temp_out_info;
                        cache.trendVal = extractDecimal(data.temptrend);
                        cache.popupImg = 0;
                        if (gaugeGlobals.tempTrendVisible) {
                            t1 = tempTrend(+cache.trendVal, data.tempunit, false);
                            if (t1 === -9999) {
                                // trend value isn't currently available
                                cache.trend = steelseries.TrendState.OFF;
                            } else if (t1 > 0) {
                                cache.trend = steelseries.TrendState.UP;
                            } else if (t1 < 0) {
                                cache.trend = steelseries.TrendState.DOWN;
                            } else {
                                cache.trend = steelseries.TrendState.STEADY;
                            }
                        }
                    } else {
                        // Indoor
                        cache.title = strings.temp_title_in;
                        cache.loc = strings.temp_in_info;
                        cache.value = extractDecimal(data.intemp);
                        cache.popupImg = 1;
                        if (data.intempTL && data.intempTH) {
                            // Indoor - and Max/Min values supplied
                            cache.low = extractDecimal(data.intempTL);
                            cache.high = extractDecimal(data.intempTH);
                            cache.lowScale = getMinTemp(cache.minValue);
                            cache.highScale = getMaxTemp(cache.maxValue);
                        } else {
                            // Indoor - no Max/Min values supplied
                            cache.low = cache.value;
                            cache.lowScale = cache.value;
                            cache.high = cache.value;
                            cache.highScale = cache.value;
                        }
                        if (gaugeGlobals.tempTrendVisible) {
                            cache.trend = steelseries.TrendState.OFF;
                        }
                    }

                    // has the gauge type changed?
                    if (cache.selected !== sel) {
                        cache.selected = sel;
                        // Change gauge title
                        ssGauge.setTitleString(cache.title);
                        ssGauge.setMaxMeasuredValueVisible(cache.maxMinVisible);
                        ssGauge.setMinMeasuredValueVisible(cache.maxMinVisible);
                        if (config.showPopupGraphs && config.tipImgs[0][cache.popupImg] !== null) {
                            var cacheDefeat = '?' + $('#imgtip0_img').attr('src').split('?')[1];
                            $('#imgtip0_img').attr('src', config.imgPathURL + config.tipImgs[0][cache.popupImg] + cacheDefeat);
                        }
                    }

                    // auto scale the ranges
                    scaleStep = data.tempunit[1] === 'C' ? 10 : 20;
                    while (cache.lowScale < cache.minValue) {
                        cache.minValue -= scaleStep;
                        if (cache.highScale <= cache.maxValue - scaleStep) {
                            cache.maxValue -= scaleStep;
                        }
                    }
                    while (cache.highScale > cache.maxValue) {
                        cache.maxValue += scaleStep;
                        if (cache.minValue >= cache.minValue + scaleStep) {
                            cache.minValue += scaleStep;
                        }
                    }

                    if (cache.minValue !== ssGauge.getMinValue() || cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setMinValue(cache.minValue);
                        ssGauge.setMaxValue(cache.maxValue);
                        ssGauge.setValue(cache.minValue);
                    }
                    if (cache.selected === 'out') {
                        cache.areas = [steelseries.Section(+cache.low, +cache.high, gaugeGlobals.minMaxArea)];
                    } else if (data.intempTL && data.intempTH) {
                        // Indoor and min/max avaiable
                        cache.areas = [steelseries.Section(+cache.low, +cache.high, gaugeGlobals.minMaxArea)];
                    } else {
                        // Indoor no min/max avaiable
                        cache.areas = [];
                    }

                    if (gaugeGlobals.tempTrendVisible) {
                        ssGauge.setTrend(cache.trend);
                    }
                    ssGauge.setArea(cache.areas);
                    ssGauge.setValueAnimated(+cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        if (cache.selected === 'out') {
                            tip = '<h5>' + cache.loc + '</h5>' +
                                  '<div class="ows-titleFlex">' +
                                  '<span>' + strings.lowestF_info + ':</span><span>' + cache.low + data.tempunit + ' ' + strings.at + ' ' + data.TtempTL + '</span>' +
                                  '</div><div class="ows-titleFlex">' +
                                  '<span>' + strings.highestF_info + ':</span><span>' + cache.high + data.tempunit + ' ' + strings.at + ' ' + data.TtempTH + '</span>' + 
                                  '</div>';
                            if (cache.trendVal !== -9999) {
                                tip += '<div class="ows-titleFlex">' + 
                                       '<span>' + strings.temp_trend_info + ':</span><span>' + tempTrend(cache.trendVal, data.tempunit, true) + ' ' + cache.trendVal + data.tempunit + '/h</span>' + 
                                       '</div>';
                            }
                        } else if (data.TintempTL && data.TintempTH) {
                            // Indoor and min/max available
                            tip = '<h5>' + cache.loc + '</h5>' + 
                                   '<div class="ows-titleFlex"><span>' + strings.lowestF_info + ':</span><span>' + cache.low + data.tempunit + ' ' + strings.at + ' ' + data.TintempTL + '</span></div>' +
                                   '<div class="ows-titleFlex"><span>' + strings.highestF_info + ':</span><span>' + cache.high + data.tempunit + ' ' + strings.at + ' ' + data.TintempTH + '</span></div>';
                        } else {
                            // Indoor no min/max
                            tip = '<div class="ows-titleFlex"><span>' + cache.loc + ':</span><span>' + data.intemp + data.tempunit + '</span></div>';
                        }
                        $('#imgtip0_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[0][cache.popupImg] !== null) {
                        $('#imgtip0_img').attr('src', config.imgPathURL + config.tipImgs[0][cache.popupImg] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(), // End singleTemp()

        //
        // Singleton for the Dewpoint Gauge
        //
        singleDew = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);
                var tmp;

                // define dew point gauge start values
                cache.sections = createTempSections(true);
                cache.areas = [];
                cache.minValue = gaugeGlobals.tempScaleDefMinC;
                cache.maxValue = gaugeGlobals.tempScaleDefMaxC;
                cache.value = gaugeGlobals.tempScaleDefMinC + 0.0001;
                // Has the end user selected a preferred 'scale' before
                tmp = getCookie('dewGauge');
                cache.selected = tmp !== null ? tmp : config.dewDisplayType;
                setRadioCheck('rad_dew', cache.selected);
                switch (cache.selected) {
                case 'dew':
                    cache.title = strings.dew_title;
                    cache.popupImg = 0;
                    break;
                case 'app':
                    cache.title = strings.apptemp_title;
                    cache.popupImg = 1;
                    break;
                case 'feel':
                    cache.title = strings.feel_title;
                    cache.popupImg = 1;
                    break;
                case 'wnd':
                    cache.title = strings.chill_title;
                    cache.popupImg = 2;
                    break;
                case 'hea':
                    cache.title = strings.heat_title;
                    cache.popupImg = 3;
                    break;
                case 'hum':
                    cache.title = strings.humdx_title;
                    cache.popupImg = 4;
                // no default
                }
                cache.minMeasuredVisible = false;
                cache.maxMeasuredVisible = false;

                // create dew point radial gauge
                if ($('#canvas_dew').length) {
                    params.size = Math.ceil($('#canvas_dew').width() * config.gaugeScaling);
                    params.section = cache.sections;
                    params.area = cache.areas;
                    params.minValue = cache.minValue;
                    params.maxValue = cache.maxValue;
                    params.thresholdVisible = false;
                    params.titleString = cache.title;
                    params.unitString = data.tempunit;

                    ssGauge = new steelseries.Radial('canvas_dew', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_dew').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_dew').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    // if rad isn't specified, just use existing value
                    var sel = cache.selected;

                    // Argument length === 2 when called from event handler
                    if (arguments.length === 1) {
                        sel = arguments[0].value;
                        // save the choice in a cookie
                        setCookie('dewGauge', sel);
                    }

                    var tip, scaleStep;

                    cache.minValue = data.tempunit[1] === 'C' ? gaugeGlobals.tempScaleDefMinC : gaugeGlobals.tempScaleDefMinF;
                    cache.maxValue = data.tempunit[1] === 'C' ? gaugeGlobals.tempScaleDefMaxC : gaugeGlobals.tempScaleDefMaxF;

                    cache.lowScale = getMinTemp(cache.minValue);
                    cache.highScale = getMaxTemp(cache.maxValue);

                    switch (sel) {
                    case 'dew': // dew point
                        cache.low = extractDecimal(data.dewpointTL);
                        cache.high = extractDecimal(data.dewpointTH);
                        cache.value = extractDecimal(data.dew);
                        cache.areas = [steelseries.Section(+cache.low, +cache.high, gaugeGlobals.minMaxArea)];
                        cache.title = strings.dew_title;
                        cache.minMeasuredVisible = false;
                        cache.maxMeasuredVisible = false;
                        cache.popupImg = 0;
                        tip = '<h5>' + strings.dew_info + '</h5>' +
                              '<div class="ows-titleFlex">' +
                              '<span>' + strings.lowest_info + ':</span><span>' + cache.low + data.tempunit + ' ' + strings.at + ' ' + data.TdewpointTL + '</span></div>' + 
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.highest_info + ':</span><span>' + cache.high + data.tempunit + ' ' + strings.at + ' ' + data.TdewpointTH + '</span></div>';
                        break;
                    case 'app': // apparent temperature
                        cache.low = extractDecimal(data.apptempTL);
                        cache.high = extractDecimal(data.apptempTH);
                        cache.value = extractDecimal(data.apptemp);
                        cache.areas = [steelseries.Section(+cache.low, +cache.high, gaugeGlobals.minMaxArea)];
                        cache.title = strings.apptemp_title;
                        cache.minMeasuredVisible = false;
                        cache.maxMeasuredVisible = false;
                        cache.popupImg = 1;
                        tip = tip = '<h5>' + strings.apptemp_info + '</h5>' +
                             '<div class="ows-titleFlex>' +
                             '<span>' + strings.lowestF_info + ':</span><span> ' + cache.low + data.tempunit + ' ' + strings.at + ' ' + data.TapptempTL + '</span>' +
                             '</div><div class="ows-titleFlex><span>' + strings.highestF_info + ':</span><span>' + cache.high + data.tempunit + ' ' + strings.at + ' ' + data.TapptempTH +
                             '</span></div>';
                        break;
                    case 'feel': // feels like
                        cache.low = extractDecimal(data.feelslikeTL);
                        cache.high = extractDecimal(data.feelslikeTH);
                        cache.value = extractDecimal(data.feelslike);
                        cache.areas = [steelseries.Section(+cache.low, +cache.high, gaugeGlobals.minMaxArea)];
                        cache.title = strings.feel_title;
                        cache.minMeasuredVisible = false;
                        cache.maxMeasuredVisible = false;
                        cache.popupImg = 1;
                        //tip = tip = strings.feel_info + ':' +
                        tip = '<h5>' + strings.feel_info + '</h5>' +
                              '<div class="ows-titleFlex">' +
                              '<span>' + strings.lowestF_info + ':</span><span>' + cache.low + data.tempunit + ' ' + strings.at + ' ' + data.TfeelslikeTL + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.highestF_info + ':</span><span>' + cache.high + data.tempunit + ' ' + strings.at + ' ' + data.TfeelslikeTH + '</span>' + 
                              '</div>';
                        break;
                    case 'wnd': // wind chill
                        cache.low = extractDecimal(data.wchillTL);
                        cache.high = extractDecimal(data.wchill);
                        cache.value = extractDecimal(data.wchill);
                        cache.areas = [];
                        cache.title = strings.chill_title;
                        cache.minMeasuredVisible = true;
                        cache.maxMeasuredVisible = false;
                        cache.popupImg = 2;
                        tip = '<h5>' + strings.chill_info + '</h5>' +
                              '<div class="ows-titleFlex">' +
                              '<span>' + strings.lowest_info + ':</span><span>' + cache.low + data.tempunit + ' ' + strings.at + ' ' + data.TwchillTL + 
                              '</span></div>';
                        break;
                    case 'hea': // heat index
                        cache.low = extractDecimal(data.heatindex);
                        cache.high = extractDecimal(data.heatindexTH);
                        cache.value = extractDecimal(data.heatindex);
                        cache.areas = [];
                        cache.title = strings.heat_title;
                        cache.minMeasuredVisible = false;
                        cache.maxMeasuredVisible = true;
                        cache.popupImg = 3;
                        tip = '<h5>' + strings.heat_info + '</h5>' +
                              '<div class="ows-titleFlex">' +
                              '<span>' + strings.highest_info + ':</span>' + 
                              '<span>' + cache.high + data.tempunit + ' ' + strings.at + ' ' + data.TheatindexTH + '</span>' +
                              '</div>';
                        break;
                    case 'hum': // humidex
                        cache.low = extractDecimal(data.humidex);
                        cache.high = extractDecimal(data.humidex);
                        cache.value = extractDecimal(data.humidex);
                        cache.areas = [];
                        cache.title = strings.humdx_title;
                        cache.minMeasuredVisible = false;
                        cache.maxMeasuredVisible = false;
                        cache.popupImg = 4;
                        tip = '<h5>' + strings.humdx_info + '</h5>' +
                              '<div class="ows-titleFlex">' + 
                              '<span>Temperature:</span><span>' + cache.value + data.tempunit + '</span></div>';
                        break;
                    // no default
                    }

                    if (cache.selected !== sel) {
                        cache.selected = sel;
                        // change gauge title
                        ssGauge.setTitleString(cache.title);
                        // and graph image
                        if (config.showPopupGraphs && config.tipImgs[1][cache.popupImg] !== null) {
                            var cacheDefeat = '?' + $('#imgtip1_img').attr('src').split('?')[1];
                            $('#imgtip1_img').attr('src', config.imgPathURL + config.tipImgs[1][cache.popupImg] + cacheDefeat);
                        }
                    }

                    // auto scale the ranges
                    scaleStep = data.tempunit[1] === 'C' ? 10 : 20;
                    while (cache.lowScale < cache.minValue) {
                        cache.minValue -= scaleStep;
                        if (cache.highScale <= cache.maxValue - scaleStep) {
                            cache.maxValue -= scaleStep;
                        }
                    }
                    while (cache.highScale > cache.maxValue) {
                        cache.maxValue += scaleStep;
                        if (cache.minValue >= cache.minValue + scaleStep) {
                            cache.minValue += scaleStep;
                        }
                    }

                    if (cache.minValue !== ssGauge.getMinValue() || cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setMinValue(cache.minValue);
                        ssGauge.setMaxValue(cache.maxValue);
                        ssGauge.setValue(cache.minValue);
                    }
                    ssGauge.setMinMeasuredValueVisible(cache.minMeasuredVisible);
                    ssGauge.setMaxMeasuredValueVisible(cache.maxMeasuredVisible);
                    ssGauge.setMinMeasuredValue(+cache.low);
                    ssGauge.setMaxMeasuredValue(+cache.high);
                    ssGauge.setArea(cache.areas);
                    ssGauge.setValueAnimated(+cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        $('#imgtip1_txt').html(tip);
                    }
                }

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[1][cache.popupImg] !== null) {
                        $('#imgtip1_img').attr('src', config.imgPathURL + config.tipImgs[1][cache.popupImg] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(), // End of singleDew()

        //
        // Singleton for the Rainfall Gauge
        //
        singleRain = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define rain gauge start values
                cache.maxValue = gaugeGlobals.rainScaleDefMaxmm;
                cache.value = 0.0001;
                cache.title = strings.rain_title;
                cache.lcdDecimals = 1;
                cache.scaleDecimals = 1;
                cache.labelNumberFormat = gaugeGlobals.labelFormat;
                cache.sections = (gaugeGlobals.rainUseSectionColours ? createRainfallSections(true) : []);
                cache.valGrad = (gaugeGlobals.rainUseGradientColours ? createRainfallGradient(true) : null);

                // create rain radial bargraph gauge
                if ($('#canvas_rain').length) {
                    params.size = Math.ceil($('#canvas_rain').width() * config.gaugeScaling);
                    params.maxValue = cache.maxValue;
                    params.thresholdVisible = false;
                    params.titleString = cache.title;
                    params.unitString = data.rainunit;
                    params.valueColor = steelseries.ColorDef.BLUE;
                    params.valueGradient = cache.valGrad;
                    params.useValueGradient = gaugeGlobals.rainUseGradientColours;
                    params.useSectionColors = gaugeGlobals.rainUseSectionColour;
                    params.useSectionColors = gaugeGlobals.rainUseSectionColours;
                    params.labelNumberFormat = cache.labelNumberFormat;
                    params.fractionalScaleDecimals = cache.scaleDecimals;
                    params.niceScale = false;

                    ssGauge = new steelseries.RadialBargraph('canvas_rain', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_rain').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_rain').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    cache.value = extractDecimal(data.rfall);
                    if (data.rainunit === 'mm') { // 10, 20, 30...
                        cache.maxValue = Math.max(nextHighest(cache.value, 10), gaugeGlobals.rainScaleDefMaxmm);
                    } else {
                        // inches 0.5, 1.0, 2.0, 3.0 ... 10.0, 12.0, 14.0
                        if (cache.value <= 1) {
                            cache.maxValue = Math.max(nextHighest(cache.value, 0.5), gaugeGlobals.rainScaleDefMaxIn);
                        } else if (cache.value <= 6) {
                            cache.maxValue = Math.max(nextHighest(cache.value, 1), gaugeGlobals.rainScaleDefMaxIn);
                        } else {
                            cache.maxValue = Math.max(nextHighest(cache.value, 2), gaugeGlobals.rainScaleDefMaxIn);
                        }
                        cache.scaleDecimals = cache.maxValue < 1 ? 2 : 1;
                    }

                    if (cache.maxValue !== ssGauge.getMaxValue()) {
                        // Gauge scale is too low, increase it.
                        // First set the pointer back to zero so we get a nice animation
                        ssGauge.setValue(0);
                        // and redraw the gauge with the new scale
                        ssGauge.setFractionalScaleDecimals(cache.scaleDecimals);
                        ssGauge.setMaxValue(cache.maxValue);
                    }
                    ssGauge.setValueAnimated(cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        $('#imgtip2_txt').html('<div class="ows-titleFlex"><span>' + strings.LastRain_info + ':</span><span>' + data.LastRained + '</span></div>');
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[2] !== null) {
                        $('#imgtip2_img').attr('src', config.imgPathURL + config.tipImgs[2] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Rainfall Rate Gauge
        //
        singleRRate = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define rain rate gauge start values
                cache.maxMeasured = 0;
                cache.maxValue = gaugeGlobals.rainRateScaleDefMaxmm;
                cache.value = 0.0001;
                cache.title = strings.rrate_title;
                cache.lcdDecimals = 1;
                cache.scaleDecimals = 0;
                cache.labelNumberFormat = gaugeGlobals.labelFormat;
                cache.sections = createRainRateSections(true);

                // create rain rate radial gauge
                if ($('#canvas_rrate').length) {
                    params.size = Math.ceil($('#canvas_rrate').width() * config.gaugeScaling);
                    params.section = cache.sections;
                    params.maxValue = cache.maxValue;
                    params.thresholdVisible = false;
                    params.maxMeasuredValueVisible = true;
                    params.titleString = cache.title;
                    params.unitString = data.rainunit + '/h';
                    params.lcdDecimals = cache.lcdDecimals;
                    params.labelNumberFormat = cache.labelNumberFormat;
                    params.fractionalScaleDecimals = cache.scaleDecimals;
                    params.niceScale = false;

                    ssGauge = new steelseries.Radial('canvas_rrate', params);
                    ssGauge.setMaxMeasuredValue(cache.maxMeasured);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_rrate').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_rrate').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var tip;

                    cache.value = extractDecimal(data.rrate);
                    cache.maxMeasured = extractDecimal(data.rrateTM);
                    cache.overallMax = Math.max(cache.maxMeasured, cache.value);  // workaround for VWS bug, not supplying correct max value today

                    if (data.rainunit === 'mm') { // 10, 20, 30...
                        cache.maxValue = nextHighest(cache.overallMax, 10);
                    } else {
                        // inches 0.5, 1.0, 2.0, 3.0 ... 10, 20, 30...
                        if (cache.overallMax <= 0.5) {
                            cache.maxValue = 0.5;
                        } else if (cache.overallMax <= 10) {
                            cache.maxValue = nextHighest(cache.overallMax, 1);
                        } else {
                            cache.maxValue = nextHighest(cache.overallMax, 10);
                        }
                        cache.scaleDecimals = cache.maxValue < 1 ? 2 : (cache.maxValue < 7 ? 1 : 0);
                    }

                    if (cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setValue(0);
                        ssGauge.setFractionalScaleDecimals(cache.scaleDecimals);
                        ssGauge.setMaxValue(cache.maxValue);
                    }

                    ssGauge.setValueAnimated(cache.value);
                    ssGauge.setMaxMeasuredValue(cache.maxMeasured);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        tip = '<h5>' + strings.rrate_info + '</h5>' +
                              '<div class="ows-titleFlex">' + 
                              '<span>' + strings.maximum_info + ':</span>' + 
                              '<span>' + data.rrateTM + ' ' + data.rainunit + '/h ' + strings.at + ' ' + data.TrrateTM + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.max_hour_info + ':</span>' +
                              '<span>' + extractDecimal(data.hourlyrainTH) + ' ' + data.rainunit + ' ' + strings.at + ' ' + data.ThourlyrainTH + '</span>' +
                              '</div>';
                        $('#imgtip3_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[3] !== null) {
                        $('#imgtip3_img').attr('src', config.imgPathURL + config.tipImgs[3] + cacheDefeat);
                    }
                }
                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Humidity Gauge
        //
        singleHum = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define humidity gauge start values
                cache.areas = [];
                cache.value = 0.0001;
                cache.title = strings.hum_title_out;
                cache.selected = 'out';

                // create humidity radial gauge
                if ($('#canvas_hum').length) {
                    params.size = Math.ceil($('#canvas_hum').width() * config.gaugeScaling);
                    params.section = [
                        steelseries.Section(0, 20, 'rgba(255,255,0,0.3)'),
                        steelseries.Section(20, 80, 'rgba(0,255,0,0.3)'),
                        steelseries.Section(80, 100, 'rgba(255,0,0,0.3)')
                    ];
                    params.area = cache.areas;
                    params.maxValue = 100;
                    params.thresholdVisible = false;
                    params.titleString = cache.title;
                    params.unitString = 'RH%';

                    ssGauge = new steelseries.Radial('canvas_hum', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_hum').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_hum').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var radio;
                    // Argument length === 2 when called from event handler
                    if (arguments.length === 1) {
                        radio = arguments[0];
                    }

                    // if rad isn't specified, just use existing value
                    var sel = (typeof radio === 'undefined' ? cache.selected : radio.value),
                        tip;

                    if (sel === 'out') {
                        cache.value = extractDecimal(data.hum);
                        cache.areas = [steelseries.Section(+extractDecimal(data.humTL), +extractDecimal(data.humTH), gaugeGlobals.minMaxArea)];
                        cache.title = strings.hum_title_out;
                        cache.popupImg = 0;
                    } else {
                        cache.value = extractDecimal(data.inhum);
                        if (data.inhumTL && data.inhumTH) {
                            cache.areas = [steelseries.Section(+extractDecimal(data.inhumTL), +extractDecimal(data.inhumTH), gaugeGlobals.minMaxArea)];
                        } else {
                            cache.areas = [];
                        }
                        cache.title = strings.hum_title_in;
                        cache.popupImg = 1;
                    }

                    if (cache.selected !== sel) {
                        cache.selected = sel;
                        // Change gauge title
                        ssGauge.setTitleString(cache.title);
                        if (config.showPopupGraphs && config.tipImgs[4][cache.popupImg] !== null) {
                            var cacheDefeat = '?' + $('#imgtip4_img').attr('src').split('?')[1];
                            $('#imgtip4_img').attr('src', config.imgPathURL + config.tipImgs[4][cache.popupImg] + cacheDefeat);
                        }
                    }

                    ssGauge.setArea(cache.areas);
                    ssGauge.setValueAnimated(cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        if (cache.selected === 'out') {
                            tip = '<h5>' + strings.hum_out_info + '</h5>' +
                                  '<div class="ows-titleFlex">' +
                                  '<span>' + strings.minimum_info + ':</span>' +
                                  '<span>' + extractDecimal(data.humTL) + '% ' + strings.at + ' ' + data.ThumTL + '</span>' +
                                  '</div><div class="ows-titleFlex">' + 
                                  '<span>' + strings.maximum_info + ':</span>' +
                                  '<span>' + extractDecimal(data.humTH) + '% ' + strings.at + ' ' + data.ThumTH + '</span></div>';
                        } else if (data.inhumTL && data.inhumTH) {
                            // we have indoor high/low data
                            tip = '<h5>' + strings.hum_in_info + '</h5>' +
                                  '<div class="ows-titleFlex">' +
                                  '<span>' + strings.minimum_info + ':</span><span>' + extractDecimal(data.inhumTL) + '% ' + strings.at + ' ' + data.TinhumTL + '</span>' +
                                  '</div><div class="ows-titleFlex">' +
                                  '<span>' + strings.maximum_info + ':</span><span>' + extractDecimal(data.inhumTH) + '% ' + strings.at + ' ' + data.TinhumTH + '</span></div>';
                        } else {
                            // no indoor high/low data
                            tip = '<div class="ows-titleFlex"><span>' + strings.hum_in_info + ':</span><span>' + extractDecimal(data.inhum) + '%</span></div>';
                        }
                        $('#imgtip4_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[4][cache.popupImg] !== null) {
                        $('#imgtip4_img').attr('src', config.imgPathURL + config.tipImgs[4][cache.popupImg] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Barometer Gauge
        //
        singleBaro = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define pressure/barometer gauge start values
                cache.sections = [];
                cache.areas = [];
                cache.minValue = gaugeGlobals.baroScaleDefMinhPa;
                cache.maxValue = gaugeGlobals.baroScaleDefMaxhPa;
                cache.value = cache.minValue + 0.0001;
                cache.title = strings.baro_title;
                cache.lcdDecimals = 1;
                cache.scaleDecimals = 0;
                cache.labelNumberFormat = gaugeGlobals.labelFormat;

                // create pressure/barometric radial gauge
                if ($('#canvas_baro').length) {
                    params.size = Math.ceil($('#canvas_baro').width() * config.gaugeScaling);
                    params.section = cache.sections;
                    params.area = cache.areas;
                    params.minValue = cache.minValue;
                    params.maxValue = cache.maxValue;
                    params.niceScale = false;
                    params.thresholdVisible = false;
                    params.titleString = cache.title;
                    params.unitString = data.pressunit;
                    params.lcdDecimals = cache.lcdDecimals;
                    params.trendVisible = gaugeGlobals.pressureTrendVisible;
                    params.labelNumberFormat = cache.labelNumberFormat;
                    params.fractionalScaleDecimals = cache.scaleDecimals;

                    ssGauge = new steelseries.Radial('canvas_baro', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_baro').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_baro').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var tip, t1, dps;

                    cache.recLow = +extractDecimal(data.pressL);
                    cache.recHigh = +extractDecimal(data.pressH);
                    cache.todayLow = +extractDecimal(data.pressTL);
                    cache.todayHigh = +extractDecimal(data.pressTH);
                    cache.value = +extractDecimal(data.press);
                    // Convert the WD change over 3 hours to an hourly rate
                    cache.trendVal = +extractDecimal(data.presstrendval) / (config.weatherProgram === 2 ? 3 : 1);

                    if (data.pressunit === 'hPa' || data.pressunit === 'mb') {
                        //  default min range 990-1030 - steps of 10 hPa
                        cache.minValue = Math.min(nextLowest(cache.recLow - 2, 10), gaugeGlobals.baroScaleDefMinhPa);
                        cache.maxValue = Math.max(nextHighest(cache.recHigh + 2, 10), gaugeGlobals.baroScaleDefMaxhPa);
                        dps = 1; // 1 decimal place
                    } else if (data.pressunit === 'kPa') {
                        //  default min range 99-105 - steps of 1 kPa
                        cache.minValue = Math.min(nextLowest(cache.recLow - 0.2, 1), gaugeGlobals.baroScaleDefMinkPa);
                        cache.maxValue = Math.max(nextHighest(cache.recHigh + 0.2, 1), gaugeGlobals.baroScaleDefMaxkPa);
                        dps = 2;
                    } else {
                        // inHg: default min range 29.5-30.5 - steps of 0.5 inHg
                        cache.minValue = Math.min(nextLowest(cache.recLow - 0.1, 0.5), gaugeGlobals.baroScaleDefMininHg);
                        cache.maxValue = Math.max(nextHighest(cache.recHigh + 0.1, 0.5), gaugeGlobals.baroScaleDefMaxinHg);
                        dps = 3;
                    }
                    cache.trendValRnd = cache.trendVal.toFixed(dps);
                    cache.todayLowRnd = cache.todayLow.toFixed(dps);
                    cache.todayHighRnd = cache.todayHigh.toFixed(dps);

                    if (cache.minValue !== ssGauge.getMinValue() || cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setMinValue(cache.minValue);
                        ssGauge.setMaxValue(cache.maxValue);
                        ssGauge.setValue(cache.minValue);
                    }
                    if (cache.recHigh === cache.todayHigh && cache.recLow === cache.todayLow) {
                        // VWS does not provide record hi/lo values
                        cache.sections = [];
                        cache.areas = [steelseries.Section(cache.todayLow, cache.todayHigh, gaugeGlobals.minMaxArea)];
                    } else {
                        cache.sections = [
                            steelseries.Section(cache.minValue, cache.recLow, 'rgba(255,0,0,0.5)'),
                            steelseries.Section(cache.recHigh, cache.maxValue, 'rgba(255,0,0,0.5)')
                        ];
                        cache.areas = [
                            steelseries.Section(cache.minValue, cache.recLow, 'rgba(255,0,0,0.5)'),
                            steelseries.Section(cache.recHigh, cache.maxValue, 'rgba(255,0,0,0.5)'),
                            steelseries.Section(cache.todayLow, cache.todayHigh, gaugeGlobals.minMaxArea)
                        ];
                    }

                    if (gaugeGlobals.pressureTrendVisible) {
                        // Use the baroTrend rather than simple arithmetic test - steady is more/less than zero!
                        t1 = baroTrend(cache.trendVal, data.pressunit, false);
                        if (t1 === -9999) {
                            // trend value isn't currently available
                            cache.trend = steelseries.TrendState.OFF;
                        } else if (t1 > 0) {
                            cache.trend = steelseries.TrendState.UP;
                        } else if (t1 < 0) {
                            cache.trend = steelseries.TrendState.DOWN;
                        } else {
                            cache.trend = steelseries.TrendState.STEADY;
                        }
                        ssGauge.setTrend(cache.trend);
                    }

                    ssGauge.setArea(cache.areas);
                    ssGauge.setSection(cache.sections);
                    ssGauge.setValueAnimated(cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        tip = '<h5>' + strings.baro_info + '</h5>' +
                              '<div class="ows-titleFlex">' +
                              '<span>' + strings.minimum_info + ':</span><span>' + cache.todayLowRnd + ' ' + data.pressunit + ' ' + strings.at + ' ' + data.TpressTL + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.maximum_info + ':</span><span>' + cache.todayHighRnd + ' ' + data.pressunit + ' ' + strings.at + ' ' + data.TpressTH + '</span>' +
                              '</div>';
                        if (cache.trendVal !== -9999) {
                            tip += '<div class="ows-titleFlex">' +
                                   '<span>' + strings.baro_trend_info + ':</span><span>' + baroTrend(cache.trendVal, data.pressunit, true) + ' ' +
                                (cache.trendValRnd > 0 ? '+' : '') + cache.trendValRnd + ' ' + data.pressunit + '/h</span></div>';
                        }
                        $('#imgtip5_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[5] !== null) {
                        $('#imgtip5_img').attr('src', config.imgPathURL + config.tipImgs[5] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Wind Speed Gauge
        //
        singleWind = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define wind gauge start values
                cache.maxValue = gaugeGlobals.windScaleDefMaxKph;
                cache.areas = [];
                cache.maxMeasured = 0;
                cache.value = 0.0001;
                cache.title = strings.wind_title;

                // create wind speed radial gauge
                if ($('#canvas_wind').length) {
                    params.size = Math.ceil($('#canvas_wind').width() * config.gaugeScaling);
                    params.area = cache.areas;
                    params.maxValue = cache.maxValue;
                    params.niceScale = false;
                    params.thresholdVisible = false;
                    params.maxMeasuredValueVisible = true;
                    params.titleString = cache.title;
                    params.unitString = data.windunit;

                    ssGauge = new steelseries.Radial('canvas_wind', params);
                    ssGauge.setMaxMeasuredValue(cache.maxMeasured);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_wind').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_wind').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var tip;

                    cache.value = extractDecimal(data.wlatest);
                    cache.average = extractDecimal(data.wspeed);
                    cache.gust = extractDecimal(data.wgust);
                    cache.maxGustToday = extractDecimal(data.wgustTM);
                    cache.maxAvgToday = extractDecimal(data.windTM);

                    switch (data.windunit) {
                    case 'mph':
                    case 'kts':
                        cache.maxValue = Math.max(nextHighest(cache.maxGustToday, 10), gaugeGlobals.windScaleDefMaxMph);
                        break;
                    case 'm/s':
                        cache.maxValue = Math.max(nextHighest(cache.maxGustToday, 5), gaugeGlobals.windScaleDefMaxMs);
                        break;
                    default:
                        cache.maxValue = Math.max(nextHighest(cache.maxGustToday, 20), gaugeGlobals.windScaleDefMaxKmh);
                    }
                    cache.areas = [
                        steelseries.Section(0, +cache.average, gaugeGlobals.windAvgArea),
                        steelseries.Section(+cache.average, +cache.gust, gaugeGlobals.minMaxArea)
                    ];
                    if (cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setValue(0);
                        ssGauge.setMaxValue(cache.maxValue);
                    }

                    ssGauge.setArea(cache.areas);
                    ssGauge.setMaxMeasuredValue(cache.maxGustToday);
                    ssGauge.setValueAnimated(cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        tip = '<h5>' + strings.wind_title + '</h5>' + 
                              '<div class="ows-titleFlex">' + 
                              '<span>' + strings.tenminavgwind_info + ':</span><span>' + cache.average + ' ' + data.windunit + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.maxavgwind_info + ':</span><span>' + cache.maxAvgToday + ' ' + data.windunit + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.tenmingust_info + ':</span><span>' + cache.gust + ' ' + data.windunit + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + strings.maxgust_info + ':</span><span>' + cache.maxGustToday + ' ' + data.windunit + ' ' +
                              strings.at + ' ' + data.TwgustTM + ' ' + strings.bearing_info + ': ' + data.bearingTM +
                              (isNaN(parseFloat(data.bearingTM)) ? '' : '° (' + getord(+data.bearingTM) + ')') + '</span></div>' ;
                        $('#imgtip6_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[6] !== null) {
                        $('#imgtip6_img').attr('src', config.imgPathURL + config.tipImgs[6] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(), // End of singleWind()

        //
        // Singleton for the Wind Direction Gauge
        //
        singleDir = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {}; // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define wind direction gauge start values
                cache.valueLatest = 0;
                cache.valueAverage = 0;
                cache.titles = [strings.latest_web, strings.tenminavg_web];

                // create wind direction/compass radial gauge
                if ($('#canvas_dir').length) {
                    params.size = Math.ceil($('#canvas_dir').width() * config.gaugeScaling);
                    params.pointerTypeLatest = gaugeGlobals.pointer; // default TYPE8,
                    params.pointerTypeAverage = gaugeGlobals.dirAvgPointer; // default TYPE8
                    params.pointerColorAverage = gaugeGlobals.dirAvgPointerColour;
                    params.degreeScale = true;             // Show degree scale rather than ordinal directions
                    params.pointSymbols = strings.compass;
                    params.roseVisible = false;
                    params.lcdTitleStrings = cache.titles;
                    params.useColorLabels = false;

                    ssGauge = new steelseries.WindDirection('canvas_dir', params);
                    ssGauge.setValueAverage(+cache.valueAverage);
                    ssGauge.setValueLatest(+cache.valueLatest);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_dir').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_dir').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var windSpd, windGst, range, tip, i,
                        rosePoints = 0,
                        roseMax = 0,
                        roseSectionAngle = 0,
                        roseAreas = [];

                    cache.valueLatest = extractInteger(data.bearing);
                    cache.valueAverage = extractInteger(data.avgbearing);
                    cache.bearingFrom = extractInteger(data.BearingRangeFrom10);
                    cache.bearingTo = extractInteger(data.BearingRangeTo10);

                    ssGauge.setValueAnimatedAverage(+cache.valueAverage);
                    if (cache.valueAverage === 0) {
                        cache.valueLatest = 0;
                    }
                    ssGauge.setValueAnimatedLatest(+cache.valueLatest);

                    if (config.showWindVariation) {
                        windSpd = +extractDecimal(data.wspeed);
                        windGst = +extractDecimal(data.wgust);
                        switch (data.windunit.toLowerCase()) {
                        case 'mph':
                            cache.avgKnots = 0.868976242 * windSpd;
                            cache.gstKnots = 0.868976242 * windGst;
                            break;
                        case 'kts':
                            cache.avgKnots = windSpd;
                            cache.gstKnots = windGst;
                            break;
                        case 'm/s':
                            cache.avgKnots = 1.94384449 * windSpd;
                            cache.gstKnots = 1.94384449 * windGst;
                            break;
                        case 'km/h':
                            cache.avgKnots = 0.539956803 * windSpd;
                            cache.gstKnots = 0.539956803 * windGst;
                            break;
                        // no default
                        }
                        cache.avgKnots = Math.round(cache.avgKnots);
                        cache.gstKnots = Math.round(cache.gstKnots);
                        if (config.showWindMetar) {
                            ssGauge.VRB = ' - METAR: ' + ('0' + data.avgbearing).slice(-3) + ('0' + cache.avgKnots).slice(-2) +
                                          'G' + ('0' + cache.gstKnots).slice(-2) + 'KT ';
                        } else {
                            ssGauge.VRB = '';
                        }
                        if (windSpd > 0) {
                            // If variation less than 60 degrees, then METAR = Steady
                            // Unless range = 0 and from/to direction = avg + 180
                            range = (+cache.bearingTo < +cache.bearingFrom ? 360 + (+cache.bearingTo) : +cache.bearingTo) - (+cache.bearingFrom);

                            if (cache.avgKnots < 3) { // Europe uses 3kts, USA 6kts as the threshold
                                if (config.showRoseOnDirGauge) {
                                    ssGauge.setSection([steelseries.Section(cache.bearingFrom, cache.bearingTo, gaugeGlobals.windVariationSector)]);
                                    ssGauge.setSection([]);
                                } else {
                                    ssGauge.setSection([steelseries.Section(cache.bearingFrom, cache.bearingTo, gaugeGlobals.minMaxArea)]);
                                    ssGauge.setArea([]);
                                }
                            } else if (config.showRoseOnDirGauge) {
                                ssGauge.setSection([steelseries.Section(cache.bearingFrom, cache.bearingTo, gaugeGlobals.windVariationSector)]);
                            } else {
                                ssGauge.setSection([]);
                                ssGauge.setArea([steelseries.Section(cache.bearingFrom, cache.bearingTo, gaugeGlobals.minMaxArea)]);
                            }
                            if (config.showWindMetar) {
                                if ((range < 60 && range > 0) || range === 0 && cache.bearingFrom === cache.valueAverage) {
                                    ssGauge.VRB += ' STDY';
                                } else if (cache.avgKnots < 3) { // Europe uses 3kts, USA 6kts as the threshold
                                    ssGauge.VRB += ' VRB';
                                } else {
                                    ssGauge.VRB += ' ' + cache.bearingFrom + 'V' + cache.bearingTo;
                                }
                            }
                        } else {
                            // Zero wind speed, calm
                            if (config.showWindMetar) {
                                ssGauge.VRB = ' - METAR: 00000KT';
                            }
                            ssGauge.setSection([]);
                            if (!config.showRoseOnDirGauge) {
                                ssGauge.setArea([]);
                            }
                        }
                    } else {
                        ssGauge.VRB = '';
                    }

                    // optional rose data on direction gauge
                    if (config.showRoseOnDirGauge && data.WindRoseData) {
                        // Process rose data
                        rosePoints = data.WindRoseData.length;
                        roseSectionAngle = 360 / rosePoints;
                        // Find total for all directions
                        for (i = 0; i < rosePoints; i++) {
                            roseMax = Math.max(roseMax, data.WindRoseData[i]);
                        }
                        // Check we actually have some data, bad things happen if roseMax=0!
                        if (roseMax > 0) {
                            // Find relative value for each point, and create a gauge area for it
                            for (i = 0; i < rosePoints; i++) {
                                roseAreas[i] = steelseries.Section(
                                    i * roseSectionAngle - roseSectionAngle / 2,
                                    (i + 1) * roseSectionAngle - roseSectionAngle / 2,
                                    'rgba(' + gradient('2020D0', 'D04040', data.WindRoseData[i] / roseMax) + ',' +
                                    (data.WindRoseData[i] / roseMax).toFixed(2) + ')'
                                );
                            }
                        }
                        ssGauge.setArea(roseAreas);
                    }

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        tip = '<h5>' + strings.bearing_info + '</h5>' + 
                              '<div class="ows-titleFlex">' + 
                              '<span>' + strings.latest_title + ' ' + strings.bearing_info + ':</span>' + 
                              '<span>' + cache.valueLatest + '° (' + getord(+cache.valueLatest) + ')' + ssGauge.VRB + '</span>' + 
                              '</div><div class="ows-titleFlex">' +
                              '<span>' + strings.tenminavg_web + ' ' + strings.bearing_info + ':</span>' +
                              '<span>' + cache.valueAverage + '° (' + getord(+cache.valueAverage) + ')</span></div>' + 
                              '<div class="ows-titleFlex"><span>' + strings.dominant_bearing + ':</span><span>' + data.domwinddir + '</span><div>' ;
                        if (!config.showRoseGauge) {
                            // Wind run is shown on the wind rose if it is available
                            tip += '<div class="ows-titleFlex"><span>' + strings.windruntoday + ':</span><span>' + data.windrun + ' ' + displayUnits.windrun + '</span></div>';
                        }
                        $('#imgtip7_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[7] !== null) {
                        $('#imgtip7_img').attr('src', config.imgPathURL + config.tipImgs[7] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Wind Rose Gauge
        //
        singleRose = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge

            var buffers = {};   // Stores references to the various canvas buffers
            var cache = {};     // various parameters to store for the life time of gauge
            var ctxRoseCanvas;  // 2D context for the plotted gauge

            cache.firstRun = true;
            cache.odoDigits = 5;  // Total number of odometer digits including the decimal

            function init() {
                var div, roseCanvas;
                // Get the context of the gauge canvas on the HTML page
                if ($('#canvas_rose').length) {
                    cache.gaugeSize = Math.ceil($('#canvas_rose').width() * config.gaugeScaling);
                    cache.gaugeSize2 = cache.gaugeSize / 2;
                    cache.showOdo = config.showRoseGaugeOdo || false;

                    cache.compassStrings = strings.compass;
                    cache.titleString = strings.windrose;
                    cache.gaugeOdoTitle = strings.km;

                    // Create a hidden div to host the Rose plot
                    div = document.createElement('div');
                    div.style.display = 'none';
                    document.body.appendChild(div);

                    // Calcuate the size of the gauge background and so the size of rose plot required
                    cache.plotSize = Math.floor(cache.gaugeSize * 0.68);
                    cache.plotSize2 = cache.plotSize / 2;

                    // rose plot canvas buffer
                    buffers.plot = document.createElement('canvas');
                    buffers.plot.width = cache.plotSize;
                    buffers.plot.height = cache.plotSize;
                    buffers.plot.id = 'rosePlot';
                    buffers.ctxPlot = buffers.plot.getContext('2d');
                    div.appendChild(buffers.plot);

                    // Create a steelseries gauge frame
                    buffers.frame = document.createElement('canvas');
                    buffers.frame.width = cache.gaugeSize;
                    buffers.frame.height = cache.gaugeSize;
                    buffers.ctxFrame = buffers.frame.getContext('2d');
                    steelseries.drawFrame(
                        buffers.ctxFrame,
                        gaugeGlobals.frameDesign,
                        cache.gaugeSize2,
                        cache.gaugeSize2,
                        cache.gaugeSize,
                        cache.gaugeSize
                    );

                    // Create a steelseries gauge background
                    buffers.background = document.createElement('canvas');
                    buffers.background.width = cache.gaugeSize;
                    buffers.background.height = cache.gaugeSize;
                    buffers.ctxBackground = buffers.background.getContext('2d');
                    steelseries.drawBackground(
                        buffers.ctxBackground,
                        gaugeGlobals.background,
                        cache.gaugeSize2,
                        cache.gaugeSize2,
                        cache.gaugeSize,
                        cache.gaugeSize
                    );

                    // Optional - add a background image
                    /*
                    if (g_imgSmall !== null) {
                        var drawSize = g_size * 0.831775;
                        var x = (g_size - drawSize) / 2;
                        buffers.ctxBackground.drawImage(g_imgSmall, x, x, cache.gaugeSize, cache.gaugeSize);
                    }
                    */
                    // Add the compass points
                    drawCompassPoints(buffers.ctxBackground, cache.gaugeSize);

                    // Create a steelseries gauge foreground
                    buffers.foreground = document.createElement('canvas');
                    buffers.foreground.width = cache.gaugeSize;
                    buffers.foreground.height = cache.gaugeSize;
                    buffers.ctxForeground = buffers.foreground.getContext('2d');
                    steelseries.drawForeground(
                        buffers.ctxForeground,
                        gaugeGlobals.foreground,
                        cache.gaugeSize,
                        cache.gaugeSize,
                        false
                    );

                    roseCanvas = document.getElementById('canvas_rose');
                    ctxRoseCanvas = roseCanvas.getContext('2d');
                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_rose').css({width: cache.gaugeSize + 'px', height: cache.gaugeSize + 'px'});
                    }
                    // resize canvas on main page
                    roseCanvas.width = cache.gaugeSize;
                    roseCanvas.height = cache.gaugeSize;
                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_rose').css(gaugeShadow(cache.gaugeSize));
                    }

                    // Render an empty gauge, looks better than just the shadow background and odometer ;)
                    // Paint the gauge frame
                    ctxRoseCanvas.drawImage(buffers.frame, 0, 0);

                    // Paint the gauge background
                    ctxRoseCanvas.drawImage(buffers.background, 0, 0);

                    // Paint the gauge foreground
                    ctxRoseCanvas.drawImage(buffers.foreground, 0, 0);

                    // Create an odometer
                    if (cache.showOdo) {
                        cache.odoHeight = Math.ceil(cache.gaugeSize * 0.08); // Sets the size of the odometer
                        cache.odoWidth = Math.ceil(Math.floor(cache.odoHeight * 0.68) * cache.odoDigits);  // 'Magic' number, do not alter
                        // Create a new canvas for the oodometer
                        buffers.Odo = document.createElement('canvas');
                        $(buffers.Odo).attr({
                            id    : 'canvas_odo',
                            width : cache.odoWidth,
                            height: cache.odoHeight
                        });
                        // Position it
                        $(buffers.Odo).attr('class', 'odo');
                        // Insert it into the DOM before the Rose gauge
                        $(buffers.Odo).insertBefore('#canvas_rose');
                        // Create the odometer
                        ssGauge = new steelseries.Odometer('canvas_odo', {
                            height  : cache.odoHeight,
                            digits  : cache.odoDigits - 1,
                            decimals: 1
                        });
                    }
                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                cache.firstRun = false;

                function update() {
                    var rose, offset;

                    if (ctxRoseCanvas && !cache.firstRun) {
                        // Clear the gauge
                        ctxRoseCanvas.clearRect(0, 0, cache.gaugeSize, cache.gaugeSize);

                        // Clear the existing rose plot
                        buffers.ctxPlot.clearRect(0, 0, cache.plotSize, cache.plotSize);

                        // Create a new rose plot
                        rose = new RGraph.Rose('rosePlot', data.WindRoseData);
                        rose.Set('chart.strokestyle', 'black');
                        rose.Set('chart.background.axes.color', 'gray');
                        rose.Set('chart.colors.alpha', 0.5);
                        rose.Set('chart.colors', ['Gradient(#408040:red:#7070A0)']);
                        rose.Set('chart.margin', Math.ceil(40 / data.WindRoseData.length));

                        rose.Set('chart.title', cache.titleString);
                        rose.Set('chart.title.size', Math.ceil(0.05 * cache.plotSize));
                        rose.Set('chart.title.bold', false);
                        rose.Set('chart.title.color', gaugeGlobals.background.labelColor.getRgbColor());
                        rose.Set('chart.gutter.top', 0.2 * cache.plotSize);
                        rose.Set('chart.gutter.bottom', 0.2 * cache.plotSize);

                        rose.Set('chart.tooltips.effect', 'snap');
                        rose.Set('chart.labels.axes', '');
                        rose.Set('chart.background.circles', true);
                        rose.Set('chart.background.grid.spokes', 16);
                        rose.Set('chart.radius', cache.plotSize2);
                        rose.Draw();

                        // Add title to windrun odometer to the plot
                        if (cache.showOdo) {
                            drawOdoTitle(buffers.ctxPlot);
                        }

                        // Paint the gauge frame
                        ctxRoseCanvas.drawImage(buffers.frame, 0, 0);

                        // Paint the gauge background
                        ctxRoseCanvas.drawImage(buffers.background, 0, 0);

                        // Paint the rose plot
                        offset = Math.floor(cache.gaugeSize2 - cache.plotSize2);
                        ctxRoseCanvas.drawImage(buffers.plot, offset, offset);

                        // Paint the gauge foreground
                        ctxRoseCanvas.drawImage(buffers.foreground, 0, 0);

                        // update the odometer
                        if (cache.showOdo) {
                            ssGauge.setValueAnimated(extractDecimal(data.windrun));
                        }

                        // update tooltip
                        if (ddimgtooltip.showTips) {
                            var tip = '<h5>' + strings.bearing_info + '</h5>' +
                                  '<div class="ows-titleFlex">' + 
                                  '<span>' + strings.dominant_bearing + ':</span><span>' + data.domwinddir + '</span>' +
                                  '</div><div class="ows-titleFlex">' +
                                  '<span>' + strings.windruntoday + '</span><span>' + data.windrun + ' ' + displayUnits.windrun + '</span>' +
                                  '</div>';
                            $('#imgtip10_txt').html( tip );

                        }
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[10] !== null) {
                        $('#imgtip10_img').attr('src', config.imgPathURL + config.tipImgs[10] + cacheDefeat);
                    }
                }

                // Helper function to put the compass points on the background
                function drawCompassPoints(ctx, size) {
                    ctx.save();
                    // set the font
                    ctx.font = 0.08 * size + 'px serif';
                    ctx.strokeStyle = gaugeGlobals.background.labelColor.getRgbaColor();
                    ctx.fillStyle = gaugeGlobals.background.labelColor.getRgbColor();
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';

                    // Draw the compass points
                    for (var i = 0; i < 4; i++) {
                        ctx.translate(size / 2, size * 0.125);
                        ctx.fillText(cache.compassStrings[i * 2], 0, 0, size);
                        ctx.translate(-size / 2, -size * 0.125);
                        // Move to center
                        ctx.translate(size / 2, size / 2);
                        ctx.rotate(Math.PI / 2);
                        ctx.translate(-size / 2, -size / 2);
                    }
                    ctx.restore();
                }

                function drawOdoTitle(ctx) {
                    ctx.save();
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.font = 0.05 * cache.gaugeSize + 'px Arial,Verdana,sans-serif';
                    ctx.strokeStyle = gaugeGlobals.background.labelColor.getRgbaColor();
                    ctx.fillStyle = gaugeGlobals.background.labelColor.getRgbaColor();
                    ctx.fillText(cache.gaugeOdoTitle, cache.plotSize2, cache.plotSize * 0.75, cache.plotSize * 0.5);
                    ctx.restore();
                }

                function setTitle(newTitle) {
                    cache.titleString = newTitle;
                }

                function setOdoTitle(newTitle) {
                    cache.gaugeOdoTitle = newTitle;
                }

                function setCompassStrings(newArray) {
                    cache.compassStrings = newArray;
                    if (!cache.firstRun) {
                        // Redraw the background
                        steelseries.drawBackground(
                            buffers.ctxBackground,
                            gaugeGlobals.background,
                            cache.gaugeSize2,
                            cache.gaugeSize2,
                            cache.gaugeSize,
                            cache.gaugeSize
                        );
                        // Add the compass points
                        drawCompassPoints(buffers.ctxBackground, cache.gaugeSize);
                    }
                }

                return {
                    update           : update,
                    gauge            : ssGauge,
                    drawCompassPoints: drawCompassPoints,
                    setTitle         : setTitle,
                    setCompassStrings: setCompassStrings,
                    setOdoTitle      : setOdoTitle
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the UV-Index Gauge
        //
        singleUV = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define UV start values
                cache.value = 0.0001;
                cache.sections = [
                    steelseries.Section(0, 2.9, '#289500'),
                    steelseries.Section(2.9, 5.8, '#f7e400'),
                    steelseries.Section(5.8, 7.8, '#f85900'),
                    steelseries.Section(7.8, 10.9, '#d8001d'),
                    steelseries.Section(10.9, 20, '#6b49c8')
                ];
                // Define value gradient for UV
                cache.gradient = new steelseries.gradientWrapper(0, 16,
                    [0, 0.1, 0.19, 0.31, 0.45, 0.625, 1],
                    [
                        new steelseries.rgbaColor(0, 200, 0, 1),
                        new steelseries.rgbaColor(0, 200, 0, 1),
                        new steelseries.rgbaColor(255, 255, 0, 1),
                        new steelseries.rgbaColor(248, 89, 0, 1),
                        new steelseries.rgbaColor(255, 0, 0, 1),
                        new steelseries.rgbaColor(255, 0, 144, 1),
                        new steelseries.rgbaColor(153, 140, 255, 1)
                    ]
                );
                cache.useSections = false;
                cache.useValueGradient = true;

                // create UV bargraph gauge
                if ($('#canvas_uv').length) {
                    params.size = Math.ceil($('#canvas_uv').width() * config.gaugeScaling);
                    params.gaugeType = steelseries.GaugeType.TYPE3;
                    params.maxValue = gaugeGlobals.uvScaleDefMax;
                    params.titleString = strings.uv_title;
                    params.niceScale = false;
                    params.section = cache.sections;
                    params.useSectionColors = cache.useSections;
                    params.valueGradient = cache.gradient;
                    params.useValueGradient = cache.useValueGradient;
                    params.lcdDecimals = gaugeGlobals.uvLcdDecimals;

                    ssGauge = new steelseries.RadialBargraph('canvas_uv', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_uv').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_uv').css(gaugeShadow(params.size));
                    }

                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var tip, indx;

                    cache.value = extractDecimal(data.UV);

                    if (+cache.value === 0) {
                        indx = 0;
                    } else if (cache.value < 2.5) {
                        indx = 1;
                    } else if (cache.value < 5.5) {
                        indx = 2;
                    } else if (cache.value < 7.5) {
                        indx = 3;
                    } else if (cache.value < 10.5) {
                        indx = 4;
                    } else {
                        indx = 5;
                    }

                    cache.maxValue = Math.max(nextHighest(cache.value, 2), gaugeGlobals.uvScaleDefMax);
                    if (cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setValue(0);
                        ssGauge.setMaxValue(cache.maxValue);
                    }

                    cache.risk = strings.uv_levels[indx];
                    cache.headLine = strings.uv_headlines[indx];
                    cache.detail = strings.uv_details[indx];
                    ssGauge.setUnitString(cache.risk);
                    ssGauge.setValueAnimated(cache.value);

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        tip = '<h5>' + strings.uv_title + '</h5>' +
                              '<div class="ows-titleFlex">' + 
                              '<span>' + strings.uv_title + ':</span><span>' +  cache.value + '</span>' +
                              '</div><div class="ows-titleFlex">' +
                              '<span>' + strings.solar_maxToday + '</span><span>' + data.UVTH + '</span>' +
                              '</div><div class="ows-titleFlex">' + 
                              '<span>' + cache.headLine.replaceAll('<br>',' ') + '</span></div>'
                        $('#imgtip8_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[8] !== null) {
                        $('#imgtip8_img').attr('src', config.imgPathURL + config.tipImgs[8] + cacheDefeat);
                    }
                }

                return {
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Solar Irradiation Gauge
        //
        singleSolar = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                // define Solar start values
                cache.value = 0.0001;
                cache.sections = [
                    steelseries.Section(0, 600, 'rgba(40,149,0,0.3)'),
                    steelseries.Section(600, 800, 'rgba(248,89,0,0.3)'),
                    steelseries.Section(800, 1000, 'rgba(216,0,29,0.3)'),
                    steelseries.Section(1000, 1800, 'rgba(107,73,200,0.3)')
                ];

                // create Solar gauge
                if ($('#canvas_solar').length) {
                    params.size = Math.ceil($('#canvas_solar').width() * config.gaugeScaling);
                    params.section = cache.sections;
                    params.maxValue = gaugeGlobals.solarGaugeScaleMax;
                    params.titleString = strings.solar_title;
                    params.unitString = 'W/m\u00B2';
                    params.niceScale = false;
                    params.thresholdVisible = false;
                    params.lcdDecimals = 0;

                    if (config.showSunshineLed) {
                        params.userLedVisible = true;
                        params.userLedColor = steelseries.LedColor.YELLOW_LED;
                    }

                    ssGauge = new steelseries.Radial('canvas_solar', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_solar').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_solar').css(gaugeShadow(params.size));
                    }
                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    var tip, percent;

                    cache.value = +extractInteger(data.SolarRad);
                    cache.maxToday = extractInteger(data.SolarTM);
                    cache.currMaxValue = +extractInteger(data.CurrentSolarMax);
                    percent = (+cache.currMaxValue === 0 ? '--' : Math.round(+cache.value / +cache.currMaxValue * 100));

                    // Need to rescale the gauge?
                    cache.maxValue = Math.max(cache.value, cache.currMaxValue, cache.maxToday, gaugeGlobals.solarGaugeScaleMax);
                    cache.maxValue = nextHighest(cache.maxValue, 100);
                    if (cache.maxValue !== ssGauge.getMaxValue()) {
                        ssGauge.setValue(0);
                        ssGauge.setMaxValue(cache.maxValue);
                    }

                    // Set a section (15% of maxScale wide) to show current theoretical max value
                    if (data.CurrentSolarMax !== 'N/A') {
                        ssGauge.setArea([
                            // Sunshine threshold
                            steelseries.Section(
                                Math.max(cache.currMaxValue * gaugeGlobals.sunshineThresholdPct / 100, gaugeGlobals.sunshineThreshold),
                                cache.currMaxValue,
                                'rgba(255,255,50,0.4)'
                            ),
                            // Over max threshold
                            steelseries.Section(
                                cache.currMaxValue,
                                Math.min(cache.currMaxValue + cache.maxValue * 0.15, cache.maxValue),
                                'rgba(220,0,0,0.5)'
                            )
                        ]);
                    }

                    // Set the values
                    ssGauge.setMaxMeasuredValue(cache.maxToday);
                    ssGauge.setValueAnimated(cache.value);

                    if (config.showSunshineLed) {
                        ssGauge.setUserLedOnOff(
                            percent !== '--' &&
                            percent >= gaugeGlobals.sunshineThresholdPct &&
                            +cache.value >= gaugeGlobals.sunshineThreshold
                        );
                    }

                    if (ddimgtooltip.showTips) {
                        // update tooltip
                        tip = '<h5>' + strings.solar_title + '</h5>' +
                              '<div class="ows-titleFlex">' + 
                              '<span>' + strings.solar_title + ':</span>' +
                              '<span>' + cache.value + ' W/m²' + '</span>' + 
                              '<span><i>' + percent + '% ' + strings.solar_ofMax + '</i></span>' + 
                              '</div><div class="ows-titleFlex">' +
                              '<span>' + strings.solar_currentMax + ':</span><span>' + cache.currMaxValue + ' W/m²</span>' +
                              '</div>'; 
                        if (typeof data.SolarTM !== 'undefined') {
                            tip += '<div class="ows-titleFlex"><span>' + strings.solar_maxToday + ':</span><span>' + cache.maxToday + ' W/m²</span></div>';
                        }
                        $('#imgtip9_txt').html(tip);
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[9] !== null) {
                        $('#imgtip9_img').attr('src', config.imgPathURL + config.tipImgs[9] + cacheDefeat);
                    }
                }

                return {
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // Singleton for the Cloudbase Gauge
        //
        singleCloudBase = (function () {
            var instance;   // Stores a reference to the Singleton
            var ssGauge;    // Stores a reference to the SS Gauge
            var cache = {};      // Stores various config values and parameters

            function init() {
                var params = $.extend(true, {}, commonParams);

                cache.sections = createCloudBaseSections(true);
                cache.value = 0.0001;
                cache.maxValue = gaugeGlobals.cloudScaleDefMaxm;

                // create Cloud base radial gauge
                if ($('#canvas_cloud').length) {
                    params.size = Math.ceil($('#canvas_cloud').width() * config.gaugeScaling);
                    params.section = cache.sections;
                    params.maxValue = cache.maxValue;
                    params.titleString = strings.cloudbase_title;
                    params.unitString = strings.metres;
                    params.thresholdVisible = false;
                    params.lcdDecimals = 0;

                    ssGauge = new steelseries.Radial('canvas_cloud', params);
                    ssGauge.setValue(cache.value);

                    // over-ride CSS applied size?
                    if (config.gaugeScaling !== 1) {
                        $('#canvas_cloud').css({width: params.size + 'px', height: params.size + 'px'});
                    }

                    // add a shadow to the gauge
                    if (config.showGaugeShadow) {
                        $('#canvas_cloud').css(gaugeShadow(params.size));
                    }
                    // subscribe to data updates
                    $.subscribe('gauges.dataUpdated', update);
                    $.subscribe('gauges.graphUpdate', updateGraph);
                } else {
                    // cannot draw gauge, return null
                    return null;
                }

                function update() {
                    cache.value = extractInteger(data.cloudbasevalue);

                    if (data.cloudbaseunit === 'm') {
                        // adjust metre gauge in jumps of 1000 metres, don't downscale during the session
                        cache.maxValue = Math.max(nextHighest(cache.value, 1000), gaugeGlobals.cloudScaleDefMaxm, cache.maxValue);
                        if (cache.value <= 1000 && config.roundCloudbaseVal) {
                            // and round the value to the nearest  10 m
                            cache.value = Math.round(cache.value / 10) * 10;
                        } else if (config.roundCloudbaseVal) {
                            // and round the value to the nearest 50 m
                            cache.value = Math.round(cache.value / 50) * 50;
                        }
                    } else {
                        // adjust feet gauge in jumps of 2000 ft, don't downscale during the session
                        cache.maxValue = Math.max(nextHighest(cache.value, 2000), gaugeGlobals.cloudScaleDefMaxft, cache.maxValue);
                        if (cache.value <= 2000 && config.roundCloudbaseVal) {
                            // and round the value to the nearest 50 ft
                            cache.value = Math.round(cache.value / 50) * 50;
                        } else if (config.roundCloudbaseVal) {
                            // and round the value to the nearest 100 ft
                            cache.value = Math.round(cache.value / 100) * 100;
                        }
                    }

                    if (cache.maxValue !== ssGauge.getMaxValue()) {
                        if (ssGauge.getMaxValue() > cache.maxValue) {
                            // Gauge currently showing more than our max (nice scale effct),
                            // so reset our max to match
                            cache.maxValue = ssGauge.getMaxValue();
                        } else {
                            // Gauge scale is too low, increase it.
                            // First set the pointer back to zero so we get a nice animation
                            ssGauge.setValue(0);
                            // and redraw the gauge with teh new scale
                            ssGauge.setMaxValue(cache.maxValue);
                        }
                    }
                    ssGauge.setValueAnimated(cache.value);

                    if (config.showPopupData) {
                        // static tooltip on cloud gauge
                        $('#imgtip11_txt').html('<h5>' + strings.cloudbase_popup_title + '</h5>' + strings.cloudbase_popup_text.replaceAll('<br>', ' '));
                        //$('#imgtip11_txt').html( tip );
                    }
                } // End of update()

                function updateGraph(evnt, cacheDefeat) {
                    if (config.tipImgs[11] !== null) {
                        $('#imgtip11_img').attr('src', config.imgPathURL + config.tipImgs[11] + cacheDefeat);
                    }
                }

                return {
                    data  : cache,
                    update: update,
                    gauge : ssGauge
                };
            } // End of init()

            return {
                // Get the Singleton instance if one exists
                // or create one if it doesn't
                getInstance: function () {
                    if (!instance) {
                        instance = init();
                    }
                    return instance;
                }
            };
        })(),

        //
        // getRealtime() fetches the realtimegauges JSON data from the server
        //
        getRealtime = function () {
            var url = config.realTimeURL;
            if ($.active > 0 && undefined != jqXHR) {
                // kill any outstanding requests
                jqXHR.abort();
            }
            if (config.longPoll) {
                url += '?timestamp=' + timestamp;
            }
            jqXHR = $.ajax({
                url     : url,
                cache   : (config.longPoll),
                dataType: 'json',
                timeout : config.longPoll ? (Math.min(config.realtimeInterval, 20) + 21) * 1000 : 21000 // 21 second time-out by default
            }).done(function (data) {
                checkRtResp(data);
            }).fail(function (xhr, status, err) {
                checkRtError(xhr, status, err);
            });
        },

        //
        // checkRtResp() called by the Ajax fetch once data has been downloaded
        //
        checkRtResp = function (response) {
            var delay;
            statusTimer.reset(config.longPoll ? 1 : config.realtimeInterval);
            if (config.longPoll && response.status !== 'OK') {
                checkRtError(null, 'PHP Error', response.status);
            } else {
                if (processData(response)) {
                    delay = ajaxDelay;
                } else {
                    delay = 5;
                }
                if (delay > 0) {
                    downloadTimer = setTimeout(getRealtime, delay * 1000);
                } else {
                    getRealtime();
                }
            }
        },

        //
        // checkRtError() called by the Ajax fetch if an error occurs during the fetching realtimegauges.txt
        //
        checkRtError = function (xhr, status, error) {
            if (xhr == null || xhr.statusText !== 'abort') {
                // Clear any existing download timer
                clearTimeout(downloadTimer);
                // Set the status LED to off
                ledIndicator.setLedOnOff(false);
                ledIndicator.setTitle(strings.led_title_unknown);
                statusScroller.setText(status + ': ' + error);
                // wait 5 seconds, then try again...
                downloadTimer = setTimeout(getRealtime, 5000);
            }
        },

        //
        // processData() massages the data returned in realtimegauges.txt, and posts a gauges.dataUpdated event to update the page
        //
        processData = function (dataObj) {
            var str, dt, tm, today, now, then, tmp, elapsedMins, retVal;
            // copy the realtime fields into the global 'data' object
            if (config.longPoll) {
                timestamp = dataObj.timestamp;
                data = dataObj.data;
            } else {
                // normal polling
                data = dataObj;
            }

            // and check we have the expected version number
            if (typeof data.ver !== 'undefined' && data.ver >= realtimeVer) {
                // manpulate the last rain time into something more friendly
                try {
                    str = data.LastRainTipISO.split(' ');
                    dt = str[0].replace(/\//g, '-').split('-');  // WD uses dd/mm/yyyy, we use a '-'
                    tm = str[1].split(':');
                    today = new Date();
                    today.setHours(0, 0, 0, 0);
                    if (typeof data.dateFormat === 'undefined') {
                        data.dateFormat = 'y/m/d';
                    } else {
                        // frig for WD bug which leaves a trailing % character from the tag
                        data.dateFormat = data.dateFormat.replace('%', '');
                    }
                    if (data.dateFormat === 'y/m/d') {
                        // ISO/Cumulus format
                        then = new Date(dt[0], dt[1] - 1, dt[2], tm[0], tm[1], 0, 0);
                    } else if (data.dateFormat === 'd/m/y') {
                        then = new Date(dt[2], dt[1] - 1, dt[0], tm[0], tm[1], 0, 0);
                    } else { // m/d/y
                        then = new Date(dt[2], dt[0] - 1, dt[1], tm[0], tm[1], 0, 0);
                    }
                    if (then.getTime() >= today.getTime()) {
                        data.LastRained = strings.LastRainedT_info + ' ' + str[1];
                    } else if (then.getTime() + 86400000 >= today.getTime()) {
                        data.LastRained = strings.LastRainedY_info + ' ' + str[1];
                    } else {
                        data.LastRained = then.getDate().toString() + ' ' + strings.months[then.getMonth()] + ' ' + strings.at + ' ' + str[1];
                    }
                } catch (e) {
                    data.LastRained = data.LastRainTipISO;
                }
                if (data.tempunit.length > 1) {
                    // clean up temperature units - remove html encoded degree symbols
                    data.tempunit = data.tempunit.replace(/&\S*;/, '°');  // old Cumulus versions uses &deg;, WeatherCat uses &#176;
                } else {
                    // using new realtimegaugesT.txt with Cumulus > 1.9.2
                    data.tempunit = '°' + data.tempunit;
                }

                // Check for station off-line
                now = Date.now();
                tmp = data.timeUTC.split(',');
                sampleDate = Date.UTC(tmp[0], tmp[1] - 1, tmp[2], tmp[3], tmp[4], tmp[5]);
                if (now - sampleDate > config.stationTimeout * 60 * 1000) {
                    elapsedMins = Math.floor((now - sampleDate) / (1000 * 60));
                    // the realtimegauges.txt file isn't being updated
                    ledIndicator.setLedColor(steelseries.LedColor.RED_LED);
                    ledIndicator.setTitle(strings.led_title_offline);
                    ledIndicator.blink(true);
                    if (elapsedMins < 120) {
                        // up to 2 hours ago
                        tm = elapsedMins.toString() + ' ' + strings.StatusMinsAgo;
                    } else if (elapsedMins < 2 * 24 * 60) {
                        // up to 48 hours ago
                        tm = Math.floor(elapsedMins / 60).toString() + ' ' + strings.StatusHoursAgo;
                    } else {
                        // days ago!
                        tm = Math.floor(elapsedMins / (60 * 24)).toString() + ' ' + strings.StatusDaysAgo;
                    }
                    data.forecast = strings.led_title_offline + ' ' + strings.StatusLastUpdate + ' ' + tm;
                } else if (+data.SensorContactLost === 1) {
                    // Fine Offset sensor status
                    ledIndicator.setLedColor(steelseries.LedColor.RED_LED);
                    ledIndicator.setTitle(strings.led_title_lost);
                    ledIndicator.blink(true);
                    data.forecast = strings.led_title_lost;
                } else {
                    ledIndicator.setLedColor(steelseries.LedColor.GREEN_LED);
                    ledIndicator.setTitle(strings.led_title_ok + '. ' + strings.StatusLastUpdate + ': ' + data.date);
                    ledIndicator.blink(false);
                    ledIndicator.setLedOnOff(true);
                }

                // de-encode the forecast string if required (Cumulus support for extended characters)
                data.forecast = $('<div/>').html(data.forecast).text();
                data.forecast = data.forecast.trim();

                data.pressunit = data.pressunit.trim();  // WView sends ' in', ' mb', or ' hPa'
                if (data.pressunit === 'in') {  // Cumulus and WView send 'in'
                    data.pressunit = 'inHg';
                }

                data.windunit = data.windunit.trim(); // WView sends ' kmh' etc
                data.windunit = data.windunit.toLowerCase(); // WeatherCat sends "MPH"
                if (data.windunit === 'knots') {             // WeatherCat/weewx send "Knots", we use "kts"
                    data.windunit = 'kts';
                }

                if (data.windunit === 'kmh' || data.windunit === 'kph') {  // WD wind unit omits '/', weewx sends 'kph'
                    data.windunit = 'km/h';
                }

                data.rainunit = data.rainunit.trim(); // WView sends ' mm' etc

                // take a look at the cloud base data...
                // change WeatherCat units from Metres/Feet to m/ft
                try {
                    if (data.cloudbaseunit.toLowerCase() === 'metres') {
                        data.cloudbaseunit = 'm';
                    } else if (data.cloudbaseunit.toLowerCase() === 'feet') {
                        data.cloudbaseunit = 'ft';
                    }
                } catch (e) {
                    data.cloudbaseunit = '';
                }
                if (config.showCloudGauge && (
                    (config.weatherProgram === 4 || config.weatherProgram === 5) ||
                    data.cloudbasevalue === '')) {
                    // WeatherCat and VWS (and WView?) do not provide a cloud base value, so we have to calculate it...
                    // assume if the station uses an imperial wind speed they want cloud base in feet, otherwise metres
                    data.cloudbaseunit = (data.windunit === 'mph' || data.windunit === 'kts') ? 'ft' : 'm';
                    data.cloudbasevalue = calcCloudbase(data.temp, data.tempunit, data.dew, data.cloudbaseunit);
                }

                // Temperature data conversion for display required?
                if (data.tempunit[1] !== displayUnits.temp && userUnitsSet) {
                    // temp needs converting
                    if (data.tempunit[1] === 'C') {
                        convTempData(c2f);
                    } else {
                        convTempData(f2c);
                    }
                } else if (firstRun) {
                    displayUnits.temp = data.tempunit[1];
                    setRadioCheck('rad_unitsTemp', displayUnits.temp);
                }

                // Rain data conversion for display required?
                if (data.rainunit !== displayUnits.rain && userUnitsSet) {
                    // rain needs converting
                    convRainData(displayUnits.rain === 'mm' ? in2mm : mm2in);
                } else if (firstRun) {
                    displayUnits.rain = data.rainunit;
                    setRadioCheck('rad_unitsRain', displayUnits.rain);
                }

                // Wind data conversion for display required?
                if (data.windunit !== displayUnits.wind && userUnitsSet) {
                    // wind needs converting
                    convWindData(data.windunit, displayUnits.wind);
                } else if (firstRun) {
                    displayUnits.wind = data.windunit;
                    displayUnits.windrun = getWindrunUnits(data.windunit);
                    setRadioCheck('rad_unitsWind', displayUnits.wind);
                }

                // Pressure data conversion for display required?
                if (data.pressunit !== displayUnits.press && userUnitsSet) {
                    convBaroData(data.pressunit, displayUnits.press);
                } else if (firstRun) {
                    displayUnits.press = data.pressunit;
                    setRadioCheck('rad_unitsPress', displayUnits.press);
                }

                // Cloud height data conversion for display required?
                if (data.cloudbaseunit !== displayUnits.cloud && userUnitsSet) {
                    // Cloud height needs converting
                    convCloudBaseData(displayUnits.cloud === 'm' ? ft2m : m2ft);
                } else if (firstRun) {
                    displayUnits.cloud = data.cloudbaseunit;
                    setRadioCheck('rad_unitsCloud', displayUnits.cloud);
                }

                statusScroller.setText(data.forecast);

                // first time only, setup units etc
                if (firstRun) {
                    doFirst();
                }

                // publish the update, use the shared data object rather than transferring it
                $.publish('gauges.dataUpdated', {});

                retVal = true;
            } else {
                // set an error message
                if (data.ver < realtimeVer) {
                    statusTimer.setValue(0);
                    statusScroller.setText('Your ' + config.realTimeURL.substr(config.realTimeURL.lastIndexOf('/') + 1) + ' file template needs updating!');
                    return false;
                } else {
                    // oh-oh! The number of data fields isn't what we expected
                    statusScroller.setText(strings.realtimeCorrupt);
                }
                ledIndicator.setLedOnOff(false);
                ledIndicator.setTitle(strings.led_title_unknown);

                retVal = false;
            }
            return retVal;
        },

        //
        // pagetimeout() called once every config.pageUpdateLimit minutes to stop updates and prevent page 'sitters'
        //
        pageTimeout = function () {
            statusScroller.setText(strings.StatusPageLimit);
            ledIndicator.setLedColor(steelseries.LedColor.RED_LED);
            ledIndicator.setTitle(strings.StatusPageLimit);
            ledIndicator.blink(true);
            ledIndicator.setTitle(strings.StatusTimeout);

            // stop any pending download
            clearTimeout(downloadTimer);

            // stop any long polling in progress
            if ($.active > 0) {
                jqXHR.abort();
            }

            // stop the clock
            clearInterval(tickTockInterval);

            // clear the timer display
            statusTimer.setValue(0);

            // set an onclick event on the LED to restart everything
            $('#canvas_led').click(
                function click() {
                    // disable the onClick event again
                    $('#canvas_led').unbind('click');
                    // reset the timer count to 1
                    statusTimer.reset(1);
                    // restart the timer to update the status time
                    tickTockInterval = setInterval(
                        function tick() {
                            $.publish('gauges.clockTick', null);
                        },
                        1000
                    );

                    // restart the page timeout timer, so we hit this code again
                    setTimeout(pageTimeout, config.pageUpdateLimit * 60 * 1000);

                    // refresh the page data
                    getRealtime();
                }
            );
        },

        //
        // doFirst() called by doUpdate() the first time the page is updated to set-up various things that are
        // only known when the realtimegauges.txt data is available
        //
        doFirst = function () {
            var cacheDefeat = '?' + (new Date()).getTime().toString();

            if (data.tempunit[1] === 'F') {
                displayUnits.temp = 'F';
                setRadioCheck('rad_unitsTemp', 'F');
                setTempUnits(false);
            }

            if (data.pressunit !== 'hPa') {
                displayUnits.press = data.pressunit;
                setRadioCheck('rad_unitsPress', data.pressunit);
                setBaroUnits(data.pressunit);
            }

            if (data.windunit !== 'km/h') {
                displayUnits.wind = data.windunit;
                setRadioCheck('rad_unitsWind', data.windunit);
                setWindUnits(data.windunit);
            }

            if (data.rainunit !== 'mm') {
                displayUnits.rain = data.rainunit;
                setRadioCheck('rad_unitsRain', data.rainunit);
                setRainUnits(false);
            }

            if (config.showSolarGauge && typeof data.SolarTM !== 'undefined' && gaugeSolar) {
                gaugeSolar.gauge.setMaxMeasuredValueVisible(true);
            }

            if (config.showCloudGauge && data.cloudbaseunit !== 'm') {
                displayUnits.cloud = data.cloudbaseunit;
                setRadioCheck('rad_unitsCloud', data.cloudbaseunit);
                setCloudBaseUnits(false);
            }

            // set the script version on the page
            $('#scriptVer').html(config.scriptVer);
            // set the version information from the station
            $('#programVersion').html(data.version);
            $('#programBuild').html(data.build);
            $('#programName').html(programLink[config.weatherProgram]);

            if (config.showPopupData) {
                // now initialise the pop-up script and download the trend images
                // - has to be done here as doFirst may remove elements from the page
                // - and we delay the download of the images speeding up page display
                ddimgtooltip.init('[id^="tip_"]');
                // Are we running on a phone device (or really low res screen)?
                if ($(window).width() < 480) {
                    $('.ddimgtooltip').filter(':hidden').width('200px');
                }
            }

            if (config.showPopupData && config.showPopupGraphs) {
                // now download the trend images
                // - has to be done here as doFirst may remove elements from the page
                // - and we delay the download of the images speeding up page display
                //
                $('#imgtip0_img').attr('src', config.imgPathURL + config.tipImgs[0][0] + cacheDefeat);
                if (gaugeDew) {
                    $('#imgtip1_img').attr('src', config.imgPathURL + config.tipImgs[1][gaugeDew.data.popupImg] + cacheDefeat);
                }
                $('#imgtip2_img').attr('src', config.imgPathURL + config.tipImgs[2] + cacheDefeat);
                $('#imgtip3_img').attr('src', config.imgPathURL + config.tipImgs[3] + cacheDefeat);
                $('#imgtip4_img').attr('src', config.imgPathURL + config.tipImgs[4][0] + cacheDefeat);
                $('#imgtip5_img').attr('src', config.imgPathURL + config.tipImgs[5] + cacheDefeat);
                $('#imgtip6_img').attr('src', config.imgPathURL + config.tipImgs[6] + cacheDefeat);
                $('#imgtip7_img').attr('src', config.imgPathURL + config.tipImgs[7] + cacheDefeat);
                $('#imgtip8_img').attr('src', config.imgPathURL + config.tipImgs[8] + cacheDefeat);
                $('#imgtip9_img').attr('src', config.imgPathURL + config.tipImgs[9] + cacheDefeat);
                $('#imgtip10_img').attr('src', config.imgPathURL + config.tipImgs[10] + cacheDefeat);
                $('#imgtip11_img').attr('src', config.imgPathURL + config.tipImgs[11] + cacheDefeat);
                // start a timer for popup graphic updates
                setInterval(
                    function timeout() {
                        $.publish('gauges.graphUpdate', '?' + (new Date()).getTime().toString());
                    },
                    config.graphUpdateTime * 60 * 1000
                );
            }

            firstRun = false;
        },

        //
        // createTempSections() creates an array of gauge sections appropriate for Celsius or Fahrenheit scales
        //
        createTempSections = function (celsius) {
            var section;
            if (celsius) {
                section = [
                    steelseries.Section(-100, -35, 'rgba(195, 92, 211, 0.4)'),
                    steelseries.Section(-35, -30, 'rgba(139, 74, 197, 0.4)'),
                    steelseries.Section(-30, -25, 'rgba(98, 65, 188, 0.4)'),
                    steelseries.Section(-25, -20, 'rgba(62, 66, 185, 0.4)'),
                    steelseries.Section(-20, -15, 'rgba(42, 84, 194, 0.4)'),
                    steelseries.Section(-15, -10, 'rgba(25, 112, 210, 0.4)'),
                    steelseries.Section(-10, -5, 'rgba(9, 150, 224, 0.4)'),
                    steelseries.Section(-5, 0, 'rgba(2, 170, 209, 0.4)'),
                    steelseries.Section(0, 5, 'rgba(0, 162, 145, 0.4)'),
                    steelseries.Section(5, 10, 'rgba(0, 158, 122, 0.4)'),
                    steelseries.Section(10, 15, 'rgba(54, 177, 56, 0.4)'),
                    steelseries.Section(15, 20, 'rgba(111, 202, 56, 0.4)'),
                    steelseries.Section(20, 25, 'rgba(248, 233, 45, 0.4)'),
                    steelseries.Section(25, 30, 'rgba(253, 142, 42, 0.4)'),
                    steelseries.Section(30, 40, 'rgba(236, 45, 45, 0.4)'),
                    steelseries.Section(40, 100, 'rgba(245, 109, 205, 0.4)')
                ];
            } else {
                section = [
                    steelseries.Section(-200, -30, 'rgba(195, 92, 211, 0.4)'),
                    steelseries.Section(-30, -25, 'rgba(139, 74, 197, 0.4)'),
                    steelseries.Section(-25, -15, 'rgba(98, 65, 188, 0.4)'),
                    steelseries.Section(-15, -5, 'rgba(62, 66, 185, 0.4)'),
                    steelseries.Section(-5, 5, 'rgba(42, 84, 194, 0.4)'),
                    steelseries.Section(5, 15, 'rgba(25, 112, 210, 0.4)'),
                    steelseries.Section(15, 25, 'rgba(9, 150, 224, 0.4)'),
                    steelseries.Section(25, 32, 'rgba(2, 170, 209, 0.4)'),
                    steelseries.Section(32, 40, 'rgba(0, 162, 145, 0.4)'),
                    steelseries.Section(40, 50, 'rgba(0, 158, 122, 0.4)'),
                    steelseries.Section(50, 60, 'rgba(54, 177, 56, 0.4)'),
                    steelseries.Section(60, 70, 'rgba(111, 202, 56, 0.4)'),
                    steelseries.Section(70, 80, 'rgba(248, 233, 45, 0.4)'),
                    steelseries.Section(80, 90, 'rgba(253, 142, 42, 0.4)'),
                    steelseries.Section(90, 110, 'rgba(236, 45, 45, 0.4)'),
                    steelseries.Section(110, 200, 'rgba(245, 109, 205, 0.4)')
                ];
            }
            return section;
        },

        //
        // createRainSections() returns an array of section highlights for the Rain Rate gauge
        //
        /*
          Assumes 'standard' descriptive limits from UK met office:
           < 0.25 mm/hr - Very light rain
           0.25mm/hr to 1.0mm/hr - Light rain
           1.0 mm/hr to 4.0 mm/hr - Moderate rain
           4.0 mm/hr to 16.0 mm/hr - Heavy rain
           16.0 mm/hr to 50 mm/hr - Very heavy rain
           > 50.0 mm/hour - Extreme rain

           Roughly translated to the corresponding Inch rates
           < 0.001
           0.001 to 0.05
           0.05 to 0.20
           0.20 to 0.60
           0.60 to 2.0
           > 2.0
        */
        createRainRateSections = function (metric) {
            var factor = metric ? 1 : 1 / 25;
            return [
                steelseries.Section(0, 0.25 * factor, 'rgba(0, 140, 0, 0.5)'),
                steelseries.Section(0.25 * factor, 1 * factor, 'rgba(80, 192, 80, 0.5)'),
                steelseries.Section(1 * factor, 4 * factor, 'rgba(150, 203, 150, 0.5)'),
                steelseries.Section(4 * factor, 16 * factor, 'rgba(212, 203, 109, 0.5)'),
                steelseries.Section(16 * factor, 50 * factor, 'rgba(225, 155, 105, 0.5)'),
                steelseries.Section(50 * factor, 1000 * factor, 'rgba(245, 86, 59, 0.5)')
            ];
        },

        //
        // createRainFallSections()returns an array of section highlights for total rainfall in mm or inches
        //
        createRainfallSections = function (metric) {
            var factor = metric ? 1 : 1 / 25;
            return [
                steelseries.Section(0, 5 * factor, 'rgba(0, 250, 0, 1)'),
                steelseries.Section(5 * factor, 10 * factor, 'rgba(0, 250, 117, 1)'),
                steelseries.Section(10 * factor, 25 * factor, 'rgba(218, 246, 0, 1)'),
                steelseries.Section(25 * factor, 40 * factor, 'rgba(250, 186, 0, 1)'),
                steelseries.Section(40 * factor, 50 * factor, 'rgba(250, 95, 0, 1)'),
                steelseries.Section(50 * factor, 65 * factor, 'rgba(250, 0, 0, 1)'),
                steelseries.Section(65 * factor, 75 * factor, 'rgba(250, 6, 80, 1)'),
                steelseries.Section(75 * factor, 100 * factor, 'rgba(205, 18, 158, 1)'),
                steelseries.Section(100 * factor, 125 * factor, 'rgba(0, 0, 250, 1)'),
                steelseries.Section(125 * factor, 500 * factor, 'rgba(0, 219, 212, 1)')
            ];
        },

        //
        // createRainfallGradient() returns an array of SS colours for continuous gradient colouring of the total rainfall LED gauge
        //
        createRainfallGradient = function (metric) {
            var grad = new steelseries.gradientWrapper(
                0,
                (metric ? 100 : 4),
                [0, 0.1, 0.62, 1],
                [
                    new steelseries.rgbaColor(15, 148, 0, 1),
                    new steelseries.rgbaColor(213, 213, 0, 1),
                    new steelseries.rgbaColor(213, 0, 25, 1),
                    new steelseries.rgbaColor(250, 0, 0, 1)
                ]
            );
            return grad;
        },

        //
        // createClousBaseSections() returns an array of section highlights for the Cloud Base gauge
        //
        createCloudBaseSections = function (metric) {
            var section;
            if (metric) {
                section = [
                    steelseries.Section(0, 150, 'rgba(245, 86, 59, 0.5)'),
                    steelseries.Section(150, 300, 'rgba(225, 155, 105, 0.5)'),
                    steelseries.Section(300, 750, 'rgba(212, 203, 109, 0.5)'),
                    steelseries.Section(750, 1000, 'rgba(150, 203, 150, 0.5)'),
                    steelseries.Section(1000, 1500, 'rgba(80, 192, 80, 0.5)'),
                    steelseries.Section(1500, 2500, 'rgba(0, 140, 0, 0.5)'),
                    steelseries.Section(2500, 5500, 'rgba(19, 103, 186, 0.5)')
                ];
            } else {
                section = [
                    steelseries.Section(0, 500, 'rgba(245, 86, 59, 0.5)'),
                    steelseries.Section(500, 1000, 'rgba(225, 155, 105, 0.5)'),
                    steelseries.Section(1000, 2500, 'rgba(212, 203, 109, 0.5)'),
                    steelseries.Section(2500, 3500, 'rgba(150, 203, 150, 0.5)'),
                    steelseries.Section(3500, 5500, 'rgba(80, 192, 80, 0.5)'),
                    steelseries.Section(5500, 8500, 'rgba(0, 140, 0, 0.5)'),
                    steelseries.Section(8500, 18000, 'rgba(19, 103, 186, 0.5)')
                ];
            }
            return section;
        },

        //
        // --------------- Helper functions ------------------
        //

        //
        // getord() converts a value in degrees (0-360) into a localised compass point (N, ENE, NE, etc)
        //
        getord = function (deg) {
            if (deg === 0) {
                // Special case, 0=No wind, 360=North
                return strings.calm;
            } else {
                return (strings.coords[Math.floor((deg + 11.25) / 22.5) % 16]);
            }
        },

        //
        // getUrlParam() extracts the named parameter from the current page URL
        //
        getUrlParam = function (paramName) {
            var name, regexS, regex, results;
            name = paramName.replace(/(\[|\])/g, '\\$1');
            regexS = '[\\?&]' + name + '=([^&#]*)';
            regex = new RegExp(regexS);
            results = regex.exec(window.location.href);
            if (results === null) {
                return '';
            } else {
                return results[1];
            }
        },

        //
        // extractDecimal() returns a decimal number from a string, the decimal point can be either a dot or a comma
        // it ignores any text such as pre/appended units
        //
        extractDecimal = function (str, errVal) {
            try {
                return (/[-+]?[0-9]+\.?[0-9]*/).exec(str.replace(',', '.'))[0];
            } catch (e) {
                // error condition
                return errVal || -9999;
            }
        },

        //
        // extractInteger() returns an integer from a string
        // it ignores any text such as pre/appended units
        //
        extractInteger = function (str, errVal) {
            try {
                return (/[-+]?[0-9]+/).exec(str)[0];
            } catch (e) {
                // error condition
                return errVal || -9999;
            }
        },

        //
        // tempTrend() converts a temperature trend value into a localised string, or +1, 0, -1 depending on the value of bTxt
        //
        tempTrend = function (trend, units, bTxt) {
            // Scale is over 3 hours, in Celsius
            var val = trend * 3 * (units[1] === 'C' ? 1 : (5 / 9)),
                ret;
            if (trend === -9999) {
                ret = (bTxt ? '--' : trend);
            } else if (val > 5) {
                ret = (bTxt ? strings.RisingVeryRapidly : 1);
            } else if (val > 3) {
                ret = (bTxt ? strings.RisingQuickly : 1);
            } else if (val > 1) {
                ret = (bTxt ? strings.Rising : 1);
            } else if (val > 0.5) {
                ret = (bTxt ? strings.RisingSlowly : 1);
            } else if (val >= -0.5) {
                ret = (bTxt ? strings.Steady : 0);
            } else if (val >= -1) {
                ret = (bTxt ? strings.FallingSlowly : -1);
            } else if (val >= -3) {
                ret = (bTxt ? strings.Falling : -1);
            } else if (val >= -5) {
                ret = (bTxt ? strings.FallingQuickly : -1);
            } else {
                ret = (bTxt ? strings.FallingVeryRapidly : -1);
            }
            return ret;
        },

        //
        // baroTrend() converts a pressure trend value into a localised string, or +1, 0, -1 depending on the value of bTxt
        //
        baroTrend = function (trend, units, bTxt) {
            var val = trend * 3,
                ret;
            // The terms below are the UK Met Office terms for a 3 hour change in hPa
            // trend is supplied as an hourly change, so multiply by 3
            if (units === 'inHg') {
                val *= 33.8639;
            } else if (units === 'kPa') {
                val *= 10;
                // assume everything else is hPa or mb, could be dangerous!
            }
            if (trend === -9999) {
                ret = (bTxt ? '--' : trend);
            } else if (val > 6.0) {
                ret = (bTxt ? strings.RisingVeryRapidly : 1);
            } else if (val > 3.5) {
                ret = (bTxt ? strings.RisingQuickly : 1);
            } else if (val > 1.5) {
                ret = (bTxt ? strings.Rising : 1);
            } else if (val > 0.1) {
                ret = (bTxt ? strings.RisingSlowly : 1);
            } else if (val >= -0.1) {
                ret = (bTxt ? strings.Steady : 0);
            } else if (val >= -1.5) {
                ret = (bTxt ? strings.FallingSlowly : -1);
            } else if (val >= -3.5) {
                ret = (bTxt ? strings.Falling : -1);
            } else if (val >= -6.0) {
                ret = (bTxt ? strings.FallingQuickly : -1);
            } else {
                ret = (bTxt ? strings.FallingVeryRapidly : -1);
            }
            return ret;
        },

        //
        // getMinTemp() returns the lowest temperature today for gauge scaling
        //
        getMinTemp = function (deflt) {
            return Math.min(
                extractDecimal(data.tempTL, deflt),
                extractDecimal(data.dewpointTL, deflt),
                extractDecimal(data.apptempTL, deflt),
                extractDecimal(data.feelslikeTL, deflt),
                extractDecimal(data.wchillTL, deflt));
        },

        //
        // getMaxTemp() returns the highest temperature today for gauge scaling
        //
        getMaxTemp = function (deflt) {
            return Math.max(
                extractDecimal(data.tempTH, deflt),
                extractDecimal(data.apptempTH, deflt),
                extractDecimal(data.feelslikeTH, deflt),
                extractDecimal(data.heatindexTH, deflt),
                extractDecimal(data.humidex, deflt));
        },

        // Celsius to Fahrenheit
        c2f = function (val) {
            return (extractDecimal(val) * 9 / 5 + 32).toFixed(1);
        },
        // Fahrenheit to Celsius
        f2c = function (val) {
            return ((extractDecimal(val) - 32) * 5 / 9).toFixed(1);
        },
        // kph to ms
        kmh2ms = function (val) {
            return (extractDecimal(val) * 0.2778).toFixed(1);
        },
        // ms to kph
        ms2kmh = function (val) {
            return (extractDecimal(val) * 3.6).toFixed(1);
        },
        kmh2ms = function (val){
            return (extractDecimal(val) / 3.6).toFixed(1);
        },
        // mm to inches
        mm2in = function (val) {
            return (extractDecimal(val) / 25.4).toFixed(2);
        },
        // inches to mm
        in2mm = function (val) {
            return (extractDecimal(val) * 25.4).toFixed(1);
        },
        // miles to km
        miles2km = function (val) {
            return (extractDecimal(val) * 1.609344).toFixed(1);
        },
        // nautical miles to km
        nmiles2km = function (val) {
            return (extractDecimal(val) * 1.85200).toFixed(1);
        },
        // km to miles
        km2miles = function (val) {
            return (extractDecimal(val) / 1.609344).toFixed(1);
        },
        // km to nautical miles
        km2nmiles = function (val) {
            return (extractDecimal(val) / 1.85200).toFixed(1);
        },
        // hPa to inHg (@0°C)
        hpa2inhg = function (val, decimals) {
            return (extractDecimal(val) * 0.029528744).toFixed(decimals || 3);
        },
        // inHg to hPa (@0°C)
        inhg2hpa = function (val) {
            return (extractDecimal(val) / 0.029528744).toFixed(1);
        },
        // kPa to hPa
        kpa2hpa = function (val) {
            return (extractDecimal(val) * 10).toFixed(1);
        },
        // hPa to kPa
        hpa2kpa = function (val, decimals) {
            return (extractDecimal(val) / 10).toFixed(decimals || 2);
        },
        // m to ft
        m2ft = function (val) {
            return (val * 3.2808399).toFixed(0);
        },
        // ft to m
        ft2m = function (val) {
            return (val / 3.2808399).toFixed(0);
        },

        //
        // setCookie() writes the 'obj' in cookie 'name' for persistent storage
        //
        setCookie = function (name, obj) {
            var date = new Date(),
                expires;
            // cookies valid for 1 year
            date.setYear(date.getFullYear() + 1);
            expires = '; expires=' + date.toGMTString();
            document.cookie = name + '=' + encodeURIComponent(JSON.stringify(obj)) + expires;
        },

        //
        // getCookie() reads the value of cookie 'name' from persistent storage
        //
        getCookie = function (name) {
            var i, x, y,
                ret = null,
                arrCookies = document.cookie.split(';');

            for (i = arrCookies.length; i--;) {
                x = arrCookies[i].split('=');
                if (x[0].trim() === name) {
                    try {
                        y = decodeURIComponent(x[1]);
                    } catch (e) {
                        y = x[1];
                    }
                    ret = JSON.parse(unescape(y));
                    break;
                }
            }
            return ret;
        },

        //
        // setRadioCheck() sets the desired value of the HTML radio buttons to be selected
        //
        setRadioCheck = function (obj, val) {
            $('input:radio[name="' + obj + '"]').filter('[value="' + val + '"]').prop('checked', true);
        },

        //
        // convTempData() converts all the temperature values using the supplied conversion function
        //
        convTempData = function (convFunc) {
            data.apptemp = convFunc(data.apptemp);
            data.apptempTH = convFunc(data.apptempTH);
            data.apptempTL = convFunc(data.apptempTL);
            data.feelslike = convFunc(data.feelslike);
            data.feelslikeTH = convFunc(data.feelslikeTH);
            data.feelslikeTL = convFunc(data.feelslikeTL);
            data.dew = convFunc(data.dew);
            data.dewpointTH = convFunc(data.dewpointTH);
            data.dewpointTL = convFunc(data.dewpointTL);
            data.heatindex = convFunc(data.heatindex);
            data.heatindexTH = convFunc(data.heatindexTH);
            data.humidex = convFunc(data.humidex);
            data.intemp = convFunc(data.intemp);
            if (data.intempTL && data.intempTH) {
                data.intempTL = convFunc(data.intempTL);
                data.intempTH = convFunc(data.intempTH);
            }
            data.temp = convFunc(data.temp);
            data.tempTH = convFunc(data.tempTH);
            data.tempTL = convFunc(data.tempTL);
            data.wchill = convFunc(data.wchill);
            data.wchillTL = convFunc(data.wchillTL);
            if (convFunc === c2f) {
                data.temptrend = (+extractDecimal(data.temptrend) * 9 / 5).toFixed(1);
                data.tempunit = '°F';
            } else {
                data.temptrend = (+extractDecimal(data.temptrend) * 5 / 9).toFixed(1);
                data.tempunit = '°C';
            }
        },

        //
        // convRainData() converts all the rain data units using the supplied conversion function
        //
        convRainData = function (convFunc) {
            data.rfall = convFunc(data.rfall);
            data.rrate = convFunc(data.rrate);
            data.rrateTM = convFunc(data.rrateTM);
            data.hourlyrainTH = convFunc(data.hourlyrainTH);
            data.rainunit = convFunc === mm2in ? 'in' : 'mm';
        },

        //
        // convWindData() converts all the wind values using the supplied conversion function
        //
        convWindData = function (from, to) {
            var fromFunc1, toFunc1,
                fromFunc2, toFunc2,
                dummy = function (val) {
                    return val;
                };

            // convert to km/h & km
            switch (from) {
            case 'mph':
                fromFunc1 = miles2km;
                fromFunc2 = miles2km;
                break;
            case 'kts':
                fromFunc1 = nmiles2km;
                fromFunc2 = nmiles2km;
                break;
            case 'm/s':
                fromFunc1 = ms2kmh;
                fromFunc2 = dummy;
                break;
            case 'km/h':
            // falls through
            default:
                fromFunc1 = dummy;
                fromFunc2 = dummy;
            }
            // conversion function from km to required units
            switch (to) {
            case 'mph':
                toFunc1 = km2miles;
                toFunc2 = km2miles;
                displayUnits.windrun = 'miles';
                break;
            case 'kts':
                toFunc1 = km2nmiles;
                toFunc2 = km2nmiles;
                displayUnits.windrun = 'n.miles';
                break;
            case 'm/s':
                toFunc1 = kmh2ms;
                toFunc2 = dummy;
                displayUnits.windrun = 'km';
                break;
            case 'km/h':
            // falls through
            default:
                toFunc1 = dummy;
                toFunc2 = dummy;
                displayUnits.windrun = 'km';
            }
            // do the conversions
            data.wgust = toFunc1(fromFunc1(data.wgust));
            data.wgustTM = toFunc1(fromFunc1(data.wgustTM));
            data.windTM = toFunc1(fromFunc1(data.windTM));
            data.windrun = toFunc2(fromFunc2(data.windrun));
            data.wlatest = toFunc1(fromFunc1(data.wlatest));
            data.wspeed = toFunc1(fromFunc1(data.wspeed));
            data.windunit = to;
        },

        //
        // convBaroData() converts all the pressure values using the supplied conversion function
        //
        convBaroData = function (from, to) {
            var fromFunc, toFunc,
                dummy = function (val) {
                    return val;
                };

            // convert to hPa
            switch (from) {
            case 'hPa':
            // falls through
            case 'mb':
                fromFunc = dummy;
                break;
            case 'inHg':
                fromFunc = inhg2hpa;
                break;
            case 'kPa':
                fromFunc = kpa2hpa;
                break;
            // no default
            }
            // convert to required units
            switch (to) {
            case 'hPa':
            // falls through
            case 'mb':
                toFunc = dummy;
                break;
            case 'inHg':
                toFunc = hpa2inhg;
                break;
            case 'kPa':
                toFunc = hpa2kpa;
                break;
            // no default
            }

            data.press = toFunc(fromFunc(data.press));
            data.pressH = toFunc(fromFunc(data.pressH));
            data.pressL = toFunc(fromFunc(data.pressL));
            data.pressTH = toFunc(fromFunc(data.pressTH));
            data.pressTL = toFunc(fromFunc(data.pressTL));
            data.presstrendval = toFunc(fromFunc(data.presstrendval), 3);
            data.pressunit = to;
        },

        //
        // convCloudBaseData() converts all the cloud base data units using the supplied conversion function
        //
        convCloudBaseData = function (convFunc) {
            data.cloudbasevalue = convFunc(data.cloudbasevalue);
            data.cloudbaseunit = convFunc === m2ft ? 'ft' : 'm';
        },

        //
        // setUnits() Main data conversion routine, calls all the setXXXX() sub-routines
        //
        setUnits = function (radio) {
            var sel = radio.value;

            userUnitsSet = true;

            switch (sel) {
            // == Temperature ==
            case 'C':
                displayUnits.temp = sel;
                if (data.tempunit[1] !== sel) {
                    setTempUnits(true);
                    convTempData(f2c);
                    if (gaugeTemp) {gaugeTemp.update();}
                    if (gaugeDew) {gaugeDew.update();}
                }
                break;
            case 'F':
                displayUnits.temp = sel;
                if (data.tempunit[1] !== sel) {
                    setTempUnits(false);
                    convTempData(c2f);
                    if (gaugeTemp) {gaugeTemp.update();}
                    if (gaugeDew) {gaugeDew.update();}
                }
                break;
            // == Rainfall ==
            case 'mm':
                displayUnits.rain = sel;
                if (data.rainunit !== sel) {
                    setRainUnits(true);
                    convRainData(in2mm);
                    if (gaugeRain) {gaugeRain.update();}
                    if (gaugeRRate) {gaugeRRate.update();}
                }
                break;
            case 'in':
                displayUnits.rain = sel;
                if (data.rainunit !== sel) {
                    setRainUnits(false);
                    convRainData(mm2in);
                    if (gaugeRain) {gaugeRain.update();}
                    if (gaugeRRate) {gaugeRRate.update();}
                }
                break;
            // == Pressure ==
            case 'hPa':
            // falls through
            case 'inHg':
            // falls through
            case 'mb':
            // falls through
            case 'kPa':
                displayUnits.press = sel;
                if (data.pressunit !== sel) {
                    convBaroData(data.pressunit, sel);
                    setBaroUnits(sel);
                    if (gaugeBaro) {gaugeBaro.update();}
                }
                break;
            // == Wind speed ==
            case 'mph':
            // falls through
            case 'kts':
            // falls through
            case 'm/s':
            // falls through
            case 'km/h':
                displayUnits.wind = sel;
                if (data.windunit !== sel) {
                    convWindData(data.windunit, sel);
                    setWindUnits(sel);
                    if (gaugeWind) {gaugeWind.update();}
                    if (gaugeDir) {gaugeDir.update();}
                    if (gaugeRose) {gaugeRose.update();}
                }
                break;
            // == CloudBase ==
            case 'm':
                displayUnits.cloud = sel;
                if (data.cloudbaseunit !== sel) {
                    setCloudBaseUnits(true);
                    convCloudBaseData(ft2m);
                    if (gaugeCloud) {gaugeCloud.update();}
                }
                break;
            case 'ft':
                displayUnits.cloud = sel;
                if (data.cloudbaseunit !== sel) {
                    setCloudBaseUnits(false);
                    convCloudBaseData(m2ft);
                    if (gaugeCloud) {gaugeCloud.update();}
                }
                break;
            // no default
            }
            if (config.useCookies) {
                setCookie('units', displayUnits);
            }
        },

        setTempUnits = function (celsius) {
            if (celsius) {
                data.tempunit = '°C';
                if (gaugeTemp) {
                    gaugeTemp.data.sections = createTempSections(true);
                    gaugeTemp.data.minValue = gaugeGlobals.tempScaleDefMinC;
                    gaugeTemp.data.maxValue = gaugeGlobals.tempScaleDefMaxC;
                }
                if (gaugeDew) {
                    gaugeDew.data.sections = createTempSections(true);
                    gaugeDew.data.minValue = gaugeGlobals.tempScaleDefMinC;
                    gaugeDew.data.maxValue = gaugeGlobals.tempScaleDefMaxC;
                }
            } else {
                data.tempunit = '°F';
                if (gaugeTemp) {
                    gaugeTemp.data.sections = createTempSections(false);
                    gaugeTemp.data.minValue = gaugeGlobals.tempScaleDefMinF;
                    gaugeTemp.data.maxValue = gaugeGlobals.tempScaleDefMaxF;
                }
                if (gaugeDew) {
                    gaugeDew.data.sections = createTempSections(false);
                    gaugeDew.data.minValue = gaugeGlobals.tempScaleDefMinF;
                    gaugeDew.data.maxValue = gaugeGlobals.tempScaleDefMaxF;
                }
            }
            if (gaugeTemp) {
                gaugeTemp.gauge.setUnitString(data.tempunit);
                gaugeTemp.gauge.setSection(gaugeTemp.data.sections);
            }
            if (gaugeDew) {
                gaugeDew.gauge.setUnitString(data.tempunit);
                gaugeDew.gauge.setSection(gaugeTemp.data.sections);
            }
        },

        setRainUnits = function (mm) {
            if (mm) {
                data.rainunit = 'mm';
                if (gaugeRain) {
                    gaugeRain.data.lcdDecimals = 1;
                    gaugeRain.data.scaleDecimals = 1;
                    gaugeRain.data.labelNumberFormat = gaugeGlobals.labelFormat;
                    gaugeRain.data.sections = (gaugeGlobals.rainUseSectionColours ? createRainfallSections(true) : []);
                    gaugeRain.data.maxValue = gaugeGlobals.rainScaleDefMaxmm;
                    gaugeRain.data.grad = (gaugeGlobals.rainUseGradientColours ? createRainfallGradient(true) : null);
                }
                if (gaugeRRate) {
                    gaugeRRate.data.lcdDecimals = 1;
                    gaugeRRate.data.scaleDecimals = 0;
                    gaugeRRate.data.labelNumberFormat = gaugeGlobals.labelFormat;
                    gaugeRRate.data.sections = createRainRateSections(true);
                    gaugeRRate.data.maxValue = gaugeGlobals.rainRateScaleDefMaxmm;
                }
            } else {
                data.rainunit = 'in';
                if (gaugeRain) {
                    gaugeRain.data.lcdDecimals = 2;
                    gaugeRain.data.scaleDecimals = gaugeGlobals.rainScaleDefMaxIn < 1 ? 2 : 1;
                    gaugeRain.data.labelNumberFormat = steelseries.LabelNumberFormat.FRACTIONAL;
                    gaugeRain.data.sections = (gaugeGlobals.rainUseSectionColours ? createRainfallSections(false) : []);
                    gaugeRain.data.maxValue = gaugeGlobals.rainScaleDefMaxIn;
                    gaugeRain.data.grad = (gaugeGlobals.rainUseGradientColours ? createRainfallGradient(false) : null);
                }
                if (gaugeRRate) {
                    gaugeRRate.data.lcdDecimals = 2;
                    gaugeRRate.data.scaleDecimals = gaugeGlobals.rainRateScaleDefMaxIn < 1 ? 2 : 1;
                    gaugeRRate.data.labelNumberFormat = steelseries.LabelNumberFormat.FRACTIONAL;
                    gaugeRRate.data.sections = createRainRateSections(false);
                    gaugeRRate.data.maxValue = gaugeGlobals.rainRateScaleDefMaxIn;
                }
            }
            if (gaugeRain) {
                gaugeRain.data.value = 0;
                gaugeRain.gauge.setUnitString(data.rainunit);
                gaugeRain.gauge.setSection(gaugeRain.data.sections);
                gaugeRain.gauge.setGradient(gaugeRain.data.grad);
                gaugeRain.gauge.setFractionalScaleDecimals(gaugeRain.data.scaleDecimals);
                gaugeRain.gauge.setLabelNumberFormat(gaugeRain.data.labelNumberFormat);
                gaugeRain.gauge.setLcdDecimals(gaugeRain.data.lcdDecimals);
                gaugeRain.gauge.setValue(0);
                gaugeRain.gauge.setMaxValue(gaugeRain.data.maxValue);
            }
            if (gaugeRRate) {
                gaugeRRate.data.value = 0;
                gaugeRRate.gauge.setUnitString(data.rainunit + '/h');
                gaugeRRate.gauge.setSection(gaugeRRate.data.sections);
                gaugeRRate.gauge.setFractionalScaleDecimals(gaugeRRate.data.scaleDecimals);
                gaugeRRate.gauge.setLabelNumberFormat(gaugeRRate.data.labelNumberFormat);
                gaugeRRate.gauge.setLcdDecimals(gaugeRRate.data.lcdDecimals);
                gaugeRRate.gauge.setValue(0);
                gaugeRRate.gauge.setMaxValue(gaugeRRate.data.maxValue);
            }
        },

        setWindUnits = function (to) {
            var maxVal;
            data.windunit = to;
            if (gaugeWind) {
                // conversion function to required units
                switch (to) {
                case 'mph':
                    maxVal = gaugeGlobals.windScaleDefMaxMph;
                    break;
                case 'kts':
                    maxVal = gaugeGlobals.windScaleDefMaxKts;
                    break;
                case 'km/h':
                    maxVal = gaugeGlobals.windScaleDefMaxKmh;
                    break;
                case 'm/s':
                    maxVal = gaugeGlobals.windScaleDefMaxMs;
                    break;
                // no default
                }
                // set the gauges
                gaugeWind.data.maxValue = maxVal;
                gaugeWind.gauge.setUnitString(data.windunit);
                gaugeWind.gauge.setValue(0);
            }
            if (gaugeRose) {
                gaugeRose.setOdoTitle(strings[getWindrunUnits(data.windunit)]);
            }
        },

        setBaroUnits = function (to) {
            var minVal, maxVal;

            if (!gaugeBaro) {return;}

            // set to the required units
            switch (to) {
            case 'hPa':
            // falls through
            case 'mb':
                minVal = gaugeGlobals.baroScaleDefMinhPa;
                maxVal = gaugeGlobals.baroScaleDefMaxhPa;
                gaugeBaro.data.lcdDecimals = 1;
                gaugeBaro.data.scaleDecimals = 0;
                gaugeBaro.data.labelNumberFormat = gaugeGlobals.labelFormat;
                break;
            case 'inHg':
                minVal = gaugeGlobals.baroScaleDefMininHg;
                maxVal = gaugeGlobals.baroScaleDefMaxinHg;
                gaugeBaro.data.lcdDecimals = 2;
                gaugeBaro.data.scaleDecimals = 1;
                gaugeBaro.data.labelNumberFormat = steelseries.LabelNumberFormat.FRACTIONAL;
                break;
            case 'kPa':
                minVal = gaugeGlobals.baroScaleDefMinkPa;
                maxVal = gaugeGlobals.baroScaleDefMaxkPa;
                gaugeBaro.data.lcdDecimals = 2;
                gaugeBaro.data.scaleDecimals = 1;
                gaugeBaro.data.labelNumberFormat = steelseries.LabelNumberFormat.FRACTIONAL;
                break;
            // no default
            }

            data.pressunit = to;
            gaugeBaro.gauge.setUnitString(to);
            gaugeBaro.gauge.setLcdDecimals(gaugeBaro.data.lcdDecimals);
            gaugeBaro.gauge.setFractionalScaleDecimals(gaugeBaro.data.scaleDecimals);
            gaugeBaro.gauge.setLabelNumberFormat(gaugeBaro.data.labelNumberFormat);
            gaugeBaro.data.minValue = minVal;
            gaugeBaro.data.maxValue = maxVal;
            gaugeBaro.data.value = gaugeBaro.data.minValue;
        },

        setCloudBaseUnits = function (m) {
            if (!gaugeCloud) {return;}

            if (m) {
                gaugeCloud.data.sections = createCloudBaseSections(true);
                gaugeCloud.data.maxValue = gaugeGlobals.cloudScaleDefMaxm;
            } else {
                gaugeCloud.data.sections = createCloudBaseSections(false);
                gaugeCloud.data.maxValue = gaugeGlobals.cloudScaleDefMaxft;
            }
            gaugeCloud.data.value = 0;
            gaugeCloud.gauge.setUnitString(m ? strings.metres : strings.feet);
            gaugeCloud.gauge.setSection(gaugeCloud.data.sections);
        },

        //
        // setLang() switches the HTML page language set, called by changeLang() in language.js
        //
        setLang = function (newLang) {
            // reset to the new language
            strings = newLang;

            // temperature
            if (gaugeTemp) {
                if (config.showIndoorTempHum) {
                    if ($('#rad_temp1').is(':checked')) {
                        gaugeTemp.data.title = strings.temp_title_out;
                    } else {
                        gaugeTemp.data.title = strings.temp_title_in;
                    }
                } else {
                    gaugeTemp.data.title = strings.temp_title_out;
                }
                gaugeTemp.gauge.setTitleString(gaugeTemp.data.title);
                if (data.ver) {gaugeTemp.update();}
            }
            if (gaugeDew) {
                switch ($('input[name="rad_dew"]:checked').val()) {
                case 'dew':
                    gaugeDew.data.title = strings.dew_title;
                    break;
                case 'app':
                    gaugeDew.data.title = strings.apptemp_title;
                    break;
                case 'feel':
                    gaugeDew.data.title = strings.feel_title;
                    break;
                case 'wnd':
                    gaugeDew.data.title = strings.chill_title;
                    break;
                case 'hea':
                    gaugeDew.data.title = strings.heat_title;
                    break;
                case 'hum':
                    gaugeDew.data.title = strings.humdx_title;
                    break;
                // no default
                }
                gaugeDew.gauge.setTitleString(gaugeDew.data.title);
                if (data.ver) {gaugeDew.update();}
            }
            // rain
            if (gaugeRain) {
                gaugeRain.data.title = strings.rain_title;
                gaugeRain.gauge.setTitleString(gaugeRain.data.title);
                if (data.ver) {gaugeRain.update();}
            }
            // rrate
            if (gaugeRRate) {
                gaugeRRate.data.title = strings.rrate_title;
                gaugeRRate.gauge.setTitleString(gaugeRRate.data.title);
                if (data.ver) {gaugeRRate.update();}
            }
            // humidity
            if (gaugeHum) {
                if (config.showIndoorTempHum) {
                    if ($('#rad_hum1').is(':checked')) {
                        gaugeHum.data.title = strings.hum_title_out;
                    } else {
                        gaugeHum.data.title = strings.hum_title_in;
                    }
                } else {
                    gaugeHum.data.title = strings.hum_title_out;
                }
                gaugeHum.gauge.setTitleString(gaugeHum.data.title);
                if (data.ver) {gaugeHum.update();}
            }
            // barometer
            if (gaugeBaro) {
                gaugeBaro.data.title = strings.baro_title;
                gaugeBaro.gauge.setTitleString(gaugeBaro.data.title);
                if (data.ver) {gaugeBaro.update();}
            }
            // wind
            if (gaugeWind) {
                gaugeWind.data.title = strings.wind_title;
                gaugeWind.gauge.setTitleString(gaugeWind.data.title);
                if (data.ver) {gaugeWind.update();}
            }
            if (gaugeDir) {
                gaugeDir.gauge.setPointSymbols(strings.compass);
                gaugeDir.data.titles = [strings.latest_web, strings.tenminavg_web];
                gaugeDir.gauge.setLcdTitleStrings(gaugeDir.data.titles);
                if (data.ver) {gaugeDir.update();}
            }
            if (gaugeUV) {
                gaugeUV.gauge.setTitleString(strings.uv_title);
                if (data.ver) {gaugeUV.update();}
            }
            if (gaugeSolar) {
                gaugeSolar.gauge.setTitleString(strings.solar_title);
                if (data.ver) {gaugeSolar.update();}
            }
            if (gaugeRose) {
                gaugeRose.setTitle(strings.windrose);
                gaugeRose.setCompassStrings(strings.compass);
                gaugeRose.setOdoTitle(strings[getWindrunUnits(displayUnits.wind)]);
                if (data.ver) {gaugeRose.update();}
            }
            if (gaugeCloud) {
                // Cloudbase
                gaugeCloud.data.units = data.cloudunit === 'm' ? strings.metres : strings.feet;
                gaugeCloud.gauge.setTitleString(strings.cloudbase_title);
                gaugeCloud.gauge.setUnitString(gaugeCloud.data.units);
                if (data.ver) {gaugeCloud.update();}
            }
        },

        //
        // return windrun units based on the windspeed units
        //
        getWindrunUnits = function (spdUnits) {
            var retVal;
            switch (spdUnits) {
            case 'mph':
                retVal = 'miles';
                break;
            case 'kts':
                retVal = 'n_miles';
                break;
            case 'km/h':
            // falls through
            case 'm/s':
            // falls through
            default:
                retVal = 'km';
                break;
            }
            return retVal;
        },

        //
        // performs a simple cloudbase calculation for those weather programs that don't supply it
        //
        calcCloudbase = function (temp, tempunit, dew, cloudbaseunit) {
            var sprd = temp - dew;
            var cb = sprd * (tempunit[1] === 'C' ? 400 : 227.3); // cloud base in feet
            if (cloudbaseunit === 'm') {
                cb = ft2m(cb);
            }
            return cb;
        },

        //
        // create a shadow effect for the gauge using CSS
        //
        gaugeShadow = function (size) {
            var offset = Math.floor(size * 0.015);
            return {
                'box-shadow'   : offset + 'px ' + offset + 'px ' + offset + 'px ' + gaugeGlobals.shadowColour,
                'border-radius': Math.floor(size / 2) + 'px'
            };
        },

        //
        // generate a colour gradient based on start and end values
        //
        gradient = function (startCol, endCol, fraction) {
            var redOrigin, grnOrigin, bluOrigin,
                gradientSizeRed, gradientSizeGrn, gradientSizeBlu;

            redOrigin = parseInt(startCol.substr(0, 2), 16);
            grnOrigin = parseInt(startCol.substr(2, 2), 16);
            bluOrigin = parseInt(startCol.substr(4, 2), 16);

            gradientSizeRed = parseInt(endCol.substr(0, 2), 16)  - redOrigin; // Graduation Size Red
            gradientSizeGrn = parseInt(endCol.substr(2, 2), 16)  - grnOrigin;
            gradientSizeBlu = parseInt(endCol.substr(4, 2), 16)  - bluOrigin;

            return (redOrigin + (gradientSizeRed * fraction)).toFixed(0) + ',' +
                (grnOrigin + (gradientSizeGrn * fraction)).toFixed(0) + ',' +
                (bluOrigin + (gradientSizeBlu * fraction)).toFixed(0);
        },
        //
        // returns the next highest number in the step sequence
        //
        nextHighest = function (value, step) {
            return +value == 0 ? step : Math.ceil(+value / step) * step;
        },
        //
        // returns the next lowest number in the step sequence
        //
        nextLowest = function (value, step) {
            return +value == 0 ? -step : Math.floor(+value / step) * step;
        };
        // ########################################################
        // End of gauges() var declarations
        // ########################################################

    //
    // Execution starts here
    //

    // test for canvas support before we do anything else, especially reference steelseries which will cause the script to abort!
    if (!document.createElement('canvas').getContext) {
        // failed, no canvas support detected
        $('body').html(strings.canvasnosupport);
        setTimeout(function () {
            window.location = config.oldGauges;
        }, 3000);
        return false;
    } else {
        //
        // Called when the document object has loaded
        // This starts the whole script.
        //
        $().ready(function () {
            dataLoadedPromise.then(function() {
                // Kick it all off - false for web page, true for dashboard
                init(config.dashboardMode);

            });
        });
    }

    return {
        setLang    : setLang,
        setUnits   : setUnits,
        processData: processData,
        config     : config,
        init       : init
    };
}());

// ===============================================================================================================================
// ===============================================================================================================================
// ===============================================================================================================================

    /*!
    * Image w/ description tooltip v2.0  -  For FF1+ IE6+ Opr8+
    * Created: April 23rd, 2010. This notice must stay intact for usage
    * Author: Dynamic Drive at http://www.dynamicdrive.com/
    * Visit http://www.dynamicdrive.com/ for full source code
    * Modified: M Crossley June 2011, January 2012
    * v2.-
    */
var ddimgtooltip;
ddimgtooltip = {
    tiparray: (function () {
        var style = {background: '#FFFFFF', color: 'black', border: '2px ridge darkblue'},
            i = 12,  // set to number of tooltips required
            tooltips = [];
        for (;i--;) {
            tooltips[i] = [null, ' ', style];
        }
        return tooltips;
    }()),

    tooltipoffsets: [20, -30], // additional x and y offset from mouse cursor for tooltips

    tipDelay: 1000,

    delayTimer: 0,

    tipprefix: 'imgtip', // tooltip DOM ID prefixes

    createtip: function ($, tipid, tipinfo) {
        if ($('#' + tipid).length === 0) { // if this tooltip doesn't exist yet
            return $('<div id="' + tipid + '" class="ddimgtooltip" />')
                .html(
                    ((tipinfo[1]) ? '<div class="tipinfo" id="' + tipid + '_txt">' + tipinfo[1] + '</div>' : '') +
                    (tipinfo[0] !== null ? '<div style="text-align:center"><img class="tipimg" id="' + tipid +
                    '_img" src="' + tipinfo[0] + '" /></div>' : '')
                )
                .css(tipinfo[2] || {})
                .appendTo(document.body);
        }
        return null;
    },

    positiontooltip: function ($, $tooltip, e) {
        var x = e.pageX + this.tooltipoffsets[0],
            y = e.pageY + this.tooltipoffsets[1],
            tipw = $tooltip.outerWidth(),
            tiph = $tooltip.outerHeight(),
            wWidth = $(window).width(),
            wHght = $(window).height(),
            dTop = $(document).scrollTop();

        x = (x + tipw > $(document).scrollLeft() + wWidth) ? x - tipw - (ddimgtooltip.tooltipoffsets[0] * 2) : x;
        y = (y + tiph > dTop + wHght) ? dTop + wHght - tiph - 10 : y;
        // last ditch attempt to keep the graphs 'on page'
        x = Math.max(x, 0);
        if (tipw >= wWidth) {
            $($tooltip.attr('id') + '_img').css({width: wWidth - 20});
            $('#' + $tooltip.attr('id') + '_img').css({width: wWidth - 20});
            y = e.pageY + 5;
        }
        $tooltip.css({left: x, top: y});
    },

    delaybox: function ($, $tooltip) {
        if (this.showTips) {
            ddimgtooltip.delayTimer = setTimeout(
                function () {
                    ddimgtooltip.showbox($tooltip);
                }, ddimgtooltip.tipDelay);
        }
    },

    showbox: function (tooltip) {
        if (this.showTips) {
            // $(tooltip).show();
            $(tooltip).fadeIn();
        }
    },

    hidebox: function ($, $tooltip) {
        clearTimeout(ddimgtooltip.delayTimer);
        // $tooltip.hide();
        $tooltip.fadeOut();
    },

    showTips: false,

    init: function (targetselector) {
        var tiparray = ddimgtooltip.tiparray,
            $targets = $(targetselector);

        if ($targets.length === 0) {
            return;
        }
        $targets.each(function () {
            var $target = $(this),
                tipsuffix, tipid,
                $tooltip;
            var ind = ($target.attr('id').match(/_(\d+)/) || [])[1] || ''; // match d of attribute id='tip_d'
            tipsuffix = parseInt(ind, 10); // get d as integer
            tipid = this.tipid = ddimgtooltip.tipprefix + tipsuffix; // construct this tip's ID value and remember it
            $tooltip = ddimgtooltip.createtip($, tipid, tiparray[tipsuffix]);

            $target.mouseenter(function (e) {
                var $tooltip = $('#' + this.tipid);
                // ddimgtooltip.showbox($, $tooltip, e);
                ddimgtooltip.delaybox($, $tooltip, e);
            });
            $target.mouseleave(function () {
                var $tooltip = $('#' + this.tipid);
                ddimgtooltip.hidebox($, $tooltip);
            });
            $target.mousemove(function (e) {
                var $tooltip = $('#' + this.tipid);
                ddimgtooltip.positiontooltip($, $tooltip, e);
            });
            if ($tooltip) { // add mouseenter to this tooltip (only if event hasn't already been added)
                $tooltip.mouseenter(function () {
                    ddimgtooltip.hidebox($, $(this));
                });
            }
        });
    }
};

// if String doesn't offer a .trim() method, add it
String.prototype.trim = String.prototype.trim || function trim() {
    return this.replace(/^\s+|\s+$/g, '');
};
