//
// Javascript for GODIVA2 page.
//

var map = null;
var zPositive = false; // Will be true if the selected z axis is positive
var calendar = null; // The calendar object
var datesWithData = null; // Will be populated with the dates on which we have data
                          // for the currently-selected variable
var isoTValue = null; // The currently-selected t value (ISO8601)
var isIE;
var scaleMinVal;
var scaleMaxVal;
var gotScaleRange = false;
var scaleLocked = false; // see toggleLockScale()
var autoLoad = new Object(); // Will contain data for auto-loading data from a permalink
var menu = ''; // The menu that is being displayed (e.g. "mersea", "ecoop")
var bbox = null; // The bounding box of the currently-displayed layer
var featureInfoUrl = null; // The last-called URL for getFeatureInfo (following a click on the map)

var layerSwitcher = null;
var ncwms = null; // Points to the currently-active layer that is coming from this ncWMS
                  // Will point to either ncwms_tiled or ncwms_untiled.
var ncwms_tiled = null; // We shall maintain two separate layers, one tiles (for scalar
var ncwms_untiled = null; // quantities) and one untiled (for vector quantities)

var animation_layer = null; // The layer that will be used to display animations

var servers = ['']; // URLs to the servers from which we will display layers
                    // An empty string means the server that is serving this page.
var activeLayer = null; // The currently-selected layer metadata

var tree = null; // The tree control in the left-hand panel

var paletteSelector = null; // Pop-up panel for selecting a new palette
var paletteName = null; // Name of the currently-selected palette
var baseLayer = null;

//AG Ecosystem vars - tidy up
var obsDetailsUrl = null; // The last-called URL for getObsDetails (when loading layer with obs)
var markersLayer = null;
var popup = null;
var metadataPopup = null;
var loadingObsPopup = null;
var features = [];
var markers = [];
var activeFeature = null;
var modelPointLon = null;
var modelPointLat = null;
var obsPointLon = null;
var obsPointLat = null;
var obsPointVal = null;
var obsTimes = [];
var obsValues = [];

var gotObsData = false;
var gotModelData = false;

var obsPopupContent = "";
var modelPopupContent = "";
var obsTimesCommaStr = "";
var obsValuesCommaStr = "";
var modelValsCommaStr = "";
var popupPresent = false;
var popupContent = "";
var loadingStrings = new Array("/", "-", "\\", "|");
var loadingIndex = 0;
var loadingId;
var loadingObsId;

var loadObsWhenAutoScale = true;
var autoScaleWhenUpdateMap = true;

var debugTimes = "";

var firstDataDay = 0;
var datesArr = new Array();
var modelTimeBounds;
var feature;
//var obsIds = new Array("62052","62114","62125","62142","62144","62145","62147","62164","62166","63110","63112","63113","AARHUS","ABERDEEN","ABERPORTHBUOY","AKKAERT","ALBORAN","ALESUND","ALICANTECOAST","ALMEIRA","AMELANDERZEEGATBOEI1-1","AMELANDERZEEGATBOEI1-2","AMELANDERZEEGATBOEI2-1","AMELANDERZEEGATBOEI3-1","AMELANDERZEEGATBOEI3-2","AMELANDERZEEGATBOEI4-2","AMELANDERZEEGATBOEI5-1","AMELANDERZEEGATBOEI5-2","ANASURIA","ARINAGA","ARKONA","ARKONAWR","ARKONA","ATHOS","AUKFIELD2","BALLEN","BARMOUTH","BARSEBACK","BERGEN","BILBAOCOST","BILBAO","BODO","BOLVANHEIST","BORKUM","BOURNEMOUTH","BREMERHAVEN","BREST","BRITTANYBUOY","BROUWERSHAVENSEGAT1","BROUWERSHAVENSEGAT2","BROUWERSHAVENSEGAT","CABOBEGURMOON","CABODEGATA","CABODEPALOS","CABODEPENAS","CABOSILLEIRO","CADIZCOST","CADIZ","CADZAND","CAPDEPERA","CASLETOWNBERE","CEUTA","CHANNELLIGHTSHIP","CHERBOURG","CORUNA","CROMER","CUXHAVEN","DARSSERSILLWR","DARSSSILL","DAUGAGRIVA","DBUCHT","DEGERBY","DELFZIJL","DENHELDER","DENOEVERBUITEN","DEURLO","DORDRECHT","DOVER","DRAGONERAMOON","DRAGONERA","DROGDEN","DUBLINPORT","DUNDALK","E1M3ACRETANSEA","EDAM","EEMSHAVEN","EMS","ESTACADEBARES","EUROGEULDWE","EUROGEULE13","EUROGEULE5","EUROPLATFORM2","EUROPLATFORM3","EUROPLATFORM","FEHMARNBELT","FELIXSTOWE","FERRINGHAVN","FINO1","FISHGUARD","FORSMARK","FREDERICIA","FUERTEVENTURA","FURUOGRUND","FYNSHAV","GALWAYPORT","GEDSER","GIJONCOST","GIJONEXT","GIJON","GOTEBORG","GRANADILLA","GRANCANARIA","GREENWICHLIGHTSHIP","GRENA","HAMMERFEST","HANSTHOLM","HARINGVLIET","HARLINGEN","HARSTAD","HAVNEBYROMO","HEIMSJO","HELGEROA","HELGOLAND","HELSINKI","HEYSHAM","HIERRO","HINKLEYPOINT","HOEKVANHOLLANDNAP","HOEKVANHOLLAND","HOLLANDSEBRUG","HOLYHEAD","HONNINGSVAG","HORNBAEK","HOUTRIBSLUIZENZUID","HOWTHHARBOUR","HUELVA","HUIBERTGAT","HUSUM","HUVUDSKAR","HVIDESANDEHAVN","IBIZA","IJGEUL5","IJGEULSTROOMPAAL","IJMUIDENMUNITIESTORT1","IJMUIDENMUNITIESTORT2","IJMUIDEN","ILFRACOMBE","IMMINGHAM","INISHMORE","JUELSMINDE","K13PLATFORM2","K13PLATFORM3","K13PLATFORM","K1BUOY","K2BUOY","K5BUOY","K7BUOY","KABELVAG","KADOELEN","KALAMATA","KALIX","KAMPEN","KAMPERHOEK","KASKINEN","KATERVEER","KEMI","KETELHAVEN","KIEL-HOLTENAU","KIEL","KILLYBEGSPORT","KISH","KLAGSHAMN","KLAIPEDA","KOBENHAVN","KOLKA","KOPER","KORNWERDERZANDBINNEN","KORNWERDERZANDBUITEN","KORNWERDERZAND","KORSOR","KOSEROW","KRABBERSGATSLUIZENZUID","KRISTIANSUND","KRONSTADT","KUNGSHOLMSFORT","KUNGSVIK","LANDSORTNORRA","LANGOSTEIRA","LASO","LASPALMASESTE","LASPALMAS","LAUWERSOOG","LECONQUET","LEHAVRE","LEHTMA","LEITH","LESVOS","LICHTEILANDGOEREE2","LICHTEILANDGOEREE","LIONBUOY","LIVBAY","LIVERPOOL","LOWESTOFT","M2EOFLAMBAY","M3SWMIZENHEAD","M4BOUYDONEGALBAY","M5SOUTHEAST","M6","MAHONMOON","MALAGA","MALINHEAD","MALOY","MALTAMEDGLOSSPORTOMASO","MARVIKEN","MILFORDHAVEN","MUMBLES","MYKONOS","NARVIK","NBALTIC","NEWHAVEN","NEWLYN","NICEBUOY","NIEUWSTATENZIJL","NIIJKERKERSLUIS","NOORDWIJKMEETPOST1","NORDREROSE","NORTHSHIELDS","NYALESUND","ODERBANK","OLST","OOSTENDE","OOSTERSCHELDE11","OOSTERSCHELDED4","OSCARSBORG","OSKARSHAMN","OSLO","OYSTERGRD","PALAMOS","PALDISKI","PARNU","PEMBROKEBUOY","PIETARSAARI","PLYMOUTH","PYLOS","RAMSPOLBRUG","RATAN","RINGHALS","RIVERDODDER","RIVERLIFFEY","RODBY","RODVIG","ROGGEBOTSLUISNOORD","ROOMPOTBUITEN","RORVIK","ROTTERDAM","SANDETTIELIGHTSHIP","SANTACRUZ","SANTANDER","SANTORINI","SARONIKOS","SASSNITZ","SBALTIC","SBOTHNIAN","SCHELLINGWOUDE","SCHEUROOST","SCHEURWEST","SCHIERMONNIKOOGWESTGAT","SCHIERMONNIKOOG","SCHOUWENBANK","SERVILLABONANZA","SERVILLA","SEVENSTONELIGHTSHIP","SHEERNESS","SILLAMAE","SIMRISHAMN","SJAELLANDSODDE","SKAGEN","SKANOR","SKERRIESHARBOUR","SKYROS","SLIPSHAVN","SMOGEN","SORU","SPIJKENISSE","SPIKARNA","SPOLDERSLUIS","STAVANGER","STAVOREN","STOCKHOLM","STORNOWAY","STPETERSBURG","TALLIN","TARRAGONACOAST","TARRAGONA","TEJN","TENERIFE","TERSCHELLINGNOORDZEE","THYBORONHAVN","TORSMINDEHAVN","TRAVEMUNDE","TROMSO","TRONDHEIM","VADEROARNA","VALENCIACOAST","VALENCIA","VARDO","VECHTERWEERD","VIGO","VIKEN","VIKER","VILLAGARCIADEAROSA","VILLANOSISARGAS","VISBY","VLAKTEVRAAN","VLISSINGEN","WADDENEIERLANDSEGAT","WADDENSCHIERMONNIKOOG","WARNEMUNDE","WARP","WERKENDAMBUITEN","WESTHINDER","WEXFORD","WEYMOUTH","WGABBARD","WHITBY","WICK","WIERUMERGRONDEN","WIJDENES","WITTDUEN","WORKINGTON","ZAKYNTHOS","ZWARTSLUIS");
//var iOb = 0;

var obsTimesVals = [];
var modelTimesVals = [];
var newModelTimesVals = [];
var highlightMarkers = [];
var months = new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December");


var plot; // flot plot
var ndays = 10;
var maxNDays = 10;
var numDays;
var zoomedInViaSelection = false;
var earliestStartTime = null;
var latestEndTime = null;
var newDataIsFwd = null;
var MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
var startChange = 0;
var endChange = 0;
var highlightIdx = 0;
var obsPointsToDelete = new Array();
var modelPointsToDelete = new Array();

// Called when the page has loaded
window.onload = function()
{
    
    alert("As the ECOOP project finished in 2010, there may be data feeds in this portal which have also come to an end or are unreliable.");

    function showTooltip(x, y, contents) {
        jQuery('<div id="tooltip">' + contents + '</div>').css( {
            position: 'absolute',
            display: 'none',
            top: y + 5,
            left: x + 10,
            border: '1px solid #fdd',
            padding: '2px',
            'background-color': '#fee',
            opacity: 0.80
        }).appendTo("body").fadeIn(200);
    }

    var previousPoint = null;
    jQuery("#placeholder").bind("plothover", function (event, pos, item) {
        jQuery("#x").text(pos.x.toFixed(2));
        jQuery("#y").text(pos.y.toFixed(2));

        if (item) {
            if (previousPoint != item.datapoint) {
                previousPoint = item.datapoint;

                jQuery("#tooltip").remove();

                var x = item.datapoint[0];
                var y = item.datapoint[1];

                var date = new Date(x);
                var offset = date.getTimezoneOffset();
                var monthStr = months[date.getMonth()];
                var dayOfMonthStr = date.getDate();
                var hour = date.getHours() + (offset / 60);
                var hourStr = hour < 10 ? "0" + hour : hour;
                var minStr = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
                var dateStr = dayOfMonthStr + " " + monthStr + " " + hourStr + ":" + minStr;

                showTooltip(item.pageX, item.pageY, "time = " + dateStr + ", value = " + y);
            }
        }
        else {
                jQuery("#tooltip").remove();
                previousPoint = null;            
            }
    });
    
    jQuery("#placeholder").bind("plotselected", function (event, ranges) {
        var xaxisMin = ranges.xaxis.from;
        var xaxisMax = ranges.xaxis.to;
        var yaxisMin = ranges.yaxis.from;
        var yaxisMax = ranges.yaxis.to;
   
        var obsLabel = plot.getData()[0].label;
        var modelLabel = plot.getData()[1].label;
        var modelAxis = plot.getData()[1].yaxis;
   
        var zoomData = getFlotData(plot.getData(), xaxisMin, xaxisMax);
        
        plot = jQuery.plot(jQuery("#placeholder"),
           [ 
             { data: zoomData.mainObs, label: obsLabel, color: "red", points: { show: true } }, 
             { data: zoomData.mainModel, label: modelLabel, yaxis: modelAxis, color: "blue", points: { show: true } },
             { data: zoomData.minObs, color: "red", points: { show: false } },
             { data: zoomData.maxObs, color: "red", points: { show: false } },
             { data: zoomData.minModel, color: "blue", points: { show: false } },
             { data: zoomData.maxModel, color: "blue", points: { show: false } }
           ],
           { lines: { show: true },
             selection: { mode: "xy" },
             grid: { hoverable: true, clickable: true, markings: getCurrentDateMarkings(ranges.xaxis) },
             xaxis: { mode: "time", min: xaxisMin, max: xaxisMax }, 
             yaxis: { min: yaxisMin, max: yaxisMax }
           });
           
           zoomedInViaSelection = true;
           document.getElementById('endBackArrow').src = "images/arrow_back_grey.png";
           document.getElementById('endBackArrow').style.cursor = "default";
           document.getElementById('endFwdArrow').src = "images/arrow_fwd_grey.png";
           document.getElementById('endFwdArrow').style.cursor = "default";
           document.getElementById('startBackArrow').src = "images/arrow_back_grey.png";
           document.getElementById('startBackArrow').style.cursor = "default";
           document.getElementById('startFwdArrow').src = "images/arrow_fwd_grey.png";
           document.getElementById('startFwdArrow').style.cursor = "default";
           document.getElementById('maxUnzoomLeft').src = "images/max_unzoom.png";
           document.getElementById('maxUnzoomLeft').style.cursor = "pointer";
           document.getElementById('maxUnzoomRight').src = "images/max_unzoom.png";
           document.getElementById('maxUnzoomRight').style.cursor = "pointer";
    });
    
    highlightIdx = 0;
    
    jQuery("#placeholder").bind("plotclick", function (event, pos, item) {
        if (item) {
            jQuery("#clickdata").text("You clicked point " + item.dataIndex + " in " + item.series.label + ".");
            if (item.highlighted == highlightIdx || item.highlighted == -1) {
                plot.highlight(item.series, item.datapoint);
                item.highlighted = highlightIdx;
                highlightIdx++;
                $('deletePoint').style.visibility = "visible";
                $('deletePoint').innerHTML = "Rogue data point disturbing axes scaling? Delete this point from the plot? <br> (or select more points and then click 'yes' to delete multiple points)<br><br>\n\
                    <input type=\"button\" value=\"Remove point(s)\" onClick=\"javascript:deletePointFromPlot(" + item.seriesIndex + "," + item.datapoint[0]  + ")\"><br>\n\
                    <input type=\"button\" value=\"Close\" onClick=\"javascript:removeDeletePointDiv()\">";
                
                if (item.seriesIndex == 0) {
                    obsPointsToDelete.push(item.datapoint);
                } else if (item.seriesIndex == 1) {
                    modelPointsToDelete.push(item.datapoint);
                }
            }
            else {
                plot.unhighlight(item.series, item.datapoint);
                item.highlighted = 0;
                highlightIdx--;
                if (item.seriesIndex == 0) {
                    for (var iPoint = 0; iPoint < obsPointsToDelete.length; iPoint++) {
                        if (obsPointsToDelete[iPoint][0] == item.datapoint[0]) {
                            obsPointsToDelete.splice(iPoint, 1);
                        }
                    }
                } else if (item.seriesIndex == 1) {
                    for (var iPoint = 0; iPoint < modelPointsToDelete.length; iPoint++) {
                        if (modelPointsToDelete[iPoint][0] == item.datapoint[0]) {
                            modelPointsToDelete.splice(iPoint, 1);
                        }
                    }
                }
                if (obsPointsToDelete.length == 0 && modelPointsToDelete.length == 0) {
                    $('deletePoint').style.visibility = "hidden";
                }
                
            }
        }
    });
    
    // reset the scale markers
    $('scaleMax').value = '';
    $('scaleMin').value = '';

    // Make sure 100% opacity is selected
    $('opacityValue').value = '100';

    // Detect the browser (IE6 doesn't render PNGs properly so we don't provide
    // the option to have partial overlay opacity)
    isIE = navigator.appVersion.indexOf('MSIE') >= 0;

    // Stop the pink tiles appearing on error
    OpenLayers.Util.onImageLoadError = function() {  this.style.display = ""; this.src="./images/blank.png"; }
    
    // Set up the OpenLayers map widget
    map = new OpenLayers.Map('map');
    var ol_wms = new OpenLayers.Layer.WMS( "OpenLayers WMS", 
        "http://labs.metacarta.com/wms-c/Basic.py?", {layers: 'basic', format: 'image/png' }, { wrapDateLine: true} );
    var bluemarble_wms = new OpenLayers.Layer.WMS( "Blue Marble", 
        "http://labs.metacarta.com/wms-c/Basic.py?", {layers: 'satellite' }, { wrapDateLine: true} );
    var osm_wms = new OpenLayers.Layer.WMS( "Openstreetmap", 
        "http://labs.metacarta.com/wms-c/Basic.py?", {layers: 'osm-map' }, { wrapDateLine: true} );
    var human_wms = new OpenLayers.Layer.WMS( "Human Footprint", 
        "http://labs.metacarta.com/wms-c/Basic.py?", {layers: 'hfoot' }, { wrapDateLine: true} );
    var demis_wms = new OpenLayers.Layer.WMS( "Demis WMS",
        "http://www2.Demis.nl/MapServer/Request.asp?WRAPDATELINE=TRUE", {layers:
        'Bathymetry,Topography,Hillshading,Coastlines,Builtup+areas,Waterbodies,Rivers,Streams,Railroads,Highways,Roads,Trails,Borders,Cities,Airports'},
        {wrapDateLine: true});

    // ESSI WMS (see Stefano Nativi's email to me, Feb 15th)
    /*var essi_wms = new OpenLayers.Layer.WMS.Untiled( "ESSI WMS", 
        "http://athena.pin.unifi.it:8080/ls/servlet/LayerService?",
        {layers: 'sst(time-lat-lon)-T0', transparent: 'true' } );
    essi_wms.setVisibility(false);*/
            
    // The SeaZone Web Map server
    var seazone_wms = new OpenLayers.Layer.WMS("SeaZone bathymetry", "http://ws.cadcorp.com/seazone/wms.exe?",
        {layers: 'Bathymetry___Elevation.bds', transparent: 'true'});
    seazone_wms.setVisibility(false);
    
    map.addLayers([bluemarble_wms, demis_wms, ol_wms, osm_wms, human_wms/*, seazone_wms, essi_wms*/]);
    baseLayer = demis_wms;
    map.setBaseLayer(baseLayer);
    
    // Make sure the Google Earth and Permalink links are kept up to date when
    // the map is moved or zoomed
    //map.events.register('moveend', map, setGEarthURL);
    //map.events.register('moveend', map, setPermalinkURL); No Permalink in ECOOP Ecosystem DESS (at the moment)
    
    // If we have loaded Google Maps and the browser is compatible, add it as a base layer
    if (typeof GBrowserIsCompatible == 'function' && GBrowserIsCompatible()) {
        var gmapLayer = new OpenLayers.Layer.Google("Google Maps (satellite)", {type: G_SATELLITE_MAP});
        var gmapLayer2 = new OpenLayers.Layer.Google("Google Maps (political)", {type: G_NORMAL_MAP});
        map.addLayers([gmapLayer, gmapLayer2]);
    }
    
    layerSwitcher = new OpenLayers.Control.LayerSwitcher()
    map.addControl(layerSwitcher);
    //map.addControl(new OpenLayers.Control.MousePosition({prefix: 'Lon: ', separator: ' Lat:'}));
     
    // Add a listener for changing the base map
    //map.events.register("changebaselayer", map, function() { alert(this.projection) });
    // Add a listener for GetFeatureInfo
    map.events.register('click', map, getFeatureInfo);
    
    // Set up the autoload object
    // Note that we must get the query string from the top-level frame
    // strip off the leading question mark
    populateAutoLoad(window.location);
    if (window.top.location != window.location) {
        // We're in an iframe so we must also use the query string from the top frame
        populateAutoLoad(window.top.location);
    }
    
    if (menu == 'ecoop') map.setCenter(new OpenLayers.LonLat(0.5, 55), 5, true, true);
    else if (menu == 'ecoopobs') {
        map.setCenter(new OpenLayers.LonLat(10, 45), 4, true, true);
        document.getElementById('titleText').innerHTML = "ECOOP Obs";
    }
    else {
        map.setCenter(new OpenLayers.LonLat(10, 45), 4, true, true);
        document.getElementById('titleText').innerHTML = "ECOOP Obs";
    }
    
    // Set up the left-hand menu
    setupTreeControl(menu);
    
    // Set up the palette selector pop-up
    /*paletteSelector = new YAHOO.widget.Panel("paletteSelector", { 
        width:"400px",
        constraintoviewport: true,
        fixedcenter: true,
        underlay:"shadow",
        close:true,
        visible:false,
        draggable:true,
        modal:true
    });*/
    //paletteSelector.setHeader('Click to choose a colour palette');
}

// Populates the autoLoad object from the given window location object
function populateAutoLoad(windowLocation)
{
    var queryString = windowLocation.search.split('?')[1];
    if (queryString != null) {
        var kvps = queryString.split('&');
        for (var i = 0; i < kvps.length; i++) {
            keyAndVal = kvps[i].split('=');
            if (keyAndVal.length > 1) {
                var key = keyAndVal[0].toLowerCase();
                if (key == 'layer') {
                    autoLoad.layer = keyAndVal[1];
                } else if (key == 'elevation') {
                    autoLoad.zValue = keyAndVal[1];
                } else if (key == 'time') {
                    autoLoad.isoTValue = keyAndVal[1];
                } else if (key == 'bbox') {
                    autoLoad.bbox = keyAndVal[1];
                } else if (key == 'scale') {
                    autoLoad.scaleMin = keyAndVal[1].split(',')[0];
                    autoLoad.scaleMax = keyAndVal[1].split(',')[1];
                } else if (key == 'menu') {
                    // we load a specific menu instead of the default
                    menu = keyAndVal[1];
                }
            }
        }
    }
}

function setupTreeControl(menu)
{
    tree = new YAHOO.widget.TreeView('layerSelector');
    // Add an event callback that gets fired when a tree node is clicked
    tree.subscribe('labelClick', treeNodeClicked);
    
    // The servers can be specified using the global "servers" array above
    // but if not, we'll just use the default server
    if (typeof servers == 'undefined' || servers == null) {
        servers = [''];
    }

    // Add a root node in the tree for each server.  If the user has supplied
    // a "menu" option then this will be sent to all the servers.
    for (var i = 0; i < servers.length; i++) {
        var layerRootNode = new YAHOO.widget.TextNode(
            {label: "Loading ...", server: servers[i]},
            tree.getRoot(),
            servers.length == 1 // Only show expanded if this is the only server
        );
        layerRootNode.multiExpand = false;
        // The getMenu() function is asynchronous.  Once we have received
        // the result from the server we shall pass it to the makeLayerMenu() function.
        getMenu(layerRootNode, {
            menu: menu,
            callback : function(layerRootNode, layers) {
                layerRootNode.data.label = layers.label;
                layerRootNode.label = layers.label;
                // Add layers recursively.
                addNodes(layerRootNode, layers.children);
                tree.draw();
                
                // Now look to see if we are auto-loading a certain layer
                if (typeof autoLoad.layer != 'undefined') {
                    var node = tree.getNodeByProperty('id', autoLoad.layer);
                    if (node == null) {
                        alert("Layer " + autoLoad.layer + " not found");
                    } else {
                        if (node.parent != null) node.parent.expand();
                        treeNodeClicked(node); // act as if we have clicked this node
                    }
                }
            }
        });
    }
}

// Called when a node in the tree has been clicked
function treeNodeClicked(node)
{      
    // We're only interested if this is a displayable layer, i.e. it has an id.
    if (typeof node.data.id != 'undefined') {
        // Update the breadcrumb trail
        var s = node.data.label;
        var theNode = node;
        while(theNode.parent != tree.getRoot()) {
            theNode = theNode.parent;
            s = theNode.data.label + ' &gt; ' + s;
        }
        $('layerPath').innerHTML = s;

        // See if we're auto-loading a certain time value
        if (typeof autoLoad.isoTValue != 'undefined') {
            isoTValue = autoLoad.isoTValue;
        }
        if (isoTValue == null ) {
            // Set to the present time if we don't already have a time selected
            isoTValue = new Date().print('%Y-%m-%dT%H:%M:%SZ');
        }

        // Get the details of this layer from the server, calling layerSelected()
        // when we have the result
        var layerDetails = getLayerDetails(node.data.server, {
            callback: layerSelected,
            layerName: node.data.id,
            time: isoTValue
        });
    }
    
    setTimeout("colourModelParamLinks()", 100);
}

function colourModelParamLinks() {
    //start test code for colouring model parameters which have associated obs
    // only problem is that this is executed BEFORE the Yahoo js method to expand
    // the meny (and create the link DOM Element) has completed.  Needs to be the
    // other way round. Trying to solve by calling this function with a delay.
    
    var linkArray = document.getElementsByTagName("a");
    for (var iLink = 0; iLink < linkArray.length; iLink++) {
        var linkText = linkArray[iLink].firstChild.nodeValue;
        //alert(linkText);
        if (linkText == "Sea Water Salinity (has obs)" ||
            linkText == "Sea Water Temperature (has obs)" ||
            linkText == "Sea Water Velocity (has obs)" ||
            linkText == "Chlorophyll a (has obs)" ||
            linkText == "Photosynthetically Active Radiation (has obs)" ||
            linkText == "Suspended Particulate Matter (2um) (has obs)" ||
            linkText == "Suspended Particulate Matter (35um) (has obs)" ||
            linkText == "Dissolved Oxygen Concentration (has obs)") {
            //linkArray[iLink].style.color = "green";
        }
        else if ((linkText.contains("Sea") ||
                 linkText.contains("Concentration") || 
                 linkText.contains("biomass")) && menu == "ecoop") {
            linkArray[iLink].style.color = "#888";
        }
    }
    
    //end test code for colouring model parameters which have associated obs
}

// Recursive method to add nodes to the layer selector tree control
function addNodes(parentNode, layerArray)
{
    for (var i = 0; i < layerArray.length; i++) {
        var layer = layerArray[i];
        if (layer.server == null) {
            // If the layer does not specify a server explicitly, use the URL of
            // the server that provided this layer
            layer.server = parentNode.data.server;
        }
        // The treeview control uses the layer.label string for display
        var newNode;
        
        if ((layer.label == "Sea Water Salinity" ||
            layer.label == "Sea Water Temperature" ||
            layer.label == "Sea Water Velocity" ||
            layer.label == "Chlorophyll a" ||
            layer.label == "Photosynthetically Active Radiation" ||
            layer.label == "Suspended Particulate Matter (2um)" ||
            layer.label == "Suspended Particulate Matter (35um)" ||
            layer.label == "Dissolved Oxygen Concentration") && menu == 'ecoop') {
            newNode = new YAHOO.widget.TextNode(
                {label: layer.label + " (has obs)", id: layer.id, server: layer.server},
                parentNode,
                false
            );
        }
        else {
            newNode = new YAHOO.widget.TextNode(
                {label: layer.label, id: layer.id, server: layer.server},
                parentNode,
                false
            );
        }
        if (typeof layer.children != 'undefined') {
            newNode.multiExpand = false;
            addNodes(newNode, layer.children);
        }
    }
}

// Function that is used by the calendar to see whether a date should be disabled
function isDateDisabled(date, year, month, day)
{
    // datesWithData is a hash of year numbers mapped to a hash of month numbers
    // to an array of day numbers, i.e. {2007 : {0 : [3,4,5]}}.
    // Month numbers are zero-based.
    if (datesWithData == null ||
        datesWithData[year] == null || 
        datesWithData[year][month] == null) {
        // No data for this year or month
        return true;
    }
    // Cycle through the array of days for this month, looking for the one we want
    var numDays = datesWithData[year][month].length;
    for (var d = 0; d < numDays; d++) {
        if (datesWithData[year][month][d] == day) return false; // We have data for this day
    }
    // If we've got this far, we've found no data
    return true;
}

// Event handler for when a user clicks on a map
// TODO: how can we detect whether the click is off the map (i.e. |lat| > 90)?
function getFeatureInfo(e)
{
    // Check we haven't clicked off-map
    var lonLat = map.getLonLatFromPixel(e.xy);
    if (ncwms != null && Math.abs(lonLat.lat) <= 90)
    {
        // See if the click was within range of the currently-visible layer
        var layerBounds = new OpenLayers.Bounds(activeLayer.bbox[0],
            activeLayer.bbox[1], activeLayer.bbox[2], activeLayer.bbox[3]);
        if (layerBounds.contains(lonLat.lon, lonLat.lat)) {
            $('featureInfo').innerHTML = "Getting feature info...";
            var params = {
                REQUEST: "GetFeatureInfo",
                BBOX: map.getExtent().toBBOX(),
                I: e.xy.x,
                J: e.xy.y,
                INFO_FORMAT: 'text/xml',
                QUERY_LAYERS: ncwms.params.LAYERS,
                WIDTH: map.size.w,
                HEIGHT: map.size.h
            };
            if (activeLayer.server != '') {
                // This is the signal to the server to load the data from elsewhere
                params.url = activeLayer.server;
            }
            featureInfoUrl = ncwms.getFullRequestString(
                params,
                'wms' // We must always load from the home server
            );
            OpenLayers.loadURL(featureInfoUrl, '', this, gotFeatureInfo);
            Event.stop(e);
        }
    }
}

// Called when we have received some feature info
function gotFeatureInfo(response)
{
    var xmldoc = response.responseXML;
    var lon = xmldoc.getElementsByTagName('longitude')[0];
    var lat = xmldoc.getElementsByTagName('latitude')[0];
    var val = xmldoc.getElementsByTagName('value')[0];
    if (lon) {
        $('featureInfo').innerHTML = "<b>Lon:</b> " + lon.firstChild.nodeValue + 
            "&nbsp;&nbsp;<b>Lat:</b> " + lat.firstChild.nodeValue + "&nbsp;&nbsp;<b>Value:</b> " +
            toNSigFigs(parseFloat(val.firstChild.nodeValue), 4);
        if (timeSeriesSelected()) {
            // Construct a GetFeatureInfo request for the timeseries plot
            // Get a URL for a WMS request that covers the current map extent
            var urlEls = featureInfoUrl.split('&');
            // Replace the parameters as needed.  We generate a map that is half the
            // width and height of the viewport, otherwise it takes too long
            var newURL = urlEls[0];
            for (var i = 1; i < urlEls.length; i++) {
                if (urlEls[i].startsWith('TIME=')) {
                    newURL += '&TIME=' + $('firstFrame').innerHTML + '/' + $('lastFrame').innerHTML;
                } else if (urlEls[i].startsWith('INFO_FORMAT')) {
                    newURL += '&INFO_FORMAT=image/png';
                } else {
                    newURL += '&' + urlEls[i];
                }
            }
            // Image will be 400x300, need to allow a little elbow room
            $('featureInfo').innerHTML += "&nbsp;&nbsp;<span id='createTimeseries' class='fakeLink' \n\
                onmouseover=mouseOverFakeLink('createTimeseries') onmouseout=mouseOutFakeLink('createTimeseries') onclick=popUp('"
                + newURL + "',450,350)>Create timeseries plot</a>";
            
        }
    } else {
        $('featureInfo').innerHTML = "Can't get feature info data for this layer <a href=\"javascript:popUp('whynot.html', 200, 200)\">(why not?)</a>";
    }
}

function popUp(url, width, height)
{
    var day = new Date();
    var id = day.getTime();
    window.open(url, id, 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width='
        + width + ',height=' + height + ',left = 300,top = 300');
}

// Called when the user clicks on the name of a displayable layer in the left-hand menu
// Gets the details (units, grid etc) of the given layer. 
function layerSelected(layerDetails)
{
    //layerChange = true;
    
    //if (initialLoad == true) {
    //  	loadObs = true; // load Obs if this is the first data to be requested
    //}
    //else {
    //	loadObs = true; // if just changing parameter and don't want to reload obs then change this to false
    //}

    if (document.getElementById('flot').style.visibility == "hidden") {
      $('panelHeader').style.display = 'block';
      $('legend').style.display = 'block';
      $('imagePanel').style.top = '225px';
      $('layerSelector').style.top = '350px';
      //$('mainCol').style.height = '600px';
      //$('sidebar').style.height = '600px';
      //$('mainWindow').style.height = '883px';
      //$('contentPanel').style.height = '700px';
    }
    else {
      highlightMarkers = new Array();
      $('imagePanel').style.top = '525px';
      $('layerSelector').style.top = '645px';
      plot.setData(new Array());
      plot.draw();
      //plot.setupGrid();
    }
    map.updateSize();
    
    activeLayer = layerDetails;
    gotScaleRange = false;
    resetAnimation();
    
    // Units are ncWMS-specific
    var isNcWMS = false;
    if (typeof layerDetails.units != 'undefined') {
        $('units').innerHTML = '(units: ' + layerDetails.units + ')';
        isNcWMS = true;
    } else {
        $('units').innerHTML = '';
    }

    // clear the list of z values
    $('zValues').options.length = 0; 

    // Set the range selector objects
    var zValue = typeof autoLoad.zValue == 'undefined'
        ? getZValue()
        : parseFloat(autoLoad.zValue);

    var zAxis = layerDetails.zaxis;
    if (zAxis == null) {
        $('zAxis').innerHTML = ''
        $('zValues').style.display = 'none';
    } else {
        if (zAxis.positive) {
            $('zAxis').innerHTML = '<b>Elevation (' + zAxis.units + '): </b>';
        } else {
            $('zAxis').innerHTML = '<b>Depth (' + zAxis.units + '): </b>';
        }
        // Populate the drop-down list of z values
        // Make z range selector invisible if there are no z values
        var zValues = zAxis.values;
        zPositive = zAxis.positive;
        $('zValues').style.display = 'none';
        var zDiff = 1e10; // Set to some ridiculously-high value
        var nearestIndex = 0;
        for (var j = 0; j < zValues.length; j++) {
            // Create an item in the drop-down list for this z level
            $('zValues').options[j] = new Option(zValues[j], j);
            // Find the nearest value to the currently-selected
            // depth level
            var diff;
            // This is nasty: improve!
            if (zPositive) {
                diff = Math.abs(parseFloat(zValues[j]) - zValue);
            } else {
                diff = Math.abs(parseFloat(zValues[j]) + zValue);
            }
            if (diff < zDiff) {
                zDiff = diff;
                nearestIndex = j;
            }
        }
        $('zValues').selectedIndex = nearestIndex;
        $('zValues').style.display = 'none';
    }
    
    // Only show the scale bar if the data are coming from an ncWMS server
    var scaleVisibility = isNcWMS ? 'visible' : 'hidden';
    $('scaleBar').style.visibility = scaleVisibility;
    $('scaleMin').style.visibility = scaleVisibility;
    $('scaleMax').style.visibility = scaleVisibility;
    $('scaleControls').style.visibility = scaleVisibility;
    $('autoScale').style.visibility = scaleLocked ? 'hidden' : scaleVisibility;
    
    // Set the scale value if this is present in the metadata
    if (typeof layerDetails.scaleRange != 'undefined' &&
            layerDetails.scaleRange != null &&
            layerDetails.scaleRange.length > 1 &&
            layerDetails.scaleRange[0] != layerDetails.scaleRange[1] &&
            !scaleLocked) {
        scaleMinVal = layerDetails.scaleRange[0];
        scaleMaxVal = layerDetails.scaleRange[1];
        $('scaleMin').value = toNSigFigs(scaleMinVal, 4);
        $('scaleMax').value = toNSigFigs(scaleMaxVal, 4);
        gotScaleRange = true;
    }
    
    if (!isIE) {
        // Only show this control if we can use PNGs properly (i.e. not on Internet Explorer)
        $('opacityControl').style.visibility = 'visible';
    }

    // Set the auto-zoom box
    bbox = layerDetails.bbox;
    //$('autoZoom').innerHTML = '<a href="#" onclick="map.zoomToExtent(new OpenLayers.Bounds(' +
    //    bbox[0] + ',' + bbox[1] + ',' + bbox[2] + ',' + bbox[3] +
    //    '));\">Fit layer to window</a>';
    $('autoZoom').innerHTML = '<span id="autoZoom" class="fakeLink" onmouseover=mouseOverFakeLink("autoZoom") \n\
        onmouseout=mouseOutFakeLink("autoZoom") onclick="map.zoomToExtent(new OpenLayers.Bounds(' +
        bbox[0] + ',' + bbox[1] + ',' + bbox[2] + ',' + bbox[3] +
        '));\">Fit layer to window</a>';
    
    // Set up the copyright statement
    $('copyright').innerHTML = layerDetails.copyright;

    // Now set up the calendar control
    if (layerDetails.datesWithData == null) {
        // There is no calendar data.  Just update the map
        if (calendar != null) calendar.hide();
        $('date').innerHTML = '';
        $('time').innerHTML = '';
        $('utc').style.display = 'none';
        //loadObs = true;
        updateMap(true);
    } else {
        datesWithData = layerDetails.datesWithData; // Tells the calendar which dates to disable
        if (calendar == null) {
            // Set up the calendar
            calendar = Calendar.setup({
                flat : 'calendar', // ID of the parent element
                align : 'bl', // Aligned to top-left of parent element
                weekNumbers : false,
                flatCallback : dateSelected
            });
            // For some reason, if we add this to setup() things don't work
            // as expected (dates not selectable on web page when first loaded).
            calendar.setDateStatusHandler(isDateDisabled);
        }
        // Set the range of valid years in the calendar.  Look through
        // the years for which we have data, finding the min and max
        var minYear = 100000000;
        var maxYear = -100000000;
        for (var year in datesWithData) {
            if (typeof datesWithData[year] != 'function') { // avoid built-in functions
                if (year < minYear) minYear = year;
                if (year > maxYear) maxYear = year;
            }
        }
        calendar.setRange(minYear, maxYear);
        // Get the time on the t axis that is nearest to the currently-selected
        // time, as calculated on the server
        calendar.setDate(layerDetails.nearestTime);
        calendar.refresh();
        calendar.show();
        // Load the timesteps for this date
        loadTimesteps(true);
    }
}

// Function that is called when a user clicks on a date in the calendar
function dateSelected(cal)
{
    if (cal.dateClicked) {
	loadTimesteps(false);
        
        if (plot != null) {
            var obsLabel = plot.getData()[0].label;
            var modelLabel = plot.getData()[1].label;
            var modelAxis = plot.getData()[1].yaxis;
            var xaxis = plot.getAxes().xaxis;
            var date = calendar.date;
            if (date >= xaxis.min && date <= xaxis.max) {
                plot = flot(obsLabel, modelLabel, modelAxis, {mode:"time", min:xaxis.min, max:xaxis.max}, null);
            } else {
                var newMax = date.getTime();
                var newMin = newMax - (10 * MILLIS_IN_DAY);
                newDataIsFwd = null; 
                ndays = 0;
                maxNDays = 0;
                earliestStartTime = newMin;
                latestEndTime = newMax;
                endChange = 0;
                startChange = 0;
                plot = null;
                createObsModelPopup(null, 10, newMin, newMax)
            }
        }
    }
}

// Updates the time selector control.  Finds all the timesteps that occur on
// the same day as the currently-selected date.  Called from the calendar
// control when the user selects a new date
function loadTimesteps(autoScale)
{
    autoScaleWhenUpdateMap = autoScale;

    // Print out date, e.g. "15 Oct 2007"
    $('date').innerHTML = '<b>Date/time: </b>' + calendar.date.print('%d %b %Y');
    $('titleDate').innerHTML = ' on ' + calendar.date.print('%d %b %Y');

    // Get the timesteps for this day
    getTimesteps(activeLayer.server, {
        callback: updateTimesteps,
        layerName: activeLayer.id,
        day: makeIsoDate(calendar.date)
    });
}

// Gets an ISO Date ("yyyy-mm-dd") for the given Javascript date object.
// Does not contain the time.
function makeIsoDate(date)
{
    // Watch out for low-numbered years when generating the ISO string
    var prefix = '';
    var year = date.getFullYear();
    if (year < 10) prefix = '000';
    else if (year < 100) prefix = '00';
    else if (year < 1000) prefix = '0';
    return prefix + date.print('%Y-%m-%d'); // Date only (no time) in ISO format
}

// Called when we have received the timesteps from the server
function updateTimesteps(times)
{
    // We'll get back a JSON array of ISO8601 times ("hh:mm:ss", UTC, no date information)
    // Build the select box
    var s = '<select id="tValues" onchange="javascript:updateMap()">';
    for (var i = 0; i < times.length; i++) {
        // Construct the full ISO Date-time
        var isoDateTime = makeIsoDate(calendar.date) + 'T' + times[i] + 'Z';
        s += '<option value="' + isoDateTime + '">' + times[i] + '</option>';
    }
    s += '</select>';

    $('time').innerHTML = s;
    $('utc').style.display = 'none';

    // If we're autoloading, set the right time in the selection box
    if (autoLoad != null && autoLoad.isoTValue != null) {
        var timeSelect = $('tValues');
        for (var i = 0; i < timeSelect.options.length; i++) {
            if (timeSelect.options[i].value == autoLoad.isoTValue) {
                timeSelect.selectedIndex = i;
                break;
            }
        }
    }
    $('setFrames').style.visibility = 'visible';

    if (typeof autoLoad.scaleMin != 'undefined' && typeof autoLoad.scaleMax != 'undefined') {
        $('scaleMin').value = autoLoad.scaleMin;
        $('scaleMax').value = autoLoad.scaleMax;
        validateScale(true); // this calls updateMap()
    } else if (!gotScaleRange && !scaleLocked) {// We didn't get a scale range from the layerDetails
	autoScale(true);
    } else {
	if (autoScaleWhenUpdateMap) {
	    autoScale(true);
	}
	else {
            updateMap(true); // Update the map without changing the scale
    	}
    }
}

// Calls the WMS to find the min and max data values, then rescales.
// If this is a newly-selected variable the method gets the min and max values
// for the whole layer.  If not, this gets the min and max values for the viewport.
function autoScale(newVariable)
{
    loadObsWhenAutoScale = newVariable;    

    var dataBounds;
    if ($('tValues')) {
        isoTValue = $('tValues').value;
    }
    if (newVariable) {
        // We use the bounding box of the whole layer 
        dataBounds = bbox[0] + ',' + bbox[1] + ',' + bbox[2] + ',' + bbox[3];
    } else {
        // Use the intersection of the viewport and the layer's bounding box
        dataBounds = getIntersectionBBOX();
    }
    getMinMax(activeLayer.server, {
        callback: gotMinMax,
        layerName: activeLayer.id,
        bbox: dataBounds,
        elevation: getZValue(),
        time: isoTValue
    });
}

// When the scale is locked, the user cannot change the colour scale either
// by editing manually or clicking "auto".  Furthermore the scale will not change
// when a new layer is loaded
function toggleLockScale()
{
    if (scaleLocked) {
        // We need to unlock the scale
        scaleLocked = false;
        $('lockScale').text = 'lock';
        $('autoScale').style.visibility = 'visible';
        $('scaleSpacing').disabled = false;
        $('scaleMin').disabled = false;
        $('scaleMax').disabled = false;
    } else {
        // We need to lock the scale
        scaleLocked = true;
        $('lockScale').text = 'unlock';
        $('autoScale').style.visibility = 'hidden';
        $('scaleSpacing').disabled = true;
        $('scaleMin').disabled = true;
        $('scaleMax').disabled = true;
    }
}

// This function is called when we have received the min and max values from the server
function gotMinMax(minmax)
{
    $('scaleMin').value = toNSigFigs(minmax.min, 4);
    $('scaleMax').value = toNSigFigs(minmax.max, 4);
    var loadObs = loadObsWhenAutoScale;
    validateScale(loadObs); // This calls updateMap()
}

// Validates the entries for the scale bar
function validateScale(loadObs)
{
    var fMin = parseFloat($('scaleMin').value);
    var fMax = parseFloat($('scaleMax').value);
    if (isNaN(fMin)) {
        alert('Scale limits must be set to valid numbers');
        // Reset to the old value
        $('scaleMin').value = scaleMinVal;
    } else if (isNaN(fMax)) {
        alert('Scale limits must be set to valid numbers');
        // Reset to the old value
        $('scaleMax').value = scaleMaxVal;
    } else if (fMin > fMax) {
        alert('Minimum scale value must be less than the maximum');
        // Reset to the old values
        $('scaleMin').value = scaleMinVal;
        $('scaleMax').value = scaleMaxVal;
    } else if (fMin <= 0 && $('scaleSpacing').value == 'logarithmic') {
        alert('Cannot use a logarithmic scale with negative or zero values');
        $('scaleSpacing').value = 'linear';
    } else {
        $('scaleMin').value = fMin;
        $('scaleMax').value = fMax;
        scaleMinVal = fMin;
        scaleMaxVal = fMax;
	updateMap(loadObs);  
    }
}

function resetAnimation()
{
    hideAnimation();
    $('setFrames').style.visibility = 'hidden';
    $('animation').style.visibility = 'hidden';
    $('firstFrame').innerHTML = '';
    $('lastFrame').innerHTML = '';
}
function setFirstAnimationFrame()
{
    $('firstFrame').innerHTML = $('tValues').value;
    $('animation').style.visibility = 'visible';
    //setGEarthURL();
}
function setLastAnimationFrame()
{
    $('lastFrame').innerHTML = $('tValues').value;
    $('animation').style.visibility = 'visible';
    //setGEarthURL();
}
function createAnimation()
{
    if (!timeSeriesSelected()) {
        alert("Must select a first and last frame for the animation");
        return;
    }
    
    // Get a URL for a WMS request that covers the current map extent
    var urlEls = ncwms.getURL(getMapExtent()).split('&');
    // Replace the parameters as needed.  We generate a map that is half the
    // width and height of the viewport, otherwise it takes too long
    var width = $('map').clientWidth;// / 2;
    var height = $('map').clientHeight;// / 2;
    var newURL = urlEls[0];
    for (var i = 1; i < urlEls.length; i++) {
        if (urlEls[i].startsWith('TIME=')) {
            newURL += '&TIME=' + $('firstFrame').innerHTML + '/' + $('lastFrame').innerHTML;
        } else if (urlEls[i].startsWith('FORMAT')) {
            newURL += '&FORMAT=image/gif';
        } else if (urlEls[i].startsWith('WIDTH')) {
            newURL += '&WIDTH=' + width;
        } else if (urlEls[i].startsWith('HEIGHT')) {
            newURL += '&HEIGHT=' + height;
        } else if (!urlEls[i].startsWith('OPACITY')) {
            // We remove the OPACITY ARGUMENT as GIFs do not support partial transparency
            newURL += '&' + urlEls[i];
        }
    }
    $('featureInfo').style.visibility = 'hidden';
    $('autoZoom').style.visibility = 'hidden';
    $('hideAnimation').style.visibility = 'visible';
    // We show the "please wait" image then immediately load the animation
    $('loadingAnimationDiv').style.visibility = 'visible'; // This will be hidden by animationLoaded()
    
    // When the mapOverlay has been loaded we call animationLoaded() and place the image correctly
    // on the map
    $('mapOverlay').src = newURL;
    $('mapOverlay').width = width;
    $('mapOverlay').height = height;
}
// Gets the current map extent, checking for out-of-range latitude values
function getMapExtent()
{
    var bounds = map.getExtent();
    // This assumes a lat-lon projection!
    if (bounds.top > 90.0) bounds.top = 90.0;
    if (bounds.bottom < -90.0) bounds.bottom = -90.0;
    return bounds;
}
function animationLoaded()
{
    $('loadingAnimationDiv').style.visibility = 'hidden';
    //$('mapOverlayDiv').style.visibility = 'visible';
    // Load the image into a new layer on the map
    animation_layer = new OpenLayers.Layer.Image(
        "ncWMS", // Name for the layer
        $('mapOverlay').src, // URL to the image
        getMapExtent(), // Image bounds
        new OpenLayers.Size($('mapOverlay').width, $('mapOverlay').height), // Size of image
        {isBaseLayer : false} // Other options
    );
    setVisibleLayer(true);
    map.addLayers([animation_layer]);
}
function hideAnimation()
{
    setVisibleLayer(false);
    $('featureInfo').style.visibility = 'visible';
    $('autoZoom').style.visibility = 'visible';
    $('hideAnimation').style.visibility = 'hidden';
    $('mapOverlayDiv').style.visibility = 'hidden';
}

function updateMap(loadObs)
{   
        var logscale = $('scaleSpacing').value == 'logarithmic';

        // Update the intermediate scale markers
        var min = logscale ? Math.log(parseFloat(scaleMinVal)) : parseFloat(scaleMinVal);
        var max = logscale ? Math.log(parseFloat(scaleMaxVal)) : parseFloat(scaleMaxVal);
        var third = (max - min) / 3;
        var scaleOneThird = logscale ? Math.exp(min + third) : min + third;
        var scaleTwoThirds = logscale ? Math.exp(min + 2 * third) : min + 2 * third;
        $('scaleOneThird').innerHTML = toNSigFigs(scaleOneThird, 4);
        $('scaleTwoThirds').innerHTML = toNSigFigs(scaleTwoThirds, 4);

        if ($('tValues')) {
            isoTValue = $('tValues').value;
        }

        var opacity = $('opacityValue').value;

        // Set the map bounds automatically
        if (typeof autoLoad.bbox != 'undefined') {
            map.zoomToExtent(getBounds(autoLoad.bbox));
        }

        // Make sure the autoLoad object is cleared
        autoLoad = new Object();

        // Get the default style for this layer.  There is some defensive programming here to 
        // take old servers into account that don't advertise the supported styles
        var style = typeof activeLayer.supportedStyles == 'undefined' ? 'boxfill' : activeLayer.supportedStyles[0];
        if (paletteName != null) {
            style += '/' + paletteName;
        }

        // Notify the OpenLayers widget
        // TODO get the map projection from the base layer
        // TODO use a more informative title
        // Buffer is set to 1 to avoid loading a large halo of tiles outside the
        // current viewport
        var params = {
            layers: activeLayer.id,
            elevation: getZValue(),
            time: isoTValue,
            transparent: 'true',
            styles: style,
            colorscalerange: scaleMinVal + ',' + scaleMaxVal,
            opacity: opacity,
            numcolorbands: $('numColorBands').value,
            logscale: logscale
        };
        if (ncwms == null) {
            ncwms_tiled = new OpenLayers.Layer.WMS("Model",
                activeLayer.server == '' ? 'wms' : activeLayer.server, 
                params,
                {buffer: 1, wrapDateLine: true}
            );
            ncwms_untiled = new OpenLayers.Layer.WMS("Model",
                activeLayer.server == '' ? 'wms' : activeLayer.server, 
                params,
                {buffer: 1, ratio: 1.5, singleTile: true, wrapDateLine: true}
            );
            setVisibleLayer(false);
            map.addLayers([ncwms_tiled, ncwms_untiled]);
            // Create a layer for coastlines
            // TOOD: only works at low res (zoomed out)
            //var coastline_wms = new OpenLayers.Layer.WMS( "Coastlines", 
            //    "http://labs.metacarta.com/wms/vmap0?", {layers: 'coastline_01', transparent: 'true' } );
            //map.addLayers([ncwms, coastline_wms]);
            //map.addLayers([ncwms_tiled, ncwms_untiled]);
        } else {
            setVisibleLayer(false);
            ncwms.url = activeLayer.server == '' ? 'wms' : activeLayer.server;
            ncwms.mergeNewParams(params);
        }

        $('featureInfo').innerHTML = "Click on the map to get more information";
        $('featureInfo').style.visibility = 'visible';

        var imageURL = ncwms.getURL(new OpenLayers.Bounds(bbox[0], bbox[1], bbox[2], bbox[3]));

        if (/*(activeLayer.id.contains('MED') || activeLayer.id.contains('WAVES') || 
             activeLayer.id.contains('MRCS') || activeLayer.id.contains('ROMS') ||
             activeLayer.id.contains('BALTIC') || activeLayer.id.contains('ALERMO')) &&*/ loadObs == true)
        {  
            if (markersLayer != null)
            {
                var layerToRemove = markersLayer;
                map.removeLayer(layerToRemove);
                markersLayer = null;
            }
        
            getObsList(activeLayer.server,
            {
              callback: gotObsList,
              layerName: activeLayer.id,
              bbox: bbox[0] + ',' + bbox[1] + ',' + bbox[2] + ',' + bbox[3],
              width: map.size.w,
              height: map.size.h,
              day: makeIsoDate(calendar.date),
              ndays: "10"
            });
        }
}

function gotObsList(obsList)
{ 
    var obsIds = obsList.matchingObs;
    var obsLons = obsList.matchingLons;
    var obsLats = obsList.matchingLats;
    
    var newLayer;
    if (markersLayer == null) {
        markersLayer = new OpenLayers.Layer.Markers("Obs");
        newLayer = true;
    }
   
    var iconSize =  new OpenLayers.Size(15,15);
    var iconOffset = new OpenLayers.Pixel(-(iconSize.w/2), -iconSize.h);
    var squareIcon = new OpenLayers.Icon('http://www.resc.reading.ac.uk/whiteSquare.png', iconSize, iconOffset)
    var dotIcon = new OpenLayers.Icon('http://www.resc.reading.ac.uk/whiteDot.png', iconSize, iconOffset);
    var triangleIcon = new OpenLayers.Icon('http://www.resc.reading.ac.uk/whiteTriangle.png', iconSize, iconOffset);

    var nObs = obsIds.length;

    var noObs = true;
    for(var iObs = 0; iObs < nObs; iObs++)
    {
        if (obsIds[iObs] != "null")
        {
            noObs = false;
        }
        var location = new OpenLayers.LonLat(obsLons[iObs], obsLats[iObs]);
        if (obsIds[iObs].contains('ferry')) {
            feature = new OpenLayers.Feature(markersLayer, location, {icon:dotIcon.clone(), id:obsIds[iObs], type:"ferry"});
        }
        else if ((obsIds[iObs].contains('LIVBAY')) || (obsIds[iObs].contains('WARP')) || (obsIds[iObs].contains('WGABBARD')) || (obsIds[iObs].contains('OYSTERGRD'))) {
            feature = new OpenLayers.Feature(markersLayer, location, {icon:squareIcon.clone(), id:obsIds[iObs], type:"smartBuoy"});
        }
        else {
            feature = new OpenLayers.Feature(markersLayer, location, {icon:triangleIcon.clone(), id:obsIds[iObs], type:"SEPRISE"});
        }
        var marker = feature.createMarker();
        markersLayer.addMarker(marker);
        marker.events.register("mousedown", feature, clickOnObs);
        marker.events.register("mouseover", feature, hoverOverObs);
        marker.events.register("mouseout", feature, mouseOffObs);
        
        if (highlightMarkers.length > 0) {
            if (feature.data.id == highlightMarkers[0].id) {
                highlightObsMarker(feature);
            }
        }
        
        //if (noObs == true)
        //{
        //    loadingObsPopup.setContentHTML("<div align=\"center\"><code>no obs for this date and/or parameter<code></div>");
        //    window.clearInterval(loadingObsId);
        //    window.setTimeout('closeLoadingObsPopup()', 2000);
        //}
    }
    
    markersLayer.setVisibility(true);
    //loadingObsPopup.hide();
    //loadingObsPopup = null;
    //window.clearInterval(loadingObsId);

    if (newLayer == true) {
        map.addLayer(markersLayer);
        newLayer = false;
    }
}

function hoverOverObs(e) {
    popupMetadata(this);
}

function mouseOffObs(e) {
    markersLayer.map.removePopup(metadataPopup);
    metadataPopup = null;
}

function popupMetadata(feature) {
    //alert(feature.data.id);
    metadataPopup = feature.createPopup(false);
    metadataPopup.setBackgroundColor("white");
    metadataPopup.setOpacity(0.97);
    metadataPopup.padding = 10;
    markersLayer.map.addPopup(metadataPopup, false);
    var popupContent = "Platform Id: " + feature.data.id + "<br> Click for timeseries";
    metadataPopup.setContentHTML(popupContent);
    //metadataPopup.setSize(new OpenLayers.Size(180, 50));
    metadataPopup.updateSize();
}

function clickOnObs(e) 
{
    markersLayer.map.removePopup(metadataPopup);
    metadataPopup = null;
    
    if (popup != null) {
     	//feature.destroyPopup();
	markersLayer.map.removePopup(popup);
        window.clearInterval(loadingId);
    }  

    debugTimes += new Date().getTime() + "  start clickOnObs" + "\n";
    activeFeature = this;
    popupPresent = false;
 
    // create popup. This method calls the obs and model data request functions
    if (plot == null) {
        createObsModelPopup(this, 10, calendar.date.getTime() - (10 * MILLIS_IN_DAY), calendar.date.getTime());
    } else {
        newDataIsFwd = null; 
        var min = plot.getAxes().xaxis.min;
        var max = plot.getAxes().xaxis.max;
        var ndays = Math.ceil((max - min) / MILLIS_IN_DAY);
        maxNDays = ndays;
        earliestStartTime = min;
        latestEndTime = max;
        endChange = 0;
        startChange = 0;
        createObsModelPopup(this, ndays, min, max);
    }
    Event.stop(e);
}

function createObsModelPopup(feature, ndaysLocal, startTime, endTime)
{
    if (!feature) {
        feature = this.activeFeature;
        popupPresent = false;
        ndays += ndaysLocal;
        maxNDays = ndays > maxNDays ? ndays : maxNDays;
    }
    else {
        ndays = ndaysLocal;
        maxNDays = ndays > maxNDays ? ndays : maxNDays;
    }
    
    if (ndays < maxNDays) {
        document.getElementById('maxUnzoomLeft').src = "images/max_unzoom.png";
        document.getElementById('maxUnzoomLeft').style.cursor = "pointer";
        document.getElementById('maxUnzoomRight').src = "images/max_unzoom.png";
        document.getElementById('maxUnzoomRight').style.cursor = "pointer";
    } else {
        document.getElementById('maxUnzoomLeft').src = "images/max_unzoom_grey.png";
        document.getElementById('maxUnzoomLeft').style.cursor = "default";
        document.getElementById('maxUnzoomRight').src = "images/max_unzoom_grey.png";
        document.getElementById('maxUnzoomRight').style.cursor = "default";
    }
   
    this.feature = feature;
    debugTimes += new Date().getTime() + "  start createObsModelPopup" + "\n";
    var endDate = new Date(endTime);
    var startDate = new Date(startTime);
    numDays = Math.ceil((endDate.getTime() - startDate.getTime()) / MILLIS_IN_DAY);
    
    var year = startDate.getYear() + 1900;
    var month = startDate.getMonth();
    var day = startDate.getDate();
    var noData = isDateDisabled(startDate, year, month, day);
     
    modelValsCommaStr = "";
    //check if startDate has model data available. If not search forward until find date with model data
    var foundFirstData = false;
    datesArr = new Array();
    for (var iDay = 0; iDay < numDays + 1; iDay++) {
    	milisec = endDate.getTime() - ((numDays - iDay ) * MILLIS_IN_DAY);
        var date = new Date(milisec);
        datesArr[iDay] = date;
	var year = date.getYear();
	if (year < 1000) {
	    year += 1900;
	    // on IE and Opera, Date.getYear() returns YYYY (e.g. 1997) but on other browsers, YY (e.g. 97) is returned
	}
	noData = isDateDisabled(date, year, date.getMonth(), date.getDate());
	if (noData == false) {
            if (foundFirstData == false) {
                startDate = date;
                firstDataDay = iDay;
                foundFirstData = true;
            }
     	}
 	else {
	    modelValsCommaStr += "null,";
	}
    }
    
    // get the times available for the startdate, so we can pick the earliest time.
    getTimesteps(activeLayer.server, {
        callback: gotStartDateTimes,
        layerName: activeLayer.id,
        day: makeIsoDate(startDate)
    });
}  

function gotStartDateTimes(times) {
    modelTimeBounds = "";
    newModelTimesVals = new Array();
    for (var iDay = firstDataDay; iDay < datesArr.length; iDay++) {
        var modelTime = makeIsoDate(datesArr[iDay]) + "T" + times[0] + "Z";
        modelTimeBounds += modelTime + ",";
        var date = iso8601ToDate(modelTime);
        newModelTimesVals[iDay] = date.getTime();
    }
    // remove trailing comma
    modelTimeBounds = modelTimeBounds.substr(0, modelTimeBounds.length - 1);

    debugTimes += new Date().getTime() + "  done date calcs" + "\n";
    
    // request the model data from ncWMS
    var params = {
        TIME: modelTimeBounds,
        REQUEST: "GetFeatureInfo",
        BBOX: map.getExtent().toBBOX(),
        I: map.getPixelFromLonLat(feature.marker.lonlat).x,
        J: map.getPixelFromLonLat(feature.marker.lonlat).y,
        INFO_FORMAT: 'text/xml',
        QUERY_LAYERS: ncwms.params.LAYERS,
        WIDTH: map.size.w,
        HEIGHT: map.size.h
    };
    if (activeLayer.server != '') {
        // This is the signal to the server to load the data from elsewhere
        params.url = activeLayer.server;
    }
    featureInfoUrl = ncwms.getFullRequestString(
        params,
        'wms' // We must always load from the home server
    );

    debugTimes += new Date().getTime() + "  formulated featureInfoUrl. About to get Model data" + "\n";
    OpenLayers.loadURL(featureInfoUrl, '', feature, gotModelPointValues);
    
    // now that we have requested model data from ncWMS, we can fill in the times of any
    // null model times so that it balances for the later plot drawing request to Obs code
    var initialModelTimes = "";
    for (var iDay = 0; iDay < firstDataDay; iDay++) {
        initialModelTimes += (makeIsoDate(datesArr[iDay]) + "T" + times[0] + "Z,");
    }
    modelTimeBounds = initialModelTimes + modelTimeBounds;
    var timesArr = modelTimeBounds.split(",");
    var firstTime = iso8601ToDate(timesArr[0]).getTime();
    var lastTime = iso8601ToDate(timesArr[timesArr.length - 1]).getTime();
    var numDays = Math.ceil((lastTime - firstTime) / MILLIS_IN_DAY);
    
    debugTimes += new Date().getTime() + "  About to get Obs data" + "\n";
    // get obs data
    
    getObsValues(activeLayer.server, 
    {
      callback: gotObsPointValues,
      layerName: activeLayer.id,
      bbox: bbox[0] + ',' + bbox[1] + ',' + bbox[2] + ',' + bbox[3],
      width: map.size.w,
      height: map.size.h,
      day: timesArr[timesArr.length - 1],
      id: activeFeature.data.id,
      ndays: numDays
    });

    // create the popup. The data will be introduced to this from the obs
    // and model functions called above when they have completed.
    popup = feature.createPopup(false);
    popup.setBackgroundColor("white");
    popup.setOpacity(0.95);
   
    popup.setSize(new OpenLayers.Size(120, 40));
    markersLayer.map.addPopup(popup, false); 
   
    document.body.style.cursor = 'wait'; 
    //popupContent = "<div align=\"center\"><font color=\"black\"><code>getting data |<code></font></div>"
    //popup.setContentHTML(popupContent);
    //loadingId = window.setInterval('animateLoadingWheel(popup)', 500);
}

function animateLoadingWheel(popup)
{
    var wheelIndex = popupContent.indexOf('|');
    var firstBit = popupContent.substring(0, wheelIndex);
    var lastBit = popupContent.substring(wheelIndex + 1);
    popup.setContentHTML(firstBit + loadingStrings[loadingIndex % 4] + lastBit);
    loadingIndex++;
}

function closeLoadingObsPopup()
{
    loadingObsPopup.hide();
    loadingObsPopup = null;
}

function gotModelPointValues(response)
{
    debugTimes += new Date().getTime() + "  start gotModelPointValues" + "\n";
    var xmldoc = response.responseXML;
    var lon = xmldoc.getElementsByTagName('longitude')[0];
    var lat = xmldoc.getElementsByTagName('latitude')[0];
    var vals = xmldoc.getElementsByTagName('value');
    
    for (var iVal =  0; iVal < vals.length; iVal++)
    {
        var modelVal = vals[iVal].firstChild.nodeValue;
        if (activeLayer.id == "ECOOP_NSBS/analysed_sst" || activeLayer.id == "ECOOP_BSH_TS_hindcast/wtemp") {
            modelVal -= 273.15;
        }
        modelValsCommaStr += modelVal + ",";
        newModelTimesVals[iVal] = new Array(newModelTimesVals[iVal], modelVal);
    }
    modelPointLon = lon.firstChild.nodeValue;
    modelPointLat = lat.firstChild.nodeValue;
    
    if (newDataIsFwd == null) {
        modelTimesVals = newModelTimesVals; // first time we've loaded this timeseries
    }
    else if (newDataIsFwd == true) {
        modelTimesVals = modelTimesVals.concat(newModelTimesVals); // expanding timeseries going fwd in time
    } else {
        modelTimesVals = newModelTimesVals.concat(modelTimesVals); // expanding timeseries going back in time
    }
    
    gotModelData = true;
    
    if (gotObsData == true && popupPresent == false)
    {
        debugTimes += new Date().getTime() + "  about to call updatePopupContent from gotModel" + "\n";
        updatePopupContent();
    }
}

function gotObsPointValues(obsTimesAndValues)
{
    debugTimes += new Date().getTime() + "  start gotObsPointValues" + "\n";
    // get the times and values and record globally
    obsTimes = obsTimesAndValues.times;
    obsValues = obsTimesAndValues.values;
    
    obsTimesCommaStr = "";
    obsValuesCommaStr = "";
    var newObsTimesVals = new Array();
    for (var iTime = 0; iTime < obsTimes.length; iTime++)
    {
        if (obsTimes[iTime] != "x") {
          var tmp = obsTimes[iTime].replace(/ /g, "_") + ",";
          obsTimesCommaStr += tmp;
        }
    }
    
    for (var iValue = 0; iValue < obsValues.length; iValue++)
    {   
        var sigFigStr = toNSigFigs(parseFloat(obsValues[iValue]), 5);
        var valueStr = "" + obsValues[iValue];
        var valueLength = valueStr.length;
        var sigFigLength = sigFigStr.length;

        var shorterStr;
        if (valueLength < sigFigLength) {
            shorterStr = obsValues[iValue];
        }
        else {
            shorterStr = sigFigStr;
        }
        obsValuesCommaStr += shorterStr + ",";
        
        var date = iso8601ToDate(obsTimes[iValue]);
        newObsTimesVals[iValue] = new Array(date.getTime(), shorterStr);
    }
    
    if (newDataIsFwd == null) {
        obsTimesVals = newObsTimesVals; // first time we've loaded this timeseries
    }
    else if (newDataIsFwd == true) {
        obsTimesVals = obsTimesVals.concat(newObsTimesVals); // expanding timeseries going fwd in time
    } else {
        obsTimesVals = newObsTimesVals.concat(obsTimesVals); // expanding timeseries going back in time
    }

    gotObsData = true;
    
    // if the model data request is completed we can send both this obs data and
    // that model data to the popup. If not, the model function will do this.
    if (gotModelData == true && popupPresent == false)
    {
        debugTimes += new Date().getTime() + "  about to call updatePopupContent from gotObs" + "\n";
        updatePopupContent();
    }
}

function updatePopupContent()
{   
    debugTimes += new Date().getTime() + "  start updatePopupContent" + "\n";
    popupPresent = true;
    
    var modelLonLat = new OpenLayers.LonLat(modelPointLon, modelPointLat);
    var i = map.getPixelFromLonLat(modelLonLat).x;
    var j = map.getPixelFromLonLat(modelLonLat).y;
   
    modelTimeBounds = modelTimeBounds.substr(0, modelTimeBounds.indexOf(",", 0)) + "/" +
                      modelTimeBounds.substr(modelTimeBounds.lastIndexOf(",", modelTimeBounds.length - 1) + 1);
    
    //var url = "http://localhost:8084/ECOOPObs/ECOOPObsServlet?item=plot&modelTimes=" + modelTimeBounds + "&modelValues=" + 
    //    modelValsCommaStr + "&obsTimes=" + obsTimesCommaStr + "&obsValues=" + obsValuesCommaStr + 
    //    "&title=" + activeFeature.data.id + "&layer=" + activeLayer.id + "&id=" + activeFeature.data.id + "&i=" + i + "&j=" + j + "&BBOX=" + map.getExtent().toBBOX();
    
    var url = "http://lovejoy.nerc-essc.ac.uk:8080/ECOOPObs/ECOOPObsServlet?item=plot&modelTimes=" + modelTimeBounds + "&modelValues=" + 
        modelValsCommaStr + "&obsTimes=" + obsTimesCommaStr + "&obsValues=" + obsValuesCommaStr + 
        "&title=" + activeFeature.data.id + "&layer=" + activeLayer.id + "&id=" + activeFeature.data.id + "&i=" + i + "&j=" + j + "&BBOX=" + map.getExtent().toBBOX();
    
    //var url = "http://lovejoy.nerc-essc.ac.uk:9080/ECOOPObs/ECOOPObsServlet?item=plot&modelTimes=" + modelTimeBounds + "&modelValues=" + 
    //    modelValsCommaStr + "&obsTimes=" + obsTimesCommaStr + "&obsValues=" + obsValuesCommaStr + 
    //    "&title=" + activeFeature.data.id + "&layer=" + activeLayer.id + "&id=" + activeFeature.data.id + "&i=" + i + "&j=" + j + "&BBOX=" + map.getExtent().toBBOX();
    
    debugTimes += new Date().getTime() + "  formulated url" + "\n";
    //popUp(url, 500, 400);
    flotPlot();
}

function getValsFromXAxis(timesValsArr, xaxis) {
    var min = xaxis.min;
    var max = xaxis.max;
    
    if (min == null || max == null) return timesValsArr;
    
    var newTimesValsArr = new Array();
    
    for (var i = 0; i < timesValsArr.length; i++) {
        if (timesValsArr[i][0] >= min && timesValsArr[i][0] <= max) {
            newTimesValsArr.push(timesValsArr[i]);
        }
    }
    return newTimesValsArr;
}

function flot(obsLabel, modelLabel, modelAxis, xaxis, yaxis) 
{
    obsTimesVals.sort();
 
    var newObsTimesVals = getValsFromXAxis(obsTimesVals, xaxis);
    var newModelTimesVals = getValsFromXAxis(modelTimesVals, xaxis);
 
    plot = jQuery.plot(jQuery("#placeholder"),
           [ 
             { data: newObsTimesVals, label: obsLabel, color: "red" }, 
             { data: newModelTimesVals, label: modelLabel, yaxis: modelAxis, color: "blue" }
           ],
           { lines: { show: true },
             points: { show: true },
             selection: { mode: "xy" },
             grid: { hoverable: true, clickable: true, markings: getCurrentDateMarkings(xaxis) },
             xaxis: xaxis, 
             yaxis: yaxis
           });
           
    return plot;
}

function flotPlot() 
{
    var obsLabel = "Obs (" + feature.data.id + ")";
    var modelLabel = "Model";
    var modelData = activeLayer.id.substring(activeLayer.id.lastIndexOf('/') + 1);
    if (modelData == "spm1") { modelLabel = "Model (right axis): Suspended Particulate Matter (35um) (kg m-3)"; obsLabel = "Obs (left axis): Turbidity (FTU)";}
    if (modelData == "spm2") { modelLabel = "Model (right axis): Suspended Particulate Matter (2um) (kg m-3)"; obsLabel = "Obs (left axis): Turbidity (FTU)";}
    if (modelData == "par") { modelLabel = "Model (right axis): Photosynthetically Active Radiation (W m-2)"; obsLabel = "Obs (left axis): Irradiance (E x10-6 m-2 s-1)";}
    if (modelData == "chl") { modelLabel = "Model (right axis): Chlorophyll a (mg m-3)"; obsLabel = "Obs (left axis): Flourescence (arb. unit)";}
    
    var dualAxesModels = "chl,spm1,spm2,par";
    var modelAxis = 1;
    if (dualAxesModels.contains(modelData)) modelAxis = 2;

    var endTime = plot == null ? calendar.date.getTime() : plot.getAxes().xaxis.max + (endChange * MILLIS_IN_DAY);
    var startTime = endTime - (this.ndays * MILLIS_IN_DAY);
    
    plot = flot(obsLabel, modelLabel, modelAxis, {mode:"time", min: startTime, max: endTime }, { autoscaleMargin : null });
    
    
    $('flot').style.visibility = "visible";
    $('imagePanel').style.top = '525px';
    $('layerText').style.top = '460px';
    $('panelHeader').style.top = '470px';
    $('layerSelector').style.top = '645px';
    $('calendar').style.top = '-5px';
    $('mainCol').style.height = '930px';
    $('mainCol').style.minHeight = '930px';
    
    // these lines were at the bottom of updatePopupContent (below where the call to flotPlot() now is)
    markersLayer.map.removePopup(popup);
    window.clearInterval(loadingId);
    debugTimes += new Date().getTime() + "  called popup - done" + "\n";
    gotObsData = false;
    gotModelData = false;
    popup = null;
    
    document.body.style.cursor = 'default';
    highlightObsMarker(feature);
    
    document.body.style.cursor = 'default';
    document.getElementById('startFwdArrow').style.cursor = 
        document.getElementById('startFwdArrow').src.contains('grey') ? "default" : "pointer";
    document.getElementById('startBackArrow').style.cursor = 
        document.getElementById('startBackArrow').src.contains('grey') ? "default" : "pointer";
    document.getElementById('endFwdArrow').style.cursor = 
        document.getElementById('endFwdArrow').src.contains('grey') ? "default" : "pointer";
    document.getElementById('endBackArrow').style.cursor = 
        document.getElementById('endBackArrow').src.contains('grey') ? "default" : "pointer";
}

function removeDeletePointDiv() {
    $('deletePoint').style.visibility = "hidden";
}

function deletePointFromPlot(series, dataPoint) {
    
    
        $('deletePoint').style.visibility = "hidden";
      
        
        if (series == 0) {
            obsPointsToDelete.sort( function (a,b) { return a-b });
            for (var iPoint = 0; iPoint < obsPointsToDelete.length; iPoint++) {
                highlightIdx--;
                var pointDate = obsPointsToDelete[iPoint][0];
                for (var i = 0; i < obsTimesVals.length; i++) {
                    if (obsTimesVals[i][0] == pointDate) {
                        obsTimesVals.splice(i, 1);
                        break;
                    }
                }
            }
            obsPointsToDelete = new Array();
        } else if (series == 1) {
            modelPointsToDelete.sort( function (a,b) { return a-b });
            for (var iPoint = 0; iPoint < modelPointsToDelete.length; iPoint++) {
                highlightIdx--;
                var pointDate = modelPointsToDelete[iPoint][0];
                for (var i = 0; i < modelTimesVals.length; i++) {
                    if (modelTimesVals[i][0] == pointDate) {
                        modelTimesVals.splice(i, 1);
                        break;
                    }
                }
            }
            modelPointsToDelete = new Array();
        }
        
        var modelData = activeLayer.id.substring(activeLayer.id.lastIndexOf('/') + 1);
        var obsLabel = "obs (" + feature.data.id + ")";
        var modelLabel = "Model";
        if (modelData == "spm1") modelLabel = "Model: Suspended Particulate Matter (35um) (kg m-3)";
        if (modelData == "spm2") modelLabel = "Model: Suspended Particulate Matter (2um) (kg m-3)";
        if (modelData == "par") modelLabel = "Model: Photosynthetically Active Radiation (W m-2)";
        if (modelData == "chl") modelLabel = "Model: Chlorophyll a (mg m-3)";
        var dualAxesModels = "chl,spm1,spm2,par";
        var modelAxis = 1;
        if (dualAxesModels.contains(modelData)) modelAxis = 2;
        var minTime = plot.getAxes().xaxis.min;
        var maxTime = plot.getAxes().xaxis.max;
        plot = flot(obsLabel, modelLabel, modelAxis, {mode:"time", min:minTime, max:maxTime}, null);
    
}

function highlightObsMarker(feature) {
    var iconSize =  new OpenLayers.Size(15,15);
    var iconOffset = new OpenLayers.Pixel(-(iconSize.w/2), -iconSize.h);
    var squareHighlight = new OpenLayers.Icon('http://www.resc.reading.ac.uk/blackSquare.png', iconSize, iconOffset);
    var triangleHighlight = new OpenLayers.Icon('http://www.resc.reading.ac.uk/blackTriangle.png', iconSize, iconOffset);
    var dotHighlight = new OpenLayers.Icon('http://www.resc.reading.ac.uk/blackDot.png', iconSize, iconOffset);
    
    if (feature.data.type == "smartBuoy") {
        feature.data.icon = squareHighlight.clone();
    }
    else if (feature.data.type == "SEPRISE") {
        feature.data.icon = triangleHighlight.clone();
    }
    else {
        feature.data.icon = dotHighlight.clone();
    }
    
    if (highlightMarkers.length > 0) markersLayer.removeMarker(highlightMarkers[0]);
    highlightMarkers[0] = feature.createMarker();
    highlightMarkers[0].id = feature.data.id;
    markersLayer.addMarker(highlightMarkers[0]);
}

function flotExpandBackward(ndays) {
    if (zoomedInViaSelection) return; // if we are zoomed in, the expand left/right arrows are disabled
    
    this.startChange = ndays;
    this.endChange = 0;
    
    if (this.ndays + ndays <= 0) {
        alert("can't move timeseries start forwards by 10 days -- it must be before the timeseries end date!");
        return;
    } else if (this.ndays + ndays <= 10) { 
        document.getElementById('endBackArrow').src = "images/arrow_back_grey.png";
        document.getElementById('endBackArrow').style.cursor = "default";
        document.getElementById('startFwdArrow').src = "images/arrow_fwd_grey.png";
        document.getElementById('startFwdArrow').style.cursor = "default";
    } else {
        document.getElementById('endBackArrow').src = "images/arrow_back.png";
        document.getElementById('endBackArrow').style.cursor = "pointer";
        document.getElementById('startFwdArrow').src = "images/arrow_fwd.png";
        document.getElementById('startFwdArrow').style.cursor = "pointer";
    }
    
    document.body.style.cursor = 'wait';
    document.getElementById('startFwdArrow').style.cursor = "wait";
    document.getElementById('startBackArrow').style.cursor = "wait";
    document.getElementById('endFwdArrow').style.cursor = "wait";
    document.getElementById('endBackArrow').style.cursor = "wait";
    
    if (ndays > 0) {
        // we are expanding the start of the timeseries further back in time
        // we need to check if we are expanding past the earliest data retrieved from
        // the server during this session or not. If the former we need to make a new
        // request to the server. If the latter we can use data cached in an array here.
        
        var newStartTime = plot.getAxes().xaxis.min - (this.startChange * MILLIS_IN_DAY );
        if (earliestStartTime == null) earliestStartTime = plot.getAxes().xaxis.min;
        
        if (newStartTime < earliestStartTime) {
            // we are going past (before) the earliest cached data - new server request needed 
            earliestStartTime = newStartTime;
            this.newDataIsFwd = false; 
            createObsModelPopup(null, ndays, newStartTime, plot.getAxes().xaxis.min);
        } else {
            // we are NOT going past (before) the earliest cached data - just use cached data
            var obsLabel = plot.getData()[0].label;
            var modelLabel = plot.getData()[1].label;
            var modelAxis = plot.getData()[1].yaxis;
            var allData = [ { data: obsTimesVals, label: obsLabel, color: "red" }, { data: modelTimesVals, label: modelLabel, yaxis: modelAxis, color: "blue" } ]
            flotFunction(ndays, 0, allData);
        }
    } else {
        // we are contracting the timeseries by moving the start of it forward in time.
        // we can achieve this be effectively zooming in on the data from the left
        if (earliestStartTime == null) earliestStartTime = plot.getAxes().xaxis.min;
        var displayedData = plot.getData();
        flotFunction(ndays, 0, displayedData);
    }
}

function flotExpandForward(ndays) {  
    if (zoomedInViaSelection) return; // if we are zoomed in, the expand left/right arrows are disabled
    
    var sigNum = ndays < 0 ? -1 : 1;
  
    
    var date = calendar.date;
    var prevDate = date;
    for (var iDay = 0; iDay != (ndays + sigNum); iDay+=sigNum) {
        date = new Date(plot.getAxes().xaxis.max + iDay * MILLIS_IN_DAY);
        var year = date.getYear();
        if (year < 1000) {
            year += 1900;
            // on IE and Opera, Date.getYear() returns YYYY (e.g. 1997) but on other browsers, YY (e.g. 97) is returned
        }
        var noData = isDateDisabled(date, year, date.getMonth(), date.getDate());
        if (noData) {
            date = prevDate;
            break;
        }
        else {
            prevDate = date;
        }
    }
    
    if (noData) {
        if (iDay == 1) {
            alert("already at last available date");
            document.getElementById('endFwdArrow').src = "images/arrow_fwd_grey.png";
            document.getElementById('endFwdArrow').style.cursor = "default";
            ndays = iDay - 1;
            return;
        } 
        document.getElementById('endFwdArrow').src = "images/arrow_fwd_grey.png";
        document.getElementById('endFwdArrow').style.cursor = "default";
        ndays = iDay - 1;
    } /* else {
        calendar.setDate(date);
        calendar.refresh();
        calendar.show();
        loadTimesteps(true);
    }
    */
       
    this.startChange = 0;
    this.endChange = ndays;
    
    if (this.ndays + ndays <= 0) {
        alert("can't move timeseries end back by 10 days -- it must be after the timeseries start date!");
        return;
    } else if (this.ndays + ndays <= 10) { 
        document.getElementById('endBackArrow').src = "images/arrow_back_grey.png";
        document.getElementById('endBackArrow').style.cursor = "default";
        document.getElementById('startFwdArrow').src = "images/arrow_fwd_grey.png";
        document.getElementById('startFwdArrow').style.cursor = "default";
    } else {
        document.getElementById('endBackArrow').src = "images/arrow_back.png";
        document.getElementById('endBackArrow').style.cursor = "pointer";
        document.getElementById('startFwdArrow').src = "images/arrow_fwd.png";
        document.getElementById('startFwdArrow').style.cursor = "pointer";
    }
    
    document.body.style.cursor = 'wait';
    document.getElementById('startFwdArrow').style.cursor = "wait";
    document.getElementById('startBackArrow').style.cursor = "wait";
    document.getElementById('endFwdArrow').style.cursor = "wait";
    document.getElementById('endBackArrow').style.cursor = "wait";

    if (sigNum == -1) {
        // contracting timeseries by moving end date back in time
        if (latestEndTime == null) latestEndTime = plot.getAxes().xaxis.max;
        document.getElementById('endFwdArrow').src = "images/arrow_fwd.png";
        document.getElementById('endFwdArrow').style.cursor = "pointer";
        
        var displayedData = plot.getData();
        flotFunction(0, ndays, displayedData);
    }
    else /*if (noData == false )*/ {
        // extending timeseries by moving end data forward in time (if model data exists this far in the future)
        var newEndTime = plot.getAxes().xaxis.max + (this.endChange * MILLIS_IN_DAY ); 
        if (latestEndTime == null) latestEndTime = plot.getAxes().xaxis.max;
        
        if (newEndTime > latestEndTime) {
            // we are going past (after) the latest cached data - new server request needed 
            latestEndTime = newEndTime;
            this.newDataIsFwd = true; 
            createObsModelPopup(null, ndays, plot.getAxes().xaxis.max, newEndTime);
        } else {
            // we are NOT going past (after) the latest cached data - just use cached data
            var obsLabel = plot.getData()[0].label;
            var modelLabel = plot.getData()[1].label;
            var modelAxis = plot.getData()[1].yaxis;
            var allData = [ { data: obsTimesVals, label: obsLabel, color: "red" }, { data: modelTimesVals, label: modelLabel, yaxis: modelAxis, color: "blue" } ]
            flotFunction(0, ndays, allData);
        }
    }
}

function flotZoomOut() {    
    if (zoomedInViaSelection == false && this.ndays == this.maxNDays) return;
    
    zoomedInViaSelection = false;
    document.getElementById('maxUnzoomLeft').src = "images/max_unzoom_grey.png";
    document.getElementById('maxUnzoomLeft').style.cursor = "default";
    document.getElementById('maxUnzoomRight').src = "images/max_unzoom_grey.png";
    document.getElementById('maxUnzoomRight').style.cursor = "default";
    
    if (latestEndTime == null) latestEndTime = plot.getAxes().xaxis.max;
    if (earliestStartTime == null) earliestStartTime = plot.getAxes().xaxis.min;
    
    var obsLabel = plot.getData()[0].label;
    var modelLabel = plot.getData()[1].label;
    var modelAxis = plot.getData()[1].yaxis;
    plot = flot(obsLabel, modelLabel, modelAxis, {mode:"time"}, null);

    this.ndays = this.maxNDays;
    document.getElementById('endFwdArrow').src = "images/arrow_fwd.png";
    document.getElementById('endFwdArrow').style.cursor = "pointer";
    document.getElementById('startBackArrow').src = "images/arrow_back.png";
    document.getElementById('startBackArrow').style.cursor = "pointer";
    if (this.ndays <= 10) { 
        document.getElementById('endBackArrow').src = "images/arrow_back_grey.png";
        document.getElementById('endBackArrow').style.cursor = "default";
        document.getElementById('startFwdArrow').src = "images/arrow_fwd_grey.png";
        document.getElementById('startFwdArrow').style.cursor = "default";
    } else {
        document.getElementById('endBackArrow').src = "images/arrow_back.png";
        document.getElementById('endBackArrow').style.cursor = "pointer";
        document.getElementById('startFwdArrow').src = "images/arrow_fwd.png";
        document.getElementById('startFwdArrow').style.cursor = "pointer";
    }

}

function getFlotData(datasets, x1, x2) {
    //alert(new Date(x1) + " " + new Date(x2));
    var obsData = datasets[0].data;
    var modelData = datasets[1].data;
    var zoomedObsData = [];
    var zoomedModelData = [];
    
    if (datasets.length > 2) {
        var minObsData = datasets[2].data[0];
        var maxObsData = datasets[3].data[1];
        var minModelData = datasets[4].data[0];
        var maxModelData = datasets[5].data[1];

        //alert("minObs: " + minObsData + "maxObs: " + maxObsData);

        if (minObsData != null) obsData.unshift(minObsData);
        if (maxObsData != null) obsData.push(maxObsData);
        if (minModelData != null) modelData.unshift(minModelData);
        if (maxModelData != null) modelData.push(maxModelData);
    }
    
    var lowerIdx = -1;
    var upperIdx = obsData.length;
    for (var i = 0; i < obsData.length; i++) {
        if (obsData[i][0] >= x1 && lowerIdx == -1) lowerIdx = (i - 1);
        if (obsData[i][0] > x2 && upperIdx == obsData.length) {
            upperIdx = i;
            break;
        } 
        if (obsData[i][0] >= x1 && obsData[i][0] <= x2) {
            zoomedObsData.push(new Array(obsData[i][0], obsData[i][1]));
        }
    }
    var lowerObs = lowerIdx < 0 ? new Array(x1, Number.Nan) 
        : new Array(obsData[lowerIdx][0], obsData[lowerIdx][1]);
    var upperObs = upperIdx >= obsData.length ? new Array(x2, Number.NaN)
        : new Array(obsData[upperIdx][0], obsData[upperIdx][1]);
    
    var rightOfLowerObs = zoomedObsData.length > 0 ? zoomedObsData[0] : upperObs;
    var leftOfUpperObs = zoomedObsData.length > 0 ? zoomedObsData[zoomedObsData.length - 1] : lowerObs;
    var minObsPoint = interpolate(lowerObs, rightOfLowerObs, x1);
    var maxObsPoint = interpolate(leftOfUpperObs, upperObs, x2);
    zoomedObsData.unshift(new Array(x1, minObsPoint));
    zoomedObsData.push(new Array(x2, maxObsPoint));
    
    //for (var i = 0; i < zoomedObsData.length; i++) {
    //    alert("zoomedObsData " + i + ": " + new Date(zoomedObsData[i][0]) + "," + zoomedObsData[i][1]);
    //}
    
    lowerIdx = -1;
    upperIdx = modelData.length;
    for (var i = 0; i < modelData.length; i++) {
        if (modelData[i][0] >= x1 && lowerIdx == -1) lowerIdx = (i - 1);
        if (modelData[i][0] > x2 && upperIdx == modelData.length) { 
            upperIdx = i;
            break;
        }
        if (modelData[i][0] >= x1 && modelData[i][0] <= x2) {
            zoomedModelData.push(new Array(modelData[i][0], modelData[i][1]));
        }
    }
    
    var lowerModel = lowerIdx < 0 ? new Array(x1, Number.NaN) 
        : new Array(modelData[lowerIdx][0], modelData[lowerIdx][1]);
    var upperModel = upperIdx >= modelData.length ? new Array(x2, Number.NaN)
        : new Array(modelData[upperIdx][0], modelData[upperIdx][1]);
    
    var rightOfLowerModel = zoomedModelData.length > 0 ? zoomedModelData[0] : upperModel;
    var leftOfUpperModel = zoomedModelData.length > 0 ? zoomedModelData[zoomedModelData.length - 1] : lowerModel;
    var minModelPoint = interpolate(lowerModel, rightOfLowerModel, x1);
    var maxModelPoint = interpolate(leftOfUpperModel, upperModel, x2);
    zoomedModelData.unshift(new Array(x1, minModelPoint));
    zoomedModelData.push(new Array(x2, maxModelPoint));
    
    //for (var i = 0; i < zoomedModelData.length; i++) {
    //    alert("zoomedModelData " + i + ": " + new Date(zoomedModelData[i][0]) + "," + zoomedModelData[i][1]);
    //}
    
    var zoomData = {
        mainObs: zoomedObsData.slice(1, zoomedObsData.length - 1),
        minObs: [[x1,minObsPoint],[zoomedObsData[1][0],zoomedObsData[1][1]]],
        maxObs: [[zoomedObsData[zoomedObsData.length - 2][0],zoomedObsData[zoomedObsData.length - 2][1]],[x2, maxObsPoint]],
        mainModel: zoomedModelData.slice(1, zoomedModelData.length - 1),
        minModel: [[x1, minModelPoint],[zoomedModelData[1][0],zoomedModelData[1][1]]],
        maxModel: [[zoomedModelData[zoomedModelData.length - 2][0],zoomedModelData[zoomedModelData.length - 2][1]],[x2, maxModelPoint]]
    };
    
    return zoomData;
} 

function flotFunction(ndaysBack, ndaysFwd, data) {
    this.ndays = this.ndays + ndaysBack + ndaysFwd;
    this.maxNDays = this.ndays > this.maxNDays ? this.ndays : this.maxNDays;
    
    if (this.ndays < this.maxNDays) {
        document.getElementById('maxUnzoomLeft').src = "images/max_unzoom.png";
        document.getElementById('maxUnzoomLeft').style.cursor = "pointer";
        document.getElementById('maxUnzoomRight').src = "images/max_unzoom.png";
        document.getElementById('maxUnzoomRight').style.cursor = "pointer";
    } else {
        document.getElementById('maxUnzoomLeft').src = "images/max_unzoom_grey.png";
        document.getElementById('maxUnzoomLeft').style.cursor = "default";
        document.getElementById('maxUnzoomRight').src = "images/max_unzoom_grey.png";
        document.getElementById('maxUnzoomRight').style.cursor = "default";
    }
    
    var backMillis = ndaysBack * MILLIS_IN_DAY;
    var fwdMillis = ndaysFwd * MILLIS_IN_DAY;

    var xaxis = plot.getAxes().xaxis;

    var xaxisMin = xaxis.min - backMillis;
    var xaxisMax = xaxis.max + fwdMillis;
    var yaxisMin = null;  // to auto-scale to new data (not keep old y-axis bounds)
    var yaxisMax = null;  // to auto-scale to new data (not keep old y-axis bounds)

    var obsLabel = plot.getData()[0].label;
    var modelLabel = plot.getData()[1].label;
    var modelAxis = plot.getData()[1].yaxis;

    var zoomData = getFlotData(data, xaxisMin, xaxisMax);

    plot = jQuery.plot(jQuery("#placeholder"),
       [ 
         { data: zoomData.mainObs, label: obsLabel, color: "red", points: { show: true } }, 
         { data: zoomData.mainModel, label: modelLabel, yaxis: modelAxis, color: "blue", points: { show: true } },
         { data: zoomData.minObs, color: "red", points: { show: false } },
         { data: zoomData.maxObs, color: "red", points: { show: false } },
         { data: zoomData.minModel, color: "blue", points: { show: false } },
         { data: zoomData.maxModel, color: "blue", points: { show: false } }
       ],
       { lines: { show: true },
         selection: { mode: "xy" },
         grid: { hoverable: true, clickable: true, markings: getCurrentDateMarkings(xaxis) },
         xaxis: { mode: "time", min: xaxisMin, max: xaxisMax }, 
         yaxis: { min: yaxisMin, max: yaxisMax }
       });

       document.body.style.cursor = 'default';
       document.getElementById('startFwdArrow').style.cursor = 
          document.getElementById('startFwdArrow').src.contains('grey') ? "default" : "pointer";
       document.getElementById('startBackArrow').style.cursor = 
          document.getElementById('startBackArrow').src.contains('grey') ? "default" : "pointer";
       document.getElementById('endFwdArrow').style.cursor = 
          document.getElementById('endFwdArrow').src.contains('grey') ? "default" : "pointer";
       document.getElementById('endBackArrow').style.cursor = 
          document.getElementById('endBackArrow').src.contains('grey') ? "default" : "pointer";
}

function interpolate(point1, point2, targetX) {
    var xFrac = ((targetX - point1[0]) / (point2[0] - point1[0]));
    var targetY = parseFloat(point1[1]) + parseFloat((xFrac * (point2[1] - point1[1])));
    
    //alert("point1: " + point1 + ", point2: " + point2 + ", targetX: " + targetX + ",...targetY is: " + targetY);
    return targetY;
}

function getCurrentDateMarkings(xaxis) {
    var barTime = calendar.date.getTime();
    var barWid = 24 * 60 * 60 * 1000;
    var barCentredRightEdge = barTime + (barWid * 0.5);
    if (xaxis.max <= barCentredRightEdge) {
        barTime -= (barWid * 0.25);
        barWid *= 0.5;
    }
    var currentDateMarking = [ { xaxis: { from: (barTime - (barWid*0.5)), to: (barTime + (barWid*0.5)) }, color: "#f8e6b2" }];
    return currentDateMarking;
}

// Shows a pop-up window with the available palettes for the user to select
// This is called when the user clicks the colour scale bar
function showPaletteSelector()
{
    updatePaletteSelector();
    paletteSelector.render(document.body);
    paletteSelector.show();
}

// Updates the contents of the palette selection table
function updatePaletteSelector()
{
    // Populate the palette selector dialog box
    // TODO: revert to default palette if layer doesn't support this one
    var palettes = activeLayer.palettes;
    if (palettes == null || palettes.length == 0) {
        $('paletteDiv').innerHTML = 'There are no alternative palettes for this layer';
        return;
    }
    
    // TODO test if coming from a different server
    var width = 50;
    var height = 200;
    var paletteUrl = activeLayer.server + 'wms?REQUEST=GetLegendGraphic' +
        '&LAYER=' + activeLayer.id +
        '&COLORBARONLY=true' +
        '&WIDTH=1' +
        '&HEIGHT=' + height +
        '&NUMCOLORBANDS=' + $('numColorBands').value;
    var palStr = '<div style="overflow: auto">'; // ensures scroll bars appear if necessary
    palStr += '<table border="1"><tr>';
    for (var i = 0; i < palettes.length; i++) {
        palStr += '<td><img src="' + paletteUrl + '&PALETTE=' + palettes[i] +
            '" width="' + width + '" height="' + height + '" title="' + palettes[i] +
            '" onclick="paletteSelected(\'' + palettes[i] + '\')"' +
            '/></td>';
    }
    palStr += '</tr></table></div>';
    $('paletteDiv').innerHTML = palStr;
}

// Called when the user selects a new palette in the palette selector
function paletteSelected(thePalette)
{
    paletteName = thePalette;
    paletteSelector.hide();
    // Change the colour scale bar on the main page
    $('scaleBar').src = 'wms?REQUEST=GetLegendGraphic&COLORBARONLY=true&WIDTH=1&HEIGHT=398'
        + '&PALETTE=' + thePalette + '&NUMCOLORBANDS=' + $('numColorBands').value;
    updateMap(false);
}

// Decides whether to display the animation, or the tiled or untiled
// version of the ncwms layer
function setVisibleLayer(animation)
{
    // TODO: repeats code above
    var style = typeof activeLayer.supportedStyles == 'undefined' ? 'boxfill' : activeLayer.supportedStyles[0];
    if (animation) {
        setLayerVisibility(animation_layer, true);
        setLayerVisibility(ncwms_tiled, false);
        setLayerVisibility(ncwms_untiled, false);
    } else if (style.toLowerCase() == 'vector') {
        setLayerVisibility(animation_layer, false);
        setLayerVisibility(ncwms_tiled, false);
        setLayerVisibility(ncwms_untiled, true);
        ncwms = ncwms_untiled;
    } else {
        setLayerVisibility(animation_layer, false);
        setLayerVisibility(ncwms_tiled, true);
        setLayerVisibility(ncwms_untiled, false);
        ncwms = ncwms_tiled;
    }
    layerSwitcher.layerStates = []; // forces redraw
    layerSwitcher.redraw();
}

function setLayerVisibility(layer, visible)
{
    if (layer != null) {
        layer.setVisibility(visible);
        layer.displayInLayerSwitcher = visible;
    }
}

// Gets the Z value set by the user
function getZValue()
{
    // If we have no depth information, assume we're at the surface.  This
    // will be ignored by the map server
    var zIndex = $('zValues').selectedIndex;
    var zValue = $('zValues').options.length == 0 ? 0 : $('zValues').options[zIndex].firstChild.nodeValue;
    return zPositive ? zValue : -zValue;
}

// Sets the permalink, i.e. the link back to this view of the page
function setPermalinkURL()
{
    if (activeLayer != null) {
        // Note that we must use window.top to get the containing page, in case
        // the Godiva2 page is embedded in an iframe
        // Watch out for trailing hashes, which screw the permalink up
        var url = window.top.location.toString().replace('#', '');
        url +=
            '?menu=' + menu +
            '&layer=' + activeLayer.id +
            '&elevation=' + getZValue() +
            '&time=' + isoTValue +
            '&scale=' + scaleMinVal + ',' + scaleMaxVal +
            '&bbox=' + map.getExtent().toBBOX();
        $('permalink').innerHTML = '<a target="_blank" href="' + url +
            '">Permalink</a>&nbsp;|&nbsp;<a href="mailto:?subject=Godiva2%20link&body='
            + escape(url) + '">email</a>';
        $('permalink').style.visibility = 'visible';
    }
}

// Sets the URL for "Open in Google Earth"
function setGEarthURL()
{
    if (ncwms != null) {
        // Get a URL for a WMS request that covers the current map extent
        var mapBounds = map.getExtent();
        var urlEls = ncwms.getURL(mapBounds).split('&');
        var gEarthURL = urlEls[0];
        for (var i = 1; i < urlEls.length; i++) {
            if (urlEls[i].startsWith('FORMAT')) {
                // Make sure the FORMAT is set correctly
                gEarthURL += '&FORMAT=application/vnd.google-earth.kmz';
            } else if (urlEls[i].startsWith('TIME') && timeSeriesSelected()) {
                // If we can make an animation, do so
                gEarthURL += '&TIME=' + $('firstFrame').innerHTML + '/' + $('lastFrame').innerHTML;
            } else if (urlEls[i].startsWith('BBOX')) {
                // Set the bounding box so that there are no transparent pixels around
                // the edge of the image: i.e. find the intersection of the layer BBOX
                // and the viewport BBOX
                gEarthURL += '&BBOX=' + getIntersectionBBOX();
            } else if (urlEls[i].startsWith('WIDTH')) {
                gEarthURL += '&WIDTH=' + map.size.w;
            } else if (urlEls[i].startsWith('HEIGHT')) {
                gEarthURL += '&HEIGHT=' + map.size.h;
            } else if (!urlEls[i].startsWith('OPACITY')) {
                // We remove the OPACITY argument as Google Earth allows opacity
                // to be controlled in the client
                gEarthURL += '&' + urlEls[i];
            }
        }
        //if (timeSeriesSelected()) {
        //    $('googleEarth').innerHTML = '<a href=\'' + gEarthURL + '\'>Open animation in Google Earth</a>';
        //} else {
        //    $('googleEarth').innerHTML = '<a href=\'' + gEarthURL + '\'>Open in Google Earth</a>';
        //}
    }
}

// Returns a bounding box as a string in format "minlon,minlat,maxlon,maxlat"
// that represents the intersection of the currently-visible map layer's 
// bounding box and the viewport's bounding box.
function getIntersectionBBOX()
{
    var mapBounds = map.getExtent();
    var mapBboxEls = mapBounds.toBBOX().split(',');
    // bbox is the bounding box of the currently-visible layer
    var newBBOX = Math.max(parseFloat(mapBboxEls[0]), bbox[0]) + ',';
    newBBOX += Math.max(parseFloat(mapBboxEls[1]), bbox[1]) + ',';
    newBBOX += Math.min(parseFloat(mapBboxEls[2]), bbox[2]) + ',';
    newBBOX += Math.min(parseFloat(mapBboxEls[3]), bbox[3]);
    return newBBOX;
}

// Formats the given value to numSigFigs significant figures
// WARNING: Javascript 1.5 only!
function toNSigFigs(value, numSigFigs)
{
    if (!value.toPrecision) {
        // TODO: do this somewhere more useful
        alert("Your browser doesn't support Javascript 1.5");
        return value;
    } else {
        return value.toPrecision(numSigFigs);
    }
}

// Returns true if the user has selected a time series
function timeSeriesSelected()
{
    return $('firstFrame').innerHTML != '' && $('lastFrame').innerHTML != '';
}

// Takes a BBOX string of the form "minlon,minlat,maxlon,maxlat" and returns
// the corresponding OpenLayers.Bounds object
// TODO: error checking
function getBounds(bboxStr)
{
    var bboxEls = bboxStr.split(",");
    return new OpenLayers.Bounds(parseFloat(bboxEls[0]), parseFloat(bboxEls[1]),
        parseFloat(bboxEls[2]), parseFloat(bboxEls[3]));
}

function mouseOverFakeLink(idName)
{
    $(idName).style.textDecoration = "underline";
    $(idName).style.cursor = "pointer";
}

function mouseOutFakeLink(idName)
{
    $(idName).style.textDecoration = "none";
    $(idName).style.cursor = "default";
}

