if(mapengine=="ajax") {
	$(document).ready(function() {
	
		function ajaxError(data) {
			if( data != undefined )
				$('.ajaxError .messageContainer').html(data);
				
			$('.ajaxError').show();
			$('.ajaxError')
			 	.animate({opacity: 1.0}, 3000)
			 	.slideUp('fast');
	    }
		
		$('.ajaxstatus').bind('ajaxStart', function() {
			$('.loading', this).removeClass('hidden');
			$('.ajaxstatus').show();
		}).bind('ajaxStop', function() {
			$('.loading', this).addClass('hidden');
			$('.ajaxstatus').hide();
		}).bind('ajaxError', ajaxError);
	});
}

/**
 * Legacy code being updated:
 */
 
/**
 * NOTICE!
 * This is quite dump... the exactly same code is copypasted to commons.js
 */
function CustomMapTypeControl() {}
CustomMapTypeControl.prototype = new GControl();
CustomMapTypeControl.prototype.initialize = function(map) {

  /*
   * Following variables are set in variables.js
   * var buttonWidthMap;
   * var buttonWidthSatellite;
   * var buttonWidthHybrid;
   * var buttonWidthTerrain;
   * var backgroundWidth
   * 
   * var backgroundMSHT;
   * 
   * var backgroundMap;
   * var backgroundSatellite;
   * var backgroundHybrid;
   * var backgroundTerrain;
   */

  var margin = 4;
  var startPosition = 5;
  var leftPosition = startPosition; // Start position, left margin.

  var map_type_container = $('<div/>').attr('id', "typeControl")
		.css('background', 'url(' + backgroundMSHT + ') repeat-x top left')
		.css('width', backgroundWidth + 'px').css('height', '31px')
		.css('border-bottom', '1px solid #888')
		.css('border-right', '1px solid #888');

  map_type_container = map_type_container[0];

  var mapDiv = document.createElement("div");
  map_type_container.appendChild(mapDiv);
  mapDiv.style.width = buttonWidthMap + "px";
  mapDiv.style.height = "24px";
  mapDiv.style.background = "url(" + backgroundMap + ") no-repeat";
  mapDiv.style.position = "absolute";
  mapDiv.style.left = leftPosition + "px";
  mapDiv.style.top = "4px";
  mapDiv.style.cursor = "pointer";
  GEvent.addDomListener(mapDiv, "click", function() {
    map.setMapType(G_NORMAL_MAP);
  });
  leftPosition += buttonWidthMap + margin;
  
  

  var satelliteDiv = document.createElement("div");
  map_type_container.appendChild(satelliteDiv);
  satelliteDiv.style.width = buttonWidthSatellite + "px";
  satelliteDiv.style.height = "24px";
  satelliteDiv.style.background = "url(" + backgroundSatellite + ") no-repeat";
  satelliteDiv.style.position = "absolute";
  satelliteDiv.style.left = leftPosition + "px";
  satelliteDiv.style.top = "4px";
  satelliteDiv.style.cursor = "pointer";
  GEvent.addDomListener(satelliteDiv, "click", function() {
    map.setMapType(G_SATELLITE_MAP);
  });
  leftPosition += buttonWidthSatellite + margin;

  var hybridDiv = document.createElement("div");
  map_type_container.appendChild(hybridDiv);
  hybridDiv.style.background = "url(" + backgroundHybrid + ") no-repeat";
  buttonWidth = 42;
  hybridDiv.style.width = buttonWidthHybrid + "px";
  hybridDiv.style.height = "24px";
  hybridDiv.style.position = "absolute";
  hybridDiv.style.left = leftPosition + "px";
  hybridDiv.style.top = "4px";
  hybridDiv.style.cursor = "pointer";
  GEvent.addDomListener(hybridDiv, "click", function() {
    map.setMapType(G_HYBRID_MAP);
  });
  leftPosition += buttonWidthHybrid + margin;
  
  var terrainDiv = document.createElement("div");
  map_type_container.appendChild(terrainDiv);
  terrainDiv.style.background = "url(" + backgroundTerrain + ") no-repeat";
  buttonWidth = 44;
  terrainDiv.style.width = buttonWidthTerrain + "px";
  terrainDiv.style.height = "24px";
  terrainDiv.style.position = "absolute";
  terrainDiv.style.left = leftPosition + "px";
  terrainDiv.style.top = "4px";  	
  terrainDiv.style.cursor = "pointer";
  GEvent.addDomListener(terrainDiv, "click", function() {
    map.setMapType(G_PHYSICAL_MAP);
  });
  leftPosition += buttonWidthTerrain + margin;

  map.getContainer().appendChild(map_type_container);
  return map_type_container;		
}

function CustomZoomControl() {}
CustomZoomControl.prototype = new GControl();
CustomZoomControl.prototype.initialize = function(map) {
  var container = document.createElement("div");
  container.style.background="url(../images/map_icons/button-bg.gif) repeat-x top left";
  container.style.width = "85px";
  container.style.height = "31px";
  container.style.borderBottom = "1px solid #888";
  container.style.borderRight = "1px solid #888";

  container.id = "zoomControl";

  var zoomInDiv = document.createElement("div");
  container.appendChild(zoomInDiv);
  zoomInDiv.style.background = "url(../images/map_icons/zoom_in_button.png) no-repeat";
  zoomInDiv.style.width = "24px";
  zoomInDiv.style.height = "24px";
  zoomInDiv.style.position = "absolute";
  zoomInDiv.style.left = "56px";
  zoomInDiv.style.top = "4px";
  zoomInDiv.style.cursor = "pointer";
  GEvent.addDomListener(zoomInDiv, "click", function() {
    map.doZoomIn();
  });

  var zoomOutDiv = document.createElement("div");
  container.appendChild(zoomOutDiv);
  zoomOutDiv.style.width = "24px";
  zoomOutDiv.style.height = "24px";
  zoomOutDiv.style.background = "url(../images/map_icons/zoom_out_button.png) no-repeat";
  zoomOutDiv.style.position = "absolute";
  zoomOutDiv.style.left = "5px";
  zoomOutDiv.style.top = "4px";
  zoomOutDiv.style.cursor = "pointer";
  GEvent.addDomListener(zoomOutDiv, "click", function() {
    map.zoomOut();
  });

  map.getContainer().appendChild(container);
  return container;
}

function CustomHelpControl() {}
CustomHelpControl.prototype = new GControl();
CustomHelpControl.prototype.initialize = function(map) {

	var container = document.createElement("div");
	container.style.width = "25px";
	container.style.height = "41px";
	container.id = "helpControl";
	
	var helpQuestionMark = $("<img />")
		.attr("id", "helpQuestionMark")
		.attr("src", contextPath + "/images/search_questionmark.png")
		.css("display", "block")
		.css("margin", "11px 3px");
	
	// Add tooltip
	$(helpQuestionMark).attr("title", mapZoomHelpText);
	$(helpQuestionMark).tooltip();
	
	$(container).append(helpQuestionMark);
	
	// Add message box component
	var messageBox = document.createElement("div");
	messageBox.id = "mapMessageBox";
	map.getContainer().appendChild(messageBox);
	$('#mapMessageBox').attr("class", "mapMessageBox");

	map.getContainer().appendChild(container);
	return container;
}

/**
* Display message in a box above the map.
*/
GMap2.prototype.displayMessage = function(message) {
	$('#mapMessageBox').html(message);
	$('#mapMessageBox').show(1000);
}

/**
* Hide message box.
*/
GMap2.prototype.hideMessage = function() {
	$('#mapMessageBox').hide(1000);
}

/**
 * Zoom map in. PreZoomin is skipped in dashboard because all
 * the six tracks have already all the points needed to draw the map
 */
GMap2.prototype.doZoomIn = function() {
	var zoomlevel = this.getZoom() + 1;
	
	if(this.trackList != null){
		//Sweep through tracklines and hide those which will be redrawn anyway.
		for(var i = 0; i < this.trackList.length ; i++ ) {
			var track = this.trackList[i];
			track.preZoom(zoomlevel)
		}
	}
	
	this.zoomIn();
}

/**
 * Creates a filtered polyline
 *
 * Returns the created polyline or null if polyline was unable to create.
 * Polyline can be unable to create if array does not contain any proper location
 * points (0,0 is not a proper point for example)
 *
 * @return 
 */
GMap2.prototype.createPolyLine = function(arr, settings, colorSettings) {
	logger.debug("createPolyline [map-api.js]");
	if(arr == null){
		return;
	}
	 
	var coord = new Array(), icon;
	var zoom = this.getZoom();
	
	// Find first coordinate. Skip point 0,0
	// This fix prevents map to draw workout route to start from point 0,0 if
	// workout has bad gps fix.
	var startPoint = 0;
	var oldCoord;
	for(var i = startPoint; i < arr.length-1; i++){
		startPoint++;
		oldCoord = new F.createPosition(arr[i]);
		if(oldCoord.lat() != 0 || oldCoord.lng() != 0){
			break;		
		}
	}
	coord.push(oldCoord);
	
	// QUICK FIX 3.7.09 after community page crash...
	// This should work but the fix is made very quickly
	if(oldCoord == null){
		// oldCoord is null which means that all the points were 0,0
		logger.warn("oldCoord is null. Do not draw the polyline");
		return null;
	}
	
	//<-- Filter coordinates
	var projection = this.getCurrentMapType().getProjection();
				
	
	
	var oldPoint = projection.fromLatLngToPixel(oldCoord, zoom)
	var jump = 1;

	var bounds = this.getBounds();
	var crossSection = bounds.getSouthWest().distanceFrom(bounds.getNorthEast());
			
	for (var i = startPoint; i < arr.length-1; i+=jump) {
		var newCoord = F.createPosition(arr[i]);
		var newPoint = projection.fromLatLngToPixel(newCoord,  zoom);
		var tempX = (oldPoint.x - newPoint.x);
		var tempY = (oldPoint.y - newPoint.y);
		 
		//var distanceSquared = 	Math.pow(oldPoint.x - newPoint.x, 2) + 
		//  						Math.pow(oldPoint.y - newPoint.y, 2);
		var distanceSquared = tempX * tempX + tempY * tempY;
		  						
		if( distanceSquared >= 25) {
			oldPoint = newPoint;
			coord.push( oldCoord );
		}
		oldCoord = newCoord;
		//coord.push( F.createPosition(arr[i][0], arr[i][1]) );
	}
	coord.push(F.createPosition(arr[arr.length-1]));
	// Filter coordinates --!>
	
	var colorManager = this.colorManager;
	var color = colorManager.acquire(colorSettings);
	var poly = new GPolyline(coord, color, settings.width, settings.alpha);
	poly.color = color;
	GEvent.addDomListener(poly, "remove", function() {
		colorManager.release(color);
	})
	return poly;
}

/**
 * The API:
 */

;(function($) {
	$.NST = {
		defaults: {
			query: {
				tracks: {
					location: '/nts/json/query.do',
					data: {
						key: "12345",
						count: 5,
						//callback: "?",
						order: "TIME",
						// for query.do
						personFilter: "everybody",
						orderBy: 0,
						maxResults: 40,
						firstResult: 0,
						ph: "n"
					}
				}
			},
			map: {
				autozoom: true,
				useMapArea: true,
				tracks: "default", 
				colors: ["#e44046", "#53b6f1", "#b61ca8", "#e0a033",  "#0e718a", "#cd591f","#43bfa5", "#bcc931",  "#734bf7", "#8a603a"]
			},
			track: {
				line: true,
				start: true,
				end: false
			}
		},
		apikey: function(key) {
			$.NST.defaults.query.tracks.data.publickey = key;
		}
	}
	
	/**
	 * Methods to work with legacy code which
	 * should be replaced with new implementations.
	 */
		
	var icons = F.icons;
	var createPosition = F.createPosition;	
	var createIcon = function(name) {
		if( isNaN(name) ) {
			var icon = F.createIcon(name);
			icon.name = name;
			return icon;
		} else {
			return createNumberIcon(name);
		}
	}
	
	/**
	 * Create map number icon
	 * 
	 * If USE_SPRITE is true, number icon is made by using icon sprite. 
	 * Icon sprite is undocumented and thus unoffically supported feature 
	 * by Google.
	 * 
	 * Turn USE_SPRITE to false it Google stops supporting this feature or 
	 * other weird things start happening to fall back to separate icons 
	 * 
	 * More about sprites:
	 * Google Maps sprites: http://esa.ilmari.googlepages.com/sprite.htm
	 * Google Maps sprites: http://esa.ilmari.googlepages.com/hugesprite.htm
	 * Sprite generator: http://spritegen.website-performance.org/
	 */
	var createNumberIcon = function(number) {
		var USE_SPRITE = true;
		
		var icon;
		
		if(!USE_SPRITE){
			icon = new GIcon();
			icon.image = "../images/map_icons/" + number + ".png";
		} else {
			// Without G_DEFAULT_ICON IE6 transparency is not working for 
			// sprites
			icon = new GIcon(G_DEFAULT_ICON);
			
			// Count the image position on the sprite
			var iconHeight = 37;
			var iconWidth = 32;
			var offset = 30;
			var topPosition = ((number - 1) * iconHeight) + (number * offset);
			icon.sprite = {
					image:"../images/sprites/map_number_sprite.png", 
					top: topPosition
			}
		}
		icon.shadow = "";
		icon.shadow = "../images/map_icons/shadow.png";
		icon.iconSize = new GSize(iconWidth, iconHeight);
		icon.shadowSize = new GSize(40, 45);
		icon.iconAnchor = new GPoint(0, iconHeight);
		icon.infoWindowAnchor = new GPoint(0, 0);
		icon.name = number;
		return icon;
	}
	
	/**
	 * Minmax object is used to detect the bounding box of a Track.
	 */
	function MinMaxObject() {
		this.max_lat = -1000;
		this.max_long = -1000;
		this.min_lat = 1000;
		this.min_long = 1000;
	
		this.update = function(coords) {
			var me = this;
			$.each(coords, function(key, coord) {
				var coord = F.createPosition(coord);
				if (coord.lat() > me.max_lat) {
					me.max_lat = coord.lat();
				}
				
				if (coord.lat() < me.min_lat) {
		      		me.min_lat = coord.lat();
				}
		
		  	    if (coord.lng() > me.max_long) {
					me.max_long = coord.lng();
		  	    }
		  	    
		    	if (coord.lng() < me.min_long) {
		      		me.min_long = coord.lng();
				}
			});
		}
		
		this.getBoundingBox = function() {
			return new GLatLngBounds(
				F.createPosition(this.min_lat, this.min_long),
				F.createPosition(this.max_lat, this.max_long));
		}
		this.getBounds = this.getBoundingBox;
	}
	
	function openWorkout(id) {
		window.open( "../workoutdetail/index.do?id=" + id );
	}
	
	function launchMap(element) {
   		var map = new GMap2(element);
		map.addControl(new CustomZoomControl(), new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(66, 5)));
		map.addControl(new CustomMapTypeControl(), new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(161, 5)));
		map.addControl(new CustomHelpControl(), new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(348, 0)));

		// Add IE6 transparency
		
		if($.ifixpng) {
			/*
			$("#zoomControl").ifixpng();
			$("#typeControl").ifixpng();
			IFix brokes click event, so it cannot be used on those buttons.
			*/ 
	    	$("#helpQuestionMark").ifixpng();
	    }

        map.enableScrollWheelZoom()

        GEvent.addListener(map, "doubleclick", function() {map.doZoomIn()} );
		
		//var bounds = new GLatLngBounds( new GLatLng(-90, -180),	new GLatLng(90, 180) );
	   	var center = new GLatLng(32, 15)
	   	map.setCenter(center, 1, G_PHYSICAL_MAP);
        return map;
	}
	
	/**
	 * Color manager definition: color manager can give tracks color to use
	 * for overlays (and hopefully icons in the future).
	 */
	 var NColorManager = function(colors, defaultColor) {
	 	this.colors = colors;
	 	this.defaultColor = defaultColor;
	 	this.used = new Array(colors.length);
	 	for( var i = 0; i < this.used.length; i++ ) {
	 		this.used[i] = false;
	 	}
	 }
	 $.extend(NColorManager.prototype, {
	 	acquire: function(index) {
	 		if( isNaN(index) ) {
		 		if( index == true ) {
		 			for( var i = 0; i < this.used.length; i++ ) {
		 				if( this.used[i] == false ) {
		 					this.used[i] = true;
		 					this.colors[i];
		 				}
		 			}
		 			return this.defaultColor
		 		} else {
		 			return index
		 		}
	 		}
	 		else { //When it is a number.
	 			this.used[index-1] = true;
	 			return this.colors[index-1]
	 		}
	 	},
	 	get: function(index) {
	 		if( isNaN(index) ) {
	 			return index;
	 		} else {
	 			return this.colors[index-1];
	 		}
	 	},
	 	release: function(color) {
	 		if( color == this.defaultColor )
	 			return;
	 		for( var i = 0; i < this.colors.length; i++ ) {
	 			if( this.colors[i] == color ) {
	 				this.used[i] = false;
	 				return;
	 			}
	 		}
	 		throw "Attempting to release an undefined color: " + color;
	 	}
	 });
	
	/**
	 * Track Definition
	 */
	 
	 var NTrack = function(settings) {
	 	this.overlays = {};
	 }
	
	/**
	 * Add on overlay with given name to this NTrack.
	 */
	NTrack.prototype.overlay = function(name, overlay) {
		var map = this.map;
		if( this.overlays[name] != undefined ) {
			map.removeOverlay(this.overlays[name]);
		}
		map.addOverlay(overlay);
		this.overlays[name] = overlay;
	}
	
	/**
	 * Check if this NTrack needs to have it's line redrawn.
	 * A line needs to be redrawn if either it's color changes or
	 * the map has been zoomed significantly since the last time
	 * the line was drawn. This is particularly useful to check before
	 * zooming in or out so that Google Maps api does not need to handle
	 * the line drawing twice.
	 * 
	 * @zoom The map zoom level to test against.
	 */
	NTrack.prototype.needLineRedraw = function(zoom, color) {
		var overlay = this.overlays.line;
		// logger.debug("Checking if line need redraw [zoom: " + zoom + ", overlay.zoom: " + overlay.zoom + "]");
		if( Math.abs(zoom - overlay.zoom) > 2 ) {
			logger.debug("Line needs redraw");
			return true;
		}
		if( color != undefined && this.map.colorManager.get(color) != overlay.color )
			return true;
		return false;
	}
	
	/**
	 * Check if this NTrack needs to have it's icon overlay redrawn.
	 * @param overlayName Name of the marker to check.
	 * @param iconName Name of the icon to check.
	 */
	NTrack.prototype.needIconRedraw = function(overlayName, iconName) {
		var overlay = this.overlays[overlayName];
		if( overlay == undefined ) return true;
		return this.overlays[overlayName].name != iconName;
	}
	
	NTrack.prototype.line = function(lineSettings, colorSettings) {
		var map = this.map;
		if( this.overlays.line == undefined ) {
			if( lineSettings == false )
				// lineSettings is true only for the first 10 workouts
				return;
			var overlay = map.createPolyLine(this.coords, lineSettings, colorSettings);
			if(overlay != null){
				overlay.zoom = map.getZoom();
				map.addOverlay( overlay );
				this.overlays.line = overlay;
			}
		}
		else {
			var overlay = this.overlays.line;
			if( lineSettings == false ) {
				map.removeOverlay(overlay);
				this.overlays.line = undefined;
			} else if( this.needLineRedraw(map.getZoom(), colorSettings) ) {
				map.removeOverlay(overlay);
				var overlay = map.createPolyLine(this.coords, lineSettings, colorSettings);
				if(overlay != null){
					overlay.zoom = map.getZoom();
					map.addOverlay( overlay );
					this.overlays.line = overlay;
				}
			}
		}
	}
	
	NTrack.prototype.preZoom = function(level) {
		var map = this.map;
		var overlay = this.overlays.line;
		if( overlay != undefined && this.needLineRedraw(level) ) {
			map.removeOverlay(overlay);
			this.overlays.line = undefined;
		}
	}
	
	/* AJAX states */
	var UPDATE_STATE = {
		NONE: 0,
		WAITING: 1,
		PENDING: 2
	}
	
	/**
	 * TrackSource definition. NTrackSource is used to communicate with
	 * the Sportstracker server. It keeps a local cache of workouts which
	 * is used to reduce communication overhead. Caching works like this:
	 * The server keeps a cache of the JSON response for each Workout. The
	 * cache is kept for a configurable time (which should be significantly
	 * shorter for Live workouts).
	 * 
	 * Additionally, each time a map screen is opened, the javascript generates
	 * a random "scriptSession" id which is in turn used by server to
	 * determine what workouts the current JavaScript should already
	 * have available. When a workout data is already available
	 * to the script, instead of sending the entire workout data, the server
	 * only sends the workout ID. This should result in significantly reduced
	 * traffic between the client and the server.
	 *
	 * Additionally NTrackSource actively tracks update state - it won't make
	 * a new request until the previous one has finished (to prevent the
	 * server from erraneously assuming client would have a workout
	 * available to it).
	 */
	var NTrackSource = function(settings) {
		this.settings = settings;
		this.cache = [];
		this.cache.session = Math.random() + new Date().getUTCMilliseconds();
		this.updateStatus = UPDATE_STATE.NONE;
		
		var me = this;
		$("body").bind("ajaxStop", function() {
				if( me.updateStatus == UPDATE_STATE.PENDING ) {
					me.updateStatus = UPDATE_STATE.NONE;
					var pending = me.pendingTask;
					//alert(pending.params + " hey " + pending.callback)
					me.pendingTask = undefined;
					window.setTimeout(function() {
						me.load( pending.params, pending.callback );
					}, 0);
				} else {
					me.updateStatus = UPDATE_STATE.NONE;
				}
			}).bind("ajaxStart", function() {
				me.updateStatus = UPDATE_STATE.WAITING;
			})
	};
	
	$.extend(NTrackSource.prototype, {
		load: function(params, callback) {
			var me = this;
			if( this.updateStatus != UPDATE_STATE.NONE ) {
				this.pendingTask = {params: params, callback: callback}
				this.updateStatus = UPDATE_STATE.PENDING;
				return;
			}

			if( callback == undefined ) {
				callback = params;
			}
		
			var data = {};
			$.extend(data, this.settings.data, params, {scriptSession: this.cache.session});
			var cache = this.cache;
			jQuery.getJSON(this.settings.location, data, function(tracks) {
				var temp = $.extend([], tracks);
				var cacheUpdated = 0;
				$.each(tracks, function(index, track) {
					if( isNaN(track) ) {	//Is an actual track.
						/*
						 * Track needs cache update if:
						 * - it is not yet cached
						 * - coors length increased
						 */
						var needsCacheUpdate;
						if(cache[track.id] == undefined || cache[track.id] == null){
							// Not yet cached. Needs caching
							needsCacheUpdate = true;
						} else if(track.coords != null) {
							// New track contains coordinate information. Check if it has 
							// more detailed information than the cached track
							var cachedTrack = cache[track.id];
							if(cachedTrack.coords == null){
								// Cached track does not contain coordinate information at all
								// It definitely needs to be updated
								needsCacheUpdate = true;
							} else {
								if(cachedTrack.coords.length < track.coords.length){
									// New track has more coordinate points than the old 
									// cached track. Update cache.
									needsCacheUpdate = true;
								}
							}
						} else {
							needsCacheUpdate = false;
						}
						
						if(needsCacheUpdate){
							cache[track.id] = track;
							cacheUpdated++;	// this is just for logging
							if( track.startPosition.coords.lat == 0 && track.startPosition.coords.lon == 0) {
								track.startPosition = null;
							}
							tracks[index] = track;
						}
					} else {				//Is only the ID of a track.
						tracks[index] = cache[track];
					}
					$.extend(track, new NTrack());
				});
				if(cacheUpdated > 0){
					logger.info(cacheUpdated + " workouts updated to cache");
				}
				$.extend(tracks, new NTrackList());
				callback(tracks);
			});
		},
		orderBy: function(order) {
			if( this.settings.data.orderBy != order ) {
				this.settings.data.orderBy = order;
				return true;
			}
		},
		nextPage: function() {
			this.settings.data.firstResult += this.settings.data.maxResults;
		},
		previousPage: function() {
			if( this.settings.data.firstResult >= this.settings.data.maxResults) {
				this.settings.data.firstResult -= this.settings.data.maxResults;
			} else {
				this.settings.data.firstResult = 0;
			}
		},
		maxResults: function(max) {
			if( max == undefined ) {
				return this.settings.data.maxResults;
			} else {
				this.settings.data.maxResults = max;
			}
		},
		page: function(page) {
			if( page == undefined ) {
				return this.settings.data.firstResult / this.settings.data.maxResults + 1;
			} else {
				this.settings.data.firstResult = (page-1) * this.settings.data.maxResults;
			}
		},
		filters: function(settings) {
			$.extend(this.settings.data, settings);
		}
	});
	
	/**
	 * TrackList defition. Tracklist is an ordered list of tracks.
	 */
	 var NTrackList = function(array) {
	 }
	 NTrackList.prototype = new Array();
	 /**
	  * Finds out if this track list contains this particular track.
	  */
	 NTrackList.prototype.contains = function(track) {
	 	for(var i = 0; i < this.length; i++ ) {
	 		if( this[i] != undefined && this[i].id == track.id ) {
	 			return true;
	 		}
	 	}
	 	return false;
	 }
	 NTrackList.prototype.remove = function(track) {
	 	for(var i = 0; i < this.length; i++ ) {
	 		if( this[i] != undefined && this[i].id == track.id ) {
	 			this.splice(i,1);
	 		}
	 	}
	}

	NTrackList.prototype.get = function(track) {
		for(var i = 0; i < this.length; i++ ) {
	 		if( this[i] != undefined && this[i].id == track.id ) {
	 			return this[i];
	 		}
	 	}
	 	return null;
	}
	
	NTrackList.prototype.indexOf = function(track) {
		for(var i = 0; i < this.length; i++ ) {
	 		if( this[i] != undefined && this[i].id == track.id ) {
	 			return i;
	 		}
	 	}
	 	return -1;
	}
	
	/**
	 * Updates a track in the track list. Updating is done merging the 
	 * old and the new track. The merging is important because the new track
	 * (if it is straight from server json response) contains only the
	 * workout data but not any map related objects like overlays etc. which are added 
	 * to the track here and there after loading the data from server.
	 * 
	 * This method is useful for example if the 
	 * track gets more coordinate points when zooming map. In this case the 
	 * tracklist must be updated.
	 * 
	 * @param NTrack track to be updated
	 * @return The new merged track or false if the were nothing to update
	 */
	NTrackList.prototype.update = function(track) {
	 	for(var i = 0; i < this.length; i++ ) {
	 		if( this[i] != undefined && this[i].id == track.id ) {
	 			// Found the track which should be updated
	 			// Do merging
	 			var oldTrack = this[i];
				var newMergedTrack = $.extend({}, oldTrack, track);
				this[i] = newMergedTrack;
				return newMergedTrack;
	 		}
	 	}
	 	return false;		
	}
	
	$.fn.extend({
		gmap: function(settings) {
			settings = $.extend({}, $.NST.defaults.map, settings)
			
			$(this).each(function() {
				var map;
				if(mapengine=="ajax") {
					map = launchMap(this);
				} else {
					// Janne A 2.2.2009, hope this works
					map = {};
				}
				
				this.map = map;
				this.map.colorManager = new NColorManager(settings.colors);
				this.map.settings = $.extend({}, settings);
				this.map.trackList = $.extend([], new NTrackList());
				this.map.trackSource = new NTrackSource($.NST.defaults.query.tracks);
					
				$(this).bind('trackList', function(event, tracks) {
					$(this).tracks(tracks)
				});
				
				if(mapengine=="ajax") {
					var me = this;
					GEvent.addListener(map, "moveend", function() {
						// "moveend" is actually fired for zooming and all the other moving on the map
						$(me).updateMap();
					});
				}
			});
			
			return this;
		},
		updateMap: function() {
			this.each(function() {
				var target = this;
				var args = $(this).boundingArgs();
				this.map.trackSource.page(1);
				this.map.trackSource.load(args, function(tracks) {
					// Trigger an event for updating tracklist after
					// map is updated
					$(target).trigger('trackList', [tracks]);
				});
			});
			return this;
		},
		updateOnPage: function() {
			this.each(function() {
				var target = this;
				var args = $(this).boundingArgs();
				this.map.trackSource.load(args, function(tracks) {
					$(target).trigger('trackList', [tracks]);
				});
			})
			
			return this;	
		},
		retrieveSettings: function() {
			return this[0].map.trackSource.settings.data;
		},
		boundingArgs: function() {
			var first = this[0];
			var args = {};
			if( first.map.settings.useMapArea ) {
				if(mapengine=="ajax") {
					var bounds = first.map.getBounds();
					args.sw = bounds.getSouthWest().toUrlValue(6);
					args.ne = bounds.getNorthEast().toUrlValue(6);
				} else {
					args.sw = flexBounds.sw;
					args.ne = flexBounds.ne;
				}
			}
			return args;
		},
		overlay: function(p1) {
			if( p1.prototype = NTrack ) {
				var track = p1;
				var me = this;
				$.each(track.overlays, function(index, overlay) {
					me.each(function() {
						this.map.addOverlay(overlay);
						this.map.trackList.push(track);
					})
				});
			} else {
				var overlay = p1;
				this.each(function() {
					this.map.addOverlay(overlay);
				});
			}
			
			return this;
		},
		removeOverlay: function(p1) {
			//if( p1.prototype == NTrack) {
				var track = p1;
				var me = this;
				$.each(track.overlays, function(index, overlay) {
					if( overlay != undefined ) {
						track.overlays[index] = undefined;
						me.each(function() {
							this.map.removeOverlay(overlay);
							this.map.trackList.remove(track);
						});
					}
				});
			/*} else {
				var overlay = p1;
				this.each(function() {
					this.map.addOverlay(overlay);
				});
			}*/
			return this;
		},
		clearOverlays: function() {
			this.each(function() {
				this.map.clearOverlays();
			});
			
			return this;			
		},
		/**
		 * Order the map based on an order.
		 */
		orderBy: function(order) {
			this.each( function() {
				if( this.map.trackSource.orderBy(order) ) {
					$(this).updateMap();
				}
			});
			return this;
		},
		nextPage: function() {
			this.each( function() {
				this.map.trackSource.nextPage();
				$(this).updateOnPage();
			});
			return this;
		},
		previousPage: function() {
			this.each( function() {
				this.map.trackSource.previousPage();
				$(this).updateOnPage();
			});
			return this;
		},
		maxResults: function(max) {
			if(max == undefined) {
				return this[0].map.trackSource.maxResults();
			} else {
				this.each( function() {
					this.map.trackSource.maxResults(max);
				});
				return this;
			}
		},
		page: function(page) {
			if( page == undefined ) {
				return this[0].map.trackSource.page();
			} else {
				this.each( function() {
					this.map.trackSource.page(page);
				});
				return this;
			}
		},
		filters: function(filters) {
			this.each( function() {
				this.map.trackSource.filters(filters);
			});
			return this;
		},
		
		/**
		 * This method handles new tracks from tracklist loaded from server.
		 * 
		 * @param various settings (for example if the track is drawn on 
		 * the map or not)
		 */
		track: function(track, settings) {
			this.each(function() {
				var map = this.map;
				var update = false;
				
				if( !map.trackList.contains(track) ) {
					// Push a new track to tracklist
					track = $.extend({map: map}, track);
					map.trackList.push(track);
				} else {
					// Update the track.
					// Update returns track which is merged from the old 
					// track
					track = map.trackList.update(track);
					var update = true;
				}
				
				if( track == undefined || track == null ) {
					throw 'Track must not be null or undefined.';
				}
								
				settings = $.extend({}, $.NST.defaults.track, settings);
	
				if(mapengine=="ajax") {
					if( track.coords != null) {
						//var line = map.createPolyLine(track.coords);
						logger.debug("[track.id: " + track.id + ", track.coords.length:  draw: " + settings.line + "]")
						track.line(settings.line, settings.color);
					}
				}
		
				// These need be done only once.
			
				if( settings.start != false && track.startPosition != null ) {
					var name;
					if( settings.start == true ) {
						if( settings.number == undefined ) {
							if( track.live ) {
								name = "live"
							} else {
								name = "start"
							}
						} else if( track.live ) {
							name = "live"
						} else {								
							name = settings.number;
						}
					} else {
						name = settings.start;
					}
				}
				
				if(mapengine=="ajax") {
					if( settings.start != false && track.startPosition != null ) {
							if( track.needIconRedraw('start', name) ) {
							icon = createIcon(name);
							var coordinate = track.startPosition.coords;
							var pin = icon.newMarker(createPosition(coordinate), 'start', track.id);
							GEvent.bind(pin, "click", null, function() {
								openWorkout(track.id);
							});
							track.overlay('start', pin);
						}
					}
						
					if( !update && settings.end == true) {
						icon = createIcon('man');
						var coordinate = track.endPosition.coords;
						var pin = icon.newMarker(createPosition(coordinate), 'end', track.id);
						track.overlay('end', pin);
					}
				}
			});
			return this;
		},
		/**
		 * Add all tracks in a list to all the listed maps.
		 * After new tracklist is loaded from server this method is called
		 *
		 * @param tracks the new tracklist
		 */
		tracks: function(tracks, settings, more) {
			var start = new Date().getUTCMilliseconds( );
			var me = this;
			var trackList = this[0].map.trackList;
			var toRemove = new Array();
			
			// Remove all the old tracks that the new tracklist
			// does not contain
			for( var i = 0; i < trackList.length; i++ ) {
				var oldTrack = trackList[i];
				if( !tracks.contains(oldTrack) ) {
					toRemove.push(oldTrack); 
				}
			}
			
			// Remove old overlays
			for( var i = 0; i < toRemove.length; i++ ) {
				this.removeOverlay(toRemove[i]);
			}
			
			this.each( function() {
				var method = this.map.settings.tracks;
				if( method == "community" || method == "default" ) {
					$(this).community(tracks, settings)
				} else if (method == "frontpage") {
					$(this).frontpage(tracks, settings)
				}
			});
			
			var end = new Date().getUTCMilliseconds( );
			$('.time .tracks').html(end-start + "ms");
			return this;
		},
		/**
		 * For a single element selections.
		 *
		 * @param the new tracklist
		 */
		community: function(tracks, settings) {
			var offset = (this.page() - 1) * this.maxResults(); 
			for( var i = 0; i < tracks.length; i++ ) {
				var track = tracks[i];
				var j = i + offset;
				
				// Handle new tracks one by one
				this.track(track, 
						// Settings for the ten first workouts
						j<10 ? {start: true, color: j+1, line: {width: 5, alpha: 0.8}, number: j+1}
						// Settings for the others
						:{start: "man", line: false});
			}
			return this;
		},
		frontpage: function(tracks, settings) {
			for( var i = 0; i < tracks.length; i++ ) {
				var track = tracks[i];
				this.track(track, {start: track.live?"live":"man", line: false, end: false});		
			}	
		},
		tracktable: function(targetMap, UserIsLogIn) {
			$(this).each( function() {
				var target = this;
				target.tracks = $.extend([], new NTrackList());
				function update(tracklist) {
					logger.debug("update [map-api.js]");
					logger.time("update");
					var map = $(targetMap)
					var max = map.maxResults();
					if( max > tracklist.length ) max = tracklist.length;
					var offset = (map.page() - 1) * map.maxResults()
					
					var toRemove = [];
					var toAdd = [];
					// target is class pathContainer (community/index.jsp);
					var rows = $('tr', target);
					var lastindex = 0;
					for( var i = 0; i < rows.length; i++ ) {
						var track = target.tracks[i];
						try {
						var index = tracklist.indexOf(track)
						} catch(e) {
							//alert(i + " -> " + rows.length + " <- " + e)
						}
						if( index <= lastindex || index >= max) {
							toRemove.push(i);
						} else {
							lastindex = index;
							rows[i].targetIndex = index;
							//toAdd.push(rows[i]); 
						}
					}
					for(var i = 0; i < toRemove.length; i++ ) {
						tracklist.remove(toRemove[i]);
						$(rows[toRemove[i]]).remove();
						rows[toRemove[i]] = null;
					}
					
					var i = 0;
					for( var j = 0; i < max ; i++ ) {
						target.tracks[i] = tracklist[i];
						while( rows[j] == null && j < rows.length ) {
							j++;
						}
						if( j >= rows.length ) break;
						if( rows[j].targetIndex > i ) {
							$(rows[j]).before( getPath(i+offset, tracklist[i],UserIsLogIn));
						} else {
							$(rows[j]).children(".pathNumberbox").remove();
							$(rows[j]).prepend(getNumberBox(rows[j].targetIndex + offset))
							j++;
						}
					}
					logger.time("pathListAppend");
					insertAfterLoop = false;
					textToInsert = [];
					var insertIndex = 0;
					for(; i < max; i++) {
						target.tracks[i] = tracklist[i];
						/*
						 * getPath should return string for the sake of performance.
						 * This is done only in default branch (not in Salomon etc.)
						 * Salomon getPath returns jQuery object.
						 * 
						 * It would be good to refactor also the brandings so that 
						 * getPath in every brand would return string, but I'm not 
						 * doing it at the moment. That's why we have to check 
						 * whether the getPath returns jQuery object or string 
						 * and act accordinly.
						 */
						var pathToInsert = getPath(i+offset, tracklist[i],UserIsLogIn);
						if(typeof(pathToInsert) != 'string'){
							// pathToInsert is jQuery object. This 
							// is the old implementation
							$(target).append(pathToInsert);
						} else {
							// Greit! Path is string! This is the 
							// new and faster implementation
							insertAfterLoop = true;
							
							// Add path to array. This is faster way that string
							// concatenation
							textToInsert[insertIndex++] = pathToInsert;
						}
					}
					
					if(insertAfterLoop){
						$(target).append(textToInsert.join(''))
						
						// Add tooltips
						
						// This a performance issue. Without tooltips the 
						// list is populated in less than 100ms but with 
						// tooltips populating required over 600ms
						var $tooltips = $("a.tooltip", $(target));
						
						logger.debug("Tooltip count: " + $tooltips.length);
						$tooltips.tooltip();
						
					}
					
					logger.timeEnd("pathListAppend");
					
					$('tr:nth-child(even)', target).addClass('even');
					
					try { 
						// refresh pngs (fix for IE)
						refreshPngs();
					} catch (e) {}
					
					// Show/hide next page button
					toggleNextPageButton(tracklist, map);
	
					// Show/hide previous page button
					togglePreviousButton(tracklist, map);
	
					// Display page number
					displayPageNumber(map);
					
					logger.timeEnd("update");
				}
				$(targetMap).bind('trackList', function(event, tracks) {
					update(tracks);
				});	
			})
		}
	})
})(jQuery);