// vim: sw=4:ts=4:nu:nospell
// vim: sw=4:ts=4:nu:nospell

/**
 * HyperCities earth Objects
 *
 * @author    Jay Tung
 * @copyright (c) 2008, by HyperCities Tech Team
 * @date      2009-01-30
 * @version   0.1
 *
 */

// create application namespace
HyperCities.earth = function() {

    var _id = "[HyperCities.earth] ";
    var _CAMERA_MIN_ALTITUDE = 400;	//the camera can only zoom to 400 meters, for KmlPoint
    
    //internal variables
    var _GEarth;
    var _maps            = new Array();
    var _proxy           = new Array();    
    var _collections     = [];	//the items for collection list rendering
	var _blackoutOverlay;
	var _blackoutPanel;
	var _enableSync = true;
    //the mapping between altitude and zoom level
    var _altZoomMTable = [52966030, 26482177, 13240250, 6619287, 3309786, 1654545,
    								857483, 377255, 157225, 78108, 39998, 21556, 11816, 6962,
    								4534, 3322, 2716];

    //event handler
	var _zoomendHandlers = [];
	var _dragendHandlers = [];

	//utility variables
   	var syncQueue;	//for performance issue, add sync event in queue, and clear queue before any new sync event added into queue
	var syncDelay = 1500;	//the delay between sync event generated and sync function executes


	//return the zoom level according to altitude
	var _getZoomLevel = function($alt) {
		if ($alt >= _altZoomMTable[0])
			return 0;
		if ($alt < _altZoomMTable[_altZoomMTable.length-1])
			return _altZoomMTable.length;

		for (var i =0; i <= _altZoomMTable.length-2 ; i++)
		{
			if ($alt < _altZoomMTable[i] && $alt >= _altZoomMTable[i+1] )
			{
				return i+1;
			}
		}
		return 0;
	};

	var _formatFloat = function($src, $pos)
	{
        return Math.round($src*Math.pow(10, $pos))/Math.pow(10, $pos);
	};

	//detect if the zoom changed
	var _zoomChange = function($cam1, $cam2) {

		var lat1 = _formatFloat($cam1.getLatitude(), 6);
		var lng1 = _formatFloat($cam1.getLongitude(), 6);
		var alt1 = Math.round($cam1.getAltitude());
		var heading1 = $cam1.getHeading();
		var tilt1 = $cam1.getTilt();
		var roll1 = $cam1.getRoll();

		var lat2 = _formatFloat($cam2.getLatitude(), 6);
		var lng2 = _formatFloat($cam2.getLongitude(), 6);
		var alt2 = Math.round($cam2.getAltitude());
		var heading2 = $cam2.getHeading();
		var tilt2 = $cam2.getTilt();
		var roll2 = $cam2.getRoll();

		//HyperCities.debug(_id + "alt2 = "+alt2);

		var oldZoom = _getZoomLevel(alt1);
		var newZoom = _getZoomLevel(alt2);
		HyperCities.debug(_id + "oldZoom = "+oldZoom);
		HyperCities.debug(_id + "newZoom = "+newZoom);

		if ( oldZoom != newZoom )
			return true;
		else
			return false;
	};

	//store the zoom handler
	var _zoomendHandler = function($oldLevel, $newLevel) {
		var cam = _GEarth.getView().copyAsCamera(_GEarth.ALTITUDE_ABSOLUTE);
		var lookAt = _GEarth.getView().copyAsLookAt(_GEarth.ALTITUDE_ABSOLUTE);

		//the region can see depends on lookAt, deal with tilt problem
		var lat = lookAt.getLatitude();
		var lng = lookAt.getLongitude();
		//zoom level deponds on camera altitude, lookAt's altitude is always 0
		var zoom = _getZoomLevel(lookAt.getRange());

		if ( $oldLevel < HyperCities.config.ZOOM_THRESHOLD && $newLevel >= HyperCities.config.ZOOM_THRESHOLD) {
		    HyperCities.mainMap.removeCities();
		}
		if ( $oldLevel >= HyperCities.config.ZOOM_THRESHOLD+1 && $newLevel < HyperCities.config.ZOOM_THRESHOLD+1) {
		    HyperCities.mainMap.addCities(HyperCities.city.getCities());
		    $.each(HyperCities.session.get("map"), function () {
		            HyperCities.session.removeMap(this);
		        });
		}

		if (_enableSync)
		{
			clearTimeout(syncQueue);
			syncQueue = setTimeout("HyperCities.syncSession();", syncDelay);			
		}
	};

	//store the dragend handler
	var _dragendHandler = function() {
		if (_enableSync)
		{
			clearTimeout(syncQueue);
			syncQueue = setTimeout("HyperCities.syncSession();", syncDelay);	
		}
	};

	//configurable listener to fire an event based on movement of the Google Earth API Map
	var _moveendHandler = function() {

	};

	var _initEarthPanel = function() {

		$("#earthAutoSync").click(function() {
			if ($("#earthAutoSync").attr('checked'))
			{
				HyperCities.debug(_id + "enable Earth autoSync");
				_enableSync = true;
				//HyperCities.earth.addEventListener(_GEarth.getGlobe(), 'dragend', _dragendHandler);
				//HyperCities.earth.addEventListener(_GEarth.getWindow(), 'zoomend', _zoomendHandler);
			}
			else
			{
				HyperCities.debug(_id + "disable Earth autoSync");
				_enableSync = false;
				//HyperCities.earth.removeEventListener(_GEarth.getGlobe(), "dragend");
				//HyperCities.earth.removeEventListener(_GEarth.getWindow(), "zoomend");
			}
		});
		$("#buildings").click(function() {
			if ($("#buildings").attr('checked'))
			{
				HyperCities.debug(_id + "enable buildings");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_BUILDINGS, 1);
			}
			else
			{
				HyperCities.debug(_id + "disable buildings");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_BUILDINGS, 0);
			}
		});
		$("#terrain").click(function() {
			if ($("#terrain").attr('checked'))
			{
				HyperCities.debug(_id + "enable terrain");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_TERRAIN, 1);
			}
			else
			{
				HyperCities.debug(_id + "disable terrain");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_TERRAIN, 0);
			}
		});
		$("#roads").click(function() {
			if ($("#roads").attr('checked'))
			{
				HyperCities.debug(_id + "enable roads");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_ROADS, 1);
			}
			else
			{
				HyperCities.debug(_id + "disable roads");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_ROADS, 0);
			}
		});
		$("#borders").click(function() {
			if ($("#borders").attr('checked'))
			{
				HyperCities.debug(_id + "enable borders");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_BORDERS, 1);
			}
			else
			{
				HyperCities.debug(_id + "disable borders");
				_GEarth.getLayerRoot().enableLayerById(_GEarth.LAYER_BORDERS, 0);
			}
		});
	};

	var _initSystemOptions = function()
	{
		_GEarth.getOptions().setMouseNavigationEnabled(true);
		_GEarth.getOptions().setStatusBarVisibility(true);
		//_GEarth.getOptions().setOverviewMapVisibility(true);
		_GEarth.getOptions().setScaleLegendVisibility(true);
		//_GEarth.getNavigationControl().getScreenXY().setXUnits(_GEarth.UNITS_INSET_PIXELS);
		//_GEarth.getNavigationControl().getScreenXY().setYUnits(_GEarth.UNITS_INSET_PIXELS);
	};

	var _nullHandler = function($event) {
        $event.preventDefault();
        $event.stopPropagation();
    };

	//blackout the earth, remove all event handler,
	var _disableScreen = function()
	{
		_blackoutOverlay = _GEarth.createScreenOverlay('');
		_blackoutOverlay.getScreenXY().set(0, _GEarth.UNITS_PIXELS,
		                            0, _GEarth.UNITS_PIXELS);
		_blackoutOverlay.getOverlayXY().set(0, _GEarth.UNITS_PIXELS,
		                             0, _GEarth.UNITS_PIXELS);
		_blackoutOverlay.getSize().set(1, _GEarth.UNITS_FRACTION,
		                        1, _GEarth.UNITS_FRACTION);
		_blackoutOverlay.getColor().set("cc000000");
		_GEarth.getFeatures().appendChild(_blackoutOverlay);

		google.earth.addEventListener(_GEarth.getWindow(),"click", _nullHandler, true);
		google.earth.addEventListener(_GEarth.getWindow(),"dblclick", _nullHandler, true);
		google.earth.addEventListener(_GEarth.getWindow(),"mouseover", _nullHandler, true);
		google.earth.addEventListener(_GEarth.getWindow(),"mousedown", _nullHandler, true);
		google.earth.addEventListener(_GEarth.getWindow(),"mouseup", _nullHandler, true);
		google.earth.addEventListener(_GEarth.getWindow(),"mouseout", _nullHandler, true);
		google.earth.addEventListener(_GEarth.getWindow(),"mousemove", _nullHandler, true);

		_GEarth.getNavigationControl().setVisibility(_GEarth.VISIBILITY_HIDE);
	};

	//show earth, restore all event handler
	var _enableScreen = function()
	{
		_GEarth.getFeatures().removeChild(_blackoutOverlay);
		_blackoutOverlay = null;

		google.earth.removeEventListener(_GEarth.getWindow(),"click", _nullHandler, true);
		google.earth.removeEventListener(_GEarth.getWindow(),"dblclick", _nullHandler, true);
		google.earth.removeEventListener(_GEarth.getWindow(),"mouseover", _nullHandler, true);
		google.earth.removeEventListener(_GEarth.getWindow(),"mousedown", _nullHandler, true);
		google.earth.removeEventListener(_GEarth.getWindow(),"mouseup", _nullHandler, true);
		google.earth.removeEventListener(_GEarth.getWindow(),"mouseout", _nullHandler, true);
		google.earth.removeEventListener(_GEarth.getWindow(),"mousemove", _nullHandler, true);

		_GEarth.getNavigationControl().setVisibility(_GEarth.VISIBILITY_SHOW);
	};

	//get earth plugin and api version
	var _getSystemVersion = function() {
		HyperCities.debug(_id + "Google earth plugin version: " + _GEarth.getPluginVersion());
		HyperCities.debug(_id + "Google earth version: " + _GEarth.getEarthVersion());
		HyperCities.debug(_id + "Google earth API version: " + _GEarth.getApiVersion());
    };

	return {

		//each puclic function should return immediately if _GEarth is null
		
		//return true is addEventListener success, false if it failed
		addEventListener: function($target, $eventName, $eventHandler)
		{
			if (_GEarth == null)
				return false;

			if ($eventName === "dragend")
			{
				var oldMouseX;
				var oldMouseY;
				var newMouseX;
				var newMouseY;

				var func1 = function($event) {
												oldMouseX = $event.getScreenX();
												oldMouseY = $event.getScreenY();
				}

				var func2 = function($event) {
												newMouseX = $event.getScreenX();
												newMouseY = $event.getScreenY();
												//HyperCities.debug(_id + "Old mouse location = (" + oldMouseX + ", " + oldMouseY + ")");
												//HyperCities.debug(_id + "New mouse location = (" + newMouseX + ", " + newMouseY + ")");
												if ( (typeof(oldMouseX) !== 'undefined') &&
													 (typeof(oldMouseY) !== 'undefined') &&
													 (typeof(newMouseX) !== 'undefined') &&
													 (typeof(newMouseY) !== 'undefined') &&
													 oldMouseX != 0 && oldMouseY != 0 &&
													 newMouseX != 0 && newMouseY != 0 &&
													!((oldMouseX == newMouseX) && (oldMouseY == newMouseY)) )
												{
													HyperCities.debug(_id + "Dragend event occurs.");
													$eventHandler();
												}
				}

				//only add handlers when _dragendHandlers is empty
				if (_dragendHandlers.length == 0)
				{
					HyperCities.debug(_id + "Add dragend event listener.");
					google.earth.addEventListener($target, 'mousedown', func1);
					google.earth.addEventListener($target, 'mouseup', func2);
					_dragendHandlers.push(func1);
					_dragendHandlers.push(func2);
				}
			}
			else if ($eventName === "zoomend")
			{
				var oldCam;
				var newCam;

				var func1 = function($event) {
									oldCam = _GEarth.getView().copyAsCamera(_GEarth.ALTITUDE_ABSOLUTE);
									};
				var func2 = function($event) {
									newCam = _GEarth.getView().copyAsCamera(_GEarth.ALTITUDE_ABSOLUTE);
									if (_zoomChange(oldCam, newCam))
									{
										var oldLevel = _getZoomLevel(oldCam.getAltitude());
										var newLevel = _getZoomLevel(newCam.getAltitude());
										oldCam = newCam;
										HyperCities.debug(_id + "Zoomend event occurs.");
										$eventHandler(oldLevel, newLevel);
									}
								};

				//only add handlers when _zoomendHandlers is empty
				if (_zoomendHandlers.length == 0)
				{
					HyperCities.debug(_id + "Add zoomend event listener.");
					google.earth.addEventListener($target, 'mousedown', func1);
					google.earth.addEventListener($target, 'mouseup', func2);
					_zoomendHandlers.push(func1);
					_zoomendHandlers.push(func2);
				}
			}
			else if ($eventName === "moveend")
			{
				//TODO
			}
			else
			{
				google.earth.addEventListener($target, $eventName, $eventHandler);
			}

			return true;
		},

		//return true is removeEventListener success, false if it failed
		removeEventListener: function($target, $eventName)
		{
			if (_GEarth == null)
				return false;
				
			if ($eventName == "dragend")
			{
				HyperCities.debug(_id + "Remove dragend listener");
				var mouseup = _dragendHandlers.pop();
				var mousedown = _dragendHandlers.pop();
				google.earth.removeEventListener($target, "mouseup", mouseup);
				google.earth.removeEventListener($target, "mousedown", mousedown);
				mouseup = null;
				mousedown = null;
			}
			else if ($eventName == "zoomend")
			{
				HyperCities.debug(_id + "Remove zoomend listener");
				var mouseup = _zoomendHandlers.pop();
				var mousedown = _zoomendHandlers.pop();
				google.earth.removeEventListener($target, "mouseup", mouseup);
				google.earth.removeEventListener($target, "mousedown", mousedown);
				mouseup = null;
				mousedown = null;
			}
			
			return false;
		},

		//initial earth instance
		getEarthInstanceCB: function($object) {
			if ($object!=null)
			{
				HyperCities.debug(_id + "Create earth instance.");

				// You can now manipulate ge using the full Google Earth API.
				_GEarth = $object;
				//_getSystemVersion();

				//initialize
				_initSystemOptions();
				_initEarthPanel();

				//add event listener
				HyperCities.earth.addEventListener(_GEarth.getGlobe(), 'dragend', _dragendHandler);
				HyperCities.earth.addEventListener(_GEarth.getWindow(), 'zoomend', _zoomendHandler);
			}
			else
				HyperCities.debug(_id + "Create earth instance failed.");
		},

		//kml related functions
		//todo: add 2nd parameter callback function
		fetchKml: function($kmlUrl, $callback)
		{
			if (_GEarth == null)
				return;

			HyperCities.debug(_id + "Fetch KML URL: "+$kmlUrl);

			if (typeof($callback) !== 'undefined' && $callback!= null)
			{
				google.earth.fetchKml(_GEarth, $kmlUrl, $callback);
			}
			else
				google.earth.fetchKml(_GEarth, $kmlUrl, function($kmlObj){
					HyperCities.earth.kmlFinishedLoading(_GEarth, $kmlObj);
				});
			
		},

		kmlFinishedLoading: function($plugin, $kmlObject)
		{
			if (_GEarth == null)
				return;

			if ($kmlObject) {
				//$plugin.getFeatures().appendChild($kmlObject);
				//HyperCities.earth.traverseKml($kmlObject, HyperCities.earth.parseNode);
				_collections.push($kmlObject);
				HyperCities.debug("_collections.length="+_collections.length);
			}
			else
				HyperCities.debug("Fetch KML error.");
		},

		netLinkFinishedLoading: function($plugin, $kmlObject)
		{
			if (_GEarth == null)
				return;

			//HyperCities.debug(_id + "$kmlObject="+$kmlObject);
			if ($kmlObject) {
				//this line has problem
				_collections.push($kmlObject);
			}
		},
		
		traverseKml: function($node, $func) {
			if (_GEarth == null)
				return;


			var type = $node.getType();
			//HyperCities.debug(_id + $node.getType() + "; " + $node.getName());
			$func($node);

			//only KmlDocument and KmlFolder have children
			if (type === 'KmlDocument' || type === 'KmlFolder')
			{
				if($node.getFeatures().hasChildNodes())
				{
					var subNodes = $node.getFeatures().getChildNodes();
					var length = subNodes.getLength();
					for(var i = 0; i < length; i++)
					{
						var eachSubNode = subNodes.item(i);
						HyperCities.earth.traverseKml(eachSubNode, $func);
					}
				}
				else
					HyperCities.debug(_id + "This node has no children.");
			}
		},

		parseNode: function($node)
		{
			if (_GEarth == null)
				return;

			if ($node.getType() == 'KmlDocument' || $node.getType() == 'KmlPlacemark')
			{
				var nodeObj = { id: _collections.length,	//use array index as id
								name: $node.getName(),
								type: $node.getType(),
								description: $node.getDescription(),
								obj: $node,
								//boundary: a latLonAltBox
								boundary: HyperCities.earth.getFeatureExtent(_GEarth, $node)
								};

				google.earth.addEventListener($node, 'click', function($event){
					$event.preventDefault();
					if ($event.getTarget() == this)
						HyperCities.earth.clickMarkerHandler(nodeObj);
				});
				_collections.push(nodeObj);
			}
			else if ($node.getType() == 'KmlNetworkLink')
			{
				var url = ($node.getLink()).getHref();
				//HyperCities.debug(url);

				HyperCities.earth.fetchKml(url, function(kmlObj)
				{
					HyperCities.earth.netLinkFinishedLoading(_GEarth, kmlObj);
				});
			}
		},
		
		appendKmlObject: function($kmlObject)
		{
			if (_GEarth == null)
				return;

			var hasObj = false;
			var kmlObj = null;
			
			for (var i=0;i<_collections.length; i++)
			{
				if ($kmlObject.id == _collections[i].id)
				{
					hasObj = true;
					kmlObj = _collections[i];
					break;
				}
			}
			
			if (hasObj)
			{
				if (!HyperCities.earth.hasKmlObject(kmlObj.obj))
					_GEarth.getFeatures().appendChild(kmlObj.obj);
				
				return kmlObj;
			}
			else
			{
				if (!HyperCities.earth.hasKmlObject($kmlObject.obj))
					_GEarth.getFeatures().appendChild($kmlObject.obj);
				
				_collections.push($kmlObject);
				return $kmlObject;
			}
		},
		
		removeKmlObject: function($kmlObject)
		{
			if (_GEarth == null)
				return;

			for (var i=0; i<_collections.length; i++)
			{
				if ($kmlObject.id == _collections[i].id)
				{
					if (HyperCities.earth.hasKmlObject(_collections[i].obj))
						_GEarth.getFeatures().removeChild(_collections[i].obj);
					_collections.slice(i);
				}
			}
		},

		emptyCollections: function()
		{
			if (_GEarth == null)
				return;

			var kmlObjects = HyperCities.earth.getCollections();
			HyperCities.debug(_id + "Remove "+ kmlObjects.length + " objects.");
			
			for (var i=0; i<kmlObjects.length; i++)
			{
				HyperCities.earth.removeKmlObject(kmlObjects[i]);
			}
			kmlObjects = [];
		},

		clickMarkerHandler: function($nodeObj)
		{
			if (_GEarth == null)
				return;

			var balloon = HyperCities.earth.getEarth().createHtmlDivBalloon('');
			balloon.setMinWidth(200);
			balloon.setCloseButtonEnabled(true);

			var div = document.createElement('DIV');
			var html = "<p align=center><a href=# id=prev>Prev</a>&nbsp&nbsp<a href=# id=next>Next</a></p>" +
						'<strong>' + $nodeObj.name + '</strong><br>' + $nodeObj.description;
			div.innerHTML = html;
			balloon.setContentDiv(div);
			balloon.setFeature($nodeObj.obj);

			var index = $nodeObj.id;
			$("#prev", div).click(function(){
				var prev = Math.max(0, index-1);
				HyperCities.earth.clickMarkerHandler(_collections[prev]);
			});

			$("#next", div).click(function(){
				var next = Math.min(_collections.length-1, index+1);
				HyperCities.earth.clickMarkerHandler(_collections[next]);
			});

			$("#intelliList .highlight").removeClass('highlight');
			var item = $("#intelliItem_"+index, "#intelliList").addClass('highlight');
			HyperCities.debug(_id + "item.id="+item.attr("id"));
			if ($("#intelliList .intelliItem").length > 1)
				$("#intelliList")[0].scrollTo("#intelliItem_"+index);

			var earth = HyperCities.earth.getEarth();
			HyperCities.earth.zoomToFeature2(earth, $nodeObj.obj, $nodeObj.boundary);
			earth.setBalloon(balloon);
		},
		
		//check if earth has this kml object
		//note this function only check first level children
		hasKmlObject: function($kmlObject)
		{
			if (_GEarth == null)
				return false;

			if (_GEarth.getFeatures().hasChildNodes())
			{
				var childNodes = _GEarth.getFeatures().getChildNodes();
				for (var i=0; i<childNodes.getLength(); i++)
				{
					if ($kmlObject == childNodes.item(i))	
						return true;
				}
			}
			return false;
		},

		//add map on earth
 		addMap: function($map) {
 			if (_GEarth == null)
				return;
			
			var mapId = $map.id;
			var mapUrl = $map.tileUrl;
			
			if (mapUrl.charAt(mapUrl.length-1) !== "/")
				mapUrl = mapUrl + "/doc.kml";
			else
				mapUrl = mapUrl + "doc.kml";
				
			//check the tile URL to see if it has doc.kml
			//only http:\/\/tiles.ats.ucla.edu has doc.kml
			var regExp = new RegExp(/http:\/\/tiles.ats.ucla.edu\S*/g);
			if (!mapUrl.match(regExp))
			{
				//alert("This map will not show on earth mode because it is in old format.");
				HyperCities.debug(_id + "This map will not show on earth mode because it is in old format.");
				return;
			}

			HyperCities.debug(_id + " Add map " + mapId);
			//HyperCities.debug(_id + "Map URL=" + mapUrl);
			//add earth super overlay
			var link = _GEarth.createLink("");
			link.setHref(mapUrl);
			var networkLink = _GEarth.createNetworkLink("");
			networkLink.setName("Map"+mapId);
			//networkLink.setFlyToView(true);
			networkLink.setLink(link);

			if (typeof(_maps[mapId]) === 'undefined' || _maps[mapId] == null)
			{
				//HyperCities.debug(_id + "New map");
				_GEarth.getFeatures().appendChild(networkLink);
				_maps[mapId] = networkLink;
			}
			else
			{

 			}
		},

		//remove map on earth
		removeMap: function($mapId) {
			if (_GEarth == null)
				return;

			HyperCities.debug(_id + " Remove map " + $mapId);
			if (typeof(_maps[$mapId]) !== "undefined" && _maps[$mapId] !== null)
			{
				_GEarth.getFeatures().removeChild(_maps[$mapId]);
				_maps[$mapId] = null;
				return true;
			}
		},

		//add map polygon when cursor hover over intellist
		addMapsProxy: function($mapId, $proxy) {
			if (_GEarth == null)
				return;
			if (typeof(_proxy[$mapId]) === 'undefined' || _proxy[$mapId] == null)
			{
				//fisrt time add this proxy, create a new polygon
				var polygon = this.createPolygon($proxy);
				//add polygon into both _maps and earth
				this.addPolygon($mapId, polygon);
			}
			else
			{
				HyperCities.debug(_id + "Polygon exists.");
				//the polygon already exist, set visibility to true
				polygon = _proxy[$mapId];
				polygon.setVisibility(true);
			}
		},

		//create map polygon
		createPolygon: function($proxy) {
			if (_GEarth == null)
				return;
			HyperCities.debug(_id + "Create polygon.");

			var nBound = $proxy.getBounds().getNorthEast().lat();
			var sBound = $proxy.getBounds().getSouthWest().lat();
			var eBound = $proxy.getBounds().getNorthEast().lng();
			var wBound = $proxy.getBounds().getSouthWest().lng();
			var polyColor = '330000ff';
			var isFill = true;
			var isOutline = true;
			var lineColor = 'ff003ff3';
			var lineWidth = 1;

			var polygonPlacemark = _GEarth.createPlacemark('');
			var polygon = _GEarth.createPolygon('');
			polygonPlacemark.setGeometry(polygon);
			var outer = _GEarth.createLinearRing('');
			polygon.setOuterBoundary(outer);

			// If polygonPlacemark doesn't already have a Style associated
			// with it, we create it now.
			if (!polygonPlacemark.getStyleSelector()) {
			  polygonPlacemark.setStyleSelector(_GEarth.createStyle(''));
			}
			var polyStyle = polygonPlacemark.getStyleSelector().getPolyStyle();
			polyStyle.getColor().set(polyColor);
			polyStyle.setFill(isFill);
			polyStyle.setOutline(isOutline);
			//polygon.setExtrude(1);

			var lineStyle = polygonPlacemark.getStyleSelector().getLineStyle();
			lineStyle.getColor().set(lineColor);
			lineStyle.setWidth(lineWidth);

			// Square outer boundary.
			var coords = outer.getCoordinates();
			coords.pushLatLngAlt(nBound, wBound, 0);
			coords.pushLatLngAlt(nBound, eBound, 0);
			coords.pushLatLngAlt(sBound, eBound, 0);
			coords.pushLatLngAlt(sBound, wBound, 0);

			return polygonPlacemark;
		},

		//add map polygon on earth
		addPolygon: function($mapId, $polygon) {
			if (_GEarth == null)
				return;
			HyperCities.debug(_id + "Add polygon " + $mapId);
			//save the polygon in _maps and earth
			_proxy[$mapId] = $polygon;
			_GEarth.getFeatures().appendChild($polygon);
		},

		//remove map polygon from earth
		removePolygon: function($mapId) {
			if (_GEarth == null)
				return;
			//remove polygon by set visibility to false
			HyperCities.debug(_id + "Remove polygon " + $mapId);
			_proxy[$mapId].setVisibility(false);
			//_GEarth.getFeatures().removeChild(_maps[$mapId].proxy);
		},

		//$container: map DOM object
		//$mapInfoDiv: map info panel
		blackoutScreen: function($container, $mapInfoDiv) {
			if (_GEarth == null)
				return;
			
			HyperCities.debug(_id + "blackoutScreen");
			
			_disableScreen();

			var top = $mapInfoDiv.position().top-1;
			var left = $mapInfoDiv.position().left-2;
			var width = $mapInfoDiv.width()+12;
			var height = $mapInfoDiv.height()+12;

			//create iframe to show map info window
			//only iframe can show DOM object on top of earth
			var iframe = $(document.createElement('iframe'));
			iframe.attr('id','blackoutIframe');
			//iframe.attr("src", "javascript:false");
			iframe.css("frameborder", 0);
			iframe.css("position", "absolute");
			iframe.css("top", top);
			iframe.css("left", left);
			iframe.css("height", height-2);
			iframe.css("width", width-2);
			iframe.css("z-index", 999);

			//
			_blackoutPanel = $(document.createElement('div'));
			_blackoutPanel.attr("id", "blackoutPanel");
			_blackoutPanel.css("position", "absolute");
			_blackoutPanel.css("top", top);
			_blackoutPanel.css("left", left);
			_blackoutPanel.css("height", height);
			_blackoutPanel.css("width", width);
			_blackoutPanel.css("opacity", 1);
			_blackoutPanel.css("background", "#000");
			_blackoutPanel.css("z-index", 1000);

			//append iframe first
			$container.append(iframe);
			//then append _blackoutPanel
			$container.append(_blackoutPanel);
			_blackoutPanel.show();
			//_blackoutPanel.fadeIn("fast");
		},

		//remove blackout from earth
		removeBlackout: function() {
			if (_GEarth == null)
				return;
				
			HyperCities.debug(_id + "removeBlackout");
            //$_blackoutPanel.fadeOut("fast");
            //$("#blackoutPanel").hide();
            _blackoutPanel.remove();
			$("#blackoutIframe").remove();
			_enableScreen();
		},

        //earth utility functions
        
        //return: 
        getLookAt: function()
        {
			if (_GEarth == null)
				return null;

        	return _GEarth.getView().copyAsLookAt(_GEarth.ALTITUDE_ABSOLUTE);
        },
        
        //
        //return: void
        setView: function($type, $view)
        {
        	if (_GEarth == null)
				return;
			
			HyperCities.debug(_id + "Set earth's view");
			
			if ($type == "GLatLng")
			{
				var lookAt = _GEarth.getView().copyAsLookAt(_GEarth.ALTITUDE_ABSOLUTE);
				lookAt.setLatitude($view.lat());
				lookAt.setLongitude($view.lng());
				_GEarth.getView().setAbstractView(lookAt);
			}
			else if ($type == "KmlLookAt")
			{
				_GEarth.getView().setAbstractView($view);
			}
        }, 
        
        getZoom: function()
        {
        	if (_GEarth == null)
				return;
				
			var camera = _GEarth.getView().copyAsCamera(_GEarth.ALTITUDE_ABSOLUTE);
			var zoom = _getZoomLevel(camera.getAltitude());
			return zoom;
        },

        getBounds: function()
        {
        	if (_GEarth == null)
				return;

			var camera = _GEarth.getView().copyAsCamera(_GEarth.ALTITUDE_ABSOLUTE);
			var lookAt = _GEarth.getView().copyAsLookAt(_GEarth.ALTITUDE_ABSOLUTE);
			//var kmlLatLonBox = _GEarth.getView().getViewportGlobeBounds();
			//var north = kmlLatLonBox.getNorth();
			//var south = kmlLatLonBox.getSouth();
			//var east = kmlLatLonBox.getEast();
			//var west = kmlLatLonBox.getWest();
			//var latLonBox = new GLatLngBounds(new GLatLng(south,west), new GLatLng(north,east));
			//
			var latLonBox = HyperCities.earth.createLatLonBoxFromCamera(_GEarth, lookAt);

			return latLonBox;
        },

        inView: function($plugin, $kmlObj)
        {
        	var lookAt = $plugin.getView().copyAsLookAt($plugin.ALTITUDE_ABSOLUTE);
			var viewBounds = HyperCities.earth.createLatLonBoxFromCamera($plugin, lookAt);

			var obj3DBounds = HyperCities.earth.getFeatureExtent($plugin, $kmlObj);
			var sw = new GLatLng(obj3DBounds.getSouth(), obj3DBounds.getWest());
			var ne = new GLatLng(obj3DBounds.getNorth(), obj3DBounds.getEast());
			var objBounds = new GLatLngBounds(sw, ne);

			//HyperCities.debug("objBounds="+objBounds);
			//HyperCities.debug("viewBounds="+viewBounds);
			if (viewBounds.intersects(objBounds) || viewBounds.containsBounds(objBounds))
				return true;
			else
				return false;
        },

		inView2: function($plugin, $bounds)
        {
        	var lookAt = $plugin.getView().copyAsLookAt($plugin.ALTITUDE_ABSOLUTE);
			var viewBounds = HyperCities.earth.createLatLonBoxFromCamera($plugin, lookAt);

			if (viewBounds.intersects($bounds) || viewBounds.containsBounds($bounds))
				return true;
			else
				return false;
        },

		latLonAltBox2LatLonBox: function($latLonAltBox)
		{
			var sw = new GLatLng($latLonAltBox.getSouth(), $latLonAltBox.getWest());
			var ne = new GLatLng($latLonAltBox.getNorth(), $latLonAltBox.getEast());
			var latLonBox = new GLatLngBounds(sw, ne);

			return latLonBox;
		},

		latLonBox2LatLonAltBox: function($plugin, $latLonBox)
		{
			var sw = $latLonBox.getSouthWest();
			var ne = $latLonBox.getNorthEast();

			//to do
			var latLonAltBox = $plugin.createLatLonAltBox('');

			return latLonAltBox;
		},

		/**
		 * Function to create a camera viewing the extents of a rectangle.
		 *
		 * The centre is easy to calculate (just add each side and divide by 2), the
		 * hard part is the altitude...
		 *
		 * Problem:
		 *
		 *          C
		 *         /|\
		 *        / | \
		 *       /  |  \
		 *      / F |a  \
		 *     /    |    \
		 *    / __--|--__ \
		 *   /_-    |    -_\
		 *  //      |   x  \\
		 * P-------- --------Q
		 *  \       |       /
		 *   |      |      |
		 *   \      |     /
		 *    |     |    |
		 *     \    | A  /
		 *      |   |   | r
		 *       \  |  /
		 *        | | |
		 *        \ | /
		 *         |||
		 *          |
		 *          E
		 *
		 * C = Camera Position
		 *
		 * E = Center of the earth
		 *
		 * P = First point on the long side of the rectangle we're attempting to view.
		 *
		 * Q = Second point on the long side of the rectangle we're attempting to view.
		 *
		 * F = Field of view of google earth camera (35 degrees i think?). This is the
		 *     angle between the lines CE and CP (or CE and CQ).
		 *
		 * r = Radius of the earth (about 6378700 meters)
		 *
		 * x = Half the length of the chord between P and Q
		 *
		 * a = Distance from the chord between P and Q to the Camera position (this is
		 *     approximately the altitude)
		 *
		 * A = The angle between the lines EP and EQ.
		 *
		 * We want to find "a". Note that "a" is only an approximation of the altitude.
		 * It includes the bulge of the earth off the chord PQ. We could calculate the
		 * buldge (b = tan(A/4)) and subtract it from "a" but in practice, it's not
		 * necessary and actually gives us a bit of bonus padding around the requested
		 * area.
		 *
		 * Algorithm:
		 *
		 * - First work out "A" for each side of the rectangle. Since latitude and
		 *   longitude are really angles, we can use the difference between the points
		 *   we've been passed:
		 *
		 *   Alat = max(lat1, lat2) - min(lat1, lat2)
		 *   Alng = max(lng1, lng2) - min(lng1, lng2)
		 *
		 * - Now pick the longest side to become the value for "A":
		 *
		 *   A = max(Alat, Alng)
		 *
		 * - Next work out "x" using "A" and "r":
		 *
		 *   x = r * tan(A / 2)
		 *
		 * - And now work out "a" using "x" and "F".
		 *
		 *   a = x / tan (F)
		 *
		 */
		createCameraFromRectangle: function($plugin, $lat1, $lng1, $lat2, $lng2) {
			// Some dodgy constants

			// Radius of the earh.
			var r = 6378700;

			// Field of view used by google (i think it's actually 35 but 30 gives us a
			// bit of buffer and seems to make sure everything fits).
			//var F = 30;
			var F = 24;

			// Create the camera
			var camera = $plugin.createCamera('');

			// Set the center
			camera.setLatitude(($lat1 + $lat2) / 2.0);
			camera.setLongitude(($lng1 + $lng2) / 2.0);
			camera.setHeading(0.0);
			camera.setTilt(0.0);

			// First work out the longer of the two sides
			var Alat = Math.max($lat1, $lat2) - Math.min($lat1, $lat2);
			var Alng = Math.max($lng1, $lng2) - Math.min($lng1, $lng2);

			var A = Math.max(Alat, Alng);

			// A is now the longest side's angle in degrees, convert to radians
			A = A * Math.PI / 180.0;

			// Now work out the chord length PQ (actually we get half the chord length here).
			var x = r * Math.tan(A / 2);

			// Now work out the approximate altitude using the distance above. Note: F
			// is in degrees so need to convert that to radians
			var a = x / (Math.tan( F * Math.PI / 180.0));

			//if alt is 0, set to CAMERA_MIN_ALTITUDE;
			if (a == 0)
				a = _CAMERA_MIN_ALTITUDE;

			// Got the altitude, set it
			camera.setAltitude(a);

			return camera;
		},

		/**
		 * Wrapper for createCameraFromRectangle() to create the camera from a
		 * LatLonAltBox.
		 *
		 * @param	plugin	A reference to the Google Earth plugin instance - this is
		 *					used to create new camera's
		 *
		 * @param	box		The LatLonAltBox to create the camera from.
		 *
		 * @return A new camera that will show the extents of the LatLonAltBox.
		 */
		createCameraFromLatLonAltBox: function(plugin, box) {
			return HyperCities.earth.createCameraFromRectangle(plugin,
											 box.getNorth(),
											 box.getEast(),
											 box.getSouth(),
											 box.getWest());
		},

		createLatLonBoxFromCamera: function($plugin, $lookAt) {
			// Radius of the earh.
			var r = 6378700;
			// Field of view used by google
			var F = 35;
			//var alt = $camera.getAltitude();
			var alt = $lookAt.getRange();
			var A, x, y;

			//if y > r, we can see the whole earth
			y = Math.sin(F * Math.PI / 180.0) * (alt + r);
			//HyperCities.debug("y="+y);
			if (y > r)
				A = Math.acos( r / (alt+r) ) * 2;
			else
			{
				x = alt * (Math.tan( F * Math.PI / 180.0));
				//HyperCities.debug("x="+x);
				//A = Math.atan(x/r) * 2;
				A = Math.asin(x/r) * 2;
			}
			A = A * 180.0 / Math.PI;

			//camera FOV fits to the larger side of window, A is the degree of larger side
			//get window width and height to calculate the shorter side
			var width = $('#contentWrapper').width();
			var height = $('#contentWrapper').height();
			//HyperCities.debug("width="+width);
			//HyperCities.debug("height="+height);
			var shortWidth;
			var north, south, east, west;

			if (width >= height)
			{
				shortWidth = A / width * height;
				north = $lookAt.getLatitude() + shortWidth/2;
				south = $lookAt.getLatitude() - shortWidth/2;
				east = $lookAt.getLongitude() + A/2;
				west = $lookAt.getLongitude() - A/2;
			}
			else
			{
				shortWidth = A / height * width;
				north = $lookAt.getLatitude() + A/2;
				south = $lookAt.getLatitude() - A/2;
				east = $lookAt.getLongitude() + shortWidth/2;
				west = $camera.getLongitude() - shortWidth/2;
			}

			var sw = new GLatLng(south, west);
			var ne = new GLatLng(north, east);
			var bounds = new GLatLngBounds(sw, ne);

			return bounds;
		},

		/**
		 * Function to union the rectangles stored in a list of LatLonAltBoxs.
		 *
		 * Note: We try and take altitude into account but, for it to work properly,
		 * all the LatLonBoxs should have the same altitude mode. We ignore altitudes
		 * from boxes that have a different altitude mode to the first box in the
		 * list.
		 *
		 * @param	plugin	A reference to the Google Earth plugin instance - this is
		 *					used to create new LatLonAltBox's
		 * @param	extents	A list of LatLonAltBox's to union.
		 *
		 * @return A LatLonAltBox with the union of the given LatLonAltBox's.
		 */
		unionExtents: function(plugin, extents) {
			var results = null;
			var current_extent;

			try {
				//HyperCities.debug("extents.length="+extents.length);
				if (extents.length > 0) {
					results = plugin.createLatLonAltBox('');

					current_extent = extents[0];
					//HyperCities.debug("extents["+0+"]=("+extents[0].getNorth()+", "+extents[0].getSouth()+", "+extents[0].getEast()+", "+extents[0].getWest()+")");

					results.setAltBox(current_extent.getNorth(),
									  current_extent.getSouth(),
									  current_extent.getEast(),
									  current_extent.getWest(),
									  0.0,
									  current_extent.getMinAltitude(),
									  current_extent.getMaxAltitude(),
									  current_extent.getAltitudeMode());

					for (i = 1; i < extents.length; i++) {
						//HyperCities.debug("extents["+i+"]=("+extents[i].getNorth()+", "+extents[i].getSouth()+", "+extents[i].getEast()+", "+extents[i].getWest()+")");
						
						current_extent = extents[i];
						if (current_extent.getNorth() > results.getNorth()) results.setNorth(current_extent.getNorth());
						if (current_extent.getSouth() < results.getSouth()) results.setSouth(current_extent.getSouth());
						if (current_extent.getEast() > results.getEast()) results.setEast(current_extent.getEast());
						if (current_extent.getWest() < results.getWest()) results.setWest(current_extent.getWest());
						if (current_extent.getAltitudeMode() == results.getAltitudeMode()) {
							if (current_extent.getMinAltitude() < results.getMinAltitude()) results.setMinAltitude(current_extent.getMinAltitude());
							if (current_extent.getMaxAltitude() > results.getMaxAltitude()) results.setMaxAltitude(current_extent.getMaxAltitude());
						}
					}
				}
			} catch (err) {
				HyperCities.debug(_id + "unionExtents error: "+err);
			}
			return results;
		},

		/**
		 * Function to calculate the extents of a given geometry.
		 *
		 * We try to support all geometry types but if we're passed one we cant deal
		 * with then we throw an exception named "UnexpectedGeometryTypeError".
		 *
		 * @param	plugin		A reference to the Google Earth plugin instance -
		 *						this is used to create new LatLonAltBox's
		 * @param	geometry	The geometry to determine the extents of.
		 *
		 * @return	A LatLonAltBox containing the extents of the geometry.
		 */
		getGeometryExtents: function(plugin, geometry) {
			var extents = [];
			var new_extent;
			var coords;

			if (geometry.getType() == "KmlMultiGeometry") {
				if (geometry.getGeometries().hasChildNodes()) {
					list = geometry.getGeometries().getChildNodes();
					for (i = 0; i < list.getLength(); i++) {
						geom = list.item(i);
						extents.push(HyperCities.earth.getGeometryExtents(plugin, geom));
					}
				}
			} else if (geometry.getType() == "KmlPolygon") {
				extents.push(HyperCities.earth.getGeometryExtents(plugin, geometry.getOuterBoundary()));

				if (geometry.getInnerBoundaries().hasChildNodes()) {
					list = geometry.getInnerBoundaries().getChildNodes();
					for (i = 0; i < list.getLength(); i++) {
						geom = list.item(i);
						extents.push(HyperCities.earth.getGeometryExtents(plugin, geom));
					}
				}
			} else if (geometry.getType() == "KmlLineString" || geometry.getType() == "KmlLinearRing") {
				if (geometry.getCoordinates().getLength() > 0) {

					coords = geometry.getCoordinates();

					new_extent = plugin.createLatLonAltBox('');

					new_extent.setAltitudeMode(geometry.getAltitudeMode());

					new_extent.setNorth(coords.get(0).getLatitude());
					new_extent.setSouth(coords.get(0).getLatitude());
					new_extent.setEast(coords.get(0).getLongitude());
					new_extent.setWest(coords.get(0).getLongitude());
					new_extent.setMinAltitude(coords.get(0).getAltitude());
					new_extent.setMaxAltitude(coords.get(0).getAltitude());

					for (i = 1; i < coords.getLength(); i++) {
						coord = coords.get(i);
						if (coord.getLatitude() > new_extent.getNorth()) new_extent.setNorth(coord.getLatitude());
						if (coord.getLatitude() < new_extent.getSouth()) new_extent.setSouth(coord.getLatitude());
						if (coord.getLongitude() > new_extent.getEast()) new_extent.setEast(coord.getLongitude());
						if (coord.getLongitude() < new_extent.getWest()) new_extent.setWest(coord.getLongitude());
						if (coord.getAltitude() < new_extent.getMinAltitude()) new_extent.setMinAltitude(coords.get(0).getAltitude());
						if (coord.getAltitude() > new_extent.getMaxAltitude()) new_extent.setMaxAltitude(coords.get(0).getAltitude());
					}
					extents.push(new_extent);
				}
			} else if (geometry.getType() == "KmlModel") {
				new_extent = plugin.createLatLonAltBox('');

				new_extent.setAltitudeMode(geometry.getAltitudeMode());

				new_extent.setNorth(geometry.getLocation().getLatitude());
				new_extent.setSouth(geometry.getLocation().getLatitude());
				new_extent.setEast(geometry.getLocation().getLongitude());
				new_extent.setWest(geometry.getLocation().getLongitude());
				new_extent.setMinAltitude(geometry.getLocation().getAltitude());
				new_extent.setMaxAltitude(geometry.getLocation().getAltitude());

				extents.push(new_extent);
			} else if (geometry.getType() == "KmlPoint") {
				new_extent = plugin.createLatLonAltBox('');

				new_extent.setAltitudeMode(geometry.getAltitudeMode());

				new_extent.setNorth(geometry.getLatitude());
				new_extent.setSouth(geometry.getLatitude());
				new_extent.setEast(geometry.getLongitude());
				new_extent.setWest(geometry.getLongitude());
				new_extent.setMinAltitude(geometry.getAltitude());
				new_extent.setMaxAltitude(geometry.getAltitude());
				extents.push(new_extent);
			} else {
				var name = "UnexpectedGeometryTypeError";
				var message = "getGeometryExtent() encountered unknown geometry of type: " + geometry.getType();
				HyperCities.debug(name + ": " +message);
				throw { name: name, message: message};
			}
			return HyperCities.earth.unionExtents(plugin, extents);
		},

		/**
		 * Function to calculate the extents of a given feature.
		 *
		 * If the given feature has a predefined Region then we return that,
		 * otherwise we calculate the extents from the features geometry.
		 *
		 * We try to support all geometry types but if we're passed one we cant deal
		 * with then we throw an exception named "UnexpectedGeometryTypeError".
		 *
		 * @param	plugin		A reference to the Google Earth plugin instance -
		 *						this is used to create new LatLonAltBox's
		 * @param	feature		The feature to determine the extents of.
		 *
		 * @return	A LatLonAltBox containing the extents of the feature.
		 */
		getFeatureExtent: function(plugin, feature) {
			var extents = [];

			HyperCities.earth.traverseKml(feature, function($node){
				if ($node.getType() != 'GEPlugin')
				{
					// Have a look and see if there's a region defined for this feature. If there is just return that.
					if ($node.getRegion()) {
						//HyperCities.debug("Has a region attribute.");
						return $node.getRegion().getLatLonAltBox();
					}
	
					// Nope, so look to see if it has a geometry.
					if ($node.getType() == 'KmlPlacemark') {
						if ($node.getGeometry() !== 'undefined' && $node.getGeometry()!= null)
						{
							//HyperCities.debug("Has a geometry attribute.");
							var geoExtents = HyperCities.earth.getGeometryExtents(plugin, $node.getGeometry());
							//HyperCities.debug("geoExtents="+geoExtents);
							extents.push(geoExtents);
						}
					}
				}
			});

			return HyperCities.earth.unionExtents(plugin, extents);
		},
		
		getFeatureView: function(plugin, feature) {
			var lookAt = [];
			
			HyperCities.earth.traverseKml(feature, function($node){
				if ($node.getType() != 'GEPlugin')
				{
					if ($node.getAbstractView())
						lookAt.push($node.getAbstractView());
				}
			});
		
			if (lookAt.length==0)
				return null;
			else
				return lookAt[0];
		},

		/**
		 * Function to zoom the view in the given plugin to the extents of the given
		 * feature.
		 *
		 * @param	plugin		A reference to the Google Earth plugin instance which
		 *						owns the view to zoom.
		 *
		 * @param	feature		The feature to zoom to.
		 */
		zoomToFeature: function(plugin, feature) {
			try
			{
				//get lookAt
				var lookAt = HyperCities.earth.getFeatureView(plugin, feature);
	
				if (!lookAt)
				{
					// get the extent
					var extent = HyperCities.earth.getFeatureExtent(plugin, feature);
					//HyperCities.debug("extent=("+extent.getNorth()+", "+extent.getSouth()+", "+extent.getEast()+", "+extent.getWest()+")");
		
					// Create a camera
					lookAt = HyperCities.earth.createCameraFromLatLonAltBox(plugin, extent);
				}
	
				// Zoom
				plugin.getView().setAbstractView(lookAt);	
			}
			catch(err)
			{
				var name = "ZoomToFeatureError";
				HyperCities.debug(name + ": " +err);
				//throw { name: name, message: message};
			}
		},

		zoomToFeature2: function(plugin, feature, boundary) {

			// Create a camera
			var camera = HyperCities.earth.createCameraFromLatLonAltBox(plugin, boundary);

			// Zoom
			plugin.getView().setAbstractView(camera);
		},

		setView: function($view)
		{
			if (_GEarth == null)
				return;
			var view = _GEarth.parseKml($view);
			if (view !== null)
				_GEarth.getView().setAbstractView(view);
		},

		//get and set functions
		//these functions are not supposed to be called.
		getEarth: function() {
			return _GEarth;
		},

		getCollections: function() {
			return _collections;
		}
	};
}(); // end of Object
