/*!
	\file NEO.js
	\brief Contains commonly-used JavaScript functions for the entire NEO site.
	
	Functions defined in this file:
	-------------------------------
	Switch_Preview(src, newdate, mark, scene_id)

	OpenWindow(url, opener, width, height)
	CenterWindow(w)
	OpenHelp(topic, opener)
	
	get_element(id)
	get_form(id)
	show_element(element_ref)
	hide_element(element_ref)
	
	Highlight(element_id)
	Restore(element_id)
	Mark(element_id)
	
	PreloadImages()
	SwapImage(obj, newsrc)
	SwapImageRestore()
*/

/************************************************************************/
/*********************** Global Variables *******************************/
/************************************************************************/

var scene_id = null; // ID of the scene that is currently being viewed in the map widget
var image_id = null; // ID of the dataset id that is currently being viewed in the map widget
var image_height = null;
var image_width = null;
var image_resolution = null;
var constrain = true; // Keeps track of whether or not the user wants to constrain the downloaded image to the widget's aspect ratio
var widget_form_name = "mapForm"; // The name (id) of the form containing the map widget and controls
var timeout_id = null;	// id set by the most recent call to setTimeOut() (see functions: ToggleSelector, ShowSelector, HideSelector)
var DELAY = 1000; // Standard delay (in milliseconds) time used in calls to setTimeOut
var default_color_space = "rgb";

var currentStartDate = null;
var currentEndDate = null;

// Decimal degree resolutions for use when displaying the resize options.
// These values must stay in ascending order.
var resolutions = [
	{"value": "0.1",  "radians": "0.0017453292519943296"},
	{"value": "0.25", "radians": "0.004363323129985824"},
	{"value": "0.5",  "radians": "0.008726646259971648"},
	{"value": "1.0",  "radians": "0.017453292519943295"}
];

/************************************************************************/
/**************** Index-page Specific Functions *************************/
/************************************************************************/

/*
	\brief Hides all dataset group lists (i.e. "Land Datasets")
	\returns nothing
*/
function hide_all()
{
	for(var i=0; i<groups.length; i++)
	{
		var title = get_element(groups[i] + "_title");
		var list = get_element(groups[i] + "_list");
		var img = get_element("g_" + groups[i].toLowerCase());
		
		hide_element(title);
		hide_element(list);
		img.src = img.prev_src = "images/dataset_groups/" + groups[i].toLowerCase() + "_1.gif";
	}
}

/*!
	\brief Toggles the display of a dataset group list (on or off)
	\param group_name - string id of the surrounding div element to hide
	\returns nothing
*/
function toggle_group(group_name)
{
	hide_all();
	
	if(group_name != visible_group)	{
		var spacer = get_element('spacer');
		var title = get_element(group_name + "_title");
		var list  = get_element(group_name + "_list");
		var img = get_element("g_" + group_name.toLowerCase());

		if(group_name != "all")
		{
			show_element(title);
			show_element(list);
			img.src = img.prev_src = "images/dataset_groups/" + group_name.toLowerCase() + "_2.gif";
		}
		
		visible_group = group_name;
	} else {
		visible_group = "";
	}
}

/*!
	\brief Returns the 'Daily  8-Day  Monthly' selector for a dataset in the dataset groups list
	\param dataset_id - id of the dataset to get the selector for
	\returns HTMLElement reference
	
	Selector elements should have the following naming convention:
		[DATASET ID]_selector
	
	i.e.:
		MY1DMD_NLW_551_selector
*/
function GetSelector(dataset_id)
{
	var element_name = dataset_id.toString() + "_selector";
	var selector = get_element(element_name);
	return selector;
}

/*!
	\brief Hides the 'Daily  8-Day  Monthly' selector for a dataset in the dataset groups list
	\param dataset_id - the id of the dataset that the selector is for
	\param delay - (boolean) when true, this function will use a setTimeout call to re-call this function two seconds later
	\returns nothing
*/
function HideSelector(dataset_id, delay)
{
	// If delay is provided (and true), set a timeout to call this function again in 2 seconds
	if(arguments.length > 1 && delay)
	{
		// Pass in false for the delay parameter so we don't get an infinite-loop
		timeout_id = setTimeout("HideSelector('" + dataset_id + "', false)", DELAY);
		return;
	}
	
	var selector = GetSelector(dataset_id);
	selector.innerHTML = "";
}

/*!
	\brief Shows the 'Daily  8-Day  Monthly' selector for a dataset in the dataset groups list
	\param dataset_id - the id of the dataset that the selector is for
	\param delay - (boolean) when true, this function will use a setTimeout call to re-call this function two seconds later
	\returns nothing
*/
function ShowSelector(dataset_id, delay)
{
	// If delay is provided (and true), set a timeout to call this function again in 2 seconds
	if(arguments.length > 1 && delay)
	{
		// Pass in false for the delay parameter so we don't get an infinite-loop
		timeout_id = setTimeout("ShowSelector('" + dataset_id + "', false)", DELAY);
		return;
	}

	var selector = GetSelector(dataset_id);
	
	var daily_link = "<a class='fakeref' onclick='something();'>Daily</a>";
	var eightday_link = "<a class='fakeref' onclick='something();'>8-Day</a>";
	var monthly_link = "<a class='fakeref' onclick='something();'>Monthly</a>";
	
	selector.innerHTML = "&rarr; " + daily_link + "&nbsp;&nbsp;" + eightday_link + "&nbsp;&nbsp;" + monthly_link;
}

/*!
	\brief Toggles the 'Daily  8-Day  Monthly' selector for a dataset in the dataset groups list; shows it if it's hidden, hides it if it's shown
	\param dataset_id - the id of the dataset that the selector is for
	\param delay - (boolean) when true, this function will use a setTimeout call to re-call this function two seconds later
	\returns nothing
*/
function ToggleSelector(dataset_id, delay)
{
	// If delay is provided (and true), set a timeout to call this function again in 2 seconds
	if(arguments.length > 1 && delay)
	{
		// Pass in false for the delay parameter so we don't get an infinite-loop
		timeout_id = setTimeout("ToggleSelector('" + dataset_id + "', false)", DELAY);
		return;
	}
	
	var selector = GetSelector(dataset_id);
	
	// Show it if it's hidden (and don't delay it)
	if(selector.innerHTML == "")
		ShowSelector(dataset_id, false);
	else // Hide it if it's shown (don't delay it, either)
		HideSelector(dataset_id, false);
}


/*!
	\brief Changes the form's inputs to accomodate local or regional parameters
	\returns nothing
*/
function ChangeCoverage() {
	var f = get_form(widget_form_name);
	var c = f.coverage.options[f.coverage.selectedIndex].value;
	
	if(c.toLowerCase() == "regional")
		show_element(get_element("latlon"));
	else
		hide_element(get_element("latlon"));
	
	PopulateDuration();
}

/*!
	\brief Changes the controls in the 'Download Options' group to accomodate full-sized or subsetting
	\returns nothing
*/
function ChangeImageControls()
{
	var f = get_form(widget_form_name);
	var s = f.size.options[f.size.selectedIndex].value;
	
	hide_element("subset_controls");
	hide_element("resize_controls");
	
	if(s.toLowerCase() == "subset") {
		show_element("subset_controls");
	} else if(s.toLowerCase() == "resized") {
		show_element("resize_controls");
		ListResolutions();
		// populate the dimension fields with the full resolution
		f.h.value = image_height;
		f.w.value = image_width;
	}
}

function ListResolutions() {
	var divRef = get_element("resolutions");
	var listHTML = "";
	for (var i = 0; i < resolutions.length; i++) {
		if (image_resolution <= resolutions[i].radians) {
			listHTML += '<input type="radio" name="stepDown" onchange="SetResize('+resolutions[i].radians+')" value="'+resolutions[i].value+'"/>'+resolutions[i].value;
		}
	}
	divRef.innerHTML = listHTML;
}

function SetResize(new_resolution) {
	var f = get_form(widget_form_name);
	var multiplier = (image_resolution / new_resolution);
	f.h.value = Math.ceil(multiplier * image_height);
	f.w.value = Math.ceil(multiplier * image_width);
}

/*!
	\brief Populates the Duration drop-down based on the selected coverage
	\returns nothing
	
	In order for this function to work, you NEED to define 2 arrays before calling this function:
		Global_List
		Regional_List
	
	They should each be an array of strings that will become the new value/text in the Duration drop-down
*/
function PopulateDuration() {
	var f = document.forms[0];
	var sel = f.duration;
	var selIndex = f.duration.selectedIndex;
	var coverageValue = f.coverage.value;
	
	if(coverageValue == "global") {
		var list = Global_List;
	} else if(coverageValue == "regional") {
		var list = Regional_List;
	} else {
		return;
	}
	
	// Clear all the options in the drop-down
	sel.length = 0;
	
	// populate the drop-down, but 'select' if something was selected
	for(var i=0; i<list.length; i++) {
		if (selIndex == i) {
			sel.options[i] = new Option(list[i], list[i], false, true);
		} else {
			sel.options[i] = new Option(list[i], list[i]);
		}
	}
}

/*!
	\brief Resets the form, clearing all input values
*/
function Reset()
{
	var f = get_form(widget_form_name);
	f.reset();
	hide_element(get_element('latlon'));
	hide_element(get_element('subset_controls'));
}

/*!
	\brief Submits the main search form
	\returns nothing
*/
function Submit()
{
	var f = get_form(widget_form_name);
	f.submit();
}

/*!
	\brief Sets several variables local to the currently viewed scene
	\param imgObj - object containing the representation of the scene
	\displayDate - date range of the scene being previewed (goes under the dataset name under the widget)
	\sceneId - id of the scene being previewed
	\imageId - id of the image being previewed
	\resultDivId - id of an element to mark (see Mark function below)
	\startDate - scene beginning date (in "YYYY-MM-DD" format)
	\endDate - scene end date (in "YYYY-MM-DD" format)
	\resolutionRadians - resolution of the image in radians
	\imageHeight - the full height of the source image
	\imageWidth - the full width of the source image
*/
function Switch_Preview(imgObj) {
	// Do stuff with the widget, but only if it's there
	if(map_widget.SetShapefileURL != null)
	{
		var shapeURL = "/Granules?sceneId=" + imgObj.sceneId;
		map_widget.SetShapefileURL(shapeURL);
		
		// Switch the preview image in the map widget
		var img_url = "/RenderData?si=" + imgObj.imageId + "&cs=" + default_color_space + "&widthFit=1200&heightFit=600&format=JPEG";
		LoadMapImage(img_url);
	}

	// Put the preview scene's date up underneath the dataset name
	var date = get_element('preview_date');
	date.innerHTML = imgObj.displayDate;
	
	// Switch the granule location image
	var gran_url = "/LocateScene?sceneId=" + imgObj.sceneId;
	var granuleImg = get_element('granule_image');
	if(granuleImg != null)
		granuleImg.src = gran_url;
	
	// Highlight the currently selected date in the search results pane
	Mark(imgObj.resultDivId);
		
	// Make note of the scene and image attributes
	scene_id = imgObj.sceneId;
	image_id = imgObj.imageId;
	image_height = imgObj.imageHeight;
	image_width = imgObj.imageWidth;
	image_resolution = imgObj.resolutionRadians;
	
	// Store these to use later to populate the date select 
	// if a user selects a region in the widget
	currentStartDate = imgObj.startDate.split("-"); // start[0] = year, start[1] = month, start[2] = day
	currentEndDate   = imgObj.endDate.split("-");  // end[0] = year, end[1] = month, end[2] = day
	
	// reset the form to its previous values so that they may be consistent with the current view
	var f = get_form(widget_form_name);
	f.reset();
	
	// change the image controls back to what was originally selected (i.e., reset)
	ChangeImageControls();
	
	// clear the rubber band from the widget
	map_widget.ClearRegion();
}

function ChangeDates() {
	// if the currentStartDate and currentEndDate have value,
	// use those values to populate the date pull-downs
	if (currentStartDate != null && currentEndDate != null) {
		var f = get_form(widget_form_name);
		
		f.startMonth.selectedIndex = eval(currentStartDate[1]);
		f.startDay.selectedIndex = eval(currentStartDate[2]);
		f.endMonth.selectedIndex = eval(currentEndDate[1]);
		f.endDay.selectedIndex = eval(currentEndDate[2]);
		
		for(var i=0; i<f.startYear.length; i++)
			if(f.startYear.options[i].value == currentStartDate[0])
				f.startYear.selectedIndex = i;
				
		for(var i=0; i<f.endYear.length; i++)
			if(f.endYear.options[i].value == currentEndDate[0])
				f.endYear.selectedIndex = i;
	}
}

/************************************************************************/
/************** Bastardised Data Browser Functions **********************/
/************************************************************************/

/*!
	\brief Opens up a window where the user will download the image currently displayed in the map widget
	\returns nothing
*/
function DownloadImage()
{
	var f = get_form(widget_form_name);
	
	var size = f.size.value;
	var pal = f.palette.value;
	var fmt = f.format.value;
	
	// a few defaults
	var dimensionParams = "";
	var subsetParams = "";
	var window_width = 800;
	
	if(size == "full") {
		// just uses the defaults
	} else if(size == "resized") {
		var width = parseInt(f.w.value);
		var height = parseInt(f.h.value);
		dimensionParams = "&width=" + width + "&height=" + height;
		
		window_width = (width < 790) ? width : 800;
		
	} else if(size == "subset") {
		var northLat = parseInt(f.ssn.value);
		var southLat = parseInt(f.sss.value);
		var westLon = parseInt(f.ssw.value);
		var eastLon = parseInt(f.sse.value);
		subsetParams = "&bbn=" + northLat + "&bbs=" + southLat + "&bbw=" + westLon + "&bbe=" + eastLon;
	}

	var download_url = "/RenderData?si=" + image_id + "&cs=" + pal + "&format=" + fmt + dimensionParams + subsetParams;
	
	var window_height = window_width / widget_aspect_ratio;
	
	OpenWindow(download_url, null, window_width+20, window_height+20);
	
}


/*!
	\brief Toggles the constraint lock for the downloaded image's dimensions
	\returns nothing
*/
function ToggleLock()
{
	var img = get_element('lock');
	img.src = (constrain) ? 'images/icons/lock_open.gif' : 'images/icons/lock_closed.gif';
	constrain = !constrain;
	
	if(constrain)
		SetDownloadHeight();
}

/*!
	\brief Calculates the downloaded image's height based on the widget's aspect ratio and the user-defined width of the image
	\returns nothing
*/
function SetDownloadHeight() {
	var f = get_form(widget_form_name);
	if (f.w.value > image_width) {
		ShowResizeError("resize width cannot exceed maximum image width: "+image_width);
		f.w.value = image_width;
	} else if(constrain) {
		f.h.value = Math.round(f.w.value / widget_aspect_ratio);
		HideResizeError();
	}
}

/*!
	\brief Calculates the downloaded image's width based on the widget's aspect ratio and the user-defined height of the image
	\returns nothing
*/
function SetDownloadWidth() {
	var f = get_form(widget_form_name);
	if (f.h.value > image_height) {
		ShowResizeError("resize height cannot exceed maximum image height: "+image_height);
		f.h.value = image_height;
	} else if(constrain) {
		f.w.value = Math.round(f.h.value * widget_aspect_ratio);
		HideResizeError();
	}
}
function ShowResizeError(errorMessage) {
	var divRef = get_element("resize_error");
	divRef.innerHTML = errorMessage;
}
function HideResizeError() {
	var divRef = get_element("resize_error");
	divRef.innerHTML = "";
}

/************************************************************************/
/******************* Window Opener Functions ****************************/
/************************************************************************/

/*!
	\brief Opens a popup window.
	\param url - the url the window should load
	\param width - the width of the window
	\param height - the height of the window
	\param opener - reference to the window that is doing the opening
	\returns Window object
*/
function OpenWindowMe(url,opener,width,height,name)
{
	if(width == null)
		width = 400;
	if(height == null)
		height = 300;
	if(name == null)
		name = "awindow";
		
	// Center this window using the parent window as a guide
	var screenX = window.screenX + (window.outerWidth/2) - (width/2);
	var screenY = window.screenY + (window.outerHeight/2) - (height/2);

	var w = window.open(url,name,'height='+height+',width='+width+',menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no,screenX='+screenX+',screenY='+screenY+'left='+screenX+',top='+screenY);
	w.opener = opener;
	w.onload = function() { CenterWindow(w); }
	w.focus();
	
	return w;
}

/*!
	\brief Opens a popup window.
	\param url - the url the window should load
	\param width - the width of the window
	\param height - the height of the window
	\param opener - reference to the window that is doing the opening
	\param name - the name the window shall use
	\returns Window object
*/
function OpenWindow(url,opener,width,height,name)
{
	if(width == null)
		width = 400;
	if(height == null)
		height = 300;
	if(name == null)
		name = "awindow";
		
	// Center this window using the parent window as a guide
	var screenX = window.screenX + (window.outerWidth/2) - (width/2);
	var screenY = window.screenY + (window.outerHeight/2) - (height/2);

	var w = window.open(url,name,'height='+height+',width='+width+',menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no,screenX='+screenX+',screenY='+screenY+'left='+screenX+',top='+screenY);
	w.opener = opener;
	w.onload = function() { CenterWindow(w); }
	w.focus();
	
	return w;
}

/*!
	\brief Moves a (popup) window element to the center of its parent window
	\param w - the window element to center
	\returns nothing
*/
function CenterWindow(w)
{
	// Center this window using the parent window as a guide
	var screenX = window.screenX + (window.outerWidth/2) - (w.outerWidth/2);
	var screenY = window.screenY + (window.outerHeight/2) - (w.outerHeight/2);
	
	w.moveTo(screenX, screenY);
}

/*!
	\brief Opens a help window
	\param topic - id of the topic to load in the window
	\param opener - reference to the window that's doing the opening
	\returns Window object
*/
function OpenHelp(topic,opener)
{
	return OpenWindow("Help.html?page="+topic, opener, 500, 300);
}

/*!
	\brief Opens a help window
	\param topic - id of the topic to load in the window
	\param opener - reference to the window that's doing the opening
	\returns Window object
*/
function OpenMetadata(dataset_id,opener)
{
	return OpenWindow("AboutDataset.html?datasetId="+dataset_id, opener, 500, 400);
}

/************************************************************************/
/******************** HTML Element Functions ****************************/
/************************************************************************/
/*!
	\brief cross-browser(?) element fetcher - fetches an HTML element
	\param id - the ID of the element to fetch
	\returns HTMLElement object
*/
function get_element(id) {
	if(document.getElementById)
		return document.getElementById(id);
	return document.layers[id];
}

/*!
	\brief Returns a reference to a form in the page with a particular id
	\param id - the id of the form to retrieve
	\returns Form object, or false if the form is not found
*/
function get_form(id) {
	for(var i=0; i<document.forms.length; i++)
		if(document.forms[i].id == id)
			return document.forms[i];
	
	return false;
}

/*!
	\brief Shows an element by setting the 'display' property of its stylesheet to 'block'
	\param element_ref - reference to an HTMLElement
	\returns nothing
*/
function show_element(element_ref) {
	var element = (typeof element_ref == "string") ? get_element(element_ref) : element_ref;
	
	if(document.getElementById)
		element.style.display = 'block';
	else if(document.layers)
		element.display = 'block';
}

/*!
	\brief Hides an element by setting the 'display' property of its stylesheet to 'none'
	\param element_ref - reference to an HTMLElement
	\returns nothing
*/
function hide_element(element_ref) {
	var element = (typeof element_ref == "string") ? get_element(element_ref) : element_ref;

	if(document.getElementById)
		element.style.display = 'none';
	else if(document.layers)
		element.display = 'none';
}

/*
	Return the value of the radio button that is checked.
	Return an empty string if none are checked, or
	there are no radio buttons.
*/
function getRadioCheckedValue(radioObj) {
	if(!radioObj) { return ""; }
	var radioLength = radioObj.length;
	if(radioLength == undefined) {
		if(radioObj.checked) {
			return radioObj.value;
		} else {
			return "";
		}
	}
	for(var i = 0; i < radioLength; i++) {
		if(radioObj[i].checked) {
			return radioObj[i].value;
		}
	}
	return "";
}

/*
	Set a radio button with the given value as being checked
	Do nothing if there are no radio buttons and if the value
	does not exist, all the radio buttons are reset to unchecked.
*/
function setRadioCheckedValue(radioObj, newValue) {
	if(!radioObj) { return; }
	var radioLength = radioObj.length;
	if(radioLength == undefined) {
		radioObj.checked = (radioObj.value == newValue.toString());
		return;
	}
	for(var i = 0; i < radioLength; i++) {
		radioObj[i].checked = false;
		if(radioObj[i].value == newValue.toString()) {
			radioObj[i].checked = true;
		}
	}
}

/************************************************************************/
/********** Functions for marking / highlighting HTML elements **********/
/************************************************************************/

var last_marked = null;		// Reference to an HTML element, the last element that was marked
var highlight_bg = "#EBDECA";	// The color to highlight cells with
var mark_bg = "#DEE4EC";		// The color to mark cells with
var default_bg = "#FFFFFF";		// The color to un-highlight cells with

/*!
	\brief Highlights an HTML element by changing its background color (HTMLElement.style.backgroundColor
	\param element - id of the element to highlight
	\returns nothing
	
	This is usually called as part of a mouseover event
*/
function Highlight(element_id)
{
	var element = get_element(element_id);
	
	if(element.marked == null || element.marked == false)
	{
		element.style.backgroundColor = highlight_bg;
	}
}

/*!
	\brief Un-highlights an HTML element by changing its background color (HTMLElement.style.backgroundColor
	\param element - id of the element to un-highlight
	\returns nothing
	
	This is usually called on a mouseout event
*/
function Restore(element_id)
{
	var element = get_element(element_id);

	if(element.marked == null || element.marked == false)
	{
		element.style.backgroundColor = default_bg;
	}
}

/*!
	\brief Marks an HTML element by changing its background color
	\param element_id - the id of the element to mark
	\returns nothing
	
	This function creates 'last_marked' - a reference to the last HTML element that was marked. If this is
	not null, its original background-color is restored before the new element is marked.
*/
function Mark(element_id)
{
	var element = get_element(element_id);
	
	// Un-marked the last element that was marked
	if(last_marked != null)
	{
		last_marked.marked = false;
		last_marked.style.backgroundColor = default_bg;
	}
	
	// Mark the current div
	if(element.marked == null || element.marked == false)
	{
		element.marked = true;
		element.orig_bg = element.style.backgroundColor;
		element.style.backgroundColor = mark_bg;
	}
	else
	{
		element.marked = false;
		element.style.backgroundColor = highlight_bg;
	}
	
	// Make note that this was the last element that was markes
	last_marked = element;
}


/************************************************************************/
/******************* Image Swapper Functions ****************************/
/************************************************************************/

/*!
	\brief Image preloader (for swapping images)
	
	This function can accept any number of parameters. Each parameter should be the URL of an image
	to preload (string).
*/
function PreloadImages()
{
	if(document.images)
	{
		if(!document.preloaded_images)
			document.preloaded_images = new Array();
		
		var j = document.preloaded_images.length;
		var args = PreloadImages.arguments;
		
		for(var i=0; i < args.length; i++)
		{
			document.preloaded_images[j] = new Image;
			document.preloaded_images[j++].src = args[i];
		}
	}
}

/*!
	\brief Image swapper function
	\param obj - string or HTMLElement reference of the image object to swap
	\param newsrc - URL of the new image to load
	\returns nothing
*/
function SwapImage(obj, newsrc)
{
	var img = (typeof obj == "string") ? get_element(obj) : obj;
	
	if(!img.prev_src)
		img.prev_src = img.src;
	img.src = newsrc;

	document.swapped_image = img;
}

/*!
	\brief Restores a previously-swapped image to its original SRC
	\returns nothing
*/
function SwapImageRestore()
{
	document.swapped_image.src = document.swapped_image.prev_src;
}

/******************************************************************/
/******************* Utility Functions ****************************/
/******************************************************************/
function FormatFileSize(bytes) {
    var KB_BYTES = 1024;
	var MB_BYTES = KB_BYTES * 1024;
	var GB_BYTES = MB_BYTES * 1024;
	var TB_BYTES = GB_BYTES * 1024;
	var formatted = "";
	// this is a cheap way of getting 2 decimal precision...
	if (bytes >= TB_BYTES) {
		formatted = Math.round((bytes / TB_BYTES)*100/100) + " TB";
    } else if (bytes >= GB_BYTES) {
        formatted = Math.round((bytes / GB_BYTES)*100)/100 + " GB";
    } else if (bytes >= MB_BYTES) {
        formatted = Math.round((bytes / MB_BYTES)*100)/100 + " MB";
    } else if (bytes >= KB_BYTES) {
        formatted = Math.round(bytes / KB_BYTES) + " kb";
    } else {
        formatted = Math.round(bytes) + " bytes";
    }
    return formatted;
}