//
// display.js
//

// Call Scripts onload
window.onload = function() { 
	addHighlight('navigation', 'highlight');
	stripIndexLinks('container');
} ;



//
// layout.js
//


//====== Documentation ======//

//
// addHighlight(idToSearch, highlightClass)
// Applies $highlightClass to links matching the current URL in $idToSearch
//
// collapseNavigation(navigationId)
// Applies special  styles to <li>s above the link to the current page.
// Use with CSS to achieve navigation that only shows the current section.
//
// equalizeHeight(id0, id1, ..., idN)
// Set heights of elements (by id) to the largest height of the group.
//
// highlightSection(idToSearch, highlightClass, idToIgnore)
// Applies $highlightClass to links matching the current section in $idToSearch
//
// preload(img0, img1, ..., imgN)
// Pre-loads images (or whatever) to be used later.
//
// removeChildrenOver(maxItem, parentId)
// Removes HTML nodes inside $parentId except the first $maxItem items.
// 
// removeItemsOver(maxItem, idPrefix)
// Removes nodes from the document with id "{$idPrefix}NN" where NN > $maxItem
//
// setHeight(id, height)
// Sets the style.height of $id to $height.
//
// setWidth(id, width)
// Sets the style.width of $id to $width.
//
// setHeightToDifference(idToChange, idToCompare1, idToCompare2)
// Sets $idToChange to the difference between $idToCompare1 and $idToCompare2.
//
// setWidthToDifference(idToChange, idToCompare1, idToCompare2)
// Sets $idToChange to the difference between $idToCompare1 and $idToCompare2.
//
// stripIndexLinks(id)
// Strips unnecessary index.htm/l component from links in $id.
//
// tableAltRows(idToSearch, highlightClass)
// Applies $highlightClass to every other <tr> in $idToSearch
//



//====== Notes ======//

//
// Changing width/height
//
// Functions use element.offsetHeight, which adds top/bottom border and padding
// thickness, to determine element size.  Uses element.style.height to set
// height, which does not include border/padding.  In practice this means even
// the tallest element will expand by its top/bottom border/padding thickness.
//



//====== Public Functions ======//

//
// addHighlight(idToSearch, highlightClass)
//
// Applies a style to links matching the current URL
// Used to simulate the "current page" feeling in navigation
//
// Variables:
//     idToSearch = what id to look in
//     highlightClass = the class to apply to matching anchors
//
// Notes:
//     * The [highlightClass] has to be defined in the CSS (obviously)
//     * Does not remove the <a> itself (i.e., still a link, just looks different)
//     * Only searchs by id, not class, HTML tag, or any other mechanism
//
function addHighlight(idToSearch, highlightClass) {
	if(!document.getElementById) return ;							// browser detect DOM support
	
	var selfLinks = _getSelfLinks(idToSearch) ;
	for(var i=0; i<selfLinks.length; i++) {
		selfLinks[i].className = highlightClass ;
	}
}


//
// collapseNavigation(navigationId)
//
// Assumes navigation is a series of nested <li>.  Finds the current link
// (see _getSelfLinks()) and applies a "$navigationId-active" style to its
// <li>.  Then traverses up the DOM tree applying "$navigationId-open" to
// each ancestor <li>.  All other <li>s are given a style of "$navigationId-closed"
// Use with CSS to achieve navigation that only shows the current section.
// 
function collapseNavigation(navigationId) {
	if(!document.getElementById) return ;							// detect DOM support
	
	// Apply $navigationId-closed to all <li>
	var container = document.getElementById(navigationId) ;
	var listItems = container.getElementsByTagName("LI") ;
	for(var i=0; i<listItems.length; i++) {
		var currentNode = listItems[i] ;
		currentNode.className = navigationId + "-closed" ;
	}
	
	var selfLinks = _getSelfLinks(navigationId) ;
	for(var i=0; i<selfLinks.length; i++) {
	
		//
		// For each active link (links to the current page) give its
		// parent <li> class="$id-active" and all <li> above it in the tree
		// class="$id-open".  Once we hit an element with id="$id", exit.
		//
		
		var currentNode = selfLinks[i].parentNode ;
		if(currentNode.nodeName == "LI") currentNode.className = navigationId + "-active" ;
		
		while(currentNode.id != navigationId) {
			currentNode = currentNode.parentNode ;
			if(currentNode.nodeName == "LI") currentNode.className = navigationId + "-open" ;
		}
	}
}


//
// equalizeHeight(id0, id1, ..., idN)
//
// Used to set heights of elements (by id) to the largest height of the group.
// Simulates <td> rows of table-based layouts.  Call equalizeHeight() with the
// ids you want to equalize as parameters.
//
// Example:
//     equalizeHeight("leftColumn", "rightColumn", "mainColumn") ;
//
function equalizeHeight() {
	if(!document.getElementById) return ;
	
	var elements = equalizeHeight.arguments ;
	var height = 0 ;
	for(var i=0; i<elements.length; i++) {
		var element = document.getElementById(elements[i]) ;
		if(element != null) {
			height = Math.max(height, element.offsetHeight) ;
		}
	}
	for(var i=0; i<elements.length; i++) {
		if(element != null) {
			setHeight(elements[i], height) ;
		}
	}
}


//
// highlightSection(idToSearch, highlightClass, idToIgnore)
//
// Applies a style to the link that "looks like" the current section.  Used
// with CSS to simulate the "current section" feeling in navigation.
// 
// Assumes the URL with the fewest slashes is the current section (e.g.,
// index.html over ../people/index.html).  See _getSectionLink (below) for
// details.  Ignores the $idToIgnore when choosing a link, usually used to
// prevent the back to home link from getting highlighted.
//
function highlightSection(idToSearch, highlightClass, idToIgnore) {
	if(!document.getElementById) return ;							// browser detect DOM support
	
	// Get the section link candidate
	sectionLink = _getSectionLink(idToSearch, idToIgnore) ;
	
	// If we found one...
	if(sectionLink != null) {
		// Highlight it
		sectionLink.className = highlightClass ;
		
	// ...else we didn't find one, try to highlight the current page
	} else {
		var selfLinks = _getSelfLinks(idToSearch) ;
		
		// If there is only one candidate...
		if(selfLinks.length == 1) {
			// ...and it is not the $idToIgnore
			if(selfLinks[0].id != idToIgnore) {
				// Highlight it
				selfLinks[0].className = highlightClass ;
			}
		}
	}
}


//
// preload(img0, img1, ..., imgN)
//
// Pre-loads images (or whatever) to be used later by JavaScript or CSS effects.
// More or less a clone of MM_preloadImages v3.0 with variable names changed and
// comments added for readability.
//
function preload() {
	// if there is an images collection in this document
	if(document.images) {
		// Create an array to preload images in
		if(!document.preloader) document.preloader = new Array() ;
		
		// Keep track of where the preloader array index is
		var preloaderIndex = document.preloader.length ;
		
		// The batch of things to preload is the arguments to this function
		var preloadBatch = preload.arguments ;
		
		// iterate through the preloadBatch, adding each to the preloader
		for(var i=0; i<preloadBatch.length; i++) {
			//if (preloadBatch[i].indexOf("#") != 0) {	// Not sure why MM_preloadImages v3.0 has this
			document.preloader[preloaderIndex] = new Image ;
			document.preloader[preloaderIndex].src = preloadBatch[i] ;
			preloaderIndex++ ;	// increment preloader array index
			//}
		}
	}
}


//
// removeChildrenOver(maxItem, parentId)
// 
// Removes all HTML nodes inside the $parentId element except the first
// $maxItem items.  Used to limit the number of children displayed on a
// feed at display time (e.g., events form PlanIt Purple).
//
function removeChildrenOver(maxItem, parentId) {
	if(!document.getElementById) return ;							// browser detect DOM support
	
	if(maxItem==null) return;										// Required parameter
	if(maxItem < 1 || !_isInt(maxItem)) return;						// Sanity check
	if(parentId==null) return;										// Required parameter
	//alert("removeChildrenOver("+maxItem+", '"+parentId+"')");
	
	var parent = document.getElementById(parentId) ;
	if(parent==null) return;
	
	// Track childrenToRemove in an array
	var childrenToRemove = new Array();
	
	var htmlNodeCount = 0;
	for(var i=0; i<parent.childNodes.length; i++) {
		//alert(parent.childNodes[i].tagName) ;
		if(parent.childNodes[i].tagName != undefined) {
			if(htmlNodeCount >= maxItem) {
				//alert("remove "+htmlNodeCount+": " + parent.childNodes[i]);
				childrenToRemove.push(parent.childNodes[i]);
			} else {
				//alert("keep "+htmlNodeCount+": " + parent.childNodes[i]);
			}
			htmlNodeCount++ ;
		}
	}
	
	//alert("Iterate Children to Remove") ;
	for(var i=0; i<childrenToRemove.length; i++) {
		//alert(i + ": " + childrenToRemove[i]) ;
		parent.removeChild(childrenToRemove[i]) ;
	}
}


//
// removeItemsOver(maxItem, idPrefix)
// 
// Removes all nodes from the document with an id of {$idPrefix}NN where
// NN runs from $maxItem+1 until a node is not found.  Used to limit the
// number of children displayed on a feed at display time (e.g., events
// form PlanIt Purple).
// 
// The default value of $idPrefix is 'item.'  If you call
// removeItemsOver(5) then 'item6' will be removed, then 'item7,' etc. 
// If an id is missing the script will terminate without deleting the
// other high nodes.  E.g., in the above example if 'item8' is missing
// the script will terminate and never delete 'item9' if it is present.
//
function removeItemsOver(maxItem, idPrefix) {
	if(!document.getElementById) return ;							// browser detect DOM support
	
	if(maxItem==null) return;										// Required parameter
	if(maxItem < 1 || !_isInt(maxItem)) return;						// Sanity check
	if(idPrefix==null) idPrefix='item';								// Default value for optional parameter
	
	for(var i=maxItem+1; true; i++) {
		element = document.getElementById(idPrefix + i + "") ;
		if(element==null) break;
		element.parentNode.removeChild(element);
	}
}


//
// setHeight(id, height)
//
// Sets the style.height of $id to $height.  SLSIA
//
function setHeight(id, height) {
	element = document.getElementById(id) ;
	element.style.height = height + "px" ;
}


//
// setHeightToDifference(idToChange, idToCompare1, idToCompare2)
//
// Sets $idToChange to the difference between $idToCompare1 and $idToCompare2.
// Used to make a filler block expand as large as needed.
//
// Example:
//     setHeightToDifference("buffer-above-content", "content", "main-column") ;
//
// Makes the most sense to use this after something has been equalized.
//
function setHeightToDifference(idToChange, idToCompare1, idToCompare2) {
	if(!document.getElementById) return ;
	
	var element1 = document.getElementById(idToCompare1) ;
	var height1 = element1.offsetHeight ;
	
	var element2 = document.getElementById(idToCompare2) ;
	var height2 = element2.offsetHeight ;
	
	var difference = Math.abs(height1 - height2) ;
	setHeight(idToChange, difference) ;
}


//
// setWidth(id, width)
//
// Sets the style.width of $id to $width.  SLSIA
//
function setWidth(id, width) {
	element = document.getElementById(id) ;
	element.style.width = width + "px" ;
}


// Sets $idToChange to the difference between $idToCompare1 and $idToCompare2.
// See setHeightToDifference (above)
function setWidthToDifference(idToChange, idToCompare1, idToCompare2) {
	if(!document.getElementById) return ;
	
	var element1 = document.getElementById(idToCompare1) ;
	var width1 = element1.offsetWidth ;
	
	var element2 = document.getElementById(idToCompare2) ;
	var width2 = element2.offsetWidth ;
	
	var difference = Math.abs(width1 - width2) ;
	setWidth(idToChange, difference) ;
}


//
// stripIndexLinks(id0, id1, ..., idN)
//
// Looks through each $id for links ending in index.htm/l and strips the index 
// part off the link.  Used to make prettier links so people don't copy-and-paste 
// (and print/publish/e-mail) links with the unnecessary index component.
//
function stripIndexLinks() {
	if(!document.getElementById) return ;
	
	var idsToStrip = stripIndexLinks.arguments ;
	for(var j=0; j<idsToStrip.length; j++) {
		var id = idsToStrip[j] ;
	
		var element = document.getElementById(id) ;
		if(element != null) {
			var links = element.getElementsByTagName("a") ;
			for(i=0; i<links.length; i++) {
				var link = links.item(i) ;
				_stripLinkFile(link, "index.html") ;
				_stripLinkFile(link, "index.htm") ;
			}
		}
	}
}


//
// tableAltRows(idToSearch, highlightClass)
//
// Applies $highlightClass to every other <tr> in $idToSearch
// Used to achieve alternating row style without asking editors to manually 
// apply style themselves
//
function tableAltRows(idToSearch, highlightClass) {
	if(!document.getElementById) return ;							// browser detect DOM support
	
	// get the container we should be searching
	var container = document.getElementById(idToSearch) ;			// get the container
	if(container==null) return new Array() ;						// bail if we can't find it
	
	var rows = container.getElementsByTagName("tr") ;
	for(var i=0; i<rows.length; i++) {
		if(i%2==0) {
			var row = rows.item(i) ;
			row.className = highlightClass ;
		}
	}
}



//====== Private Functions ======//

//
// _getSectionLink(idToSearch, idToIgnore)
//
// Only works when using document-realtive links in $idToSearch.
//
// Returns the DOM anchor object in $idToSearch, as long as it is not
// $idToIgnore, most likely to be the current section for this page.  The
// algorithm assumes the link with the fewest slashes is the link to this
// section (e.g., index.html over ../people/index.html).  Ignores links
// that don't start with ../, unless there are no slashes in the URL.  
// Returns null if there are more than one possible matches.
//
function _getSectionLink(idToSearch, idToIgnore) {
	if(!document.getElementById) return null ;						// browser detect DOM support
	
	// get the container we should be searching
	var container = document.getElementById(idToSearch) ;			// get the container
	if(container==null) return null ;								// bail if we can't find it
	
	// init variables
	var fewestSlashes = -1 ;
	var fewestSlashesCount = -1 ;
	var sectionCandidate = null ;
	
	var anchors = container.getElementsByTagName("a") ;				// get all anchors
	for(var i=0; i<anchors.length; i++) {								// iterate anchors...
		var linkCandidate = anchors.item(i) ;							// this candidate
		
		var href = linkCandidate.getAttribute("href", 2) ;
		var slashCount = _substrCount(href, "/") ;
		
		// only consider cases where href start with ".." or there are no slashes ("index.html")
		// and the $idToIgnore is undefined or not the current link
		if(href.indexOf("..") == 0 || slashCount == 0) {
			if((slashCount < fewestSlashes || fewestSlashes == -1) && (idToIgnore == null || linkCandidate.id != idToIgnore)) {
				fewestSlashes = slashCount ;
				fewestSlashesCount = 1 ;
				sectionCandidate = linkCandidate ;
			} else if(slashCount == fewestSlashes) {
				fewestSlashesCount++ ;
				sectionCandidate = null ;
			}
		}
	}																//...iterate anchors
	
	return sectionCandidate ;
}

//
// _getSelfLinks(idToSearch)
// 
// Returns array containing all DOM anchor objects in $idToSearch that link
// to the current page.  Returns an empty array if nothing is found.  Used
// for functions like menuHighlight.
//
function _getSelfLinks(idToSearch) {
	if(!document.getElementById) return new Array() ;				// browser detect DOM support
	
	var selfLinks = new Array() ;									// the return array
	var url = newURLFromLocationAndDocument(location, document) ;	// the URL of this document
	
		// get the container we should be searching
	var container = document.getElementById(idToSearch) ;			// get the container
	if(container==null) return new Array() ;						// bail if we can't find it
	
	var anchors = container.getElementsByTagName("a") ;				// get all anchors
	for(var i=0; i<anchors.length; i++) {								// iterate anchors...
		var linkCandidate = anchors.item(i) ;							// this candidate
		var candidateHREF = linkCandidate.getAttribute("href") ;		// get the href
				
		if(candidateHREF != null && !candidateHREF.indexOf("#")==0) {	// Ignore cases where candidate HREF is null or a jump link to this page...
			var joinedHREF = url.joinHREF(candidateHREF) ;					// relative to this URL
			if(url.equalsHREF(joinedHREF)) {								// if equal...
				selfLinks[selfLinks.length] = linkCandidate ;					// add it to return array
			}																// ...if equal
		}																// ...end ignore cases
	}																//...iterate anchors
	
	return selfLinks ;
}

// SLSIA.  Returns true if $candidate is an integer
function _isInt(candidate) {
	return (candidate.toString().search(/^-?[0-9]+$/) == 0);
}

// SLSIA.  Returns true if $string ends in $substring, false otherwise
function _stringEndsWith(string, substring) {
	var index = string.indexOf(substring) ;
	if(index == -1) return false ;
	
	// if substring is the last part of string, this is true
	// examples:
	//     string = "friend", substring = "end"
	//     "fri" (3) + "end" (3) = "friend" (6)
	//
	//     string = "the beginning", substring = "begin"
	//     "the " (4) + "begin" (5) != "the beginning" (13)
	//
	if((index+substring.length) == string.length) {
		return true ;
	}
	return false ;
}

// Helper for stripIndexLinks().  Strips $filename off the $link <a> object 
function _stripLinkFile(link, filename) {
	var href = link.getAttribute("href") ;				// Get the href of the link
	if(href == null) return ;
	
	filenameIndex = href.indexOf(filename) ;			// Get the index of this filename in href
	if(filenameIndex == -1) return ;					// Bail if filename isn't present
	
	if(href==filename) {								// Linking to this filename is a special case
		link.href = "." ;									// Just link to current folder
		return ;											// and return
	}
	
	if(_stringEndsWith(href, filename)) {				// if we should strip this
		link.href = href.substring(0, filenameIndex) ;		// strip it
	}
}

// _substrCount(stringToSearch, substrToFind)
// Returns number of $substrToFind needles in $stringToSearch haystack
function _substrCount(stringToSearch, substrToFind) {
	var count = 0 ;			// number of instances found
	var indexStart = 0 ;	// current index point in stringToSearch
	
	var nextIndex = stringToSearch.indexOf(substrToFind) ;
	
	while(nextIndex != -1) {
		count++ ;
		indexStart = nextIndex + substrToFind.length ;
		
		nextIndex = stringToSearch.indexOf(substrToFind, indexStart) ;
	}
	
	return count ;
}




//
// URL.js - v0.1.3
//


//====== Constructor ======//

// 
// Iniitialize some variables
// Define the memebers of the object
//
function URL(newURL) {
		// using JavaScript's Location members
	this.hash = getHash(newURL) ;
	this.host = getHost(newURL) ;
	this.hostname = getHostname(newURL) ;
	this.href = newURL ;
	this.pathname = getPathname(newURL) ;
	this.port = getPort(newURL) ;
	this.protocol = getProtocol(newURL) ;
	this.search = getSearch(newURL) ;
	
		// convenience members (from URL spec)
	this.path = this.pathname ;
	this.query = this.search ;
	this.part = this.hash ;
	this.fragment = this.hash ;
	
		// URL Functions
	this.getResource = getResource ;
	this.getSearchHash = getSearchHash ;
	this.getProtocolHost = getProtocolHost ;
	this.joinHREF = joinHREF ;
	this.getNormalHREF = getNormalHREF ;
	this.getNormalHrefWithoutSearchHash = getNormalHrefWithoutSearchHash ;
	this.getFilename = getFilename ;
	this.equalsHREF = equalsHREF ;
}

function newURLFromLocationAndDocument(location, document) {
	var locationURL = new URL(location.href) ;
	var url = locationURL ;
	
	var baseList = document.getElementsByTagName("base") ;
	if(baseList.length > 0) {
		var base = baseList[baseList.length-1] ;
		if(base != null) {
			var baseHREF = base.getAttribute("href") ;
			if(baseHREF != null) {
				joinedHREF = locationURL.joinHREF(baseHREF) ;
				url = new URL(joinedHREF) ;
			}
		}
	}
	
	return url ;
}



//====== Location Functions for Constructor ======//

//
// protocol://hostname:port/pathname?search#HASH
//
function getHash(url) {
	url_parts = url.split("/") ;
	filename_query_hash = url_parts[url_parts.length-1] ;
	
	crosshatchIndex = filename_query_hash.indexOf("#") ;
	if(crosshatchIndex==-1) crosshatchIndex = filename_query_hash.length ;
	hash = filename_query_hash.substring(crosshatchIndex+1, filename_query_hash.length) ;
	
	return hash ;
}

//
// protocol://HOSTNAME:PORT/pathname?search#hash
//
function getHost(url) {
	if(getProtocol(url)=="") return "" ;
	
	hostname = getHostname(url) ;
	port = getPort(url) ;
	
	//if(port==80) return hostname ;
	if(getDefaultPort(getProtocol(url)) == port) {
		return hostname ;	
	}
	return hostname + ":" + port ;
}

//
// protocol://HOSTNAME:port/pathname?search#hash
//
function getHostname(url) {
	if(getProtocol(url)=="") return "" ;
	
	url_parts = url.split("://") ;
	host_plus = url_parts[1] ;
	
	host_plus_parts = host_plus.split("/") ;
	host_port = host_plus_parts[0] ;
	
	host_port_parts = host_port.split(":") ;
	host = host_port_parts[0] ;
	
	return host ;
}

//
// protocol://hostname:port/PATHNAME?search#hash
//
function getPathname(url) {
	var path_query_hash = "" ;
	
	if(getProtocol(url)=="") {
		path_query_hash = url ;
	} else {
		url_parts = url.split("://") ;
		host_plus = url_parts[1] ;
		
		slashIndex = host_plus.indexOf("/") ;
		if(slashIndex==-1) return "" ;
		path_query_hash = host_plus.substring(slashIndex, host_plus.length) ;
	}
	
	questionIndex = path_query_hash.indexOf("?") ;
	if(questionIndex==-1) questionIndex = path_query_hash.length ;
	crosshatchIndex = path_query_hash.indexOf("#") ;
	if(crosshatchIndex==-1) crosshatchIndex = path_query_hash.length ;
	pathname = path_query_hash.substring(0, Math.min(questionIndex,crosshatchIndex)) ;
	
	return pathname ;
	//return "!" ;
}

//
// protocol://hostname:PORT/pathname?search#hash
//
function getPort(url) {
	if(getProtocol(url)=="") return -1 ;
	
	url_parts = url.split("://") ;
	host_plus = url_parts[1] ;
	
	host_plus_parts = host_plus.split("/") ;
	host_port = host_plus_parts[0] ;
	
	host_port_parts = host_port.split(":") ;
	port = host_port_parts[1] ;
	if(port==null) return getDefaultPort(getProtocol(url)) ;
	return port ;
}

//
// PROTOCOL://hostname:port/pathname?search#hash
//
function getProtocol(url) {
	if(url.indexOf("://") > -1) {
		url_parts = url.split("://") ;
		protocol = url_parts[0] ;
		return protocol ;
	} else {
		return "" ;	
	}
}

//
// protocol://hostname:port/pathname?SEARCH#hash
//
function getSearch(url) {
	url_parts = url.split("/") ;
	filename_query_hash = url_parts[url_parts.length-1] ;
	
	questionIndex = filename_query_hash.indexOf("?") ;
	if(questionIndex==-1) questionIndex = filename_query_hash.length ;
	query_hash = filename_query_hash.substring(questionIndex+1, filename_query_hash.length) ;
	
	crosshatchIndex = query_hash.indexOf("#") ;
	if(crosshatchIndex==-1) crosshatchIndex = query_hash.length ;
	query = query_hash.substring(0, crosshatchIndex) ;
	
	return query ;
}



//====== URL Functions ======//

//
// protocol://hostname:port[/PATH?SEARCH#HASH]
// includes leading slash
//
function getResource() {
	var resource = this.pathname ;
	return this.pathname + this.getSearchHash() ;
}

//
// protocol://hostname:port/pathname[?SEARCH#HASH]
// includes leading ? or # (when appropriate)
//
function getSearchHash() {
	var search_hash = "" ;
	if(this.search != "") search_hash += "?" + this.search ;
	if(this.hash != "") search_hash += "#" + this.hash ;
	return search_hash ;
}

//
// [PROTOCOL://HOSTNAME:PORT]/pathname?search#hash
// does not include trailing slash
//
function getProtocolHost() {
	if(this.protocol=="") return "" ;
	return this.protocol + "://" + this.host ;
}

//
// Merge this URL with otherHREF (string)
// Returns string of otherHREF relative to this.href
//
function joinHREF(otherHREF) {
	var otherURL = new URL(otherHREF) ;
	var joinedHREF = "" ;
	
		// if otherHREF has a protocol, it is a fully-qualified URL
	if(otherURL.protocol != "") return otherURL.href ;
	
		// if otherHREF is root-relative
	if(otherURL.pathname.indexOf("/")==0) return (this.getProtocolHost() + otherURL.getResource()) ;
		// otherwise, joinedHREF goes at least through the filename
	joinedHREF = this.getProtocolHost() ;
	if(this.pathname.indexOf("/") != 0) joinedHREF += "/" ;
	joinedHREF += this.pathname ;
	
		// if otherHREF is a search
	if(otherHREF.indexOf("?") == 0) {
		return joinedHREF + otherURL.getSearchHash() ;
	}
		// otherwise, joinedHREF contians at least search
	if(this.search != "") joinedHREF += "?" + this.search ;
	
		// if otherHREF is a hash
	if(otherHREF.indexOf("#") == 0) {
		return joinedHREF + "#" + otherURL.hash ;
	}
	
	
	// if we're still here, otherHREF is document relative (which is hard)
	thisPathStack = this.pathname.split("/") ;
		// if it doesn't end in trail slash, the first thing comes off the stack
	//if(this.pathname.lastIndexOf("/") != (this.pathname.length-1)) thisPathStack.pop() ;	//v0.1.2
	if(this.pathname.lastIndexOf("/") != (this.pathname.length)) thisPathStack.pop() ;
	
	otherPathQueue = otherHREF.split("/") ;
	while(otherPathQueue.length > 0) {
		pathElement = otherPathQueue.shift() ;
		if(pathElement == "..") {
			thisPathStack.pop() ;
		} else if(pathElement == ".") {
			// do nothing
		} else {
			thisPathStack.push(pathElement) ;
		}
	}
	
	joinedPath = "" ;
	while(thisPathStack.length > 0) {
		item = thisPathStack.shift() ;
		if(item != "") {
			if(joinedPath != "") joinedPath += "/" ;
			joinedPath += item ;
		}
	}
		// does this start with a slash (i.e., not a relative URL)
	if(otherHREF.charAt(0)=="/" || this.pathname.charAt(0)=="/" || this.getProtocolHost() != "") {
		joinedPath = "/" + joinedPath ;
	}
	return this.getProtocolHost() + joinedPath ;
	
		// tsnh
	//return "-1" ;
}

//
// returns normalized href (removed assumed portions such as index.html)
// *** could use a lot of work as protocols are added to getDefaultPort()
// *** could use normalizing of hostname:port (when default)
// *** could use normalizing of search, hash (i.e., /file.html# -> file.html)
//
function getNormalHREF() {
	var normalPath = "" ;
	
	// first, normailize . and .. in paths
	var thisPathStack = this.pathname.split("/") ;
	var normalPathElements = new Array() ;
	
		// iterate though each element (/ delimited)
	while(thisPathStack.length > 0) {
		var pathElement = thisPathStack.shift() ;
		if(pathElement == "..") {
			// if it goes up, pop off the normaized version
			normalPathElements.pop() ;
		} else if(pathElement == ".") {
			// do nothing
		} else {
			// if it's a regualr string, push it
			normalPathElements.push(pathElement) ;
		}
	}
	
		// create the normalPath
	while(normalPathElements.length > 0) {
		var item = normalPathElements.shift() ;
		if(item != "") {
			if(normalPath != "") normalPath += "/" ;	// don't add / to the first item
			normalPath += item ;
		}
	}
	
	
	// changed in v0.1.3
	// not sure why files without protocol/host should start with slash
		// should it start with a slash?
	//if(this.pathname.charAt(0)=="/" || getProtocolHost != "") {
	//	normalPath = "/" + normalPath ;
	//}
	
	
		// should it start with a slash?
	if(this.pathname.charAt(0)=="/") {								// v0.1.3
		normalPath = "/" + normalPath ;
	}
		// should it end with a slash?
	if(this.pathname.charAt(this.pathname.length-1)=="/") {
		normalPath += "/" ;
	}
	
		// prevent // case when path is /
		// That case satisfies both of the above [v0.1.1]
	if(normalPath == "//") {
		normalPath = "/" ;
	}
	
	//
	// Handle normalization for each protocol
	// *** should be split into various functions, as should the above
	//
	
	
	// http and https
	// trim index.html, index.php, etc
	if(this.protocol=="http" || this.protocol=="https") {
		var filename = this.getFilename() ;
			// if the filename starts with "index."
		if(filename.indexOf("index.")==0) {
			normalPath = normalPath.substring(0, normalPath.lastIndexOf(filename)) ;
		}
	}
	
	return this.getProtocolHost() + normalPath + this.getSearchHash() ;
}


//
// v0.1.3
// returns normalized href (see above) minus the hash part (#part)
// used to compare equality of two HREFs when you don't care what part of the page it jumps to
//
function getNormalHrefWithoutSearchHash() {
	var normalizedURL = new URL(this.getNormalHREF()) ;
	
	hrefWithoutSearchHash = normalizedURL.getProtocolHost() + normalizedURL.pathname ;
	if(normalizedURL.search != "") hrefWithoutSearchHash += "?" + normalizedURL.search ;
	
	return hrefWithoutSearchHash ;
}


//
// protocol://hostname:port/various/folder/[FILENAME]?search#hash
//
function getFilename() {
		// if doesn't end in "/"
	if(this.pathname.lastIndexOf("/") != (this.pathname.length-1)) {
		var pathParts = this.pathname.split("/") ;
		if(pathParts.length > 0) {
			return pathParts[pathParts.length-1] ;
		}
	}
	return "" ;
}

//
// compare this.href to otherHREF (string) in normailzed form
// return boolean
//
function equalsHREF(otherHREF) {
	var otherURL = new URL(otherHREF) ;
	//alert("this = " + this.getNormalHREF()) ;
	//alert("other = " + otherURL.getNormalHREF()) ;
	
	
		// see if their normal forms compare
	//if(this.getNormalHREF() == otherURL.getNormalHREF()) {	// v0.1.3: getNormalHrefWithoutSearchHash is a better choice here
	if(this.getNormalHrefWithoutSearchHash() == otherURL.getNormalHrefWithoutSearchHash()) {
			// if they both have filenames, check them (so index.php != index.html)
		if(this.getFilename() != "" && otherURL.getFilename() != "") {
			if(this.getFilename() == otherURL.getFilename()) {
				return true ;
			} else {
				return false ;
			}
		}
		return true ;
	} else {
		return false ;	
	}
}


//====== Internal Functions ======//

//
// returns default ports for common protocols
// *** could use a lot of work
//
function getDefaultPort(protocol) {
	if(protocol=="http") return 80;
	if(protocol=="https") return 443;
	return -1 ;	
}
