// ------------------------------------------------
// DataAccessor.js
// Author:	jpotter@troupscreeksoftware.com
// Date:	Oct 27, 2006
// Desc:	Data Accessor Manager
// Copyright 2006 Troups Creek Software LLC
// ------------------------------------------------
// Manages data access for the application.
// -----------------------------------------
// DataAccessor::DataAccessor
function DataAccessor(path)
{
	// data -------------------
	this.filepath = path;
	this.sourceData = null;
	this.currentData = null;
	this.currentListFilename = "";
	this.logFilename = "";
	this.featureData = null;
	this.rssData = null;
	this.rssId = "";
	// -------------------------------------------------------------------------------------------
	//            A C C E S S O R S
	// -------------------------------------------------------------------------------------------
	// Methods that access the application data.

	// -----------------------------------------
	// DataAccessor::getConfig
	this.getConfig = function()
	{	// load the site configuration file (gSiteConfig is spec'ed on the main page)
		this.loadFile(gSiteConfig, this.onConfig, false);
	}

	// -----------------------------------------
	// DataAccessor::getDataPath
	this.getDataPath = function()
	{
		return this.filepath;
	}

	// -----------------------------------------
	// DataAccessor::getItemList
	this.getItemList = function(domain)
	{	// load up the item list
		this.loadFile(domain+"ItemList.xml", this.onItemList, true);
	}

	// -----------------------------------------
	// DataAccessor::getItemXml
	this.getItemXml = function(id)
	{	// load up any auxillary data
		this.rssId = id;
		this.loadFile(id + ".xml", this.onItemXml, true);
	}
	
	// -----------------------------------------
	// DataAccessor::getItemListName
	this.getItemListName = function()
	{	// return the relative path the item list file
		var rp = this.filepath + "/" + gConfig.domainid + "ItemList.xml";
		return rp;
	}

	// -----------------------------------------
	// DataAccessor::getItem
	this.getItem = function(itemId)
	{	// return the data pertaining to the specified item
		var allItems = this.sourceData;
		if(allItems != null)
		{
			var itemList = allItems.getElementsByTagName("item");		
			var itemCount = itemList.length;
			var thisItem, thisItemId;
			// for each item in our list
			for(var i=0; i<itemCount; i++)
			{
				thisItem = itemList[i];
				thisItemId = thisItem.getElementsByTagName("id")[0].firstChild.nodeValue;
				// see if the id matches what we are looking for
				if(itemId == thisItemId)
				{
					return thisItem;
				}
			}
		}
		return null;
	}

	// -----------------------------------------
	// DataAccessor::getMapFeatures
	this.getMapFeatures = function(domain)
	{	// retrieve the map features / corrections list
		this.loadFile(domain+"FeatureList.xml", this.onFeatureList, true);
	}
	
	// -----------------------------------------
	// DataAccessor::createCurrentItemList
	// create an item list that contains only those items that fall within the current map boundries
	this.createCurrentItemList = function(maxLat, maxLng, minLat, minLng)
	{	// create xml doc for our current list
		var xmlData = this.createXmlDoc();
		if(xmlData != null)
		{	// load it here, synchronously!
			xmlData.async = false;
			xmlData.load("_private/EmptyItemList.xml");
		}
		if(xmlData != null)
		{	// ok to proceed - init a bunch 'o stuff
			var markerNode;
			var testLat; var testLng;
			var thisItem; var newNode;
			// look through the source list
			var itemList = this.sourceData.getElementsByTagName("item");
			var itemCount = itemList.length;
			for(var i=0; i < itemCount; i++)
			{	// look at the item's location and if in bounds, add it
				thisItem = itemList[i];
				markerNode = thisItem.getElementsByTagName("marker")[0];
				testLat = parseFloat(markerNode.getElementsByTagName("latitude")[0].firstChild.nodeValue);
				testLng = parseFloat(markerNode.getElementsByTagName("longitude")[0].firstChild.nodeValue);
				if((testLat < maxLat && testLat > minLat && testLng < maxLng && testLng > minLng))
				{	// it is in bounds -- add it to the new list
					newNode = itemList[i].cloneNode(true);
					xmlData.documentElement.appendChild(newNode);
				}
			}
			// what we have in our list is what is currently in bounds (no items is perfectly okay)
			this.currentData = xmlData;
		}	
	}

	// -----------------------------------------
	// DataAccessor::logContact
	this.logContact = function(type, title)
	{	// log the click-thru event if user mode
		if(!gApp.isAdminMode)
		{	// user mode
			if(title == undefined)
			{	// get the title from the current data based on the id of the opened marker ballooon
				if(gApp.openedId != 0)
				{	// ID is valid, go ahead
					var itemData = this.getItem(gApp.openedId);
					title = itemData.getElementsByTagName("title")[0].firstChild.nodeValue;
				}
			}
			// create our request
			var requestName = "ClickThru.aspx?filename=" + this.logFilename;
			requestName += "&category=" + gApp.category;
			requestName += "&title=" + title;
			requestName += "&click=" + type;
			requestName += "&user=" + "user";
			// load the request
			this.loadPage(requestName, this.onLogContact, false);
		}
	}

	// -----------------------------------------
	// DataAccessor::updateContent
	this.updateContent = function()
	{	// load a server page to search for auxillary content
		var appId = gConfig.domainid;
		var requestName = "ContentMgr.aspx?app=" + appId;
		this.loadPage(requestName, this.onUpdateContent, false);
	}


	// -------------------------------------------------------------------------------------------
	//            C A L L B A C K   M E T H O D S
	// -------------------------------------------------------------------------------------------
	// Callback methods, from requests for documents or pages
	//
	//	NOTE: like event handlers, the "this" reference does not necessarily refer to the data accessor 
	//  object - it may refer to the element that triggered the event... hence, the use of the global references

	// -----------------------------------------
	// DataAccessor::onConfig
	this.onConfig = function()
	{	// instantiate our configuration storage & load it up
		//  NOTE: the configuration object is a GLOBAL object
		gConfig = new Object();
		if(gApp.isW3 && gTmpDoc != null)
		{
			var domainInfo = gTmpDoc.getElementsByTagName("domain")[0];
			gConfig.domainid = domainInfo.getElementsByTagName("id")[0].firstChild.nodeValue;		// app name
			gData.filepath += "/" + gConfig.domainid;	// update filepath for subsequent processing
			gConfig.contact = domainInfo.getElementsByTagName("contact")[0].firstChild.nodeValue;	// admin email
			var mapInfo = gTmpDoc.getElementsByTagName("map")[0];
			gConfig.provider = mapInfo.getElementsByTagName("provider")[0].firstChild.nodeValue;
			gConfig.latitude = parseFloat(mapInfo.getElementsByTagName("latitude")[0].firstChild.nodeValue);
			gConfig.defaultLat = gConfig.latitude;
			gConfig.longitude = parseFloat(mapInfo.getElementsByTagName("longitude")[0].firstChild.nodeValue);
			gConfig.defaultLng = gConfig.longitude;
			gConfig.zoomlevel = parseInt(mapInfo.getElementsByTagName("zoomlevel")[0].firstChild.nodeValue);
			gConfig.defaultZoom = gConfig.zoomlevel;		
			gConfig.zoommax = parseInt(mapInfo.getElementsByTagName("zoommax")[0].firstChild.nodeValue);
			gConfig.type = mapInfo.getElementsByTagName("type")[0].firstChild.nodeValue;			// map type
			gConfig.controls = mapInfo.getElementsByTagName("controls")[0].firstChild.nodeValue;	// map controls
			gConfig.locked = mapInfo.getElementsByTagName("locked")[0].firstChild.nodeValue;		// cookie override
			var markerInfo = gTmpDoc.getElementsByTagName("markers")[0];
			gConfig.markerMax = parseInt(markerInfo.getElementsByTagName("max")[0].firstChild.nodeValue);
			gConfig.markerOnOpen = markerInfo.getElementsByTagName("openon")[0].firstChild.nodeValue;
			gConfig.useDefaultThumb = true;
			var useThumb = markerInfo.getElementsByTagName("defaultthumb")[0].firstChild.nodeValue;
			if(useThumb == "off") gConfig.useDefaultThumb = false;
			var adInfo = gTmpDoc.getElementsByTagName("ads")[0];
			gConfig.ads = adInfo.getElementsByTagName("enabled")[0].firstChild.nodeValue;
			gConfig.timer = parseInt(adInfo.getElementsByTagName("timer")[0].firstChild.nodeValue);
			gConfig.hostScore = parseInt(adInfo.getElementsByTagName("hostscore")[0].firstChild.nodeValue);
			var categoryInfo = gTmpDoc.getElementsByTagName("category");
			var numCategories = categoryInfo.length;
			var categories;
			if(numCategories > 0)
			{	// save the category info for later processing
				categories = new Array(numCategories);
				for(var j=0; j<numCategories; j++)
				{
					var category = new Object();
					category.name = categoryInfo[j].getElementsByTagName("name")[0].firstChild.nodeValue;
					category.icon = categoryInfo[j].getElementsByTagName("icon")[0].firstChild.nodeValue;
					category.key = categoryInfo[j].getElementsByTagName("key")[0].firstChild.nodeValue;
					categories[j] = category;
				}
			}
			else
			{
				categories = "";
			}
			gConfig.categories = categories;
		}
		// see if should allow cookie values to override the default location
		if(gConfig.locked == "0")
		{	// not locked, allow it if they exist
			var lat = gData.getMapCookie("latitude");
			var lng = gData.getMapCookie("longitude");
			var zom = gData.getMapCookie("zoom");
			if((lat != null) && (lng != null) && (zom != null))
			{
				gConfig.latitude = parseFloat(lat);
				gConfig.longitude = parseFloat(lng);
				gConfig.zoomlevel = parseInt(zom);
			}
		}
		gTmpDoc = null;			// reclaim memory
		// create and save our log file name
		gData.logFilename = gData.filepath + "/" + gConfig.domainid + "Clicks.CSV";
		// now let our app flow control know we have the config data
		gApp.triggerFlow(eConfigOk);
	}

	// -----------------------------------------
	// DataAccessor::onItemList
	this.onItemList = function()
	{	// process the application item list
		gData.sourceData = gTmpDoc;
		gTmpDoc = null;			// reclaim memory
		// now let our app flow control know we have the data
		gApp.triggerFlow(eDataOk);
	}

	// -----------------------------------------
	// DataAccessor::onItemXml
	this.onItemXml = function()
	{	// process the extra item data for the map window
		gData.rssData = gTmpDoc;
		gTmpDoc = null;			// reclaim memory
		gApp.updateRssInfo(gData.rssId);
		gData.rssId = "";
	}

	// -----------------------------------------
	// DataAccessor::onFeatureList
	this.onFeatureList = function()
	{	// process the map feature / correction list
		// if the feature data file doesn't exist, gTmpDoc will be null
		gData.featureData = gTmpDoc;
		gTmpDoc = null;				// reclaim memory
		gApp.updateMapFeatures();	// update the map view
		// now let the app flow control know our map is ready
		gApp.triggerFlow(eMapOk);
	}

	// -----------------------------------------
	// DataAccessor::onLogContact
	this.onLogContact = function()
	{	// click-thru log callback
		// if the file is loaded
		if(gXmlHttp.readyState == 4)
		{	// if the data is not ok
			if(gXmlHttp.status != 200)
			{	// Error!
				gMsg.displayMsg(2,"Log Contact failed: " + gXmlHttp.status + "-" + gXmlHttp.statusText);
			}
			gXmlHttp = null;		// reclaim memory
		}
	}

	// -----------------------------------------
	// DataAccessor::onUpdateContent
	this.onUpdateContent = function()
	{	// click-thru log callback
		// if the file is loaded
		if(gXmlHttp.readyState == 4)
		{	// see if the data is ok
			if(gXmlHttp.status == 200)
			{	// ok!
				gMsg.displayMsg(0,"Content update successful.");
			}
			else
			{	// Error!
				gMsg.displayMsg(2,"Update Content failed: " + gXmlHttp.status + "-" + gXmlHttp.statusText);
				//gMsg.displayMsg(0,"Content update completed.");
			}
			gXmlHttp = null;		// reclaim memory
		}
	}
	
	// -------------------------------------------------------------------------------------------
	//            C O O K I E   M E T H O D S
	// -------------------------------------------------------------------------------------------
	// Methods that access the application cookie(s).

	// -----------------------------------------
	// DataAccessor::getMapCookie
	this.getMapCookie = function(cookieName)
	{	// get the value of our stored cookie - keyed off app name
		var fullCookieName = gConfig.domainid + cookieName;
		var start = -1;
		var end = -1;
		if(document.cookie.length > 0)
		{ 
			start = document.cookie.indexOf(fullCookieName + "=");
			if(start != -1)
			{ 
				start = start + fullCookieName.length + 1;
				end = document.cookie.indexOf(";", start)
				if(end == -1)
				{
					end = document.cookie.length;
				}
				return unescape(document.cookie.substring(start, end))
			} 
		}
		return null
	}

	// -----------------------------------------
	// DataAccessor::setMapCookie
	this.setMapCookie = function(cookieName, cookieValue, expiredays)
	{	// set a cookie to save our value - key off app name
		var expireDate = new Date();
		expireDate.setDate(expireDate.getDate() + expiredays);
		document.cookie = gConfig.domainid + cookieName + "=" + escape(cookieValue) + "; expires=" + expireDate;
	}

	// -----------------------------------------
	// DataAccessor::updateMapCookie
	this.updateMapCookie = function(lat, lng, zoom)
	{	// update our map cookie data
		var expireDays = 30;
		this.setMapCookie("latitude", lat.toString(), expireDays);
		this.setMapCookie("longitude", lng.toString(), expireDays);
		this.setMapCookie("zoom", zoom,	expireDays);
	}

	// -------------------------------------------------------------------------------------------
	//            U T I L I T I E S
	// -------------------------------------------------------------------------------------------
	// Utility methods.

	// -----------------------------------------
	// DataAccessor::createUniqueId
	this.createUniqueId = function()
	{	// create a unique ID for a new item
		var then = new Date(2006, 01, 20);	// tcs birthdate
		var now = new Date();
		var diff = now.getTime() - then.getTime();
		return diff.toString();
	}

	// -----------------------------------------
	// DataAccessor::createXmlDoc
	this.createXmlDoc = function()
	{	// return a new XML doc object based on browser capabilities
		var xDoc = null;
		if(window.ActiveXObject)
		{	// Windows IE
			xDoc = new ActiveXObject("Microsoft.XMLDOM"); //  Msxml2.DOMDocument.3.0
		}
		else if (document.implementation && document.implementation.createDocument)
		{	// Netscape, et al.
			xDoc = document.implementation.createDocument("","",null);
		}
		else
		{
			xDoc = null;
		}
		return xDoc;
	}

	// -----------------------------------------
	// DataAccessor::createXmlRequest
	this.createXmlRequest = function()
	{	// return a new XmlHttpRequest object based on browser capabilities
		var xReq = null;
		if(window.ActiveXObject)
		{	// Windows IE
			xReq = new ActiveXObject("Microsoft.XMLHTTP");
		}	
		else if (window.XMLHttpRequest)
		{	// Mozilla, Firefox, et al.
			xReq = new XMLHttpRequest();
		}
		else
		{
			xReq = null;
		}
		return xReq;
	}
	
	// -----------------------------------------
	// DataAccessor::loadFile
	//	Rewritten for 2.1.0 (Mozilla/Firefox admin)
	//	Used to use XmlDocument to synchronously load the XML file, but due to Firefox caching issues
	//	on the initial load (i.e. post-create-app), forced to switch to using asynchronous xmlhttprequest
	//	object and forcing the request to have a unique identifier on each invocation.
	//
	this.loadFile = function(filename, callback, append)
	{	// general function to asynchronously load an XML file
		var requestName = filename;
		gTmpDoc = null;
		if(append)
			{requestName = this.filepath + "/" + filename;}
		var xmlReq = this.createXmlRequest();
		if(xmlReq != null)
		{	// ok, continue
			xmlReq.onreadystatechange = function()
				{
					if(xmlReq.readyState == 4)
					{	// request complete
						if(xmlReq.status == 200)
						{	// found, ok
							gTmpDoc = xmlReq.responseXML;
							callback();
						}
						else if(xmlReq.status == 404)
						{	// not found, but's that's perfectly ok too
							gTmpDoc = null;
							callback();
						}
						else
						{	// Error!
							gMsg.displayMsg(2,"Loadfile failed: " + xmlReq.status + "-" + xmlReq.statusText);
						}
					}
				};
			// we're appending a unique number as a query string parameter to guarantee that
			// the browsers will not cache the response and give us old data
			xmlReq.open("Get", requestName + "?" + this.createUniqueId() , true);
			xmlReq.send(null);
		}
	}

	// -----------------------------------------
	// DataAccessor::loadPage
	this.loadPage = function(filename, callback, append)
	{	// general function to asynchronously load a web page
		if(gXmlHttp != null)
		{	// we are already processing a load page request so wait a bit and try again
			gMsg.displayMsg(0,"Retrying request for " + filename + ".");
			var tid = setTimeout("gData.loadPage('" + filename + "', " + callback + "," + append + ")", 50);
		}
		else
		{	// okay to process the request
			gMsg.displayMsg(0," ");
			var requestURL = filename;
			if(append)
				{requestURL = this.filepath + "/" + filename;}
			// get an XmlHttpRequest
			gXmlHttp = this.createXmlRequest();
			if(gXmlHttp != null)
			{
				gXmlHttp.onreadystatechange = callback;
				gXmlHttp.open("Get", requestURL, true);
				gXmlHttp.send(null);
			}	
		}
	}
	
	// -------------------------------------------------------------------------------------------
	//            V A L I D A T O R S
	// -------------------------------------------------------------------------------------------
	// Methods that validate the application data prior to storage.

	// -----------------------------------------
	// DataAccessor::validateEmail
	this.validateEmail = function(fieldname, value, isRqrd) 
	{	// create our return value
		var errObj = new Object();
		errObj.isError = false;
		errObj.errMsg = "";
		// begin the validation
		if(value == null || value.length == 0)
		{	// it's blank, see if it is a required field
			if(isRqrd)
			{	// yep - flag it
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is required.";
			}
		}
		else
		{	// not blank, check the value
			var emailReg = "^[\\w-_\.]*[\\w-_\.]\@[\\w]\.+[\\w]+[\\w]$";
			var regex = new RegExp(emailReg);
			if(regex.test(value) == false)
			{
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is invalid. Enter a valid e-mail address (e.g. account@example.com).";
			}
		}
		return errObj;
	}
	
	// -----------------------------------------
	// DataAccessor::validateNumber
	this.validateNumber = function(fieldname, value, isRqrd) 
	{	// create our return value
		var errObj = new Object();
		errObj.isError = false;
		errObj.errMsg = "";
		// begin the validation
		if(value == null || value.length == 0)
		{	// it's blank, see if it is a required field
			if(isRqrd)
			{	// yep - flag it
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is required.";
			}
		}
		else
		{	// not blank, check the value
			var numericReg = /^[-]?\d*\.?\d*$/;
			var regex = new RegExp(numericReg);
			if(regex.test(value) == false)
			{
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is invalid. Enter a valid number (e.g. 38.39848).";
			}
		}
		return errObj;
	}
	
	// -----------------------------------------
	// DataAccessor::validatePhone 
	this.validatePhone = function(fieldname, value, isRqrd) 
	{	// create our return value
		var errObj = new Object();
		errObj.isError = false;
		errObj.errMsg = "";
		// begin the validation
		if(value == null || value.length == 0)
		{	// it's blank, see if it is a required field
			if(isRqrd)
			{	// yep - flag it
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is required.";
			}
		}
		else
		{	// not blank, check the value
			var phoneReg = /^[0-9]{3,3}\-[0-9]{3,3}\-[0-9]{4,4}$|^\([0-9]{3,3}\) [0-9]{3,3}\-[0-9]{4,4}$|^\([0-9]{3,3}\)[0-9]{3,3}\-[0-9]{4,4}$|^[0-9]{3,3}\-[0-9]{4,4}$/;
			var regex = new RegExp(phoneReg);
			if(regex.test(value) == false)
			{
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is invalid. Enter a valid phone number (e.g. (800) 555-1212).";
			}
		}
		return errObj;
	}
	
	// -----------------------------------------
	// DataAccessor::validateText 
	this.validateText = function(fieldname, value, isRqrd) 
	{	// create our return value
		var errObj = new Object();
		errObj.isError = false;
		errObj.errMsg = "";
		// begin the validation
		if(value == null || value.length == 0)
		{	// it's blank, see if it is a required field
			if(isRqrd)
			{	// yep - flag it
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is required.";
			}
		}
		else
		{	// not blank, check the value
			var illegalCharsReg = /[&@\(\)\<\>\;\:\\\/\"\[\]]/
			var regex = new RegExp(illegalCharsReg);
			if(regex.test(value) == true)
			{
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " contains illegal characters.";
			}
		}
		return errObj;
	}
	
	// -----------------------------------------
	// DataAccessor::validateURL
	// var urlReg = "^[A-Za-z]+://[A-Za-z0-9-_]+\\.[A-Za-z0-9-_%&amp;\?\/.=]+$";
	// var urlReg = /^(http:\/\/www.|https:\/\/www.|http:\/\/|https:\/\/){1}([\w]+)(.[\w]+){1,2}$/;
	this.validateURL = function(fieldname, value, isRqrd) 
	{	// create our return value
		var errObj = new Object();
		errObj.isError = false;
		errObj.errMsg = "";
		// begin the validation
		if(value == null || value.length == 0)
		{	// it's blank, see if it is a required field
			if(isRqrd)
			{	// yep - flag it
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is required.";
			}
		}
		else
		{	// not blank, check the value
			var urlReg = /^(http:|https:)\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
			var regex = new RegExp(urlReg);
			if(regex.test(value) == false)
			{
				errObj.isError = true;
				errObj.errMsg = "The " + fieldname + " is invalid. Enter a valid URL (e.g. http://www.example.com/mypage.html).";
			}
		}
		return errObj;
	}
}

