// Holds all the GISMap instances so we can iterate through them.
var g_MapRegistry = new GISMapRegistry();

// A List of all GISMap Objects
function GISMapRegistry() {
	this.toString = function(){return 'GISMapRegistry';};
	this.baseURL = getBaseURL();

	// currentActiveMap is the map that has the focus of the mouse.
	this.currentActiveMap = null;
	
	// Array list of all maps on the page.
	this.mapList = [];

	// Adds the mapObj to the Array of GISMap
	this.addMap = function(mapObj) {this.mapList[this.mapList.length] = mapObj;};

	// Used to make sure a DIV is not already associated with a map.
	this.checkMapExists = function(divID) {
		// Make sure that this div ID isn't already in use.
		for (var i=0;i<this.mapList.length;i++) {
			if (this.mapList[i].id == divID) {
				// Found it return true.
				return true;
			}
		}
		return false;
	};

	// Determine which GISMap object that was clicked by comparing it to the mapList array in the GISMapRegistry object.
	this.getMapByParent=function(targetElement) {
		// The targetElement needs to be a mapFrame, get the mapFrame based on the current class of the target.
		if (!targetElement) {return null;}
		var activeMap = null;
		if (targetElement.className == 'mapImage') {
			// Go up one node.
			targetElement = targetElement.parentNode;
		}
		
		if (targetElement.className == 'mapContainer') {
			// Attempt to go up one more node.
			if (targetElement.parentNode) {
				targetElement = targetElement.parentNode;
			} else {
				// mapContainer doesn't have a parentNode.  That is very strange.
				targetElement = null;
			}
		}

		// We should have the top-level DIV now (the one that the user actually created in the HTML document).
		// Make sure we have a targetElement.
		if (!targetElement) {return null;}
		
		// Look in all the registered maps for a match on id.
		for (var i=0;i<this.mapList.length;i++) {
			if (this.mapList[i].id == targetElement.id) {
				// Found it, return it.
				activeMap = this.mapList[i];
				break;
			}
		}
		return activeMap;
	};
	
	// Determine which GISMap object to use from the ID, comparing it to the mapList array in the GISMapRegistry object.
	this.getMapById=function(mapID) {
		// The targetElement has to be a mapFrame.
		var activeMap = null;
		if ((mapID === '') || (mapID == null)) {
			return null;
		}

		for (var i=0;i<this.mapList.length;i++) {
			if (this.mapList[i].id == mapID) {
				activeMap = this.mapList[i];
				break;
			}
		}

		return activeMap;
	};

	// Gets the map that the user is using a mouse to interacting with.
	// 'e' already takes into account window.event vs [e].
	this.getActiveMap=function(e) {
		// In order to make this work even when the mouse moves off the map, we cache the
		// map that is clicked on and uncache it when the mouse is unclicked.
		// If a map isn't in the cache then we will figure it out the normal way.  This value will be cached.
		if (this.currentActiveMap != null) {return this.currentActiveMap;}

		var srcElement = null;

		// The clicked element is not mapContainer but one of the map images.  In order to figure out the mapContainer offset (which
		// is what we really want to know) we need to get the offset from the clicked element to the mapContainer.
		if (e.target) {
			// Firefox treats the DIV.tile as the container.
			srcElement = e.target.parentNode;
		} else {
			// IE treats the actual Image that was clicked as the container.
			// We want the parent div.
			srcElement = event.srcElement.parentNode;
		}

		// Make sure we actually have an object
		if (srcElement == null) {return null;}

		// If this isn't a left mouse button click then ignore it.
		// IE-Left = 1; FF-Left = 0; both are less than 2 so ignore higher.
		if (e.button > 1) {return null;}

		return this.getMapByParent(srcElement);
	};
	
	// Cache the current active map
	this.setActiveMap=function(mapObj) {
		this.currentActiveMap = mapObj;
	};

	// Load the supporting JavaScript files.
	loadScript(this.baseURL + 'scripts/GISHelperClasses.js');
	loadScript(this.baseURL + 'scripts/AJAXRequest.js');
	loadScript(this.baseURL + 'scripts/ajaxFunctions.js');

	// Setup an onUnload event handler which will release resources held by GISMap objects.  It just calls the Unload function and they handle the rest.
	var myself = this;
	this.unload = function() {
			for (var i=0;i<myself.mapList.length;i++) {
				myself.mapList[i].unload();
				// Destroy this map object.
				myself.mapList[i] = null;
			}
			// Destroy the map registry.
			myself = null;
		};

	// If there isn't a onunload event already set then use ours.
	if (window.onunload == null) {
		window.onunload = this.unload;
	} else {
		// There is one set.  Capture the current one and build a new function which calls theirs and ours.
		var current_onunload = window.onunload;
		window.onunload = function () {
			if (current_onunload) {
				current_onunload();
			}
			myself.unload();
		};
	}

	// Implement the Array.shift() method if it isn't available.
	if (typeof([].shift) !== 'function') {
		Array.prototype.shift = function() {
			var temp = this[0];
			var oldArray = this;
			for (var i=0;i<oldArray.length-1;i++) {
				this[i] = oldArray[i+1];
			}
			return temp;
		};
	}
}

// This is the main class.  It creates an instance of a map.  More than one map can exist on a page.
// Parameters:	mapFrameDiv - HTML DIV element which will hold the map.
//					initialSize - (Optional) The initial size of the map.
// Returns:		GISMap Object
function GISMap(mapFrameDiv, initialSize) {
	// The mapFrame is the actual DIV element that the map will exist in.  The id of this class is the id of that DIV.
	this.mapFrame = mapFrameDiv;
	this.id = mapFrameDiv.id;
	this.toString = function(){return 'GISMap';};

	// Make sure this mapFrameDiv has not already been used.
	if (g_MapRegistry.checkMapExists(mapFrameDiv.id)) {alert('You cannot associate two maps with the same HTML DIV element');return null;}

	// The initialContext will always contain the default map settings.
	// The currentContext will contain the current viewport.
	this.initialContext = null;
	this.currentContext = null;

	// Holding variables for the various map elements.  For example, so we don't have to call document.getElementById('mapImage') constantly.
	this.mapContainer = null;
	this.mapImage = null;
	this.scaleBar = null;
	this.minimap = null;
	this.minimapHighlight = null;
	this.loadingNotification = null;
	this.zoomDiv = null;
	this.legendImage = null;
	
	// infoFrame is a GISInfoFrame object.  It is the data-display panel.
	this.infoFrame = null;

	// Local variables
	this.sizes = {small:230,medium:330,large:530};
	this.mapSize = (parseInt(initialSize, 10) || this.sizes.medium);	// Default to 360px
	this.initialParcelID = '';
	this.isIE6 = true;
	this.capabilitiesList = [];
	this.clearMapOnResize = false;
	
	// The currentMapTool is the tool the user wants to use.
	// The activeMapTool is the tool that is currenting being used (ie, the user is dragging the box).
	this.currentMapTool = '';
	this.activeMapTool = '';

	// Event hooks.  Must be a GISMapEvent object.
	this.onMouseClick = null;
	this.onMouseDoubleClick = null;
	this.onMapContextInitialized = null;
	this.onMapContextReset = null;
	this.onMapLoaded = null;
	this.onParcelDataReceived = null;
	this.onBufferReceived = null;
	this.onInitializeMapSize = null;
	this.onInfoFrameUnsupportedMSXMLError = null;

	// Add this object to the global map cache, this will be used to dereference a HTML element to it's owner GISMap object.
	// This is a reminder to me: Dereference: To access the thing to which a "pointer" points.  I think it makes sense in this context.
	g_MapRegistry.addMap(this);

	// This will create the HTML elements, pass the function that will remove the Loading Notification.
	this.initializeHTML();
	this.initializeGlobalStylesheet();
	this.initializeInstanceStylesheet();
	this.initializeEventHandlers();
}

// Returns the name of the class.  If this wasn't implemented it would simply return "[object]".
GISMap.toString = function(){return 'GISMap';};

// Generate all the divs that make up the map.
// Returns:		null
GISMap.prototype.initializeHTML=function() {
	var myself = this;

	// Clear the div
	this.mapFrame.innerHTML = '';
	this.mapFrame.className = 'mapFrame';

	// Create the various map slide-pan objects.
	var newElement = document.createElement('div');
	newElement.className = "slide north";
	newElement.onclick = function() {return GISMap.input_panMap(myself.id, 'N');};
	this.mapFrame.appendChild(newElement);
	
	newElement = document.createElement('div');
	newElement.className = "slide south";
	newElement.onclick = function() {return GISMap.input_panMap(myself.id, 'S');};
	this.mapFrame.appendChild(newElement);

	newElement = document.createElement('div');
	newElement.className = "slide east";
	newElement.onclick = function() {return GISMap.input_panMap(myself.id, 'E');};
	this.mapFrame.appendChild(newElement);
	
	newElement = document.createElement('div');
	newElement.className = "slide west";
	newElement.onclick = function() {return GISMap.input_panMap(myself.id, 'W');};
	this.mapFrame.appendChild(newElement);

	// Create the actual map image.
	var mapContainer = document.createElement('div');
	this.mapContainer = mapContainer;
	mapContainer.className = 'mapContainer';

	var containerElement = document.createElement('div');
	containerElement.className = 'mapImage';

	var subElement = document.createElement('img');
	subElement.src='';
	subElement.removeAttribute('height');
	subElement.removeAttribute('width');
	subElement.alt='GIS Map Unavailable';
	subElement.className='mapImg';
	// Load for FF
	subElement.onload = this.mapImageLoaded;
	// Readystatechange for IE
	addEvent(subElement,'readystatechange',this.mapImageLoaded);

	// Add the Image
	containerElement.appendChild(subElement);
	
	// Cache the div that holds the image.
	this.mapImage = containerElement;
	mapContainer.appendChild(containerElement);
	
	// Create the scalebar
	containerElement = document.createElement('div');
	containerElement.className = 'scaleBar';
	subElement = document.createElement('img');
	subElement.src=g_MapRegistry.baseURL + 'images/scale.gif';
	subElement.alt = 'Scale';
	containerElement.appendChild(document.createTextNode('0 '));
	containerElement.appendChild(subElement);
	containerElement.appendChild(document.createTextNode(' '));
	subElement = document.createElement('span');
	subElement.appendChild(document.createTextNode('0'));
	containerElement.appendChild(subElement);
	containerElement.appendChild(document.createTextNode(' ft.'));
	this.scaleBar = containerElement;
	mapContainer.appendChild(containerElement);

	// Create the map loading notification popup
	containerElement = document.createElement('div');
	containerElement.className = 'loadingMapNotification';
	containerElement.innerHTML = '<center>Loading</center>';
	// Cache this object, it displays the loading notification overlaid on the map.
	this.loadingNotification = containerElement;
	mapContainer.appendChild(containerElement);

	// Create the minimap
	containerElement = document.createElement('div');
	containerElement.className = 'minimap';
	this.minimap = containerElement;
	subElement = document.createElement('img');
	subElement.src = g_MapRegistry.baseURL + 'images/county/countyMinimap.gif';
	subElement.className = 'countyMinimap';
	subElement.alt = 'Minimap';
	containerElement.appendChild(subElement);
	// Highlight minimap
	subElement = document.createElement('img');
	subElement.src = g_MapRegistry.baseURL + 'images/county/countyMinimapHighlight.gif';
	subElement.className = 'countyMinimapHighlight';
	subElement.alt = 'Minimap';
	this.minimapHighlight = subElement;
	containerElement.appendChild(subElement);
	// Toggle button
	subElement = document.createElement('img');
	subElement.src = g_MapRegistry.baseURL + 'images/icons/toggleMinimap.gif';
	subElement.className = 'icon toggleMinimapImg';
	subElement.alt = 'Toggle Minimap';
	subElement.onclick=this.toggleMinimap;
	containerElement.appendChild(subElement);
	mapContainer.appendChild(containerElement);

	// Create the Zoom Box (highlight the area to be zoomed to).
	containerElement = document.createElement('div');
	containerElement.className = 'zoomBox';
	this.zoomDiv = containerElement;
	mapContainer.appendChild(containerElement);
	
	this.mapFrame.appendChild(mapContainer);	

	// IE7 is much better at standards compliance.  The quickest way I can figure to determine IE7 from IE6 is
	// to check the window.XMLHttpRequest object (which exists in FF and IE7).
	if (window.XMLHttpRequest) {this.isIE6 = false;}
	
	return null;
};

// Loads the main map stylesheet from the server.
// Individual instances of a map will have a special "size" stylesheet, but this one controls
// most of the properties.
// Returns:		null
GISMap.prototype.initializeGlobalStylesheet = function() {
	var documentHead = document.getElementsByTagName('head').item(0);
	// Load the styleMap.css file if it isn't already loaded.
	var headLinks = documentHead.getElementsByTagName('link');
	var g_mapStyleExists = false;
	for (var i=0;i<headLinks.length;i++) {
		if (headLinks[i].id == "g_mapStyle") {
			g_mapStyleExists = true;
			break;
		}
	}
	if (!g_mapStyleExists) {
		var g_mapStyle = document.createElement('link');
		g_mapStyle.rel = 'stylesheet';
		g_mapStyle.type = 'text/css';
		g_mapStyle.id = 'g_mapStyle';
		g_mapStyle.href = g_MapRegistry.baseURL + 'styleMap.css';
		// Add it before any other link tag.
		if (headLinks.length !== 0) {
			documentHead.insertBefore(g_mapStyle, headLinks[0]);
		} else {
			// There are no link tags.  Make it the first child of the head tag.
			var headObjects = documentHead.childNodes;
			documentHead.insertBefore(g_mapStyle, headObjects[0]);
		}
	}
	
	return null;
};

// Creates a stylesheet that is specific to this instance of GISMap.
// It will handle the map size.
// Returns:		null
GISMap.prototype.initializeInstanceStylesheet=function() {
	// Make sure one doesn't already exist.
	// Check IE first, then FF
	if ((getCSS('div.mapFrame#' + this.id + '','width',this.id + 'style') == null) && (getCSS('div#' + this.id + '.mapFrame div.mapContainer','width',this.id + 'style') == null)){
		var defaultSize = this.mapSize;

		var newStylesheet = document.createElement('style');
		newStylesheet.setAttribute('type','text/css');
		newStylesheet.id = this.id + 'style';
		newStylesheet.media = 'all';
		
		var CSSText = 'div.mapFrame#' + this.id + ' {width:' + (defaultSize + 32) + 'px;height:' + (defaultSize + 32) + 'px;}\r\n';
		CSSText += 'div.mapFrame#' + this.id + ' div.mapContainer {width:' + (defaultSize + 0) + 'px;height:' + (defaultSize + 0) + 'px;}';
		
		if (newStylesheet.styleSheet) {
			// IE Version
			newStylesheet.styleSheet.cssText = CSSText;
		} else {
			// FF Version
			newStylesheet.appendChild(document.createTextNode(CSSText));		
		}

		// Insert it after the first LINK tag.  We want this sheet to load before any user stylesheets but after our global stylesheet.
		var documentHead = document.getElementsByTagName('head').item(0);
		// AMW - changed[1] to zero because alltel (aircard) removes link tag from file
		documentHead.insertBefore(newStylesheet,document.getElementsByTagName('link')[0].nextSibling);
	}
	
	return null;
};

// Sets the events for the document and the map.
// Returns:		null
GISMap.prototype.initializeEventHandlers = function() {
	// Register the event handlers on the appropriate objects.
	// The document object gets the mouse movement handlers so that we can continue to move stuff outside the mapImage.
	addEvent(document,'mousedown',this.onMapMouseDown);
	addEvent(document,'mouseup',this.onMapMouseUp);

	// Disable the dragstart event so we don't get the "Unavailable" mouse cursor when we drag the mouse (zoom and pan).
	addEvent(document,'dragstart',function () {return false;});
	
	// Add the click events to the mapImage itself.
 	addEvent(this.mapImage,'click',this.onMapClick);
	addEvent(this.mapImage,'dblclick',this.onMapDoubleClick);
	
	return null;
};

// Waits for all auxiliary functions to load and then fires the startLoad() method.
// If it doesn't load within 10 tries (which works out to 10 seconds) then an error is thrown.
// Parameters:	currTry - Optional; Total number of tries thus far.  This shouldn't be passed.
// Returns:		Boolean value indicating that startLoad was called the first time.  This is not actually useful.
GISMap.prototype.load=function(currTry) {
	if (!this.isLoadingMap()) {this.isLoadingMap(true);}
	// Keep checking to see if certain required functions come online.
	// If they do then go to startLoad, if not keep trying.
	// If we hit 10 seconds then abandon.
	if (currTry==null) {currTry=0;}
	currTry++;
	
	if (currTry>10) {
		alert('GISMap.load() - Error: Timed out waiting for mapping functions to load.');
		this.setMapImage(this.baseURL + 'images/mapImageUnavailable.jpg');
		return false;
	}
	
	if ((typeof(request_getInitialSettings) != 'undefined') && (typeof(AJAXRequest) != 'undefined') && (typeof(GISInfoFrame) != 'undefined')) {
		// The function is available, go ahead and start loading.
		this.startLoad();
		return true;
	} else {
		// It is not available yet.  Call this function again in 1 second.
		var myself = this;
		window.setTimeout(function() {myself.load(currTry);},1000);
	}
	return false;
};

// Gets the initial server settings.
// The server's response to request_getInitialSettings will load this GISMap object's context
// and pass control to this.finalizeLoad.
// Returns:		Boolean indicating the existence of the request_getInitialSettings function.
// Do not call this function.
GISMap.prototype.startLoad=function() {
	request_getInitialSettings(this); // All request_* functions are in the ajaxFunctions.js file.
	return true;
};

// This is called by the server after the initial values have been loaded.
// It uses the currentContext (which is a copy of initialContext) to get a map.
// Returns:		Boolean indicating successful executing (false means it was aborted).
GISMap.prototype.finalizeLoad=function(getArgs) {
	// Application is active, load the rest of the data.	
	// Initial Extent
	var minX=getArgs.shift();
	var minY=getArgs.shift();
	var maxX=getArgs.shift();
	var maxY=getArgs.shift();
	
	// Cached, default map url for the Medium sized map (was originally a good idea, but I designed it badly).
	var defaultMapURL=getArgs.shift();

	// Create the "master context".  This will not change.  Clicking on the "Zoom to Extent" icon will replace the currentContext
	// object with this object and reload the map.

	this.initialContext = new GISMapContext(minX, minY, maxX, maxY);
	this.initialContext.mapURL = defaultMapURL;

	this.initialContext.imageWidth = this.getMapSize();
	this.initialContext.imageHeight = this.initialContext.imageWidth;

	// Start the layer loading process.
	var totalGroups = parseInt(getArgs.shift(), 10);
	if (isNaN(totalGroups)) {totalGroups = 0;}

	// Create the groupList
	this.initialContext.groupList = new GISGroupList();

	// Get the DataLayer data.  The dataLayer is the layer that SDE will query for parcel identification purposes.
	// Usually this will be the same exact thing as the parcelLayer, but they are seperated just in case.
	if (getArgs[0]) {
		var dataLayerData = getArgs.shift().split("|");
		this.initialContext.groupList.dataLayer.id = dataLayerData[0];
		this.initialContext.groupList.dataLayer.name = dataLayerData[1];
		this.initialContext.groupList.dataLayer.dataNodeName = dataLayerData[2];
	
		// Get the ParcelLayer data
		// This is the layer that will be highlighted when a parcel is selected.
		var parcelLayerData = getArgs.shift().split("|");
		this.initialContext.groupList.parcelLayer.id = parcelLayerData[0];
		this.initialContext.groupList.parcelLayer.name = parcelLayerData[1];
		this.initialContext.groupList.parcelLayer.dataNodeName = parcelLayerData[2];

		// Only if the DataLayer and ParcelLayer IDs are non-zero should we allow parcel selection.
		if ((this.initialContext.groupList.parcelLayer.id != 0) && (this.initialContext.groupList.dataLayer.id != 0)) {
			this.addCapability('ALLOW_SELECTPARCEL');
		}

		// Populate the Groups list.
		// Starting at getArgs[9] go to [totalGroups].  The list is vertical bar delimited [|]
		for (var groupIndex=0;groupIndex<totalGroups;groupIndex++) {
			var currGroupInfo = getArgs.shift();
			
			// Split the list
			var groupElements = currGroupInfo.split("|");
			var tmpGroup = new GISGroup();
			tmpGroup.id = groupElements[0];
			tmpGroup.name = groupElements[1];
			if (groupElements[2].toLowerCase() == 'true') {
				tmpGroup.visible = true;
			} else {
				tmpGroup.visible = false;
			}
			// Get the number of layers in the group
			var totalLayers = groupElements[3];

			// Get all the layers in this group.
			for (var layerIndex=0;layerIndex<totalLayers;layerIndex++) {
				var currLayerInfo = getArgs.shift();

				//	Split the list
				var layerElements = currLayerInfo.split("|");
				var tmpLayer = new GISLayer();
				tmpLayer.id = layerElements[0];
				tmpLayer.name = layerElements[1];

				// Store the layer.
				tmpGroup.mapLayers[layerIndex] = tmpLayer;	
			}
			
			// Store the group.			
			this.initialContext.groupList.groups[groupIndex] = tmpGroup;
		}
	} else {
		// There was no dataLayer (and persumably no information after it).
	}

	// Make a copy of the initialContext for the currentContext.  We will act on the currentContext.
	this.currentContext = this.initialContext.clone();

	// Fire the event handler if one is registered.
	if (this.onMapContextInitialized) {
		if (!this.onMapContextInitialized.onReceive(this)) {
			return false;
		}
	}

	// Now initialize the map.
	// Set the mapSize dropdown box.
	var currSize = this.getMapSize();

	// Fire the event handler if one is registered.
	if (this.onInitializeMapSize) {
		if (!this.onInitializeMapSize.onReceive(this, currSize)) {
			return false;
		}
	}

	// Turn on buffering if ALLOW_SELECTPARCEL is true.
	if (this.hasCapability('ALLOW_SELECTPARCEL')) {
		this.addCapability('ALLOW_BUFFERSEARCH');
	}

	// Turn on certain map tool capabilities now that we have finished loading.
	this.addCapability('ALLOW_ZOOMIN');
	this.addCapability('ALLOW_ZOOMOUT');
	this.addCapability('ALLOW_PAN');
	this.addCapability('ALLOW_SELECT');

	if (this.initialParcelID !== '') {
		// Request the envelope for this parcel... Zoom out 250 feet on each side.
		this.getParcelZoomToExtent(this.initialParcelID);
		return false;
	}

	// Load the map image from the initialContext settings.
	this.getMapImage();
	return true;
};

// Removes any event handlers and releases critial objects.
// Returns:		null
GISMap.prototype.unload=function() {
	// Unregister the event handlers.
	removeEvent(document,'mousedown',this.onMapMouseDown);
	removeEvent(document,'mouseup',this.onMapMouseUp);

	removeEvent(document,'dragstart',function () {return false;});
	
	// Remove the click events to the mapImage itself.
 	removeEvent(this.mapImage,'click',this.onMapClick);
	removeEvent(this.mapImage,'dblclick',this.onMapDoubleClick);

	this.mapImage.getElementsByTagName('img')[0].onload = null;
	removeEvent(this.mapImage.getElementsByTagName('img')[0],'readystatechange',this.mapImageLoaded);

	// Remove properties that reference HTML elements.
	this.isLoadingMap(false);
	if (this.infoFrame) {
		this.infoFrame.unload();
	}

	// Destroy all properties and methods.
	for (var tmp in this) {
		tmp = null;
	}
	
	return null;
};

//-- Capabilities Management --//

// Based on the data we have received from the server, we can enable/disable certain functionality.
// By default, everything is off.  They are turned on as data is received (by calling GISMap.addCapability([capabilityName]);)
// Parameters:	capabilityName - The name of the capability to add.
// Returns:		null
GISMap.prototype.addCapability=function(capabilityName) {
	// It is case-insensitive.
	capabilityName = capabilityName.toLowerCase();
	this.capabilitiesList[capabilityName] = true;
	return null;
};

// Disable a capability
// Parameters:	capabilityName - The name of the capability to remove.
// Returns:		null;
GISMap.prototype.removeCapability=function(capabilityName) {
	// It is case-insensitive.
	capabilityName = capabilityName.toLowerCase();
	delete this.capabilitiesList[capabilityName];
	return null;
};

// Determines if a capability has been registered.
// Parameters:	capabilityName - The name of the capability to check.
// Returns:		Boolean
GISMap.prototype.hasCapability=function(capabilityName) {
	// It is case-insensitive.
	capabilityName = capabilityName.toLowerCase();
	return typeof(this.capabilitiesList[capabilityName]) != 'undefined';
};

// Returns a list of all registered capabilities.  This is not an exhaustive list of possible/used/checked capabilties, only the added ones.
// Returns:		Array
GISMap.prototype.getCapabilityList=function() {
	return this.capabilitiesList.clone();
};

//-- Map Control --//

// Retrieves the current map size from the stylesheet.
// Returns:		Integer indicating map width.
GISMap.prototype.getMapSize=function() {
	// IE uses form div.mapFrame#mapID
	// FF uses form div#mapID.mapFrame
	
	// Check IE first.
	var styleSheetSize = getCSS('div.mapFrame#' + this.id + ' div.mapContainer','width',this.id + 'style');

	// if IE didn't return, try FF (swap the class and id selectors)
	if (styleSheetSize == null) {
		styleSheetSize = getCSS('div#' + this.id + '.mapFrame div.mapContainer','width',this.id + 'style');
	}
	
	// If we have a value now (and we should), strip the [px] off of it and return it.
	if (styleSheetSize != null) {
		return parseInt(styleSheetSize.replace(/px/,''), 10);
	}	

	// This should have already returned.  Throw error and return the medium size.
	alert('GISMap.getMapSize() - Warning: Stylesheet not found, defaulting to medium size.');
	return parseInt(this.sizes.medium, 10);
};

// Sets the width of the map.  It does this by setting the appropriate stylesheet declarations
// for the instance stylesheet.
// Parameters:	newSize - Integer value specifying new width/height.
// Returns:		Boolean value indicating successful change of map size.
GISMap.prototype.setMapSize=function(newSize) {
	newSize=parseInt(newSize, 10);

	// Don't do anything if the newSize is the current size.
	if (newSize == this.getMapSize()) {
		return false;
	}

	// The map is always square.  Convert the newSize to an integer and set the width
	// and height to this new size.  This is nice because we don't have to use set-in-stone
	// sizes in the stylesheet.
	var newBaseWidth = newSize;
	var newBaseHeight = newBaseWidth;

	// Clear the map first only if the flag is set.
	if (this.clearMapOnResize) {
		this.setMapImage('');
	} else {
		// Flag isn't set, just resize the map image.
		this.mapImage.getElementsByTagName('img')[0].style.width = newBaseWidth + 'px';
		this.mapImage.getElementsByTagName('img')[0].style.height = newBaseHeight + 'px';
	}

	// Change the display sheet (IE and Firefox swap the class and id selectors).
	// IE method
	changeCSS('div.mapFrame#' + this.id + '','width',(newBaseWidth + 32) + 'px',this.id + 'style');
	changeCSS('div.mapFrame#' + this.id + '','height',(newBaseHeight + 32) + 'px',this.id + 'style');
	changeCSS('div.mapFrame#' + this.id + ' div.mapContainer','width',(newBaseWidth) + 'px',this.id + 'style');
	changeCSS('div.mapFrame#' + this.id + ' div.mapContainer','height',(newBaseHeight) + 'px',this.id + 'style');

	// Firefox method.
	changeCSS('div#' + this.id + '.mapFrame','width',(newBaseWidth + 32) + 'px',this.id + 'style');
	changeCSS('div#' + this.id + '.mapFrame','height',(newBaseHeight + 32) + 'px',this.id + 'style');
	changeCSS('div#' + this.id + '.mapFrame div.mapContainer','width',(newBaseWidth) + 'px',this.id + 'style');
	changeCSS('div#' + this.id + '.mapFrame div.mapContainer','height',(newBaseHeight) + 'px',this.id + 'style');

	// Change the size of the context.  Change both the currentContext and the initialContext
	if (this.initialContext) {
		this.initialContext.imageWidth = newSize;
		this.initialContext.imageHeight = newSize;
	}

	// If we are still loading the application then currentContext will not be available.  But it will be updated
	// when it is inialized since we set initialContext already.
	if (this.currentContext) {
		this.currentContext.imageWidth = newSize;
		this.currentContext.imageHeight = newSize;

		// Resubmit the map request.
		this.getMapImage();
	}
	return true;
};

// Request the map image for the current context from the server.
// Returns:		null
GISMap.prototype.getMapImage=function() {
	// Display the loading map notification (it might already be on, no big deal).
	this.isLoadingMap(true);

	request_getMapImage(this);
	
	return null;
};

// Set the map image.  This is the only place this should be done at.
// Parameters:	mapURL (optional) - HTTP address of map image, used for initial load only.
//					getArgs (optional) - Direct dump of server response.
// Returns:		null
GISMap.prototype.setMapImage=function(mapURL, getArgs) {
	if (getArgs) {
		this.currentContext.minX = getArgs.shift();
		this.currentContext.minY = getArgs.shift();
		this.currentContext.maxX = getArgs.shift();
		this.currentContext.maxY = getArgs.shift();	
		this.currentContext.mapURL = getArgs.shift();
		this.currentContext.legendURL = getArgs.shift();

		// Update the minimap
		this.updateMinimap();
	
		// Update the scalebar
		this.updateScaleBar();
		
		mapURL = this.currentContext.mapURL;
		
		// If a legend object has been associated with this GISMap instance, then update it.
		if ((this.legendImage != null) && (this.currentContext.legendURL !== '')) {
			this.legendImage.src = this.currentContext.legendURL;
		}
	}

	// Load the actual image.  It is the first (and only) image in the mapImage div.
	var mapImage = this.mapImage.getElementsByTagName('img')[0];
	if (!mapImage) {return;}
	
	mapImage.src = mapURL;

	// When the user hovers over the image it shows the Alt text.  We don't want this to happen.
	// However, if no image is loaded we do want the text to be there.
	if (mapURL === '') {
		// Set the ALT text
		mapImage.alt = 'GIS Map Unavailable';
	} else {
		// Clear the ALT text while a map is visible.
		window.setTimeout(function() {mapImage.alt = '';}, 3000);
	}
	
	return null;
};

// Set the currentContext ParcelID and request the extent for this Parcel.  The call back will initiate
// an update of the map.
// Parameters:	ParcelID
// Returns:		null
GISMap.prototype.getParcelZoomToExtent=function(ParcelID) {
	this.currentContext.selectedParcelID = ParcelID;
	request_getParcelZoomToExtent(this);
	
	return null;
};

// Resets the currentContext to the initialContext.
// Returns:		null
GISMap.prototype.resetContext=function() {
	// Make a copy of the initialContext for the currentContext.
	if (this.initialContext) {
		return this.setContext(this.initialContext);
	} else {
		alert('GISMap.resetContext() - Error: There is no initial context to load.');
		return false;
	}
};

// Overwrites the currentContext with a new instance of GISContext.  Requests a new map image.
// Parameters:	GISMapContext - The new context to use.
// Returns:		Boolean indicating success.
GISMap.prototype.setContext=function(thisGISMapContext) {
	// Make a copy of the thisGISMapContext object and store it in the currentContext.
	if (thisGISMapContext) {
		this.currentContext = thisGISMapContext.clone();
		this.getMapImage();

		// Fire the event handler if one is registered.
		if (this.onMapContextReset) {
			if (!this.onMapContextReset.onReceive(this)) {
				return false;
			}
		}
		return true;
	} else {
		alert('GISMap.setContext() - Error: No map context was passed.');
		return false;
	}
};

// Displays the overlay on the map indicating that the API is waiting for a server response.
// It is fine to set the status to the same thing several times.
// Parameters:	isLoading - Boolean value indicating new status.  If null, the return value contains the current status.
// Returns:		Boolean value indicating original status.  Returns null if no loading notification object has been registered.
GISMap.prototype.isLoadingMap=function(isLoading) {
	// Make sure the loadingNotification DIV has been registered.
	if (!this.loadingNotification) {return null;}
	
	var origValue = (this.loadingNotification.style.display == 'block');

	if (isLoading == null) {return origValue;}
	
	if (isLoading) {
		this.loadingNotification.style.display='block';
	} else {
		this.loadingNotification.style.display='none';
	}

	return origValue;	
};

// Removes the loading notification and puts the map back to 0,0
// Returns:		Boolean indicating that the map image has been loaded.
//					(note, this method is called internally so the return value is not available).
GISMap.prototype.mapImageLoaded = function(e) {
	if (window.event) {e=window.event;}
	if (this.className == 'mapImg') {
		// Get the map object that is associated with this image.
		var activeMap = g_MapRegistry.getMapByParent(this.parentNode);
		if (activeMap == null) {return null;}

		if (this.readyState) {
			if (this.readyState == 'complete') {
				activeMap.isLoadingMap(false);
				activeMap.mapImage.style.top = 0;
				activeMap.mapImage.style.left = 0;

				// Fire the event handler if one is registered.
				if (activeMap.onMapLoaded) {
					if (!activeMap.onMapLoaded.onReceive(activeMap)) {
						return false;
					}
				}
				return true;
			}
		} else {
			// No readystate, this must be FF.
			activeMap.isLoadingMap(false);
			activeMap.mapImage.style.top = 0;
			activeMap.mapImage.style.left = 0;
			
			// Fire the event handler if one is registered.
			if (activeMap.onMapLoaded) {
				if (!activeMap.onMapLoaded.onReceive(activeMap)) {
					return false;
				}
			}
			return true;
		}
	}
};

// Based on currentContext, write a new value for the scalebar.
// Returns:		Integer indicating scale distance.  Returns null if no scaleBarImage is registered.
GISMap.prototype.updateScaleBar=function() {
	// Make sure a scaleBar has been registered.
	if (!this.scaleBar) {return null;}
	
	// Get the scalebar image.  It is the first (and only) image in div[scaleBar]
	var scaleBarImage = this.scaleBar.getElementsByTagName('img')[0];
	if (!scaleBarImage) {return null;}
	
	var scaleBarImageWidth = scaleBarImage.width - 10;
	
	// Get the text box for the scalebar.  It is the first (and only) span in div[scaleBar]
	var scaleBarText = this.scaleBar.getElementsByTagName('span')[0];
	
	var mScale = Math.round((this.currentContext.maxX - this.currentContext.minX) * scaleBarImageWidth / this.currentContext.imageWidth);
	scaleBarText.innerHTML = mScale;

	return mScale;
};

// Based on the intitialContext compared to the currentContext, set the minimap highlighting.
// Returns:		null
GISMap.prototype.updateMinimap=function() {
	// Get the clipping image
	if ((!this.minimapHighlight) && (this.minimapHighlight == null)) {return null;}
	var clipImage = this.minimapHighlight;

	var clipImageWidth = clipImage.width;
	var clipImageHeight = clipImage.height;
	
	var clipLeft = parseInt((this.currentContext.minX - this.initialContext.minX) / (this.initialContext.maxX - this.initialContext.minX) * clipImageWidth, 10);
	var clipRight = parseInt((this.currentContext.maxX - this.initialContext.minX) / (this.initialContext.maxX - this.initialContext.minX) * clipImageWidth, 10);
	var clipTop = parseInt((this.initialContext.maxY - this.currentContext.maxY) / (this.initialContext.maxY - this.initialContext.minY) * clipImageHeight, 10);
	var clipBottom = parseInt((this.initialContext.maxY - this.currentContext.minY) / (this.initialContext.maxY - this.initialContext.minY) * clipImageHeight, 10);

	if ((clipRight - clipLeft) < 3) {
		clipRight = clipLeft + 3;
	}
	if ((clipBottom - clipTop) < 3) {
		clipBottom = clipTop + 3;
	}

	clipImage.style.clip='rect(' + clipTop + 'px ' + clipRight + 'px ' + clipBottom + 'px ' + clipLeft + 'px)';	
	
	return null;
};

// Shows/hides the minimap.
// Parameters:	setVisibility - Boolean indicating that the visibility should be explictly set rather than toggled.
// Returns:		Boolean indicating the original visibility.  Returns null if there is a problem.
GISMap.prototype.toggleMinimap=function(setVisibility) {
	// This can be directly called or fired from a click event.
	// If [this] is a GISMap object then set activeMap = this, otherwise, figure it out.
	var activeMap = null;
	if (this.toString() == 'GISMap') {
		activeMap = this;
	} else {
		activeMap = g_MapRegistry.getMapByParent(this.parentNode.parentNode.parentNode);
	}
	if (activeMap == null) {return null;}
	
	// Make sure a minimap is registered.
	if (!activeMap.minimap) {return null;}
	
	// Get the current visibility of the minimap.
	var currVisibility = (activeMap.minimap.className != 'minimap collapsed');
	var origVisibility = currVisibility;

	if (setVisibility != null) {currVisibility = setVisibility;}

	if (currVisibility) {
		// It is visible, collapse it.
		activeMap.minimap.className = 'minimap collapsed';
	} else {
		// It is not visible, uncollapse it.
		activeMap.minimap.className = 'minimap';
	}
	
	return origVisibility;
};

//-- Mouse Management --//

// This function is called when the mouse is pressed down.  It dereferences the map that was
// clicked based on the targetElement.  A call to GISMapRegistry.getActiveMap() is used.
// Since only one map can be clicked on at any time, we cache this map as the activeMap so the
// next call to getActiveMap will be faster (and work even if the user moves the mouse out of the mapFrame).
// Returns:		null
GISMap.prototype.onMapMouseDown = function(e) {
	if (!e) {e=window.event;}

	var activeMap = g_MapRegistry.getActiveMap(e);
	// Cache it
	g_MapRegistry.setActiveMap(activeMap);

	// Make sure we have a map.
	if (activeMap == null) {return null;}

	activeMap.isLoadingMap(false);
	// If the currentMapTool isn't set, or it is set to SELECT then return.
	if ((activeMap.currentMapTool === '') || (activeMap.currentMapTool === 'SELECT')) {
		return null;
	}
	
	var mouseLoc = activeMap.getImageXY(e);

	// We clicked onside the map, set the active tool to the current tool.
	// This will tell the onMouseUp function that we started in a valid section.
	activeMap.activateCurrentMapTool();

	// Depending on the current map tool, fire the appropriate handler.
	if (activeMap.currentMapTool == 'ZOOMIN') {
		activeMap.startZoom(mouseLoc.x, mouseLoc.y);
	}
	if (activeMap.currentMapTool == 'ZOOMOUT') {
		activeMap.startZoom(mouseLoc.x, mouseLoc.y);
	}
	if (activeMap.currentMapTool == 'PAN') {
		activeMap.startPan(mouseLoc.x, mouseLoc.y);
	}
	
	addEvent(document,'mousemove', onMapMouseMove);

	// We canceled the drag event for IE.  If this is firefox then do not allow the onMouseDown event to propagate.
	// We know it isn't IE if it doesn't have a window.event object.
	if (!window.event) {
		e.preventDefault();
	}
	
	return null;
};

// When the mouse is pressed, the onMouseMove event handler is registered.  It will fire this event.
// Returns:		null
function onMapMouseMove(e) {
	if (!e) {e=window.event;}
	var activeMap = g_MapRegistry.getActiveMap(e);

	// Make sure we have a map.
	if (activeMap == null) {return false;}
	if (activeMap.currentContext == null) {return false;}

	// Get the current mouse position.
	var mouseLoc = activeMap.getImageXY(e);

	if (activeMap.currentMapTool == 'ZOOMIN') {
		activeMap.updateZoom(mouseLoc.x, mouseLoc.y);
	}
	if (activeMap.currentMapTool == 'ZOOMOUT') {
		activeMap.updateZoom(mouseLoc.x, mouseLoc.y);
	}
	if (activeMap.currentMapTool == 'PAN') {
		activeMap.updatePan(mouseLoc.x, mouseLoc.y);
	}
	
	return null;
}

// When the mouse button is lifted, this function is called.
// It also removes the onMouseMove event handler and de-activates the current map.
// Returns:		null
GISMap.prototype.onMapMouseUp = function(e) {
	var activeMap = g_MapRegistry.getActiveMap(e);

	// Make sure we have a map.
	if (activeMap == null) {return false;}
	if (activeMap.currentContext == null) {return false;}

	// Remove this map from the cache.
	g_MapRegistry.setActiveMap(null);

	// Get the current mouse position.
	var mouseLoc = activeMap.getImageXY(e);

	removeEvent(document,'mousemove',onMapMouseMove);

	// Return if the active tool is not the current tool.  I don't remember how this can happen.  But it did.
	if (activeMap.activeMapTool != activeMap.currentMapTool) {
		return;
	} else {
		// It is the current tool, but we are finalizing it so de-activate it
		activeMap.deactivateCurrentMapTool();
	}

	// If the current tool is SELECT then return
	if (activeMap.currentMapTool == 'SELECT') {
		return;
	}
	if (activeMap.currentMapTool == 'ZOOMIN') {
		activeMap.stopZoom(mouseLoc.x, mouseLoc.y);
	}
	if (activeMap.currentMapTool == 'ZOOMOUT') {
		activeMap.stopZoom(mouseLoc.x, mouseLoc.y);
	}
	if (activeMap.currentMapTool == 'PAN') {
		activeMap.stopPan(mouseLoc.x, mouseLoc.y);
	}
	
	return null;
};

// Any time the mapFrame is clicked this is fired.  Used for parcel selection only.
// Return:		null
GISMap.prototype.onMapClick = function(e) {
	if (!e) {e=window.event;}
	var activeMap = g_MapRegistry.getActiveMap(e);

	// Make sure we have a map.
	if (activeMap == null) {return false;}
	if (activeMap.currentContext == null) {return false;}
	
	// Determine where the user clicked.
	var mouseLoc = activeMap.getImageXY(e);

	// Translate the clicked location into feet.
	var clickedFeet = activeMap.currentContext.ConvertPixelsToFeet(mouseLoc);

	// Fire the event handler if one is registered.
	if (activeMap.onMouseClick) {
		if (!activeMap.onMouseClick.onReceive(activeMap, mouseLoc, clickedFeet)) {
			return false;
		}
	}
	
	if (activeMap.currentMapTool == 'SELECT') {
		// Check to see if the user is allowed to select a parcel.
		if (!activeMap.hasCapability('ALLOW_SELECTPARCEL')) {
			alert('The server has not returned enough data to allow parcel selection.');
			return false;
		}
	
		// Set the select variables in currentContext as the feet.
		activeMap.currentContext.selectedX = clickedFeet.x;
		activeMap.currentContext.selectedY = clickedFeet.y;

		// We don't want the buffer to move around, clear it.
		// If there is a buffer then clear the highlighted parcels as well, if not then leave them.
		if (activeMap.currentContext.bufferDistance != 0) {
			activeMap.currentContext.bufferDistance = 0;
			activeMap.currentContext.highlightedParcels = '';
		}

		// Request the ParcelID from the server.  Once we have it we can request the map image and parcel data.
		activeMap.isLoadingMap(true);
		request_getSelectedParcelID(activeMap);
	}
	return null;
};

// Whenever the mapFrame is double-clicked this function is called.  Used only to center the map while in PAN mode.
// Returns:		null
GISMap.prototype.onMapDoubleClick = function(e) {
	if (!e) {e=window.event;}
	var activeMap = g_MapRegistry.getActiveMap(e);

	// Make sure we have a map.
	if (activeMap == null) {return false;}
	if (activeMap.currentContext == null) {return false;}

	var mouseLoc = activeMap.getImageXY(e);
	// Translate the clicked location into feet.
	var clickedFeet = activeMap.currentContext.ConvertPixelsToFeet(mouseLoc);

	// Fire the event handler if one is registered.
	if (activeMap.onMouseDoubleClick) {
		if (!activeMap.onMouseDoubleClick.onReceive(activeMap, mouseLoc, clickedFeet)) {
			return false;
		}
	}

	// The only functionality that fire onDblClick is the PAN tool.
	if (activeMap.currentMapTool != 'PAN') {
		return null;
	}

	// Center the map on this clicked location.

	// To do this, we simulate a pan.  We act as if the mouse was clicked in the very center, we then
	// update the pan towards the direction the user chose.  Then the stopPan function is called.
	
	// Get half of the map (this pixel location is where the clicked location will go, it is center).
	var halfWidth = activeMap.currentContext.imageWidth / 2;
	var halfHeight = activeMap.currentContext.imageHeight / 2;

	// Act as if the user clicked in the very center of the image and started dragging.
	activeMap.startPan(halfWidth, halfHeight);

	// The put the clicked location in the center and then update the pan.
	var newX = (halfWidth - (mouseLoc.x - halfWidth));
	var newY = (halfHeight - (mouseLoc.y - halfHeight));
	activeMap.updatePan(newX, newY);
	activeMap.stopPan(newX, newY);
	
	return null;
};

// Gets the location of the mouse pointer relative to the map frame.
// Returns:		Array{x,y} where x and y are pixels.
GISMap.prototype.getImageXY = function(e) {
	// Private function; returns the distance from [obj] to the edge of the screen.
	var findPosX = function(obj) {
		var curleft = 0;
		if ((typeof(obj.offsetParent) != 'unknown') && (obj.offsetParent))
		{
			while (obj.offsetParent)
			{
				curleft += obj.offsetLeft;
				obj = obj.offsetParent;
			}
		} else if (obj.x) {
			curleft += obj.x;
		}
		
		return curleft;
	};
	
	// Private function; returns the distance from [obj] to the edge of the screen.
	var findPosY = function(obj) {
		var curtop = 0;
		if ((typeof(obj.offsetParent) != 'unknown') && (obj.offsetParent))
		{
			while (obj.offsetParent)
			{
				curtop += obj.offsetTop;
				obj = obj.offsetParent;
			}
		} else if (obj.y) {
			curtop += obj.y;
		}
		
		return curtop;
	};

	e = e || window.event;
	var cursor = {x:0, y:0};
	if (e.pageX || e.pageY) {
		cursor.x = e.pageX;
		cursor.y = e.pageY;
	} else {
		cursor.x = e.clientX + 
			(document.documentElement.scrollLeft || 
			document.body.scrollLeft) - 
			document.documentElement.clientLeft;
		cursor.y = e.clientY + 
			(document.documentElement.scrollTop || 
			document.body.scrollTop) - 
			document.documentElement.clientTop;
	}

	// The mouse cooridinates are relative to the map image.
	// Get the location of the mapContainer and then subtract the mouse cooridinates from it.
	var mouseLoc = {x:cursor.x - parseInt(findPosX(this.mapContainer), 10)-3, y:cursor.y - parseInt(findPosY(this.mapContainer), 10)-3};
	return mouseLoc;
};

//-- Map Manipulation --//

// Begins the pan process.  Stores the current location in the panMouseX and panMouseY attributes.
GISMap.prototype.startPan=function(X, Y) {
	// We cannot just move the image with the mouse unless the user clicked at the very top-left corner.
	// We have to figure out how far into the image the mouse was when it was clicked and then move based on that offset.

	// Store the current mouse position in the div for reference.
	// We need to take into account the current location of the image.
	var currLeft = parseInt(this.mapImage.style.left.replace(/px/,''), 10);
	var currTop = parseInt(this.mapImage.style.top.replace(/px/,''), 10);
	if (isNaN(currLeft)) {currLeft = 0;}
	if (isNaN(currTop)) {currTop = 0;}
	
	this.mapImage.setAttribute('panMouseX',X - currLeft);
	this.mapImage.setAttribute('panMouseY',Y - currTop);
	// That is all, the updatePan() function handles the movement.
};

// Moves the image based on the mouse movement.
GISMap.prototype.updatePan=function(X, Y) {
	// Get the difference between the current location and the original starting location.
	var origX = parseInt(this.mapImage.getAttribute('panMouseX'), 10);
	var origY = parseInt(this.mapImage.getAttribute('panMouseY'), 10);

	var newX = X - origX;
	var newY = Y - origY;

	if (!isNaN(newX)) {
		this.mapImage.style.left = newX + 'px';
	}
	if (!isNaN(newY)) {
		this.mapImage.style.top = newY + 'px';
	}
};

// Stops the pan process.  If the map moved then fire a request to have the map updated.
GISMap.prototype.stopPan=function(X, Y) {
	// Get the new min and max locations
	var minX = -parseInt(this.mapImage.style.left.replace(/px/,''), 10);
	var minY = -parseInt(this.mapImage.style.top.replace(/px/,''), 10);

	if (isNaN(minX)) {minX = 0;}
	if (isNaN(minY)) {minY = 0;}

	var maxX = minX + parseInt(this.mapImage.clientWidth, 10);
	var maxY = minY + parseInt(this.mapImage.clientHeight, 10);

	// Make sure we have a context.
	if (!this.currentContext) {alert('GISMap.stopPan() - Error: There is no map to pan.');return false;}

	// Convert these points to feet
	var newMin = this.currentContext.ConvertPixelsToFeet({x:minX, y:minY});
	var newMax = this.currentContext.ConvertPixelsToFeet({x:maxX, y:maxY});

	// Check to see if there was a change in *any* of the values.
	// For some reason the min and max Y values become inverted.  It doesn't affect mapping, but it does affect comparisons.
	var refreshMap = false;
	if (this.currentContext.minX != newMin.x) {refreshMap = true;}
	if (this.currentContext.minY != newMax.y) {refreshMap = true;}
	if (this.currentContext.maxX != newMax.x) {refreshMap = true;}
	if (this.currentContext.maxY != newMin.y) {refreshMap = true;}

	if (refreshMap) {
		// Set the current context to the new values and send it to the server.
		this.currentContext.minX = newMin.x;
		this.currentContext.minY = newMin.y;
		this.currentContext.maxX = newMax.x;
		this.currentContext.maxY = newMax.y;

		// Request a new image from the server.
		this.getMapImage();
	}

	// Reset the original mouse click location
	this.mapImage.setAttribute('panMouseX', null);
	this.mapImage.setAttribute('panMouseY', null);
};

// Initializes the zoom box.  Both ZOOMIN and ZOOMOUT use this.
GISMap.prototype.startZoom=function(X,Y) {
	// If this is IE6- then use a filter to add PNG alpha-transparency support.

	if (this.isIE6) {
		this.zoomDiv.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='images/zoomBoxBackground.png', sizingMethod='scale')";	
	} else {
		this.zoomDiv.style.backgroundImage = 'url(images/zoomBoxBackground.png)';
	}

	// Offset the zoom box corner from the pointer a small bit.
	X -= 1;
	Y -= 1;

	// Store the current mouse location as the top corner of the box.
	this.zoomDiv.style.left = X + 'px';
	this.zoomDiv.style.top = Y + 'px';
	
	// Reset the values.
	this.zoomDiv.style.width = '0px';
	this.zoomDiv.style.height = '0px';
	
	// Show the box.
	this.zoomDiv.style.display='block';
};

// This function handles the zoom box resizing process.  Both ZOOMIN and ZOOMOUT use this.
GISMap.prototype.updateZoom=function(X, Y) {
	var zoomTop = parseFloat(this.zoomDiv.style.top.replace(/px/,''));
	var zoomLeft = parseFloat(this.zoomDiv.style.left.replace(/px/,''));

	// Offset the corner from the pointer a small bit.
	X -= 1;
	Y -= 1;

	// If the new location is less than the originally clicked location then swap them.
	var tmp = 0;
	if (zoomTop > Y) {
		tmp = Y;
		this.zoomDiv.style.top = Y + 'px';
		Y = zoomTop;
		zoomTop = tmp;		
	}
	if (zoomLeft > X) {
		tmp = X;
		this.zoomDiv.style.left = X + 'px';
		X = zoomLeft;
		zoomLeft = tmp;		
	}

	// Make sure it doesn't go out of bounds
	if (zoomTop < 0) {
		zoomTop = 0;
		this.zoomDiv.style.top = '0px';
	}
	if (zoomLeft < 0) {
		zoomLeft = 0;
		this.zoomDiv.style.left = '0px';
	}

	// Get the difference
	var zoomWidth = X - zoomLeft;
	var zoomHeight = Y - zoomTop;

	if ((zoomWidth + zoomLeft) > (this.mapImage.clientWidth - 4)) {
		zoomWidth = this.mapImage.clientWidth - 2 - zoomLeft;
	}
	if ((zoomHeight + zoomTop) > (this.mapImage.clientHeight - 4)) {
		zoomHeight = this.mapImage.clientHeight - 2 - zoomTop;
	}
	
	// If either is a negative number then something is wrong, return.
	if ((zoomWidth < 0) || (zoomHeight < 0)) {
		return;
	}
	
	this.zoomDiv.style.width = zoomWidth + 'px';
	this.zoomDiv.style.height = zoomHeight + 'px';
};

// Determines what extent is encompassed by the zoom box.  Modifys the extent in case of ZOOMOUT.  Submits the updated
// extent to the server.  ZOOMIN and ZOOMOUT use this.
GISMap.prototype.stopZoom=function(X, Y) {
	// Just make sure that there is a context.
	if (!this.currentContext) {
		// Odd, there is no context.
		// Reload the webpage.
		window.location.reload(true);
		return;
	}

	// If the width and height of the zoomBox is less than 2 then return
	if ((parseInt(this.zoomDiv.style.width.replace(/px/,''), 10) < 2) && (parseInt(this.zoomDiv.style.height.replace(/px/,''), 10) < 2)){
		this.removeZoomBox();
		return;
	}

	// Convert the starting and stopping points to feet, update the current context and request a new image from the server.
	var minX = parseInt(this.zoomDiv.style.left.replace(/px/,''), 10);
	var minY = parseInt(this.zoomDiv.style.top.replace(/px/,''), 10);
	var maxX = parseInt(parseInt(minX, 10) + parseInt(this.zoomDiv.style.width.replace(/px/,''), 10), 10) + 2; // Add two for the borders, I would have prefered to get the
	var maxY = parseInt(parseInt(minY, 10) + parseInt(this.zoomDiv.style.height.replace(/px/,''), 10), 10) + 2; // actual border width, but I cannot figure it out in short order.

	var newMin = this.currentContext.ConvertPixelsToFeet({x:minX, y:minY});
	// The base + length is the ending point (cannot use mouse position because it could be beyond bounds of map)
	var newMax = this.currentContext.ConvertPixelsToFeet({x:maxX, y:maxY});

	// If we are zooming out then we want to resize the entire map to be the size of the box.
	if (this.currentMapTool == 'ZOOMOUT') {
		var minXoffset = (parseFloat(this.currentContext.minX) - parseFloat(newMin.x)) * 2;
		var maxXoffset = (parseFloat(this.currentContext.maxX) - parseFloat(newMax.x)) * 2;
		newMin.x = parseFloat(this.currentContext.minX) + minXoffset;
		newMax.x = parseFloat(this.currentContext.maxX) + maxXoffset;

		// Swap the values in newMin.y and newMax.y
		var tmp = newMin.y;
		newMin.y = newMax.y;
		newMax.y = tmp;
		
		var minYoffset = (parseFloat(this.currentContext.minY) - parseFloat(newMin.y)) * 2;
		var maxYoffset = (parseFloat(this.currentContext.maxY) - parseFloat(newMax.y)) * 2;
		newMin.y = parseFloat(this.currentContext.minY) + minYoffset;
		newMax.y = parseFloat(this.currentContext.maxY) + maxYoffset;
	}

	// Set the current context to the new values and send it to the server.
	this.currentContext.minX = newMin.x;
	this.currentContext.minY = newMin.y;
	this.currentContext.maxX = newMax.x;
	this.currentContext.maxY = newMax.y;

	// Hide the zoom box and reset the data.
	this.removeZoomBox();
	
	// Request a new image from the server.
	this.getMapImage();
};

// Removes the zoom box and resets the data.
GISMap.prototype.removeZoomBox=function() {
	// Hide it.
	this.zoomDiv.style.display='none';
	
	// Reset the zoomBox values
	this.zoomDiv.style.width='0px';
	this.zoomDiv.style.height='0px';

	// Turn the active tool off.
	this.deactivateCurrentMapTool();
};

// Sets the current tool.  Changes the mouse cursor for some.
// Returns:		String - Name of original map tool.
//					may return Boolean false if the map doesn't support the tool.
GISMap.prototype.setCurrentMapTool=function(toolName) {
	toolName = toolName.toUpperCase();
	if (!GISMap.isValidToolName(toolName)) {
		alert('Invalid tool name (' + toolName + ').');
		return false;
	}

	// Make sure the map is capable of doing this.
	if ((toolName == 'ZOOMIN') && (!this.hasCapability('ALLOW_ZOOMIN'))) {
		alert('The server has not returned enough information to allow you to zoom in.');
		return false;
	}
	if ((toolName == 'ZOOMOUT') && (!this.hasCapability('ALLOW_ZOOMOUT'))) {
		alert('The server has not returned enough information to allow you to zoom out.');
		return false;
	}
	if ((toolName == 'PAN') && (!this.hasCapability('ALLOW_PAN'))) {
		alert('The server has not returned enough information to allow you to pan the map.');
		return false;
	}
	if ((toolName == 'SELECT') && (!this.hasCapability('ALLOW_SELECT'))) {
		alert('The server has not returned enough information to allow you to select a parcel.');
		return false;
	}

	var originalMapTool = this.currentMapTool;
	this.currentMapTool = toolName;

	var newMouseCursor = '';
	switch (toolName) {
		case 'PAN' : newMouseCursor = 'move'; break;
		case 'SELECT' : newMouseCursor = 'pointer'; break;
	}
	
	this.setMapCursor(newMouseCursor);
	return originalMapTool;
};

// Sets the "active" tool to the current tool.  This lets the mouse handlers know that the user
// started off in a valid section.
GISMap.prototype.activateCurrentMapTool=function() {
	this.activeMapTool = this.currentMapTool;
};

// Un-sets the "active" tool.
GISMap.prototype.deactivateCurrentMapTool=function() {
	this.activeMapTool = '';
};

// Changes the type of cursor displayed on the map.
GISMap.prototype.setMapCursor=function(cursorName) {
	if ((cursorName == null) || (cursorName === '')) {
		cursorName = 'default';
	}
	this.mapImage.style.cursor = cursorName.toLowerCase();
};

// STATIC
GISMap.isValidToolName=function(toolName) {
	switch (toolName.toUpperCase()) {
		case 'ZOOMIN' : return true;
		case 'ZOOMOUT' : return true;
		case 'PAN' : return true;
		case 'SELECT' : return true;
		case '' : return true;
	}
	return false;
};

// STATIC
GISMap.input_panMap=function(mapID, direction) {
	// Start the panning process
	// Tell it to move a certain amount based on the size of the map and the direction to go.\
	// Stop the panning process to submit the map change request.

	var activeMap = g_MapRegistry.getMapById(mapID);
	if (activeMap == null) {return false;}
	if (activeMap.currentContext == null) {return false;}

	var dirX=0;
	var dirY=0;
	switch (direction) {
		case 'N': dirX=0;dirY=1;break;
		case 'S': dirX=0;dirY=-1;break;
		case 'E': dirX=-1;dirY=0;break;
		case 'W': dirX=1;dirY=0;break;
	}
	
	// Move the map 50% in the desired direction.
	// Get the width and height
	var x=(activeMap.mapImage.clientWidth / 2) * dirX;
	var y=(activeMap.mapImage.clientHeight /2) * dirY;

	activeMap.startPan(0,0);
	activeMap.updatePan(x,y);
	activeMap.stopPan(null,null);

	return false;
};

//-- Data Handling --//

// Requests that the information for the current parcel be populated into the infoFrame
// If an infoFrame hasn't been registered then nothing will happen.
GISMap.prototype.getParcelData=function() {
	// Make sure an infoFrame is registered and that we are allowed to use it (one in the same, but it is more clear to check both).
	if ((this.hasCapability('ALLOW_USEINFOFRAME')) && (this.infoFrame != null)) {
		// Make sure that a parcel has been selected.
		if (this.currentContext.selectedParcelID !== '') {
			// Request the parcel data for the ParcelID selected in this map.
			request_getParcelData(this);
		}
	} else {
		// Don't request the parcel data.  There isn't anywhere to put it.
		// I am not going to alert the user because this is not a requirement.
	}
};

// Specifies which DIV will handle the infoFrame.  (If you don't set one, it is ok).
// Parameters:		HTMLElement Object
GISMap.prototype.setInfoFrame=function(infoFrameDiv) {
	if (infoFrameDiv == null) {return false;}
	if (typeof(GISInfoFrame) == 'undefined') {return false;}
	
	// A real element was passed and the GISInfoFrame class is available.
	// Create a new instance of the GISInfoFrame and associate it with this GISMap.
	var GISInfoFrameObj = new GISInfoFrame(infoFrameDiv, this.id);
	// Make sure it is initialized--this essentially means that an XML parser was available.
	if ((GISInfoFrameObj) && (GISInfoFrameObj.initialized)) {
		this.infoFrame = GISInfoFrameObj;
		// Add the capability to use the infoFrame now.
		this.addCapability('ALLOW_USEINFOFRAME');
	}
};

// Returns true if a parcel is selected.
// Returns:		Boolean
GISMap.prototype.verifyParcelIsSelected=function() {
	if (this.currentContext) {
		if (this.currentContext.selectedParcelID !== '') {
			return true;
		}
	}
	return false;
};

// Entry point for creating a buffer
// Requests the extent to load and a list of parcels in range.
// Parameters:		radiusFeet - The distance which to check for parcels.
// Remarks:			A parcel must be selected before calling.
GISMap.prototype.retrieveBuffer=function(radiusFeet) {
	// Verify that a parcel has been selected.  This should have been called by the function that
	// handled the user input, but we absolutely cannot have this called if a parcel isn't selected.
	// A rogue call could be made so we have to make absolute sure.
	if (!this.verifyParcelIsSelected) {
		throw new Error('GISMap.retrieveBuffer() - Error: A parcel must be selected before calling retrieveBuffer.');
		// Return False
	}

	this.isLoadingMap(true);
	this.currentContext.bufferDistance = radiusFeet;
	request_getParcelBuffer(this);
	return false;
};

GISMap.prototype.bufferReceived=function(getArgs) {
	var parcelList = getArgs.shift();
	this.currentContext.highlightedParcels = parcelList;

	var updateEnvelope = getArgs.shift();
	
	// Fire the onBufferReceived event if one is registered.
	if (this.onBufferReceived) {
		// Pass the parcel list as well as the updateEnvelope flag.
		if (!this.onBufferReceived.onReceive(this, {parcelList:parcelList, updateEnvelope:updateEnvelope, selectedParcelID:this.currentContext.selectedParcelID, bufferDistance:this.currentContext.bufferDistance})) {
			return false;
		}
	}
	
	if (updateEnvelope == 'true') {
		this.currentContext.minX = getArgs.shift();
		this.currentContext.minY = getArgs.shift();
		this.currentContext.maxX = getArgs.shift();
		this.currentContext.maxY = getArgs.shift();	

		this.getMapImage();
	}

	// UPDATE: There is no pre-defined method of handling buffered parcel results.
	// The programmer must register an event handler for it.
};

// Requests the image services from the server.
// Remarks:			The received data will be placed in a popup box.
GISMap.prototype.debug_getImageServices=function() {
	request_getImageServices();
};

// Requests the image services from the server.
// Remarks:			The received data will be placed in a new window.  However, you have to View Source to see it.
GISMap.prototype.debug_getServiceInfo=function() {
	request_getServiceInfo();
};
//-- END GISMap Class --//

//-- BEGIN GISMapEvent Class --//
// Creates a structured event for the GISMap class.
// Parameters:		onReceiveFunction - Optional: Specifies which function to call when the event is received.
function GISMapEvent(onReceiveFunction) {
	this.onReceiveFunction = onReceiveFunction || null;
}

GISMapEvent.prototype.setOnReceiveFunction=function(functionConstruct) {
	this.onReceiveFunction = functionConstruct;
};

GISMapEvent.prototype.onReceive=function(GISMapObj, additionalParameters) {
	if (this.onReceiveFunction) {
		var returnValue = this.onReceiveFunction(GISMapObj, additionalParameters);

		// If the onReceiveFunction returns true or doesn't return a value then return true (which allows the calling
		// method to continue execution).  If it returns false then return false.
		if ((typeof(returnValue) == 'undefined') || (returnValue)) {
			return true;
		} else {
			return false;
		}
	} else {
		alert('No received function defined for registered event handler.');
		return true;
	}
};

//-- END GISMapEvent Class --//

//-- BEGIN GISMapContext Class --//
// This is the GISMapContext object.  It stores the current extent as well as the turned on layers and selected x,y.
function GISMapContext(minX, minY, maxX, maxY) {
	this.minX = minX;
	this.minY = minY;
	this.maxX = maxX;
	this.maxY = maxY;
	this.imageWidth = 0;
	this.imageHeight = 0;

	this.selectedX = 0;
	this.selectedY = 0;
	this.selectedParcelID = '';
	
	this.bufferDistance = 0;
	this.highlightedParcels = '';
	
	this.mapURL = '';
	this.legendURL = '';
	this.groupList = null;
}

// Converts a pixel location on the map into feet.
GISMapContext.prototype.ConvertPixelsToFeet=function(pixelArray) {
	var feet={x:0,y:0};

	var pixelX = Math.abs(this.maxX - this.minX) / this.imageWidth;
	feet.x = parseFloat((pixelX * parseInt(pixelArray.x, 10))) + parseFloat(this.minX);

	var mouseY = this.imageHeight - parseInt(pixelArray.y, 10);
	var pixelY = Math.abs(this.maxY-this.minY) / this.imageHeight;

	feet.y = parseFloat(pixelY * mouseY) + parseFloat(this.minY);
	
	return feet;
};

GISMapContext.prototype.debug_dumpValues=function() {
	var variableList = '';
	variableList += 'MinX: ' + this.minX + '\r\n';
	variableList += 'MinY: ' + this.minY + '\r\n';
	variableList += 'MaxX: ' + this.maxX + '\r\n';
	variableList += 'MaxY: ' + this.maxY + '\r\n';
	alert(variableList);
};

// This is the GroupList object.  It stores all the available groups with their respective layers as well as the visiblity of the contained layers.
function GISGroupList() {
	// Group Array is in format (id, name, visible)
	this.groups = [];

	this.dataLayer = new GISLayer();
	this.parcelLayer = new GISLayer();
}

function GISGroup() {
	this.id = '';
	this.name = '';
	this.visible = null;
	
	// Layer Array is in format (id, name, visible)
	this.mapLayers = [];
}

function GISLayer() {
	this.id = 0;
	this.name = '';
	this.dataNodeName = '';
}

// Helper Functions
// Provides commonly used functions such as css management and location detection.
// Also provides uniform event handler interface.

/* Client-side access to querystring name=value pairs -	Version 1.2.3 :: 22 Jun 2005 :: Adam Vandenberg */
// Usage:
//		var qs = new Querystring();
//		var thisValue = qs.get('thisValue');
function Querystring() {
	this.get=function(key, default_) {
		// This silly looking line changes UNDEFINED to NULL
		if (default_ == null) {default_ = null;}
	
		var value=this.params[key];
		if (value==null) {value=default_;}
	
		return value;
	};

	this.params = {};

	var qs=location.search.substring(1,location.search.length);

	if (qs.length === 0) {return "";}

	// Turn <plus> back to <space>
	// See: http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1
	qs = qs.replace(/\+/g, ' ');
	var args = qs.split('&'); // parse out name/value pairs separated via &
	
	// Split out each name=value pair
	for (var i=0;i<args.length;i++) {
		var value;
		var pair = args[i].split('=');
		var name = unescape(pair[0]);

		if (pair.length == 2) {
			value = unescape(pair[1]);
		} else {
			value = name;
		}
		
		this.params[name] = value;
	}
}

// This function was pulled from a script
// It gives credit to Scott Andrew, so I will do the same.
function addEvent(targetObject, eventType, executeFunction) {
	if (targetObject.addEventListener) {
		// FF
		targetObject.addEventListener(eventType, executeFunction, false);
		return true;
	} else if (targetObject.attachEvent) {
		// IE
		var r = targetObject.attachEvent("on" + eventType, executeFunction);
		return r;
	} else {
		alert('Handler could not be added.');
	}
}

// This function does the opposite of addEvent
function removeEvent(targetObject, eventType, executeFunction) {
	if (targetObject.removeEventListener) {
		// FF
		targetObject.removeEventListener(eventType, executeFunction, false);
		return true;
	} else if (targetObject.detachEvent) {
		// IE
		var r = targetObject.detachEvent("on" + eventType, executeFunction);
		return r;
	} else {
		alert('Handler could not be removed.');
	}
}

// Dynamically loads the script file into the browser.
function loadScript(filename) {
	// Only load scripts relative to this file.
	var documentHead = document.getElementsByTagName('head').item(0);

	// Determine if the script has already been loaded.
	// Look at all the loaded scripts.
	var tester = new RegExp(filename);
	for (var i=0;i<documentHead.getElementsByTagName('script').length;i++) {
		var testScriptSrc = documentHead.getElementsByTagName('script')[i].src;
		if (testScriptSrc.search(tester) != -1) {
			return true;
		}
	}

	// Create the script object
	var newScript = document.createElement('script');
	newScript.type = 'text/javascript';
	newScript.src = filename;
	
	// Add script to the head section after all other objects.
	documentHead.appendChild(newScript);
}

function getBaseURL() {
	// Find the GISMap.js script file.
	var GISMapScript = null;
	var scripts = document.getElementsByTagName('script');
	if (scripts.length !== 0) {
		for (var i=0;i<scripts.length;i++) {
			var thisScriptName = scripts[i].src;
			if (thisScriptName.substring(thisScriptName.length, thisScriptName.length - 9) == 'GISMap.js') {
				GISMapScript = scripts[i];
			}
		}
	}

	if (GISMapScript == null) {
		// This shouldn't even be possible because that is this file.
		alert('GISMapRegistry.getBaseURL() - Error: Could not find script definition reference.');
		return '';
	}
	
	// Determine the full path of this script file.  If none is found then assume that it is the same as the document.
	// It appears to be as simple as looking for 'http:' at the beginning.
	var scriptBaseURL = '';
	var scriptURL_location = '';
	if (GISMapScript.src.substring(0,5) == 'http:') {
		// This is absolutely linked.  Determine the URL up to /script and use that as the base path.
		scriptURL_location = GISMapScript.src.indexOf('\/scripts\/');
		if (scriptURL_location != -1) {
			scriptBaseURL = GISMapScript.src.substring(0,scriptURL_location+1);
		}
	} else {
		// The script is relatively linked.  The base url is everything up to the script/.
		// If the first thing is the script/ then the baseURL is blank (as it is in the GIS app).
		scriptURL_location = GISMapScript.src.indexOf('scripts\/');
		if (scriptURL_location != -1) {
			scriptBaseURL = GISMapScript.src.substring(0,scriptURL_location);
		}
	}
	return scriptBaseURL;
}

// Looks for newValue in selectObj drop-down list.  If it finds it, selects it.
function setDropDownValue(selectObj, newValue) {
	for (var i=0;i<selectObj.options.length;i++) {
		if (selectObj.options[i].value == newValue) {
			selectObj.options[i].selected = true;
			break;
		}
	}
}

// Dynamically change class styles
function changeCSS(declaration, styleName, newValue, styleSheetID) {
	var styleSheet = null;
	if (styleSheetID == null) {
		// Default to the first stylesheet.
		styleSheet = document.styleSheets[0];
	} else {
		// An ID was passed.  Get the object.
		if (document.getElementById(styleSheetID)) {
			if (document.getElementById(styleSheetID).sheet) {
				styleSheet = document.getElementById(styleSheetID).sheet;
			} else {
				styleSheet = document.styleSheets(styleSheetID);
			}
		} else {
			// The object doesn't exist, return null.
			return null;
		}
	}

	// If the styleSheet is still null, return null.
	if (styleSheet == null) {
		throw new Error('window.changeCSS() - Error: The map stylesheet object was found, but it didn\'t return a valid sheet');
		// return null
	}
	
	var theRules = [];
	if (styleSheet.cssRules) {
		theRules = styleSheet.cssRules;
	} else if (styleSheet.rules) {
		theRules = styleSheet.rules;
	}

	for (var i = 0; i<theRules.length; i++) {
		if (theRules[i].selectorText.toUpperCase() == declaration.toUpperCase()) {
			theRules[i].style[styleName] = newValue;
		}
	}
}

function getCSS(declaration, styleName, styleSheetID) {
	var styleSheet = null;
	if (styleSheetID == null) {
		// Default to the first stylesheet.
		styleSheet = document.styleSheets[0];
	} else {
		// An ID was passed.  Get the object.
		if (document.getElementById(styleSheetID)) {
			if (document.getElementById(styleSheetID).sheet) {
				styleSheet = document.getElementById(styleSheetID).sheet;
			} else {
				styleSheet = document.styleSheets(styleSheetID);
			}
		} else {
			// The object doesn't exist, return null.
			return null;
		}
	}

	// If the styleSheet is still null, return null.
	if (styleSheet == null) {
		throw new Error('window.getCSS() - Error: The map stylesheet object was found, but it didn\'t return a valid sheet');
		// return null
	}

	var theRules = [];
	if (styleSheet.cssRules) {
		theRules = styleSheet.cssRules;
	} else if (styleSheet.rules) {
		theRules = styleSheet.rules;
	}

	for (var i = 0; i<theRules.length; i++) {
		if (theRules[i].selectorText.toUpperCase() == declaration.toUpperCase()) {
			return theRules[i].style[styleName];
		}
	}
}

Object.prototype.clone = function() {
	var deep=true;
	var objectClone = new this.constructor();
	for (var property in this) {
		if (!deep) {
			objectClone[property] = this[property];
		} else if (typeof this[property] == 'object') {
			objectClone[property] = this[property].clone(deep);
		} else {
			objectClone[property] = this[property];
		}
	}
	return objectClone;
};

String.prototype.right = function(n) {
	if (n<=0) {
		return "";
	} else if (n > this.length) {
		return this;
	} else {
		var iLen = this.length;
		return this.substring(iLen, iLen - n);
	}
};