/**
 *	jquery.mapify.js
 *
 *	A plugin for jQuery that creates a GPS coord based map and plots clickable points against it.
 *
 *
 *	version 0.5
 *	2009-04-26
 *	Andrew J Wright
 *
 *	iomer.com
 *
 **/


(function ($) {
	//
	// ***** Private
	//
	/**
	 *	gps.js
	 *
	 *	Converts GPS coordinates to screen coordinates.
	 *	2009-04-24
	 *
	 *	Andrew J Wright	
	 *
	 *	gpsCoord = { lat, long }
	 *
	 *	box = { t, r, b, l} // like css coords
	 *
	 **/
	
	var GPS = {
		
		EarthRadius 	: 6378137,
		MinLatitude 	: -85.05112878,
		MaxLatitude 	: 85.05112878,
		MinLongitude 	: -180,
		MaxLongitude 	: 180,
		piOver180 		: Math.PI/180,
		
		clip 	: function (n, minValue, maxValue) { return Math.min(Math.max(n, minValue), maxValue)},
		mapSize : function (levelOfDetail){return 256*Math.pow (2,levelOfDetail)},
		// ground resolution in meters per pixel
		groundResolution : function (latitude, levelOfDetail){
			latitude = this.clip(latitude, this.MinLatitude, this.MaxLatitude);
			return Math.cos(latitude * this.piOver180) * 2 * Math.PI * this.EarthRadius / this.mapSize(levelOfDetail);
		},
		//	mapScale: function (latitude, levelOfDetail, screenDpi){ return groundResolution(latitude, levelOfDetail) * screenDpi / 0.0254;},
			
		normaliseGPS : function (gpsCoord, gpsBox){
			var _x, _y;
			var _long = this.clip (gpsCoord.long, this.MinLongitude, this.MaxLongitude);
			var _lat = this.clip (gpsCoord.lat, this.MinLatitude, this.MaxLatitude);
			//
			// cartesianize the gps box lats, and the coords.
			// normalize the x coord
			_x = (_long - gpsBox.l) / (gpsBox.r - gpsBox.l);
			var cartT = this.latToCartesian (gpsBox.t);
			var cartB = this.latToCartesian (gpsBox.b);
			var cartLat = this.latToCartesian (_lat);
			_y = (cartLat-cartB) / (cartT - cartB);
			
			//
			return {x:_x, y: _y};
		},
		latLongToLocalXY : function (_lat, _long, gpsBox, width, height) {
			var _norm = this.normaliseGPS ({lat:_lat, long:_long}, gpsBox);
			// localise
			var _x = Math.round (_norm.x * width);
			var _y = Math.round (height - (_norm.y * height));
			return { x:_x, y:_y };
		},
		latLongToPixelXY : function (latitude, longitude, levelOfDetail){
			latitude = this.clip(latitude, this.MinLatitude, this.MaxLatitude);
			longitude = this.clip(longitude, this.MinLongitude, this.MaxLongitude);
		
			x = (longitude + 180) / 360; 
			sinLatitude = Math.sin(latitude * this.piOver180);
			y = 0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);
		
			_mapSize = this.mapSize(levelOfDetail);
			pixelX = Math.round (this.clip(x * _mapSize + 0.5, 0, _mapSize - 1));
			pixelY = Math.round (this.clip(y * _mapSize + 0.5, 0, _mapSize - 1));
			return {x:pixelX, y:pixelY };
		},
		/*
			res = 0.5 * Math.Log((1 + Math.Sin(phi)) / (1 - Math.Sin(phi)));
			= log (tan (theta/2 + pi/4))
		*/
		latToCartesian : function (lat) {
			var phi = this.piOver180 * lat;
			var sinLat = Math.sin (phi);
			var _y1 = 0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI);
			//var _y1 = 0.5 * Math.log ((1+sinLat) / (1-Math.sin(sinLat))); // this one seems faster
			return _y1;
		}
	}
	//
	// ***** End GPS
	//

	//
	//	Removes the 'filter' style attribute after an animation to render text nicely again!
	//	Optionally, pass in a scope to use if not using as a callback.
	//
	function removeIEfilter (el) {
		var el = el || this;
		if($.browser.msie) el.style.removeAttribute('filter');
	}
	
	// default is true
	function isZoomable (opts) { return (typeof opts.zoomable === 'undefined') ? true : opts.zoomable }
	
	////////////////////////////////////////////////////////////////////////////////////////////
	function createPanelBindings ($scope, fadeDuration) {
		$scope.children ('.panel')
		//
		// **** OPEN PANEL
		//
		.bind ('map.panel.openRequest', function (e, callback) {
			var $panel = $(this);
			
			var callback = (callback && (typeof callback == 'function')) ? callback : function () {};
			
			e.stopPropagation ();
			//
			//	make sure this panel isn't already open
			//
			if ($panel.is(':visible')) return;
			//
			//	check for any open panels and close them
			//
			var $showingPanels = $panel.siblings ('.panel:visible');
			if ($showingPanels.length > 0) {
				$showingPanels.trigger ('map.panel.closeRequest', function () { 
					$panel.trigger ('map.panel.openRequest', callback); 
				});
				return;
			}
			
			//
			//	open and set up the panel
			//
			var $mapCanvas = $panel.parent('.map-canvas').eq(0);
			var top = ($mapCanvas.height() - $panel.height()) / 2;//Math.max(($('#mapCanvas').height() - $flyout.height()) / 2 - 30, 0);
			var left = ($mapCanvas.width() - $panel.width()) / 2;//Math.max(($('#mapCanvas').width() - $flyout.width()) / 2, 0);
	
			$panel
			.css ({ 'top': top, 'left': left })
			.fadeIn (fadeDuration, function () {
				removeIEfilter(this); 
				$panel
				.children('.closeButton')
				.click (function (e) {
					e.stopPropagation ();
					e.preventDefault ();
					//
					//	trigger the panel close event.
					//
					$panel.trigger ('map.panel.closeRequest');
				})
				.show();
				//
				//	If a callback is assigned, call it!
				//
				callback ();
			});
			//
			//	Allow the panel to be closed by pressing ESC / x / X
			//
			$(document).keydown (function (e) { if (e.keyCode === 27 || e.keyCode === 88 || e.keyCode === 120) $panel.trigger ('map.panel.close') });
		})
		//
		// **** CLOSE PANEL
		//
		.bind ('map.panel.closeRequest', function (e, callback) {
			var $panel = $(this);
			
			var callback = (callback && (typeof callback == 'function')) ? callback : function () {};
			
			e.stopPropagation ();
			//
			//	make sure this panel IS already open
			//
			if (!$panel.is(':visible')) return;
			
			$panel
			.children ('.closeButton')
				.unbind ('click')
				.hide ()
				.end ()
			.fadeOut (fadeDuration, callback);
			//
			//	Allow the panel to be closed by pressing ESC
			//
			$(document).unbind ('keydown');
		});
	}
	function destroyPanelBindings ($scope) { $scope.children ('.panel').unbind () }
	////////////////////////////////////////////////////////////////////////////////////////////
	
	function addLabel (id, $pin, $appendTarget) {
		var pointer_id = id + '-pointer';
		var tip_id = id +'-tip';
		
		
		// Find the anchor with the pins' title
		var $anchor = $('> .pinHead', $pin);
		if ($anchor.length == 0) $anchor = $('> .map-cluster-box', $pin);
		
		var tip_text = $anchor.attr ('tooltip') || '';
		
		// Create the pointer and the tooltip box.
		var pointer = $('<div/>')
			.addClass ('map-pointer')
			.css ({
				left: $pin.position().left + 2,
				top: $pin.position().top - 7
			})
			.attr ('id', pointer_id)
			.appendTo ($appendTarget);

		var tooltip = $('<div/>')
			.addClass ('map-tip')
			.attr ('id', tip_id)
			.text (tip_text)
			.css ({
				left: $pin.position().left - 12,
				top: $pin.position().top - 30
			})
			.appendTo ($appendTarget);
	}
	function removeLabel (id) {
		$('#'+id+'-tip').remove ();
		$('#'+id+'-pointer').remove ();
	}
	////////////////////////////////////////////////////////////////////////////////////////////
	//
	//	makeZoomables
	//
	function makeZoomables ($scope, opts) {
		$scope.children ('.map-stretcher') // debug: change to .map-stretcher
		.each (function (i) {
			var $stretcher = $(this);
			var $zoomable = $stretcher.children ('.map-cluster-box'); //debug: change to .map-canvas
			var $localCanvas = $stretcher.children ('.map-canvas');
			
			// quickly swap out the title attribute for the tooltip.
			swapTooltip ($zoomable);
			
			// the stretcher may have been hidden by clicking a zoomable, so re-display it.
			$stretcher.show ();
			//	Hide the pins on the canvas
			$localCanvas.children ('.pin').hide();
			//  And hide the summary link button
			$localCanvas.children ('.summaryLink').hide();
			//	
			//	Locate the details for this zoomable stretcher
			//
			var n=opts.zoomables.length;
			var found = false;
			while (!found && --n>=0) { found = (opts.zoomables[n].id == $stretcher.attr('id')) }

			var zoomable = {};
			if (found) {
				zoomable = opts.zoomables[n];
				var w = zoomable.width,
					h = zoomable.height,
					l = $scope.width()/2-(zoomable.width/2),
					t = $scope.height()/2-(zoomable.height/2);
				//
				//	This is actually a 4 coord box, not a point
				//
				var ctl = GPS.latLongToLocalXY (zoomable.gpsBox.t, zoomable.gpsBox.l, opts.gpsBox, opts.width, opts.height);
				var cbr = GPS.latLongToLocalXY (zoomable.gpsBox.b, zoomable.gpsBox.r, opts.gpsBox, opts.width, opts.height);
			
			
				var _w = cbr.x - ctl.x;
				var _h = cbr.y - ctl.y;
	
	
				// make sure the point is within the frame
				if (ctl.x>=0 && ctl.y>=0 && ctl.x<=clipPixelCoords.x && ctl.y<=clipPixelCoords.y) {
					$stretcher.css ({
						top:	ctl.y+'px',
						left:	ctl.x+'px',
						width:	_w+'px',
						height:	_h+'px'
					});
					//
					//	Bind the click event to the cluster -- 
					//		: hide showing pins
					//		: zoom in!
					//		: mapify the zoom'd canvas
					//		: add close button
					//
					
					
					
                    if (opts.interactive) {
					    $zoomable.click (function (e) {
						    e.stopPropagation ();
						    e.preventDefault ();
    	
						    // hide the parent pins
						    $stretcher.siblings ().fadeOut (300);
    						
						    // hide the map-cluster-box
						    $zoomable.hide ();

						    //
						    //	Add the image to the dom that will zoom in.
						    //
						    //	NOTE: for whatever (good?) reason, jQuery removes nodes from
						    //	the DOM with $(...).remove() but not from its internal object.
						    //
						    //	Somehow, this if test here prevents accessing the same stored node multiple times
						    //
						    var $iph = $('#imgPlaceholder');
						    if ($iph.length == 0) $iph = $('<img src='+zoomable.imgSrc+' id="imgPlaceholder" alt="" />');

						    $iph
						    .appendTo ($scope)
						    .css ({
							    border: 'none',
							    width: $stretcher.width(),
							    height: $stretcher.height (),
							    position: 'absolute',
							    top: $stretcher.position().top,
							    left: $stretcher.position().left
						    })
						    .hide ()
						    .animate ({
							    width : (zoomable.imgWidth>=0) ? zoomable.imgWidth : zoomable.width,
							    height : (zoomable.imgHeight>=0) ? zoomable.imgHeight : zoomable.height,
							    left : l,
							    top: t,
							    opacity: 'show'
						    },1000, function () {
							    //
							    //	Animation complete, show the new map and plot the pins.
							    //
							    $stretcher.mapify ($.extend ({}, { fadeDuration : opts.fadeDuration, left: l, top: t }, zoomable ));
							    //
							    //	Remove the animated image placeholder from the dom
							    //
							    $('#imgPlaceholder').remove();
							    //
							    //	Add the back button
							    //
							    $("<a href='javascript:void(0);'>&laquo; Back to Alberta Map</a>")
							    .click (function (e) {
								    e.stopPropagation();
								    e.preventDefault ();
    								
								    $localCanvas.children ('.panel:visible').hide();
								    $localCanvas.children ('.summaryLink').hide();
								    deactivatePins ($stretcher);
								    destroyPanelBindings ($stretcher);
								    $(this).siblings().fadeOut(opts.fadeDuration)
								    .end ()
								    .remove ();
    								
								    //$stretcher.fadeOut (opts.fadeDuration, function () {
								    //$stretcher.css ('backgroundImage', 'none')
    								
								    $stretcher.children ('.map-backgroundImage').remove ();
    								
								    //})
								    $stretcher.children ('.map-canvas').css ({
									    width : '100%',
									    height : '100%'
								    });

								    // debug: grab the parent stretcher... should have the class included in the search: .parent('.map-stretcher')...
								    $scope.parent().eq(0).mapify (opts);

							    })
							    .addClass ('backButton')
							    .appendTo ($localCanvas);
							    
							    $localCanvas.children ('.summaryLink').show ();
						    });
					    })
					    .mouseenter (function (e) { addLabel (e.currentTarget.id, $stretcher, $scope) })
					    .mouseleave (function (e) { removeLabel (e.currentTarget.id) })
					    .hide (1)
					    .fadeIn (opts.fadeDuration);
                    } else {
                        $zoomable
					    .mouseenter (function (e) { addLabel (e.currentTarget.id, $stretcher, $scope) })
					    .mouseleave (function (e) { removeLabel (e.currentTarget.id) })
					    .hide (1)
					    .fadeIn (opts.fadeDuration);
                    }
				}
			} else { output ('zoomable could not be found'); }
		});
	}

	//
	//	Utility function to swap the title into the tooltip.
	//
	function swapTooltip ($elem) {
		if ($elem.attr('tooltip') === undefined) {
			$elem.attr('tooltip', $elem.attr('title') || '');
			$elem.attr('title', '');
		}
	}
	////////////////////////////////////////////////////////////////////////////////////////////
	//
	//	Activate Pins
	//
	function activatePins ($scope, opts) {
		$scope.children ('.pin')
		.each (function (i) {
			var $pin = $(this);
			var $pinHead = $pin.children ('.pinHead');
			
			// quickly and discretely move the 'title' attribute to the 'tooltip' attribute
			swapTooltip ($pinHead);
			
			var gpsCoords = $pinHead.attr('rel').split('/'); // [0]=lat [1]=long
		
			var localCoord = GPS.latLongToLocalXY (gpsCoords[0], gpsCoords[1], opts.gpsBox, opts.width, opts.height);
	
			// make sure the point is within the frame
			if ( localCoord.x>=0 && localCoord.y>=0 && localCoord.x<=$scope.width() && localCoord.y<=$scope.height() ) {
				// create a pin div at (pxX, pxY)
				$pin.css ({
					top:	(localCoord.y-18)+'px',//(localCoord.y-26)+'px',
					left:	(localCoord.x-6)+'px'//(localCoord.x-12)+'px'
				})
				.fadeIn ();
				//
				//	Bind the click event to the pin -- opens the associated panel
				//
				$pinHead.click (function (e) {
					var id = e.currentTarget.id + '-panel';
					e.stopPropagation ();
					e.preventDefault ();
					$('#'+id).trigger ('map.panel.openRequest');
				})
				.mouseenter (function (e) { addLabel (e.currentTarget.id, $pin, $scope) })
				.mouseleave (function (e) { removeLabel (e.currentTarget.id) });
			}
		}); // END pins
	}
	//
	//	De-activate Pins
	//
	function deactivatePins ($scope) { $('> .pin .pinHead',$scope).unbind () }
	////////////////////////////////////////////////////////////////////////////////////////////
	
	
	// giant map coords of top-left and bottom-right.
	var clipPixelCoords;
	
	
	// for cross browser safety
	function output (strText) {
		if (typeof console !== "undefined" && typeof console.log !== "undefined") {
			console.log (strText);
		} else {
			alert (strText);
		}
	}
	////////////////////////////////////////////////////////////////////////////////////////////
	//
	// ***** Public
	//
	////////////////////////////////////////////////////////////////////////////////////////////
	$.fn.mapify = function (options) {

		// extend default args with those provided
		var opts = $.extend ({}, $.fn.mapify.defaults, options);

		clipPixelCoords = { x:opts.width, y:opts.height };
		
		return this.each (function () {
			var $this = $(this);
			

			var $mapCanvas = $this.children('.map-canvas');
			
			$this.css ({
				//'background' : 'transparent url('+opts.imgSrc+') 0 0 no-repeat scroll',
				width	: opts.imgWidth,
				height	: opts.imgHeight,
				left	: opts.left,
				top		: opts.top,
				position: 'relative'
				
			});
			//
			// Insert the background image as an <img> element so that it will print.
			// First make sure that there is not one already there.
			if ($this.children('.map-backgroundImage').length == 0) {
				var $img = $("<img src='"+opts.imgSrc+"' alt='' />")
				.addClass ('map-backgroundImage')
				.css ({
					position: 'absolute',
					top		: 0,
					left	: 0
				});
				$this.prepend ($img);
			}
			//
			// Set sizing and postion properties on the canvas
			$mapCanvas.css ({ 
				height	: opts.height, 
				width	: opts.width,
				left	: opts.canvasOffset.x,
				top		: opts.canvasOffset.y
			});
			
			//
			createPanelBindings ($mapCanvas, opts.fadeDuration);
			//
			
			//
			activatePins ($mapCanvas, opts);
			//			

			//
			//	activate clusters themselves
			//
			makeZoomables ($mapCanvas, opts);
			//
		}); // return
	}, // $.fn.mapify
	
	$.fn.mapify.defaults = {
		// Alberta's boundaries
		gpsBox : {
			t:60.00557,
			r:-110.00610,
			b:49.00328,
			l:-120.00366
		},
		width	: 100,
		height	: 100,
		left	: 0,
		top		: 0,
		canvasOffset : { x: 0, y: 0 },
		imgSrc	: '',
		imgWidth : -1,
		imgHeight: -1,
		fadeDuration : 400,
		zoomables : [],
		interactive : true
	}
}) (jQuery);

//
//  Returns an object of format:
//      {
//          queryParam1 : value1,
//          queryParam2 : value2,
//          queryParam3 : value3,
//              .
//              .
//              .
//      }
function parseQueryString () {
	var reQueryString = /(\?[^#]*)/,
		reQueryParams = /[?|&]([^\=]+)\=([^&#]+)/g,
		arrQueryString = reQueryString.exec(window.location),
		arrQueryParamVal = [],
		oParams = {};

    if (arrQueryString !== null) {
	    while ((arrQueryParamVal = reQueryParams.exec (arrQueryString[1])) != null) {
		    oParams[arrQueryParamVal[1]] = arrQueryParamVal[2];
	    }
    }
	return oParams;
}

/*
 *	Actual initialization code
 *
 */
$(function () {
    var regionKeys = { edmonton : 'edm', calgary : 'cgy', allRegions : 'all' };
    //
    // GPS coordinate rectangles defining Edmonton and Calgary regions.
    //
    var regionRects = [];
    regionRects[regionKeys.edmonton]= {t:53.72206, r:-112.99575, b:53.25798, l:-113.95568};
    regionRects[regionKeys.calgary] = {t:51.31791, r:-113.70849, b:50.70272, l:-114.38278};
    //
    //	Utility function to determine if the point is inside a given box
    //
    function intersectsRect (point, rect) { return ((point.x > rect.l) && (point.x < rect.r) && (point.y > rect.b) && (point.y < rect.t)); }    
    
    function filterByRegion (selector, strRegion) {
        var regionRect = regionRects[strRegion];
        selector = $(selector);

        if ( !regionRect ) return;
        selector.each (function () {
            var $this = $(this);
            var data = ($this.attr('coords') || '-1/-1').split ('/');
	        //
	        //	Test if the pin is in one of the regions and don't plot it yet.
	        //
	        var point = { x:data[1], y:data[0] };
	        if (!intersectsRect (point, regionRect)) $this.remove ();
        });
    }


	function getZoomableRegion (key) { // returns region obj
		var region;
		switch (key.toLowerCase()) {
			case regionKeys.calgary :
				region = {
					id		: 'cal-cluster',
					width	: 381, 
					height	: 549,
					imgWidth : 391,
					imgHeight: 557,
					canvasOffset : {x:4, y: 4},
					imgSrc	: '/_layouts/iomerSPPCNMap/images/calgary_map.gif',
					gpsBox	: {
						t:51.31791,
						r:-113.70849,
						b:50.70272,
						l:-114.382783
					}
				}; break;
			case regionKeys.edmonton :
				region = {
					id		: 'edm-cluster',
					width	: 473,
					height	: 386,
					imgWidth : 481,
					imgHeight: 394,
					canvasOffset : {x:4, y: 4},
					imgSrc	: '/_layouts/iomerSPPCNMap/images/edm_map2.gif',
					gpsBox	: {
						t:53.72206,
						r:-112.99575,
						b:53.25798,
						l:-113.95568
					},
					regionSummaryHref : ''
				}; break;
			case regionKeys.allRegions :
			default:
				region = {
					width	: 277,
					height	: 512,
					imgWidth : 326,
					imgHeight: 560,
					canvasOffset : {x: 25, y: 25},
					imgSrc	: '/_layouts/iomerSPPCNMap/images/map.png',
					gpsBox : {
						t:60.00557,
						r:-109.99511,
						b:49.00328,
						l:-120.03662
					},
					fadeDuration : 400,
					zoomable : false,
					zoomables : [
						getZoomableRegion (regionKeys.edmonton),
						getZoomableRegion (regionKeys.calgary)
					]
				}; break;
		}
		return region;
	}

    // Grab the query string values
    var params = parseQueryString(),
        boolInteractive = ( params.interactive ) ? ( params.interactive == 'false' ? false : true ) : true, // defaults to true
        regionKey = params.region || regionKeys.allRegions,
        region = getZoomableRegion (regionKey);
    
    /*
        when displaying the full provincial map, determine if we need to display the map with borders or not.
        NOTE: this is done by using a pseudo-querystring value that is appended to the .js file include by
        the webpart.
        
        <script src="jquery.mapify-1.0.0.js?q=border" ... ></script>
    */
    if (regionKey==regionKeys.allRegions) {
	    var filename = /jquery\.mapify-1\.0\.0\.js(\?.*)?$/;
	    
	    var _$scripts = $('script[src]')
	    .filter (function () {
	        return this.src.match (filename);
	    })
        .each (function () {
	        var query = this.src.match(/\?.*q=([a-z,]*)/);
	        if (query && query[1]=='borders') {
		        region = $.extend (region, { imgSrc : '/_layouts/iomerSPPCNMap/images/map_borders.png' });
	        }
        });
    }
    /* end border switching */
    
    // make the map markup into the map-actual!
	$('#mapBG').mapify ($.extend ({}, region, { interactive : boolInteractive }));
	
	// based on the gps coords region, sort / filter / toggle visibility of appropriate info that may exist on page
	var filterables = $('.PCN_pcnItemWrapper');
	if ( filterables.length && params.region != '' ) {
	    filterByRegion ( filterables, params.region );
	}
	
	// if the map is NOT interactive then we will hide the provincial summary / details links
	if ( !boolInteractive ) $('.mapPrintablePageLinks').hide ();
		
});