//////////////////////////////////////////////////////////////////////////////
//
// Copyright © 2002-2004, Microsoft Corporation.  All rights reserved.
//
// File: LMSAPI.js
//
// Description: Contains the SCORM and viewer client-side LMS API implementation.
//
//////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////
// GLOBAL CONSTANTS
//////////////////////////////////////////////////////////////////////////////
var SCORM_MANIFEST_FILE = "imsmanifest.xml";			// SCORM compliant manifest
var COLLECTION_MANIFEST_FILE = "mpimanifest.xml";		// Our proprietary collection manifest
var VIEWER_SETTINGS_FILE = "settings.xml";			// Global viewer settings
var XSL_FILE = "viewer/xsl/cpitem.xsl";				// Stylesheet to transform the manifest into an internal xml format
var LMS_DATA_MODEL_FILE = "viewer/lms/lmsdatamodel.xml";	// SCORM data model
var NOTES_TREEVIEW_XSL_FILE = "viewer/xsl/notestreeview.xsl";	// Stylesheet to transform the manifest for notes management
var COMMUNITY_LINK_FILE = "community.xml"			// Community link feature settings files
var LCDS_PLATFORM_MANIFEST = "platform/platformmanifest.xml";	// LCDS platform manifest location relative to the imsmanifest
var LCDS_NOTES_DELIMITER = "*$^%";

// Error details types: these strings are used to construct LRNError objects
var ERROR_DETAILS_PARSE = "XML_ERROR_DETAILS_PARSE";
var ERROR_DETAILS_SCRIPT = "ERROR_DETAILS_SCRIPT";
var ERROR_DETAILS_MULTIPLE = "Multi-Status_Error";
var ERROR_DETAILS_STRING = "String";
var ERROR_DETAILS_NONE = "Null";

//////////////////////////////////////////////////////////////////////////////
// GLOBAL FUNCTIONS
//////////////////////////////////////////////////////////////////////////////

//--------------------------------------------------------------------
// Function: ErrorStringFromID
//
// Purpose: Searches LMS_Errors.xml for the description of an error.
//	    If the ID cannot be found in the XML, an empty string is returned.
//	    If the ID is found, the error description is returned.
//
// Parameters:
//	Number nID [in] - The error number.
//
// Returns: String
//--------------------------------------------------------------------
function ErrorStringFromID(nID)
{
	var sError = "";

	if (!isNaN(nID))
	{
		switch (nID)
		{
			case 0:
				sError = SCORM_LMS_ERROR_0;
				break;
			case 101:
				sError = SCORM_LMS_ERROR_101;
				break;
			case 201:
				sError = SCORM_LMS_ERROR_201;
				break;
			case 202:
				sError = SCORM_LMS_ERROR_202;
				break;
			case 203:
				sError = SCORM_LMS_ERROR_203;
				break;
			case 204:
				sError = SCORM_LMS_ERROR_204;
				break;
			case 205:
				sError = SCORM_LMS_ERROR_205;
				break;
			case 206:
				sError = SCORM_LMS_ERROR_206;
				break;
			case 301:
				sError = SCORM_LMS_ERROR_301;
				break;
			case 401:
				sError = SCORM_LMS_ERROR_401;
				break;
			case 402:
				sError = SCORM_LMS_ERROR_402;
				break;
			case 403:
				sError = SCORM_LMS_ERROR_403;
				break;
			case 404:
				sError = SCORM_LMS_ERROR_404;
				break;
			case 405:
				sError = SCORM_LMS_ERROR_405;
				break;

		}
	}

	return sError;
}

//////////////////////////////////////////////////////////////////////////////
// CLASSES
//////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////
// Class: LRNError
//
// Purpose: Constructor function for LRN error objects.
//
// Parameters:
//	Number nNumber 	     [in] - The numeric identifier of this error.  The description string will be loaded from the XML.
//	String sDetailsType  [in] - A string description of the type of information in oErrorDetails.  Several constants have been recorded above as types LRN understands.  This parameter is required if oErrorDetails is not null, but is ignored if oErrorDetails is null.
//	Object oErrorDetails [in] - This parameter is optional.  Additional information about the error that occurred.  The type of this object is indicated by the sDetailsType parameter.
//
// Returns: Object OR null
////////////////////////////////////////////////////////////////////////
function LRNError(nNumber,sDetailsType,oErrorDetails)
{
	// Checking parameter
	if ("number" != typeof(nNumber)) return null;
	
	if ("string" != typeof(sDetailsType)) sDetailsType = ERROR_DETAILS_NONE;
	
	if ("undefined" != typeof(oErrorDetails)) oErrorDetails = null;
		
	// Object Initialization
	// conformance to jscript error
	this.number = nNumber;		
	this.sDetailsType = sDetailsType;
	this.oErrorDetails = oErrorDetails;
	this.description = ErrorStringFromID(nNumber);

	return this;
}

//////////////////////////////////////////////////////////////////////////////
// Class: LMSAPI
//
// Purpose: Constructor function for LMSAPI objects.  This class implements
//	    the SCORM API adapter.  It exposes SCORM-specific methods as well
//	    as methods specific to the viewer. Note that only one LMSAPI object
//	    should be instantiated.
//
// Parameters:
//	Boolean bPreview [in] - A flag indicating whether to run in preview mode.
//				If true, this object will not read/write to IE 
//				userdata.
//
//	Object  oMsgQ	 [in] - A message queue object used to display runtime
//				messages to the end-user.
//
// Note: Can throw during instantiation
//////////////////////////////////////////////////////////////////////////////
function LMSAPI(bPreview,oMsgQ)
{
	// Check parameters
	if ("boolean" != typeof(bPreview) || "object" != typeof(oMsgQ))
		throw new LRNError(201);

	//////////////////////////////////////////////////////////////////////
	// PRIVATE CONSTANTS
	//////////////////////////////////////////////////////////////////////
	
	// Used for userData
	var PERS_XML_ATTRIBUTE = "persXML";
	var USER_DATA_PREFIX = "lrnpers&";
	var VIEWER_STATE_PREFIX = "vwrstate&";

	// For working with persisted XML
	var PERS_ELEMENT_NAME = "l";	// lrnpers
	var PROPERTY_PROP_NAME = "p";	// Property
	var VALUE_PROP_NAME = "v";	// Value
	var SCOPE_PROP_NAME = "s";	// Scope
	var DATE_PROP_NAME = "d";	// Date
	var TIME_PROP_NAME = "t";	// Time
	var ATTEMPTS_PROP_NAME = "a";	// Attempts
	var ERROR_ELEMENT_NAME = "Error";
	var ERROR_PROP_NUMBER = "number";
	var ERROR_PROP_DESCRIPTION = "description";

	var NOTES_XML_ROOT_NODE = "Notes";
	var NOTES_XML_ROOT_NODE_MANIFEST_ID_ATTRIB = "manifestID";
	var NOTES_XML_NOTE_NODE = "Note";
	var NOTES_XML_NOTE_NODE_SCO_ID_ATTRIB = "scoID";
	
	//////////////////////////////////////////////////////////////////////
	// PRIVATE VARIABLES
	//////////////////////////////////////////////////////////////////////

	var m_bInitialized = false;					// Flag to indicate LMSAPI object initialization state
	var m_bNeedSave = false;					// Flag to indicate whether data needs to be persisted
	var m_oLastError = null;					// Object to hold last error
	var m_sManifestPath = "";					// Manifest path
	var m_sManifestFileName = SCORM_MANIFEST_FILE;			// Manifest file name
	var m_bLMSInitialized = false;	 				// Flag to indicate whether LMSInitialize was called for the current SCO

	var m_oLMSDataModel = new LMSDataModel();			// LMS data model XML

	var m_sIdentifier = GetPersistenceIdentifier();			// String used as partial filename for user data

	var m_oBasePersXML = LoadXML("<ROOT></ROOT>");			// Base Pers XML
	var m_oPersXML = m_oBasePersXML.cloneNode(true);		// Pers XML
	var m_oViewerStateXML = m_oBasePersXML.cloneNode(true); 	// Viewer state XML

	var m_oUserData = window.frames.UserDataFrame.document.getElementById("g_oDefaultUserData");

	var m_oViewerEvents = new ViewerEvents();			// Instantiate the viewer events class
	var m_oViewer = new Viewer();					// Instantiate the viewer class

	var m_oHostLMS = new HostLMS(this);				// Instantiate the HostLMS class

	var m_oViewerSettings = LoadXMLFile(VIEWER_SETTINGS_FILE);	// Viewer settings XML

	var m_bPreview = bPreview;					// Viewer mode
	var m_oMsgQ = oMsgQ;						// Message queue for runtime user feedback

	var m_bUsingSQLDB = false;					// Flag to indicate whether data is persisted within a SQL DB

	var m_oLMSAPI = this;						// LMSAPI instance referrence

	var m_sCurrentScoID = "";					// Current SCO identifier

	//////////////////////////////////////////////////////////////////////
	// PUBLIC METHODS
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: LRNDoNext
 	//
 	// Purpose: Wrapper for Viewer method DoNext.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNDoNext=function()
	{
		m_oViewer.DoNext();
	}

	//--------------------------------------------------------------------
	// Method: LRNDoPrevious
 	//
 	// Purpose: Wrapper for Viewer method DoPrevious.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNDoPrevious=function()
	{
		m_oViewer.DoPrevious();
	}

	//--------------------------------------------------------------------
	// Method: LRNDoSelect
 	//
 	// Purpose: Wrapper for Viewer method DoSelect.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNDoSelect=function(sID)
	{
		m_oViewer.DoSelect(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNDoSelectFirst
 	//
 	// Purpose: Wrapper for Viewer method DoSelectFirst.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNDoSelectFirst=function()
	{
		m_oViewer.DoSelectFirst();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetCurrentID
 	//
 	// Purpose: Wrapper for Viewer method GetCurrentID.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetCurrentID=function()
	{
		return m_oViewer.GetCurrentID();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetNewID
 	//
 	// Purpose: Wrapper for Viewer method GetNewID.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetNewID=function()
	{
		return m_oViewer.GetNewID();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetNextID
 	//
 	// Purpose: Wrapper for Viewer method GetNextID.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetNextID=function(sID)
	{
		return m_oViewer.GetNextID(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetPreviousID
 	//
 	// Purpose: Wrapper for Viewer method GetPreviousID.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetPreviousID=function(sID)
	{
		return m_oViewer.GetPreviousID(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetParentID
 	//
 	// Purpose: Wrapper for Viewer method GetParentID.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetParentID=function(sID)
	{
		return m_oViewer.GetParentID(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNSetCurrentTOC
 	//
 	// Purpose: Wrapper for Viewer method SetCurrentTOC.
 	//
 	// Parameters:
	//	String sID [in] -
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetCurrentTOC=function(sID)
	{
		m_oViewer.SetCurrentTOC(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetCurrentTOC
 	//
 	// Purpose: Wrapper for Viewer method GetCurrentTOC.
 	//
 	// Parameters: None
	//
	// Returns:
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetCurrentTOC=function()
	{
		return m_oViewer.GetCurrentTOC();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetTOCS
 	//
 	// Purpose: Wrapper for Viewer method GetTOCS.
 	//
 	// Parameters: None
	//
	// Returns:
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetTOCS=function()
	{
		return m_oViewer.GetTOCS();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetChildren
 	//
 	// Purpose: Wrapper for Viewer method GetChildren.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns:
	//--------------------------------------------------------------------					
	LMSAPI.prototype.LRNGetChildren=function(sID)
	{
		return m_oViewer.GetChildren(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetDescendants
 	//
 	// Purpose: Wrapper for Viewer method GetDescendants.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns:
	//--------------------------------------------------------------------					
	LMSAPI.prototype.LRNGetDescendants=function(sID)
	{
		return m_oViewer.GetDescendants(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetItem
 	//
 	// Purpose: Wrapper for Viewer method GetItem.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns:
	//--------------------------------------------------------------------	
	LMSAPI.prototype.LRNGetItem=function(sID)
	{
		return m_oViewer.GetItem(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetItemTitle
 	//
 	// Purpose: Wrapper for Viewer method GetItemTitle.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------	
	LMSAPI.prototype.LRNGetItemTitle=function(sID)
	{
		return m_oViewer.GetItemTitle(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsReady
 	//
 	// Purpose: Wrapper for Viewer method IsReady.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------	
	LMSAPI.prototype.LRNIsReady=function()
	{
		return m_oViewer.IsReady();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetSubmanifestCount
 	//
 	// Purpose: Wrapper for Viewer method GetSubmanifestCount.
 	//
 	// Parameters:
	//	String sID [in] - Manifest identifier
	//
	// Returns: Number
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetSubmanifestCount=function(sID)
	{
		return m_oViewer.GetSubmanifestCount(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetSubmanifestIDs
 	//
 	// Purpose: Wrapper for Viewer method GetSubmanifestIDs.
 	//
 	// Parameters:
	//	String sID [in] - Manifest identifier
	//
	// Returns: Array
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetSubmanifestIDs=function(sID)
	{
		return m_oViewer.GetSubmanifestIDs(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetRootManifestID
 	//
 	// Purpose: Wrapper for Viewer method GetRootManifestID.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetRootManifestID=function()
	{
		return m_oViewer.GetRootManifestID()
	}

	//--------------------------------------------------------------------
	// Method: LRNGetCurrentManifestID
 	//
 	// Purpose: Wrapper for Viewer method GetCurrentManifestID.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetCurrentManifestID=function()
	{
		return m_oViewer.GetCurrentManifestID();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetResourcesPathByManifestID
 	//
 	// Purpose: Wrapper for Viewer method GetResourcesPathByManifestID.
 	//
 	// Parameters:
	//	String sID [in] - Manifest identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetResourcesPathByManifestID=function(sID)
	{
		return m_oViewer.GetResourcesPathByManifestID(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetManifestTitle
 	//
 	// Purpose: Wrapper for Viewer method GetManifestTitle.
 	//
 	// Parameters:
	//	String sID [in] - Manifest identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetManifestTitle=function(sID)
	{
		return m_oViewer.GetManifestTitle(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsViewerOffline
 	//
 	// Purpose: Specifies whether the viewer is running locally.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsViewerOffline=function()
	{
		return IsFileProtocol();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetManifestPath
 	//
 	// Purpose: Gets the location of the manifest.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetManifestPath=function()
	{
		return m_sManifestPath;
	}

	//--------------------------------------------------------------------
	// Method: LRNUsingSQLDB
 	//
 	// Purpose: Specifies whether a SQL database is used for data persistence.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNUsingSQLDB=function()
	{
		return m_bUsingSQLDB;
	}

	//--------------------------------------------------------------------
	// Method: LRNIsCollection
	//
 	// Purpose: Wrapper for Viewer method IsCollection.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsCollection=function()
	{
		return m_oViewer.IsCollection();
	}

	//--------------------------------------------------------------------
	// Method: LRNSynchronize
	//
 	// Purpose: Wrapper for Viewer method Synchronize.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSynchronize=function()
	{
		return m_oViewer.Synchronize();
	}

	//--------------------------------------------------------------------
	// Method: LRNResolveItemIDToCourseID
	//
 	// Purpose: Wrapper for Viewer method ResolveItemIDToCourseID.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNResolveItemIDToCourseID=function(sID)
	{
		return m_oViewer.ResolveItemIDToCourseID(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetCustomHelpMenuItemList
 	//
 	// Purpose: Wrapper for Viewer method GetCustomHelpMenuItemList.
 	//
 	// Parameters: None
	//
	// Returns: Array
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetCustomHelpMenuItemList=function()
	{
		return m_oViewer.GetCustomHelpMenuItemList();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetResourceIdFromItemId
 	//
 	// Purpose: Wrapper for Viewer method GetResourceIdFromItemId.
	//	
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetResourceIdFromItemId=function(sID)
	{
		return m_oViewer.GetResourceIdFromItemId(sID);
	}

	//--------------------------------------------------------------------
	// Method: GetItemIDFromEKScoID
 	//
 	// Purpose: Wrapper for Viewer method GetItemIdFromResourceId.
	//	
 	// Parameters:
	//	String sID [in] - Resource identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetItemIdFromResourceId=function(sID)
	{
		return m_oViewer.GetItemIdFromResourceId(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsLessonNode
 	//
 	// Purpose: Wrapper for Viewer method IsLessonNode
	//	
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsLessonNode=function(sID)
	{
		return m_oViewer.IsLessonNode(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsTopicNode
 	//
 	// Purpose: Wrapper for Viewer method IsTopicNode.
	//	
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsTopicNode=function(sID)
	{
		return m_oViewer.IsTopicNode(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNResolveUrlToItemID
	//
 	// Purpose: Wrapper for Viewer method ResolveUrlToItemID.
 	//
 	// Parameters:
	//	String sURL [in] - Uniform Resource Locator
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNResolveUrlToItemID=function(sURL)
	{
		return m_oViewer.ResolveUrlToItemID(sURL);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsHostOfflinePlayer
	//
 	// Purpose: Determines whether the viewer is hosted by the offline
	//	    player of an LMS.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsHostOfflinePlayer=function()
	{
		return (this.LRNIsViewerHosted() && IsFileProtocol());
	}

	//--------------------------------------------------------------------
	// Method: LRNIsItemAvailable
	//
 	// Purpose: Wrapper for Viewer method IsItemAvailable.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsItemAvailable=function(sID)
	{
		return m_oViewer.IsItemAvailable(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNAddMessage
 	//
 	// Purpose: Wrapper for ViewerMessages method AddMessage.
	//
 	// Parameters:
	//	String sMsg [in] - Message
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNAddMessage=function(sMsg)
	{
		return m_oMsgQ ? m_oMsgQ.AddMessage(sMsg) : "";
	}

	//--------------------------------------------------------------------
	// Method: LRNRemoveMessage
 	//
 	// Purpose: Wrapper for ViewerMessages method RemoveMessage.
 	//
 	// Parameters:
	//	String sID [in] - Message token
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNRemoveMessage=function(sID)
	{
		if (m_oMsgQ) m_oMsgQ.RemoveMessage(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNListenForEvents
 	//
 	// Purpose: Wrapper for ViewerEvents method ListenForEvents.
 	//
 	// Parameters:
	//	fHandler [in] - Delegate function pointer
	//	Number nEvents [in] - Event messages to process
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNListenForEvents=function(fHandler,nEvents)
	{
		m_oViewerEvents.ListenForEvents(fHandler,nEvents);
	}

	//--------------------------------------------------------------------
	// Method: LRNUnListenForEvents
 	//
 	// Purpose: Wrapper for ViewerEvents method UnListenForEvents.
 	//
 	// Parameters:
	//	fHandler [in] - Delegate function pointer
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNUnListenForEvents=function(fHandler)
	{
		m_oViewerEvents.UnListenForEvents(fHandler);
	}

	//--------------------------------------------------------------------
	// Method: LRNRaiseEvent
 	//
 	// Purpose: Wrapper for ViewerEvents method RaiseEvent.
 	//
 	// Parameters:
	//	Number nType [in] - Event type identifier
	//	String sID   [in] - Item identifier
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNRaiseEvent=function(nType,sID)
	{
		m_oViewerEvents.RaiseEvent(nType,sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsViewerHosted
	//
 	// Purpose: Determines whether the viewer is hosted by an LMS.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsViewerHosted=function()
	{
		return (m_oHostLMS.API != null);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetScoIdFromItemId
	//
 	// Purpose: Wrapper for HostLMS method GetScoIdFromItemId
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetScoIdFromItemId=function(sID)
	{
		return m_oHostLMS.GetScoIdFromItemId(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetItemIdFromScoId
	//
 	// Purpose: Wrapper for HostLMS method GetItemIdFromScoId
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetItemIdFromScoId=function(sID)
	{
		return m_oHostLMS.GetItemIdFromScoId(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsComplete
	//
 	// Purpose: Wrapper for HostLMS method IsComplete
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsComplete=function(sID)
	{
		if (this.LRNIsViewerHosted())
		{
			var sScoID = m_oLMSAPI.LRNGetScoIdFromItemId(sID);
			return m_oHostLMS.IsComplete(sScoID);
		}
		else
		{
			return this.LRNGetScopedValue("cmi.core.lesson_status",sID,true) == "completed";
		}
	}

	//--------------------------------------------------------------------
	// Method: LRNIsTopicDownloaded
	//
 	// Purpose: Wrapper for HostLMS method IsTopicDownloaded
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsTopicDownloaded=function(sID)
	{
		return m_oHostLMS.IsTopicDownloaded(sID);
	}

	//--------------------------------------------------------------------
	// Method: LRNSetComplete
	//
 	// Purpose: Wrapper for HostLMS method SetComplete
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//	Boolean bIsComplete [in] - 
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetComplete=function(sID, bIsComplete)
	{
		m_oHostLMS.SetComplete(sID, bIsComplete);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetBookmarks
	//
 	// Purpose: Wrapper for HostLMS method GetBookmarks
 	//
 	// Parameters: None
	//
	// Returns: Array
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetBookmarks=function()
	{
		return m_oHostLMS.GetBookmarks();
	}

	//--------------------------------------------------------------------
	// Method: LRNSetBookmark
	//
 	// Purpose: Wrapper for HostLMS method SetBookmark
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//	String sValue [in] - Bookmark title
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetBookmark=function(sScoID, sValue)
	{
		m_oHostLMS.SetBookmark(sScoID, sValue);
	}

	//--------------------------------------------------------------------
	// Method: LRNGetNote
	//
 	// Purpose: Wrapper for HostLMS method GetNote
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetNote=function(sID)
	{
		if (this.LRNIsViewerHosted())
		{
			var sScoId = this.LRNGetScoIdFromItemId(sID);
			return m_oHostLMS.GetNote(sScoId);
		}
		else
		{
			return this.LRNGetScopedValue("cmi.comments", sID, true);
		}
	}

	//--------------------------------------------------------------------
	// Method: LRNSetNote
	//
 	// Purpose: Wrapper for HostLMS method SetNote
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//	String sText [in] - Note text
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetNote=function(sID,sText)
	{
		if (this.LRNIsViewerHosted())
		{
			var sScoId = this.LRNGetScoIdFromItemId(sID);
			m_oHostLMS.SetNote(sScoId,sText);
		}
		else
		{
			this.LRNSetScopedValue("cmi.comments", sText, sID, true);
		}
	}

	//--------------------------------------------------------------------
	// Method: LRNHasNote
	//
 	// Purpose: Wrapper for HostLMS method HasNote
 	//
 	// Parameters:
	//	String sID [in] - SCO identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNHasNote=function(sID)
	{
		if (this.LRNIsViewerHosted())
		{
			return m_oHostLMS.HasNote(sID);
		}
		else
		{
			return (this.LRNGetScopedValue("cmi.comments", sID, true) != "");
		}
	}

	//--------------------------------------------------------------------
	// Method: LRNGetNotes
	//
 	// Purpose: Wrapper for HostLMS method GetNotes
 	//
 	// Parameters: None
	//
	// Returns: Array
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetNotes=function()
	{
		if (this.LRNIsViewerHosted())
			return m_oHostLMS.GetNotes();
		else
			return InternalGetNotes();
	}

	//--------------------------------------------------------------------
	// Method: LRNHasNotes
	//
 	// Purpose: Determines whether notes exist.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNHasNotes=function()
	{
		return InternalHasNotes();
	}

	//--------------------------------------------------------------------
	// Method: LRNExportNotesXml
	//
 	// Purpose: Exports notes to XML.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNExportNotesXml=function()
	{
		try {window.external.UpdateProgress(i);} catch(e) {}

		var sReport = "";
		var sLessonID = "";
		var sModuleID = "";
		var nExportedNotesCount = 0;
		var totalNotesCount = InternalGetNotesCount();

		var sNotes = "<" + NOTES_XML_ROOT_NODE + " " + NOTES_XML_ROOT_NODE_MANIFEST_ID_ATTRIB + "='" + this.LRNGetRootManifestID() + "'>";

		if (totalNotesCount > 0)
		{
			var aNotes = this.LRNGetNotes();

			for (oNote in aNotes)
			{
				sNotes += "<" + NOTES_XML_NOTE_NODE + " " + NOTES_XML_NOTE_NODE_SCO_ID_ATTRIB + "='" + oNote + "'>";
				sNotes += aNotes[oNote] + "</" + NOTES_XML_NOTE_NODE + ">";

				if (this.LRNIsLcdsContent())
				{
					sModuleID = this.LRNGetParentID(oNote);
					sLessonID = oNote;

					var aTopicNotes = aNotes[oNote].split(LCDS_NOTES_DELIMITER);
				
					for (var i = 0; i < aTopicNotes.length; i++)
					{
						if (aTopicNotes[i] != "")
						{
							nExportedNotesCount++;
							sReport += this.LRNGetItemTitle(sModuleID) + " /\ " + this.LRNGetItemTitle(sLessonID) + " /\ " + m_oViewer.GetLcdsTopicTitle(oNote, i) + "\n";
							try {window.external.UpdateProgress(parseInt((nExportedNotesCount/totalNotesCount)*100));} catch(e) {}
						}
					}
				}
				else
				{
					var sTopicTitle = "";

					if (this.LRNIsLessonNode(oNote))
					{
						sModuleID = this.LRNGetParentID(oNote);
						sLessonID = oNote;
						sTopicTitle = LBL_CONTENTWHEEL_INTRO_TOPIC;
					}
					else
					{
						sLessonID = this.LRNGetParentID(oNote);
						sModuleID = this.LRNGetParentID(sLessonID);
						sTopicTitle =  this.LRNGetItemTitle(oNote);	
					}

					nExportedNotesCount++;
					sReport += this.LRNGetItemTitle(sModuleID) + " | " + this.LRNGetItemTitle(sLessonID) + " | " + sTopicTitle + "\n";
					try {window.external.UpdateProgress(parseInt((nExportedNotesCount/totalNotesCount)*100));} catch(e) {}
				}
			}

			var sHeader = EXPORT_NOTES_FILE_PROCESSING_REPORT_HEADER + "\n";
			sHeader += "----------------------------------------------------------------------------------------------" + "\n";

			var aPlaceholders = new Array("%n1","%n2");
			var aValues = new Array(nExportedNotesCount, totalNotesCount);
			var sMsg = TextReplace(EXPORT_NOTES_FILE_PROCESSING_REPORT_EXPORTED_COUNT, aPlaceholders, aValues);

			sHeader += sMsg + "\n";
			sHeader += "----------------------------------------------------------------------------------------------" + "\n";
						
			try {window.external.ReportProcessingResults(sHeader + sReport);} catch(e) {}
		}
		else
		{
			try {window.external.ReportError(EXPORT_NOTES_ERR_NO_NOTES_TO_EXPORT);} catch(e) {}
		}

		sNotes += "</" + NOTES_XML_ROOT_NODE + ">";

		return sNotes;
	}

	//--------------------------------------------------------------------
	// Method: LRNImportNotesXml
	//
 	// Purpose: Imports notes from XML.
 	//
 	// Parameters: 
	//	String sNotesXML [in] - Notes XML
	//	Boolean bReplace [in] - Whether to append or replace existing
	//				notes.
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNImportNotesXml=function(sNotesXML, bReplace)
	{
		var bSuccess = true;
		var oNotesXML = null;
		
		try
		{
			oNotesXML = LoadXML(sNotesXML);
		}
		catch(e)
		{
			bSuccess = false;
			try {window.external.ReportError(ERR_IMPORT_NOTES_ERROR_LOADING_XML);} catch(e) {}
		}

		if (oNotesXML != null)
		{
			var oRoot = oNotesXML.selectSingleNode(NOTES_XML_ROOT_NODE);

			if (oRoot != null)
			{
				if (oRoot.getAttribute(NOTES_XML_ROOT_NODE_MANIFEST_ID_ATTRIB) == this.LRNGetRootManifestID())
				{
					var oNodes = oRoot.selectNodes(NOTES_XML_NOTE_NODE);

					var totalNotesCount = oNodes.length;

					if (totalNotesCount > 0)
					{
						var notesCount = 0;
						try {window.external.UpdateProgress(i);} catch(e) {}

						var oNode = null;
						var sNote = "";
						var sScoId = "";
                        
                        for(ic = 0; ic < oNodes.length; ic++)
						//while (oNode = oNodes.nextNode())
						{
						    oNode = oNodes[ic];
							// Get the sco identifier
							sScoID = oNode.getAttribute(NOTES_XML_NOTE_NODE_SCO_ID_ATTRIB);

							// Get the notes for this sco
							sNote = this.LRNGetNote(sScoID);

							// Replace the notes, otherwise append them.
							if (bReplace || sNote == "")
								sNote = oNode.text;
							else
							{
								if (this.LRNIsLcdsContent())
								{
									var aExistingTopicNotes = sNote.split(LCDS_NOTES_DELIMITER);
									var aImportedTopicNotes = oNode.text.split(LCDS_NOTES_DELIMITER);

									if (aExistingTopicNotes.length == aImportedTopicNotes.length)
									{
										for (var i = 0; i < aExistingTopicNotes.length; i++)
										{
											if (aExistingTopicNotes[i] == "" && aImportedTopicNotes[i] != "")
												aExistingTopicNotes[i] = aImportedTopicNotes[i];	
											else if (aExistingTopicNotes[i] != "" && aImportedTopicNotes[i] != "")
												aExistingTopicNotes[i] += "\n" + aImportedTopicNotes[i];
										}

										sNote = aExistingTopicNotes.join(LCDS_NOTES_DELIMITER);
									}
									else
									{
										bSuccess = false;
										var aPlaceholders = new Array("%s1");
										var aValues = new Array(sScoID);
										var sMsg = TextReplace(ERR_IMPORT_NOTES_TOPIC_COUNT_NOTES_COUNT_MISMATCH, aPlaceholders, aValues);
										try {window.external.ReportError();} catch(e) {}
									}
								}
								else
								{
									sNote += "\n" + oNode.text;
								}
							}

							this.LRNSetNote(sScoID, sNote);

							// Increment the record count and update the KonaH progress bar.
							try {window.external.UpdateProgress(parseInt((++notesCount/totalNotesCount)*100));} catch(e) {}
						}
					}
					else
					{
						bSuccess = false;
						try {window.external.ReportError(ERR_IMPORT_NOTES_NO_NOTES_TO_IMPORT);} catch(e) {}
					}			
				}
				else
				{
					bSuccess = false;
					try {window.external.ReportError(ERR_IMPORT_NOTES_WRONG_COURSE);} catch(e) {}	
				}
			}
			else
			{
				bSuccess = false;
				try {window.external.ReportError(ERR_IMPORT_NOTES_UNEXPECTED_XML_FORMAT);} catch(e) {}
			}
		}
		

		return bSuccess;
	}

	//--------------------------------------------------------------------
	// Method: LRNGetNotesTreeViewXmlDOM
	//
 	// Purpose: Wrapper for Viewer method GetNotesTreeViewXmlDOM.
	//
 	// Parameters: None
	//
	// Returns: Object
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetNotesTreeViewXmlDOM=function()
	{
		return m_oViewer.GetNotesTreeViewXmlDOM();
	}

	//--------------------------------------------------------------------
	// Method: LRNSetViewerSetting
	//
 	// Purpose: Wrapper for HostLMS method SetViewerSetting.
	//
 	// Parameters:
	//	String sProperty {in] - 
	// 	String sValue [in] -
	//
	// Returns: Object
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetViewerSetting=function(sProperty,sValue)
	{
		m_oHostLMS.SetViewerSetting(sProperty,sValue);
	}

	//--------------------------------------------------------------------
	// Method: LRNIsAllHostSupportPresent
	//
 	// Purpose: Wrapper for HostLMS method IsAllHostSupportPresent.
	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsAllHostSupportPresent=function()
	{
		return m_oHostLMS.IsAllHostSupportPresent();
	}

	//--------------------------------------------------------------------
	// Method: LRNGetCommunityURL
	//
 	// Purpose: Gets the URL used by the Community link feature.
	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetCommunityURL=function()
	{
		var sURL = "";

		try
		{
			var oXML = LoadXMLFile(m_sManifestPath+COMMUNITY_LINK_FILE);
			sURL = oXML.selectSingleNode("Community/URL").text;
		}
		catch(e)
		{
		}

		return sURL;
	}

	//--------------------------------------------------------------------
	// Method: LRNIsLcdsContent
	//
 	// Purpose: Wrapper for Viewer method IsLcdsContent.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNIsLcdsContent=function()
	{
		return m_oViewer.IsLcdsContent();
	}

	//--------------------------------------------------------------------
	// Method: LRNInitialize
 	//
 	// Purpose: Initialization method for the client LMSAPI object.  This
 	//	    function loads the persisted XML from userdata.  If this
	//	    method returns false, only the LMS error methods may be
	//	    called on the LMSAPI object.
  	//
 	// Parameters:
	//	String sManifestPath [in] - Manifest path
	//	Object oContentFrame [in] - Content iframe
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNInitialize=function(sManifestPath,oContentFrame)
	{
		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		if (!m_bInitialized)
		{
			try
			{
				if ("string" != typeof(sManifestPath) || "object" != typeof(oContentFrame))
					throw new LRNError(201);

				// Get persisted viewer settings	
				LoadViewerStateXML();

				// Try to get the most recently used manifest path, if an empty string was passed-in
				if (!sManifestPath) sManifestPath = this.LRNGetViewerStateValue("LastManifestPath");

				m_sManifestPath = sManifestPath;

				// Determine whether to use a collection manifest, if so change the default file name
				if (this.LRNGetViewerSetting("CollectionManifest") == "1")
					m_sManifestFileName = COLLECTION_MANIFEST_FILE;

				// Load XML from userdata
				LoadPersXML(sManifestPath+m_sManifestFileName);
				
				// Initialize the Viewer object
				m_oViewer.Init(sManifestPath+m_sManifestFileName,this,oContentFrame,m_oHostLMS);

				// Cache the manifest path in case the viewer is refreshed or launched directly
				this.LRNSetViewerStateValue("LastManifestPath",sManifestPath);

				
				m_bInitialized = true;
			}
			catch(e)
			{
				if (!e.sDetailsType)
					// Return a general error, because we don't know exactly what happened.
					m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
				else
					m_oLastError = e;
			}
		}

		return m_bInitialized;
	}

	//--------------------------------------------------------------------
	// Method: LRNTerminate
 	//
 	// Purpose: Performs tasks that need to be done prior to exit, which
	//	    includes committing XML to userdata.
  	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNTerminate=function()
	{
		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{
			// Save the starting location for next time
			var sID = this.LRNGetCurrentID();
			this.LRNSetScopedValue("lrn.startlocation",sID,"",true);
			var sCourseID = this.LRNResolveItemIDToCourseID(sID);
			if (sCourseID) this.LRNSetScopedValue("lrn.startcourse",sCourseID,"",true);
			
			// Allow the Viewer object to perform any operations before we exit
			m_oViewer.Finish();

			SavePersXML();	

			SaveViewerStateXML();

			if (m_oMsgQ) m_oMsgQ.FlushMessages();
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		m_bInitialized = false;
	}

	//--------------------------------------------------------------------
	// Method: LRNGetScopedValue
 	//
 	// Purpose: Retrieves the value of the named personalization property
	//	    for the named scope.  If the property exists, its value is
	//	    returned as a string.  Otherwise, an empty string is returned.
  	//
 	// Parameters:
	//	String  sProperty [in] - The name of the property to retrieve.
	//	String  sScope	  [in] - The name of the scope of the property to retrieve.
	//	Boolean bLMS	  [in] - Only LMS is allowed to use this parameter
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetScopedValue=function(sProperty,sScope,bLMS)
	{
		var sValue = "";
		var sOrigProperty = sProperty;

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{
			// Parameter and Object State Check		
			if (!sProperty || "string" != typeof(sProperty) || "string" != typeof(sScope))
				throw new LRNError(201);
		
			// Make sure we are initialized
			if (!(m_bInitialized || m_oPersXML))
				throw new LRNError(301);

			// Test for "cmi.interactions.n.result" where n is a numeric value.
			// We need to temporarily replace sProperty with "cmi.interactions.n.result"
			// in order to pass the following tests.
			if ("cmi.interactions" == sProperty.substring(0,16))
			{
				var aProp = sProperty.split(".");
				if (aProp.length == 4 && !isNaN(aProp[2]) && aProp[3] == "result")
					sProperty = "cmi.interactions.n.result";
			}
		
			// Check if property is part of the Datamodel
			if (!m_oLMSDataModel.IsInDataModel(sProperty))
				throw new LRNError(201);

			// If not implemented, throw an error
			if (!m_oLMSDataModel.IsImplemented(sProperty))
			 	throw new LRNError(401);
			 
			// The LMS is allowed to read from any datamodel properties; If not read access, throw an error
			if (!bLMS && !m_oLMSDataModel.IsAllowRead(sProperty))
				throw new LRNError(404);

			// If the object is not item scope, set the scope to an empty string.
			if (!m_oLMSDataModel.IsItemScoped(sProperty)) sScope = "";

			sValue = InternalGetValue(sOrigProperty,sScope);
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		return sValue;
	}

	//--------------------------------------------------------------------
	// Method: LRNSetScopedValue
 	//
 	// Purpose: Sets the value of the specified property for the named scope.
	//	    The change is not commited to the data store until LMSCommit
  	//	    or LMSFinish is called.
	//
 	// Parameters:
	//	String  sProperty [in] - The name of the property to modify.
	//	String  sValue	  [in] - The value to set.  This must be a number,
	//				 a boolean, or a string.  We will convert
	//				 the value to a string before setting it.
	//	String  sScope	  [in] - The name of the scope for this property
	//	Boolean bLMS	  [in] - Only LMS is allowed to use this parameter
	//
	// Returns: String ("true" or "false")
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetScopedValue=function(sProperty,sValue,sScope,bLMS)
	{
		var sRetVal = "false";
		var sOrigProperty = sProperty;

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{		
			if ("number" == typeof(sValue) || "boolean" == typeof(sValue))
				sValue = sValue.toString();

			if (!sProperty || "string" != typeof(sProperty) || 
			    "string" != typeof(sValue) 	|| "string" != typeof(sScope))
				throw new LRNError(201);
			
			// Make sure we are initialized
			if (!m_bInitialized || !m_oPersXML)
				throw new LRNError(301);

			// Test for "cmi.interactions.n.result" where n is a numeric value.
			// We need to temporarily replace sProperty with "cmi.interactions.n.result"
			// in order to pass the following tests.
			if ("cmi.interactions" == sProperty.substring(0,16))
			{
				var aProp = sProperty.split(".");
				if (aProp.length == 4 && !isNaN(aProp[2]) && aProp[3] == "result")
					sProperty = "cmi.interactions.n.result";
			}
			
        		// Check if property is part of the Datamodel
			if (!m_oLMSDataModel.IsInDataModel(sProperty))
				throw new LRNError(201);

			// If not implemented, throw an error
			if (!m_oLMSDataModel.IsImplemented(sProperty))
				throw new LRNError(401);
			
			// Cannot set data model keyword
			if (m_oLMSDataModel.IsKeyword(sProperty))
				throw new LRNError(402);
					
			// The LMS is allowed to write to any datamodel properties; If not write access, throw an error
			if (!bLMS && !m_oLMSDataModel.IsAllowWrite(sProperty))
				throw new LRNError(403);
			
			// Verify that value is a valid vocabulary item
			if (!m_oLMSDataModel.IsValidVocabulary(sProperty,sValue))
				throw new LRNError(201);
			
			// If the object is not item scope, set the scope to an empty string
			if (!m_oLMSDataModel.IsItemScoped(sProperty)) sScope = "";
			
			// If necessary, restore the original property after all checks have been performed.
			if (sProperty != sOrigProperty) sProperty = sOrigProperty;

			// Set the value
			InternalSetValue(sProperty,sValue,sScope);

			switch (sProperty)
			{
				case "cmi.core.session_time":
					// Update 'cmi.core.total_time' if the property is 'cmi.core.session_time'
					var sSessionTime = sValue;
					var sTotalTime = this.LRNGetScopedValue("cmi.core.total_time",sScope,true);
					var oTimer = new Timer();
					var sNewTime = oTimer.AddTime(sTotalTime,sSessionTime);

					if ("false" == this.LRNSetScopedValue("cmi.core.total_time",sNewTime,sScope,true))
						throw m_oLastError;
					break;
				case "cmi.core.lesson_status":
					this.LRNRaiseEvent(EVENT_LESSONSTATUS,sScope);
					break;
				case "cmi.core.exit":
					this.LRNRaiseEvent(EVENT_EXITSTATUS,sScope);
					break;
			}
					
			sRetVal = "true";
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		return sRetVal;
	}

	//--------------------------------------------------------------------
	// Method: LRNGetViewerStateValue
 	//
 	// Purpose: Gets the value of the specified viewer state property.
 	//
 	// Parameters:
	//	String sProperty [in] - Viewer property
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetViewerStateValue=function(sProperty)
	{
		var sValue = "";

		if (sProperty && "string" == typeof(sProperty))
		{
			try
			{
				sValue = m_oViewerStateXML.selectSingleNode("/ROOT/" + sProperty + "/@value").text;
			}
			catch(e)
			{
			}
		}

		return sValue;
	}

	//--------------------------------------------------------------------
	// Method: LRNSetViewerStateValue
 	//
 	// Purpose: Sets the value of the specified viewer state property.
 	//
 	// Parameters:
	//	String sProperty [in] - Viewer property
	//	String sValue	 [in] - Property value
	//
	// Returns: None
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNSetViewerStateValue=function(sProperty,sValue)
	{
		if (sProperty && "string" == typeof(sProperty) && sValue)
		{
			try
			{	
				var oRow = m_oViewerStateXML.selectSingleNode("/ROOT/" + sProperty);

				if (!oRow) // Adding an element and its attribute
				{
					var oNewRow = m_oViewerStateXML.createElement(sProperty);

					var oProperty = m_oViewerStateXML.createAttribute("value");
					oProperty.value	= sValue;
					oNewRow.attributes.setNamedItem(oProperty);

					// Add the new row to Pers XML 
					m_oViewerStateXML.documentElement.appendChild(oNewRow);
				}
				else	   // Adding/Updating an attribute
				{
					var oValue = oRow.attributes.getNamedItem("value");

					// Change the value
					if (oValue.value != sValue) oValue.value = sValue;
				}
			}
			catch(e)
			{
			}
		}
	}

	//--------------------------------------------------------------------
	// Method: LRNGetViewerSetting
 	//
 	// Purpose: Gets the Value of the sprecified item from settings.xml.
 	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LRNGetViewerSetting=function(sProperty)
	{
		var sValue = "";

		if (sProperty && "string" == typeof(sProperty))
		{
			try
			{
				if (this.LRNIsViewerHosted())
					sValue = m_oHostLMS.GetViewerSetting(sProperty);

				if (!sValue)
					sValue = m_oViewerSettings.selectSingleNode("Settings/" + sProperty + "/@value").text;
				
			}
			catch(e)
			{
			}
		}

		return sValue;
	}

	//--------------------------------------------------------------------
	// Method: LMSInitialize
 	//
 	// Purpose: SCORM API which initializes communication between the LMS
	//	    and personalization client (SCO).  The real work of initializing
	//	    the LMSAPI object is done in LRNInitialize.
  	//
 	// Parameters:
	//	String sProperty [in] - An empty string must be passed for conformance
	//				to the current SCORM standard.
	//
	// Returns: String ("true" or "false")
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSInitialize=function(sProperty)
	{
		var sRetVal = "false";

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{
			this.LRNSynchronize();					// Synch in case page load was due to search link

			var sItemID = this.LRNGetNewID();			// Get the newly selected item identifier

			InternalSetSco(sItemID);				// Save the current item ID

			if (this.LRNIsViewerHosted())
			{
				sRetVal = m_oHostLMS.API.LMSInitialize(sProperty);
			}
			else
			{
				if (sProperty != "") throw new LRNError(201);

    				// Check to see if LMSInitialize has already been called
				if (m_bLMSInitialized) throw new LRNError(101);
		
				// Checking object state 
				if (!m_bInitialized) throw new LRNError(301);

				sRetVal = "true";
			}

			m_bLMSInitialized = (sRetVal == "true");
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		return sRetVal;
	}

	//--------------------------------------------------------------------
	// Method: LMSFinish
 	//
 	// Purpose: SCORM API which ends communication between the LMS and personalization
	//	    client (SCO).  Saves the current set of personalization data to
 	//	    the personalization store and terminates the connection to the
	//	    personalization store.  This function just resets the LastError
	//	    object to null.  Data is not committed to the server 
	//	    until LRNTerminate is called.
  	//
 	// Parameters:
	//	String sProperty [in] - An empty string must be passed for conformance
	//				to the current SCORM standard.
	//
	// Returns: String ("true" or "false")
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSFinish=function(sProperty)
	{
		var sRetVal = "false";

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{
			if (this.LRNIsViewerHosted())
			{
				sRetVal = m_oHostLMS.API.LMSFinish(sProperty);
			}
			else
			{
				if (sProperty != "") throw new LRNError(201);

				if (!m_bLMSInitialized) throw new LRNError(301);
	
				sRetVal = "true";
			}
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		m_bLMSInitialized = false;

		return sRetVal;
	}

	//--------------------------------------------------------------------
	// Method: LMSGetValue
 	//
 	// Purpose: SCORM API which retrieves the value of the named personalization
 	//	    property. If the property exists, its value is returned as a string.
  	//	    Otherwise, an empty string is returned.
	//
 	// Parameters:
	//	String sProperty [in] - The name of the property to retrieve.
	//
	// Returns: String
	//
	// Note: The current LRN implementation of the LMS API does not support datamodels
	//	 or the _children, _count, or _version keywords.
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSGetValue=function(sProperty)
	{
		var sValue = "";

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{
			if (this.LRNIsViewerHosted())
			{
				sValue = m_oHostLMS.API.LMSGetValue(sProperty);
			}
			else
			{
				if (!m_bLMSInitialized) throw new LRNError(301);

				// Set the scope to the current ID
				var sScope = m_sCurrentScoID;
		
				sValue = this.LRNGetScopedValue(sProperty,sScope,false);
			}
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		return sValue;
	}

	//--------------------------------------------------------------------
	// Method: LMSSetValue
 	//
 	// Purpose: SCORM API which sets the value of the named personalization
	//	    property.  The change is not commited to the store until
 	//	    either LMSCommit or LMSFinish is called.
  	//
 	// Parameters:
	//	String sProperty [in] - The name of the property to modify.
	//	String sValue	 [in] - The value to set.  This must be a number, a boolean, or a string.  We will convert the value to a string before setting it.
	//
	// Returns: String ("true" or "false")
	//
	// Note: The current LRN implementation of the LMS API does not support datamodels
	//	 or the _children, _count, or _version keywords.
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSSetValue=function(sProperty,sValue)
	{
		var sRetVal = "false";

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{		
			if (this.LRNIsViewerHosted())
			{
				sRetVal = m_oHostLMS.API.LMSSetValue(sProperty,sValue);

				if (sProperty == "cmi.core.lesson_status")
				{
					var sItemID = this.LRNGetCurrentID();			// Get the current SCO identifier
					var sScoID = m_oHostLMS.GetScoIdFromItemId(sItemID);	// Map from Manifest item ID to Host LMS' SCO ID
					var bComplete = (sValue == "completed");
					if (m_oHostLMS.SetComplete) m_oHostLMS.SetComplete(sScoID, bComplete);
				}
			}
			else
			{
				if (!m_bLMSInitialized) throw new LRNError(301);

				sRetVal = this.LRNSetScopedValue(sProperty,sValue,m_sCurrentScoID,false);
			}

			if (sProperty == "cmi.core.lesson_status")
				RollupCurrentNodeLessonStatus();
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		return sRetVal;
	}

	//--------------------------------------------------------------------
	// Method: LMSCommit
 	//
 	// Purpose: SCORM API which saves the current set of personalization data
	//	    to the personalization store.  We don't actually save anything
	//	    until LRNTerminate is called.
  	//
 	// Parameters:
	//	String sProperty [in] - An empty string must be passed for conformance
	//				to the current SCORM standard.
	//
	// Returns: String ("true" or "false")
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSCommit=function(sProperty)
	{
		var sRetVal = "false";

		// Reset the API's last error so the current call starts clean.
		m_oLastError = null;

		try
		{
			if (this.LRNIsViewerHosted())
			{
				sRetVal = m_oHostLMS.API.LMSCommit(sProperty);
			}
			else
			{
				if (sProperty != "") throw new LRNError(201);

				if (!m_bLMSInitialized) throw new LRNError(301);

				sRetVal = "true";
			}
		}
		catch(e)
		{
			if (!e.sDetailsType)
				// Return a general error, because we don't know exactly what happened.
				m_oLastError = new LRNError(101,ERROR_DETAILS_SCRIPT,e);
			else
				m_oLastError = e;
		}

		return sRetVal;
	}

	//--------------------------------------------------------------------
	// Method: LMSGetLastError
 	//
 	// Purpose: SCORM API which returns the ID of the last error to occur.
 	//	    A return value of zero indicates that the last operation
	//	    was successful.  Multiple consecutive calls to this method
	//	    will return the same value.
  	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSGetLastError=function()
	{
		if (this.LRNIsViewerHosted())
		{
			return m_oHostLMS.API.LMSGetLastError();
		}
		else
		{
			return (m_oLastError ? m_oLastError.number.toString() : "0");
		}
	}

	//--------------------------------------------------------------------
	// Method: LMSGetErrorString
 	//
 	// Purpose: SCORM API which returns the string associated with a 
	//	    specific error ID.
  	//
 	// Parameters:
	//	String sErrorNumber [in] - Error number supplied as a string
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSGetErrorString=function(sErrorNumber)
	{
		if (this.LRNIsViewerHosted())
		{
			return m_oHostLMS.API.LMSGetErrorString(sErrorNumber);
		}
		else
		{
			return ErrorStringFromID(sErrorNumber);
		}
	}

	//--------------------------------------------------------------------
	// Method: LMSGetDiagnostic
 	//
 	// Purpose: SCORM API which returns additional information regarding a
 	//	    specific error.  If an empty string is passed-in, the
	//	    disgnostic will be with regard to the last error to occur.
	//	    Otherwise, a diagnostic will be returned for the specified
	//	    error number.
	//
 	// Parameters:
	//	String sErrorNumber [in] - Error number supplied as a string
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSAPI.prototype.LMSGetDiagnostic=function(sErrorNumber)
	{
		var sDiag = "";

		try
		{
			if (this.LRNIsViewerHosted())
			{
				sDiag = m_oHostLMS.API.LMSGetDiagnostic(sErrorNumber);
			}
			else if ("string" == typeof(sErrorNumber))
			{
				// Get additional info on the last error that occurred
				if (sErrorNumber == "")
				{
					switch (m_oLastError.sDetailsType)
					{
						case ERROR_DETAILS_PARSE:
							sDiag = m_oLastError.oErrorDetails.reason;
						case ERROR_DETAILS_SCRIPT:
							sDiag = m_oLastError.oErrorDetails.description;
						case ERROR_DETAILS_MULTIPLE:
							sDiag = m_oLastError.oErrorDetails.xml;
						case ERROR_DETAILS_STRING:
							sDiag = m_oLastError.oErrorDetails;
					}
				}

				// Get additional info on the specified error code
				else if (!isNaN(sErrorNumber))
					sDiag = ErrorStringFromID(sErrorNumber);
			}
		}
		catch(e)
		{
		}

		return sDiag;
	}
	
	//--------------------------------------------------------------------
	// Method: getItemNodes
	//
 	// Purpose: get the course item nodes from manifest
 	//
 	// Parameters:
	//	
	//
	// Returns:  Array containing the item nodes information
	//--------------------------------------------------------------------
	LMSAPI.prototype.getItemNodes=function()
	{
	    return m_oViewer.getItemNodes();
	}

	//////////////////////////////////////////////////////////////////////
	// PRIVATE METHODS
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: RollupCurrentNodeLessonStatus
	//
 	// Purpose: Raises a lesson status event for the current node and all
	//	    of its ancestors so that TOC checkmarks are refreshed.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function RollupCurrentNodeLessonStatus()
	{
		var sNodeID = m_sCurrentScoID;

		while (sNodeID)
		{
			m_oLMSAPI.LRNRaiseEvent(EVENT_LESSONSTATUS,sNodeID);
			sNodeID = m_oLMSAPI.LRNGetParentID(sNodeID);
		}
	}


	//--------------------------------------------------------------------
	// Method: LoadPersXML
 	//
 	// Purpose: Attempts to retrieve the set of personalization data
 	//	    from the client side personalization store.
  	//
 	// Parameters:
	//	String sManifestURL [in] - Manifest location
	//
	// Returns: None
	//
	// Note: Function can throw
	//--------------------------------------------------------------------
	function LoadPersXML(sManifestURL)
	{
		// If we are in preview mode, we do not need to R/W userData
		if (!m_bPreview)
		{
			// Get the pers XML from user data
			var sPersXML = LoadFromUserData(m_oUserData, USER_DATA_PREFIX + m_sIdentifier, PERS_XML_ATTRIBUTE);

			// Load the XML into the DOM
			m_oPersXML = sPersXML ? LoadXML(sPersXML) : m_oBasePersXML.cloneNode(true);
			
			// If we're online, we'll try to get our data from the server
			if (!IsFileProtocol() && !m_oHostLMS && !m_oHostLMS.API) LoadUserDataFromServer(sManifestURL);
		}
	}

	//--------------------------------------------------------------------
	// Method: SavePersXML
 	//
 	// Purpose: Saves the current set of personalization data to the client side
 	//	    personalization store.
  	//
 	// Parameters: None
	//
	// Returns: None
	//
	// Note: Function can throw
	//--------------------------------------------------------------------
	function SavePersXML()
	{
		// If we are in preview mode, we do not need to R/W userData
		if (!m_bPreview && m_bNeedSave)
		{
			// If we're online, we'll try to save our runtime data to the server
			if (!IsFileProtocol() && !m_oHostLMS && !m_oHostLMS.API) SaveUserDataToServer();
			
			// Persist XML using the IE userdata behavior
			SaveToUserData(m_oUserData, USER_DATA_PREFIX + m_sIdentifier, PERS_XML_ATTRIBUTE, m_oPersXML.xml);
		}

		m_bNeedSave = false;
	}

	//--------------------------------------------------------------------
	// Method: LoadViewerStateXML
 	//
 	// Purpose: Loads the viewer state XML from userdata.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function LoadViewerStateXML()
	{
		// Get the state XML from user data
		var sStateXML = LoadFromUserData(g_oViewerState, VIEWER_STATE_PREFIX + m_sIdentifier, PERS_XML_ATTRIBUTE);

		// Load the XML into the DOM
		m_oViewerStateXML = sStateXML ? LoadXML(sStateXML) : m_oBasePersXML.cloneNode(true);
	}

	//--------------------------------------------------------------------
	// Method: SaveViewerStateXML
 	//
 	// Purpose: Saves the viewer state XML to userdata.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function SaveViewerStateXML()
	{
		SaveToUserData(g_oViewerState, VIEWER_STATE_PREFIX + m_sIdentifier, PERS_XML_ATTRIBUTE, m_oViewerStateXML.xml);
	}

	//--------------------------------------------------------------------
	// Method: InternalSetSco
	//
	// Purpose : Sets the current SCO.
	//
	// Parameters:
	// 	String sItemID [in] - ID
	//
	// Returns: None
	//--------------------------------------------------------------------
	function InternalSetSco(sItemID)
	{
		if (m_oLMSAPI.LRNIsViewerHosted())
		{
			var sScoID = m_oHostLMS.GetScoIdFromItemId(sItemID); // Map from Manifest item ID to Hosting LMS' SCO ID
			m_oHostLMS.SetSco(sScoID);
		}

		m_sCurrentScoID = sItemID;
	}

	//--------------------------------------------------------------------
	// Method: InternalGetNotes
	//
	// Purpose : Returns all notes.
	//
	// Parameters: None
	//
	// Returns: Array
	//--------------------------------------------------------------------
	function InternalGetNotes()
	{
		var sPattern = "/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "=\'" + "cmi.comments" + "\']";
		var oNodes;
		var oNode = null;
		var aNotes;
		var aIndex = "";
		
		oNodes = m_oPersXML.selectNodes(sPattern);

		if (oNodes.length > 0) aNotes = new Array();

		while (oNode = oNodes.nextNode())
		{
			aNotes[oNode.getAttribute(SCOPE_PROP_NAME)] = oNode.getAttribute(VALUE_PROP_NAME);
		}
		
		return aNotes;
	}

	//--------------------------------------------------------------------
	// Method: InternalHasNotes
	//
	// Purpose : Determines whether any notes exist.
	//
	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	function InternalHasNotes()
	{
		var sPattern = "/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "=\'" + "cmi.comments" + "\']";
		var oNodes = m_oPersXML.selectNodes(sPattern);

		return (oNodes.length > 0);
	}

	//--------------------------------------------------------------------
	// Method: InternalGetNotesCount
	//
	// Purpose : Gets the number of notes that exist.
	//
	// Parameters: None
	//
	// Returns: Integer
	//--------------------------------------------------------------------
	function InternalGetNotesCount()
	{
		var aNotes = m_oLMSAPI.LRNGetNotes();
		var nCount = 0;
		
		for (oNote in aNotes)
		{
			if (m_oLMSAPI.LRNIsLcdsContent())
			{
				var aTopicNotes = aNotes[oNote].split(LCDS_NOTES_DELIMITER);
				
				for (var i = 0; i < aTopicNotes.length; i++)
					if (aTopicNotes[i] != "") nCount++;
			}
			else
			{
				nCount++;
			}
		}

		return nCount;
	}

	//--------------------------------------------------------------------
	// Method: InternalGetValue
 	//
 	// Purpose: Used for getting a value from Pers XML or Admin Pers XML
 	//	    If the property exists, its value is returned as a string.
	//	    Otherwise, "" is returned.
  	//
 	// Parameters:
	//	String sProperty [in] - Property
	//	String sScope    [in] - Scope
	//
	// Returns: String
	//--------------------------------------------------------------------
	function InternalGetValue(sProperty,sScope)
	{
		var sValue = "";

		try
		{
			// Getting a property value from xml
			var sPattern = "/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "=\'" + sProperty +
					"\' and @" + SCOPE_PROP_NAME + "=\'" + sScope + "\']";

			var oProperty = m_oPersXML.selectSingleNode(sPattern);

			// If the value wasn't found in persisted data, get the property's default value
			if (!oProperty)
				sValue = m_oLMSDataModel.GetDefaultValue(sProperty);
			else if (oProperty.getAttribute(VALUE_PROP_NAME) != "null")
				sValue = oProperty.getAttribute(VALUE_PROP_NAME);
		}
		catch(e)
		{
		}

		return sValue;
	}

	//--------------------------------------------------------------------
	// Method: InternalSetValue
 	//
 	// Purpose: Finds the specified row in the internal XML, sets the Value
 	//	    attribute to sValue.
  	//
 	// Parameters:
	//	String sProperty [in] - Property
	//	String sValue    [in] - New Value
	//	String sScope    [in] - Scope
	//
	// Returns: None
	//
	// Note: Function can throw
	//--------------------------------------------------------------------
	function InternalSetValue(sProperty,sValue,sScope)
	{
		// Setting a property value to xml
		var sPattern = "/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "=\'" + sProperty +
			 	"\' and @" + SCOPE_PROP_NAME + "=\'" + sScope + "\']";

		var oRow = m_oPersXML.selectSingleNode(sPattern);

		if (!oRow)	// Adding a property
		{
			var oNewRow = m_oPersXML.createElement(PERS_ELEMENT_NAME);

			var oProperty = m_oPersXML.createAttribute(PROPERTY_PROP_NAME);
			oProperty.value	= sProperty;
			oNewRow.attributes.setNamedItem(oProperty);

			var oValue = m_oPersXML.createAttribute(VALUE_PROP_NAME);
			oValue.value = sValue;
			oNewRow.attributes.setNamedItem(oValue);

			var oScope = m_oPersXML.createAttribute(SCOPE_PROP_NAME);
			oScope.value = sScope;
			oNewRow.attributes.setNamedItem(oScope);

			if ("cmi.core.lesson_status" == sProperty || "cmi.core.total_time" == sProperty)
			{
				var oDate = m_oPersXML.createAttribute(DATE_PROP_NAME);
				oDate.value = GetCurrentDate();
				oNewRow.attributes.setNamedItem(oDate);

				var oTime = m_oPersXML.createAttribute(TIME_PROP_NAME);
				oTime.value = GetCurrentTime();					
				oNewRow.attributes.setNamedItem(oTime);

				if ("cmi.core.total_time" == sProperty)
				{
					var oAttempts = m_oPersXML.createAttribute(ATTEMPTS_PROP_NAME);
					oAttempts.value = 1;
					oNewRow.attributes.setNamedItem(oAttempts);
				}
			}
		
			// Add the new row to Pers XML 
			m_oPersXML.documentElement.appendChild(oNewRow);
		}
		else	// Updating a property
		{
			var oValue = oRow.attributes.getNamedItem(VALUE_PROP_NAME);
			if (oValue.value != sValue) oValue.value = sValue;
			
			if ("cmi.core.lesson_status" == sProperty || "cmi.core.total_time" == sProperty)
			{
				// Update the date stamp; create the attribute if it doesn't exist
				var oDate = oRow.attributes.getNamedItem(DATE_PROP_NAME);
				if (!oDate)
				{
					oDate = m_oPersXML.createAttribute(DATE_PROP_NAME);
					oDate.value = GetCurrentDate();
					oRow.attributes.setNamedItem(oDate);
				}
				else
					oDate.value = GetCurrentDate();

				// Update the time stamp; create the attribute if it doesn't exist
				var oTime = oRow.attributes.getNamedItem(TIME_PROP_NAME);
				if (!oTime)
				{
					oTime = m_oPersXML.createAttribute(TIME_PROP_NAME);
					oTime.value = GetCurrentTime();
					oRow.attributes.setNamedItem(oTime);
				}
				else
					oTime.value = GetCurrentTime();
			}

			if ("cmi.core.total_time" == sProperty)
			{
				// Update the number of attempts for the item; create the attribute if it doesn't exist
				var oAttempts = oRow.attributes.getNamedItem(ATTEMPTS_PROP_NAME);
				if (!oAttempts)
				{
					oAttempts = m_oPersXML.createAttribute(ATTEMPTS_PROP_NAME);
					oAttempts.value = 1;
					oRow.attributes.setNamedItem(oAttempts);
				}
				else
					oAttempts.value = parseInt(oAttempts.value) + 1;
			}

			if ("cmi.comments" == sProperty && sValue == "")
			{
				oRow.parentNode.removeChild(oRow);
			}
		}

		m_bNeedSave = true;
	}

	//--------------------------------------------------------------------
	// Method: LoadFromUserData
 	//
 	// Purpose: Loads the persisted XML from user data.
 	//
 	// Parameters:
	//	Object oUserData      [in] - UserData object
	//	String sStoreName     [in] - String that specifies the arbitrary name assigned to a persistent object within a UserData store.
	//	String sAttributeName [in] - String that specifies the name of the persistent attribute
	//
	// Returns: Variant. Returns a string, number, or Boolean, defined by strAttributeName.
	//	    If an explicit attribute doesn't exist, an empty string is returned.
	//	    If a custom attribute doesn't exist, null is returned.  See Docs on userData behavior.
	//	    On error, this function returns null.
	//--------------------------------------------------------------------
	function LoadFromUserData(oUserData,sStoreName,sAttributeName)
	{
		try
		{
	        if(window.ActiveXObject)
			{
			    oUserData.load(sStoreName);
			    return oUserData.getAttribute(sAttributeName);
			}
			else
			{
			    return localStorage.sAttributeName;
			}
		}
		catch(e)
		{
			return null;
		}
	}

	//--------------------------------------------------------------------
	// Method: LoadUserDataFromServer
 	//
 	// Purpose: Gets persisted data from the server.
 	//
 	// Parameters:
	//	String sManifestURL [in] - Manifest location
	//
	// Returns: None
	//--------------------------------------------------------------------
	function LoadUserDataFromServer(sManifestURL)
	{
		try
		{
			var sXML = "<ROOT>";

			var sID = InternalGetValue("cmi.core.student_id","");
			if (sID) sXML += "<Student " + VALUE_PROP_NAME + "='" + sID + "' />";

			sManifestURL = unescape(sManifestURL.toLowerCase());
			var sBaseURL = GetBaseURL();

			var nPos = sBaseURL.substring(0,sBaseURL.length-1).lastIndexOf("/");
			var sTmpURL = sBaseURL.substring(0,nPos+1);
			nPos = sManifestURL.indexOf(sTmpURL);

			var sRelPath = (nPos == -1) ? sManifestURL : sManifestURL.substring(sTmpURL.length,sManifestURL.length);
			sXML += "<ManifestPath " + VALUE_PROP_NAME + "='" + sRelPath + "' /></ROOT>"
				
			var oXmlHttp = new ActiveXObject("Msxml2.XMLHTTP." + GetXmlParserVersion());
			oXmlHttp.Open("POST",sBaseURL + "viewer/asp/LoadUserData.asp",false);
			oXmlHttp.Send(sXML);

			if (oXmlHttp.status == 200)	// Successful communication with server
			{
				var oXML = LoadXML(oXmlHttp.responseText);

				// Test for a server-side error
				var oError = oXML.selectSingleNode("/ROOT/" + ERROR_ELEMENT_NAME);

				// No server-side error occurred, so save the returned XML
				if (!oError)
				{
					m_oPersXML = oXML;
					m_bUsingSQLDB = true;
				}
			}
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: SaveToUserData
 	//
 	// Purpose: Persists XML to userdata.
 	//
 	// Parameters:
	//	Object oUserData      [in] - UserData object
	//	String sStoreName     [in] - String that specifies the arbitrary name assigned to a persistent object within a UserData store.
	//	String sAttributeName [in] - String that specifies the name of the persistent attribute
	//	String sValue         [in] - String to save to userData
	//
	// Returns: None
	//--------------------------------------------------------------------
	function SaveToUserData(oUserData,sStoreName,sAttributeName,sValue)
	{
		try
		{
			if(window.ActiveXObject)
			{
			    oUserData.setAttribute(sAttributeName,sValue);				
			    oUserData.save(sStoreName);
			}
			else
			{
			    localStorage.sAttributeName = sValue;
			}
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: SaveUserDataToServer
 	//
 	// Purpose: Sends runtime XML to the server.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function SaveUserDataToServer()
	{
		try
		{
			// Get the student ID and manifest ID.
			var sXML = "<ROOT>" + m_oPersXML.selectSingleNode("/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "='cmi.core.student_id']").xml;
			sXML += m_oPersXML.selectSingleNode("/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "='lrn.manifestID']").xml;

			// Get the nodes where the property attribute equals 'cmi.core.lesson_status' and where a date attribute exists.
			// Records without a date/time stamp were originally sent from the server and haven't been touched during
			// the current session, so we'll exclude them from the data set we send.
			var oStatusNodes = m_oPersXML.selectNodes("/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "='cmi.core.lesson_status' and @" + DATE_PROP_NAME +"]");
			var oNode = null;
			for(ic = 0; ic < oStatusNodes.length; ic++)
			{
			    oNode = oStatusNodes[ic];
				sXML += oNode.xml;
			}
			// Get the nodes where the property attribute equals 'cmi.core.total_time'.  These were collected
			// during the current session.
			var oTimeNodes = m_oPersXML.selectNodes("/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "='cmi.core.total_time']");
			for(ic = 0; ic < oStatusNodes.length; ic++)
			{
			    oNode = oStatusNodes[ic];
				sXML += oNode.xml;
			}
			// Get the nodes where the property attribute equals "cmi.interactions.n.result".  These are
			// assessment-related records.
			var oQuizNodes = m_oPersXML.selectNodes("/ROOT/" + PERS_ELEMENT_NAME + "[contains(@" + PROPERTY_PROP_NAME + ",'cmi.interactions.') and contains(@" + PROPERTY_PROP_NAME + ",'.result')]");
			for(ic = 0; ic < oStatusNodes.length; ic++)
			{
			    oNode = oStatusNodes[ic];
				sXML += oNode.xml;
			}
			sXML += "</ROOT>";

			var oXmlHttp = new ActiveXObject("Msxml2.XMLHTTP." + GetXmlParserVersion());
			var sURL = GetBaseURL() + "viewer/asp/SaveUserData.asp";
			oXmlHttp.Open("POST",sURL,false);
			oXmlHttp.Send(sXML);

			if (oXmlHttp.status == 200)	// Successful communication with server
			{
				var oXML = LoadXML(oXmlHttp.responseText);

				// Test for a server-side error
				var oError = oXML.selectSingleNode("/ROOT/" + ERROR_ELEMENT_NAME);
				if (!oError)
				{
					// Check whether a new student ID was created.  If not, get the persisted student ID.
					var oNode = oXML.selectSingleNode("/ROOT/" + PERS_ELEMENT_NAME + "[@" + PROPERTY_PROP_NAME + "='cmi.core.student_id']");
					var sID = oNode ? oNode.getAttribute(VALUE_PROP_NAME) : InternalGetValue("cmi.core.student_id","");

					// No server-side errors were reported, so clear all records from the
					// persisted XML except for the student ID.
					m_oPersXML = m_oBasePersXML.cloneNode(true);
					InternalSetValue("cmi.core.student_id",sID,"");
				}
			}
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: GetCurrentDate
 	//
 	// Purpose: Gets the current date in the format "YY.MM.DD".
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	function GetCurrentDate()
	{
		try
		{
			var d = new Date();
			var nYear = d.getFullYear();
			var nMonth = d.getMonth() + 1;		// January == 0
			var nDay = d.getDate();

			var sYear = nYear.toString();
		
			var sMonth;
			if (10 > nMonth)
				sMonth = "0" + nMonth.toString();
			else
				sMonth = nMonth.toString();

			var sDay;
			if (10 > nDay)
				sDay = "0" + nDay.toString();
			else
				sDay = nDay.toString();

			return sYear + "." + sMonth + "." + sDay;
		}
		catch(e)
		{
			return "";
		}
	}

	//--------------------------------------------------------------------
	// Method: GetCurrentTime
 	//
 	// Purpose: Gets the current time in the format "HH.MM.SS".
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	function GetCurrentTime()
	{
		try
		{
			var d = new Date();
			var nHours = d.getHours();
			var nMinutes = d.getMinutes();
			var nSeconds = d.getSeconds();

			var sHours = nHours.toString();

			var sMinutes;
			if (10 > nMinutes)
				sMinutes = "0" + nMinutes.toString();  // single digit
			else
				sMinutes = nMinutes.toString();
		
			var sSeconds;
			if (10 > nSeconds)
				sSeconds = "0" + nSeconds.toString();  // single digit
			else
				sSeconds = nSeconds.toString();
		
			return sHours + ":" + sMinutes + ":" + sSeconds;
		}
		catch(e)
		{
			return "";
		}
	}

	//--------------------------------------------------------------------
	// Method: GetPersistenceIdentifier
 	//
 	// Purpose: Constructs an identifier string from the viewer URL.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	function GetPersistenceIdentifier()
	{
		// Use the full URL to allow each collection to maintain a separate data store (fixes bug #3487)	
		return window.location.href.split(":").join("&").split("/").join("&").split(".").join("&").split("?").join("&");
	}

	//--------------------------------------------------------------------
	// Method: IsFileProtocol
 	//
 	// Purpose: Indicates whether file protocol is currently in use.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	function IsFileProtocol()
	{
		return (window.location.protocol == "file:" ? true : false);
	}
}

//////////////////////////////////////////////////////////////////////////////
// Class: LMSDataModel
//
// Purpose: Constructor function for LMSDataModel objects.  This class provides
//	    methods which can be used to determine whether the data model supports
//	    a given property, obtain the default value of a property, etc.
//
// Parameters: None
//
// Returns: Object OR null
//
// Note: Can throw during instantiation
//////////////////////////////////////////////////////////////////////////////
function LMSDataModel()
{
	//////////////////////////////////////////////////////////////////////
	// PRIVATE VARIABLES
	//////////////////////////////////////////////////////////////////////

	var m_oXMLInfo = LoadXMLFile(LMS_DATA_MODEL_FILE);
	
	//////////////////////////////////////////////////////////////////////
	// PUBLIC METHODS
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: IsInDataModel
 	//
 	// Purpose: Determines whether the specified property is part of the
 	//	    data model.
  	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsInDataModel=function(sProperty)
	{
		var bRet = false;

		try
		{
			if (m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']")) bRet = true;
		}
		catch(e)
		{
		}

		return bRet;
	}

	//--------------------------------------------------------------------
	// Method: IsImplemented
 	//
 	// Purpose: Determines whether the specified property is implemented
 	//	    as part of the data model.
  	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsImplemented=function(sProperty)
	{
		var bRet = false;

		try
		{
			if ("true" == m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']").getAttribute("Implemented")) bRet = true;
		}
		catch(e)
		{
		}

		return bRet;
	}

	//--------------------------------------------------------------------
	// Method: IsKeyword
 	//
 	// Purpose: Determines whether the specified property is a key word.
 	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsKeyword=function(sProperty)
	{
		var bRet = false;

		try
		{
			if ("true" == m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']").getAttribute("IsKeyword")) bRet = true;
		}
		catch(e)
		{
		}

		return bRet;
	}

	//--------------------------------------------------------------------
	// Method: GetDefaultValue
 	//
 	// Purpose: Gets the default value of the specified property.
 	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: String
	//--------------------------------------------------------------------
	LMSDataModel.prototype.GetDefaultValue=function(sProperty)
	{
		var sValue = "";

		try
		{
			sValue = m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']").getAttribute("DefaultValue");
		}
		catch(e)
		{
		}

		return sValue;
	}

	//--------------------------------------------------------------------
	// Method: IsAllowRead
 	//
 	// Purpose: Determines whether the specified property can be read from.
 	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsAllowRead=function(sProperty)
	{
		var bRet = false;

		try
		{
			if ("true" == m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']").getAttribute("Read")) bRet = true;
		}
		catch(e)
		{
		}

		return bRet;
	}

	//--------------------------------------------------------------------
	// Method: IsAllowWrite
 	//
 	// Purpose: Determines whether the specified property can be written to.
 	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsAllowWrite=function(sProperty)
	{
		var bRet = false;

		try
		{
			if ("true" == m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']").getAttribute("Write")) bRet = true;
		}
		catch(e)
		{
		}

		return bRet;
	}

	//--------------------------------------------------------------------
	// Method: IsValidVocabulary
 	//
 	// Purpose: Determines whether the specified item is a valid vocabulary
 	//	    member for the given property.
  	//
 	// Parameters:
	//	String sProperty [in] - Property
	//	String sItem	 [in] - Vocabulary item
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsValidVocabulary=function(sProperty,sItem)
	{
		var bRet = true;

		try
		{
			var sPattern = "/ROOT/LMSDataModel[@Property='" + sProperty + "']";

			if ("true" == m_oXMLInfo.selectSingleNode(sPattern).getAttribute("IsControlledVocabulary"))
			{
				if (sItem)
					sPattern += "/LMSVocabulary[text()='" + sItem + "']";
				else
					// Vocabulary might allow an empty string
					// (ie. "ab-initio","resume" and "" for 'cmi.core.entry')
					sPattern += "/LMSVocabulary[not(text())]";

				if (!m_oXMLInfo.selectSingleNode(sPattern)) bRet = false;
			}
		}
		catch(e)
		{
		}

		return bRet;
	}

	//--------------------------------------------------------------------
	// Method: IsItemScoped
 	//
 	// Purpose: Determines whether the scope of the specified data model
 	//	    property is "item".
  	//
 	// Parameters:
	//	String sProperty [in] - Property
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	LMSDataModel.prototype.IsItemScoped=function(sProperty)
	{
		var bRet = false;

		try
		{
			if ("item" == m_oXMLInfo.selectSingleNode("/ROOT/LMSDataModel[@Property='" + sProperty + "']").getAttribute("Scope")) bRet = true;
		}
		catch(e)
		{
		}

		return bRet;
	}
}
if(!window.ActiveXObject)
{
    Node.prototype.__defineGetter__("text", function () {
            return this.textContent;    
    });
}
//////////////////////////////////////////////////////////////////////////////
// Class: Viewer
//
// Purpose: Constructor function for Viewer objects.  This class provides a
//	    layer of abstraction to exposes methods
//	    which can be used load, traverse, and obtain various information
//	    from one or more manifests.
//
// Parameters: None
//
// Returns: Object OR null
//
// Note: Can throw during instantiation
//////////////////////////////////////////////////////////////////////////////
function Viewer() 
{
	//////////////////////////////////////////////////////////////////////
	// PRIVATE CONSTANTS
	//////////////////////////////////////////////////////////////////////

	// Selection type enumerations used when calling method TransformFromIMStoMLT.
	var SELECT_SINGLEITEM = 0;
	var SELECT_ITEMCHILDREN = 1;
	var SELECT_DESCENDANTS = 2;
	var SELECT_TOCS = 3;
	var SELECT_TOC = 4;

	var LCDS_LESSON_MANIFEST_FILENAME_PREFIX = "lessonmanifest";

 	//////////////////////////////////////////////////////////////////////
	// PRIVATE VARIABLES
	//////////////////////////////////////////////////////////////////////
		
	var m_oManifest = null;		// XMLDOM representation of manifest
	var m_oXSLCache = null;		// XSL template cache

	var m_sCurrentNodeID = "";	// Current node identifier
	var m_oTimer = null;		// Timer object used for updating session/total time
	var m_sCurrentOrgID = "";	// Current manifest organization identifier
	var m_oLMSAPI = null;		// LMS API 
	
	var m_bIsReady = false;		// Flag to indicate initialization state
	var m_bIsCollection = false;	// Flag to indicate whether mpimanifest.xml is used
		
	var m_oContentFrame = null;	// Content frame reference
	var m_sNewHref = "";		// Newly selected node URL
	var m_nNewEvent = -1;		// New viewer event to raise
	var m_sNewID = "";		// New node identifier
	var m_bLoadingContent = false;	// Flag to indicate that new content is loading

	var m_bIsHostOLP = false;

	var m_bIsLcdsContent = false;	// Flag to indicate whether content was authored via LCDS

	var m_oLoadingInterval = 0;	// Used with window.setInterval() when loading content

	//////////////////////////////////////////////////////////////////////
	// PUBLIC METHODS
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: Init
	//
 	// Purpose: Initializes the object prior to use.
 	//
 	// Parameters:
	//	String sManifestPath [in] - Fully qualified mainfest path
	//	Object oLMSAPI	     [in] - Pointer to LMSAPI object
	//
	// Returns: None
	//
	// Note: Function can throw
	//--------------------------------------------------------------------
	Viewer.prototype.Init=function(sManifestPath,oLMSAPI,oContentFrame,oHostLMS)
	{
		// Check parameters
		if (!sManifestPath || "string" != typeof(sManifestPath) || "object" != typeof(oLMSAPI) || "object" != typeof(oContentFrame) || "object" != typeof(oHostLMS))
			throw new LRNError(201);
			
		m_oLMSAPI = oLMSAPI;

		m_oContentFrame = oContentFrame;

		m_bIsHostOLP = m_oLMSAPI.LRNIsHostOfflinePlayer();

		// Create the XMLDOM representation of the manifest
		m_oManifest = LoadXMLFile(sManifestPath);
        cleanWhitespace(m_oManifest);
			
		// Flag whether we are using a collection manifest or imsmanifest
		if (sManifestPath.indexOf(COLLECTION_MANIFEST_FILE) != -1) m_bIsCollection = true;

		// Per article Q313372: When you use the MSXML 4.0 DOM to run XPath queries against an XML document
		// that specifies a default namespace declaration, you must use the setProperty method of the
		// DOMDocument object to add the default namespace declaration by specifying a namespace prefix.
		//
		// We also need to add the other namespace declarations, since selectSingleNode() & selectNodes()
		// fail to return anything without them.
		SetXMLDOMNamespaces(m_oManifest);

		// Remove comments to avoid a navigation bug
		RemoveComments(m_oManifest);
		
		// Flag items that are available within EK's OLP
		if (m_bIsHostOLP) MarkOLPAvailableItems();
		
		// Get the default table of contents.
		m_sCurrentOrgID = m_oManifest.selectSingleNode("dflt:manifest/dflt:organizations").getAttribute("default");
		if (!m_sCurrentOrgID) m_sCurrentOrgID = m_oManifest.selectSingleNode("dflt:manifest/dflt:organizations/dflt:organization").getAttribute("identifier");

		// Initialize the cache for the XSL that is used to transform the manifest XML to our
		// internal format.
		InitXSLCache();
			
		// Create a timer object.
		m_oTimer = new Timer();

		// Determine whether the content was authored using LCDS
		try
		{
			LoadXMLFile(m_oLMSAPI.LRNGetManifestPath() + LCDS_PLATFORM_MANIFEST);
			m_bIsLcdsContent = true;
		}
		catch(e) {}

		m_bIsReady = true;
	}

	//--------------------------------------------------------------------
	// Method: Finish
	//
 	// Purpose: Performs any actions that need to be done prior to the
 	//	    destruction of an instance of this class.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.Finish=function()
	{
		try
		{
			// Update time value for the current node before the viewer exits
			UpdateCurrentNodeTotalTime();
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: DoNext
	//
 	// Purpose: Selects the next item from the manifest relative to the
	//	    current item and makes it the current node. Fires a selection
	//	    event.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.DoNext=function()
	{
		var sID = GetOLPFirstAvailContentWheelItem(this.GetNextID(m_sCurrentNodeID));
		oNode = this.GetItem(sID);
		if (oNode) NewSelection(oNode,EVENT_NEXT,false);
	}
		
	//--------------------------------------------------------------------
	// Method: DoPrevious
	//
 	// Purpose: Selects the previous item from the manifest relative to the
	//	    current item and makes it the current node. Fires a selection
	//	    event.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.DoPrevious=function()
	{
		var sID = GetOLPFirstAvailContentWheelItem(this.GetPreviousID(m_sCurrentNodeID));
		oNode = this.GetItem(sID);
		if (oNode) NewSelection(oNode,EVENT_PREVIOUS,false);
	}
		
	//--------------------------------------------------------------------
	// Method: DoSelect
	//
 	// Purpose: Selects the specified item from the manifest and makes it
	//	    the current node. Fires a selection event.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.DoSelect=function(sID)
	{
		oNode = this.GetItem(GetOLPFirstAvailContentWheelItem(sID));
		if (oNode) NewSelection(oNode,EVENT_SELECT,false);
	}

	//--------------------------------------------------------------------
	// Method: DoSelectFirst
	//
 	// Purpose: Selects the first item from the manifest and makes it the
 	//	    current node. Fires a selection event.
	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.DoSelectFirst=function()
	{
		try
		{
			var sID = m_oManifest.selectSingleNode("//dflt:organization[@identifier='" + m_sCurrentOrgID + "']//dflt:item[not(@isvisible='0' or @isvisible='false')]").getAttribute("identifier");
			this.DoSelect(sID);
		}
		catch(e)
		{
		}
	}
		
	//--------------------------------------------------------------------
	// Method: GetCurrentID
	//
 	// Purpose: Gets the current item ID.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------			
	Viewer.prototype.GetCurrentID=function()
	{
		return m_sCurrentNodeID;
	}


	//--------------------------------------------------------------------
	// Method: GetNewID
	//
 	// Purpose: Gets the new item ID.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------			
	 Viewer.prototype.GetNewID=function()
	{
		return m_sNewID;
	}	
	

	//--------------------------------------------------------------------
	// Method: GetNextID
	//
 	// Purpose: Gets the identifier of the next item relative to the supplied
	//	    item ID.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetNextID=function(sID)
	{
		var sNextID = "";

		if ("string" == typeof(sID))
		{
			try
			{		
				var oNextNode = FindNext(FindItem(sID),true);
				sNextID = oNextNode.getAttribute("identifier");

				if (m_bIsCollection)
				{
					var oItem = m_oManifest.selectSingleNode("/dflt:manifest/dflt:organizations//dflt:organization/dflt:item[@identifier='" + sNextID +  "']");
					if (oItem && !oItem.getAttribute("orig_identifierref"))
						LoadExternalSubmanifest(oItem);
				}
			}
			catch(e)
			{
			}
		}
			
		return sNextID;
	}

	//--------------------------------------------------------------------
	// Method: GetPreviousID
	//
 	// Purpose: Gets the identifier of the previous item relative to the
 	//	    supplied item ID.
	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	Viewer.prototype.GetPreviousID=function(sID)
	{
		var sPrevID = "";

		if ("string" == typeof(sID))
		{
			try
			{
				var oPrevNode = FindPrevious(FindItem(sID));
				sPrevID = oPrevNode.getAttribute("identifier");

				if (m_bIsCollection)
				{
					var oItem = m_oManifest.selectSingleNode("/dflt:manifest/dflt:organizations//dflt:organization/dflt:item/dflt:item[@identifier='" + sPrevID +  "']");
					if (oItem && oItem.parentNode && !oItem.parentNode.getAttribute("orig_identifierref"))
						LoadExternalSubmanifest(oItem.parentNode);
				}
			}
			catch(e)
			{
			}
		}
					
		return sPrevID;
	}

	//--------------------------------------------------------------------
	// Method: GetParentID
	//
 	// Purpose: Gets the identifier of the parent item relative to the
	//	    supplied item ID.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetParentID=function(sID)
	{
		var sParentID = "";

		if ("string" == typeof(sID))
		{
			try
			{
				var oParent = FindParent(sID);
				sParentID = oParent.getAttribute("identifier");
			}
			catch(e)
			{
			}
		}
						
		return sParentID;
	}

	//--------------------------------------------------------------------
	// Method: SetCurrentTOC
	//
 	// Purpose: Sets the current organization to be used within the
 	//	    manifest.  This organization's item elements will be
	//	    rendered as the TOC.
 	//
 	// Parameters:
	//	String sID [in] - Manifest Organization identifier
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.SetCurrentTOC=function(sID)
	{
		if ("string" == typeof(sID)) m_sCurrentOrgID = sID;
	}

	//--------------------------------------------------------------------
	// Method: GetCurrentTOC
	//
 	// Purpose: 
 	//
 	// Parameters: None
	//
	// Returns: Object OR Null
	//--------------------------------------------------------------------	
	 Viewer.prototype.GetCurrentTOC=function()
	{
		return TransformFromIMStoMLT(m_sCurrentOrgID,SELECT_TOC);
	}

	//--------------------------------------------------------------------
	// Method: GetTOCS
	//
 	// Purpose: Returns a nodelist of available manifest organizations.
 	//
 	// Parameters: None
	//
	// Returns: Object OR Null
	//--------------------------------------------------------------------
	 Viewer.prototype.GetTOCS=function()
	{
		return TransformFromIMStoMLT(null,SELECT_TOCS);
	}

	//--------------------------------------------------------------------
	// Method: GetChildren
	//
 	// Purpose: Gets the children of the specified item.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Object OR Null
	//--------------------------------------------------------------------				
	 Viewer.prototype.GetChildren=function(sID)
	{
		return TransformFromIMStoMLT(sID,SELECT_ITEMCHILDREN);
	}

	//--------------------------------------------------------------------
	// Method: GetDescendants
	//
 	// Purpose: Gets the descendants (children, grandchildren) of the specified item.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Object OR Null
	//--------------------------------------------------------------------	
	 Viewer.prototype.GetDescendants=function(sID)
	{
		return TransformFromIMStoMLT(sID,SELECT_DESCENDANTS);
	}

	//--------------------------------------------------------------------
	// Method: GetItem
	//
 	// Purpose: Gets the specified item from the manifest.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Object OR Null
	//--------------------------------------------------------------------
	Viewer.prototype.GetItem=function(sID)
	{
		var oNode = null;
        var oItem = null;

        if (navigator.userAgent.indexOf('MSIE') < 0)
        {
            if(typeof(sID) == "object")
            {         
                sID=sID.value;
            }
        }
        
		if ("string" == typeof(sID))
		{
			try
			{
				if (m_bIsCollection)
				{
					oItem = m_oManifest.selectSingleNode("/dflt:manifest/dflt:organizations//dflt:organization/dflt:item[@identifier='" + sID +  "']");
					if (oItem && !oItem.getAttribute("orig_identifierref"))
						if (LoadExternalSubmanifest(oItem) != 0) return null;
				}

				oNode = TransformFromIMStoMLT(sID,SELECT_SINGLEITEM);
			}
			catch(e)
			{
			}
		}
			
		return oNode;
	}

	//--------------------------------------------------------------------
	// Method: GetItemTitle
	//
 	// Purpose: Gets the text from the child title element for the specified
 	//	    item.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetItemTitle=function(sID)
	{
		var sTitle = "";

		if ("string" == typeof(sID))
		{
			try
			{
				sTitle = m_oManifest.selectSingleNode("//dflt:item[@identifier='" + sID +  "']/dflt:title").text;	
			}
			catch(e)
			{
			}
		}
			
		return sTitle;
	}

	//--------------------------------------------------------------------
	// Method: IsReady
	//
 	// Purpose:
 	//	Gets the initialization status of a Viewer instance.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.IsReady=function()
	{
		return m_bIsReady;
	}

	//--------------------------------------------------------------------
	// Method: GetSubmanifestCount
	//
 	// Purpose: Gets the number of nested manifest elements.
 	//
 	// Parameters:
	//	String sID [in] - Manifest element identifier
	//
	// Returns: Number
	//--------------------------------------------------------------------
	 Viewer.prototype.GetSubmanifestCount=function(sID)
	{
		var nCount = 0;

		try
		{	
			var oNodes = null;

			if (m_bIsCollection)
				oNodes = m_oManifest.selectNodes("/dflt:manifest/dflt:organizations/dflt:organization//dflt:item[not(@isvisible='0') and not(@isvisible='false')]/dflt:item/@manifestid");
			else
			{
				// Get the children for the root manifest if no id was specified, 
				// otherwise get the children for the specified submanifest (direct descendants only).
				if (!sID)
					oNodes = m_oManifest.selectNodes("/dflt:manifest/dflt:manifest");
				else if ("string" == typeof(sID))
					oNodes = m_oManifest.selectNodes("//dflt:manifest[@identifier='" + sID + "']/dflt:manifest");
			}
				
			nCount = oNodes.length;
		}
		catch(e)
		{
		}

		return nCount;
	}

	//--------------------------------------------------------------------
	// Method: GetSubmanifestIDs
	//
 	// Purpose: Gets a list of identifiers for nested manifest elements.
 	//
 	// Parameters:
	//	String sID [in] - Manifest element identifier
	//
	// Returns: Object - Array of manifest identifiers
	//--------------------------------------------------------------------
	 Viewer.prototype.GetSubmanifestIDs=function(sID)
	{
		var aManifestID = null;

		try
		{
			var oNodes = null;

			if (m_bIsCollection)
				oNodes = m_oManifest.selectNodes("/dflt:manifest/dflt:organizations/dflt:organization//dflt:item[not(@isvisible='0') and not(@isvisible='false')]/dflt:item/@manifestid");
			else
			{
				if (!sID)
					oNodes = m_oManifest.selectNodes("/dflt:manifest/dflt:manifest/@identifier");
				else if ("string" == typeof(sID))
					oNodes = m_oManifest.selectNodes("//dflt:manifest[@identifier='" + sID + "']/dflt:manifest/@identifier");
			}
			
			var nLen = oNodes.length;	
			if (nLen) aManifestID = new Array();

			for (var i = 0; i < nLen; i++)
				aManifestID[i] = oNodes.item(i).text;
		}
		catch(e)
		{
			aManifestID = null;
		}

		return aManifestID;
	}

	//--------------------------------------------------------------------
	// Method: GetRootManifestID
	//
 	// Purpose: Gets the identifier of the root manifest element.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetRootManifestID=function()
	{
		var sID = "";

		try
		{
			sID = m_oManifest.selectSingleNode("/dflt:manifest/@identifier").text;
		}
		catch(e)
		{
		}

		return sID;
	}

	//--------------------------------------------------------------------
	// Method: GetCurrentManifestID
	//
 	// Purpose: Gets the identifier of the current manifest element.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetCurrentManifestID=function()
	{
		var sID = "";

		try
		{
			if (m_bIsCollection)
			{
				// If the node is a course node then return the submanifest identifier so that
				// the scope is correct for Search
				var oNode = m_oManifest.selectSingleNode("/dflt:manifest/dflt:organizations/dflt:organization/dflt:item[@identifier='" + m_sCurrentNodeID +  "']/dflt:item");
				if (oNode) sID = oNode.getAttribute("manifestid");
			}

			// Get the current node's manifest identifier
			if (!sID) sID = m_oManifest.selectSingleNode("//dflt:manifest[dflt:organizations//dflt:organization//dflt:item[@identifier='" + m_sCurrentNodeID +  "']]/@identifier").text;
		}
		catch(e)
		{
		}

		return sID;
	}

	//--------------------------------------------------------------------
	// Method: GetResourcesPathByManifestID
	//
 	// Purpose: Gets the fully qualified path for the specified resource.
	//	    The path may be partially composed of xml:base attribute
	//	    values from the resource, resources and manifest elements.
 	//
 	// Parameters:
	//	String sID [in] - Manifest identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetResourcesPathByManifestID=function(sID)
	{
		var sPath = "";

		if ("string" == typeof(sID))
		{
			try
			{
				if (m_bIsCollection)
				{
					// Test whether the course is visible.
					oNode = m_oManifest.selectSingleNode("/dflt:manifest/dflt:organizations/dflt:organization//dflt:item[@manifestid='" + sID + "']");
					if (ItemIsVisible(oNode.parentNode))
					{
						var sIDRef = oNode.getAttribute("orig_identifierref");
						if (!sIDRef) sIDRef = oNode.getAttribute("identifierref");
						var sHref = m_oManifest.selectSingleNode("/dflt:manifest/dflt:resources/dflt:resource[@identifier='" + sIDRef + "']").getAttribute("href");
						var nPos = sHref.indexOf(SCORM_MANIFEST_FILE);
						if (nPos > -1) sPath = m_oLMSAPI.LRNGetManifestPath() + sHref.substring(0,nPos);
					}
				}
				else
				{	
					sPath = m_oLMSAPI.LRNGetManifestPath();

					var sManifestXmlBase = m_oManifest.selectSingleNode("//dflt:manifest[@identifier='" + sID + "']").getAttribute("xml:base");
					if (sManifestXmlBase) sPath += AppendSlash(sManifestXmlBase);

					var sResourcesXmlBase = m_oManifest.selectSingleNode("//dflt:manifest[@identifier='" + sID + "']/dflt:resources").getAttribute("xml:base");
					if (sResourcesXmlBase) sPath += AppendSlash(sResourcesXmlBase);

					sPath = MassagePath(sPath);
				}
			}
			catch(e)
			{
			}
		}
			
		return sPath;
	}

	//--------------------------------------------------------------------
	// Method: GetManifestTitle
	//
 	// Purpose: Gets the title from the metadata for the specified manifest.
 	//
 	// Parameters:
	//	String sID [in] - Manifest identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetManifestTitle=function(sID)
	{
		var sTitle = "";
			
		try
		{
			if (!sID || "string" == typeof(sID))
			{
				var sQueryBasePath = "";
				var sQueryBase = "";
				var sQuery = "";
				var sNsPrefix = "";
				var oNode = null;

				// Set the XPath manifest element to query
				sQueryBasePath = sQueryBase = sID ? "//dflt:manifest[@identifier='" + sID + "']" : "/dflt:manifest";
				
				// Get the schema and schema version from the manifest metadata
				var oSchema = m_oManifest.selectSingleNode(sQueryBase + "/dflt:metadata/dflt:schema");
				var oSchemaVersion = m_oManifest.selectSingleNode(sQueryBase + "/dflt:metadata/dflt:schemaversion");

				// Not found within the manifest metadata?  Look for it within the organization metadata.
				if ( !(oSchema && oSchemaVersion) )
				{
					sQueryBase += "/dflt:organizations/dflt:organization";

					var oSchema = m_oManifest.selectSingleNode(sQueryBase + "/dflt:metadata/dflt:schema");
					var oSchemaVersion = m_oManifest.selectSingleNode(sQueryBase + "/dflt:metadata/dflt:schemaversion");
				}

				// If we found the schema info, attempt to get the title from the metadata
				if (oSchema && oSchemaVersion)
				{
					// Set the appropriate namespace prefix based on the manifest type
					switch (oSchema.text)
					{
						case "IMS CONTENT":
							if (oSchemaVersion.text.indexOf("1.1") == 0) sNsPrefix = "imsmd:";
							break;
						case "ADL SCORM":
							if (oSchemaVersion.text == "1.2") sNsPrefix = "imsmd:";
							break;
						case "MPI COLLECTION":
							sNsPrefix = "dflt:";
							break;
					}
					
					sQuery = sQueryBase;
					sQuery += "/dflt:metadata/" + sNsPrefix + "lom/" + sNsPrefix + "general/" + sNsPrefix + "title/" + sNsPrefix + "langstring";
					oNode = m_oManifest.selectSingleNode(sQuery);
				}
				
				// Title wasn't found?  If the 'adlcp:location' element is present, get the title from
				// an external metadata file
			    if (!oNode && oSchema.nodeValue == "ADL SCORM" && oSchemaVersion.nodeValue == "1.2")
				{
					sQuery = sQueryBase;
					sQuery += "/dflt:metadata/adlcp:location";
					var oLocation = m_oManifest.selectSingleNode(sQuery);
					if (oLocation)
					{
						var sManifestPath = m_oLMSAPI.LRNGetManifestPath();

						if (m_bIsCollection)
						{
							// within a collection we need to append the folder name to the path;
							// we get this from the xml:base attribute we added at runtime.
							var sManifestXmlBase = oLocation.selectSingleNode(sQueryBasePath + "/dflt:resources").getAttribute("xml:base");	
							if (sManifestXmlBase) sManifestPath += sManifestXmlBase + "/";
						}

						// Load the external metadata
						var oMetadataXMLDOM = LoadXMLFile(sManifestPath+oLocation.text);
						SetXMLDOMNamespaces(oMetadataXMLDOM);
						oNode = oMetadataXMLDOM.selectSingleNode("dflt:lom/dflt:general/dflt:title/dflt:langstring");
					}
				}	
				
				sTitle = oNode.text;
			}
		}
		catch(e)
		{
		}

		return sTitle;
	}


	//--------------------------------------------------------------------
	// Method: IsCollection
	//
 	// Purpose: Specifies whether we are using a proprietary manifest 
	//	    format with external submanifest files.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.IsCollection=function()
	{
		return m_bIsCollection;
	}

	//--------------------------------------------------------------------
	// Method: ResolveItemIDToCourseID
	//
 	// Purpose: Gets the course identifier for the specified item identifier.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.ResolveItemIDToCourseID=function(sID)
	{
		var sCourseID = "";

		if ("string" == typeof(sID))
		{
			try
			{
				var oNode = FindItem(sID);
				sCourseID = m_oManifest.selectSingleNode("/dflt:manifest//dflt:organization/dflt:item[dflt:item/@identifierref='" + oNode.selectSingleNode("ancestor::dflt:organization").getAttribute("identifier") +  "']").getAttribute("identifier");
			}
			catch(e)
			{
			}
		}
			
		return sCourseID;
	}

	//--------------------------------------------------------------------
	// Method: GetCustomHelpMenuItemList
	//
 	// Purpose: Gets a list of menu items from mpimanifest.xml that are to
 	//	    be added to the viewer's help menu at runtime.
 	//
 	// Parameters: None
	//
	// Returns: Object - Array of menu item objects
	//--------------------------------------------------------------------
	 Viewer.prototype.GetCustomHelpMenuItemList=function()
	{
		var aList = null;
			
		try
		{
			var oNodes = m_oManifest.selectNodes("/dflt:manifest/dflt:metadata/dflt:customhelpmenuitems/dflt:helpmenuitem");
			var nLen = oNodes.length;

			if (nLen)
			{
				aList = new Array();
				var sManifestPath = m_oLMSAPI.LRNGetManifestPath();

				for (var i = 0; i < nLen; i++)
				{
					var oNode = oNodes.item(i);
					var oName = oNode.selectSingleNode("dflt:helpmenuname");
					var oHref = oNode.selectSingleNode("dflt:helpmenuhref");
					if (oName && oHref)
					{
						var oItem = new Object();
						oItem.name = oName.text;
						
						var sHref = oHref.text;
						if (!(sHref.indexOf("http:") != -1 || sHref.indexOf("https:") != -1 ||
						      sHref.indexOf("file:") != -1 || sHref.indexOf("\\\\") != -1))
							oItem.href = sManifestPath + sHref;
						else
							oItem.href = sHref;

						aList[i] = oItem;
					}
				}
			}
		}
		catch(e)
		{
			aList = null;
		}

		return aList;
	}

	//--------------------------------------------------------------------
	// Method: Synchronize
	//
 	// Purpose: Causes a new selection to occur if the currently loaded
	//	    content can be resolved to an item identifier that does
	//	    not match the current item identifier.  The main purpose
	//	    of this method is to ensure that the TOC synchronizes with
	//	    the content after a search-related link is selected OR the
	//	    IE Back/Forward button is selected.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	 Viewer.prototype.Synchronize=function()
	{
		// Don't attempt synchronization while content is being loaded
		if (!m_bLoadingContent)
		{
			// Try to match the content URL to an item identifier
			var sItemID = this.ResolveUrlToItemID(m_oContentFrame.location.href);

			// Attempt synchronization only if the node was found in the manifest
			if (sItemID != "")
			{
				if (sItemID != m_sCurrentNodeID)
				{
					var oNode = this.GetItem(sItemID);

					// Sets the current node and performs some housekeeping
					NewSelection(oNode,EVENT_SELECT,true);
				}

				if (this.IsTopicNode(sItemID))
				{
					// Get the topic's parent lesson
					var sLessonID = this.GetParentID(sItemID);

					// Raise an event to synch the lesson node within the TOC
					m_oLMSAPI.LRNRaiseEvent(EVENT_SYNCH_LESSON,sLessonID);
				}
				else if (this.IsLessonNode(sItemID))
				{
					// Raise an event to synch the lesson node within the TOC
					m_oLMSAPI.LRNRaiseEvent(EVENT_SYNCH_LESSON,sItemID);	
				}

				// Raise an event to allow the appropriate item to be selected
				m_oLMSAPI.LRNRaiseEvent(EVENT_SYNCH_TOPIC,sItemID);
			}
			else if (m_bIsLcdsContent && m_oContentFrame.location.hash != "")
			{
				// Try to match the content URL to an item identifier
				var sLessonID = GetLcdsLessonIDFromTopicHash(m_oContentFrame.location.hash);

				if (sLessonID != "")
				{
					var oNode = this.GetItem(sLessonID);

					if (oNode)
					{
    				    var oChild = oNode.documentElement.childNodes.firstChild;

						if (oChild)
						{
							// Replace the href so that the correct hash value is used
							oChild.setAttribute("Href", m_oContentFrame.location.href);

							// Sets the current node and performs some housekeeping
							NewSelection(oNode,EVENT_SELECT,true);
						}
					}
				}
			}
		}
		else
		{
			// Busy loading new content, so we'll try again...
			window.setTimeout(m_oLMSAPI.LRNSynchronize,100);
		}
	}

	//--------------------------------------------------------------------
	// Method: GetResourceIdFromItemId
	//
 	// Purpose: Obtains a manifest resource ID (item identifierref) from
	//	    the corresponding item identifier.
	//
 	// Parameters: String sItemID [in] - A manifest item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetResourceIdFromItemId=function(sItemID)
	{
		var sID = "";

		try
		{
			sID = m_oManifest.selectSingleNode("//dflt:item[@identifier='" + sItemID + "']/@identifierref").text;
		}
		catch(e)
		{
		}

		return sID;
	}

	//--------------------------------------------------------------------
	// Method: GetItemIdFromResourceId
	//
 	// Purpose: Obtains a manifest item identifier from the corresponding
	//	    resource ID (item identifierref).
	//
 	// Parameters: String sResourceID [in] - A manifest resource identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.GetItemIdFromResourceId=function(sResourceID)
	{
		var sItemID = "";

		try
		{
			// Search for an identifierref that contains the EK topic ID.  The identifierref value will contain a leading 'R'. 
			sItemID = m_oManifest.selectSingleNode("//dflt:item[contains(@identifierref,'" + sResourceID + "')]/@identifier").text;
		}
		catch(e)
		{
		}

		return sItemID;
	}

	//--------------------------------------------------------------------
	// Method: IsLessonNode
	//
 	// Purpose: Determines whether the specified item is a lesson node.
	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.IsLessonNode=function(sID)
	{
		var bResult = false;

		try
		{
			var oItem = m_oManifest.selectSingleNode("//dflt:item[@identifier='" + sID + "']/dflt:item");
			if (oItem)
			{
				// Get all metadata contained within the item's resource; a topic may contain metadata for both v-lab and content wheel icon.
				var oLangStrings = m_oManifest.selectNodes("//dflt:resource[@identifier='" + oItem.getAttribute("identifierref") + "']/dflt:metadata/imsmd:lom/imsmd:general/imsmd:description/imsmd:langstring");
				var oLangString = null;

				// Loop through the nodelist
				if(oLangStrings)
				{
				for(ic = 0; ic<oLangStrings.length; ic++)
				{
				    oLangString = oLangStrings[ic];
					// If the value is numeric, it's content wheel icon metadata, so the item is a topic.
					if (!isNaN(oLangString.text)) 
					{
						bResult = true;
						break;
					}
			        }
				}
			}
		}
		catch(e)
		{
		}

		return bResult;
	}

	//--------------------------------------------------------------------
	// Method: IsTopicNode
	//
 	// Purpose: Determines whether the specified item is a topic node.
	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.IsTopicNode=function(sID)
	{
		var bResult = false;

		try
		{
			var oItem = m_oManifest.selectSingleNode("//dflt:item[@identifier='" + sID + "']");
			if (oItem)
			{
				// Get all metadata contained within the item's resource; a topic may contain metadata for both v-lab and content wheel icon.
				var oLangStrings = m_oManifest.selectNodes("//dflt:resource[@identifier='" + oItem.getAttribute("identifierref") + "']/dflt:metadata/imsmd:lom/imsmd:general/imsmd:description/imsmd:langstring");
				var oLangString = null;

				// Loop through the nodelist
				for(ic = 0; ic < oLangStrings.length; ic++)
				{
				    oLangString = oLangStrings[ic];
					// If the value is numeric, it's content wheel icon metadata, so the item is a topic.
					if (!isNaN(oLangString.text)) 
					{
						bResult = true;
						break;
					}
				}
			}
		}
		catch(e)
		{
		}

		return bResult;
	}

	//--------------------------------------------------------------------
	// Method: ResolveUrlToItemID
	//
 	// Purpose: Gets the item identifier that corresponds to a given URL.
 	//
 	// Parameters:
	//	String sURL [in] - Uniform Resource Locator
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Viewer.prototype.ResolveUrlToItemID=function(sURL)
	{
		var sID = "";

		if ("string" == typeof(sURL))
		{
			try
			{
				// Find and remove the hash, if one exists 
				sURL = sURL.replace("default.html#", "");
				var nPos = sURL.indexOf("#");
				if (nPos != -1)	sURL = sURL.substr(0,nPos);
				sURL = NormalizeURL(MassagePath(sURL));
										
				// Get the current node's URL
				var oItem = this.GetItem(m_sCurrentNodeID);
				var sItemURL = MassagePath(oItem.documentElement.childNodes[0].getAttribute("Href"));
				sItemURL = NormalizeURL(sItemURL);
				
				// Test the current node's URL against the URL passed-in
				if (sURL.indexOf(sItemURL) > -1)
					sID = m_sCurrentNodeID;
				else
				{
					// Load the external submanifest into the XML DOM, if necessary
					if (m_bIsCollection)
					{
						var sColMnfstPath = NormalizeURL(m_oLMSAPI.LRNGetManifestPath());
						
						nPos = sURL.indexOf(sColMnfstPath);
						if (nPos > -1)
						{
							var sPath = sURL.substring(sColMnfstPath.length,sURL.length);
							nPos = sPath.lastIndexOf("/");
							if (nPos > -1)	sPath = AppendSlash(sPath.substring(0,nPos)) + SCORM_MANIFEST_FILE;
				
							var oRes = m_oManifest.selectSingleNode("/dflt:manifest/dflt:resources/dflt:resource[contains(translate(@href,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + sPath + "')]");
							if (oRes)
							{
								var sResID = oRes.getAttribute("identifier");
								if (sResID)
								{
									var oItem = m_oManifest.selectSingleNode("/dflt:manifest/dflt:organizations/dflt:organization//dflt:item[@identifierref='" + sResID + "']");
									if (oItem && oItem.parentNode)
										LoadExternalSubmanifest(oItem.parentNode);
								}
							}
						}
					}

					// Get the filename from the URL
					var aURL = sURL.split("/");
					var sFile = (aURL) ? aURL[aURL.length-1] : sURL;

					// Select all resource nodes containing the filename
					sFile = sFile.replace(".xml","");
					var oResNodes = m_oManifest.selectNodes("//dflt:resource[contains(translate(@href,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + sFile + "')]");
					var oResItem = null;

					// Iterate over the node list
					//while (oResItem = oResNodes.nextNode())
					for(iNode = 0; iNode < oResNodes.length; iNode++)
					{
					    var oResItem = oResNodes[iNode];
						var sResItemURL = oResItem.getAttribute("href")

						// For EK support
						if (sResItemURL.indexOf("viewer.htm?") > -1)
						{
							var sItem = "LaunchPage";
							var sValue = "";
							var nItemLen = sItem.length;
							var aQuery = sResItemURL.split("?");
	
							for (var i = 0; i < aQuery.length; i++)
							{
								var nPos = aQuery[i].indexOf(sItem);
								if (nPos > -1) sValue = unescape(aQuery[i].substring(nPos+nItemLen+1,aQuery[i].length));
							}

							if (sValue != "") sResItemURL = sValue;
						}

						// Build a fully qualified URL
						var sResURL = this.GetResourcesPathByManifestID(oResItem.parentNode.parentNode.getAttribute("identifier")) + sResItemURL;
						sResURL = NormalizeURL(sResURL);

						// Match found
						if (sResURL.indexOf(sFile) > -1)
						{
							// Get the item if its visible
							var oItem = m_oManifest.selectSingleNode("//dflt:item[@identifierref='" + oResItem.getAttribute("identifier") + "' and not(@isvisible='false' or @isvisible='0')]");
							if (oItem)
							{
								sID = oItem.getAttribute("identifier");
								break;
							}
						}
					}
				}
			}
			catch(e)
			{
			}
		}
		
		return sID;
	}

	//--------------------------------------------------------------------
	// Method: IsItemAvailable
	//
 	// Purpose: Determines whether the an item is available within the EK
	//		offline player.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.IsItemAvailable=function(sID)
	{
		var bAvailable = true;

		try
		{
			if (m_oManifest.selectSingleNode("//dflt:item[@identifier='" + sID + "' and @available='false']"))
				bAvailable = false;
		}
		catch(e)
		{
		}

		return bAvailable;
	}

	//--------------------------------------------------------------------
	// Method: GetNotesTreeViewXmlDOM
	//
 	// Purpose: Returns an XML hierarchy of nodes that is rendered
	//	    by the Notebook treeview.
 	//
 	// Parameters: None
	//
	// Returns: XML DOM or null
	//--------------------------------------------------------------------
	 Viewer.prototype.GetNotesTreeViewXmlDOM=function()
	{
		var NOTEBOOK_XML_ROOT_NODE = "Root";
		
		var oXMLDOM = null;

		try
		{
			var sItemFilter = "";
			var sIntroFilter = "";
			var sItemID = "";

			var bIsViewerHosted = m_oLMSAPI.LRNIsViewerHosted();
			var bIsLcds = m_oLMSAPI.LRNIsLcdsContent();

			var aNotesScoIDs = m_oLMSAPI.LRNGetNotes();

			for (sScoID in aNotesScoIDs)
			{
				// Build a filter to identify annotated items
				if (sItemFilter != "") sItemFilter += " or ";

				sItemID = bIsViewerHosted ? m_oLMSAPI.LRNGetItemIdFromScoId(sScoID) : sScoID;
				
				sItemFilter += "@identifier='" + sItemID + "'";
			}

			if (sItemFilter != "")
			{
				var oRootNodes = m_oManifest.selectNodes("/dflt:manifest/dflt:organizations/dflt:organization/dflt:item");
				var oRootNode = null;

				oXMLDOM = LoadXML("<"+ NOTEBOOK_XML_ROOT_NODE + "></" + NOTEBOOK_XML_ROOT_NODE + ">");

				//while (oRootNode = oRootNodes.nextNode())
				for(ic = 0; ic < oRootNodes.length; ic++)
				{
				    oRootNode = oRootNodes[ic];
					var oLessonNodes = oRootNode.selectNodes("dflt:item[descendant-or-self::dflt:item[" + sItemFilter + "]]");

					if (oLessonNodes.length)
					{
						var sRootItemID = oRootNode.getAttribute("identifier");
						var oItem1 = CreateNotesTreeViewXmlItemElement(oXMLDOM, oXMLDOM.documentElement, sRootItemID, "0", this.IsItemAvailable(sRootItemID), oRootNode.selectSingleNode("dflt:title").text);

						var oLessonNode = null;

						//while (oLessonNode = oLessonNodes.nextNode())
						for(lNodeCout = 0; lNodeCout < oLessonNodes.length; lNodeCout++)
						{
						    oLessonNode = oLessonNodes[lNodeCout];
							if (!bIsLcds)
							{
								var sLessonItemID = oLessonNode.getAttribute("identifier");

								var oItem2 = CreateNotesTreeViewXmlItemElement(oXMLDOM, oItem1, sLessonItemID, "0", this.IsItemAvailable(sLessonItemID), oLessonNode.selectSingleNode("dflt:title").text); 

								var oTopics = oLessonNode.selectNodes("dflt:item");
								var oTopic = null;

								if (oTopics)
								{
									if (m_oLMSAPI.LRNHasNote(sLessonItemID))
									{
										CreateNotesTreeViewXmlItemElement(oXMLDOM, oItem2, sLessonItemID, "1", this.IsItemAvailable(sLessonItemID), LBL_CONTENTWHEEL_INTRO_TOPIC);
									}

									//while (oTopic = oTopics.nextNode())
									for(topicNodeCount = 0; topicNodeCount < oTopics.length; topicNodeCount++)
									{
									    oTopic = oTopics[topicNodeCount];
										var sTopicItemID = oTopic.getAttribute("identifier");
										
										if (m_oLMSAPI.LRNHasNote(sTopicItemID))
										{
											CreateNotesTreeViewXmlItemElement(oXMLDOM, oItem2, sTopicItemID, "1", this.IsItemAvailable(sTopicItemID), oTopic.selectSingleNode("dflt:title").text);
										}
									}
								}
							}
							else
							{
								var oResource = m_oManifest.selectSingleNode("/dflt:manifest/dflt:resources/dflt:resource[@identifier='" + oLessonNode.getAttribute("identifierref") + "']");

								if (oResource)
								{
									var oFile = oResource.selectSingleNode("dflt:file[contains(translate(@href,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + LCDS_LESSON_MANIFEST_FILENAME_PREFIX + "')]");

									if (oFile)
									{
										// Get the portion of the href value following 'COURSE6024/COURSE?????'.
										var sHref = oFile.getAttribute("href");
										var nPos = sHref.indexOf("ORG6024/COURSE");
										if (nPos > -1) sHref = sHref.substring(nPos + 20);

										var oLessonManifestDOM = LoadXMLFile(m_oLMSAPI.LRNGetManifestPath() + sHref);

										if (oLessonManifestDOM)
										{
											var oLessonManifestNodes = oLessonManifestDOM.selectNodes("/lessonManifest/list/topic");

											if (oLessonManifestNodes)
											{
												var sLessonItemID = oLessonNode.getAttribute("identifier");
												var sNotes = m_oLMSAPI.LRNGetNote(sLessonItemID);
												var aNotes = sNotes.split(LCDS_NOTES_DELIMITER);

												if (sNotes != "" && aNotes.length == oLessonManifestNodes.length)
												{
													var oItem2 = CreateNotesTreeViewXmlItemElement(oXMLDOM, oItem1, sLessonItemID, "0", this.IsItemAvailable(sLessonItemID), oLessonNode.selectSingleNode("dflt:title").text);

													var i = 0;
													var oTopicNode = null;

													//while (oTopicNode = oLessonManifestNodes.nextNode())
                									for(topicNodeCount = 0; topicNodeCount < oTopics.length; topicNodeCount++)
													{
													    oTopicNode = oLessonManifestNodes[topicNodeCount];
														if (aNotes[i] != "")
														{
															var sURI = oTopicNode.getAttribute("uri");

															if (sURI != "")
															{								
																var oTopicManifestDOM = LoadXMLFile(m_oLMSAPI.LRNGetManifestPath() + sURI);

																if (oTopicManifestDOM)
																{
																	var oTopic = oTopicManifestDOM.selectSingleNode("/topic");

																	CreateNotesTreeViewXmlItemElement(oXMLDOM, oItem2, "#" + sURI.substring(0, sURI.length - 4), "1", "true", oTopic.getAttribute("title"));
																}
															}
														}
									
														i++;
													}
												}
											}
										}
									}
								}
							}
						}

					}
				}
				
			}
		}
		catch(e)
		{
		}

		return oXMLDOM;
	}

	//--------------------------------------------------------------------
	// Method: IsLcdsContent
	//
 	// Purpose: Determines whether the current content was authored using
	//	    LCDS.
 	//
 	// Parameters: None
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.IsLcdsContent=function()
	{
		return m_bIsLcdsContent;
	}

	//--------------------------------------------------------------------
	// Method: GetLcdsTopicTitle
	//
 	// Purpose: Gets the title for the specified topic.
 	//
 	// Parameters:
	//	String sLessonID [in] - Item ID for the lesson SCO
	//	Number nTopicIndex [in] - Topic ordinal
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	 Viewer.prototype.GetLcdsTopicTitle=function(sLessonID, nTopicIndex)
	{
		var sTitle = "";

		try
		{
			var oLessonNode = m_oManifest.selectSingleNode("/dflt:manifest//dflt:item[@identifier='" + sLessonID + "']");

			if (oLessonNode)
			{
				var oResource = m_oManifest.selectSingleNode("/dflt:manifest/dflt:resources/dflt:resource[@identifier='" + oLessonNode.getAttribute("identifierref") + "']");

				if (oResource)
				{
					var oFile = oResource.selectSingleNode("dflt:file[contains(translate(@href,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + LCDS_LESSON_MANIFEST_FILENAME_PREFIX + "')]");

					if (oFile)
					{
						var oLessonManifestDOM = LoadXMLFile(m_oLMSAPI.LRNGetManifestPath() + oFile.getAttribute("href"));

						if (oLessonManifestDOM)
						{
							var oTopicNode = oLessonManifestDOM.selectSingleNode("/lessonManifest/list/topic[" + (nTopicIndex + 1) + "]");

							if (oTopicNode)
							{
								var sURI = oTopicNode.getAttribute("uri");

								if (sURI != "")
								{								
									var oTopicManifestDOM = LoadXMLFile(m_oLMSAPI.LRNGetManifestPath() + sURI);

									if (oTopicManifestDOM)
									{
										sTitle = oTopicManifestDOM.selectSingleNode("/topic/@title").text
									}
								}
							}
						}
					}
				}
			}
		}
		catch(e)
		{
		}

		return sTitle;
	}
	//--------------------------------------------------------------------
	// Method: getItemNodes
	//
 	// Purpose: get the course item nodes from manifest
 	//
 	// Parameters:
	//	
	//
	// Returns:  Array containing the item nodes information
	//--------------------------------------------------------------------
     Viewer.prototype.getItemNodes=function()
    {
        var itemNodes = m_oManifest.selectNodes("//dflt:item");
        var itemArray = new Array();
        var itemId = null;
        var itemTitle = null;
        var itemIdRef = null;
        var itemResc = null;
        var itemUrl = null;
        var delimiter = "$&%#";
        for(var itemCount = 0; itemCount < itemNodes.length; itemCount++)
        {
            itemId = itemNodes[itemCount].getAttribute("identifier") ;
            itemIdRef = itemNodes[itemCount].getAttribute("identifierref");
            itemTitle = itemNodes[itemCount].childNodes[0].text;
            if(itemIdRef != "" && itemIdRef != null)
            {
                var itemResc = m_oManifest.selectSingleNode("/dflt:manifest/dflt:resources/dflt:resource[@identifier='" + itemIdRef + "']");
                itemUrl = itemResc.attributes[2].value;
            }
            else{itemUrl = "";}
            itemArray.push(itemId + delimiter + itemTitle + delimiter + itemUrl);
        }
        return itemArray;
    }
	//////////////////////////////////////////////////////////////////////
	//PRIVATE METHODS.
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: NewSelection
	//
 	// Purpose: Loads the new content and prepares to set the active node.
 	//
 	// Parameters:
	//	String sID	  [in] - Item identifier
	//	Number nEvent	  [in] - Selection event to raise
	//	Boolean bSynching [in] - Synchronization flag
	//
	// Returns: None
	//--------------------------------------------------------------------
	 NewSelection=function(oItem,nEvent,bSynching)
	{
		if ("object" == typeof(oItem) && "number" == typeof(nEvent) && "boolean" == typeof(bSynching))
		{
			try
			{
			   var oChild = oItem.documentElement.childNodes[0]; 

				// Save the new item ID and event type
				m_sNewID = oChild.getAttribute("ID");
				m_nNewEvent = nEvent;

				// Save the item Href
				m_sNewHref = MassagePath(oChild.getAttribute("Href"))
				m_sNewHref = NormalizeURL(m_sNewHref);
				
				// If synchronizing, don't reload the content
				if (!bSynching)
				{
					window.clearInterval(m_oLoadingInterval);
					m_bLoadingContent = true;

					// Temporarily disable the content wheel
					if (m_oLMSAPI.LRNIsLessonNode(m_sNewID))
					{
						m_oLMSAPI.LRNRaiseEvent(EVENT_DISABLE_WHEEL,true);
					}
		
					// Reload the page if only the hash changes.
					//
					var bCallReload = false;
					var sHref = NormalizeURL(m_oContentFrame.location.href);
					var nPosOrig = sHref.indexOf("#");
					var nPosNew = m_sNewHref.indexOf("#");

					if (nPosOrig > -1 && nPosNew > -1 && sHref.substring(0,nPosOrig) == m_sNewHref.substring(0,nPosNew))
					{
						bCallReload = true;
					}

					// Replace the page history if the current page is 'blank.htm' (fixes MSLPE #3136)
					if (m_oContentFrame.location.href.indexOf("blank.htm") == -1)
						m_oContentFrame.location.href = m_sNewHref;
					else
						m_oContentFrame.location.replace(m_sNewHref);

					if (bCallReload)
						Reload();
					else
						m_oLoadingInterval = window.setInterval(SetCurrentNode,50);
				}
				else
				{
					m_oLoadingInterval = window.setInterval(SetCurrentNode,50);
				}
				
			}
			catch(e)
			{
				// Warn the user that the navigation failed.
				alert(ERR_NODE_SELECTION);

				// Raise an event to indicate that the selection could not be made.
				// This will typically occur when attempting to navigate to the
				// starting node (from the prior session) after the node has been
				// removed by editing the manifest.
				m_oLMSAPI.LRNRaiseEvent(EVENT_SELECTION_ERROR,null);
			}
		}
	}

	//--------------------------------------------------------------------
	// Method: Reload()
	//
 	// Purpose: Calls location.reload() on the content window after 
	//	    location.href has settled.  We need to do this because
	//	    the reload could happen asyncronously and occur before
	//	    window.location has updated, causing the original page
	//	    to display.
	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function Reload()
	{
		var sHref = NormalizeURL(m_oContentFrame.location.href);

		if (m_sNewHref.indexOf(sHref) > -1)
		{
			m_oContentFrame.location.reload();
			m_oLoadingInterval = window.setInterval(SetCurrentNode,50);
		}
		else
		{
			window.setTimeout(Reload,50);
		}
	}

	//--------------------------------------------------------------------
	// Method: SetCurrentNode
	//
 	// Purpose: Performs housekeeping functions for the prior node such as
	//	    total time, lesson status (for node as well as ancestors),
 	//	    sets the current node ID and raises a selection event.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function SetCurrentNode()
	{
		try
		{
		    var sHref = NormalizeURL(m_oContentFrame.location.href);
		    var contentFrameReadyState = window.ActiveXObject ? m_oContentFrame.document.readyState : this.contentFrameReadyState;

			// Continue if the content frame's URL matches the new URL
			// or an error occured loading the page.
		    if ((m_sNewHref.indexOf(sHref) > -1 || sHref.indexOf("res://") == 0) && (contentFrameReadyState == "interactive" || contentFrameReadyState == "complete"))
			{
				window.clearInterval(m_oLoadingInterval);

				UpdateCurrentNodeTotalTime();

				if (!m_oLMSAPI.LRNIsViewerHosted())
				{
					// If the SCO set 'cmi.core.exit' to 'suspend' we need to set 'cmi.core.entry' to 'resume'.
					// Otherwise, we should set the value to an empty string, since the SCO has been visited.
					var sEntryVal = m_oLMSAPI.LRNGetScopedValue("cmi.core.entry",m_sCurrentNodeID,true);
					var sExitVal = m_oLMSAPI.LRNGetScopedValue("cmi.core.exit",m_sCurrentNodeID,true);
					if ("suspend" == sExitVal || "" != sEntryVal)
					{
						sEntryVal = ("suspend" == sExitVal) ? "resume" : "";
						m_oLMSAPI.LRNSetScopedValue("cmi.core.entry",sEntryVal,m_sCurrentNodeID,true);
					}
				}
	
				// Set the current node id
				m_sCurrentNodeID = m_sNewID;

				// Signal that a new selection was made
				m_oLMSAPI.LRNRaiseEvent(m_nNewEvent,m_sCurrentNodeID);

				// Raise an event to allow the appropriate content wheel item to be selected
				if (m_oLMSAPI.LRNIsTopicNode(m_sCurrentNodeID))
				{
					m_oLMSAPI.LRNRaiseEvent(EVENT_SYNCH_TOPIC,m_sCurrentNodeID);
				}				
				else if (m_oLMSAPI.LRNIsLessonNode(m_sCurrentNodeID))
				{
					// Re-enable the content wheel
					m_oLMSAPI.LRNRaiseEvent(EVENT_DISABLE_WHEEL,false);
				}
				else if (m_bIsLcdsContent && m_oContentFrame.location.hash != "")
				{
					var sLessonID = GetLcdsLessonIDFromTopicHash(m_oContentFrame.location.hash);

					// Raise an event to synch the lesson node within the TOC
					m_oLMSAPI.LRNRaiseEvent(EVENT_SYNCH_LESSON,sLessonID);
				}

				m_bLoadingContent = false;
			}
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: GetViewerPath
	//
 	// Purpose: Gets the location of viewer.htm.
 	//
 	// Parameters: None
	//
	// Returns: String
	//--------------------------------------------------------------------
	function GetViewerPath()
	{
		var sURL = window.location.href;
		var nPos = sURL.indexOf("viewer.htm");
		sURL = sURL.substring(0, nPos);
		return sURL;
	}

	//--------------------------------------------------------------------
	// Method: GetLcdsLessonIDFromTopicHash
	//
 	// Purpose: Returns the corresponding lesson identifier.
 	//
 	// Parameters: String sHash [in] - The URL hash value
	//
	// Returns: String
	//--------------------------------------------------------------------
	function GetLcdsLessonIDFromTopicHash(sHash)
	{
		sLessonID = "";
		var oResource =  null;
		var sLessonID =null;
		try
		{
			    oResource =  m_oManifest.selectSingleNode("//dflt:resource[dflt:file[contains(translate(@href,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + sHash.substring(1,sHash.length).toLowerCase() + "')]]");
			    sLessonID = m_oManifest.selectSingleNode("//dflt:item[@identifierref='" + oResource.getAttribute("identifier") + "']/@identifier").text;
		}
		catch(e)
		{
		}

		return sLessonID;
	}

	//--------------------------------------------------------------------
	// Method: MassagePath
	//
 	// Purpose: Converts a relative path to a fully qualified URL.
 	//
 	// Parameters: String sURL [in] - Relative path to convert
	//
	// Returns: String
	//--------------------------------------------------------------------
	function MassagePath(sURL)
	{
		var sTmp = sURL;
	
		try
		{
			if (sURL.indexOf("../") == 0)
				sTmp = GetViewerPath() + sURL;
			else
				sTmp = sURL;

			var oExp = new RegExp("\\.\\.\/","g");
			var arr = sTmp.match(oExp,"");
			
			if (arr)
			{
				var sTmp2 = "";
				var nCnt = arr.length;

				for (var i = 0; i < nCnt; i++)
				{
					var nPos = sTmp.indexOf("../");
					var nPos2 = 0;

					if (nPos > 0)
					{
						sTmp2 = sTmp.substring(0, nPos - 1);
						nPos2 = sTmp2.lastIndexOf("/");
						if (nPos2 > 0) sTmp2 = sTmp2.substring(0, nPos2 + 1);
						sTmp = sTmp2 + sTmp.substring(nPos + 3,sTmp.length);
					}
				}
			}	
		}
		catch(e)
		{
		}

		return sTmp;
	}
		
	//--------------------------------------------------------------------
	// Method: UpdateCurrentNodeTotalTime
	//
 	// Purpose: Determines the session time and updates total time for the
	//	    current node.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function UpdateCurrentNodeTotalTime()
	{
		//if (m_sCurrentNodeID && !EK_API)
		if (m_sCurrentNodeID && !m_oLMSAPI.LRNIsViewerHosted())
		{
			var sSessionTime = m_oLMSAPI.LRNGetScopedValue("cmi.core.session_time",m_sCurrentNodeID,true);
				
			// Don't update total time if the SCO set 'cmi.core.session_time' because
			// 'cmi.core.total_time' is automatically updated
			if (!sSessionTime)
			{
				var sTimeDelta = m_oLMSAPI.LRNGetScopedValue("cmi.core.total_time",m_sCurrentNodeID,true);
				sTimeDelta = m_oTimer.CalculateTimeDelta(sTimeDelta);
				m_oLMSAPI.LRNSetScopedValue("cmi.core.total_time",sTimeDelta,m_sCurrentNodeID,true);
			}
		}
	}

	//--------------------------------------------------------------------
	// Method: LoadExternalSubmanifest
	//
 	// Purpose: Loads an imsmanifest.xml into the XMLDOM.  This function
	//	    will only be used when a collection is present.
 	//
 	// Parameters:
	//	Object oItem [in] - Item node
	//
	// Returns: Number (0 == Success; -1 == Failure)
	//--------------------------------------------------------------------
	function LoadExternalSubmanifest(oItem)
	{
		var nRetVal = 0;
		var hMsg = null;

		try
		{
			// Load the external submanifest if the course node is visible
			if (ItemIsVisible(oItem))
			{
				var oItemChild = oItem.selectSingleNode("dflt:item");
				if (oItemChild)
				{
					var sIDRef = oItemChild.getAttribute("identifierref");
					var oRes = m_oManifest.selectSingleNode("/dflt:manifest/dflt:resources/dflt:resource[@identifier='" + sIDRef + "']");
					if (oRes)
					{
						var sHref = oRes.getAttribute("href");
						var nPos = sHref.indexOf(SCORM_MANIFEST_FILE);
						if (nPos > -1)
						{
							hMsg = m_oLMSAPI.LRNAddMessage(MSG_LOADING_MANIFEST);

							// Prepend the manifest path; all external submanifests are relative
							// to the location of mpimanifest.xml
							var sPath = m_oLMSAPI.LRNGetManifestPath() + sHref;
								
							try 
							{
								var oXML = LoadXMLFile(sPath);
								SetXMLDOMNamespaces(oXML);
								RemoveComments(oXML);

								// Add the relative path to imsmanifest.xml as the 'xml:base'
								// attribute of the resources element
								var oSubRes = oXML.selectSingleNode("/dflt:manifest/dflt:resources");
								if (oSubRes) oSubRes.setAttribute("xml:base",sHref.substring(0,nPos));
					
								var oNodes = oXML.documentElement.cloneNode(true);
								if (oNodes) m_oManifest.documentElement.appendChild(oNodes);

								oItemChild.setAttribute("orig_identifierref",oItemChild.getAttribute("identifierref"));
								oItemChild.setAttribute("identifierref",oXML.selectSingleNode("/dflt:manifest/dflt:organizations").getAttribute("default"));
								AddSubmanifestNamespaces(oXML);
							}
							catch(e)
							{
								alert(e.description);

								// Remove the element from the XMLDOM so that we don't render an empty child node
								oItem.removeChild(oItemChild);
							}
						}
					}
				}
			}
		}
		catch(e)
		{
			nRetVal = -1;
		}
			
		try {if (hMsg) m_oLMSAPI.LRNRemoveMessage(hMsg);} catch(e) {}
			
		return nRetVal;
	}

	//--------------------------------------------------------------------
	// Method: AddSubmanifestNamespaces
	//
 	// Purpose: Compares the submanifest namespaces to the namespace
 	//	    declarations for the parent manifest.  If the submanifest
	//	    contains aditional namespace declarations, the namespaces
	//	    are added to the parent manifest.
 	//
 	// Parameters:
	//	Object oXML [in] - Submanifest XML DOM
	//
	// Returns: None
	//--------------------------------------------------------------------
	function AddSubmanifestNamespaces(oXML)
	{
		try
		{
			var sNamespaces = m_oManifest.getProperty("SelectionNamespaces");
			var nLen = oXML.documentElement.attributes.length;

			for (var i = 0; i < nLen; i++)
			{
				var oAttrib = oXML.documentElement.attributes(i);

				if (oAttrib.nodeName.indexOf("xmlns") != -1 && sNamespaces.indexOf(oAttrib.nodeValue) == -1)
				{
					if (oAttrib.nodeName != "xmlns")
					{
						var nPos = sNamespaces.indexOf(oAttrib.nodeName+"=");
						if (nPos == -1)
							sNamespaces += oAttrib.nodeName + "='" + oAttrib.nodeValue + "' ";
						else
						{
							// Prepare to display an error message for
							// debugging purposes.  This message should never
							// be seen by end-users only builders.
							var sSub = sNamespaces.substring(nPos);
							var nPos2 = sSub.indexOf(" xmlns:");
							if (nPos2 < 0) nPos2 = sSub.length;
								
							var sMsg = ERR_NAMESPACE_REDEFINITION;
							var sReplace = sSub.substring(0,nPos2);
							sMsg = sMsg.replace(new RegExp("%s1","gi"),sReplace);
							sReplace = oAttrib.nodeName + "='" + oAttrib.nodeValue + "'";
							sMsg = sMsg.replace(new RegExp("%s2","gi"),sReplace);
							alert(sMsg);
						}
					}		
				}
			}

			// Set the property
			m_oManifest.setProperty("SelectionNamespaces",sNamespaces);
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: InitXSLCache
	//
 	// Purpose: Caches the stylesheet used to transform manifest XMLDOM nodes
	//	    into an internal xml format used by the viewer.
 	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	function InitXSLCache()
	{
	    if (window.ActiveXObject)
	    {
		// Get the version of the XML Parser
		var sVersion = GetXmlParserVersion();
			
		var oXSL = new ActiveXObject("Msxml2.FreeThreadedDOMDocument." + sVersion);
		oXSL.async = false;
		try
		{
			oXSL.setProperty("AllowXsltScript", true);
		}
		catch(err) {}

		oXSL.validateOnParse = false;
		oXSL.resolveExternals = false;
		try {oXSL.setProperty("NewParser",true);} catch(e) {}

		// Load the stylesheet
		if (!oXSL.load(XSL_FILE))
		{
			var oError = new Object();
			oError.number = oXSL.parseError.errorCode;
			oError.description = ERR_CANNOT_LOAD_XML_FILE + XSL_FILE + "\n" + oXSL.parseError.reason;
			throw oXSL.parseError;
		}
			
		// Get the root manifest element
		var oNode = m_oManifest.selectSingleNode("/dflt:manifest");
		if (oNode)
		{
			var oAtt = null;

			// Get the default namespace from the manifest and
			// add it to the stylesheet
			var sNS = oNode.getAttribute("xmlns");
			if (sNS)
			{
				oAtt = oXSL.createAttribute("xmlns:dflt");
				oAtt.value = sNS;
				oXSL.documentElement.attributes.setNamedItem(oAtt);
			}

			// Get the 'adlcp' namespace from the manifest and
			// add it to the stylesheet
			sNS = oNode.getAttribute("xmlns:adlcp");
			if (sNS)
			{
				oAtt = oXSL.createAttribute("xmlns:adlcp");
				oAtt.value = sNS;
				oXSL.documentElement.attributes.setNamedItem(oAtt);
			}
			else
			{
				// A namespace having the 'adlcp' prefix was not declared, so declare
				// it using the default namespace URI.
				oAtt = oXSL.createAttribute("xmlns:adlcp");
				oAtt.value = oXSL.documentElement.getAttribute("xmlns:dflt");
				oXSL.documentElement.attributes.setNamedItem(oAtt);
			}

			// Get the 'imsmd' namespace from the manifest and
			// add it to the stylesheet
			sNS = oNode.getAttribute("xmlns:imsmd");
			if (sNS)
			{
				oAtt = oXSL.createAttribute("xmlns:imsmd");
				oAtt.value = sNS;
				oXSL.documentElement.attributes.setNamedItem(oAtt);
			}

			// Cache the stylesheet so we don't have to compile it with each transform.
			m_oXSLCache = new ActiveXObject("Msxml2.XSLTemplate." + sVersion);

			try
			{
         			m_oXSLCache.stylesheet = oXSL;
         		}
         		catch(err)
         		{
         			//alert(err.description);
         		}
		     }
		}
        else if (navigator.userAgent.indexOf('MSIE') < 0)
	    { 
		    m_oXSLCache = new XmlDom();
	        m_oXSLCache.resolveExternals = false;
		    var success = m_oXSLCache.load(XSL_FILE);
		    cleanWhitespace(m_oXSLCache);
		    
		    // Load the stylesheet
		    if (!success)
		    {
			    var oError = new Object();
			    oError.number = oXSL.parseError.errorCode;
			    oError.description = ERR_CANNOT_LOAD_XML_FILE + XSL_FILE + "\n" + oXSL.parseError.reason;
			    throw oXSL.parseError;
		    }
	    }
	}

	//--------------------------------------------------------------------
	// Method: MarkOLPAvailableItems
	//
 	// Purpose: Adds an 'available' attribute to item elements to indicate
	//	    whether they are available offline within EK's player.
 	//
 	// Parameters:
	//	Object oXMLDOM [in] - Manifest XML DOM
	//
	// Returns: None
	//--------------------------------------------------------------------
	function MarkOLPAvailableItems()
	{
		try
		{
			var oNodes = m_oManifest.selectNodes("//dflt:item[not(@isvisible[. = 'false'] or @isvisible[. = '0'])]");
			var oItem = null;
			var sScoID = "";
			var oAtt = null;
			var bIsAvail = false;

            for(itemCount = 0; itemCount < oNodes.length; itemCount++)
			//while (oItem = oNodes.nextNode())
			{
			    oItem = oNodes[itemCount];
				sItemID = oItem.getAttribute("identifier");
				sScoID = m_oLMSAPI.LRNGetScoIdFromItemId(sItemID);

				if (sScoID)
				{
					bIsAvail = m_oLMSAPI.LRNIsTopicDownloaded(sScoID);

					// Create an 'available' attribute and add it to the existing item
					oAtt = m_oManifest.createAttribute("available");
					oAtt.value = bIsAvail.toString();
					oItem.attributes.setNamedItem(oAtt);

					if (bIsAvail && m_oLMSAPI.LRNIsTopicNode(sItemID))
					{
						// Make sure that the topic's parent (lesson) node is marked as available,
						// otherwise the topic cannot be launched except from EK's 'Course Content' page.
						oParentNode = FindParent(sItemID);
						if (oParentNode && oParentNode.getAttribute("available") == "false")
						{
							oParentNode.setAttribute("available","true");
						}
					}
				}
			}
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: GetOLPFirstAvailContentWheelItem
	//
 	// Purpose: Gets the first available content wheel item for a given 
	//	    lesson node when running within the offline player.  This
	//	    avoids a 404 page not found error, if the lesson (intro)
	//	    node has not been downloaded.
 	//
 	// Parameters: String sID [in] - Item identifier
	//
	// Returns: String
	//--------------------------------------------------------------------
	function GetOLPFirstAvailContentWheelItem(sID)
	{
		if (m_bIsHostOLP && m_oLMSAPI.LRNIsLessonNode(sID))
		{
			//var sScoID = m_oLMSAPI.GetEKScoIDFromItemID(sID);
			var sScoID = m_oLMSAPI.LRNGetScoIdFromItemId(sID);

			// If the lesson (content wheel intro item) hasn't been
			// downloaded, get the first available content wheel item
			if (sScoID && !m_oLMSAPI.LRNIsTopicDownloaded(sScoID))
			{
				var oChildren = m_oLMSAPI.LRNGetChildren(sID);
				var oChildNodes = oChildren.documentElement.childNodes;
				var oChild = null;

				while (oChild = oChildNodes.nextNode())
				{
					sID = oChild.getAttribute("ID");

					if (m_oLMSAPI.LRNIsItemAvailable(sID)) break;
				}
			}
		}

		return sID;
	}

	//--------------------------------------------------------------------
	// Method: FindParent
	//
 	// Purpose: Finds the parent node for the specified item identifier.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Object
	//--------------------------------------------------------------------
	function FindParent(sID)
	{
		var oParent = null;

		if ("string" == typeof(sID))
		{
			try
			{
				var oNode = FindItem(sID);
			 
				if (oNode.parentNode)
				{
					var oNodeParent = oNode.parentNode;
					switch (oNodeParent.nodeName)
					{
						case "item":
							oParent = oNodeParent;
							break;
						case "organization":
							var oParentItem = GetItemInParentManifest(oNodeParent);
							if (oParentItem) oParent = FindPrevious(oParentItem);
							break;
					}
				}
			}
			catch(e)
			{
			}
		}
			
		return oParent;
	}

	//--------------------------------------------------------------------
	// Method: FindItem
	//
 	// Purpose: Finds the item in the manifest that corresponds to the supplied
 	//	    identifier.
 	//
 	// Parameters:
	//	String sID [in] - Item identifier
	//
	// Returns: Object
	//--------------------------------------------------------------------		
	function FindItem(sID)
	{
		var oItem = null;

		if ("string" == typeof(sID))
		{
			try
			{				
				oItem = m_oManifest.selectSingleNode("//dflt:item[@identifier='" + sID + "']");
			}
			catch(e)
			{
			}
		}

		return oItem;
	}

	//--------------------------------------------------------------------
	// Method: GetItemInParentManifest
	//
 	// Purpose: Gets the item with the specified identifierref attribute.
 	//
 	// Parameters:
	//	Object oNode [in] - Node
	//
	// Returns: Object
	//--------------------------------------------------------------------
	function GetItemInParentManifest(oNode)
	{
		var oItem = null;

		try
		{
			oItem = oNode.selectSingleNode("ancestor::dflt:manifest//dflt:item[@identifierref='" + oNode.getAttribute("identifier") + "']");
		}
		catch(e)
		{
		}

		return oItem;
	}

	//--------------------------------------------------------------------
	// Method: FindNext
	//
 	// Purpose: Finds the next node relative to the current node.
 	//
 	// Parameters:
	//	Object oNode	 [in] - Node
	//	Boolean bDescend [in] - Flag to indicate whether to get child or sibling
	//
	// Returns: Object
	//--------------------------------------------------------------------
	function FindNext(oNode,bDescend)
	{
		var oNext = null;

		if ("boolean" == typeof(bDescend))
		{
			try
			{
				var oOrg = GetItemFromOrganization(oNode);
				if (bDescend && oOrg)
					oNext = oOrg;
				else if (bDescend && oNode.hasChildNodes())
				{
					oNext = GetItemFromOrganization(oNode.firstChild);
					if (!oNext) oNext = oNode.firstChild;
				}
				else if (oNode.nextSibling)
				{
					oNext = GetItemFromOrganization(oNode.nextSibling);
					if (!oNext) oNext = oNode.nextSibling;
				}
				else if (oNode.parentNode)
				{
					var oParent = oNode.parentNode;
					switch (oParent.nodeName)
					{
						case "item":
							oNext = FindNext(oParent,false);
							break;
						case "organization":
							oNext = FindNext(GetItemInParentManifest(oParent),false);
							break;
					}
				}
					
				if (oNext && (oNext.nodeName != "item" || !ItemIsVisible(oNext)))
					oNext = FindNext(oNext,false);

				if (oNext && m_oLMSAPI.LRNIsTopicNode(oNext.getAttribute("identifier")))
					oNext = FindNext(oNext,true);

				if (oNext && m_bIsHostOLP && !m_oLMSAPI.LRNIsItemAvailable(oNext.getAttribute("identifier")))
					oNext = FindNext(oNext,true);
					
	            if (oNext && ItemHasDefaultReference(oNext.getAttribute("identifierref")))
				    oNext = FindNext(oNext,true);
			}
			catch(e)
			{
			}
		}
			
		return oNext;
	}

	//--------------------------------------------------------------------
	// Method: GetItemFromOrganization
	//
 	// Purpose: Gets the item from a manifest organization
 	//
 	// Parameters:
	//	Object oNode [in] - Node
	//
	// Returns: Object
	//--------------------------------------------------------------------
	function GetItemFromOrganization(oNode)
	{
		var oItem = null;

		try
		{
			oItem = oNode.selectSingleNode("//dflt:organizations/dflt:organization[@identifier='" + oNode.getAttribute("identifierref") + "']").childNodes[0];
		}
		catch(e)
		{
		}
			
		return oItem;
	}

	//--------------------------------------------------------------------
	// Method: FindPrevious
	//
 	// Purpose: Finds the previous node relative to the current node.
 	//
 	// Parameters:
	//	Object oNode [in] - Node
	//
	// Returns: Object
	//--------------------------------------------------------------------
	function FindPrevious(oNode)	
	{
		var oPrev = null;

		try
		{
		    if (navigator.userAgent.indexOf('MSIE') < 0)
		        cleanWhitespace(oNode);

			if (oNode && m_oLMSAPI.LRNIsTopicNode(oNode.getAttribute("identifier")))
			{
				oPrev = oNode.parentNode.previousSibling;
			}
			else if (oNode.previousSibling)
			{
				if (ItemIsVisible(oNode.previousSibling))
				{
					oPrev = oNode.previousSibling;

					while (true)
					{
						var oOrg = oPrev.selectSingleNode("//dflt:organizations/dflt:organization[@identifier='" + oPrev.getAttribute("identifierref") + "']")
						if (oOrg) oPrev = oOrg;

						if (!ItemIsVisible(oPrev))
						{
							oPrev = FindPrevious(oPrev);
							break;
						}
						else
						{
							if ((oPrev.nodeName == "item" || oPrev.nodeName == "organization") && oPrev.lastChild && !m_oLMSAPI.LRNIsTopicNode(oPrev.lastChild.getAttribute("identifier")))
								oPrev = oPrev.lastChild;
							else
								break;
						}						
					}
				}
				else
				{
					oPrev = FindPrevious(oNode.previousSibling);
				}
			}
			else if (oNode.parentNode)
			{
				if (oNode.parentNode.nodeName == "organization")
					oPrev = FindPrevious(GetItemInParentManifest(oNode.parentNode));
				else
					oPrev = oNode.parentNode;
			}

			if (oPrev && (oPrev.nodeName != "item" || !ItemIsVisible(oPrev) || ItemHasDefaultReference(oPrev.getAttribute("identifierref")) || (m_bIsHostOLP && !m_oLMSAPI.LRNIsItemAvailable(oPrev.getAttribute("identifier")))))
				oPrev = FindPrevious(oPrev);
		}
		catch(e)
		{
		}
			
		return oPrev;
	}

    // remove empty whitespace nodes from DOM (newlines and tabs within original document)
    function cleanWhitespace(node) {
	for (var x = 0; x < node.childNodes.length; x++) {
		var childNode = node.childNodes[x];
		// If it's a whitespace text node, remove the node.
		if ((childNode.nodeType == 3) && (childNode.nodeValue.match(/\S/) == null)) {
			// If the previous or following nodes are not "a", "strong", "em", "u", "span".
			if(x!=0)
			{
			    if (node.childNodes[x - 1] != null) {
				    var previousSiblingNodeName = node.childNodes[x - 1].nodeName.toLowerCase();
			    } else { var previousSiblingNodeName = ""; }
			if (node.childNodes[x + 1] != null) {
				var followingSiblingNodeName = node.childNodes[x - 1].nodeName.toLowerCase();
			} else { var followingSiblingNodeName = "" }
			}else { var previousSiblingNodeName = ""; var followingSiblingNodeName = "";}

			if (previousSiblingNodeName != "a" && previousSiblingNodeName != "strong" &&
				previousSiblingNodeName != "em" && previousSiblingNodeName != "u" &&
				followingSiblingNodeName != "a" && followingSiblingNodeName != "strong" &&
				followingSiblingNodeName != "em" && followingSiblingNodeName != "u") {

				node.removeChild(childNode);
				x--;
			}
		} else if (childNode.nodeType == 1) {
			// elements can have text child nodes of their own
			cleanWhitespace(childNode);
		}
	}
}

	//--------------------------------------------------------------------
	// Method: ItemIsVisible
	//
 	// Purpose: Determines whether an item element is visible/hidden via
	//	    its 'isvisible' attribute.
 	//
 	// Parameters:
	//	Object oNode [in] - Node
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	function ItemIsVisible(oNode)
	{
		var bIsVisible = true;

		try
		{
			var sIsVisible = oNode.getAttribute("isvisible");
			if (sIsVisible == "false" || sIsVisible == "0")
				bIsVisible = false;
			
		}
		catch(e)
		{
		}

		return bIsVisible;
	}
	
	//--------------------------------------------------------------------
	// Method: ItemHasResource
	//
 	// Purpose: Determines whether an item element has a resource 
 	// associated with it.
 	//
 	// Parameters:
	//	Object oNode [in] - Node
	//
	// Returns: Boolean
	//--------------------------------------------------------------------
	function ItemHasResource(oNode)
	{
	    var bHasResource = true;
		try
		{
			var sResource = oNode.getAttribute("identifierref");
			if (sResource == "")
				bHasResource = false;
		}
		catch(e)
		{
		}
		return bHasResource;
	}
    //--------------------------------------------------------------------
    // Method: ItemHasDefaultReference
    //
    // Purpose: Determines whether an resource element refers to lesson based on the href 
    //attribute of its resource node.
    //
    // Parameters:
    //	String sID [in] - Resource identifier
    //
    // Returns: Boolean
    //--------------------------------------------------------------------
    function ItemHasDefaultReference(sId) 
    {
        var bHasDefaultReference = false;
        var sResource = m_oManifest.selectSingleNode("dflt:manifest/dflt:resources/dflt:resource[@identifier='" + sId + "']");
        var sHref = sResource.childNodes[0].getAttribute("href");
        if (sHref.indexOf("default.htm") > -1) 
        {
            bHasDefaultReference = true;
        }
        return bHasDefaultReference;
    }
   

	//--------------------------------------------------------------------
	// Method: TransformFromIMStoMLT
	//
 	// Purpose:
 	//	Transforms the manifest into an internal xml representation.
	//
 	// Parameters:
	//	String sID   [in] - Item identifier used to establish context
	//	Number nType [in] - Selection type
	//
	// Returns: Object
	//--------------------------------------------------------------------						
	function TransformFromIMStoMLT(sID,nType)
	{
		var oXMLResult = null;
        var ResultXMl =null;
		if ("number" == typeof(nType))
		{
			try
			{
				var sValue = sID;
				var nMode = 0;
				
				switch (nType)
				{
					case SELECT_SINGLEITEM:
						nMode = 1;
						break;
					case SELECT_ITEMCHILDREN:
					{
						// The children could be in another manifest.
						if (!sID)
						{
							sValue = m_sCurrentOrgID;
							nMode = 2;
						}
						else
							nMode = 3;

						break;
					}
					case SELECT_DESCENDANTS:
						nMode = 4;
						break;

					case SELECT_TOCS:
						sValue = "";
						nMode = 5;
						break;
						
					case SELECT_TOC:
						nMode = 6;
						break;
				}
								
				if (window.ActiveXObject)
                {			
				    // Do the transform using the cached stylesheet.
				    //
				    // FIXME: Find a way to pass-in the XPath expression as a single parameter
				    //	  rather than using nMode to select the XPath within the
				    //	  template.  The current implementation was chosen because apply-templates
				    // 	  doesn't like it when a parameter is used as the select expression.
				    var oXSLT = m_oXSLCache.createProcessor();
				    oXSLT.input = m_oManifest;
				    oXSLT.addParameter("query_mode",nMode); 
				    oXSLT.addParameter("query_param",sValue);
				    oXSLT.addParameter("bIsOLP",m_bIsHostOLP);
				    oXSLT.transform();
				    
				    // The resulting transformation XML.
				    oXMLResult = LoadXML(oXSLT.output);
				    
				    ResultXMl = oXMLResult.xml;
                }
				else if (navigator.userAgent.indexOf('MSIE') < 0)
		        {
		            if((m_oXSLCache.readyState == "complete" && m_oManifest.readyState == "complete") || (m_oXSLCache.readyState == 4 && m_oManifest.readyState == 4))
		            {
		                var oXSLTProcessor = new XSLTProcessor();
		                oXSLTProcessor.importStylesheet(m_oXSLCache);
                        oXSLTProcessor.setParameter(null, "query_mode", nMode);
                        oXSLTProcessor.setParameter(null, "query_param", sValue);
                        oXSLTProcessor.setParameter(null, "bIsOLP", m_bIsHostOLP);   
                        oXMLResult = oXSLTProcessor.transformToDocument(m_oManifest); 
				        ResultXMl = oXMLResult.xml;
				    }
		        }
				switch (nType)
				{
					case SELECT_SINGLEITEM:
					case SELECT_ITEMCHILDREN:
					case SELECT_DESCENDANTS:
						UpdateNodesetProperties(oXMLResult);
						break;
				}
			}
			catch(e)
			{
			}
		}

		return oXMLResult;
	}	

	//--------------------------------------------------------------------
	// Method: UpdateNodesetProperties
	//
 	// Purpose: Updates various properties that cannot be set during the
 	//	    transform as they require data only available at runtime
	//	    via the viewer's LMS API.
 	//
 	// Parameters:
	//	Object oXML [in] - XML DOM
	//
	// Returns: None
	//--------------------------------------------------------------------
	function UpdateNodesetProperties(oXML)
	{
		try
		{
			var oChildNodes = oXML.documentElement.childNodes;
			var oNode = null;

			for (var child = 0; child < oChildNodes.length; child++) {
			    oNode = oChildNodes[child];
			    // Update the href
			    var sHref = oNode.getAttribute("Href");
			    if (sHref) {
			        // For EK support, otherwise we have a circular reference to viewer.htm
			        if (sHref.indexOf("viewer.htm") > -1) {
			            var sItem = "LaunchPage";
			            var sValue = "";
			            var nItemLen = sItem.length;
			            var aQuery = sHref.split("?");

			            for (var i = 0; i < aQuery.length; i++) {
			                var nPos = aQuery[i].indexOf(sItem);
			                if (nPos > -1) sValue = unescape(aQuery[i].substring(nPos + nItemLen + 1, aQuery[i].length));
			            }

			            if (sValue != "") sHref = sValue;
			        }

			        if (!(sHref.indexOf("http:") != -1 || sHref.indexOf("https:") != -1 ||
				     	      sHref.indexOf("file:") != -1 || sHref.indexOf("\\\\") != -1))
			        // Build a fully-qualified URL
			            sHref = m_oLMSAPI.LRNGetManifestPath() + sHref;

			        // Get item 'parameters' attribute if present
			        var oItem = m_oManifest.selectSingleNode("//dflt:item[@identifier = '" + oNode.getAttribute("ID") + "']");

			        if (oItem && oItem.getAttribute("parameters")) {
			            // Add the parameters to the existing URL
			            // (Refer to IMSCP - Href URL Construction Algorithm)

			            var sParams = oItem.getAttribute("parameters");

			            if (sParams.charAt(0) == "?" || sParams.charAt(0) == "&")
			                sParams = sParams.substring(1);

			            if (sParams.charAt(0) == "#") {
			                if (sHref.indexOf("#") == -1)
			                    sHref += sParams;
			            }
			            else if (sHref.indexOf("?") > -1) {
			                sHref += "&" + sParams;
			            }
			            else {
			                sHref += "?" + sParams;
			            }
			        }
			    }
			    else {
			        // The Href was an empty string, so point to a blank page
			        var sPath = window.location.href;
			        var nPos = sPath.indexOf("viewer.htm");
			        if (nPos != -1) sHref = sPath.substring(0, nPos);

			        sHref += "viewer/blank.htm";
			    }

			    oNode.setAttribute("Href", NormalizeURL(sHref));

			    // Set the lesson status for the node
			    var sID = oNode.getAttribute("ID");
			    var sLessonStatus = "";

			    if (m_oLMSAPI.LRNIsViewerHosted()) {
			        sLessonStatus = m_oLMSAPI.LRNIsComplete(sID) ? "completed" : "not attempted";
			        oNode.setAttribute("LessonStatus", sLessonStatus);
			    }
			    else {
			        sLessonStatus = m_oLMSAPI.LRNGetScopedValue("cmi.core.lesson_status", sID);
			        oNode.setAttribute("LessonStatus", sLessonStatus);

			        // Indicate whether prerequisites have been met
			        var sPrereqsMet = "true";
			        var sPrerequisites = oNode.getAttribute("Prerequisites");
			        if (sPrerequisites) {
			            // Currently we only support simple prerequisites
			            var sPrereqStatus = m_oLMSAPI.LRNGetScopedValue("cmi.core.lesson_status", sPrerequisites);
			            if (!("completed" == sPrereqStatus || "passed" == sPrereqStatus))
			                sPrereqsMet = "false";
			        }
			        oNode.setAttribute("PrerequisitesMet", sPrereqsMet);

			        // Set datafromlms.
			        var sDataFromLMS = oNode.getAttribute("DataFromLMS");
			        if (sDataFromLMS) m_oLMSAPI.LRNSetScopedValue("cmi.launch_data", sDataFromLMS, sID, true);
			    }
			}
		}
		catch(e)
		{
		}	
	}

	//--------------------------------------------------------------------
	// Function: NormalizeURL
	//
	// Purpose: Unescapes, removes extra slashes, and converts the URL to
	//	    lower case.
	//
	// Parameters:
	//	String sURL [in] - Uniform Resource Locator
	//
	// Returns: String
	//--------------------------------------------------------------------
	function NormalizeURL(sURL)
	{
		var sTmpURL = unescape(sURL.toLowerCase());

		// Don't process a UNC path
		if (sTmpURL.substring[0,2] != "\\\\")
		{
			// Force all slashes forward
			var sReplace = "/";
			var oExp = new RegExp("\\\\","g");
			sTmpURL = sTmpURL.replace(oExp,sReplace);

			// Compile expression that tests for more than two forward slashes
			// following the protocol and colon.
			oExp = new RegExp();
			oExp.compile("([a-z]{3,}\:{1}\/{3,})","g");

			// Replace instances containing additional slashes.  We match within a loop
			// in the event that more than one protocol is present within the URL (ie. the
			// query string contains a URL).
			var bDone = false;
			while (!bDone)
			{
				var oMatches = sTmpURL.match(oExp);
				if (oMatches)
				{
					var sFound = oMatches[0];
					var nPos = sFound.indexOf("/");
					var sReplace = sFound.substr(0,nPos+2);
					var oExp2 = new RegExp(sFound,"g");
					sTmpURL = sTmpURL.replace(oExp2,sReplace);
				}
				else
					bDone = true;
			}
		}

		return sTmpURL;
	}

	//--------------------------------------------------------------------
	// Function: CreateNotesTreeViewXmlItemElement
	//
	// Purpose: Helper function that creates a new item element.
	//
	// Parameters:
	//	XMLDOM oXML [in] - XML DOM
	//	XmlNode oParent [in] - Parent node
	//	String sID [in] - ID attribute value
	//	String sHasNote [in] - hasNote attribute value
	//	String sIsAvail [in] - isAvailable attribute value
	//	String sText [in] - Child Text element value
	//
	// Returns: XmlNode
	//--------------------------------------------------------------------
	function CreateNotesTreeViewXmlItemElement(oXML, oParent, sID, sHasNote, sIsAvail, sText)
	{
		var NOTEBOOK_XML_ITEM_NODE = "Item";
		var NOTEBOOK_XML_ITEM_NODE_ID_ATTRIB = "ID";
		var NOTEBOOK_XML_ITEM_NODE_HASNOTE_ATTRIB = "hasNote";
		var NOTEBOOK_XML_ITEM_NODE_ISAVAILABLE_ATTRIB = "isAvailable";
		var NOTEBOOK_XML_TEXT_NODE = "Text"

		var oNewItem = oXML.createElement(NOTEBOOK_XML_ITEM_NODE);

		AddNodeAttribute(oXML, oNewItem, NOTEBOOK_XML_ITEM_NODE_ID_ATTRIB, sID);
		AddNodeAttribute(oXML, oNewItem, NOTEBOOK_XML_ITEM_NODE_HASNOTE_ATTRIB, sHasNote);
		AddNodeAttribute(oXML, oNewItem, NOTEBOOK_XML_ITEM_NODE_ISAVAILABLE_ATTRIB, sIsAvail);

		var oNewText = oXML.createElement(NOTEBOOK_XML_TEXT_NODE);
		oNewText.text = sText;
		oNewItem.appendChild(oNewText);

		oParent.appendChild(oNewItem);

		return oNewItem;
	}

	//--------------------------------------------------------------------
	// Function: AddNodeAttribute
	//
	// Purpose: Adds an attribute to an XML element.
	//
	// Parameters:
	//	XMLDOM oXML [in] - XML DOM
	//	XmlNode oElem [in] - Element to update
	//	String sAttrName [in] - Attribute name
	//	String sAttrVal [in] - Attribute value
	//
	// Returns: None
	//--------------------------------------------------------------------
	function AddNodeAttribute(oXML, oElem, sAttrName, sAttrVal)
	{
		var oAttrib = oXML.createAttribute(sAttrName);
		oAttrib.value = sAttrVal;
		oElem.attributes.setNamedItem(oAttrib);
	}
}


//////////////////////////////////////////////////////////////////////////////
// Class: ViewerEvents
//
// Purpose: Constructor function for ViewerEvents objects.  This class exposes
//	    methods which can be used to establish a delegate to receive event
//	    callbacks, remove a delegate, and raise events.
//	
// Parameters: None
//
// Returns: Object OR null
//
// Note: Can throw during instantiation
//////////////////////////////////////////////////////////////////////////////
function ViewerEvents()
{
	//////////////////////////////////////////////////////////////////////
	// PRIVATE VARIABLES
	//////////////////////////////////////////////////////////////////////
	var m_aListeners = new Array();

	//////////////////////////////////////////////////////////////////////
	// PUBLIC METHODS
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: ListenForEvents
 	//
 	// Purpose: Adds an event handler to the listeners array.
  	//
 	// Parameters:
	//	Object fHandler [in] - Event handler function
	//
	// Returns: None
	//--------------------------------------------------------------------
	 ViewerEvents.prototype.ListenForEvents=function(fHandler,nEvents)
	{
		if ("function" == typeof(fHandler))
		{
			var o = new Object();
			o.fHandler = fHandler;
			o.nEvents = nEvents;
			m_aListeners.push(o);
		}
	}	

	//--------------------------------------------------------------------
	// Method: UnListenForEvents
 	//
 	// Purpose: Removes the specified event handler from the listeners array.
  	//
 	// Parameters: 
	//	Object fHandler [in] - Event handler function
	//
	// Returns: None
	//--------------------------------------------------------------------
	 ViewerEvents.prototype.UnListenForEvents=function(fHandler)
	{
		try
		{
			if ("function" == typeof(fHandler))
			{
				for (o in m_aListeners)
				{
					if (m_aListeners[o].fHandler == fHandler)
						delete m_aListeners[o];
				}
			}
		}
		catch(e)
		{
		}
	}	

	//--------------------------------------------------------------------
	// Method: RaiseEvent
 	//
 	// Purpose: Raises the specified event by calling all functions previously
  	//	    registered via ListenForEvents().
	//
 	// Parameters:
	//	Number nType [in] - Event type
	//	String sID   [in] - Item identifier
	//
	// Returns: None
	//--------------------------------------------------------------------
	 ViewerEvents.prototype.RaiseEvent=function(nType,sID)
	{
		try
		{
			if ("number" == typeof(nType))
			{
				var oEvent = new Object();
				oEvent.type = nType;
				oEvent.id = sID;
			
				for (o in m_aListeners)
				{
					if ((nType & m_aListeners[o].nEvents) == nType)
						m_aListeners[o].fHandler(oEvent);
				}
			}		
		}
		catch(e)
		{
		}
	}	
}

//////////////////////////////////////////////////////////////////////////////
// Class: Timer
//
// Purpose: Constructor function for Timer objects.  This class exposes methods
//	    which can be used to obtain and format time values and deltas.
//
// Parameters: None
//
// Returns: Object OR null
//
// Note: Can throw during instantiation
//////////////////////////////////////////////////////////////////////////////
function Timer()
{
	//////////////////////////////////////////////////////////////////////
	// PRIVATE VARIABLES
	//////////////////////////////////////////////////////////////////////

	var m_oDateTime = new Date();
	
	//////////////////////////////////////////////////////////////////////
	// PUBLIC METHODS
	//////////////////////////////////////////////////////////////////////

	//--------------------------------------------------------------------
	// Method: ResetTimer
 	//
 	// Purpose: Reinitializes the timer.
  	//
 	// Parameters: None
	//
	// Returns: None
	//--------------------------------------------------------------------
	// Return Value: none
	 Timer.prototype.ResetTimer=function()
	{
		try
		{
			m_oDateTime = new Date();
		}
		catch(e)
		{
		}
	}

	//--------------------------------------------------------------------
	// Method: GetTimeDelta
 	//
 	// Purpose: Gets the number of seconds that have elapsed since the timer
 	//	    was reinitialized.
  	//
 	// Parameters: None
	//
	// Returns: Number
	//--------------------------------------------------------------------
	// Return Value: time delta in seconds
	 Timer.prototype.GetTimeDelta=function()
	{
		var nDelta = 0;

		try
		{
			nDelta = Math.round((new Date() - m_oDateTime)/1000);
		}
		catch(e)
		{
		}

		return nDelta;
	}

	//--------------------------------------------------------------------
	// Method: UnFormatTimeDelta
 	//
 	// Purpose: Converts a time string (formatted as HH:MM:SS) to a numeric
	//	    value denoting the corresponding seconds.
	//
 	// Parameters:
	//	String sTime [in] - Time string
	//
	// Returns: Number
	//--------------------------------------------------------------------
	 Timer.prototype.UnFormatTimeDelta=function(sTime)
	{
		var nDelta = 0;

		if ("string" == typeof(sTime))
		{
			try
			{
				var arrTime = sTime.split(":");
				
				// Coerse the hour, minute, and second strings to numeric values via multiplication
				if (3 == arrTime.length) nDelta = (arrTime[0] * 60 * 60) + (arrTime[1] * 60) + (arrTime[2] * 1);
			}
			catch(e)
			{
			}
		}

		return nDelta;
	}

	//--------------------------------------------------------------------
	// Method: FormatTimeDelta
 	//
 	// Purpose: Formats the supplied seconds value as HH:MM:SS.
  	//
 	// Parameters:
	//	Number nSeconds [in] - The time value to format.
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Timer.prototype.FormatTimeDelta=function(nSeconds)
	{
		var sDelta = "0:00:00";

		if ("number" == typeof(nSeconds) && nSeconds > 0)
		{
			try
			{
				// 1 hour = 60 minutes * 60 seconds 
				var nHours = Math.floor(nSeconds / (60 * 60));
				nSeconds = nSeconds - (nHours * 60 * 60);

				var nMinutes = Math.floor(nSeconds / 60);
				nSeconds = nSeconds - (nMinutes * 60);

				var sHours = nHours.toString();

				var sMinutes;
				if (10 > nMinutes)
					sMinutes = "0" + nMinutes.toString();  // single digit
				else
					sMinutes = nMinutes.toString();

				var sSeconds;
				if (10 > nSeconds)
					sSeconds = "0" + nSeconds.toString();  // single digit
				else
					sSeconds = nSeconds.toString();

				sDelta = sHours + ":" + sMinutes + ":" + sSeconds;
			}
			catch(e)
			{
			}
		}

		return sDelta;
	}

	//--------------------------------------------------------------------
	// Method: CalculateTimeDelta
 	//
 	// Purpose: Calculates a time delta based upon the supplied time.
  	//
 	// Parameters:
	//	String sOldTime [in] - Original time value
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Timer.prototype.CalculateTimeDelta=function(sOldTime)
	{
		var sDelta = sOldTime;

		try
		{
			var nTimeDelta = this.GetTimeDelta();
			this.ResetTimer();

			var nOldTime = this.UnFormatTimeDelta(sOldTime);
			sDelta = this.FormatTimeDelta(nTimeDelta + nOldTime);
		}
		catch(e)
		{
		}

		return sDelta;
	}

	//--------------------------------------------------------------------
	// Method: AddTime
 	//
 	// Purpose: Adds two time values of the format HH:MM:SS.
  	//
 	// Parameters:
	//	String sTime1 [in] - First time value
	//	String sTime2 [in] - Second time value
	//
	// Returns: String
	//--------------------------------------------------------------------
	 Timer.prototype.AddTime=function(sTime1,sTime2)
	{
		var sTimeSum = sTime1;

		try
		{
			sTimeSum = this.FormatTimeDelta(this.UnFormatTimeDelta(sTime1) + this.UnFormatTimeDelta(sTime2));
		}
		catch(e)
		{
		}

		return sTimeSum;
	}
}
// SIG // Begin signature block
// SIG // MIIbDAYJKoZIhvcNAQcCoIIa/TCCGvkCAQExCzAJBgUr
// SIG // DgMCGgUAMGcGCisGAQQBgjcCAQSgWTBXMDIGCisGAQQB
// SIG // gjcCAR4wJAIBAQQQEODJBs441BGiowAQS9NQkAIBAAIB
// SIG // AAIBAAIBAAIBADAhMAkGBSsOAwIaBQAEFBAhACurAnUh
// SIG // MDuq9XNStr0NXa2noIIV5zCCBIUwggNtoAMCAQICCmEI
// SIG // d18AAAAAAEowDQYJKoZIhvcNAQEFBQAweTELMAkGA1UE
// SIG // BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
// SIG // BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
// SIG // b3Jwb3JhdGlvbjEjMCEGA1UEAxMaTWljcm9zb2Z0IENv
// SIG // ZGUgU2lnbmluZyBQQ0EwHhcNMTAwNzE5MjI1MzEwWhcN
// SIG // MTExMDE5MjI1MzEwWjCBgzELMAkGA1UEBhMCVVMxEzAR
// SIG // BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
// SIG // bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
// SIG // bjENMAsGA1UECxMETU9QUjEeMBwGA1UEAxMVTWljcm9z
// SIG // b2Z0IENvcnBvcmF0aW9uMIIBIjANBgkqhkiG9w0BAQEF
// SIG // AAOCAQ8AMIIBCgKCAQEAzH6Ae0H6i7RNmSlD3eb4Ut9b
// SIG // Qpe9Mxmc77Xa0MDc6PG3vlzhCrLCNaB+5PPUOKfyD0Jy
// SIG // p7qbzx8k3lYNhHPM02P3F82vaW9hTUyAT8h3HFFkyNME
// SIG // 8xQms7hCPzNDsNsmWViTtW2JYg8HVlvVEqCxCcwWK/mM
// SIG // 32FW6AZ4f9dziCYDZ9Gj/zSCrC1M2ALs7W+5ojHFMky7
// SIG // 9v1fYlte8fZkkceFd0cc9qcrRy0LN1RPth8EjbXsYCen
// SIG // 8362fKFMkaaLcdbGNWFHvMEVS92sMyK+BhmWS4oebgDr
// SIG // vh5p3VhZIQMrPL7mEwUXZjWnhi0a8dk0wAGAGvyaRp1u
// SIG // pOdI3DXqNwIDAQABo4IBAjCB/zATBgNVHSUEDDAKBggr
// SIG // BgEFBQcDAzAdBgNVHQ4EFgQU5G9fyon5U77wcFcGUKrC
// SIG // eQ2sVpswDgYDVR0PAQH/BAQDAgeAMB8GA1UdIwQYMBaA
// SIG // FFdFdBxdsPbIQwXgjFQtjzKn/kiWMEkGA1UdHwRCMEAw
// SIG // PqA8oDqGOGh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9w
// SIG // a2kvY3JsL3Byb2R1Y3RzL0NvZGVTaWdQQ0EuY3JsME0G
// SIG // CCsGAQUFBwEBBEEwPzA9BggrBgEFBQcwAoYxaHR0cDov
// SIG // L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9Db2Rl
// SIG // U2lnUENBLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAlkFd
// SIG // PVwrpM64KkUvQgJeoZAcTHQnCc4LVH1wKEFyO6QVvNEE
// SIG // Ulevd3RtC1VXhzcQSR7W5tTCY5S0/aQuLfpf4hlZFYTZ
// SIG // t7bpUqJS3iNfIC9Z06sahH8d473qD98BiDkl2O1kYRkb
// SIG // eq+mFEpGUEjZckvVS5Ld8A+EfMvTxvJMeG79vP9mEAr3
// SIG // A10V6l6YDAnSmY8utSC9YebJbwUUEct7+0IIvQPxJldn
// SIG // TY9Zi7hEx0XTYgQMLTADWF7sSPRuqBQxBP00orgELQLd
// SIG // OqEJXq1hsY9wwHhVI2Oon1l3otu5G3EoHPwu4QSPte80
// SIG // vkqZR+NkUhY8+r3g6OAUDBAiNIKCHjCCBMowggOyoAMC
// SIG // AQICCmEEs/UAAAAAAA0wDQYJKoZIhvcNAQEFBQAwdzEL
// SIG // MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
// SIG // EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
// SIG // c29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9z
// SIG // b2Z0IFRpbWUtU3RhbXAgUENBMB4XDTA4MDcyNTE5MTM0
// SIG // NVoXDTExMDcyNTE5MjM0NVowgbMxCzAJBgNVBAYTAlVT
// SIG // MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
// SIG // ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
// SIG // YXRpb24xDTALBgNVBAsTBE1PUFIxJzAlBgNVBAsTHm5D
// SIG // aXBoZXIgRFNFIEVTTjo5RTc4LTg2NEItMDM5RDElMCMG
// SIG // A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
// SIG // ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
// SIG // AKtGL7yoFPL4Rj8MBaGkY0UFf5rpM3LMJ9My7JjD4DSI
// SIG // hc6hEpeuSFBOSCB/cORZM/yZW7kHgEgovI24+mYCZrXx
// SIG // fxxQliiTVY2BVapwW6XJzu8u4uclw3ZvKQufLMhTgJrB
// SIG // DeO9p0W/Md/mhot9iQGLHHhYOLLdk0pbnh3XnMbwxE3Q
// SIG // vydKB5QEcQKIDXDm+CrnDmHjmEvR3atUtjf7xkb9pBy8
// SIG // /6/sspeA3LiT/bgiqJ0lWXA+XHscQSn+c5kiSYHXVSbA
// SIG // 7PJOKFRiO3AtYZMdUNWfAcIFw1UaSoekAIZUoHtisuoP
// SIG // +l1cxSoRgpe+hhsdq7erCin8zM00ib9Atk8CAwEAAaOC
// SIG // ARkwggEVMB0GA1UdDgQWBBSghTFeh64PqgoE6+HA02S9
// SIG // PLtp9TAfBgNVHSMEGDAWgBQjNPjZUkZwCu1A+3b7syuw
// SIG // wzWzDzBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vY3Js
// SIG // Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9N
// SIG // aWNyb3NvZnRUaW1lU3RhbXBQQ0EuY3JsMFgGCCsGAQUF
// SIG // BwEBBEwwSjBIBggrBgEFBQcwAoY8aHR0cDovL3d3dy5t
// SIG // aWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNyb3NvZnRU
// SIG // aW1lU3RhbXBQQ0EuY3J0MBMGA1UdJQQMMAoGCCsGAQUF
// SIG // BwMIMA4GA1UdDwEB/wQEAwIGwDANBgkqhkiG9w0BAQUF
// SIG // AAOCAQEAR3FPtkhFwAjrnC/TJVvbZ1ERkqbZ2bIUmibC
// SIG // /QbBzNeI9aY1tc0rtmtYas35nfNa60YvofqnH316QrKw
// SIG // 0RoJKFRsb5mLOPw65MEkqnJMEgQv8dR6djvFz3EnC238
// SIG // OGenxSJiK3t/wXasp8UmTKmvmN2KOF3PZaSBLqnvVgJM
// SIG // uQi+ZcQeiyEtbqfydWS03Bpr0PEl5cLngEQ51C8KeX4B
// SIG // Kqd4W6NrW84J7zk6ObfEZ1O5qvNh/515489IZ/+ryMYr
// SIG // YgmU0B+iePdzSTtYU8EJq0wGC9VAKH72sWbFv1LEvj47
// SIG // 4PmOdI3mIgzBxjOYtXnoDPcAfOqyTsyAmhYtyKQQBTCC
// SIG // BgcwggPvoAMCAQICCmEWaDQAAAAAABwwDQYJKoZIhvcN
// SIG // AQEFBQAwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZMBcG
// SIG // CgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UEAxMk
// SIG // TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9y
// SIG // aXR5MB4XDTA3MDQwMzEyNTMwOVoXDTIxMDQwMzEzMDMw
// SIG // OVowdzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
// SIG // bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
// SIG // FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMY
// SIG // TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBMIIBIjANBgkq
// SIG // hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn6Fssd/bSJIq
// SIG // fGsuGeG94uPFmVEjUK3O3RhOJA/u0afRTK10MCAR6wfV
// SIG // VJUVSZQbQpKumFwwJtoAa+h7veyJBw/3DgSY8InMH8sz
// SIG // JIed8vRnHCz8e+eIHernTqOhwSNTyo36Rc8J0F6v0LBC
// SIG // BKL5pmyTZ9co3EZTsIbQ5ShGLieshk9VUgzkAyz7apCQ
// SIG // MG6H81kwnfp+1pez6CGXfvjSE/MIt1NtUrRFkJ9IAEpH
// SIG // ZhEnKWaol+TTBoFKovmEpxFHFAmCn4TtVXj+AZodUAiF
// SIG // ABAwRu233iNGu8QtVJ+vHnhBMXfMm987g5OhYQK1HQ2x
// SIG // /PebsgHOIktU//kFw8IgCwIDAQABo4IBqzCCAacwDwYD
// SIG // VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUIzT42VJGcArt
// SIG // QPt2+7MrsMM1sw8wCwYDVR0PBAQDAgGGMBAGCSsGAQQB
// SIG // gjcVAQQDAgEAMIGYBgNVHSMEgZAwgY2AFA6sgmBAVieX
// SIG // 5SUT/CrhClOVWeSkoWOkYTBfMRMwEQYKCZImiZPyLGQB
// SIG // GRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0
// SIG // MS0wKwYDVQQDEyRNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp
// SIG // Y2F0ZSBBdXRob3JpdHmCEHmtFqFKoKWtTHNY9AcTLmUw
// SIG // UAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC5taWNy
// SIG // b3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvbWljcm9z
// SIG // b2Z0cm9vdGNlcnQuY3JsMFQGCCsGAQUFBwEBBEgwRjBE
// SIG // BggrBgEFBQcwAoY4aHR0cDovL3d3dy5taWNyb3NvZnQu
// SIG // Y29tL3BraS9jZXJ0cy9NaWNyb3NvZnRSb290Q2VydC5j
// SIG // cnQwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcN
// SIG // AQEFBQADggIBABCXisNcA0Q23em0rXfbznlRTQGxLnRx
// SIG // W20ME6vOvnuPuC7UEqKMbWK4VwLLTiATUJndekDiV7uv
// SIG // WJoc4R0Bhqy7ePKL0Ow7Ae7ivo8KBciNSOLwUxXdT6uS
// SIG // 5OeNatWAweaU8gYvhQPpkSokInD79vzkeJkuDfcH4nC8
// SIG // GE6djmsKcpW4oTmcZy3FUQ7qYlw/FpiLID/iBxoy+cwx
// SIG // SnYxPStyC8jqcD3/hQoT38IKYY7w17gX606Lf8U1K16j
// SIG // v+u8fQtCe9RTciHuMMq7eGVcWwEXChQO0toUmPU8uWZY
// SIG // sy0v5/mFhsxRVuidcJRsrDlM1PZ5v6oYemIp76KbKTQG
// SIG // dxpiyT0ebR+C8AvHLLvPQ7Pl+ex9teOkqHQ1uE7FcSMS
// SIG // JnYLPFKMcVpGQxS8s7OwTWfIn0L/gHkhgJ4VMGboQhJe
// SIG // GsieIiHQQ+kr6bv0SMws1NgygEwmKkgkX1rqVu+m3pmd
// SIG // yjpvvYEndAYR7nYhv5uCwSdUtrFqPYmhdmG0bqETpr+q
// SIG // R/ASb/2KMmyy/t9RyIwjyWa9nR2HEmQCPS2vWY+45CHl
// SIG // tbDKY7R4VAXUQS5QrJSwpXirs6CWdRrZkocTdSIvMqgI
// SIG // bqBbjCW/oO+EyiHW6x5PyZruSeD3AWVviQt9yGnI5m7q
// SIG // p5fOMSn/DsVbXNhNG6HY+i+ePy5VFmvJE6P9MIIGgTCC
// SIG // BGmgAwIBAgIKYRUIJwAAAAAADDANBgkqhkiG9w0BAQUF
// SIG // ADBfMRMwEQYKCZImiZPyLGQBGRYDY29tMRkwFwYKCZIm
// SIG // iZPyLGQBGRYJbWljcm9zb2Z0MS0wKwYDVQQDEyRNaWNy
// SIG // b3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw
// SIG // HhcNMDYwMTI1MjMyMjMyWhcNMTcwMTI1MjMzMjMyWjB5
// SIG // MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
// SIG // bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
// SIG // cm9zb2Z0IENvcnBvcmF0aW9uMSMwIQYDVQQDExpNaWNy
// SIG // b3NvZnQgQ29kZSBTaWduaW5nIFBDQTCCASIwDQYJKoZI
// SIG // hvcNAQEBBQADggEPADCCAQoCggEBAJ+N34U3jLEGVmY2
// SIG // TtQK6zYjkR6vq41Y6cTPwZ86CiTExMResO4VFMX6Ppkz
// SIG // axlpcvR8BV/lmSVE1zUZipDcKI6QDO/VkGGt3twYcC2D
// SIG // yW+vhSz43bm5MP1xeByD/cipuYJnDXq4wQnCvBaXVz8c
// SIG // mNNp1zCQkJCNMB/YJtSee4jZ4gntl0lF6wRSgXV7uLUi
// SIG // uC5Wu9fPqU48CoOiqDrFrbYBDmrPIrQjH2zVJ+e3fsG0
// SIG // 1TKteSUcQobinYX0V9obWS8tJpIpxbPXhaOeYFqU5nuc
// SIG // e16CNEtt/9TuJ9Ci3NGQDM59DhV8iKw0x8BsmIRKyUR5
// SIG // aCjfIoMNuIosSGHi/hECAwEAAaOCAiMwggIfMBAGCSsG
// SIG // AQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRXRXQcXbD2yEMF
// SIG // 4IxULY8yp/5IljALBgNVHQ8EBAMCAcYwDwYDVR0TAQH/
// SIG // BAUwAwEB/zCBmAYDVR0jBIGQMIGNgBQOrIJgQFYnl+Ul
// SIG // E/wq4QpTlVnkpKFjpGEwXzETMBEGCgmSJomT8ixkARkW
// SIG // A2NvbTEZMBcGCgmSJomT8ixkARkWCW1pY3Jvc29mdDEt
// SIG // MCsGA1UEAxMkTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
// SIG // dGUgQXV0aG9yaXR5ghB5rRahSqClrUxzWPQHEy5lMFAG
// SIG // A1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwubWljcm9z
// SIG // b2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL21pY3Jvc29m
// SIG // dHJvb3RjZXJ0LmNybDBUBggrBgEFBQcBAQRIMEYwRAYI
// SIG // KwYBBQUHMAKGOGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
// SIG // bS9wa2kvY2VydHMvTWljcm9zb2Z0Um9vdENlcnQuY3J0
// SIG // MHYGA1UdIARvMG0wawYJKwYBBAGCNxUvMF4wXAYIKwYB
// SIG // BQUHAgIwUB5OAEMAbwBwAHkAcgBpAGcAaAB0ACAAqQAg
// SIG // ADIAMAAwADYAIABNAGkAYwByAG8AcwBvAGYAdAAgAEMA
// SIG // bwByAHAAbwByAGEAdABpAG8AbgAuMBMGA1UdJQQMMAoG
// SIG // CCsGAQUFBwMDMA0GCSqGSIb3DQEBBQUAA4ICAQAwvLAg
// SIG // pGKgp+85JmE93KzGmdCGxC71gzJlXiI+m9aG+Oi2n8qL
// SIG // 1jt1C6GRPkzdZHSMSfKIjBhnbP4VZka4OkZCl8iRN9Qk
// SIG // sees0+pBFIN308lPBV+jFFK/lqQvPlZbEHXOU8POBVRp
// SIG // tGXJJKUP4SW4GrlN5QK5UB5Ps5gMHZUC7iJZrSLLBXQL
// SIG // BEV7BFng2A+z60z4YN3CeJ7Rup9r9/PufkQRQNK9uptL
// SIG // FghupL5V5KY4EqNI9BxVeoog0X3+kduUjy/Ce2umZIVP
// SIG // o+UsNCldC7/1xzgvxCDEVjH2ac6F+AqR7NDWrro4BQzr
// SIG // bk9MnAMpqqL8GKApDA1cXFYjV9oclg3IJjbBRMvl4eZv
// SIG // ieeP6Zi1c9N44+2jATx05V68bPYhiWcF7JedtbH9r6bp
// SIG // cqXDNOEvn/n0ajniLQSCW/zQnK58nRH55rVTGXS6OUo5
// SIG // 631Cs0o7Nz3CSnsnmOfiTpsbSlQ4aiM3vmq3SO7qQg1J
// SIG // JJGOtwQul2/k50W7j039YNnXWcLYgNZgNHu3oZMg/oG4
// SIG // qqVcCemKDb4oTX7X6A/tZXjRMV+5ZtvfQucLzAIHjd//
// SIG // IAajRWW0szKNLpHiTbSpyfq8awQOsp/qn96kyQqW9I33
// SIG // 2Jio8IUCCFmkIKYsCxryUgbtaeVkGBvgo6veynwUYUO4
// SIG // ZfU2o1UTK2csTRswTDGCBJEwggSNAgEBMIGHMHkxCzAJ
// SIG // BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
// SIG // DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
// SIG // ZnQgQ29ycG9yYXRpb24xIzAhBgNVBAMTGk1pY3Jvc29m
// SIG // dCBDb2RlIFNpZ25pbmcgUENBAgphCHdfAAAAAABKMAkG
// SIG // BSsOAwIaBQCggb4wGQYJKoZIhvcNAQkDMQwGCisGAQQB
// SIG // gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
// SIG // ARUwIwYJKoZIhvcNAQkEMRYEFKbkL6cu6K6nfc5ondBl
// SIG // HLpElnphMF4GCisGAQQBgjcCAQwxUDBOoCaAJABNAGkA
// SIG // YwByAG8AcwBvAGYAdAAgAEwAZQBhAHIAbgBpAG4AZ6Ek
// SIG // gCJodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vbGVhcm5p
// SIG // bmcgMA0GCSqGSIb3DQEBAQUABIIBADDs1e4TtcU6WKGh
// SIG // zTBAPOgRDa6bSY2araTey0+QnIBSi198pzw97jBy4twg
// SIG // cFMNnGGKodHazWz/YBHRjB1GgHDTUfmtTLHxa0rJFdWt
// SIG // qV4/AvU6/T1z/gPvimasjstSAdPcY9SN2dL7tpfP+Xz3
// SIG // 5sxybwBa1/qHc8etftQRr1G+BXSiLdoSBiYJ9fdpFxJA
// SIG // gi2ZWktJUS03Jdm1eL/qSBP+Xom7I7HpJIaKmvB1XCA9
// SIG // 19N3lehBRvWKCVilFEgoX7NmfNzN17Qx2CUjyXU/0qVS
// SIG // T3NJJwQo5e4R0pcF4QjgeW8NfI4W7f6WNljW5PizS1ys
// SIG // nwFuTT3+DqYH+jG3Gm6hggIdMIICGQYJKoZIhvcNAQkG
// SIG // MYICCjCCAgYCAQEwgYUwdzELMAkGA1UEBhMCVVMxEzAR
// SIG // BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
// SIG // bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
// SIG // bjEhMB8GA1UEAxMYTWljcm9zb2Z0IFRpbWUtU3RhbXAg
// SIG // UENBAgphBLP1AAAAAAANMAcGBSsOAwIaoF0wGAYJKoZI
// SIG // hvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUx
// SIG // DxcNMTAxMTEwMTc1MDU1WjAjBgkqhkiG9w0BCQQxFgQU
// SIG // L7FY909qTFWMBq8lq2Shi+qRFXwwDQYJKoZIhvcNAQEF
// SIG // BQAEggEAjbb0ViHmjR2FPKQAToi4cIrnvJxtLPEjc0Ig
// SIG // wlDKy6Zr5VvTS3cOiH9IhO1LUnMLrhvaR/iSCLJ0S05i
// SIG // iz1xVAXVIWLpDCEIyX7Zwy+t8fikEDN59fjjPq+wBD8y
// SIG // 8Ma5CL2/YDAR9sjcLb+MKvEFeySdf69fA3CeG2rn6MPM
// SIG // W4zPWmJI8ACKWPSrZfaFSE0aETPe2ctk2rUwJBs5OHRS
// SIG // CghaIBkG+//8udKc3byk9QRAVC+IZwzJ7pqunju4ohis
// SIG // Zr1sHzzCg1pE6ihVV7RRTjD3WR6CacV43VscuSKPzLvu
// SIG // rktkobSBLhfTpLBA6lM/s1haOZplo4Q4CCMVZqOg3A==
// SIG // End signature block
