/**
 * Core Google map wrapper class
 *
 * @param		mixed		target			ID or DOM object
 * @param		JSON		properties
 *   - controls [json]
 *     - types [boolean]: show the map types selector (map/satellite/hybrid)
 *     - zoom [large/small]
 *       - large: shows all controls (pan, zoom +/-, zoom slider)
 *       - small: shows only zoom +/-, pan (if controls.pan is true)
 *     - pan [boolean]: show/hide the pan controls (only valid when zoom is set to small)
 *   - max_zoom [int:0-17] The closest the map will zoom, regardless of marker extents
 */
var BasicMap = function( target, properties ){

	if( !properties ){
		properties =
		{
			controls:{}
		};
	}
	if( !properties.controls ){
		properties.controls =
		{
		};
	}

	this.map = new GMap2( jQuery( target )[ 0 ] );
	this.geocoder = new GClientGeocoder();

	// Create 'loading' overlay
	this.loading_div = Utilities.Create
		(
			'div',
			{
				html:'',
				css:{
					'background-color':'#fff',
					'background-image':"url('/images/map/loading.gif')",
					'background-repeat': 'no-repeat',
					'background-position': 'center',
					width:'100%',
					height:'100%',
					opacity:0.5,
					display:'none'
				}
			}
		);
	this.AddControl( this.CreateControl( this.loading_div[ 0 ] ), {} );

	this.ClearMarkers();
	this.icons = {};
	this.overlays = {};

	if( properties.wheelzoom ){
		this.map.enableScrollWheelZoom();
	}

	// Margins for map -- these change when controls are added
	// to try to avoid markers being created under controls when ZoomExtents() is called
	this.margins =
	{
		left: 20,
		right: 20,
		top: 40,
		bottom: 0
	}

	// Defaults
	if( !properties.default_latitude ){
		properties.default_latitude = 0;
	}
	if( !properties.default_longitude ){
		properties.default_longitude = 0;
	}
	if( !properties.default_zoom ){
		properties.default_zoom = 2;
	}

	// Don't zoom in closer than this
	if( properties.max_zoom ){
		this.max_zoom = properties.max_zoom;
	} else {
		this.max_zoom = 14; // Broad street view
	}

	this.map.setCenter
	(
		new GLatLng
		(
			properties.default_latitude,
			properties.default_longitude
		),
		properties.default_zoom
	);

	// Options for zoom/pan
	if( properties.controls ){
		switch( properties.controls.zoom ){
			// With zoom slider + pan options
			case 'large':{
				this.map.addControl( new GLargeMapControl() );
				break;
			}
			case 'small':{
				if( properties.controls.pan ){
					// Zoom buttons only, with pan
					this.map.addControl( new GSmallMapControl() );
				} else {
					// Zoom buttons only
					this.map.addControl( new GSmallZoomControl() );
				}
				break;
			}
		}
	}

	// Show map types switcher
	if( properties.controls && properties.controls.types ){
		switch( properties.controls.types ){
			case 'nested':
			{
				this.map.addControl( new GHierarchicalMapTypeControl() );
				break;
			}
			case 'list':
			{
				this.map.addControl( new GMenuMapTypeControl() );
				break;
			}
			default:
			case 'bar':
			{
				this.map.addControl( new GMapTypeControl() );
				break;
			}
		}
	}

	// Show location breadcrumb
	if( properties.controls.location ){
		this.map.addControl( new GNavLabelControl() );

	}

	// Show
	if( properties.controls.scale ){
		this.map.addControl( new GScaleControl() );

	}

}

BasicMap.prototype.ShowLoading = function(){
	this.loading_div.css( 'display', '' );
}
BasicMap.prototype.HideLoading = function(){
	this.loading_div.css( 'display', 'none' );
}

BasicMap.prototype.SetCenter = function( latitude, longitude ){
	this.map.setCenter( new GLatLng( latitude, longitude ) );
}
BasicMap.prototype.SetZoom = function( zoom_level ){
	this.map.setZoom( zoom_level );
}

BasicMap.prototype.Bind = function( event, callback ){
	GEvent.addListener(this.map, event, callback);
}

/**
 * Add a marker overlay with content
 *
 * @param		decimal		longitude
 * @param		decimal		latitude
 * @param		JSON		properties
 *  - content		HTML content to be shown when marker is clicked
 */
BasicMap.prototype.AddMarker = function( latitude, longitude, properties ) {

	var marker_properties = {
	};

	if( properties.icon ){
		marker_properties.icon = this.GetIcon( properties.icon );
	}
	if( properties.draggable ){
		marker_properties.draggable = true;
	}
	if( properties.zIndex ){
		marker_properties.zIndexProcess = function(){ return properties.zIndex; }
	}

	var marker = new GMarker(
		new GLatLng
		(
			latitude,
			longitude
		),
		marker_properties
	);

	if( properties.group ){
		var group = properties.group;
		if( typeof( this.markers[ group ] ) == 'undefined' ){
			this.markers[ group ] = [];
		}
	} else {
		var group = 'default';
	}

	if( properties.events ){
		for( var event in properties.events ){
			GEvent.addListener
			(
				marker,
				event,
				properties.events[ event ]
			);
		}
	}

	this.markers[ group ].push( marker );

	this.map.addOverlay( marker );

	return marker;
}

BasicMap.prototype.MoveMarker = function( marker, latitude, longitude ) {
	marker.setLatLng( new GLatLng( latitude, longitude ) );
}

/**
 * Clear all markers
 */
BasicMap.prototype.RemoveMarker = function( marker ) {

	var markers = [];

	for( var i = 0; i < this.markers.length; i++ ){
		if( this.markers[ i ] == marker ){
			this.map.removeOverlay( marker );
		} else {
			markers.push( this.markers[ i ] );
		}
	}
	this.markers = markers;
}

/**
 * Clear all markers
 */
BasicMap.prototype.ClearMarkers = function( group ) {

	if( group ) {
		if( this.markers[ group ] ) {
			for( var i = 0; i < this.markers[ group ].length; i++  ){
				this.map.removeOverlay( this.markers[ group ][ i ] );
			}
			this.markers[ group ] = [];
		}
	} else {
		for( var index in this.markers ){
			for( var i = 0; i < this.markers[ index ].length; i++  ){
				this.map.removeOverlay( this.markers[ index ][ i ] );
			}
		}
		this.markers =
		{
			'default':[]
		};
	}
}

/**
 * Adds a popup window to a marker (activated by clicking the marker)
 *
 * @param		GMarker		marker
 * @param		string		content			HTML content to be shown in the popup
 * @param		JSON		properties		[optional] maxWidth
 */
BasicMap.prototype.AddPopupToMarker = function( marker, content, properties ){
	GEvent.addListener(
		marker,
		"click",
		function() {
			marker.openInfoWindowHtml
			(
				content,
				properties
			);
		}
	);
}

/**
 * Use Google's reverse Geocoding to convert a latitude and logitude to address
 *
 * @param		decimal			latitude
 * @param		decimal			longitude
 * @param		function		callback		Callback function
 */
BasicMap.prototype.LatLngToAddress = function( latitude, longitude, callback ){
	this.geocoder.getLocations( new GLatLng( latitude, longitude ), callback );
}
/**
 * Use Google's reverse Geocoding to get details from an address (e.g. lat/long, bounding area etc)
 *
 * @param		string			address
 * @param		function		callback		Callback function (arguments: lat, lon, name)
 */
BasicMap.prototype.AddressToLatLng = function( address, callback ){
	this.geocoder.getLocations
	(
		address,
		Utilities.Bind(
			function( result ){
				if( result.Placemark ){
					callback( result.Placemark[ 0 ].Point.coordinates[1], result.Placemark[ 0 ].Point.coordinates[0] );
				} else {
					callback( null, null, result.name );
				}
			}
		)
	);
}

//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
//:::                                                                         :::
//:::  This routine calculates the distance between two points (given the     :::
//:::  latitude/longitude of those points). It is being used to calculate     :::
//:::  the distance between two ZIP Codes or Postal Codes using our           :::
//:::  ZIPCodeWorld(TM) and PostalCodeWorld(TM) products.                     :::
//:::                                                                         :::
//:::  Definitions:                                                           :::
//:::    South latitudes are negative, east longitudes are positive           :::
//:::                                                                         :::
//:::  Passed to function:                                                    :::
//:::    lat1, lon1 = Latitude and Longitude of point 1 (in decimal degrees)  :::
//:::    lat2, lon2 = Latitude and Longitude of point 2 (in decimal degrees)  :::
//:::    unit = the unit you desire for results                               :::
//:::           where: 'M' is statute miles                                   :::
//:::                  'K' is kilometers (default)                            :::
//:::                  'N' is nautical miles                                  :::
//:::                                                                         :::
//:::  United States ZIP Code/ Canadian Postal Code databases with latitude   :::
//:::  & longitude are available at http://www.zipcodeworld.com               :::
//:::                                                                         :::
//:::  For enquiries, please contact sales@zipcodeworld.com                   :::
//:::                                                                         :::
//:::  Official Web site: http://www.zipcodeworld.com                         :::
//:::                                                                         :::
//:::  Hexa Software Development Center © All Rights Reserved 2004            :::
//:::                                                                         :::
//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

BasicMap.prototype.GetLatLngDistance = function(lat1, lon1, lat2, lon2, unit) {
	var radlat1 = Math.PI * lat1/180
	var radlat2 = Math.PI * lat2/180
	var radlon1 = Math.PI * lon1/180
	var radlon2 = Math.PI * lon2/180
	var theta = lon1-lon2
	var radtheta = Math.PI * theta/180
	var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
	dist = Math.acos(dist)
	dist = dist * 180/Math.PI
	dist = dist * 60 * 1.1515
	if (unit=="K") { dist = dist * 1.609344 }
	if (unit=="N") { dist = dist * 0.8684 }
	return dist
}

/**
 * Use Google's Geocoding to get 'bounding box' for the country of a given lat/long and zoom to it
 *
 * @param		decimal			latitude
 * @param		decimal			longitude
 */
BasicMap.prototype.ZoomToCountryFromLatLng = function( latitude, longitude ){
	this.LatLngToAddress
	(
		latitude,
		longitude,
		Utilities.Bind
		(
			function( result, latitude, longitude )
			{
				if( result.Placemark && result.Placemark.length > 0 ){
					var country = result.Placemark.pop();
					this.ZoomToBounds
					(
						{
							latitude:country.ExtendedData.LatLonBox.north,
							longitude:country.ExtendedData.LatLonBox.west
						},
						{
							latitude:country.ExtendedData.LatLonBox.south,
							longitude:country.ExtendedData.LatLonBox.east
						}
					)
				} else {
					// No bounding box returned, so centre on lat/long and go to default zoom level
					this.map.setCenter( new GLatLng( latitude, longitude ) );
					this.map.setZoom( 7 );
				}
			},
			this,
			[ latitude, longitude ]
		)
	)

	return;
}

/**
 * Add a new icon type
 */
BasicMap.prototype.AddIcon = function( icon_name, icon_image, icon_image_width, icon_image_height, shadow_image, shadow_image_width, shadow_image_height, icon_anchor_x, icon_anchor_y, info_anchor_x, info_anchor_y, imagemap ){

	var icon = new GIcon( G_DEFAULT_ICON );

	icon.image = icon_image;
	icon.iconSize = new GSize( icon_image_width, icon_image_height );
	icon.iconAnchor = new GPoint( icon_anchor_x, icon_anchor_y );

	icon.shadow = shadow_image;
	icon.shadowSize = new GSize( shadow_image_width, shadow_image_height );

	icon.infoWindowAnchor = new GPoint( info_anchor_x, info_anchor_y );

	if( imagemap ){
		icon.imageMap = imagemap;
	}

	this.icons[ icon_name ] = icon;
},

/**
 * Retrieve an icon type to display on the map
 *
 * @param		string		icon_name		Name of icon type as specified in AddIcon()
 *
 * @return		GIcon
 */
BasicMap.prototype.GetIcon = function( icon_name ){
	if( typeof this.icons[ icon_name ] != 'undefined' ){
		return this.icons[ icon_name ];
	} else {
		console.log( "Icon type '" + icon_name + "' not defined." );
	}
}

/**
 * Zoom map to show all markers
 *
 * @param		mixed		groups		Name (or array of names) of icon groups
 */
BasicMap.prototype.ZoomExtents = function( groups ) {

	var bounds = new GLatLngBounds();

	if( groups ){
		if( typeof group == 'string' ){
			group = [ group ];
		}
		for( var j = 0; j < groups.length; j++ ){
			for( var i = 0; i < this.markers[ groups[ j ] ].length; i++ ){
				bounds.extend( this.markers[ groups[ j ] ][ i ].getLatLng() );
			}
		}
	} else {
		for( var index in this.markers ){
			for( var i = 0; i < this.markers[ index ].length; i++ ){
				bounds.extend( this.markers[ index ][ i ].getLatLng() );
			}
		}
	}

	var box = this.map.showBounds
	(
		bounds,
		{
			top:this.margins.top,
			left:this.margins.left,
			right:this.margins.right,
			bottom:this.margins.bottom,
			max_zoom:this.max_zoom
		}
	)

}

/**
 * Jump to the nearest zoom level that encloses the area between two bounding points
 *
 * @param 		JSON		top_left 			JSON object for top left point, with latitude and longitude properties
 * @param 		JSON		bottom_right 		JSON object for bottom right point, with latitude and longitude properties
 */
BasicMap.prototype.ZoomToBounds = function( top_left, bottom_right ){
	var bounds = new GLatLngBounds();
	bounds.extend( new GLatLng( top_left.latitude, top_left.longitude ) );
	bounds.extend( new GLatLng( bottom_right.latitude, bottom_right.longitude ) );

	var box = this.map.showBounds
	(
		bounds,
		{
			top:this.margins.top,
			left:this.margins.left,
			right:this.margins.right,
			bottom:this.margins.bottom,
			max_zoom:this.max_zoom
		}
	)
}

/**
 * Create a control element (a 'button' or whatever that sits on top of the map and stays in the same place regardless of pan/zoom)
 * NB: this method does not add the element to the map, use AddControl() to do that
 *
 * @param		DOMElement		element		A DOM element
 *
 * @return		GControl
 */
BasicMap.prototype.CreateControl = function( element, properties, events ){

	var control = function(){
	};

	control.prototype = new GControl();
	control.prototype.initialize = function( map ) {
		map.getContainer().appendChild(element);
		return element;
	}

	// By default, the control will appear in the top left corner of the
	// map with 7 pixels of padding.
	control.prototype.getDefaultPosition = function() {
	  return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(7, 7));
	}

	if( properties ){
		this.AddControl( control, properties );
	}

	if( events ){
		for( var event in events ){
			this.BindElementEvent( element, event, events[ event ] );
		}
	}

	return control;
},

/**
 * Place a control element to the map
 *
 * @param		GControl		control			Control element (created using CreateControl)
 * @param 		JSON			properties		Control setings:
 * 													position[top_left, top_right, bottom_left, bottom_right],
 * 													h/v[horizontal/vertical offset from border in pixels]
 */
BasicMap.prototype.AddControl = function( control, properties ){

	switch( properties.position ){
		case 'top_right':{
			position = G_ANCHOR_TOP_RIGHT;
			break;
		}
		case 'bottom_left':{
			position = G_ANCHOR_BOTTOM_LEFT;
			break;
		}
		case 'bottom_right':{
			position = G_ANCHOR_BOTTOM_RIGHT;
			break;
		}
		default:
		case 'top_left':{
			position = G_ANCHOR_TOP_LEFT;
			break;
		}
	}
	if( typeof properties.h == 'undefined' ){
		properties.h = 0;
	}
	if( typeof properties.v == 'undefined' ){
		properties.v = 0;
	}

	this.map.addControl( new control( false, false ), new GControlPosition( position, new GSize( properties.h , properties.v ) ) );
},

/**
 * Bind an event to a control element
 *
 * @param		DOMElement		element			The element to add the event to
 * @param		string			event			Name of the event (e.g. 'click')
 * @param		function		callback		Callback function
 */
BasicMap.prototype.BindElementEvent = function( element, event, callback ){
	GEvent.addDomListener( element, event, callback );
},

/**
 * Create a circle polygon object
 * Adapted from http://groups.google.com/group/Google-Maps-API/browse_thread/thread/2dc8b5269b546d4e
 *
 * @param		decimal		lat					Latitude
 * @param		decimal		lon					Longitude
 * @param		int			radius				Radius (in km)
 * @param		int			num_nodes			[Optional] Number of segments making up the outline of the circle (more = smoother)(default = 10)
 * @param		string		line_colour			[Optional] Colour of outline, as hex RGB value (default = '')
 * @param		int			line_width			[Optional] Width of outline in pixels (default = 0)
 * @param		decimal		line_opacity		[Optional] Opacity of outline, number between 0 and 1 (default = 0)
 * @param		string		fill_colour			[Optional] Colour of fill, as hex RGB value (default = '')
 * @param		decimal		fill_opacity		[Optional] Opacity of fill, number between 0 and 1 (default = 0)
 *
 * @return		GPolygon
 */
BasicMap.prototype.CreateCircle = function( lat, lon, radius, num_nodes, line_colour, line_width, line_opacity, fill_colour, fill_opacity )
{

		// Defaults
        line_colour = line_colour||'';
        line_width = line_width || 0;
        line_opacity = line_opacity || 0;

		fill_colour = fill_colour || '';
		fill_opacity = fill_opacity || 0;

		num_nodes = num_nodes || 30;

        // Calculate km/degree
        var step = parseInt( 360 / num_nodes );
		var centre = new GLatLng( lat, lon );
        var latConv = centre.distanceFrom( new GLatLng( lat + 0.1, lon ) ) / 100;
        var lngConv = centre.distanceFrom( new GLatLng( lat, lon + 0.1 ) ) / 100;

        // Create an array of points representing the outline of the circle
        var points = [];
        for( var i=0; i <= 360; i += step )
        {
			var point = new GLatLng( lat + (radius/latConv * Math.cos(i * Math.PI/180)), lon + (radius/lngConv * Math.sin(i * Math.PI/180)));
			points.push( point );
        }

		// Create the polygon
		return new GPolygon( points, line_colour, line_width, line_opacity, fill_colour, fill_opacity );
}

BasicMap.prototype.AddOverlay = function( name, overlay ){
	this.RemoveOverlay( name );
	this.overlays[ name ] = overlay;
	this.map.addOverlay( overlay );
}
BasicMap.prototype.RemoveOverlay = function( name ){
	if( this.overlays[ name ] ){
		this.map.removeOverlay( this.overlays[ name ] );
	}
}


/**
 * GMap2.showBounds() method
 *
 * Zooms map to bounds with optional padding so that markers aren't lost at the top/under controls
 *
 * http://esa.ilmari.googlepages.com/showbounds.htm
 *
 * @ author Esa 2008
 * @ param bounds_ GLatLngBounds()
 * @ param opt_options Optional options object {top, right, bottom, left, instant, save}
 */
GMap2.prototype.showBounds = function(bounds_, opt_options){
  var opts = opt_options||{};
  opts.top = opt_options.top*1||0;
  opts.left = opt_options.left*1||0;
  opts.bottom = opt_options.bottom*1||0;
  opts.right = opt_options.right*1||0;
  opts.save = opt_options.save||true;
  opts.disableSetCenter = opt_options.disableSetCenter||false;
  var ty = this.getCurrentMapType();
  var port = this.getSize();
  if(!opts.disableSetCenter){
    var virtualPort = new GSize(port.width - opts.left - opts.right,
                            port.height - opts.top - opts.bottom);
	var zoom = ty.getBoundsZoomLevel(bounds_, virtualPort);
	if( opt_options.max_zoom && zoom > opt_options.max_zoom ){
		zoom = opt_options.max_zoom;
	}

    this.setZoom(zoom);
    var xOffs = (opts.left - opts.right)/2;
    var yOffs = (opts.top - opts.bottom)/2;
    var bPxCenter = this.fromLatLngToDivPixel(bounds_.getCenter());
    var newCenter = this.fromDivPixelToLatLng(new GPoint(bPxCenter.x-xOffs, bPxCenter.y-yOffs));
    this.setCenter(newCenter);
    if(opts.save)this.savePosition();
  }
  var portBounds = new GLatLngBounds();
  portBounds.extend(this.fromContainerPixelToLatLng(new GPoint(opts.left, port.height-opts.bottom)));
  portBounds.extend(this.fromContainerPixelToLatLng(new GPoint(port.width-opts.right, opts.top)));
  return portBounds;
}