//==========================================
//VirtualMap Large Image Viewport Navigation
//JavaScript Full OOP Encapsulation Module

//Copyright 2008 Online Virtual Machines
//... => http://www.ov-ms.com
//No part of this code may be copied or reproduced
//... => without written authorisation from OVMS.
//All Rights Reserved.
//==========================================

//scope encapsulator
(function() {
	//======================================================
	//shorten scope chain lookups for commonly used values (idea borrowed from John Resig's jQuery)
	var undefined;
	//======================================================
	
	//=============================
	//config
	
	//value needed as rel attribute for links to be turned into virtualMaps
	var strLinkHook_RelValue = "OVMSVirtualMap";
	
	//default dimensions for viewport area
	var intDefaultViewportWidth = 100;
	var intDefaultViewportHeight = 100;
	var intDefaultImageXOffset = 0;
	var intDefaultImageYOffset = 0;
	
	var boolAlertCursorCoordsOnImage = true;
	//=============================
	
	//=============================================
	//private variables
	
	//=============================================
	
	//class constructor (dynamically loaded private properties etc.)
	var clsVirtualMap = function() {
			//property inits
			this.arr_intCursor = [];
			this.boolLeftMouseButtonDown = false;
			this.intViewportWidth = null;
			this.intViewportHeight = null;
			//holds current translation of top-left corner of image in virtualMap viewport
			this.intImageXOffset = null;
			this.intImageYOffset = null;
			this.objMapImage = null;
			this.objCurrentPointWrapper = null;
			
			//perform additional un-property-related initialisation
			this.funcInit();
		};
	//private methods and default properties
	clsVirtualMap.prototype = {
			//initialisation handler for housekeeping
			funcInit: function() {
				var objThis = this; //persist for closures
				
				//after full page load sequence
				window.onload = function() {
					objThis.funcAttachKeyboardHooks();
					objThis.funcAttachMouseHooks();
					objThis.funcAttachEnvironmentHooks();
					//update/load any as yet unhandled links->virtualMaps on page for first run
					objThis.funcUpdateMaps();
				};
			//looks for any new virtualMaps which have not been processed yet
			//... => (eg. recall after changing DOM)
			}, funcUpdateMaps: function() {
				//preserve root object ref for closures etc.
				var objRootObject = this;
				//get collection of links on page
				var colLinks = document.getElementsByTagName("a")
						, intLinkCount = colLinks.length						//precache (speed!)
						, intLink, objLink, objDOM_ViewerWrapper, objConfig;	//reserve memory
				//iterate through all links to look for new virtualMap placeholders to process
				for (intLink = 0 ; intLink < intLinkCount ; ++intLink) {
					//link object for iteration
					objLink = colLinks[intLink];
					//ignore if link already handled before or not intended as a virtualMap
					if (objLink.rel.substr(0, strLinkHook_RelValue.length) != strLinkHook_RelValue || objLink.boolVirtualMapLoaded) continue;
					
					//hide link; no longer needed but available for possible restoration/reference later
					objLink.style.display = "none";
					
					//================================
					//DOM subtree build for viewer - will be fully constructed offline
					//... => and then appended to DOM when complete for offscreen buffering
					objDOM_ViewerWrapper = document.createElement("div");
					objDOM_ViewerWrapper.className = "clsViewerWrapper";
					
					objDOM_MapContainer = document.createElement("div");
					objDOM_MapContainer.className = "clsMapContainer";
					objDOM_ViewerWrapper.appendChild(objDOM_MapContainer);
					
					objDOM_ViewerImage = this.objMapImage = document.createElement("img");
					objDOM_ViewerImage.className = "clsImage";
					//directly load map image from link
					objDOM_ViewerImage.src = objLink.href;
					objDOM_MapContainer.appendChild(objDOM_ViewerImage);
					//================================
					
					//load config from link
					//WARNING: insufficient sanitisation for JSON string here...
					eval("objConfig = " + objLink.rel.substr(strLinkHook_RelValue.length));
					//empty config if failed load from link data
					if (!objConfig) objConfig = {};
					
					//XML file for map configuration is specified
					if (objConfig.xml) {
						var objXMLHTTPRequest = null;
						//W3C standard method (FF, Opera, WebKit etc.)
						if (window.XMLHttpRequest) {
							objXMLHTTPRequest = new window.XMLHttpRequest();
						//IE < 7 method
						} else if (window.ActiveXObject) {
							objXMLHTTPRequest = new window.ActiveXObject("MSXML2.XMLHTTP");
						}
						//download XML configuration file
						try {
							objXMLHTTPRequest.open("GET", objConfig.xml, false);
							//always treat as XML to avoid problems with incorrect server headers etc.
							//... => (NB: check as IE does not support)
							if (objXMLHTTPRequest.overrideMimeType)
								objXMLHTTPRequest.overrideMimeType("text/xml");
							objXMLHTTPRequest.send(null);
						//error during download; flag to user
						} catch (objError) {
							objConfig.xml = null;
						}
						
						//error, don't attempt to process if XML did not load successfully
						if (!objConfig.xml || !objXMLHTTPRequest.responseXML) {
							alert("XML configuration file failed to download");
						//XML config loaded OK
						} else {
							//load XMLDOM response
							var objXMLDOM = objXMLHTTPRequest.responseXML;
							//invalid XML document (level 1 of validation)
							if (!objXMLDOM) {
								alert("XML configuration load failed - XML document is malformed");
							//valid document, XMLDOM loaded, continue
							} else {
								//use lastChild instead of firstChild as IE will use <xml> element as firstChild
								var objMapRoot = objXMLDOM.lastChild;
								//ensure root element is present (level 2 of validation)
								if (!objMapRoot || objMapRoot.nodeName !== "map") {
									alert("XML configuration load failed - <map> must be root element");
								//success; continue loading
								} else {
									//load map data (as needed - allows overrides per-implementation on page)
									if (!objConfig.width) objConfig.width = parseInt(objMapRoot.getAttribute("width"));
									if (!objConfig.height) objConfig.height = parseInt(objMapRoot.getAttribute("height"));
									if (!objConfig.startX) objConfig.startX = parseInt(objMapRoot.getAttribute("startx"));
									if (!objConfig.startY) objConfig.startY = parseInt(objMapRoot.getAttribute("starty"));
									if (!objConfig.globalMarkerColour) objConfig.globalMarkerColour = objMapRoot.getAttribute("globalmarkercolour");
									
									//load collection of points from XML config
									var colPoints = objMapRoot.getElementsByTagName("point")
											, intPointsLength = colPoints.length;	//cache (speed!)
									//iterate through all points on map
									for (var intPoint = 0 ; intPoint < intPointsLength ; ++intPoint) {
										var objPoint = colPoints[intPoint];
										var intLeft = objPoint.getAttribute("x"), intTop = objPoint.getAttribute("y")
												, intMarkerWidth = objPoint.getAttribute("markerwidth"), intMarkerHeight = objPoint.getAttribute("markerheight")
												, intBoxWidth = objPoint.getAttribute("boxwidth"), intBoxHeight = objPoint.getAttribute("boxheight");
										//when reading nodeValue, text inside node is a textNode, so use firstChild to ref it
										//... => EDIT: used to keep the textNode from XML, but Chrome etc. don't like appending nodes from other documents 
										//... => (probably needs adoptNode() but easier to recreate this once)
										var strPointCaption = objPoint.getElementsByTagName("caption")[0].firstChild.nodeValue;
										var strThumbnailImageSrc = objPoint.getElementsByTagName("thumbnail")[0].getAttribute("src");
										var strDescription = objPoint.getElementsByTagName("description")[0].firstChild.nodeValue;
										
										var objPointMarker = document.createElement("div");
										objPointMarker.className = "clsMarker";
										objPointMarker.style.left = intLeft + "px";
										objPointMarker.style.top = intTop + "px";
										objPointMarker.style.width = intMarkerWidth + "px";
										objPointMarker.style.height = intMarkerHeight + "px";
										//use globalMarkerColour if specified
										if (objConfig.globalMarkerColour)
											objPointMarker.style.backgroundColor = objConfig.globalMarkerColour;
										
										//use builtin mouse handler system to trap mouseDown for opening info pane
										objPointMarker.funcOVMSVirtualMap_DOM_onmousedown = function(objThis, objEvent, objTarget) {
												//disable drag
												if (objEvent.preventDefault) objEvent.preventDefault();
												return false;
											};
										
										//use builtin mouse handler system to trap mouseDown for opening info pane
										objPointMarker.funcOVMSVirtualMap_DOM_onclick = function(objThis, objEvent, objTarget) {
												//disable drag
												if (objEvent.preventDefault) objEvent.preventDefault();
												objThis.funcOpenPointWrapper(this.firstChild);
												return false;
											};
										
										//create wrapper to hold all elements with point's information
										var objPointWrapper = document.createElement("div");
										objPointWrapper.className = "clsPointWrapper";
										objPointWrapper.style.width = intBoxWidth + "px";
										objPointWrapper.style.height = intBoxHeight + "px";
										objPointMarker.appendChild(objPointWrapper);
										
										var objCloseButton = document.createElement("div");
										objCloseButton.className = "clsCloseButton";
										//hover effects for button
										objCloseButton.funcOVMSVirtualMap_DOM_onmouseover = function(objThis, objEvent, objTarget) {
												objTarget.style.backgroundPosition = "-13px 0px";
												return false;
											};
										objCloseButton.funcOVMSVirtualMap_DOM_onmouseout = function(objThis, objEvent, objTarget) {
												objTarget.style.backgroundPosition = "-0px 0px";
												return false;
											};
										objCloseButton.funcOVMSVirtualMap_DOM_onmousedown = function(objThis, objEvent, objTarget) {
												objTarget.style.backgroundPosition = "-26px 0px";
												return false;
											};
										//button will close pointWrapper
										objCloseButton.funcOVMSVirtualMap_DOM_onclick = function(objThis, objEvent, objTarget) {
												//close currently open pointWrapper (this one)
												objThis.funcCloseCurrentPointWrapper();
												return false;
											};
										objPointWrapper.appendChild(objCloseButton);
										
										var objPointThumbnailImage = document.createElement("img");
										objPointThumbnailImage.className = "clsThumbnailImage";
										//load imageSrc
										objPointThumbnailImage.src = strThumbnailImageSrc;
										objPointWrapper.appendChild(objPointThumbnailImage);
										
										var objPointCaption = document.createElement("div");
										objPointCaption.className = "clsCaption";
										//load point's aption
										objPointCaption.appendChild(document.createTextNode(strPointCaption));
										objPointWrapper.appendChild(objPointCaption);
										
										var objPointDescription = document.createElement("div");
										objPointDescription.className = "clsDescription";
										//load point's description
										objPointDescription.appendChild(document.createTextNode(strDescription));
										objPointWrapper.appendChild(objPointDescription);
										
										//append pointWrapper, and all its new children to DOM to load online
										objDOM_MapContainer.appendChild(objPointMarker);
									}
								}
							}
						}
					}
					
					//load defaults if not specified
					if (objConfig.width == null || isNaN(objConfig.width)) {
						objConfig.width = intDefaultViewportWidth;
					}
					if (objConfig.height == null || isNaN(objConfig.height)) {
						objConfig.height = intDefaultViewportHeight;
					}
					if (objConfig.startX == null || isNaN(objConfig.startX)) {
						objConfig.startX = intDefaultImageXOffset;
					}
					if (objConfig.startY == null || isNaN(objConfig.startY)) {
						objConfig.startY = intDefaultImageYOffset;
					}
					
					//update display with configuration
					objDOM_ViewerWrapper.style.width = (this.intViewportWidth = objConfig.width) + "px";
					objDOM_ViewerWrapper.style.height = (this.intViewportHeight = objConfig.height) + "px";
					objDOM_MapContainer.style.left = (this.intImageXOffset = objConfig.startX) + "px";
					objDOM_MapContainer.style.top = (this.intImageYOffset = objConfig.startY) + "px";
					
					objDOM_ViewerImage.funcOVMSVirtualMap_DOM_onmousedown = function(objThis, objEvent, objTarget) {
							//disable drag
							if (objEvent.preventDefault) objEvent.preventDefault();
							
							//display if specifed
							if (boolAlertCursorCoordsOnImage && objEvent.ctrlKey) {
								//=========================
								//get cursor coordinates relative to entire map image
								var intCursorX = objEvent.clientX - objThis.intImageXOffset
										, intCursorY = objEvent.clientY - objThis.intImageYOffset
										, objParent = objTarget, objCurrentStyle;
								do {
									objCurrentStyle = objTarget.currentStyle
											? objTarget.currentStyle
											: window.getComputedStyle(objTarget, '');
									intCursorX -= objParent.offsetLeft;
									intCursorY -= objParent.offsetTop;
									intCursorX -= parseInt(objCurrentStyle.borderLeftWidth) || 0;
									intCursorY -= parseInt(objCurrentStyle.borderTopWidth) || 0;
									objParent = objParent.offsetParent;
								} while (objParent && objParent.nodeName != "BODY")
								//=========================
								
								//display to user
								alert("X: " + intCursorX + "\nY: " + intCursorY);
							}
							return false;
						};
					
					objDOM_ViewerImage.funcOVMSVirtualMap_DOM_onmousemove = function(objThis, objEvent, objTarget, arr_intCursor) {
							//only process as image drag if mouse button is held down
							if (objThis.boolLeftMouseButtonDown) {
								//get deltas of mouse translation
								var intDeltaX = arr_intCursor[0] - objThis.arr_intCursor[0]
										, intDeltaY = arr_intCursor[1] - objThis.arr_intCursor[1];
								//(attempt to) translate image on screen by mouse delta; may be suppressed if at boundary
								objThis.funcMoveImage(objTarget, intDeltaX, intDeltaY);
							}
						};
					
					objDOM_ViewerImage.intOVMSVirtualMap_DOM_Width = objConfig.width;
					objDOM_ViewerImage.intOVMSVirtualMap_DOM_Height = objConfig.height;
					
					//insert virtualMap wrapper DOM element adjacent to original link element in document
					objLink.parentNode.insertBefore(objDOM_ViewerWrapper, objLink);
				}
			//ensures all keyboard hooks are attached correctly for component
			}, funcAttachKeyboardHooks: function(objDOMElement) {
				//attach to document if none specified
				objDOMElement = objDOMElement || document;
			//ensures all mouse hooks are attached correctly for component's DOM element
			}, funcAttachMouseHooks: function(objDOMElement) {
				//attach to document if none specified
				objDOMElement = objDOMElement || document;
				
				var objThis = this; //persist for closures
				
				objDOMElement.onmouseover = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						var objTarget = objEvent.target || objEvent.srcElement
								, boolEventHandlerResult = true
								//read new cursor position coords
								, arr_intCursor = [objEvent.clientX, objEvent.clientY];
						
						if (objTarget && objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type]) {
							boolEventHandlerResult = objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type](objThis, objEvent, objTarget, arr_intCursor);
						}
						
						//save last cursor position coords
						objThis.arr_intCursor[0] = arr_intCursor[0];
						objThis.arr_intCursor[1] = arr_intCursor[1];
						
						//as generated by any specific handlers etc.
						return boolEventHandlerResult;
					};
				objDOMElement.onmouseout = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						var objTarget = objEvent.target || objEvent.srcElement
								, boolEventHandlerResult = true
								//read new cursor position coords
								, arr_intCursor = [objEvent.clientX, objEvent.clientY];
						
						if (objTarget && objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type]) {
							boolEventHandlerResult = objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type](objThis, objEvent, objTarget, arr_intCursor);
						}
						
						//save last cursor position coords
						objThis.arr_intCursor[0] = arr_intCursor[0];
						objThis.arr_intCursor[1] = arr_intCursor[1];
						
						//as generated by any specific handlers etc.
						return boolEventHandlerResult;
					};
				objDOMElement.onmousedown = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						var objTarget = objEvent.target || objEvent.srcElement
								, boolEventHandlerResult = true
								//read new cursor position coords
								, arr_intCursor = [objEvent.clientX, objEvent.clientY];
						
						//set flag for key state
						objThis.boolLeftMouseButtonDown = true;
						
						if (objTarget && objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type]) {
							boolEventHandlerResult = objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type](objThis, objEvent, objTarget, arr_intCursor);
						}
						
						//save last cursor position coords
						objThis.arr_intCursor[0] = arr_intCursor[0];
						objThis.arr_intCursor[1] = arr_intCursor[1];
						
						//as generated by any specific handlers etc.
						return boolEventHandlerResult;
					};
				objDOMElement.onclick = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						var objTarget = objEvent.target || objEvent.srcElement
								, boolEventHandlerResult = true
								//read new cursor position coords
								, arr_intCursor = [objEvent.clientX, objEvent.clientY];
						
						if (objTarget && objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type]) {
							boolEventHandlerResult = objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type](objThis, objEvent, objTarget, arr_intCursor);
						}
						
						//save last cursor position coords
						objThis.arr_intCursor[0] = arr_intCursor[0];
						objThis.arr_intCursor[1] = arr_intCursor[1];
						
						//as generated by any specific handlers etc.
						return boolEventHandlerResult;
					};
				objDOMElement.onmouseup = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						//clear flag for key state
						objThis.boolLeftMouseButtonDown = false;
						
						return false;
					};
				objDOMElement.onmousemove = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						var objTarget = objEvent.target || objEvent.srcElement
								, boolEventHandlerResult = true
								//read new cursor position coords
								, arr_intCursor = [objEvent.clientX, objEvent.clientY];
						
						if (objTarget && objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type]) {
							boolEventHandlerResult = objTarget["funcOVMSVirtualMap_DOM_on" + objEvent.type](objThis, objEvent, objTarget, arr_intCursor);
						}
						
						//save last cursor position coords
						objThis.arr_intCursor[0] = arr_intCursor[0];
						objThis.arr_intCursor[1] = arr_intCursor[1];
						
						//as generated by any specific handlers etc.
						return boolEventHandlerResult;
					};
				//IE only
				objDOMElement.ondragstart = function(objEvent) {
						//x-browser event object load
						if (!objEvent) objEvent = window.event;
						objEvent.cancelBubble = true;
						return false;
					};
			//ensures all misc. environment hooks are attached correctly for component
			}, funcAttachEnvironmentHooks: function() {
				//none yet
			//moves the main map image by the specified cartesian coord deltas
			}, funcMoveImage: function(objDOMElement, intDeltaX, intDeltaY) {
				//update internally stored coordinates
				this.intImageXOffset += intDeltaX;
				this.intImageYOffset += intDeltaY;
				
				var intMinX = 0, intMinY = 0
					, intMaxX = -objDOMElement.offsetWidth + objDOMElement.intOVMSVirtualMap_DOM_Width
					, intMaxY = -objDOMElement.offsetHeight + objDOMElement.intOVMSVirtualMap_DOM_Height;
				
				//clamp to viewport boundaries; do not allow map image edges to move inside the viewport
				if (this.intImageXOffset > intMinX) this.intImageXOffset = intMinX;
				if (this.intImageYOffset > intMinY) this.intImageYOffset = intMinY;
				
				if (this.intImageXOffset < intMaxX) this.intImageXOffset = intMaxX;
				if (this.intImageYOffset < intMaxY) this.intImageYOffset = intMaxY;
				
				//update DOM position
				objDOMElement.parentNode.style.left = this.intImageXOffset + "px";
				objDOMElement.parentNode.style.top = this.intImageYOffset + "px";
			//animates an element's appearance by timed stepped resizing between parameters
			}, funcFlyOutAnimation: function(objObject, intTotalTime, intTotalSteps, intInitialWidth, intInitialHeight, intFullWidth, intFullHeight, boolHideOnFinish) {
				//already in animation, ignore this one
				if (objObject.boolInFlyOutAnimation) return;
				//set flag to prevent concurrent animations
				objObject.boolInFlyOutAnimation = true;
				
				//preserve root object ref for closures etc.
				var objRootObject = this;
				//initial dimensions of object before animation
				objObject.style.width = intInitialWidth + "px";
				objObject.style.height = intInitialHeight + "px";
				//hide all children of element during its animation
				this.funcHideChildren(objObject);
				//ensure element itself is visible during animation
				objObject.style.display = "block";
				
				var intInterval = intTotalTime / intTotalSteps
						, intCurrentWidth = intInitialWidth, intCurrentHeight = intInitialHeight;
				
				var intWidthDelta = (intFullWidth-intInitialWidth) / intTotalSteps
						, intHeightDelta = (intFullHeight-intInitialHeight) / intTotalSteps;
				
				var intWidthDeltaPositive = intWidthDelta > 0, intHeightDeltaPositive = intHeightDelta > 0;
				
				//start redraw timer for animation, storing timerObject for later cancelling
				var objTimer = window.setInterval(function() {
						//scale element by deltas
						intCurrentWidth += intWidthDelta;
						intCurrentHeight += intHeightDelta;
						//update size in DOM
						objObject.style.width = Math.round(intCurrentWidth) + "px";
						objObject.style.height = Math.round(intCurrentHeight) + "px";
						//stop animation when complete
						if (
							( (intWidthDeltaPositive && intCurrentWidth >= intFullWidth)
								|| (!intWidthDeltaPositive && intCurrentWidth <= intFullWidth))
							&& ( (intHeightDeltaPositive && intCurrentHeight >= intFullHeight)
								|| (!intHeightDeltaPositive && intCurrentHeight <= intFullHeight))
							) {
							//re-show all children of element after its animation
							objRootObject.funcShowChildren(objObject);
							//hide element when animation finishes if specified
							if (boolHideOnFinish) {
								objObject.style.display = "none";
								//back to full size in DOM (for future reads), as element is now hidden
								objObject.style.width = intInitialWidth + "px";
								objObject.style.height = intInitialHeight + "px";
								//back to normal zLayer position
								objObject.parentNode.style.zIndex = 1;
							}
							//stop timer as animation is now complete
							window.clearInterval(objTimer);
							//clear flag to reenable future animations
							objObject.boolInFlyOutAnimation = false;
						}
					}, intInterval);
			//animates an element's appearance by timed stepped moving between parameters
			}, funcFlyTranslateAnimation: function(objObject, intTotalTime, intTotalSteps, intInitialX, intInitialY, intFullX, intFullY) {
				//already in animation, ignore this one
				if (objObject.boolInFlyTranslateAnimation) return;
				//set flag to prevent concurrent animations
				objObject.boolInFlyTranslateAnimation = true;
				
				//preserve root object ref for closures etc.
				var objRootObject = this;
				//initial dimensions of object before animation
				objObject.style.left = intInitialX + "px";
				objObject.style.top = intInitialY + "px";
				
				var intInterval = intTotalTime / intTotalSteps
						, intCurrentX = intInitialX, intCurrentY = intInitialY;
				
				var intXDelta = (intFullX-intInitialX) / intTotalSteps
						, intYDelta = (intFullY-intInitialY) / intTotalSteps;
				
				var intXDeltaPositive = intXDelta > 0, intYDeltaPositive = intYDelta > 0;
				
				//start redraw timer for animation, storing timerObject for later cancelling
				var objTimer = window.setInterval(function() {
						//scale element by deltas
						intCurrentX += intXDelta;
						intCurrentY += intYDelta;
						//update size in DOM
						objObject.style.left = Math.round(intCurrentX) + "px";
						objObject.style.top = Math.round(intCurrentY) + "px";
						//stop animation when complete
						if (
							( (intXDeltaPositive && intCurrentX >= intFullX)
								|| (!intXDeltaPositive && intCurrentX <= intFullX))
							&& ( (intYDeltaPositive && intCurrentY >= intFullY)
								|| (!intYDeltaPositive && intCurrentY <= intFullY))
							) {
							//stop timer as animation is now complete
							window.clearInterval(objTimer);
							//clear flag to reenable future animations
							objObject.boolInFlyTranslateAnimation = false;
							
							//possibility of overshooting due to rounding error; force correct final position just in case
							objObject.style.left = intFullX + "px";
							objObject.style.top = intFullY + "px";
						}
					}, intInterval);
			//hides all child DOMNodes
			}, funcHideChildren: function(objObject) {
				var colChildren = objObject.childNodes, intChild = colChildren.length - 1;
				//early-out if none
				if (intChild == -1) return;
				//loop reversed for speed
				do {
					colChildren[intChild].style.visibility = "hidden";
				//post-decrement (zero'th wanted) and doubles as exit condition
				} while (intChild--);
			//hides all child DOMNodes
			}, funcShowChildren: function(objObject) {
				var colChildren = objObject.childNodes, intChild = colChildren.length - 1;
				//early-out if none
				if (intChild == -1) return;
				//loop reversed for speed
				do {
					colChildren[intChild].style.visibility = "visible";
				//post-decrement (zero'th wanted) and doubles as exit condition
				} while (intChild--);
			//opens a new pointWrapper (closing any current one)
			}, funcOpenPointWrapper: function(objSubPointWrapper) {
				//close the currently open pointWrapper first (will return false if cannot close, to ignore this one)
				if (!this.funcCloseCurrentPointWrapper()) return;
				this.objCurrentPointWrapper = objSubPointWrapper;
				//full-sized dimensions of element
				var intFullWidth = parseInt(objSubPointWrapper.style.width), intFullHeight = parseInt(objSubPointWrapper.style.height);
				var intInitialImageXOffset = this.intImageXOffset, intInitialImageYOffset = this.intImageYOffset;
				//===============
				//move map to have content box as close to center as possible
				var intMarkerX = parseInt(objSubPointWrapper.parentNode.style.left), intMarkerY = parseInt(objSubPointWrapper.parentNode.style.top);
				var intNewImageXOffset = -intMarkerX + ((this.intViewportWidth - intFullWidth) / 2)
						, intNewImageYOffset = -intMarkerY + ((this.intViewportHeight - intFullHeight) / 2);
				
				var intMinX = 0, intMinY = 0
					, intMaxX = -this.objMapImage.offsetWidth + this.objMapImage.intOVMSVirtualMap_DOM_Width
					, intMaxY = -this.objMapImage.offsetHeight + this.objMapImage.intOVMSVirtualMap_DOM_Height;
				
				//clamp to viewport boundaries; do not allow map image edges to move inside the viewport
				if (intNewImageXOffset > intMinX) intNewImageXOffset = intMinX;
				if (intNewImageYOffset > intMinY) intNewImageYOffset = intMinY;
				
				if (intNewImageXOffset < intMaxX) intNewImageXOffset = intMaxX;
				if (intNewImageYOffset < intMaxY) intNewImageYOffset = intMaxY;
				//===============
				
				if (!objSubPointWrapper.boolInFlyOutAnimation && !objSubPointWrapper.boolInFlyTranslateAnimation) {
					this.intImageXOffset = intNewImageXOffset;
					this.intImageYOffset = intNewImageYOffset;
					
					//bring zLayer position of pointWrapper to front
					objSubPointWrapper.parentNode.style.zIndex = 2;
					
					//animate object through stepped move
					this.funcFlyTranslateAnimation(this.objMapImage.parentNode, 600, 20
							, intInitialImageXOffset, intInitialImageYOffset, intNewImageXOffset, intNewImageYOffset);
					//animate object through stepped resize until full size
					this.funcFlyOutAnimation(objSubPointWrapper, 600, 20
							, 0, 0, intFullWidth, intFullHeight, false);
				}
			//closes the currently open pointWrapper
			}, funcCloseCurrentPointWrapper: function() {
				//early-out if no pointWrapper to close
				if (!this.objCurrentPointWrapper) return true;
				//fail; cannot close current pointWrapper as it is in animation
				if (this.objCurrentPointWrapper.boolInFlyOutAnimation) return false;
				
				//full-sized dimensions of element
				var intFullWidth = parseInt(this.objCurrentPointWrapper.style.width), intFullHeight = parseInt(this.objCurrentPointWrapper.style.height);
				//animate object through stepped resize until full size
				this.funcFlyOutAnimation(this.objCurrentPointWrapper, 600, 20
						, intFullWidth, intFullHeight, 0, 0, true);
				//clear reference as invalid, pointWrapper is now closed
				this.objCurrentPointWrapper = null;
				//flag success
				return true;
			}
		};
	
	//====================================
	//private functions/procedures
	
	
	//====================================
	
	
	
	//========================================
	//private classes
	
	
	//========================================
	
	//copy reference to global scope for use
	window.clsVirtualMap = clsVirtualMap;
})();	//scope encapsulator actuation

//create new globally accessible OVMS virtualMap processor object and set-up to handle onload
var objOVMSVirtualMap = new clsVirtualMap();
