// ==UserScript==
// @name          Geograph Images
// @namespace     http://benchmarks.org.uk/geographimages/
// @version       1.0
// @description   (v1.0) Adds nearby Geograph images to a cache page
// @include       http://www.geocaching.com/seek/cache*
// ==/UserScript==

// Copyright (C) 2010 Gary Player <dev@benchmarks.org.uk>
// Released under the GPL http://www.gnu.org/copyleft/gpl.html
// This is a Greasemonkey user script, see http://greasemonkey.mozdev.org/.

(function(){

// items set by menu options - opera users will need to manually change these
var imagesPerRow = 3;
var defaultRowsToShow = 1;

// constants
var mapId = 'ctl00_ContentBody_uxlrgMap';
var lnkConvId = 'ctl00_ContentBody_lnkConversions';
var lightboxRelId = "lightbox[geothumbs]"; // allows images to be treated as part of geothumbs script
var collapseImgId = 'collapseImgId';
var geographDivTableId = 'geographDivTableId';
var closedImg = '../images/arrow_close.gif';
var openedImg = '../images/arrow_open.gif';
var searchDist = 2; // 2km search distance
var searchMaxImages = 30; // max number of images to return
var style = '#geographImagesTable { width:100%; border-collapse: collapse; border:1px solid #D7D7D7; table-layout: auto; }' +
			'.geographImageCell { width:120px; border: 0px none; padding: 8px; }' +
			'.geographImage { display: block; margin-left: auto; margin-right: auto; }' +
			'.geographTitleCell { width:1400px; padding-right:8px; }' +
			'.geographTitleLink { text-decoration: none; }' +
			'.geographDescription { font-size: smaller; }' +
			'.geographDescriptionLink { color: #666666; }' +
			'#geographHeadingLinkId { text-decoration: none; font-weight: bold; color: black !important; }' +
			'#geographMoreId { font-size: smaller; position: relative; bottom:0.5pt;}' +
			'#geographHeadingRightId { float:right; width:30px; position: relative; bottom:0px;}' +
			'#geographIconLinkId { float:left; } ' +
			'#geographCollapseId { float:right; position:relative; top:3px; }' +
			'#geographLicenseId { color: #666666; }' +
			'#caption #description { font-weight: normal; }' +
			'#imageData #imageDetails { width: 80%; }';

// variables
var extWindow; // window access
var numDescriptionCharsPerImage = 0; // calculated number of description chars to display
var geographLink = "";
var geographHomePage = "";
var geographPhotoPageUrl = "";
var geographGridrefPageUrl = "";
var geographApiUrl = "";
var lat = 0;
var lon = 0;
var geographData; // holds data returned by geograph
var unshownIndex = 0; // the first unshown geograph image 
var collapsed = false; // image table initially shown

//sets the geograph urls for this lat lon 
function setGeographUrls()
{
	if (inBritainAndIreland())
	{
		geographLink = 'geograph.org.uk';
		geographHomePage = 'http://www.geograph.org.uk/';
		geographApiUrl = 'http://api.geograph.org.uk/api';
		geographPhotoPageUrl = 'http://www.geograph.org.uk/photo/';
		geographGridrefPageUrl = 'http://www.geograph.org.uk/gridref/';
	}
	else if (inChannelIslands())
	{
		geographLink = 'channel-islands.geographs.org';
		geographHomePage = 'http://channel-islands.geographs.org/';
		geographApiUrl = 'http://channel-islands.geographs.org/api';
		geographPhotoPageUrl = 'http://channel-islands.geographs.org/photo/';
		geographGridrefPageUrl = 'http://channel-islands.geographs.org/gridref/';
	}
	else if (inGermany())
	{
		geographLink = 'geo-en.hlipp.de';
		geographHomePage = 'http://geo-en.hlipp.de/';
		geographApiUrl = 'http://geo-en.hlipp.de/restapi.php/api';
		geographPhotoPageUrl = 'http://geo-en.hlipp.de/photo/';
		geographGridrefPageUrl = 'http://geo-en.hlipp.de/gridref/';
	}
}

// add css styles
function addGlobalStyle(css) {
    var head = document.getElementsByTagName('head')[0];
    if (head)
    {
		var style = document.createElement('style');
		style.type = 'text/css';
		style.innerHTML = css;
		head.appendChild(style);
    }
}

// insert newNode after referenceNode
function insertAfter(referenceNode, newNode)
{
	referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}

// returns a string shortened to the nearest word boundary less than the specified length
function shortenString(str,n)
{ 
	  if(str.length > n)
	  {
		  var s = str.substr(0, n);
		  var ls = s.lastIndexOf(' ');
		  str = s.substr(0,ls);
	  }
	  return str;
}

// convert awkward html entities
function convertHtmlEntity(h)
{
	if (h)
	{
		// convert &#8470; into No
		h = h.replace(/&#8470;/g,'No ');
	}
	return h;
}

// tidies a geograph description by:
// 1. removing newlines
// 2. converting geograph links to html links
// 3. converting http strings to html links
function tidyDescription(d)
{
	if (d)
	{
		// convert /r /n to space
		d = d.replace(/(\\[rn])+/g,' ');
		
		// convert http strings to an html link
		d = d.replace(/(https?:\/\/?[a-z0-9_$!&%?,#@'\/.*+;:=~-]+)/gi,'<a class="geographDescriptionLink" href="$1" target="_blank">link</a>');
		
		// convert geograph format photo links to html links
		d = d.replace(/\[+(\d+)\]+/g,'<a class="geographDescriptionLink" href="'+geographPhotoPageUrl+'$1" target="_blank">link</a>');

		// convert geograph format gridref links to html links
		d = d.replace(/\[+([a-z]{1,3}\d+)\]+/gi,'<a class="geographDescriptionLink" href="'+geographGridrefPageUrl+'$1" target="_blank">link</a>');
		
		// ensure trailing '.'
		d = d.replace(/\.\s*$/, '') + '.';
	}
	return d;
}

// cycles number of images per row between 2 and 4
function imagesPerRowMenuAction()
{
	imagesPerRow++;
	if (imagesPerRow > 4)
		imagesPerRow = 2;
	GM_setValue('imagesPerRow', imagesPerRow);
	document.location.reload();
}

// cycles default number of rows to show between 1 and 4
function defaultRowsToShowMenuAction()
{
	defaultRowsToShow++;
	if (defaultRowsToShow > 4)
		defaultRowsToShow = 1;
	GM_setValue('defaultRowsToShow', defaultRowsToShow);
	document.location.reload();
}

// set up user defined settings
function getUserDefinedSettings()
{
	imagesPerRow = GM_getValue('imagesPerRow', imagesPerRow);
	defaultRowsToShow = GM_getValue('defaultRowsToShow', defaultRowsToShow);
}

// register menus
function registerMenus()
{
	GM_registerMenuCommand('Geograph Images - Images per row : ' + imagesPerRow, imagesPerRowMenuAction);
	GM_registerMenuCommand('Geograph Images - Default rows to show : ' + defaultRowsToShow, defaultRowsToShowMenuAction);
}

// get lat lon from page
function getLatLon()
{
	// get lat lon coordinates
	var lnkConv = document.getElementById(lnkConvId);
	if (lnkConv)
	{
		var ll = lnkConv.href.match(/-?\d+\.?\d*/g);
		lat = ll[0];
		lon = ll[1];
	}
}

// returns true if lat lon is in britain or ireland
function inBritainAndIreland()
{
	if (lat > 49.7 && lat < 61 && lon > -12.2 && lon < 2)
		return true;
	else
		return false;
}

// returns true if lat lon is in the channel islands
function inChannelIslands()
{
	if (lat > 49.1 && lat < 49.8 && lon > -2.8 && lon < 1.8)
	{
		return true;
	}
	else
		return false;
}

//returns true if lat lon is in germany
function inGermany()
{
	if (lat > 47.25 && lat < 55.1 && lon > 5.8 && lon < 15.05)
		return true;
	else
		return false;
}

// returns true if geograph covers the lat lon
function isCoveredByGeograph()
{
	if (inBritainAndIreland() || inChannelIslands() || inGermany())
		return true;
	else
		return false;
}

// get geograph api url - distance in km
function getApiUrl(distance, maxImages)
{
	return geographApiUrl+'/Latlong/'+distance+'km/'+lat+','+lon+'/geocache?output=json&perpage='+maxImages+'&callback=?';
}

// add a geograph image to the specified cell
function addImage(cell, item)
{
	var thumbnail = item.thumbnail;
	var image = item.image;
	var title = item.title;
 	var description = item.comment;
	var author = item.realname;
	var gridimage_id = item.gridimage_id;
	var imagetaken = item.imagetaken;
	var distance = item.dist;
	
	// create anchor and set the lightbox rel id
	var a = document.createElement('a');
	a.rel = lightboxRelId;
	
	// format the distance info
	var distanceStr = Math.floor(distance) + 'm away';

	// create the author license
	var licence='&copy; ' + author + ' and licensed for reuse under this ' +
				'<a id="geographLicenseId" href="http://creativecommons.org/licenses/by-sa/2.0/" target="_blank">Licence</a>.';

	// initialise lightbox and image title tag descriptions 
	var lightboxDescription = '<span id="description">';
	var imageTitleDescription = '';

	// add description if there is one
	if (description && description != '')
	{
		// create a tidied description for the lightbox
		var tDesc = tidyDescription(description);

		// add distance to tidied description
		lightboxDescription = lightboxDescription + tDesc + '<br>';
		
		// remove any trailing '. ' from image pop-up description
		imageTitleDescription = ' - ' + description.replace(/\.\s*$/,'');
	}
	
	// add distance and licence (only for lightbox)
	// lightboxDescription = lightboxDescription + distanceStr + '<br>' + licence + '</span>';
	lightboxDescription = lightboxDescription + licence +  '<br>' + distanceStr + '</span>';
	imageTitleDescription = imageTitleDescription + ' - ' + distanceStr;
	
	// link to geograph page
	var geographPage = geographPhotoPageUrl + gridimage_id;
	
	// create anchor title - this is used by lightbox
	var atitle = '"' + title + '"' +
				' by ' + author + ' on ' + imagetaken + '<br>' + lightboxDescription + '<br>' +
				'<a href="' + geographPage + '" target="_blank">Geograph Page</a>&nbsp;&nbsp;' +
				'<a href="javascript:pp(' + "'" + image + "'" + ');">Print Picture</a>';
	a.title = atitle;
	a.href = image;

	// create thumbnail image
	var thumb = new Image();
	thumb.src = thumbnail;
	thumb.className = 'geographImage';
	
	// create the image tooltip
	thumb.title = title + ' by ' + author + imageTitleDescription;
	thumb.alt = title;	
	
	// add thumbnail to anchor
	a.appendChild(thumb);
	
	// add anchor to cell
	cell.appendChild(a);
}

// add a geograph title to the specified cell
function addTitle(cell, item)
{
	var title = item.title;
	var description = item.comment;
	var geographPageUrl = geographPhotoPageUrl + item.gridimage_id;
	
	// create new anchor for text link to geograph page
	var textAnchor = document.createElement('a');
	textAnchor.className = 'geographTitleLink';
	textAnchor.href = geographPageUrl;
	textAnchor.target = '_blank';
	textAnchor.title = title; // sets tooltip
	textAnchor.textContent = title;
	
	// add anchor to cell
	cell.appendChild(textAnchor);// add description if required

	// add description if one exists
	if (description && (description != ""))
	{
		// shorten description length to allowed amount
		var cDesc = shortenString(description, numDescriptionCharsPerImage);
		
		// add continuation string if shortened
		if (cDesc.length != description.length)
		{
			cDesc = cDesc + " ...";
		}
		
		// tidy description
		var tDesc = tidyDescription(cDesc);

		// add description to title
		cell.appendChild(document.createElement('br'));
		var span = document.createElement('span');

		span.className = 'geographDescription';
		cell.appendChild(span);
		span.innerHTML = tDesc;
	}
}

// add geograph details (image and title) to the specified row
function addGeographImageDetails(r, item)
{
	// add new image cell
	var cell = r.insertCell(-1);
	cell.className = 'geographImageCell';
	addImage(cell, item);
	
	// add a new title cell
	cell = r.insertCell(-1);
	cell.className = 'geographTitleCell';
	addTitle(cell, item);
}

// add row of images to table
function addRowOfImages()
{
	var t = document.getElementById('geographImagesTable');
	
	// add table row
	var r = t.insertRow(-1);
	r.className = 'geographImagesRow';
	
	// set number of images to load
	var toShow = geographData.length - unshownIndex;

	if (toShow > imagesPerRow)
		numToLoad = imagesPerRow;
	else
		numToLoad = toShow;
	
	var i = unshownIndex;
	var endIndex = i + numToLoad;
	while ( i < endIndex )
	{
		// get geograph metadata
		var item = geographData[i];

		// and add the details to the current row
		addGeographImageDetails(r, item);
		i++;
	}
	
	unshownIndex = i;

	// hide 'more' link when all images are shown
	if (unshownIndex >= geographData.length)
	{
		var gmid = document.getElementById('geographMoreId');
		gmid.style.visibility = 'hidden';
	}
}

// collapses the image table
function collapse()
{
	// call scriptaculous Effect to hide or show the images
	extWindow.Effect.toggle(geographDivTableId, 'slide', { duration: 0.5 });

	// toggle the collapse (or open) image
	var img = document.getElementById(collapseImgId);
	if (collapsed)
	{
		img.src = openedImg;
		collapsed = false;
	}
	else
	{
		img.src = closedImg;
		collapsed = true;
	}
}

// sort function to sort by distance field
function distanceSortFn(a, b)
{
	return (a.dist - b.dist);
}

// constants for distance calculation
var DEG_TO_RAD = 0.017453292519943295769236907684886;
var RADIUS = 6371000;

// returns distance in metres between two coordinates
function distanceBetweenCoords(lat1, lon1, lat2, lon2)
{
	var dLat = (lat2-lat1) * DEG_TO_RAD;
	var dLon = (lon2-lon1) * DEG_TO_RAD; 
	var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
	        Math.cos(lat1 * DEG_TO_RAD) * Math.cos(lat2 * DEG_TO_RAD) * 
	        Math.sin(dLon/2) * Math.sin(dLon/2); 
	var d = RADIUS * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
	return d;
}

// processes the data returned by geograph
function processGeographResponse(d)
{
	// set local attribute for access by all
	geographData = d;

	// process results (if there are any)
	var i = geographData.length;
	if (i)
	{
		while (--i >= 0)
		{
			// add distance field to data
			var item = geographData[i];
			var llat = item.wgs84_lat;
			var llon = item.wgs84_long;
			item.dist = distanceBetweenCoords(lat, lon, llat, llon);

			// change awkward html entities
			item.title = convertHtmlEntity(item.title);
			item.comment = convertHtmlEntity(item.comment);
			
			// convert dates to dd-mm-yyyy
			item.imagetaken = item.imagetaken.replace(/(\d+)\-(\d+)\-(\d+)/,"$3-$2-$1");
		}

		// sort results by distance from origin
		geographData.sort(distanceSortFn);

		// create geograph div section
		var gDiv = document.createElement('div');
		gDiv.id = 'geographDiv';

		// create header span
		var hdrSpan = document.createElement('span');
		hdrSpan.id = 'geographHeader';
		hdrSpan.innerHTML = '<br>' +
							'<a id="geographHeadingLinkId" href="' + geographHomePage + '" title="Go to ' + geographLink + '" target="_blank">Geograph Images</a> ' +
							'<span id="geographMoreId"> (<a href="#" onClick="addRowOfImages();return false;">more</a>)</span>' +
							'<span id="geographHeadingRightId">' +
								'<a id="geographIconLinkId" href="' + geographHomePage + '" title="Go to ' + geographLink + '" target="_blank"><img src="' + geographHomePage + 'favicon.ico"></a>' +
								'<a id="geographCollapseId" href="#" title="Click to Expand/Collapse" onClick="collapse();return false;"><img id="' + collapseImgId + '" src="' + openedImg + '"></a>' +
							'</span>';
		
		// add header span to geograph div
		gDiv.appendChild(hdrSpan);
		
		// add new table div id and sub div (required by scriptaculous Effect) for collapsible section
		var tDiv = document.createElement('div');
		tDiv.id = geographDivTableId;
		gDiv.appendChild(tDiv);
		var div = document.createElement('div');
		tDiv.appendChild(div);
		
		// create images table
		var t = document.createElement('table');
		t.id = 'geographImagesTable';
		var tb = document.createElement('tbody');
		t.appendChild(tb);

		// add table to sub div
		div.appendChild(t);

		// position geograph div before map (this should exist as we're only called if we're logged in and have coordinates)
		var mapDiv = document.getElementById(mapId);
		if (mapDiv)
		{
			mapDiv.parentNode.insertBefore(gDiv, mapDiv);

			// show the initial rows of images
			var i = defaultRowsToShow;
			while (i-- > 0)
			{
				addRowOfImages();
			}
		}
	}
}

// determines how many description characters can be displayed
function calcNumDescChars()
{
	var w = window.innerWidth; // page width
	var i = imagesPerRow;
	var r = (w/1024);
	var c = Math.floor(120 * (-2*r*r+9*r-6) * (0.2*i*i-1.6*i+3.4));
	if (c > 450)
	{
		c = 450;
	}
	else if (c < 10)
	{
		c = 10;
	}
		
	return c;
}

// get cache location
getLatLon();

if (isCoveredByGeograph())
{
	if (window.navigator.userAgent.match('Firefox'))
	{
		// for firefox
		extWindow = unsafeWindow;
		
		// get settings and register menus - only available for firefox
		getUserDefinedSettings();
		registerMenus();
	}
	else
	{
		// for opera
		extWindow = window;
	}

	// calculate max size of description
	numDescriptionCharsPerImage = calcNumDescChars();
	
	// set the required server and paths
	setGeographUrls();
	
	// set styles
	addGlobalStyle(style);

	// allow calls from web page to our greasemonkey script
	extWindow.addRowOfImages = function(){addRowOfImages();};
	extWindow.collapse = function(){collapse();};

	// create link for geograph request
	var apiLink = getApiUrl(searchDist, searchMaxImages);

	// get geograph json data
	extWindow.jQuery.getJSON(apiLink, function(d){processGeographResponse(d);});
}

})();
