/**
 * Base layer to support the various map styles and tools in CycleStreets.
 */


/*jslint browser: true, continue: true, sloppy: false, vars: true, white: false, plusplus: true, maxerr: 50, indent: 4 */


// Use an anonymous function to create the Cyclestreets namespace
(function () {

	// This is a hint for JSLint.
	// http://stackoverflow.com/questions/1335851/what-does-use-strict-do-in-javascript-and-what-is-the-reasoning-behind-it
	'use strict';

	// Create the Cyclestreets Namespace
	var CycleStreets = {

		/* Class properties */

		// Bound to the OpenLayers map
		map: null,

		// The document element in the requested xml file.
		doc: null,

		// A popup, placed in this class so that the itinerary class can access it #!# hacky!
		popup: null,

		// The debug HTML tag
		debugtag: null,

		// The supported map styles
		supportedMapStyles: null,


		/* Settings */

		// The baseUrl
		baseUrl: null,

		// The location of the theme, starting with / after baseUrl
		themeLocation: null,

		// http://www.cyclestreets.ibsen/tiles/
		localTileHost: null,

		// Whether to include extra controls for help debugging routing; permalinks provide useful links
		addDebugControls: false,

		// Whether to select a specific layer, and if so, which one; if false, the first in the list is used
		selectMapLayer: false,

		// List of visible layers to load
		visibleLayers: false,



		/* Methods */

		// Initialisation, assigning the settings
		initialize: function (baseUrl, themeLocation, localTileHost, addDebugControls, visibleLayersString, selectMapLayer) {

			// Assign the settings
			this.baseUrl				= baseUrl;
			this.themeLocation			= themeLocation;
			this.localTileHost			= localTileHost;
			this.addDebugControls		= addDebugControls;
			this.selectMapLayer			= selectMapLayer;

			// Convert the list of visible layers (which is a comma-separated string) into an array
			this.visibleLayers			= visibleLayersString.split(',');
		},


		// Function for creating the map
		createMap: function (divId, longitude, latitude, zoom, zoomBar, restrictExtent) {

			if (arguments.length < 5) {restrictExtent = false; }	// Default for 'restrictExtent' argument

			// Limit the initial zoom - to avoid whole world being shown. (This is temp fix).
			if (zoom > 17) {zoom = 17; }

			// Restrict the extent of the map? Default to unrestricted extent
			var restrictedExtent = null;
			if (restrictExtent) {
				/*global OpenLayers */
				restrictedExtent = new OpenLayers.Bounds();
				var bounds = restrictExtent.split(',', 4);	// String format is 'N,E,S,W'
				restrictedExtent.extend(this.lonLatToMercator(new OpenLayers.LonLat(bounds[3], bounds[2])));
				restrictedExtent.extend(this.lonLatToMercator(new OpenLayers.LonLat(bounds[1], bounds[0])));
			}

			// Image to display when tile is not available
			OpenLayers.Util.onImageLoadError = function () {
				/*global CS */
				this.src = CS.baseUrl + '/images/general/cyclestreets_more_soon.png';
			};

			// Map and layers
			// http://trac.openlayers.org/wiki/SphericalMercator
			// http://trac.openlayers.org/wiki/SettingZoomLevels
			this.map = new OpenLayers.Map(
				divId,
				{
					maxExtent: new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34),
					restrictedExtent: restrictedExtent,
					numZoomLevels: 19,
					maxResolution: 156543.0339,
					units: 'm',
					controls: [

						// Layer switcher
						new OpenLayers.Control.LayerSwitcher(),

						// Permalink is only really useful for experienced users.
						new OpenLayers.Control.Permalink('permalink'),

						// Map scale
						new OpenLayers.Control.ScaleLine(),

						// Attribution display
						new OpenLayers.Control.Attribution(),	//{div: document.getElementById("mapAttribution")}),

						// Zoom bar; PZ is a local subclass that provides only zoom in and out (and pan), but not 'zoom to whole world'.
						(zoomBar ? new OpenLayers.Control.PanZoomBar() : new OpenLayers.Control.PZ()),

						// Navigation
						new OpenLayers.Control.Navigation(),

						// Note: it seems this must remain commented in to ensure IE6 compatibility.
						// Quite why this is the case has yet to be established, as it is hard to debug JavaScript in IE.
						new OpenLayers.Control.MousePosition()
					],
					projection: new OpenLayers.Projection('EPSG:900913'),
					displayProjection: new OpenLayers.Projection('EPSG:4326'),
					eventListeners: {
						'changebaselayer': this.mapBaseLayerChanged
					}
				}
			);

			// Permalink controls, i.e. assocation of the map location URL with an ID elsewhere in the page
			this.addPermalinkControls();

			// Load the supported map styles
			this.supportedMapStyles = this.defineSupportedMapStyles();

			// Load the map tile layers
			this.loadMapStyles();

			// Switch to a specific layer if required
			if (this.selectMapLayer) {
				this.selectNamedBaseLayer(this.selectMapLayer);
			}

			// Set the centre-point
			var centre = CS.lonLatToMercator(new OpenLayers.LonLat(longitude, latitude));
			if (!this.map.getCenter() && centre) {
				this.map.setCenter(centre, zoom);
			}

			// Return the map
			return this.map;
		},


		// Function to add permalink controls, i.e. assocation of the map location URL with an ID elsewhere in the page
		addPermalinkControls: function () {

			// Potlatch 2 editor
			// #!# Why does this affect the main 'Permalink' link in the bottom-right of the map corner itself?
			this.map.addControl(new OpenLayers.Control.Permalink('editLink', CS.baseUrl + '/edit/editor/'));

			// Add the Editlink control, which can be useful. It needs to be added in the same circumstances that create the editlink element: see slippyMap::routingDebuggingLinks().
			if (this.addDebugControls) {

				// OSM website
				this.map.addControl(new OpenLayers.Control.Permalink('osmViewLink', 'http://www.openstreetmap.org/'));

				// Keep Right
				this.map.addControl(new OpenLayers.Control.Permalink('keepRightLink', 'http://keepright.ipax.at/report_map.php?ch30=0&ch40=1&ch50=1&ch60=1&ch70=1&ch90=0&ch100=0&ch110=0&ch120=1&ch130=1&ch150=1&ch160=1&ch170=1&ch180=1&ch190=1&ch200=1&ch210=1&ch220=1&ch191=1&ch192=1&ch193=1&ch194=1&ch201=1&ch202=1&ch203=1&ch204=1&show_ign=1&show_tmpign=1'));

				// OpenStreetBugs
				this.map.addControl(new OpenLayers.Control.Permalink('openStreetBugsLink', 'http://openstreetbugs.schokokeks.org/?layers=0B0T'));

				// Duplicate nodes map - http://wiki.openstreetmap.org/wiki/Duplicate_nodes_map
				this.map.addControl(new OpenLayers.Control.Permalink('dupeNodes', 'http://matt.dev.openstreetmap.org/dupe_nodes/?layers=BT'));
			}

			// POI permalinks
			this.addPermalinkSet('poilinks');
		},


		// Function to add permalinks to all links within a specified element id (which must have ids of their own, e.g. <p id="something"><a id="foo" href="bar">text</a>, <a id="foox" href="barx">text</a>, ...</p>)
		addPermalinkSet: function (elementId) {

			// End if not in use on this page
			if (!document.getElementById(elementId)) {return; }

			// Create a custom permalink setting that does not have layers= in it
			// See: http://trac.osgeo.org/openlayers/browser/trunk/openlayers/tests/Control/Permalink.html?rev=7986#L115
			var argParserClass = OpenLayers.Class(OpenLayers.Control.ArgParser, {
				CLASS_NAME: 'CustomArgParser'
			});
			var permalinkSettingLayerAgnostic = {
				argParserClass: argParserClass,
				createParams: function (center, zoom, layers) {
					var params = OpenLayers.Control.Permalink.prototype.createParams.apply(this, arguments);
					params.layers = undefined;	// Remove layers= specification
					return params;
				}
			};

			// Create the permalink (without the layers= in it) for each link in the list
			var links = document.getElementById(elementId).getElementsByTagName('a');	// http://onlinetools.org/articles/unobtrusivejavascript/chapter2.html is a useful introduction to this stuff
			var i;
			for (i = 0; i < links.length; i++) {
				var currentLink = links[i];
				if (currentLink.id && currentLink.href) {
					this.map.addControl(new OpenLayers.Control.Permalink(currentLink.id, currentLink.href, permalinkSettingLayerAgnostic));
				}
			}
		},


		// Function to load the map tile layers
		loadMapStyles: function () {

			// Load each layer, if it exists
			var i;
			for (i = 0; i < this.visibleLayers.length; i++) {
				if (this.supportedMapStyles[this.visibleLayers[i]]) {
					this.map.addLayer(this.supportedMapStyles[this.visibleLayers[i]]);
				}
			}
		},


		// Function to define the supported map styles
		// Consider adding more layers as per the examples at http://trac.openlayers.org/browser/trunk/openlayers/examples/spherical-mercator.html
		// Use buffer: 0 to minimize the surrounding tiles loaded
		defineSupportedMapStyles: function () {

			// Create an array to hold the available map styles
			var supportedMapStyles = [];

			// OpenCycleMap; now served via a local URL
			supportedMapStyles.opencyclemap = new OpenLayers.Layer.OSM(
				'OpenCycleMap (shows hills)',
				this.localTileHost + 'opencyclemap/${z}/${x}/${y}.png',
				{
					attribution: '<span class="withmapkeylink">(c) OpenStreetMap and contributors, CC-BY-SA; OpenCycleMap <a class="mapkey" href="' + CS.baseUrl + '/journey/help/faq/#mapkey"><strong>Map key</strong></a></span>',
					numZoomLevels: 18,
					buffer: 0
				}
			);

			// OSM main site's style
			supportedMapStyles.osmmainsite = new OpenLayers.Layer.OSM(
				'OpenStreetMap default style',
				[	'http://a.tile.openstreetmap.org/${z}/${x}/${y}.png',
					'http://b.tile.openstreetmap.org/${z}/${x}/${y}.png',
					'http://c.tile.openstreetmap.org/${z}/${x}/${y}.png'],
				{
					attribution: '(c) OpenStreetMap and contributors, CC-BY-SA',
					buffer: 0
				}
			);

			// MapQuest Open style
			supportedMapStyles.mapquestopen = new OpenLayers.Layer.OSM(
				'MapQuest Open style',
				[	'http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png',
					'http://otile2.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png',
					'http://otile3.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png',
					'http://otile4.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png'],
				{
					attribution: '(c) MapQuest, OpenStreetMap and contributors, CC-BY-SA ',
					buffer: 0
				}
			);

			// OS Open Data (Street View) - http://wiki.openstreetmap.org/wiki/Ordnance_Survey_Opendata
			supportedMapStyles.osopendata = new OpenLayers.Layer.OSM(
				'OS Open Data',
				[	'http://a.os.openstreetmap.org/sv/${z}/${x}/${y}.png',
					'http://b.os.openstreetmap.org/sv/${z}/${x}/${y}.png',
					'http://c.os.openstreetmap.org/sv/${z}/${x}/${y}.png'],
				{
					attribution: 'Contains Ordnance Survey data (c) Crown copyright and database right 2010',
					buffer: 0
				}
			);

			// Register the uncluttered style if required
			supportedMapStyles.uncluttered = new OpenLayers.Layer.OSM(
				'Simple style',	// 27911 is by @tom_chance
				[	'http://a.tile.cloudmade.com/8bafab36916b5ce6b4395ede3cb9ddea/27911/256/${z}/${x}/${y}.png',
					'http://b.tile.cloudmade.com/8bafab36916b5ce6b4395ede3cb9ddea/27911/256/${z}/${x}/${y}.png',
					'http://c.tile.cloudmade.com/8bafab36916b5ce6b4395ede3cb9ddea/27911/256/${z}/${x}/${y}.png'],
				{
					attribution: '(c) OpenStreetMap and contributors, CC-BY-SA; Map images (c) <a href="http://www.cloudmade.com/">CloudMade</a>',
					buffer: 0
				}
			);

			// CycleStreets data tiles
			supportedMapStyles.cyclestreetsdata = new OpenLayers.Layer.OSM(
				'CycleStreets data layer',
				this.localTileHost + 'cyclestreets/${z}/${x}/${y}.png',
				{
					attribution: 'Provided by <a href="http://www.cyclestreets.net/">CycleStreets</a>',
					numZoomLevels: 22,
					buffer: 0
				}
			);

			// Google map
			/*global google */
			if (typeof google !== 'undefined') {	// Only run this if the google code exists
				supportedMapStyles.googlemap = new OpenLayers.Layer.Google(
					'Google map',
					{'sphericalMercator': true}
				);
			}

			// Google satellite view
			if (typeof google !== 'undefined') {	// Only run this if the Google Maps code exists
				if (typeof google.maps !== 'undefined') {	// Do not combine this with the previous line as otherwise IE6 will not load Gmaps layers
					supportedMapStyles.googlesatellite = new OpenLayers.Layer.Google(
						'Google satellite',
						{type: google.maps.MapTypeId.SATELLITE, 'sphericalMercator': true}
					);
				}
			}

			// Bing Maps (formerly 'Virtual Earth'); requires this script to be loaded: http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.3
			if (typeof VEMapStyle !== 'undefined') {	// Only run this if the Bing Maps code exists
			    supportedMapStyles.bingAerial = new OpenLayers.Layer.VirtualEarth(
					'Bing Aerial',
				    {type: VEMapStyle.Aerial, 'sphericalMercator': true}
			    );
			}

			// Return the list of styles
			return supportedMapStyles;
		},


		// Function to return the internal name of a map style when given its OpenLayers name
		mapStyleId: function (OpenLayersName) {

			// Loop through each loaded style and find the name
			var mapLayerIdentifier;
			for (mapLayerIdentifier in this.supportedMapStyles) {
				if (OpenLayersName === this.supportedMapStyles[mapLayerIdentifier].name) {
					return mapLayerIdentifier;
				}
			}

			// Not found
			return false;
		},


		// A null function that is redefined by some pages.
		mapBaseLayerChanged: function (event) {
			// Nothing
		},


		// Select a named base layer
		selectNamedBaseLayer: function (mapLayerIdentifier) {

			// End if no specific layer specified
			if (!mapLayerIdentifier) {return; }
			// alert('Setting layer named: ' + mapLayerId);

			// Convert the ID to the layer name as understood by OpenLayers
			if (!this.supportedMapStyles[mapLayerIdentifier]) {return; }
			var name = this.supportedMapStyles[mapLayerIdentifier].name;
			// alert(name);

			// Select the layer, if it is loaded
			var len = this.map.layers.length;
			var i;
			for (i = 0; i < len; i++) {
				var layer = this.map.layers[i];
				if (!layer.isBaseLayer) {continue; }
				if (name === layer.name) {
					CS.map.setBaseLayer(layer);
					break;
				}
			}
		},


		// In debug mode a debug tag is created that can take these messages.
		debugtagmsg: function (msg) {

		    // Null signals to initialise the debug tag, if it can be found
		    if (this.debugtag === null) { this.debugtag = document.getElementById('debug') || false; }

		    // False implies debugging is off
		    if (!this.debugtag) { return; }

		    // Prepend the msg to the tag
		    this.debugtag.innerHTML = msg + '<br />' + this.debugtag.innerHTML;
		},

		// Marker tag
		marker_tag: function (color, size) {
			return '<img class="marker_tag" src="' + CS.baseUrl + CS.themeLocation + 'mm_' + size + '_' + color + '.png" alt="' + color + ' marker" />';
		},


		// Icon
		icon_src: function (icon) {
			return CS.baseUrl + '/images/icons/' + icon + '.png';
		},


		// Icon tag; very similar to meta::icon()
		icon_tag: function (icon, altText, title) {
			altText = altText || 'Icon: ' + icon;
			title = title || altText;
			return '<img src="' + this.icon_src(icon) + '" border="0" alt="' + altText + '" title="' + title + '">';
		},


		// Helper function to ensure zoomed into suitable position
		focusLonLat: function (saytResult) {

			// Can't use 'this' because its called from outside, so use CS instead.
			var ll = CS.lonLatToMercator(new OpenLayers.LonLat(saytResult.longitude, saytResult.latitude));

			if (CS.map.getZoom() < 16) {
				CS.map.moveTo(ll, 16);
			} else {
				CS.map.panTo(ll);
			}
		},


		// Convert mercator to lon/lat
		mercatorToLonLat: function (merc) {
			var lon = (merc.lon / 20037508.34) * 180;
			var lat = (merc.lat / 20037508.34) * 180;

			lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);

			return new OpenLayers.LonLat(lon, lat);
		},


		// Convert lon/lat to mercator
		lonLatToMercator: function (ll) {
			var lon = ll.lon * 20037508.34 / 180;
			var lat = Math.log(Math.tan((90 + ll.lat) * Math.PI / 360)) / (Math.PI / 180);

			lat = lat * 20037508.34 / 180;

			return new OpenLayers.LonLat(lon, lat);
		},


		// Scale to zoom
		scaleToZoom: function (scale) {
			return Math.log(360.0 / (scale * 512.0)) / Math.log(2.0);
		},



		/* Helper functions */


		// Rounds numbers to 6 figures to make them more readable
		r6: function (x) {
			return Math.round(1000000 * x) / 1000000;
		},


		// Rounds numbers to 7 figures to make them more readable
		r7: function (x) {
			return Math.round(10000000 * x) / 10000000;
		},


		// Entity-safe string encoding 
		// !! This function is temporarily restored to fix issue #557.
		htmlspecialchars: function (string) {
		    return string ? string.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : '';
		},

		// Convert newlines to HTML linebreaks; from http://stackoverflow.com/questions/2919337
		nl2br: function (str, is_xhtml) {
		    if (!str) {return ''; }
		    var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
		    str = str.toString();	// Presumably to ensure it is a string
		    return (str).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + breakTag + '$2');
		}
	};


	// Bind Cyclestreets in this particular place - to avoid polluting the global namespace. (This is the same trick used by OpenLayers.)
	window['http://www.cyclestreets.net/schema/xml/'] = CycleStreets;

})();


// Setup CS object
var CS = window['http://www.cyclestreets.net/schema/xml/'];

//// Ends

