/**
 * Return the unique values from this array
 * http://www.martienus.com/code/javascript-remove-duplicates-from-array.html
 * @param a
 * @return
 */
function unique(a)
{
   var r = new Array();
   o:for(var i = 0, n = a.length; i < n; i++)
   {
      for(var x = 0, y = r.length; x < y; x++)
      {
         if(r[x]==a[i]) continue o;
      }
      r[r.length] = a[i];
   }
   return r;
}

/**
 * Extract a parameter from the query string.
 *
 *  From http://ilovethecode.com/Javascript/Javascript-Tutorials-How_To-Easy/Get_Query_String_Using_Javascript.shtml
 * @param {String} ji The name of the parameter to be extracted
 * @return The value of the named parameter, or null if it doesn't exist.
 */
function querySt(ji) {
    hu = window.location.search.substring(1);
    gy = hu.split("&");
    for (i = 0; i < gy.length; i++) {
        ft = gy[i].split("=");
        if (ft[0] == ji) {
            return ft[1];
        }
    }
    return null;
}

/**
 * Format the object as indented JSON.
 *
 * @param object The object to return
 * @param depth The depth of the indentation
 * @return A string representation of the object.
 */
function inspect(object, depth)
{
    if (!depth)
    {
        depth = 1;
    }
    if (object == null) {
        return "null";
    }
    var temp = "{\n";
    for (x in object) {
        if (typeof object[x] == "object")
        {
            temp += pad(x + ": " + inspect(object[x], depth + 1) + "\n", depth * 4, " ");
        } else
        {
            temp += pad(x + ": " + object[x] + "\n", depth * 4, " ");
        }
    }

    return temp + pad("}", (depth - 1) * 4, " ");

}


function pad(str, toLen, padwith)
{
    toRet = String(str);
    while (toRet.length < toLen)
    {
        toRet = padwith + toRet;
    }
    return toRet;
}

/**
 * Get the keys of an array
 * @param o The array to search
 * @return An array containing the keys.
 */
function keys(o) {
    var toRet = new Array();
    for (var x in o) {
        toRet.push(x);
    }
    return toRet;
}
 
function values_for_keys(a, k) {
	var ret = new Array();
	for (var k_ in k) {
		ret.push(a[k[k_]]);
	}
	return ret;
}

/**
 * Convert a date object into 12 hour time.
 * @param date The date object
 * @param isutc Is this date in UTC
 * @return A string representation of the date in 12 hour format
 */
function get12HTime(date, isutc)
{

    hours = date.getHours();
    if (isutc)
    {
        hours = date.getUTCHours();
    }
    M = " AM";
    if (hours >= 12)
    {
        if (hours > 12) {
            hours = hours - 12;
        }

        M = " PM";
    }
    hours = pad(hours, 2, 0);
    toRet = hours + ":" + pad(date.getMinutes(), 2, 0) + ":" + pad(date.getSeconds(), 2, 0) + M;

    if (isutc)
    {
        toRet = hours + ":" + pad(date.getUTCMinutes(), 2, 0) + ":" + pad(date.getUTCSeconds(), 2, 0) + M;
    }
    return toRet;
}

/**
 * Returns a date formatted as dd/mm/yyyy
 * @param date the Date object
 * @param isutc display the date at UTC
 * @return String date representation.
 */
function getTDate(date, isutc, include_year)
{
    var _date = date;
    if (include_year == null)
    {
        include_year = true;
    }

    if(typeof _date == typeof 1)
    {
        _date = new Date();
        _date.setTime(date);
    }

    if (isutc)
    {
        toRet = pad(_date.getUTCDate(), 2, "0") + "/" + pad(_date.getUTCMonth() + 1, 2, "0");
        if (include_year)
            toRet += "/" + _date.getUTCFullYear();
    } else
    {
        toRet = pad(_date.getDate(), 2, "0") + "/" + pad(_date.getMonth() + 1, 2, "0");
        if (include_year)
            toRet += "/" + _date.getFullYear();
    }
    return toRet;
}

function curryTDate(isUTC, include_year){return function(date){return getTDate(date, isUTC, include_year);}}

/**
 * Convert a time string HH:MM:SS X, to seconds since midnight
 * @param t1 time1
 * @return integer seconds from midnight
 */
function timeToSeconds(time) {
    var p = time.split(' ');
    var t = p[0].split(':');
    var offset = 0;
    if (p[1] == 'PM') {
        offset = 12 * 60 * 60;
    }
    return offset + parseInt(t[0] * 60 * 60 + t[1] * 60 + t[2]);
}

function mnthcmp(a, b)
{
    return mnths_rev[a] - mnths_rev[b];
}

function datecmp(a, b) {
    a = a.substring(6, 10) + a.substring(3, 5) + a.substring(0, 2);
    b = b.substring(6, 10) + b.substring(3, 5) + b.substring(0, 2);
    return a.localeCompare(b);
}

/**
 * Function for reversing the order of a date sort.
 * @param a
 * @param b
 * @return
 */
function rev_datecmp(a, b)
{
    return datecmp(b, a);
}

function caculateTimeoneOffset(tz)
{
    jQuery.logf("TimeZone: %s", tz);
    if (tz.substring(0,3) == 'GMT') {
    	tz = tz.substring(3);
    }
    var parts = tz.split(":");
    var minutes = parseInt(parts[1], 10);
    var hours = parts[0].substring(1);
    var sign = parts[0].substring(0, 1) == "+" ? 1 : -1;
    return sign * ((hours * 60 * 60 * 1000) + (minutes * 60 * 1000));
}

/**
 *
 * @param values
 * @param uniqueTimes
 * @param processed
 * @param timezone
 * @param decimals
 * @param times_of_interest defines catchment times (+- 30 minutes)
 * @return
 */
function processFeed(values, uniqueTimes, processed, timezone, decimals, times_of_interest) {
    if (decimals == null) decimals = 0;
    //    jQuery.logf("TimeZone: %s", timezone);
    var offset = caculateTimeoneOffset(timezone);
    //    jQuery.logf("TimeZone Offset: %s", offset);
    var useUTC = true;

    // If we are defining uniqueTimes then initialise them
    if (times_of_interest != null) {
        for (t_ in times_of_interest) {
            uniqueTimes.push(times_of_interest[t_]);
        }
    }

    $.each(values, function() {
        if ("time" in this) {

            //This ensures that all proceeding caculation are carried out fot the timezone
            //that the camera is in, not the timezone the browser is in.
            //            jQuery.logf("Input: %s", this.time);
            var p_time = new Date(this.time);
            
            p_time.setTime(p_time.getTime() + offset);


            d = getTDate(p_time, useUTC);
            t = get12HTime(p_time, useUTC);
            // Time in seconds
            t_s = timeToSeconds(t)

            var val = parseFloat(this.value, 10);
            val = val.toFixed(decimals);


            // Initialise the date bucket
            if (processed[d] == null) {
                processed[d] = new Array();
            }

            if (times_of_interest == null) {
                var found = false;
                for (t_ in uniqueTimes) {
                    if (t == uniqueTimes[t_]) {
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    uniqueTimes.push(t);
                    //                jQuery.logf("New Unique Time: %s",t);
                }
                // Add this value
                processed[d][t] = val;

            } else {
                for (t_ in uniqueTimes) {
                    u_t = uniqueTimes[t_]
                    if (Math.abs(timeToSeconds(u_t) - t_s) < 30 * 60) {
                        if (processed[d][u_t] == null) {
                            processed[d][u_t] = new Array();
                        }
                        processed[d][u_t].push(parseFloat(val));
                    }
                }
            }
        }
    });

    // if we're using time_of_interest then we need to sort out the averages.
    if (times_of_interest != null) {
        for (d in processed) {
            for (t in processed[d]) {
                s = sum(processed[d][t]);
                l = processed[d][t].length
                if (l == 0) {
                    processed[d][t] = 0;
                }
                else
                {
                    processed[d][t] = s / l;
                }
            }
        }
    }

}



function sortUniqueTimes(uniqueTimes) {
    uniqueTimes.sort(function(a, b) {

        //Radix needs to be explicit otherwise leading zeroes cause the string to be parsed as octal.
        var a_h = parseInt(a.substring(0, 2), 10);
        var b_h = parseInt(b.substring(0, 2), 10);

        if (a.substring(a.search(' ') + 1) == 'PM' && a_h != 12) {
            a_h += 12;

        }


        if (b.substring(b.search(' ') + 1) == 'PM' && b_h != 12) {
            b_h += 12;
        }

        return a_h - b_h;
    });
}

function getColours(data)
{
    var d_mean = mean(data);
    var d_std = std_dev(data);
    var toRet = new Array();
    var out_col = "b12e2e";
    var in_col = "ffcc33";
    jQuery.logf("STD: " + d_std);

    for (i in data)
    {
        jQuery.logf(Math.abs(data[i] - d_mean) + " : " + (Math.abs(data[i] - d_mean) > (d_std)));
        if (Math.abs(data[i] - d_mean) > (d_std))
        {
            toRet.push(out_col);
        } else {
            toRet.push(in_col);
        }
    }
    return toRet;
}

/**
 * Produce the graph URL from the components supplied
 * @param x_label
 * @param y_label
 * @param title
 * @param data
 * @param data_labels
 * @param data_colours
 * @param y_max
 * @param column_size
 * @param point_labels
 * @return
 */
function makeBarGraphURL(x_label, y_label, title, data, data_labels, data_colours, y_max, column_size, point_labels, dim, y_label_pos, additional)
{
	if (dim == null) dim = '400x200';
    return 'http://chart.apis.google.com/chart?cht=bvs&chs=' + dim + '&chxt=x,x,y'
            + '&chxl=1:|' + x_label + '|2:|' + y_label + '|&chxp=1,0,50,100'
            + '|2,' + (y_label_pos == null ? "0,50,100":y_label_pos)
            + '&chbh=' + column_size
            + '&chd=t:' + data.join(",")
            + (title == null ? "" :'&chtt=' + title)
            + '&chl=' + data_labels.join("|")
            + (point_labels ? '&chm=' + point_labels : '')
            + '&chds=0,' + y_max
            + (data_colours == null ? "" : '&chco=' + data_colours.join("|"))
            + (additional == null ? "" : additional);
}

function makeLineGraphURL(x_label, y_label, title, data, data_labels, data_colours, y_max, point_labels, x_min, x_max)
{
    return 'http://chart.apis.google.com/chart?cht=lxy&chs=400x200&chxt=x,x,y'
            + '&chxl=1:|' + x_label + '|2:|0|' + y_label + '|' + y_max + '|&chxp=1,50|2,0,50,100'
            + '&chd=t:' + data
            + '&chtt=' + title + '&chl=' + data_labels
            + (point_labels ? '&chm=' + point_labels : '')
            + '&chds=' + x_min + ',' + x_max + ',0,' + y_max + ',' + x_min + ',' + x_max + ',0,' + y_max + ',' + x_min + ',' + x_max + ',0,' + y_max
            + (data_colours == null ? "" : '&chco=' + data_colours);
}

function makeLineSeriesGraphURL(x_label, y_label, title, data, data_labels, data_colours, y_max, point_labels, x_min, x_max, dim, y_label_pos)
{
	if (dim == null) dim = '400x200';
    return 'http://chart.apis.google.com/chart?cht=lc&chs=' + dim + '&chxt=x,x,y'
            + '&chxl=1:|' + x_label + '|2:|' + y_label + '|&chxp=1,0,50,100'
            + '|2,' + (y_label_pos == null ? "0,50,100":y_label_pos)
            + '&chd=t:' + data.join(",")
            + '&chtt=' + title + '&chl=' + data_labels.join("|")
            + (point_labels ? '&chm=' + point_labels : '')
            + '&chds=0,' + y_max
            + (data_colours == null ? "" : '&chco=' + data_colours.join("|"));
}

function getWeek(date) {
    //From:
    //http://javascript.about.com/library/blweekyear.htm
    var onejan = new Date(date.getFullYear(), 0, 1);
    return Math.ceil((((date - onejan) / 86400000) + onejan.getDay() + 1) / 7);
}

/**
 * Calculate the average of the object indexed array.
 * @param d
 * @return
 */
function calculateAverage(d) {
    // Calculate an average for each day.
    count = 0;
    day_total = 0;
    for (t_ in d)
    {
        count++;
        day_total += d[t_];
    }
    // If there was no data then set to 0 to avoid NaN
    if (count == 0) {
        day_total = 0;
    }
    else
    {
        day_total = day_total / count;
    }
    return day_total;
}


function cleanLabels(graphLabel, label_count)
{
    var modu = Math.floor(graphLabel.length / label_count);
    for (var i = 0; i < graphLabel.length; i++)
    {
        if ((i % modu) != 0)
        {
            graphLabel[graphLabel.length - (i+1)] = "";
        }
    }
}

function makeYearlyData(processed, uniqueTimes, place)
{
    var yearlydata = new Array();
    var yearlylabel = new Array();
    var graphData = new Array();
    var graphLabel = new Array();
    var mnths = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
    var count = 0;
    var parts, year, week, l_week, t_date, d_week;
    var order = keys(processed);

    order.sort(rev_datecmp);

    for (x in order)
    {
        parts = order[x].split("/");
        year = parts[parts.length - 1];
        if (!yearlydata[year])
        {
            yearlydata[year] = new Array();
            yearlylabel[year] = new Array();
        }
        week = getWeek(new Date(parseInt(parts[2], 10), parseInt(parts[1] - 1, 10), parseInt(parts[0], 10)));
        if (!yearlydata[year][week])
        {
            yearlydata[year][week] = new Array();
            yearlylabel[year][week] = new Array();
        }
        yearlydata[year][week].push(processed[order[x]]);
        yearlylabel[year][week].push(mnths[parseInt(parts[1] - 1, 10)]);
    }

    //merge the fragments of a week that exists at the end and beginning of a year;
    order = keys(yearlydata);
    order.sort(datecmp);
    if (order.length > 1)
    {
        for (var i = 1; i < order.length; i++)
        {
            parts = order[i].split("/");
            t_date = new Date(parseInt(parts[2], 10), 0, 1);
            //Only if the first of jan is not day 0.
            if (yearlydata[order[i - 1]].length >= 53 && t_date.getDay() != 0)
            {
                //                yearlydata[order[i-1]][yearlydata[order[i-1]].length-1] = yearlydata[order[i-1]][yearlydata[order[i-1]].length-1].concat(yearlydata[order[i]][1]);
                //                yearlydata[order[i]].shift();
            }
        }
    }
    order.sort(datecmp);
    for (var x in order)
    {
        year = order[x];
        //starts at one because the the first week from getWeek in 1 not 0.
        for (i = 1; i < yearlydata[year].length; i++)
        {
            // The data for the ith week of the year
            d_week = yearlydata[year][i];
            // The labels for this data.
            l_week = yearlylabel[year][i];
            graphData[count] = 0;
            for (var dp in d_week)
            {
                // Get the sum of the daily averages
                graphData[count] += calculateAverage(d_week[dp]);
            }
            // Now take the weekly averages from this
            if (d_week != null && d_week.length != 0) {
                graphData[count] = graphData[count] / d_week.length;
            }
            graphLabel[count] = a_max(l_week, mnthcmp);
            count++;
        }
    }

    cleanLabels(graphLabel, 11);

    graphData = graphData.slice(graphData.length - 52);
    graphLabel = graphLabel.slice(graphLabel.length - 52);

    return makeBarGraphURL("Months", "Count", place + "+Yearly", graphData, graphLabel, null, a_max(graphData),
            "a", "N*f0*,000000,0,-1,11");
}

function makeDailyBar(processed, uniqueTimes, place)
{
    var intra_daily_data = new Array();
    var intra_daily_label = new Array();
    var order = keys(processed);
    order.sort(rev_datecmp);

    var uniqueTimesRev = uniqueTimes.slice().reverse();
    var max = 0;
    var d;
    for (var i = 0; i < 2; i++) {
        d = order[i];
        $.each(uniqueTimesRev, function(i, t) {
            if (processed[d][t] == null) {
                today_val = 0;
            } else {
                today_val = processed[d][t];
            }
            if (today_val > max) {
                max = today_val
            }
            intra_daily_data.push(today_val);
            intra_daily_label.push(t.substring(0, t.search(':')) + t.substring(t.search(' ')));
        });
    }

    var x_label;
    if (order.length > 0) {
        x_label = order[1] + " Day " + order[0];
    } else {
        x_label = " Day " + order[0];
    }
    return makeBarGraphURL(x_label, "Count", place + "+48+Hours", intra_daily_data.reverse(),
            intra_daily_label.reverse(), getColours(intra_daily_data), max, "20,20,10", "N*f0*,000000,0,-1,11");

}

/**
 * Construct a dataset suitable for plotting on a daily bar graph, and return a graph url.
 *
 * @param processed the data set
 * @param place the location object
 * @param days number of days to include in the dataset
 * @param y_label_count
 * @param chbh
 * @param chm
 * @return
 */
function makeDailyData(processed, place, days, y_label_count, chbh, chm)
{
    var daily_data = new Array();
    var daily_label = new Array();
    var order = keys(processed);
    order.sort(rev_datecmp);
    var d;
    var max = 0;
    var day_total;
    if (days > order.length) {
        days = order.length;
    }
    if (y_label_count > days)
    {
        y_label_count = days;
    }
    if (!chbh)
    {
        chbh = "a";
    }

    for (var i = 0; i < days; i++)
    {
        d = order[i];

        day_total = calculateAverage(processed[d]);
        // Record the max value for scaling
        if (day_total > max) {
            max = day_total;
        }
        daily_data.push(day_total);
        daily_label.push(d.substring(0, 5));
    }
//    var modu = Math.floor(daily_label.length / y_label_count);
//    for (var i = 0; i < daily_label.length; i++)
//    {
//        if ((i % modu) != 0)
//        {
//            daily_label[i] = "";
//        }
//    }
    cleanLabels(daily_label, y_label_count)
    return makeBarGraphURL("Day", "Count", place + "+" + days + "+Days",
            daily_data.reverse(), daily_label.reverse(), getColours(daily_data), max, chbh, chm);
}

function makeWeekleyBar(processed, uniqueTimes, place)
{
    return makeDailyData(processed, place, 7, 7, "20,20,10", "N*f0*,000000,0,-1,11");
}

function makeMonthlyBar(processed, uniqueTimes, place)
{
    return makeDailyData(processed, place, 28, 5, "10,2,10", "N*f0*,000000,0,-1,11");
}

function makeYearlyBar(processed, uniqueTimes, place)
{
    return makeYearlyData(processed, uniqueTimes, place);
}

var bar_grapher = {"daily":makeDailyBar, "weekly":makeWeekleyBar, "monthly":makeMonthlyBar,"yearly":makeYearlyBar};

/**
 * The entry point method for constructing a graph of the data stream.
 *
 * @param period The type of graph which is being produced
 * @param data The data structure for the data set
 * @param entity The location entity
 * @param times_of_interest an array containing the 12 hour times which we are interested in.
 * @return
 */
function constructGoogleGraph(period, data, entity, times_of_interest) {
    if (!period in bar_grapher) {
        return "blank.html";
    }
    var values = data['points'];


    jQuery.logf("Entity Name: %s", entity.name);
    var place = entity.name;

    //The timezone of the data/camera.
    var timezone = entity.timezone;
    //var timezone = "+10:00";

    // Organise the data
    var processed = new Array();
    var uniqueTimes = new Array();

    processFeed(values, uniqueTimes, processed, timezone, 0, times_of_interest);

    // Sort the times.
    sortUniqueTimes(uniqueTimes);

    return bar_grapher[period](processed, uniqueTimes, place);

}

function align_boundry_to_end_of_week(now)
{
    var d = new Date();
    d.setTime(now);
    d.setUTCHours(0, 0, 0, 0);
    return d.getTime() + ((7 - d.getDay()) * TimeSize.day);
}

function align_boundry_to_end_of_day(now)
{
    var d = new Date();
    d.setTime(now);
    d.setUTCHours(24, 0, 0, 0);
    return d.getTime();
}

function createHashFunction(now, bucketsize_ms)
{
    return function(to_hash)
    {
        to_hash.__hash_code = Math.floor((to_hash._time - now) / bucketsize_ms);
        return to_hash.__hash_code;
    }
}

function createRangeFilter(min, max)
{
    return function (to_filter) {
        return !(to_filter <= max && to_filter >= min);
    }
}

function createRangeReMapper(func, min)
{
    return function (to_hash)
    {
        return func(to_hash) - min;
    }
}

function getMonthLabel(val)
{
    var d = new Date();
    d.setTime(val);
    return mnths[d.getUTCMonth()];
}

var line_grapher = {"now":function(period, noaa, cam, place, timezone)
{
    var x_tmp = new Array();
    var y_tmp = new Array();
    var graph_label = new Array();
    var graph_data = new Array();
    var cmp = date_sort_closure(timezone);
    var cam_data = cam.points;
    cam_data.sort(cmp);
    noaa.points.sort(cmp);
    var now = cam_data[cam_data.length - 1]._time;
    var max = 0;

    var tl_min = -6;
    var tl_max = 40;

    cam_data = processWaveData(cam_data, now, tl_min, tl_max, timezone);
    jQuery.logf("Cam Data: %s", inspect(cam_data));

    var noaa_data = processWaveData(noaa.points, now, tl_min, tl_max, timezone);

    for (var x in cam_data)
    {
        cam_data[x].value = cam_data[x].value.toFixed(1);
        x_tmp.push(x);
        y_tmp.push(cam_data[x].value)
        if (cam_data[x].value > max)
        {
            max = cam_data[x].value;
        }
    }
    graph_data.push(x_tmp.join(","));
    graph_data.push(y_tmp.join(","));

    x_tmp = new Array();
    y_tmp = new Array();

    for (var x in noaa_data)
    {
        noaa_data[x].value = noaa_data[x].value.toFixed(1);
        x_tmp.push(x);
        y_tmp.push(noaa_data[x].value)
        if (noaa_data[x].value > max)
        {
            max = noaa_data[x].value;
        }
    }
    graph_data.push(x_tmp.join(","));
    graph_data.push(y_tmp.join(","));

    graph_data.push([0 - tl_min, 0 - tl_min].join(","));
    graph_data.push("0," + max);

    for (var i = 0; i < (tl_max - tl_min); i += 6)
    {
        graph_label[i] = i + tl_min;
    }

    graph_label[0 - tl_min] = "Now"

    jQuery.logf("Cam Data: %s", graph_data.join("|"));
    return makeLineGraphURL("Hours", "Height (m)", place + "+Wave Height Information", graph_data.join("|"),
            graph_label.join("|"), [Colours.cam, Colours.noaa, Colours.now].join(","), max, null, 0, 46);


}, "week":function(period, noaa, cam, place, timezone)
{
    var x_tmp = new Array();
    var y_tmp = new Array();
    var graph_label = new Array();
    var graph_data = new Array();
    var cmp = date_sort_closure(timezone);
    var cam_data = cam.points;
    cam_data.sort(cmp);
    var now = cam_data[cam_data.length - 1]._time;
    var max = 0;

    var tl_min = -7 * 24;
    var tl_max = 0;

    cam_data = processWaveData(cam_data, now, tl_min, tl_max, timezone);

    var last_date = null;
    for (var x in cam_data)
    {
        var this_date = getTDate(new Date(cam_data[x]._time), false, false);
        if (last_date == null || last_date != this_date)
        {
            graph_label[x] = this_date;
        }
        else
        {
            graph_label[x] = '';
        }
        last_date = this_date;

        cam_data[x].value = cam_data[x].value.toFixed(1);

        x_tmp.push(x);
        y_tmp.push(cam_data[x].value)
        if (cam_data[x].value > max)
        {
            max = cam_data[x].value;
        }
    }
    jQuery.logf("Max value: %s", max);
    graph_data.push(x_tmp.join(","));
    graph_data.push(y_tmp.join(","));

    jQuery.logf("Cam Data: %s", graph_data.join("|"));
    return makeLineGraphURL("Days", "Height", place + "+Wave Height Information", graph_data.join("|"),
            graph_label.join("|"), [Colours.cam, Colours.noaa, Colours.now].join(","), max, null, 0, 168);

}, "month":function(period, noaa, cam, place, timezone)
{
    cam.points.sort(date_sort_closure(timezone));
    var now = new Date();
    now.setTime(cam.points[cam.points.length - 1]._time);

    var cam_data =
            hash(
                    cam.points,
                /*createRangeReMapper is needed because if array indicies are negitive, they are converted to a string and
                 becomes the property key. Hence length is not incremented.*/
                    createRangeReMapper(createHashFunction(align_boundry_to_end_of_day(now.getTime()), TimeSize.day), -30),
                    createRangeFilter(0, 29), false
                    );
    var graph_data = cam_data.map(curryAgg(mean, function(a) { return a.value; }));
    var graph_label = cam_data.map(curryAgg(function(data, cmp) { return a_max(data, cmp)._time; },
            function(a, b) { return a._time - b._time }));
    graph_label = graph_label.map(curryTDate(true, false));
    cleanLabels(graph_label, 7);
    
    // These are raw arrays, not associative arrays like graph_data
    graph_keys = keys(graph_data);
    graph_values = values_for_keys(graph_data, graph_keys);
    
    return makeLineGraphURL("Days", "Height", place + "+Monthly Wave Height Information",
            [graph_keys.join(","), graph_values.join(",")].join("|"),
            graph_label.join("|"), [Colours.cam, Colours.noaa, Colours.now].join(","), Math.ceil(a_max(graph_values)), null, 0, 29);

},"year":function(period, noaa, cam, place, timezone)
{
    cam.points.sort(date_sort_closure(timezone));
    var now = new Date();
    now.setTime(cam.points[cam.points.length - 1]._time);

    var cam_data =
            hash(
                    cam.points,
                /*createRangeReMapper is needed because if array indicies are negitive, they are converted to a string and
                 becomes the property key. Hence length is not incremented.*/
                    createRangeReMapper(createHashFunction(align_boundry_to_end_of_week(now.getTime()), TimeSize.week), -52),
                    createRangeFilter(0, 51), false
                    );
    var graph_data = cam_data.map(curryAgg(mean, function(a) { return a.value; }));
    var graph_label = cam_data.map(curryAgg(function(data, cmp) { return a_max(data, cmp)._time; },
            function(a, b) { return a._time - b._time }));
    graph_label = graph_label.map(getMonthLabel);
    cleanLabels(graph_label, 11);
    
    // These are raw arrays, not associative arrays like graph_data
    graph_keys = keys(graph_data);
    graph_values = values_for_keys(graph_data, graph_keys);

    return makeLineGraphURL("Months", "Height", place + "+ Yearly Wave Height Information",
            [graph_keys.join(","), graph_values.join(",")].join("|"),
            graph_label.join("|"), [Colours.cam, Colours.noaa, Colours.now].join(","), Math.ceil(a_max(graph_values)), null, 0, 51);
}};

function hourDiff(d1, d2)
{
    var hr = 60 * 60 * 1000;
    return Math.floor((d1 - d2) / hr);
}

function processWaveData(data, now, min, max, timezone)
{
        var toRet = new Array();
        var diff;
        for(x in data)
        {
            diff = hourDiff(data[x]._time, now)
            if(diff <= max && diff >= min)
            {
                data[x].label= diff;
                toRet[diff-min] = data[x];
            }
        }
        return toRet;
    jQuery.logf("II BucketSize: %s", TimeSize.hour);
//    return hash(data, createRangeReMapper(createHashFunction(now, TimeSize.hour), min), createRangeFilter(min, max), true);

}

function date_sort_closure(timezone)
{
    jQuery.logf("DTimeZone: %s", timezone);
    var offset = caculateTimeoneOffset(timezone);

    function setTime(a)
    {
        var e_time;
        if (!("_time" in a))
        {
            e_time = new Date(a.time);
            e_time.setTime(e_time.getTime() + offset);
            a["_time"] = e_time.getTime();

        }
    }

    function sort(a, b)
    {
        setTime(a);
        setTime(b);
        return a._time - b._time;
    }

    return sort;
}


function constructWaveHeightGraph(period, noaa, cam, entity)
{
    var place = entity.name;
    jQuery.logf("===================================================================================================");

    //The timezone of the data/camera.
    var timezone = entity.timezone;
    
    //var timezone = "+10:00";


    return line_grapher[period](period, noaa, cam, place, timezone);
}

function translateTimezone(d, timezone) {
	var offset = caculateTimeoneOffset(timezone);
	var p_time = new Date(d);
	p_time.setTime(p_time.getTime() + offset);
	return p_time;
}

function toUTCString(d, timezone) {
	var offset = caculateTimeoneOffset(timezone);
	var p_time = new Date(d);
	p_time.setTime(p_time.getTime() - offset);
	return p_time.toString("ddd, dd MMM yyyy HH:mm:ss UTC");
}

/**
 * Convert a metre value to (surfer) feet.
 * @param val
 * @return
 */
function mToFeet(val, loc_id) {
	switch (loc_id) {
	case '3': // Snapper
		if (val < 1) {
			val = val / 2.4;
		} else if (val < 1.4) {
			val = val / 2.2;
		} else if (val < 1.5) {
			val = val / 2;
		} else if (val < 1.7) {
			val = val / 1.8;
		} else if (val < 2) {
			val = val / 1.7;
		} else if (val < 2.2) {
			val = val / 1.6;
		} else if (val < 2.5) {
			val = val / 1.5;
		} else {
			val = val / 1.5;
		}
	break;
	case '5': // Bondi
		if (val < 1) {
			val = val / 2;
		} else if (val < 1.5) {
		val = val / 1.8;
		} else if (val < 2) {
			val = val / 1.6;
		} else if (val < 2.5) {
			val = val / 1.5;
		} else {
			val = val / 1.5;
		}
	break
	default:
		if (val < 1) {
			val = val / 2;
		} else if (val < 1.5) {
		val = val / 1.8;
		} else if (val < 2) {
			val = val / 1.6;
		} else if (val < 2.5) {
			val = val / 1.5;
		} else {
			val = val / 1.5;
		}
	break
	
	}
	val = val * 3.3;
	val = val.toFixed(1);
	return val;
}


function getWaveHeightMap(loc_id) {
    switch (loc_id) {
    case 1:
        return 'maps/1200_WH.jpg';
    case 2:
        return 'maps/1700_WH.jpg';
    case 3:
        return 'maps/1950_WH.jpg';
    case 4:
        return 'maps/1500_WH.jpg';
    case 5:
        return 'maps/3800_WH.jpg';
    case 5155:
        return 'maps/1900_WH.jpg';
    case 516329:
        return 'maps/10220_WH.jpg';
    case 561391:
        return 'maps/13100_WH.jpg';
    case 588287:
        return 'maps/15110_WH.jpg';
    default:
    	return 'maps/NO_MAP.jpg';
    }
}

function getSiteName(loc_id) {
    switch (loc_id) {
    case 1:
        return 'Artificial Reef';
    case 2:
        return 'Kirra';
    case 3:
        return 'Snapper Rocks';
    case 4:
        return 'Burleigh Heads';
    case 5:
        return 'Bondi';
    case 5155:
        return 'Rainbow Bay';
	case 516329:
		return 'Sandys';
    }
}

