/**
 * RealWorld v1.1
 * Copyright ©2007-2008 realbuzz.com Ltd.
 * Email: info@realbuzz.com
 * WWW:   http://realbuzz.com/
 */



/**
 * This helper "class" simplifies creating and extending existing objects.
 *
 * Usually extending objects in JavaScript can be a pain, this Class helps
 * reduce the pain by creating a special constructor that calls the new objects
 * __construct method.
 *
 * Usually to extend in JS you have to do: MyObject.prototype = new BaseObject();
 * The problem with this is that it calls the construtor on BaseObject and the
 * constructor can't tell if you just created a BaseObject or are simply extending
 * it. This causes a lot of problems if BaseObject is modifying nodes in the DOM.
 *
 * @example:	BaseObject = new Class();			// a new object
 *
 * @example:	MyObject = new Class(BaseObject);	// extends BaseObject
 * 
 * @example:	// Call the base constructor
 *				MyObject.prototype.__construct = function() {
 *					console.debug('Before base class');
 *					this._baseClass.__construct.call(this)
 *					console.debug('After base class');
 *				}
 */
function Inheritance(){}
Inheritance.toString = function() { return '[object Inheritance]'; };
function Class(base)
{
	return function(t) {
		// If inheriting then copy over all prototype values into the new object
		if (base) {
			this._baseClass = new base(new Inheritance);
            for (x in this._baseClass) {
                if (typeof this[x] != 'undefined') continue;
                this[x] = this._baseClass[x];
            }
		}
        // We're just extending the class, no need to run constructor
        if (arguments[0] instanceof Inheritance) {
            return;
        } 

		// If no constructor then just exit
		if (!this.__construct) {
			return;
		}

		// Convert arguments object into an array
		var args = [];
		for (var i=0; i<arguments.length; i++) {
			args[i] = arguments[i];
		}



		// Pass arguments over to object's constructor
		//console.group(this.toString()+' construction', this);
		var rv = this.__construct.apply(this, args);
		//console.groupEnd();
		return rv;
	}
}
function cloneObject(what) {
    for (i in what) {
        if (typeof what[i] == 'object') {
            this[i] = new cloneObject(what[i]);
        }
        else
            this[i] = what[i];
    }
}

function serialise(obj)
{
	var str = '';
	switch(typeof obj) {
		case 'object':
			if (obj instanceof Array) {
				str += '[';
				for(var x=0 ;x<obj.length; x++) {
					if (!isset(obj[x])) {
						var o = null;
					} else {
						var o = obj[x];
					}
					if (str != '[') str += ',';
					str += serialise(o);
				}
				str += ']';
			} else {
				str += '{';
				for (x in obj) {
					var o = obj[x];
					if (str != '{') str += ',';
					str += '"'+x+'":';
					str += serialise(o);
				}
				str += '}';
			}
			break;
		case 'string':
			obj = obj.replace(/\\/g, '\\\\');
			obj = obj.replace(/"/g, '\\"');
			obj = obj.replace(/\r\n/g, '\\n');
			obj = obj.replace(/\n/g, '\\n');
			obj = obj.replace(/\r/g, '\\n');
			obj = obj.replace(/\t/g, '\\t');
			str += '"'+obj+'"';
			break;
		case 'number':
			str += obj;
			break;
        case 'boolean':
            str += obj.toString();
            break;
		default:
			str += 'null';
			break;
	}

	return str;
}
function unserialise(json)
{
	return eval('(' + json + ')');
}
function realLeft(o)
{
	var l = o.offsetLeft;
	while (o = o.offsetParent) {
		l += o.offsetLeft;
	}
	return l;
}
function realTop(o)
{
	var t = o.offsetTop;
	while (o = o.offsetParent) {
		t += o.offsetTop;
	}
	return t;
}
document.createNamedElement = function(el, nm)
{
    var obj;
    try {
        obj = document.createElement('<'+el+' name="'+nm+'">');
    }
    catch(e) {
        obj = document.createElement(el);
        obj.setAttribute('name', nm);
    }     
    return obj; 
}       
document.createFormElement = function(attr)
{
    var obj;
    try {
		var tag = '<form';
		for (x in attr) {
			tag += ' ' + x + '="' + attr[x] + '"';
		}
		tag += '>';
        obj = document.createElement(tag);
    } catch(e) {
        obj = document.createElement('form');
		for (x in attr) {
			obj.setAttribute(x, attr[x]);
		}
    }     
    return obj; 
}       
function isArray(obj)
{
    try {
        return obj instanceof Array;
    } catch (e) {
        return obj.constructor.toString().indexOf('function Array()') == 0;
    }
}
function isset(v)
{
    return (typeof v != 'undefined');
}
Math.roundDP = function(v, dp)
{
    var dp = dp || 0;
    dp = Math.pow(10, dp);
    return Math.round(v*dp)/dp;
}
Array.prototype.find = function(item)
{
    for (var i=0; i<this.length; i++) {
        if (this[i] === item) {
            return i;
        }
    }
    return false;
}
Array.prototype.last = function()
{
    if (this.length == 0) return false;
    return this[this.length-1];
}
document.createParagraph = function(text)
{
    var p = T.p();
    var t = text.split('\n');
    for (var i=0; i<t.length; i++) {
        if (i != 0) p.appendChild(document.createElement('br'));
        p.appendChild(document.createTextNode(t[i]));
    }
    
    return p;
}
document.emptyElement = function(el)
{
	while (el.firstChild) {
		el.removeChild(el.firstChild);
	}
}

/* Copyright (c) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 *
 *
 * Author: Doug Ricket, others
 * 
 * Marker manager is an interface between the map and the user, designed
 * to manage adding and removing many points when the viewport changes.
 *
 *
 * Algorithm: The MM places its markers onto a grid, similar to the map tiles.
 * When the user moves the viewport, the MM computes which grid cells have
 * entered or left the viewport, and shows or hides all the markers in those
 * cells.
 * (If the users scrolls the viewport beyond the markers that are loaded,
 * no markers will be visible until the EVENT_moveend triggers an update.)
 *
 * In practical consequences, this allows 10,000 markers to be distributed over
 * a large area, and as long as only 100-200 are visible in any given viewport,
 * the user will see good performance corresponding to the 100 visible markers,
 * rather than poor performance corresponding to the total 10,000 markers.
 *
 * Note that some code is optimized for speed over space,
 * with the goal of accommodating thousands of markers.
 *
 */



/**
 * Creates a new MarkerManager that will show/hide markers on a map.
 *
 * @constructor
 * @param {Map} map The map to manage.
 * @param {Object} opt_opts A container for optional arguments:
 *   {Number} maxZoom The maximum zoom level for which to create tiles.
 *   {Number} borderPadding The width in pixels beyond the map border,
 *                   where markers should be display.
 *   {Boolean} trackMarkers Whether or not this manager should track marker
 *                   movements.
 */
function MarkerManager(map, opt_opts) {
  var me = this;
  me.map_ = map;
  me.mapZoom_ = map.getZoom();
  me.projection_ = map.getCurrentMapType().getProjection();

  opt_opts = opt_opts || {};
  me.tileSize_ = MarkerManager.DEFAULT_TILE_SIZE_;
  
  var maxZoom = MarkerManager.DEFAULT_MAX_ZOOM_;
  if(opt_opts.maxZoom != undefined) {
    maxZoom = opt_opts.maxZoom;
  }
  me.maxZoom_ = maxZoom;

  me.trackMarkers_ = opt_opts.trackMarkers;

  var padding;
  if (typeof opt_opts.borderPadding == "number") {
    padding = opt_opts.borderPadding;
  } else {
    padding = MarkerManager.DEFAULT_BORDER_PADDING_;
  }
  // The padding in pixels beyond the viewport, where we will pre-load markers.
  me.swPadding_ = new GSize(-padding, padding);
  me.nePadding_ = new GSize(padding, -padding);
  me.borderPadding_ = padding;

  me.gridWidth_ = [];

  me.grid_ = [];
  me.grid_[maxZoom] = [];
  me.numMarkers_ = [];
  me.numMarkers_[maxZoom] = 0;

  GEvent.bind(map, "moveend", me, me.onMapMoveEnd_);

  // NOTE: These two closures provide easy access to the map.
  // They are used as callbacks, not as methods.
  me.removeOverlay_ = function(marker) {
    map.removeOverlay(marker);
    me.shownMarkers_--;
  };
  me.addOverlay_ = function(marker) {
    me.shownMarkers_++;
    if (marker.__hidden) {
        return;
    }
    map.addOverlay(marker);
  };

  me.resetManager_();
  me.shownMarkers_ = 0;

  me.shownBounds_ = me.getMapGridBounds_();
};

// Static constants:
MarkerManager.DEFAULT_TILE_SIZE_ = 1024;
MarkerManager.DEFAULT_MAX_ZOOM_ = 17;
MarkerManager.DEFAULT_BORDER_PADDING_ = 100;
MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE = 256;


/**
 * Initializes MarkerManager arrays for all zoom levels
 * Called by constructor and by clearAllMarkers
 */ 
MarkerManager.prototype.resetManager_ = function() {
  var me = this;
  var mapWidth = MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE;
  for (var zoom = 0; zoom <= me.maxZoom_; ++zoom) {
    me.grid_[zoom] = [];
    me.numMarkers_[zoom] = 0;
    me.gridWidth_[zoom] = Math.ceil(mapWidth/me.tileSize_);
    mapWidth <<= 1;
  }
};

/**
 * Removes all currently displayed markers
 * and calls resetManager to clear arrays
 */
MarkerManager.prototype.clearMarkers = function() {
  var me = this;
  me.processAll_(me.shownBounds_, me.removeOverlay_);
  me.resetManager_();
};


/**
 * Gets the tile coordinate for a given latlng point.
 *
 * @param {LatLng} latlng The geographical point.
 * @param {Number} zoom The zoom level.
 * @param {GSize} padding The padding used to shift the pixel coordinate.
 *               Used for expanding a bounds to include an extra padding
 *               of pixels surrounding the bounds.
 * @return {GPoint} The point in tile coordinates.
 *
 */
MarkerManager.prototype.getTilePoint_ = function(latlng, zoom, padding) {
  var pixelPoint = this.projection_.fromLatLngToPixel(latlng, zoom);
  return new GPoint(
      Math.floor((pixelPoint.x + padding.width) / this.tileSize_),
      Math.floor((pixelPoint.y + padding.height) / this.tileSize_));
};


/**
 * Finds the appropriate place to add the marker to the grid.
 * Optimized for speed; does not actually add the marker to the map.
 * Designed for batch-processing thousands of markers.
 *
 * @param {Marker} marker The marker to add.
 * @param {Number} minZoom The minimum zoom for displaying the marker.
 * @param {Number} maxZoom The maximum zoom for displaying the marker.
 */
MarkerManager.prototype.addMarkerBatch_ = function(marker, minZoom, maxZoom) {
  var mPoint = marker.getPoint();
  // Tracking markers is expensive, so we do this only if the
  // user explicitly requested it when creating marker manager.
  if (this.trackMarkers_) {
    GEvent.bind(marker, "changed", this, this.onMarkerMoved_);
  }
  var gridPoint = this.getTilePoint_(mPoint, maxZoom, GSize.ZERO);

  for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
    var cell = this.getGridCellCreate_(gridPoint.x, gridPoint.y, zoom);
    cell.push(marker);

    gridPoint.x = gridPoint.x >> 1;
    gridPoint.y = gridPoint.y >> 1;
  }
};


/**
 * Returns whether or not the given point is visible in the shown bounds. This
 * is a helper method that takes care of the corner case, when shownBounds have
 * negative minX value.
 *
 * @param {Point} point a point on a grid.
 * @return {Boolean} Whether or not the given point is visible in the currently
 * shown bounds.
 */
MarkerManager.prototype.isGridPointVisible_ = function(point) {
  var me = this;
  var vertical = me.shownBounds_.minY <= point.y &&
      point.y <= me.shownBounds_.maxY;
  var minX = me.shownBounds_.minX;
  var horizontal = minX <= point.x && point.x <= me.shownBounds_.maxX;
  if (!horizontal && minX < 0) {
    // Shifts the negative part of the rectangle. As point.x is always less
    // than grid width, only test shifted minX .. 0 part of the shown bounds.
    var width = me.gridWidth_[me.shownBounds_.z];
    horizontal = minX + width <= point.x && point.x <= width - 1;
  }
  return vertical && horizontal;
}


/**
 * Reacts to a notification from a marker that it has moved to a new location.
 * It scans the grid all all zoom levels and moves the marker from the old grid
 * location to a new grid location.
 *
 * @param {Marker} marker The marker that moved.
 * @param {LatLng} oldPoint The old position of the marker.
 * @param {LatLng} newPoint The new position of the marker.
 */
MarkerManager.prototype.onMarkerMoved_ = function(marker, oldPoint, newPoint) {
  // NOTE: We do not know the minimum or maximum zoom the marker was
  // added at, so we start at the absolute maximum. Whenever we successfully
  // remove a marker at a given zoom, we add it at the new grid coordinates.
  var me = this;
  var zoom = me.maxZoom_;
  var changed = false;
  var oldGrid = me.getTilePoint_(oldPoint, zoom, GSize.ZERO);
  var newGrid = me.getTilePoint_(newPoint, zoom, GSize.ZERO);
  while (zoom >= 0 && (oldGrid.x != newGrid.x || oldGrid.y != newGrid.y)) {
    var cell = me.getGridCellNoCreate_(oldGrid.x, oldGrid.y, zoom);
    if (cell) {
      if (me.removeFromArray(cell, marker)) {
        me.getGridCellCreate_(newGrid.x, newGrid.y, zoom).push(marker);
      }
    }
    // For the current zoom we also need to update the map. Markers that no
    // longer are visible are removed from the map. Markers that moved into
    // the shown bounds are added to the map. This also lets us keep the count
    // of visible markers up to date.
    if (zoom == me.mapZoom_) {
      if (me.isGridPointVisible_(oldGrid)) {
        if (!me.isGridPointVisible_(newGrid)) {
          me.removeOverlay_(marker);
          changed = true;
        }
      } else {
        if (me.isGridPointVisible_(newGrid)) {
          me.addOverlay_(marker);
          changed = true;
        }
      }
    }
    oldGrid.x = oldGrid.x >> 1;
    oldGrid.y = oldGrid.y >> 1;
    newGrid.x = newGrid.x >> 1;
    newGrid.y = newGrid.y >> 1;
    --zoom;
  }
  if (changed) {
    me.notifyListeners_();
  }
};


/**
 * Searches at every zoom level to find grid cell
 * that marker would be in, removes from that array if found.
 * Also removes marker with removeOverlay if visible.
 * @param {GMarker} marker The marker to delete.
 */
MarkerManager.prototype.removeMarker = function(marker) {
  var me = this;
  var zoom = me.maxZoom_;
  var changed = false;
  var point = marker.getPoint();
  var grid = me.getTilePoint_(point, zoom, GSize.ZERO);
  while (zoom >= 0) {
    var cell = me.getGridCellNoCreate_(grid.x, grid.y, zoom);

    if (cell) {
      me.removeFromArray(cell, marker);
    }
    // For the current zoom we also need to update the map. Markers that no
    // longer are visible are removed from the map. This also lets us keep the count
    // of visible markers up to date.
    if (zoom == me.mapZoom_) {
      if (me.isGridPointVisible_(grid)) {
          me.removeOverlay_(marker);
          changed = true;
      } 
    }
    grid.x = grid.x >> 1;
    grid.y = grid.y >> 1;
    --zoom;
  }
  if (changed) {
    me.notifyListeners_();
  }
};


/**
 * Add many markers at once.
 * Does not actually update the map, just the internal grid.
 *
 * @param {Array of Marker} markers The markers to add.
 * @param {Number} minZoom The minimum zoom level to display the markers.
 * @param {Number} opt_maxZoom The maximum zoom level to display the markers.
 */
MarkerManager.prototype.addMarkers = function(markers, minZoom, opt_maxZoom) {
  var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
  for (var i = markers.length - 1; i >= 0; i--) {
    this.addMarkerBatch_(markers[i], minZoom, maxZoom);
  }

  this.numMarkers_[minZoom] += markers.length;
};


/**
 * Returns the value of the optional maximum zoom. This method is defined so
 * that we have just one place where optional maximum zoom is calculated.
 *
 * @param {Number} opt_maxZoom The optinal maximum zoom.
 * @return The maximum zoom.
 */
MarkerManager.prototype.getOptMaxZoom_ = function(opt_maxZoom) {
  return opt_maxZoom != undefined ? opt_maxZoom : this.maxZoom_;
}


/**
 * Calculates the total number of markers potentially visible at a given
 * zoom level.
 *
 * @param {Number} zoom The zoom level to check.
 */
MarkerManager.prototype.getMarkerCount = function(zoom) {
  var total = 0;
  for (var z = 0; z <= zoom; z++) {
    total += this.numMarkers_[z];
  }
  return total;
};


/**
 * Add a single marker to the map.
 *
 * @param {Marker} marker The marker to add.
 * @param {Number} minZoom The minimum zoom level to display the marker.
 * @param {Number} opt_maxZoom The maximum zoom level to display the marker.
 */
MarkerManager.prototype.addMarker = function(marker, minZoom, opt_maxZoom) {
  var me = this;
  var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
  me.addMarkerBatch_(marker, minZoom, maxZoom);
  var gridPoint = me.getTilePoint_(marker.getPoint(), me.mapZoom_, GSize.ZERO);
  if(me.isGridPointVisible_(gridPoint) && 
     minZoom <= me.shownBounds_.z &&
     me.shownBounds_.z <= maxZoom ) {
    me.addOverlay_(marker);
    me.notifyListeners_();
  }
  this.numMarkers_[minZoom]++;
};

/**
 * Returns true if this bounds (inclusively) contains the given point.
 * @param {Point} point  The point to test.
 * @return {Boolean} This Bounds contains the given Point.
 */
GBounds.prototype.containsPoint = function(point) {
  var outer = this;
  return (outer.minX <= point.x &&
          outer.maxX >= point.x &&
          outer.minY <= point.y &&
          outer.maxY >= point.y);
}

/**
 * Get a cell in the grid, creating it first if necessary.
 *
 * Optimization candidate
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
MarkerManager.prototype.getGridCellCreate_ = function(x, y, z) {
  var grid = this.grid_[z];
  if (x < 0) {
    x += this.gridWidth_[z];
  }
  var gridCol = grid[x];
  if (!gridCol) {
    gridCol = grid[x] = [];
    return gridCol[y] = [];
  }
  var gridCell = gridCol[y];
  if (!gridCell) {
    return gridCol[y] = [];
  }
  return gridCell;
};


/**
 * Get a cell in the grid, returning undefined if it does not exist.
 *
 * NOTE: Optimized for speed -- otherwise could combine with getGridCellCreate_.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
MarkerManager.prototype.getGridCellNoCreate_ = function(x, y, z) {
  var grid = this.grid_[z];
  if (x < 0) {
    x += this.gridWidth_[z];
  }
  var gridCol = grid[x];
  return gridCol ? gridCol[y] : undefined;
};


/**
 * Turns at geographical bounds into a grid-space bounds.
 *
 * @param {LatLngBounds} bounds The geographical bounds.
 * @param {Number} zoom The zoom level of the bounds.
 * @param {GSize} swPadding The padding in pixels to extend beyond the
 * given bounds.
 * @param {GSize} nePadding The padding in pixels to extend beyond the
 * given bounds.
 * @return {GBounds} The bounds in grid space.
 */
MarkerManager.prototype.getGridBounds_ = function(bounds, zoom, swPadding,
                                                  nePadding) {
  zoom = Math.min(zoom, this.maxZoom_);
  
  var bl = bounds.getSouthWest();
  var tr = bounds.getNorthEast();
  var sw = this.getTilePoint_(bl, zoom, swPadding);
  var ne = this.getTilePoint_(tr, zoom, nePadding);
  var gw = this.gridWidth_[zoom];
  
  // Crossing the prime meridian requires correction of bounds.
  if (tr.lng() < bl.lng() || ne.x < sw.x) {
    sw.x -= gw;
  }
  if (ne.x - sw.x  + 1 >= gw) {
    // Computed grid bounds are larger than the world; truncate.
    sw.x = 0;
    ne.x = gw - 1;
  }
  var gridBounds = new GBounds([sw, ne]);
  gridBounds.z = zoom;
  return gridBounds;
};


/**
 * Gets the grid-space bounds for the current map viewport.
 *
 * @return {Bounds} The bounds in grid space.
 */
MarkerManager.prototype.getMapGridBounds_ = function() {
  var me = this;
  return me.getGridBounds_(me.map_.getBounds(), me.mapZoom_,
                           me.swPadding_, me.nePadding_);
};


/**
 * Event listener for map:movend.
 * NOTE: Use a timeout so that the user is not blocked
 * from moving the map.
 *
 */
MarkerManager.prototype.onMapMoveEnd_ = function() {
  var me = this;
  me.objectSetTimeout_(this, this.updateMarkers_, 0);
};


/**
 * Call a function or evaluate an expression after a specified number of
 * milliseconds.
 *
 * Equivalent to the standard window.setTimeout function, but the given
 * function executes as a method of this instance. So the function passed to
 * objectSetTimeout can contain references to this.
 *    objectSetTimeout(this, function() { alert(this.x) }, 1000);
 *
 * @param {Object} object  The target object.
 * @param {Function} command  The command to run.
 * @param {Number} milliseconds  The delay.
 * @return {Boolean}  Success.
 */
MarkerManager.prototype.objectSetTimeout_ = function(object, command, milliseconds) {
  return window.setTimeout(function() {
    command.call(object);
  }, milliseconds);
};


/**
 * Refresh forces the marker-manager into a good state.
 * <ol>
 *   <li>If never before initialized, shows all the markers.</li>
 *   <li>If previously initialized, removes and re-adds all markers.</li>
 * </ol>
 */
MarkerManager.prototype.refresh = function() {
  var me = this;
  if (me.shownMarkers_ > 0) {
    me.processAll_(me.shownBounds_, me.removeOverlay_);
  }
  me.processAll_(me.shownBounds_, me.addOverlay_);
  me.notifyListeners_();
};


/**
 * After the viewport may have changed, add or remove markers as needed.
 */
MarkerManager.prototype.updateMarkers_ = function() {
  var me = this;
  me.mapZoom_ = this.map_.getZoom();
  var newBounds = me.getMapGridBounds_();
  
  // If the move does not include new grid sections,
  // we have no work to do:
  if (newBounds.equals(me.shownBounds_) && newBounds.z == me.shownBounds_.z) {
    return;
  }

  if (newBounds.z != me.shownBounds_.z) {
    me.processAll_(me.shownBounds_, me.removeOverlay_);
    me.processAll_(newBounds, me.addOverlay_);
  } else {
    // Remove markers:
    me.rectangleDiff_(me.shownBounds_, newBounds, me.removeCellMarkers_);

    // Add markers:
    me.rectangleDiff_(newBounds, me.shownBounds_, me.addCellMarkers_);
  }
  me.shownBounds_ = newBounds;

  me.notifyListeners_();
};


/**
 * Notify listeners when the state of what is displayed changes.
 */
MarkerManager.prototype.notifyListeners_ = function() {
  GEvent.trigger(this, "changed", this.shownBounds_, this.shownMarkers_);
};


/**
 * Process all markers in the bounds provided, using a callback.
 *
 * @param {Bounds} bounds The bounds in grid space.
 * @param {Function} callback The function to call for each marker.
 */
MarkerManager.prototype.processAll_ = function(bounds, callback) {
  for (var x = bounds.minX; x <= bounds.maxX; x++) {
    for (var y = bounds.minY; y <= bounds.maxY; y++) {
      this.processCellMarkers_(x, y,  bounds.z, callback);
    }
  }
};


/**
 * Process all markers in the grid cell, using a callback.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @param {Function} callback The function to call for each marker.
 */
MarkerManager.prototype.processCellMarkers_ = function(x, y, z, callback) {
  var cell = this.getGridCellNoCreate_(x, y, z);
  if (cell) {
    for (var i = cell.length - 1; i >= 0; i--) {
      callback(cell[i]);
    }
  }
};


/**
 * Remove all markers in a grid cell.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 */
MarkerManager.prototype.removeCellMarkers_ = function(x, y, z) {
  this.processCellMarkers_(x, y, z, this.removeOverlay_);
};


/**
 * Add all markers in a grid cell.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 */
MarkerManager.prototype.addCellMarkers_ = function(x, y, z) {
  this.processCellMarkers_(x, y, z, this.addOverlay_);
};


/**
 * Use the rectangleDiffCoords function to process all grid cells
 * that are in bounds1 but not bounds2, using a callback, and using
 * the current MarkerManager object as the instance.
 *
 * Pass the z parameter to the callback in addition to x and y.
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate (x, y, z).
 */
MarkerManager.prototype.rectangleDiff_ = function(bounds1, bounds2, callback) {
  var me = this;
  me.rectangleDiffCoords(bounds1, bounds2, function(x, y) {
    callback.apply(me, [x, y, bounds1.z]);
  });
};


/**
 * Calls the function for all points in bounds1, not in bounds2
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate.
 */
MarkerManager.prototype.rectangleDiffCoords = function(bounds1, bounds2, callback) {
  var minX1 = bounds1.minX;
  var minY1 = bounds1.minY;
  var maxX1 = bounds1.maxX;
  var maxY1 = bounds1.maxY;
  var minX2 = bounds2.minX;
  var minY2 = bounds2.minY;
  var maxX2 = bounds2.maxX;
  var maxY2 = bounds2.maxY;

  for (var x = minX1; x <= maxX1; x++) {  // All x in R1
    // All above:
    for (var y = minY1; y <= maxY1 && y < minY2; y++) {  // y in R1 above R2
      callback(x, y);
    }
    // All below:
    for (var y = Math.max(maxY2 + 1, minY1);  // y in R1 below R2
         y <= maxY1; y++) {
      callback(x, y);
    }
  }

  for (var y = Math.max(minY1, minY2);
       y <= Math.min(maxY1, maxY2); y++) {  // All y in R2 and in R1
    // Strictly left:
    for (var x = Math.min(maxX1 + 1, minX2) - 1;
         x >= minX1; x--) {  // x in R1 left of R2
      callback(x, y);
    }
    // Strictly right:
    for (var x = Math.max(minX1, maxX2 + 1);  // x in R1 right of R2
         x <= maxX1; x++) {
      callback(x, y);
    }
  }
};


/**
 * Removes value from array. O(N).
 *
 * @param {Array} array  The array to modify.
 * @param {any} value  The value to remove.
 * @param {Boolean} opt_notype  Flag to disable type checking in equality.
 * @return {Number}  The number of instances of value that were removed.
 */
MarkerManager.prototype.removeFromArray = function(array, value, opt_notype) {
  var shift = 0;
  for (var i = 0; i < array.length; ++i) {
    if (array[i] === value || (opt_notype && array[i] == value)) {
      array.splice(i--, 1);
      shift++;
    }
  }
  return shift;
};


// PolylineEncoder.js copyright Mark McClure  April/May 2007
//
// This software is placed explicitly in the public
// domain and may be freely distributed or modified.
// No warranty express or implied is provided.
//
// History:
// V 2.1  July 2007
//   Minor modification in distance function to enhance
//   speed.  Suggested by Joel Rosenberg.
// V 2.0 May 2007.
//   Major revisions include:
//     Incorporation of Douglas-Peucker algorithm
//     Encapsulation into the PolylineEncoder package.
// V 1.0 September 2006
//   Original version based on simple vertex reduction
// 
// This module defines a PolylineEncoder class to encode
// polylines for use with Google Maps together with a few
// auxiliary functions. Documentation at
// http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.html
//
// Google map reference including encoded polylines:
//   http://www.google.com/apis/maps/documentation/
//
// Details on the algorithm used here:
//   http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/
//
// Constructor:
//   polylineEncoder = new PolylineEncoder(numLevels, 
//     zoomFactor, verySmall, forceEndpoints?);
// where numLevels and zoomFactor indicate how many 
// different levels of magnification the polyline has
// and the change in magnification between those levels,
// verySmall indicates the length of a barely visible 
// object at the highest zoom level, forceEndpoints 
// indicates whether or not the  endpoints should be 
// visible at all zoom levels.  forceEndpoints is 
// optional with a default value of true.  Probably 
// should stay true regardless.
// 
// Main methods:
// * PolylineEncoder.dpEncodeToPolyline(points, 
//     color?, weight?, opacity?)
// Accepts an array of latLng objects (see below) and
// optional style specifications.  Returns an encoded 
// polyline that may be directly overlayed on a Google 
// Map.  Requires that the Google Maps API be loaded.
//
// * PolylineEncoder.dpEncodeToPolygon(pointsArray, 
//     boundaryColor?, boundaryWeight?, boundaryOpacity?,
//     fillColor?, fillOpacity?, fill?, outline?)
// Accepts an array of arrays latLng objects and
// optional style specifications.  Returns an encoded 
// polylgon that may be directly overlayed on a Google 
// Map.  Requires that the Google Maps API be loaded.
//
//
// Convenience classes and methods:
// * PolylineEncoder.latLng
// Constructor:
//   myLatLng = new PolylineEncoder.latLng(y,x);
// The dpEncode* functions expect points in the
// form of an object with lat and lng methods.  A
// GLatLng as defined by the Google Maps API does 
// quite nicely.  If you're developing a javascript
// without loading the API, however, you can use
// a PolylineEncoder.latLng for this purpose.
// //
// PolylineEncoder.pointsToLatLngs
// Sometimes your points are defined in terms of an
// array of arrays, rather than an array of latLngs.
// PolylineEncoder.pointsToLatLngs converts to an array
// of arrays to an array of latLngs for use by the
// dpEncode functions.
// //
// PolylineEncoder.pointsToGLatLngs
// PolylineEncoder.pointsToGLatLngs is analagous to the 
// previous function, but it returns GLatLngs rather
// than PolylineEncoder.latLngs.  The first function may
// be used independently of Google Maps.  Use the second,
// if you need to use the result in a Goole Map function.
//
//
// Lower level methods
// PolylineEncoder.dpEncodeToJSON(points, 
//     color?, weight?, opacity?)
// Returns a legal argument to GPolyline.fromEncoded.
// //
// PolylineEncoder.dpEncode(points);
// This is where the real work is done.  The return value
// is a JSON object with properties named  encodedLevels,
// encdodedPoints and encodedPointsLiteral. These are
// strings which are acceptable input to the points and
// levels properties of the GPolyline.fromEncoded
// function. The encodedPoints string should be used for
// maps generated dynamically, while the
// encodedPointsLiteral string should be copied into a
// static document.
// 
// The standard disclaimers, such as "use at your own risk, 
// since I really don't have any idea what I'm doing," apply. 

// The constructor
PolylineEncoder = function(numLevels, zoomFactor, verySmall, forceEndpoints) {
  var i;
  if(!numLevels) {
    numLevels = 18;
  }
  if(!zoomFactor) {
    zoomFactor = 2;
  }
  if(!verySmall) {
    verySmall = 0.00001;
  }
  if(!forceEndpoints) {
    forceEndpoints = true;
  }
  this.numLevels = numLevels;
  this.zoomFactor = zoomFactor;
  this.verySmall = verySmall;
  this.forceEndpoints = forceEndpoints;
  this.zoomLevelBreaks = new Array(numLevels);
  for(i = 0; i < numLevels; i++) {
    this.zoomLevelBreaks[i] = verySmall*Math.pow(zoomFactor, numLevels-i-1);
  }
}

// The main function.  Essentially the Douglas-Peucker
// algorithm, adapted for encoding. Rather than simply
// eliminating points, we record their from the
// segment which occurs at that recursive step.  These
// distances are then easily converted to zoom levels.
PolylineEncoder.prototype.dpEncode = function(points) {
  var absMaxDist = 0;
  var stack = [];
  var dists = new Array(points.length);
  var maxDist, maxLoc, temp, first, last, current;
  var i, encodedPoints, encodedLevels;
  var segmentLength;
  
  if(points.length > 2) {
    stack.push([0, points.length-1]);
    while(stack.length > 0) {
      current = stack.pop();
      maxDist = 0;
      segmentLength = Math.pow(points[current[1]].lat()-points[current[0]].lat(),2) + 
        Math.pow(points[current[1]].lng()-points[current[0]].lng(),2);
      for(i = current[0]+1; i < current[1]; i++) {
        temp = this.distance(points[i], 
          points[current[0]], points[current[1]],
          segmentLength);
        if(temp > maxDist) {
          maxDist = temp;
          maxLoc = i;
          if(maxDist > absMaxDist) {
            absMaxDist = maxDist;
          }
        }
      }
      if(maxDist > this.verySmall) {
        dists[maxLoc] = maxDist;
        stack.push([current[0], maxLoc]);
        stack.push([maxLoc, current[1]]);
      }
    }
  }
  
  encodedPoints = this.createEncodings(points, dists);
  encodedLevels = this.encodeLevels(points, dists, absMaxDist);
  return {
    encodedPoints: encodedPoints,
    encodedLevels: encodedLevels,
    encodedPointsLiteral: encodedPoints.replace(/\\/g,"\\\\")
  }
}

PolylineEncoder.prototype.dpEncodeToJSON = function(points,
  color, weight, opacity) {
  var result;
  
  if(!opacity) {
    opacity = 0.9;
  }
  if(!weight) {
    weight = 3;
  }
  if(!color) {
    color = "#0000ff";
  }
  result = this.dpEncode(points);
  return {
    color: color,
    weight: weight,
    opacity: opacity,
    points: result.encodedPoints,
    levels: result.encodedLevels,
    numLevels: this.numLevels,
    zoomFactor: this.zoomFactor
  }
}

PolylineEncoder.prototype.dpEncodeToGPolyline = function(points,
  color, weight, opacity) {
  if(!opacity) {
    opacity = 0.9;
  }
  if(!weight) {
    weight = 3;
  }
  if(!color) {
    color = "#0000ff";
  }
  return new GPolyline.fromEncoded(
    this.dpEncodeToJSON(points, color, weight, opacity));
}

PolylineEncoder.prototype.dpEncodeToGPolygon = function(pointsArray,
  boundaryColor, boundaryWeight, boundaryOpacity,
  fillColor, fillOpacity, fill, outline) {
  var i, boundaries;
  if(!boundaryColor) {
    boundaryColor = "#0000ff";
  }
  if(!boundaryWeight) {
    boundaryWeight = 3;
  }
  if(!boundaryOpacity) {
    boundaryOpacity = 0.9;
  }
  if(!fillColor) {
    fillColor = boundaryColor;
  }
  if(!fillOpacity) {
    fillOpacity = boundaryOpacity/3;
  }
  if(fill==undefined) {
    fill = true;
  }
  if(outline==undefined) {
    outline = true;
  }
  
  boundaries = new Array(0);
  for(i=0; i<pointsArray.length; i++) {
    boundaries.push(this.dpEncodeToJSON(pointsArray[i],
      boundaryColor, boundaryWeight, boundaryOpacity));
  }
  return new GPolygon.fromEncoded({
    polylines: boundaries,
    color: fillColor,
    opacity: fillOpacity,
    fill: fill,
    outline: outline
  });
}

// distance(p0, p1, p2) computes the distance between the point p0
// and the segment [p1,p2].  This could probably be replaced with
// something that is a bit more numerically stable.
PolylineEncoder.prototype.distance = function(p0, p1, p2, segLength) {
  var u, out;
  
  if(p1.lat() === p2.lat() && p1.lng() === p2.lng()) {
    out = Math.sqrt(Math.pow(p2.lat()-p0.lat(),2) + Math.pow(p2.lng()-p0.lng(),2));
  }
  else {
    u = ((p0.lat()-p1.lat())*(p2.lat()-p1.lat())+(p0.lng()-p1.lng())*(p2.lng()-p1.lng()))/
      segLength;
  
    if(u <= 0) {
      out = Math.sqrt(Math.pow(p0.lat() - p1.lat(),2) + Math.pow(p0.lng() - p1.lng(),2));
    }
    if(u >= 1) {
      out = Math.sqrt(Math.pow(p0.lat() - p2.lat(),2) + Math.pow(p0.lng() - p2.lng(),2));
    }
    if(0 < u && u < 1) {
      out = Math.sqrt(Math.pow(p0.lat()-p1.lat()-u*(p2.lat()-p1.lat()),2) +
        Math.pow(p0.lng()-p1.lng()-u*(p2.lng()-p1.lng()),2));
    }
  }
  return out;
}

// The createEncodings function is very similar to Google's
// http://www.google.com/apis/maps/documentation/polyline.js
// The key difference is that not all points are encoded, 
// since some were eliminated by Douglas-Peucker.
PolylineEncoder.prototype.createEncodings = function(points, dists) {
  var i, dlat, dlng;
  var plat = 0;
  var plng = 0;
  var encoded_points = "";

  for(i = 0; i < points.length; i++) {
    if(dists[i] != undefined || i == 0 || i == points.length-1) {
      var point = points[i];
      var lat = point.lat();
      var lng = point.lng();
      var late5 = Math.floor(lat * 1e5);
      var lnge5 = Math.floor(lng * 1e5);
      dlat = late5 - plat;
      dlng = lnge5 - plng;
      plat = late5;
      plng = lnge5;
      encoded_points += this.encodeSignedNumber(dlat) + 
        this.encodeSignedNumber(dlng);
    }
  }
  return encoded_points;
}

// This computes the appropriate zoom level of a point in terms of it's 
// distance from the relevant segment in the DP algorithm.  Could be done
// in terms of a logarithm, but this approach makes it a bit easier to
// ensure that the level is not too large.
PolylineEncoder.prototype.computeLevel = function(dd) {
  var lev;
  if(dd > this.verySmall) {
    lev=0;
    while(dd < this.zoomLevelBreaks[lev]) {
      lev++;
    }
    return lev;
  }
}

// Now we can use the previous function to march down the list
// of points and encode the levels.  Like createEncodings, we
// ignore points whose distance (in dists) is undefined.
PolylineEncoder.prototype.encodeLevels = function(points, dists, absMaxDist) {
  var i;
  var encoded_levels = "";
  if(this.forceEndpoints) {
    encoded_levels += this.encodeNumber(this.numLevels-1)
  } else {
    encoded_levels += this.encodeNumber(
      this.numLevels-this.computeLevel(absMaxDist)-1)
  }
  for(i=1; i < points.length-1; i++) {
    if(dists[i] != undefined) {
      encoded_levels += this.encodeNumber(
        this.numLevels-this.computeLevel(dists[i])-1);
    }
  }
  if(this.forceEndpoints) {
    encoded_levels += this.encodeNumber(this.numLevels-1)
  } else {
    encoded_levels += this.encodeNumber(
      this.numLevels-this.computeLevel(absMaxDist)-1)
  }
  return encoded_levels;
}

// This function is very similar to Google's, but I added
// some stuff to deal with the double slash issue.
PolylineEncoder.prototype.encodeNumber = function(num) {
  var encodeString = "";
  var nextValue, finalValue;
  while (num >= 0x20) {
    nextValue = (0x20 | (num & 0x1f)) + 63;
//     if (nextValue == 92) {
//       encodeString += (String.fromCharCode(nextValue));
//     }
    encodeString += (String.fromCharCode(nextValue));
    num >>= 5;
  }
  finalValue = num + 63;
//   if (finalValue == 92) {
//     encodeString += (String.fromCharCode(finalValue));
//   }
  encodeString += (String.fromCharCode(finalValue));
  return encodeString;
}

// This one is Google's verbatim.
PolylineEncoder.prototype.encodeSignedNumber = function(num) {
  var sgn_num = num << 1;
  if (num < 0) {
    sgn_num = ~(sgn_num);
  }
  return(this.encodeNumber(sgn_num));
}


// The remaining code defines a few convenience utilities.
// PolylineEncoder.latLng
PolylineEncoder.latLng = function(y, x) {
	this.y = y;
	this.x = x;
}
PolylineEncoder.latLng.prototype.lat = function() {
	return this.y;
}
PolylineEncoder.latLng.prototype.lng = function() {
	return this.x;
}

// PolylineEncoder.pointsToLatLngs
PolylineEncoder.pointsToLatLngs = function(points) {
	var i, latLngs;
	latLngs = new Array(0);
	for(i=0; i<points.length; i++) {
		latLngs.push(new PolylineEncoder.latLng(points[i][0], points[i][1]));
	}
	return latLngs;
}

// PolylineEncoder.pointsToGLatLngs
PolylineEncoder.pointsToGLatLngs = function(points) {
	var i, gLatLngs;
	gLatLngs = new Array(0);
	for(i=0; i<points.length; i++) {
		gLatLngs.push(new GLatLng(points[i][0], points[i][1]));
	}
	return gLatLngs;
}

/*
* ExtMapTypeControl Class v1.2 
*  Copyright (c) 2007, Google 
*  Author: Pamela Fox, others
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
*       http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This class lets you add a control to the map which mimics GMapTypeControl
*  and allows for the addition of a traffic button/traffic key.
*/

/*
 * Constructor for ExtMapTypeControl, which uses an option hash
 * to decide what elements to put in the control.
 * @param {opt_opts} Named optional arguments:
 *   opt_opts.showTraffic {Boolean} Controls whether traffic button is shown
 *   opt_opts.showTrafficKey {Boolean} Controls whether traffic key is shown
 */
function ExtMapTypeControl(opt_opts) {
  this.options = opt_opts || {};
}


ExtMapTypeControl.prototype = new GControl();

/**
 * Is called by GMap2's addOverlay method. Creates the button 
 *  and appends to the map div.
 * @param {GMap2} map The map that has had this ExtMapTypeControl added to it.
 * @return {DOM Object} Div that holds the control
 */ 
ExtMapTypeControl.prototype.initialize = function(map) {
  var container = document.createElement("div");
  var me = this;

  var mapTypes = map.getMapTypes();
  var mapTypeDivs = me.addMapTypeButtons_(map);

  GEvent.addListener(map, "addmaptype", function() {
    var newMapTypes = map.getMapTypes();
    var newMapType = newMapTypes.pop();
    var newMapTypeDiv = me.createButton_(newMapType.getName());
    mapTypes.push(newMapType);
    mapTypeDivs.push(newMapTypeDiv);
    me.resetButtonEvents_(map, mapTypeDivs);
    container.appendChild(newMapTypeDiv);
  });
  GEvent.addListener(map, "removemaptype", function() {
    for (var i = 0; i < mapTypeDivs.length; i++) {
      GEvent.clearListeners(mapTypeDivs[i], "click");
      container.removeChild(mapTypeDivs[i]);
    }
    mapTypeDivs = me.addMapTypeButtons_(map);
    me.resetButtonEvents_(map, mapTypeDivs);
    for (var i = 0; i < mapTypeDivs.length; i++ ) {
      container.appendChild(mapTypeDivs[i]);
    }
  });

  if (me.options.showTraffic) {
    var trafficDiv = me.createButton_("Traffic");
    trafficDiv.style.marginRight = "8px";
    trafficDiv.style.visibility = 'hidden';
    trafficDiv.firstChild.style.cssFloat = "left";
    trafficDiv.firstChild.style.styleFloat = "left";
    // Sending true makes overlay hidden by default
    me.trafficInfo = new GTrafficOverlay({incidents:true, hide:true});
    me.trafficInfo.hidden = true;
    // We have to do this so that we can sense if traffic is in view
    GEvent.addListener(me.trafficInfo, "changed", function(hasTrafficInView) {
      if (hasTrafficInView) {
        trafficDiv.style.visibility = 'visible';
      } else {
        trafficDiv.style.visibility = 'hidden';
      }
    });
    map.addOverlay(me.trafficInfo);

    GEvent.addDomListener(trafficDiv.firstChild, "click", function() {
      if (me.trafficInfo.hidden) {
        me.trafficInfo.hidden = false;
        me.trafficInfo.show();
      } else {
        me.trafficInfo.hidden = true;
        me.trafficInfo.hide();
      }
      me.toggleButton_(trafficDiv.firstChild, !me.trafficInfo.hidden);
    });

    if (me.options.showTrafficKey) {
      keyDiv = document.createElement("div");
      keyDiv.style.cssFloat = "left";
      keyDiv.style.styleFloat = "left";
      keyDiv.innerHTML = "&nbsp;?&nbsp;";
  
      var keyExpandedDiv = document.createElement("div");
      keyExpandedDiv.style.clear = "both";
      keyExpandedDiv.style.padding = "2px";
      var keyInfo = [{"color": "#30ac3e", "text": "&gt; 50 MPH"},
                     {"color": "#ffcf00", "text": "25-50 MPH"},
                     {"color": "#ff0000", "text": "&lt; 25 MPH"},
                     {"color": "#c0c0c0", "text": "No data"}];
      for (var i = 0; i < keyInfo.length; i++) {
        keyExpandedDiv.innerHTML += "<div style='text-align: left'><span style='background-color: " + keyInfo[i].color + "'>&nbsp;&nbsp</span>"
            +  "<span style='color: " + keyInfo[i].color + "'> " + keyInfo[i].text + " </span>" + "</div>"; 
      }
      keyExpandedDiv.style.display = "none";

      GEvent.addDomListener(keyDiv, "click", function() {
        if (me.keyExpanded) {
          me.keyExpanded = false;
          keyExpandedDiv.style.display = "none";
        } else {
          me.keyExpanded = true;
          keyExpandedDiv.style.display = "block";
        }
        me.toggleButton_(keyDiv, me.keyExpanded);
      });

      me.toggleButton_(keyDiv, me.keyExpanded);
    }

    var separatorDiv = document.createElement("div");
    separatorDiv.style.clear = "both";

    if (me.options.showTrafficKey) trafficDiv.appendChild(keyDiv);
    trafficDiv.appendChild(separatorDiv);
    if (me.options.showTrafficKey) trafficDiv.appendChild(keyExpandedDiv);
    me.toggleButton_(trafficDiv.firstChild, false);

    container.appendChild(trafficDiv);
  }

  for (var i = 0; i < mapTypeDivs.length; i++ ) {
    container.appendChild(mapTypeDivs[i]);
  }

  map.getContainer().appendChild(container);

  return container;
}

/*
 * Creates buttons for map types.
 * @param {GMap2} Map object for which to create buttons.
 * @return {Array} Divs containing the buttons.
 */
ExtMapTypeControl.prototype.addMapTypeButtons_ = function(map) {
  var me = this;
  var mapTypes = map.getMapTypes();
  var mapTypeDivs = new Array();
  for (var i = 0; i < mapTypes.length; i++) {
    mapTypeDivs[i] = me.createButton_(mapTypes[i].getName());
  }
  me.resetButtonEvents_(map, mapTypeDivs);
  return mapTypeDivs;
}

/*
 * Ensures that map type button events are assigned correctly.
 * @param {GMap2} Map object for which to reset events.
 * @param {Array} mapTypeDivs Divs containing map type buttons.
 */
ExtMapTypeControl.prototype.resetButtonEvents_ = function(map, mapTypeDivs) {
  var me = this;
  var mapTypes = map.getMapTypes();
  for (var i = 0; i < mapTypeDivs.length; i++) {
    var otherDivs = new Array;
    for (var j = 0; j < mapTypes.length; j++ ) {
      if (j != i) {
        otherDivs.push(mapTypeDivs[j]);
      }
    }
    me.assignButtonEvent_(mapTypeDivs[i], map, mapTypes[i], otherDivs);
  }
  GEvent.addListener(map, "maptypechanged", function() {
    var divIndex = 0;
    var mapType = map.getCurrentMapType();
    for (var i = 0; i < mapTypes.length; i++) {
      if (mapTypes[i] == mapType) {
        divIndex = i;
      }
    }
    GEvent.trigger(mapTypeDivs[divIndex], "click");
  });
}

/*
 * Creates simple buttons with text nodes. 
 * @param {String} text Text to display in button
 * @return {DOM Object} The div for the button.
 */
ExtMapTypeControl.prototype.createButton_ = function(text) {
  var buttonDiv = document.createElement("div");
  this.setButtonStyle_(buttonDiv);
  buttonDiv.style.cssFloat = "left";
  buttonDiv.style.styleFloat = "left";
  var textDiv = document.createElement("div");
  textDiv.appendChild(document.createTextNode(text));
  textDiv.style.width = "6em";
  buttonDiv.appendChild(textDiv);
  return buttonDiv;
}

/*
 * Assigns events to MapType buttons to change maptype
 *  and toggle button styles correctly for all buttons
 *  when button is clicked.
 *  @param {DOM Object} div Button's div to assign click to
 *  @param {GMap2} Map object to change maptype of.
 *  @param {Object} mapType GMapType to change map to when clicked
 *  @param {Array} otherDivs Array of other button divs to toggle off
 */  
ExtMapTypeControl.prototype.assignButtonEvent_ = function(div, map, mapType, otherDivs) {
  var me = this;

  GEvent.addDomListener(div, "click", function() {
    for (var i = 0; i < otherDivs.length; i++) {
      me.toggleButton_(otherDivs[i].firstChild, false);
    }
    me.toggleButton_(div.firstChild, true);
    map.setMapType(mapType);
  });
}

/*
 * Changes style of button to appear on/off depending on boolean passed in.
 * @param {DOM Object} div  Button div to change style of
 * @param {Boolean} boolCheck Used to decide to use on style or off style
 */
ExtMapTypeControl.prototype.toggleButton_ = function(div, boolCheck) {
   div.style.fontWeight = boolCheck ? "bold" : "";
   div.style.border = "1px solid white";
   var shadows = boolCheck ? ["Top", "Left"] : ["Bottom", "Right"];
   for (var j = 0; j < shadows.length; j++) {
     div.style["border" + shadows[j]] = "1px solid #b0b0b0";
  } 
   }

/*
 * Required by GMaps API for controls. 
 * @return {GControlPosition} Default location for control
 */
ExtMapTypeControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(7, 7));
}

/*
 * Sets the proper CSS for the given button element.
 * @param {DOM Object} button Button div to set style for
 */
ExtMapTypeControl.prototype.setButtonStyle_ = function(button) {
  button.style.color = "#000000";
  button.style.backgroundColor = "white";
  button.style.font = "small Arial";
  button.style.border = "1px solid black";
  button.style.padding = "0px";
  button.style.margin= "0px";
  button.style.textAlign = "center";
  button.style.fontSize = "12px"; 
  button.style.cursor = "pointer";
}


function httpGetString()
{
	var vals = location.search.substring(1, location.search.length).split('&');
	var gets = [];
	for (var i=0; i<vals.length; i++) {
		var t = vals[i].split('=');
		if (t[1]) {
			gets[t[0]] = t[1];
		} else {
			gets[t[0]] = true;
		}
	}

    // Bodge for injury clincs
	if (gets['m'] && !gets['item']) {
		gets['item'] = gets['m'];
	}
	return gets;
}
//	{{{ Function: createIcon
function createIcon(filename, width, height, anchor, hitMap, no_shadow, no_float)
{
	var icon = new GIcon();
	var filename = escape(filename);

	// == All the images ==
	var anchor_str = anchor.x+'x'+(height-anchor.y);	// Y has to be from the bottom
	// XXX: Image URLs must end in .png so the Google API knows to do the IE6 AlphaImageLoader stuff
	icon.image = STATIC_URL + 'icons/'+filename+'-main.png';
	icon.transparent = STATIC_URL + 'icons/'+filename+'-transparent.png';
	icon.printImage = STATIC_URL + 'icons/'+filename+'-print.gif';
	icon.mozPrintImage = STATIC_URL + 'icons/'+filename+'-mozprint.gif';
	if (!no_shadow) {
		icon.shadow = STATIC_URL + 'icons/'+filename+'-shadow.png';
		icon.printShadow = STATIC_URL + 'icons/'+filename+'-printshadow.gif';
	}
	icon.hoverImage = STATIC_URL + 'icons/'+filename+'-hover.png';
	icon.normalImage = icon.image;
	// Emblems
	icon.normalPhoto = STATIC_URL + 'icons/'+filename+'-main-photo.png';
	icon.normalVideo = STATIC_URL + 'icons/'+filename+'-main-video.png';
	icon.normalPhotoVideo = STATIC_URL + 'icons/'+filename+'-main-photo-video.png';
	icon.hoverPhoto = STATIC_URL + 'icons/'+filename+'-hover-photo.png';
	icon.hoverVideo = STATIC_URL + 'icons/'+filename+'-hover-video.png';
	icon.hoverPhotoVideo = STATIC_URL + 'icons/'+filename+'-hover-photo-video.png';



	icon.iconSize = new GSize(width, height);
	icon.shadowSize = new GSize(width*1.5, height);
	icon.iconAnchor = anchor;
	icon.infoWindowAnchor = new GPoint(9, 2);
	if (hitMap) {
		icon.imageMap = hitMap;
	}

	icon.no_float = no_float;
	if (no_float) {
		icon.maxHeight = 0.1; // disable drag floating 
		icon.dragCrossImage = ""; // hide drag cross 
		icon.dragCrossSize = new GSize(0,0); // hide drag cross 
	}

	return icon;
}
//	}}} Function: createIcon
//	{{{ Function: createMarker
function createMarker(point, options)
{
	if (!options.icon) {
		return false;
	}
	if (options.icon.no_float) {
		options.bouncy = false;
		options.dragCrossMove = true;
	}

	var marker = new GMarker(point, options);
	
	marker.updateIcon = function(type)
	{
		var type = type || 'normal';
		var image = this.icon[type + 'Image']
		if (this.has_video && this.has_photo) {
			image = this.icon[type + 'PhotoVideo'];
		} else if (this.has_video) {
			image = this.icon[type + 'Video'];
		} else if (this.has_photo) {
			image = this.icon[type + 'Photo'];
		} 
		try {
			this.setImage(image);
		} catch (e) {
		}
	};




	if (options.icon) {
		marker.icon = options.icon;
		GEvent.bind(marker, 'visibilitychanged', marker,
			function(vis)
			{
				if (!vis) {
					return;
				}
				this.updateIcon('normal');
			}
		);
		GEvent.bind(marker, 'mouseover', marker,
			function()
			{
				this.updateIcon('hover');
			}
		);
		GEvent.bind(marker, 'mouseout', marker,
			function()
			{
				this.updateIcon('normal');
			}
		);
		if (!options.icon.no_float) {
			GEvent.bind(marker, 'dragstart', marker,
				function()
				{
					this.updateIcon('normal');
				}
			);
			GEvent.bind(marker, 'dragend', marker,
				function()
				{
					this.updateIcon('hover');
				}
			);
		}
	}
	return marker;
}
//	}}} Function: createMarker
//	{{{ Namespace: PNG
PNG = {
	getSprite: function(png, img_w, img_h, x, y, w, h)
	{
		var x = x < 0 ? img_w+x : x;
		var y = y < 0 ? img_h+y : y;


		// Container to cut out the area we want
		var container = document.createElement('div');
		with (container.style) {
			width = w +'px';
			height = h +'px';
			overflow = 'hidden';
		}

		// Whole image which is moved to only show the correct section
		var newimg = this.createImage(png, img_w, img_h);
		with (newimg.style) {
			marginLeft = -x +'px';
			marginTop = -y +'px';
		}

		container.appendChild(newimg);
		return container;
	},
	createImage: function(png, width, height)
	{
		var img = document.createElement('div');
		img.style.width = width +'px';
		img.style.height = height +'px';
		img.style.overflow = 'hidden';

		if (IS_IE6) {
			img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+png+'", sizingMethod="crop")';
		} else {
			img.style.background = 'url("'+png+'") no-repeat';
		}
		return img;
	},
	setImage: function(png, img, scale)
	{
		if (scale) {
			if (IS_IE6) {
				img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+png+'", sizingMethod="scale")';
			} else {
				img.style.background = 'url("'+png+'") repeat';
			}
		} else {
			if (IS_IE6) {
				img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+png+'", sizingMethod="crop")';
			} else {
				img.style.background = 'url("'+png+'") no-repeat';
			}
		}
	}
};
//	}}} Namespace: PNG
//	{{{ Namespace: RPC
function RPC()
{
	if (typeof ActiveXObject != "undefined") {
		return ActiveXObject("Microsoft.XMLHTTP");
	} else if (window.XMLHttpRequest) {
		return XMLHttpRequest;
	}
}
RPC = {
	createXMLHTTP: function()
	{
		try {
			if (typeof ActiveXObject != "undefined") {
				return new ActiveXObject("Microsoft.XMLHTTP");
			} else if (window.XMLHttpRequest) {
				return new XMLHttpRequest;
			}
		} catch (e) {}
		return null;
	},
	/**
	 * Gets data from the server then send the RPC to the given callback function
	 * @param string script The server script to post to
	 * @param Function callback The callback function to send the RPC to once complete
	 */
	getData: function(script, callback)
	{
		
		var rpc = RPC.createXMLHTTP();

		// Cache buster is to stop IE6's stupid caching.
		var cache_buster = (new Date()).getTime();
		if (script.indexOf('?') > -1) {
			script += '&rnd=' + cache_buster;
		} else {
			script += '?rnd=' + cache_buster;
		}

		rpc.open("GET", BASE_URL + script, true);
		if (typeof callback == 'function') {
			rpc.onreadystatechange = function()
			{
				//try {
					if (rpc.readyState == 4 && (!rpc.status || rpc.status == 200)) {
						var error = false;
						// Check for error object
						try {
							//error = eval('('+rpc.responseText+')');
						} catch(e) {}

						if (!error.error_code) {
							callback(rpc);
						} else{
							switch (error.error_code) {
							case ERROR_LOGIN_REQUIRED:
								realWorld.user.logout();
								realWorld.user.login(false, callback);
								break;
							default:
								alert('An unknown error occured: '+error.error_code+'\n\n'+error.error_message);
								break;
							}
						}
						delete callback;
					} else if (rpc.readyState == 4 && rpc.status) {
						//alert('Error connecting to server: '+rpc.status);
						
						delete rpc;
						delete callback;
					}
				/*
				} catch (err) {
					
					delete rpc;
					delete callback;
				}
				*/
			};
		}
		rpc.send(null);
		return rpc;
	},
	getJSON: function(script, callback)
	{
		return this.getData(script,
			function(rpc) {
				try {
					var o = eval('('+rpc.responseText+')');
				} catch(e) {
                    
					return false;
				}
				callback(o);
			}
		);
	},
	/**
	 * Posts data to the server then sends the RPC to the given callback function
	 * @param string script The server script to post to
	 * @param string data The query string to post to the script
	 * @param Function callback The callback function to send the RPC to once complete
	 */
	postData: function(script, data, callback)
	{
		
		var rpc = RPC.createXMLHTTP();

		// Cache buster is to stop IE6's stupid caching.
		var cache_buster = (new Date()).getTime();
		if (script.indexOf('?') > -1) {
			script += '&rnd=' + cache_buster;
		} else {
			script += '?rnd=' + cache_buster;
		}

		rpc.open("POST", BASE_URL + script, true);
		if (typeof(rpc.setRequestHeader) != "undefined") {
			rpc.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
			rpc.setRequestHeader("Connection", "close");
		}
		if (typeof callback == 'function') {
			rpc.onreadystatechange = function()
			{
				try {
					if (rpc.readyState == 4 && (!rpc.status || rpc.status == 200)) {
						var error = false;
						// Check for error object
						try {
							error = eval('('+rpc.responseText+')');
						} catch(e) {}

						if (!error.error_code) {
							callback(rpc);
						} else{
							switch (error.error_code) {
								case ERROR_LOGIN_REQUIRED:
									realWorld.user.logout();
									alert('You are not logged in');
									break;
								default:
									alert('Error: ' + error.error_message);
									break;
							}
						}
						delete callback;
					} else if (rpc.readyState == 4 && rpc.status) {
						//alert('Error connecting to server: '+rpc.status);
						
						delete rpc;
						delete callback;
					}
				} catch (err) {
					
					delete rpc;
					delete callback;
				}
			};
		}
		rpc.send(data);
		return rpc;
	}
};	
//	}}} Namespace: RPC
//	{{{ Addons
GEvent.stop = function(e, burst)
{
	if (!e) {
		return;
	}
	if (e.preventDefault) {
		if (burst !== false) {
			e.preventDefault();
		}
		if (burst !== undefined) {
			e.stopPropagation();
		}
	} else {
		if (burst !== false) {
			e.returnValue = false;
		}
		if (burst !== undefined) {
			e.cancelBubble = true;
		}
	}
};
function setOpacity(el, op, notForIE)
{
	if (!notForIE && typeof el.style.filter == 'string') {
		el.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity='+(op*100)+')';
	} else {
		el.style.opacity = op;
	}
	el._style_opacity = op;
};
//	}}} Addons


var METRES2MILES = 1609.344;
var METRES2KMS = 1000;
var IS_IE = (navigator.userAgent.indexOf('MSIE') != -1 && navigator.userAgent.indexOf('Opera') == -1);
var IS_IE6 = (navigator.userAgent.indexOf('MSIE 6') != -1 && navigator.userAgent.indexOf('Opera') == -1);
var IS_IE7 = (navigator.userAgent.indexOf('MSIE 7') != -1 && navigator.userAgent.indexOf('Opera') == -1);
var IS_KHTML = (navigator.userAgent.indexOf('KHTML') != -1);
var IS_GECKO = (navigator.userAgent.indexOf('Gecko') != -1);



var MY_ITEMS = -1;
var PAGE_PARAMS = httpGetString();
//		{{{ Enum: RW_MODE
var RW_MODE_BROWSE = 0;
var RW_MODE_CREATE = 1;
var RW_MODE_EDIT   = 2;
var RW_MODE_ADMIN  = 3;
//		}}} Enum: RW_MODE
//		{{{ Enum: RW_ROUTE
var RW_ROUTE_RUNNING = 1;
var RW_ROUTE_CYCLING = 2;
var RW_ROUTE_WALKING = 3;
var RW_ROUTE_HIKING  = 4;
//		}}} Enum: RW_ROUTE
//		{{{ Enum: ERROR
var ERROR_LOGIN_REQUIRED = 1;
var ERROR_ALREADY_RATED  = 5;
//		}}} Enum: ERROR
//		{{{ MARKER_CATEGORIES
var MARKER_CATEGORIES = {
	
	54: {
		filterable: true,
		private: false,
		icon: 'icon_75',
		title: 'Activity centres',
		parent: 0
	},
	
	32: {
		filterable: true,
		private: false,
		icon: 'icon_38',
		title: 'Aeroplane',
		parent: 0
	},
	
	55: {
		filterable: true,
		private: false,
		icon: 'icon_76',
		title: 'Alternative medicine',
		parent: 0
	},
	
	4: {
		filterable: true,
		private: false,
		icon: 'icon_9',
		title: 'Ambulance',
		parent: 0
	},
	
	56: {
		filterable: true,
		private: false,
		icon: 'icon_77',
		title: 'Athletics',
		parent: 0
	},
	
	5: {
		filterable: true,
		private: false,
		icon: 'icon_10',
		title: 'Bank',
		parent: 0
	},
	
	2: {
		filterable: true,
		private: false,
		icon: 'icon_2',
		title: 'Bar',
		parent: 0
	},
	
	43: {
		filterable: true,
		private: false,
		icon: 'icon_49',
		title: 'Blog post',
		parent: 0
	},
	
	57: {
		filterable: true,
		private: false,
		icon: 'icon_78',
		title: 'Boxing',
		parent: 0
	},
	
	6: {
		filterable: true,
		private: false,
		icon: 'icon_11',
		title: 'Bureau de Change',
		parent: 0
	},
	
	7: {
		filterable: true,
		private: false,
		icon: 'icon_12',
		title: 'Bus',
		parent: 0
	},
	
	8: {
		filterable: true,
		private: false,
		icon: 'icon_13',
		title: 'Cafe',
		parent: 0
	},
	
	9: {
		filterable: true,
		private: false,
		icon: 'icon_14',
		title: 'Campsite',
		parent: 0
	},
	
	10: {
		filterable: true,
		private: false,
		icon: 'icon_15',
		title: 'Car',
		parent: 0
	},
	
	11: {
		filterable: true,
		private: false,
		icon: 'icon_16',
		title: 'Cinema',
		parent: 0
	},
	
	12: {
		filterable: true,
		private: false,
		icon: 'icon_17',
		title: 'Club',
		parent: 0
	},
	
	13: {
		filterable: true,
		private: false,
		icon: 'icon_18',
		title: 'College / University',
		parent: 0
	},
	
	58: {
		filterable: true,
		private: false,
		icon: 'icon_79',
		title: 'Cricket',
		parent: 0
	},
	
	59: {
		filterable: true,
		private: false,
		icon: 'icon_80',
		title: 'Cycling',
		parent: 0
	},
	
	60: {
		filterable: true,
		private: false,
		icon: 'icon_81',
		title: 'Dance studio',
		parent: 0
	},
	
	14: {
		filterable: true,
		private: false,
		icon: 'icon_19',
		title: 'Dentist',
		parent: 0
	},
	
	61: {
		filterable: true,
		private: false,
		icon: 'icon_82',
		title: 'Diet meetings',
		parent: 0
	},
	
	15: {
		filterable: true,
		private: false,
		icon: 'icon_20',
		title: 'Doctor',
		parent: 0
	},
	
	62: {
		filterable: true,
		private: false,
		icon: 'icon_83',
		title: 'Dojo',
		parent: 0
	},
	
	63: {
		filterable: true,
		private: false,
		icon: 'icon_84',
		title: 'Dry ski',
		parent: 0
	},
	
	16: {
		filterable: true,
		private: false,
		icon: 'icon_21',
		title: 'Event',
		parent: 0
	},
	
	64: {
		filterable: true,
		private: false,
		icon: 'icon_85',
		title: 'Extreme sports',
		parent: 0
	},
	
	65: {
		filterable: true,
		private: false,
		icon: 'icon_86',
		title: 'Farmers market',
		parent: 0
	},
	
	17: {
		filterable: true,
		private: false,
		icon: 'icon_22',
		title: 'Ferry',
		parent: 0
	},
	
	18: {
		filterable: true,
		private: false,
		icon: 'icon_23',
		title: 'Fire service',
		parent: 0
	},
	
	66: {
		filterable: true,
		private: false,
		icon: 'icon_87',
		title: 'Football',
		parent: 0
	},
	
	1: {
		filterable: true,
		private: false,
		icon: 'default',
		title: 'General / Miscellaneous',
		parent: 0
	},
	
	67: {
		filterable: true,
		private: false,
		icon: 'icon_88',
		title: 'Golf',
		parent: 0
	},
	
	19: {
		filterable: true,
		private: false,
		icon: 'icon_24',
		title: 'Government',
		parent: 0
	},
	
	20: {
		filterable: true,
		private: false,
		icon: 'icon_25',
		title: 'Gym',
		parent: 0
	},
	
	68: {
		filterable: true,
		private: false,
		icon: 'icon_89',
		title: 'Healthy eating',
		parent: 0
	},
	
	69: {
		filterable: true,
		private: false,
		icon: 'icon_90',
		title: 'Hockey',
		parent: 0
	},
	
	21: {
		filterable: true,
		private: false,
		icon: 'icon_27',
		title: 'Holiday / Travel',
		parent: 0
	},
	
	46: {
		filterable: false,
		private: true,
		icon: 'icon_52',
		title: 'Holiday Inn',
		parent: 0
	},
	
	48: {
		filterable: false,
		private: true,
		icon: 'icon_54',
		title: 'Holiday Inn \x2D Crowne Plaza',
		parent: 0
	},
	
	49: {
		filterable: false,
		private: true,
		icon: 'icon_55',
		title: 'Holiday Inn \x2D InterContinental',
		parent: 0
	},
	
	47: {
		filterable: false,
		private: true,
		icon: 'icon_53',
		title: 'Holiday Inn Express',
		parent: 0
	},
	
	70: {
		filterable: true,
		private: false,
		icon: 'icon_91',
		title: 'Horse racing',
		parent: 0
	},
	
	22: {
		filterable: true,
		private: false,
		icon: 'icon_28',
		title: 'Hospital',
		parent: 0
	},
	
	23: {
		filterable: true,
		private: false,
		icon: 'icon_29',
		title: 'Hotel/Hostel',
		parent: 0
	},
	
	24: {
		filterable: true,
		private: false,
		icon: 'icon_30',
		title: 'House',
		parent: 0
	},
	
	71: {
		filterable: true,
		private: false,
		icon: 'icon_92',
		title: 'Ice rink',
		parent: 0
	},
	
	45: {
		filterable: false,
		private: false,
		icon: 'icon_51',
		title: 'Injury Clinic',
		parent: 0
	},
	
	26: {
		filterable: true,
		private: false,
		icon: 'icon_32',
		title: 'Internet',
		parent: 0
	},
	
	51: {
		filterable: true,
		private: false,
		icon: 'icon_62',
		title: 'Landmark',
		parent: 0
	},
	
	27: {
		filterable: true,
		private: false,
		icon: 'icon_33',
		title: 'Library',
		parent: 0
	},
	
	72: {
		filterable: true,
		private: false,
		icon: 'icon_93',
		title: 'Netball',
		parent: 0
	},
	
	73: {
		filterable: true,
		private: false,
		icon: 'icon_94',
		title: 'Park',
		parent: 0
	},
	
	28: {
		filterable: true,
		private: false,
		icon: 'icon_34',
		title: 'Park / Forest',
		parent: 0
	},
	
	29: {
		filterable: true,
		private: false,
		icon: 'icon_35',
		title: 'Petrol station',
		parent: 0
	},
	
	94: {
		filterable: true,
		private: false,
		icon: 'icon_20',
		title: 'Pharmacy',
		parent: 0
	},
	
	50: {
		filterable: true,
		private: false,
		icon: 'icon_61',
		title: 'Photo',
		parent: 0
	},
	
	31: {
		filterable: true,
		private: false,
		icon: 'icon_37',
		title: 'Picnic site',
		parent: 0
	},
	
	33: {
		filterable: true,
		private: false,
		icon: 'icon_39',
		title: 'Police',
		parent: 0
	},
	
	74: {
		filterable: true,
		private: false,
		icon: 'icon_95',
		title: 'Pool',
		parent: 0
	},
	
	34: {
		filterable: true,
		private: false,
		icon: 'icon_40',
		title: 'Public house',
		parent: 0
	},
	
	44: {
		filterable: false,
		private: true,
		icon: 'icon_50',
		title: 'Puma Stores',
		parent: 0
	},
	
	75: {
		filterable: true,
		private: false,
		icon: 'icon_96',
		title: 'Railway',
		parent: 0
	},
	
	35: {
		filterable: true,
		private: false,
		icon: 'icon_41',
		title: 'Restaurant',
		parent: 0
	},
	
	76: {
		filterable: true,
		private: false,
		icon: 'icon_97',
		title: 'Rugby',
		parent: 0
	},
	
	77: {
		filterable: true,
		private: false,
		icon: 'icon_98',
		title: 'Running',
		parent: 0
	},
	
	78: {
		filterable: true,
		private: false,
		icon: 'icon_99',
		title: 'Sailing',
		parent: 0
	},
	
	79: {
		filterable: true,
		private: false,
		icon: 'icon_100',
		title: 'Salad bars',
		parent: 0
	},
	
	36: {
		filterable: true,
		private: false,
		icon: 'icon_42',
		title: 'School',
		parent: 0
	},
	
	53: {
		filterable: true,
		private: false,
		icon: 'icon_66',
		title: 'Shopping',
		parent: 0
	},
	
	80: {
		filterable: true,
		private: false,
		icon: 'icon_101',
		title: 'Skate boarding',
		parent: 0
	},
	
	81: {
		filterable: true,
		private: false,
		icon: 'icon_102',
		title: 'Ski',
		parent: 0
	},
	
	82: {
		filterable: true,
		private: false,
		icon: 'icon_103',
		title: 'Spas',
		parent: 0
	},
	
	42: {
		filterable: true,
		private: false,
		icon: 'icon_48',
		title: 'Sport',
		parent: 0
	},
	
	95: {
		filterable: true,
		private: false,
		icon: 'sport_relief',
		title: 'Sport Relief',
		parent: 0
	},
	
	97: {
		filterable: true,
		private: false,
		icon: 'sport\x2Drelief\x2Dregional\x2Dmile',
		title: 'Sport Relief Local Mile',
		parent: 0
	},
	
	83: {
		filterable: true,
		private: false,
		icon: 'icon_104',
		title: 'Sports centre',
		parent: 0
	},
	
	84: {
		filterable: true,
		private: false,
		icon: 'icon_105',
		title: 'Sports practitioner',
		parent: 0
	},
	
	85: {
		filterable: true,
		private: false,
		icon: 'icon_106',
		title: 'Sports shop',
		parent: 0
	},
	
	86: {
		filterable: true,
		private: false,
		icon: 'icon_107',
		title: 'Squash',
		parent: 0
	},
	
	87: {
		filterable: true,
		private: false,
		icon: 'icon_108',
		title: 'Surfing',
		parent: 0
	},
	
	88: {
		filterable: true,
		private: false,
		icon: 'icon_109',
		title: 'Swimming',
		parent: 0
	},
	
	37: {
		filterable: true,
		private: false,
		icon: 'icon_43',
		title: 'Taxi',
		parent: 0
	},
	
	30: {
		filterable: true,
		private: false,
		icon: 'icon_36',
		title: 'Telephone',
		parent: 0
	},
	
	89: {
		filterable: true,
		private: false,
		icon: 'icon_110',
		title: 'Tennis',
		parent: 0
	},
	
	3: {
		filterable: false,
		private: true,
		icon: 'start_flag',
		title: 'Test flag',
		parent: 0
	},
	
	38: {
		filterable: true,
		private: false,
		icon: 'icon_44',
		title: 'Theatre',
		parent: 0
	},
	
	52: {
		filterable: true,
		private: false,
		icon: 'icon_63',
		title: 'Toilet',
		parent: 0
	},
	
	25: {
		filterable: true,
		private: false,
		icon: 'icon_31',
		title: 'Tourist information / General information',
		parent: 0
	},
	
	39: {
		filterable: true,
		private: false,
		icon: 'icon_45',
		title: 'Train',
		parent: 0
	},
	
	40: {
		filterable: true,
		private: false,
		icon: 'icon_46',
		title: 'Tram',
		parent: 0
	},
	
	90: {
		filterable: true,
		private: false,
		icon: 'icon_111',
		title: 'Underground',
		parent: 0
	},
	
	91: {
		filterable: true,
		private: false,
		icon: 'icon_112',
		title: 'Vitamin stores',
		parent: 0
	},
	
	92: {
		filterable: true,
		private: false,
		icon: 'icon_113',
		title: 'Water parks',
		parent: 0
	},
	
	41: {
		filterable: true,
		private: false,
		icon: 'icon_47',
		title: 'WiFi',
		parent: 0
	},
	
	93: {
		filterable: true,
		private: false,
		icon: 'icon_114',
		title: 'Yoga',
		parent: 0
	}
	
};
//		}}} MARKER_CATEGORIES
//		{{{ ROUTE_CATEGORIES
var ROUTE_CATEGORIES = {
	getNextColour: function(id) {
		if (!this[id]) {
			return '#ff0000';
		}
		var colour = this[id].colours[this[id].index];

		this[id].index++;
		while (this[id].index >= this[id].colours.length) {
			this[id].index -= this[id].colours.length;
		}

		return colour;
	},
	
	11: {
		filterable: true,
		private: false,
		colours: ('#FF0000,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Sport Relief Training Run',
		parent: 0,
		is_parent: false,
		start_flag: 'sport_relief_training',
		end_flag: 'mini_end_flag',
		index: 0
	},
	
	10: {
		filterable: true,
		private: true,
		colours: ('#FF0000,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Sport Relief',
		parent: 0,
		is_parent: false,
		start_flag: 'sport_relief',
		end_flag: 'mini_end_flag',
		index: 0
	},
	
	5: {
		filterable: true,
		private: false,
		colours: ('#0000FF,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Running',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	1: {
		filterable: true,
		private: false,
		colours: ('#0000FF,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Other running',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	9: {
		filterable: true,
		private: false,
		colours: ('#FF6600').split(','),
		title: 'General travel',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	8: {
		filterable: true,
		private: false,
		colours: ('#FFFF00').split(','),
		title: 'Backpacking',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	6: {
		filterable: true,
		private: false,
		colours: ('#4AD840,#17970D,#04621C,#36C55B,#41792D,#6FE645,#048C2D,#45974A,#215116').split(','),
		title: 'Walking',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	7: {
		filterable: true,
		private: false,
		colours: ('#E40D0D,#B90404,#D2373E,#7F191E,#F1000B,#C2301D,#5A130A,#CA1224').split(','),
		title: 'Cycling',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	}
	
};
//		}}} ROUTE_CATEGORIES
//      {{{ COMMUNITY_GROUPS
if ('' != "None") {
	var COMMUNITY_GROUPS = {
		
	}
} else {
	var COMMUNITY_GROUPS = null;
}

var ROUTE_EDIT_WIDTH = 4;
var ROUTE_VIEW_WIDTH = 2;
var MIN_ZOOM = 11;
var MAX_ZOOM = 17;
var MEDIA_URL = "http://www.realbuzz.com/static/uploads/";
//var SKIN = 'realbuzz';
var SKIN = 'realbuzz';

var BASE_URL = '/mapyourpassion/';
//var FULL_URL = BASE_URL;
var FULL_URL = location.protocol + '//' + location.host + BASE_URL;
var STATIC_URL = '/static/maps/';
var SKIN_URL =  STATIC_URL + 'skins/' + SKIN + '/';
var SITE_URL = _OPTIONS.site_url || (location.protocol+'//'+location.host+location.pathname);
var CACHE_ITEMS = false;
var ITEMS_PER_DRAW = 3;
var SEGMENTED_POLYLINE = (IS_IE || IS_KHTML);
//var SMOOTH_PAN = (!IS_IE);
var SMOOTH_PAN = false;



FloatingPanel = new Class();
FloatingPanel.numberOfPanels = 0;
FloatingPanel.openPanels = [];
FloatingPanel.prototype = {
	__construct: function(x, y, width, height, skin)
	{
		
		var x = x || 100;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;
		var skin = skin || 'realbuzz';
		this.width = w;
		this.height = h + 40;   // 40 is to take into account the large panel edge

		this.skin = skin;

		if (!this.minimumSize) {
			this.minimumSize = {
				width: 200,
				height: 200
			};
		}

		if (!this.padding) {
			this.padding = {
				left: 6,
				top: 9,
				right: 21,
				bottom: 48
			};
		}

		// Anchor is used when zooming the map
		if (!this.anchor) {
			this.anchor = {
				x: 0,
				y: 0
			};
		}


		// Add to static array of all open panels
		// TODO Get index so it can be deleted on destrction
		FloatingPanel.openPanels.push(this);
		FloatingPanel.numberOfPanels++;

		// Check if box was created in an extended class
		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/panel.png', 800, 580, this.width, this.height, 32, 32, 64, 64);
		}
		this.container = this.box.container;
		this.container.className = 'gmnoprint panel';
		with (this.container.style) {
			position = 'absolute';
			left = x +'px';
			top = y +'px';
			zIndex = '1000';
		}

		this.closeButton = document.createElement('div');
		this.closeButton.className = 'panel_close';
		with (this.closeButton.style) {
			position = 'absolute';
			right = (this.padding.right+9) +'px';
			top = (this.padding.top +8) +'px';
			width = '20px';
			height = '20px';
			background = 'url('+SKIN_URL+'images/panel_close.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}
		GEvent.bindDom(this.closeButton, 'mouseover', this.closeButton, function(e) { this.style.backgroundPosition = '0 20px'; });
		GEvent.bindDom(this.closeButton, 'mouseout', this.closeButton, function(e) { this.style.backgroundPosition = '0 0'; });
		GEvent.bindDom(this.closeButton, 'click', this, function() { this.close() });
		this.container.appendChild(this.closeButton);

		this.title = document.createElement('div');
		this.title.appendChild(document.createElement('div'));
		this.title.className = 'panel_title';
		with (this.title.style) {
			position = 'absolute';
			overflow = 'hidden';
			top = this.padding.top+'px';
			left = this.padding.left+'px';
			//whiteSpace = 'nowrap';
		}
		this.title.firstChild.style.padding = '4px 30px 4px 15px';
		this.setTitle('Untitled panel');
		this.container.appendChild(this.title);
		GEvent.bindDom(this.title, 'mousedown', this, this.startDrag);

		this.content = document.createElement('div');
		this.content.appendChild(document.createElement('div'));
		this.content.className = 'panel_content';
		with (this.content.style) {
			position = 'absolute';
			left = this.padding.left+'px';
			top = 0;
			overflow = 'hidden';
		}

		this.container.appendChild(this.content);


		// Resizer
		this.resizer = PNG.createImage(SKIN_URL+'images/resize.png', 12, 12);
		with (this.resizer.style) {
			position = 'absolute';
			bottom = (this.padding.bottom+1)+'px';
			right = (this.padding.right+1)+'px';
			cursor = 'se-resize';
			display = 'none';
		}
		this.container.appendChild(this.resizer);
		GEvent.bindDom(this.resizer, 'mousedown', this, this.startResize);
		GEvent.bindDom(this.container, 'mousedown', this, this.bringToFront);

		this.resizable(true);
		this.movable(true);

		this.container.style.visibility = 'hidden';


		// Add panel into DOM
		realWorld.content.appendChild(this.container);

		this.setZ(FloatingPanel.numberOfPanels);

		GEvent.trigger(FloatingPanel, 'created', this);

		this.resize(this.width, this.height);

	},
	__destruct: function()
	{
		this.endDrag();
		if (this.container && this.container.parentNode) {
			this.container.parentNode.removeChild(this.container);
		}
		for (x in FloatingPanel.openPanels) {
			var p = FloatingPanel.openPanels[x];
			if (p == this) {
				delete FloatingPanel.openPanels[x];
				delete p;
				FloatingPanel.numberOfPanels--;
				break;
			}
		}
        GEvent.clearListeners(this);
	},
	setTitle: function(title)
	{
		while (this.title.firstChild.firstChild) {
			this.title.firstChild.removeChild(this.title.firstChild.firstChild);
		}
		this.title.firstChild.appendChild(document.createTextNode(title));
	},
	setToolbar: function(node)
	{
		if (!this.toolbar) {
			this.toolbar = T.div('test');
			this.container.insertBefore(this.toolbar, this.content);
		}
	},
	setContent: function(node)
	{
		while (this.content.firstChild) {
			this.content.removeChild(this.content.firstChild);
		}
		this.content.appendChild(node);
	},
	addContent: function(node)
	{
		this.content.appendChild(node);
	},
	resizable: function(status)
	{
		if (status === undefined) {
			return this._resizable;
		}
		this._resizable = status;
		if (status) {
			this.resizer.style.display = 'block';
		} else {
			this.resizer.style.display = 'none';
		}
	},
	movable: function(status)
	{
		if (status === undefined) {
			return this._movable;
		}
		this._movable = status;
		if (this._movable) {
			this.title.style.cursor = 'move';
		} else {
			this.title.style.cursor = 'default';
		}
	},
	resize: function(width, height, animated, aniStep)
	{
		var aniStep = aniStep || 20;
		if (animated) {
			if (this._aniResizeTimer) {
				clearTimeout(this._aniResizeTimer);
			}
			this._targetWidth = width || this.width;
			this._targetHeight = height || this.height;
			this.aniResize(width<this.width?-aniStep:aniStep, height<this.height?-aniStep:aniStep);
		} else {
			this.width = width || this.width;
			this.height = height || this.height;

			
			this.title.style.width = (this.width -this.padding.left -this.padding.right) +'px';
			this.content.style.top = (this.title.offsetHeight +this.padding.top) +'px';
			this.content.style.width = (this.width -this.padding.left -this.padding.right) +'px';
			this.content.style.height = (this.height -this.title.offsetHeight -this.padding.top - this.padding.bottom) +'px';
			this.box.resize(this.width, this.height);

			// Resize busy indicator
			if (this._busyIndicator) {
				this._busyIndicator.style.top = this.content.style.top;
				this._busyIndicator.style.left = this.content.style.left;
				this._busyIndicator.style.width = this.content.style.width;
				this._busyIndicator.style.height = this.content.style.height;
			}
		}
		GEvent.trigger(this, 'resize', this);
	},
	aniResize: function(w_inc, h_inc)
	{
		var newW = this.width + w_inc;
		var newH = this.height + h_inc;
		if (w_inc < 0 && newW < this._targetWidth) {
			newW = this._targetWidth;
		} else if (w_inc > 0 && newW > this._targetWidth) {
			newW = this._targetWidth;
		}
		if (h_inc < 0 && newH < this._targetHeight) {
			newH = this._targetHeight;
		} else if (h_inc > 0 && newH > this._targetHeight) {
			newH = this._targetHeight;
		}

		this.resize(newW, newH);

		if (newW != this._targetWidth || newH != this._targetHeight) {
			if (newW == this._targetWidth) {
				w_inc = 0;
			}
			if (newH == this._targetHeight) {
				h_inc = 0;
			}

			this._aniResizeTimer = setTimeout(GEvent.callback(this, function() {
				this.aniResize(w_inc, h_inc);
			}), 10);
		}
	},
	resizeToContent: function(animated)
	{
		GEvent.trigger(this, 'beforeresizetocontent');
		// FIXME : This ugly height hack is because IE sucks and can't obtain the correct scrollHeight
		this.content.style.height = '30000px';
		if (this.content.scrollHeight >= 20000) {
			this.content.style.height = 'auto';
		}
		var height = this.content.scrollHeight+this.title.scrollHeight+this.padding.top+this.padding.bottom;
        this.content.scrollTop = 0; // Fix for firefox chopping off the top
		this.resize(false, height, animated);
	},
	move: function(left, top)
	{
		if (left !== false) {
			this.container.style.left = left +'px';
		}
		if (top !== false) {
			this.container.style.top = top +'px';
		}
	},
	getX: function()
	{
		return this.container.offsetLeft;
	},
	getY: function()
	{
		return this.container.offsetTop;
	},
	getZ: function()
	{
		return this._z;
	},
	setZ: function(z)
	{
		this._z = z;
		this.container.style.zIndex = z;
	},
	getWidth: function()
	{
		return this.container.offsetWidth;
	},
	getHeight: function()
	{
		return this.container.offsetHeight;
	},
	getContentWidth: function()
	{
		return this.content.offsetWidth;
	},
	getContentHeight: function()
	{
		return this.content.offsetHeight;
	},
	startDrag: function(e)
	{
		if (!this._movable) {
			return;
		}
		this.endDrag();
		this.endResize();
		this._dragOffsetX = this.getX() - e.clientX;
		this._dragOffsetY = this.getY() - e.clientY;

		this._dragHandler = GEvent.bindDom(document.documentElement, 'mousemove', this, this.doDrag);
		this._dragEndHandler = GEvent.bindDom(document.documentElement, 'mouseup', this, this.endDrag);
		setOpacity(this.container, 0.8, true);
		GEvent.trigger(this, 'dragstart', this);
		GEvent.stop(e);
	},
	doDrag: function(e)
	{
		var left = (e.clientX + this._dragOffsetX);
		var top = (e.clientY + this._dragOffsetY);

		// Only restrict positioning if the panel isn't stuck to the map
		if (!this.sticky) {
			if (left < -this.getWidth() /2) {
				left = -this.getWidth() /2;
			} else if (left > document.body.clientWidth - this.getWidth() /2) {
				left = document.body.clientWidth - this.getWidth() /2;
			}
			if (top < 24) {
				top = 24;
			} else if (top > this.container.parentNode.clientHeight - 26) {
				top = this.container.parentNode.clientHeight - 26;
			}
		}
		this.move(left, top);
		GEvent.trigger(this, 'drag', this);
		GEvent.stop(e);
	},
	endDrag: function(e)
	{
		if (this._dragHandler) {
			GEvent.removeListener(this._dragHandler);
			delete this._dragHandler;
		}
		if (this._dragEndHandler) {
			GEvent.removeListener(this._dragEndHandler);
			delete this._dragEndHandler;
		}
		if (e) {
			if (this._stickyPoint) {
				delete this._stickyPoint;
				this.stickToMap();
			}
			GEvent.trigger(this, 'dragend', this);
			setOpacity(this.container, 1, true);
		}
	},
	startResize: function(e)
	{
		this.endDrag();
		this.endResize();
		this._resizeOffsetX = (this.getX()+this.getWidth()) - e.clientX;
		this._resizeOffsetY = (this.getY()+this.getHeight()) - e.clientY;

		this._resizeHandler = GEvent.bindDom(document.documentElement, 'mousemove', this, this.doResize);
		this._resizeEndHandler = GEvent.bindDom(document.documentElement, 'mouseup', this, this.endResize);
		GEvent.trigger(this, 'resizestart', this);
		GEvent.stop(e);
	},
	doResize: function(e)
	{
		var width = e.clientX - this.getX() +this._resizeOffsetX;
		var height = e.clientY - this.getY() +this._resizeOffsetY;
		if (width < this.minimumSize.width) {
			width = this.minimumSize.width;
		} else if (width > 1000) {
			width = 1000;
		}
		if (height < this.minimumSize.height) {
			height = this.minimumSize.height;
		} else if (height > 1000) {
			height = 1000;
		}
		this.resize(width, height);
		GEvent.stop(e);
	},
	endResize: function(e)
	{
		if (this._resizeHandler) {
			GEvent.removeListener(this._resizeHandler);
			delete this._resizeHandler;
		}
		if (this._resizeEndHandler) {
			GEvent.removeListener(this._resizeEndHandler);
			delete this._resizeEndHandler;
		}
		if (e) {
			GEvent.trigger(this, 'resizeend', this);
		}
	},
	bringToFront: function()
	{
		for (var i=0; i<FloatingPanel.openPanels.length; i++) {
			var p = FloatingPanel.openPanels[i];
			if (!p || p == this) {
				continue;
			}

			//p.box.setImage('skins/'+p.skin+'/panel/frame-dull.png');
			if (p.getZ() > this.getZ()) {
				p.setZ(p.getZ()-1);
			}
		}

		//this.box.setImage('skins/'+this.skin+'/panel/frame.png');
		this.setZ(FloatingPanel.numberOfPanels);
	},
	hide: function()
	{
		this.visible = false;
		// IE can't change the opacity of PNGs properly, so don't bother
		if (typeof this.container.style.filter != 'string') {
			this._opacity = 1;
			setOpacity(this.container, 1);
			setTimeout(GEvent.callback(this, this.fadeOut), 10);
		} else {
			GEvent.trigger(this, 'hide', this);
			this.container.style.visibility = 'hidden';
		}
	},
	show: function()
	{
		this.visible = true;
		// IE can't change the opacity of PNGs properly, so don't bother
		if (typeof this.container.style.filter != 'string') {
			this._opacity = 0;
			setOpacity(this.container, 1);
			//setTimeout(GEvent.callback(this, this.fadeIn), 10);
		}
		this.container.style.visibility = 'visible';
		GEvent.trigger(this, 'show');

		if (this.sticky) {
			this.panIntoView();
		}
	},
	panIntoView: function()
	{
		if (!this.sticky) {
			return;
		}

		var map = this._stickyMap;
		if (!map) {
			return;
		}

		this.attachAnchor(this._attachedAnchor);

		// Calculate the screen X/Y pixel of the map
		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		var corner = new GPoint(this.getX() + sw.x, this.getY() + ne.y);
		var centre = new GPoint(this.getX() + sw.x + (this.getWidth()/2), this.getY() + ne.y + (this.getHeight()/2));
		corner = map.fromDivPixelToLatLng(corner);
		centre = map.fromDivPixelToLatLng(centre);

		// If the top left corner isn't on screen, then we need to pan to see the panel
		//if (!map.getBounds().containsLatLng(corner)) {
			map.setCenter(centre);
			//map.panTo(centre);
		//}

	},
	fadeIn: function()
	{
		this._opacity += 0.25;
		// Round off due to floating point errors
		this._opacity = Math.roundDP(this._opacity, 2);

		if (this._opacity > 1) {
			this._opacity = 1;
		}
		setOpacity(this.container, this._opacity);
		if (this._opacity < 1) {
			setTimeout(GEvent.callback(this, this.fadeIn), 10);
		}
	},
	fadeOut: function()
	{
		this._opacity -= 0.25;
		// Round off due to floating point errors
		this._opacity = Math.roundDP(this._opacity, 2);

		if (this._opacity < 0) {
			this._opacity = 0;
		}
		setOpacity(this.container, this._opacity);
		if (this._opacity > 0) {
			setTimeout(GEvent.callback(this, this.fadeOut), 10);
		} else {
			GEvent.trigger(this, 'hide', this);
			this.container.style.visibility = 'hidden';
		}
	},
	close: function(force)
	{
		if (this.busy() && !force) {
			//return;
		}
		GEvent.trigger(this, 'beforeclose', this);
		if (this.preventClosing) {
			return;
		}

		// Hide to cause fade out, and kill window once hidden
		GEvent.bind(this, 'hide', this, function() {
			GEvent.trigger(this, 'close', this);
			this.__destruct();
		});
		this.hide();
	},
	updateStickyPanelPosition: function(map)
	{
		var map = map || this._stickyMap;
		if (!map) {
			return false;
		}
		// Calculate the screen X/Y pixel of the map
		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		if (!this._stickyPoint) {
			this._stickyPoint = new GPoint(this.getX()+sw.x, this.getY()+ne.y);
		}

		var point = new GPoint(this._stickyPoint.x, this._stickyPoint.y);
		point.x -= sw.x;
		point.y -= ne.y;
		this.move(point.x, point.y);
	},
	stickToMap: function(map)
	{
		var map = map || this._stickyMap;
		
		if (!map) {
			
			return;
		}
		this.sticky = true;
		this._stickyMap = map;
		if (!this._stickyEventHandlers) {
			this._stickyEventHandlers = [
				GEvent.bind(map, 'move', this, this.updateStickyPanelPosition),
				GEvent.bind(map, 'moveend', this, this.updateStickyPanelPosition),
				GEvent.bind(map, 'moveend', this, function() { this.attachAnchor(this._attachedAnchor); })
			];
		}

		this.updateStickyPanelPosition();


        // Clean up to avoid memory leaks
        GEvent.bind(this, 'close', this,
            function()
            {
                this.unstickFromMap();
                this.detachAnchor();
            }
        );

	},
	unstickFromMap: function()
	{
		this.sticky = false;
		delete this._stickyPoint;
		delete this._stickyMap;
        if (this._stickyEventHandlers) {
            for (var i=0; i<this._stickyEventHandlers.length; i++) {
                GEvent.removeListener(this._stickyEventHandlers[i]);
            }
            delete this._stickyEventHandlers;
        }
	},
	getAnchorPoint: function(map)
	{
		var map = map || this._stickyMap;
		if (!map) {
			return false;
		}

		// Calculate the screen X/Y pixel of the map
		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		var point = new GPoint(this.getX()+sw.x+this.anchor.x, this.getY()+ne.y+this.anchor.y);
		return point;
	},
	getAnchorLatLng: function(map)
	{
		return map.fromDivPixelToLatLng(this.getAnchorPoint(map));
	},
	attachAnchor: function(point, map)
	{
		var map = map || this._stickyMap;
		if (!map) {
			return false;
		}

		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		if (point instanceof GLatLng) {
			this._attachedAnchor = point;
			point = map.fromLatLngToDivPixel(point);
		} else {
			this._attachedAnchor = map.fromDivPixelToLatLng(point);
		}

		point.x -= sw.x;
		point.y -= ne.y;

		

		this.move(point.x-this.anchor.x, point.y-this.anchor.y);
		if (this._stickyPoint) {
			delete this._stickyPoint;
			this.stickToMap();
		}
		this.resizable(false);
		this.movable(false);

		// Listen for zooms to adjust position
		if (!this._anchorZoomListener) {
			this._anchorZoomListener = GEvent.bind(map, 'zoomend', this, function() {
					this.attachAnchor(this._attachedAnchor);
			});
		}
	},
	detachAnchor: function()
	{
		if (this._anchorZoomListener) {
			GEvent.removeListener(this._anchorZoomListener);
			delete this._anchorZoomListener;
		}
		delete this._attachedAnchor;
	},
	busy: function(status)
	{
		if (status === undefined) {
			return this._busy;
		}
		if (this._busy == status) return;

		this._busy = status;
		if (this._busy) {
			if (!this._busyIndicator) {
				this._busyIndicator = document.createElement('div');
				this._busyIndicator.appendChild(document.createElement('div'));
				this._busyIndicator.className = 'panel_busy_container';
				this._busyIndicator.firstChild.className = 'panel_busy_indicator';


				this.container.appendChild(this._busyIndicator);
				with (this._busyIndicator.style) {
					position = 'absolute';
				}
				with (this._busyIndicator.firstChild.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
				}
				PNG.setImage(SKIN_URL+'images/busy_background.png', this._busyIndicator, true);
			}
			this._busyIndicator.style.display = 'block';
		} else {
			if (this._busyIndicator) {
				this._busyIndicator.style.display = 'none';
			}
		}
		// XXX Forces the busyIndicator box to be resized correctly in some browsers
		this.resize();
	}
};


BalloonPanel = new Class(FloatingPanel);
BalloonPanel.prototype = {
	__construct: function(x, y, width, height, single_instance)
	{

		if (single_instance) {
			if (BalloonPanel._single_instance_panel) {
				BalloonPanel._single_instance_panel.close(true);
			}
			BalloonPanel._single_instance_panel = this;

			// Delete reference to this panel when it's closed
			GEvent.addListener(this, 'close',
				function()
				{
					if (BalloonPanel._single_instance_panel == this) {
						delete BalloonPanel._single_instance_panel;
					}
				}
			);
		}
		
		// All these come from base class (FloatingPanel)
		var x = x || 100;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;


		if (!this.padding) {
			this.padding = {
				left: 3,
				top: 1,
				right: 147,
				bottom: 16
			};
		}

		if (!this.anchor) {
			this.anchor = {
				x: w+5,
				y: 210
			};
		}

		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/balloon_panel.png', 1000, 1000, w, h, 8, 16, 16, 150);
		}
		FloatingPanel.prototype.__construct.call(this, x, y, width, height);

		// TODO: Make this a method: This panel is printable
		$(this.container).removeClass('gmnoprint');

		this.updateAnchor();
		GEvent.bind(this, 'resize', this, this.updateAnchor);
	},
	updateAnchor: function()
	{
		this.anchor.x = this.getWidth()+5;
	}
};


MiniBalloonPanel = new Class(BalloonPanel);
MiniBalloonPanel.prototype = {
	__construct: function(x, y, width, height, single_instance)
	{
		
		// All these come from base class (FloatingPanel)
		var x = x || 100;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;

		if (!this.anchor) {
			this.anchor = {
				x: w+5,
				y: 162
			};
		}
		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/mini_balloon_panel.png', 1000, 1000, w, h, 8, 16, 16, 150);
		}
		BalloonPanel.prototype.__construct.call(this, x, y, width, height, single_instance);

		this.title.className = 'mini_panel_title';

		GEvent.clearListeners(this.closeButton, 'mouseover');
		GEvent.clearListeners(this.closeButton, 'mouseout');
		this.closeButton.className = 'mini_panel_close';
		with (this.closeButton.style) {
			position = 'absolute';
			right = (this.padding.right+10) +'px';
			top = (this.padding.top +7) +'px';
			width = '12px';
			height = '12px';
			background = 'url('+SKIN_URL+'images/delete_upload.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}

		//this.title.style.height = '28px';


		GEvent.bind(this, 'show', this, function() {
			// FIXME : This timeout causes the panel to be sized properly in Safari
			setTimeout(GEvent.callback(this, function() { this.resize() }), 1);
		});
	}
};
/**
 * Handles saving and loading a users settings
 */
OptionsManager = new Class;
OptionsManager.prototype = {
__construct:
	function(cookieName)
	{
		this.cookieName = cookieName || 'myp_options';
		this.options = {};
	},
loadFromCookie:
	function()
	{
		GEvent.trigger(this, 'beforeload');
		var cookie = getCookie(this.cookieName);
		this.options = unserialise(cookie);
		GEvent.trigger(this, 'load');
	},
saveToCookie:
	function()
	{
		GEvent.trigger(this, 'beforesave');
		var data = serialise(this.options);
		setCookie(this.cookieName, data);
		GEvent.trigger(this, 'save');
	},
setOption:
	function(option, value)
	{
		if (!this.options) {
			this.options = {};
		}
		var oldValue = this.options[option];
		this.options[option] = value;
		GEvent.trigger(this, 'setoption', option, value, oldValue);
		this.saveToCookie();
	},
getOption:
	function(option, fallback)
	{
		if (!this.options || typeof this.options[option] == 'undefined') {
			return fallback;
		} else {
			return this.options[option];
		}
	},
unsetOption:
	function(option)
	{
		if (!this.options) {
			this.options = {};
		}
		var oldValue = this.options[option];
		if (oldValue != undefined) {
			delete this.options[option];
		}
		GEvent.trigger(this, 'unsetoption', option, oldValue);
	}
};
/**
 * All items on a map should extend from this. i.e. Route, Marker, PubCrawl, Flight, etc.
 */
MapItem = new Class;
/**
 * Static methods
 */
MapItem.create_from_json = function(json, world, hidden)
{
	// If json is a string, we need to evaluate it into an object
	if (typeof json == 'string') {
		try {
			var json = eval('(' + json + ')');
		} catch (err) {
			return false;
		}
	}



	try {
		var item = new window[json.properties.class_name](world);
	} catch (err) {
		
		return false;
	}
	item.unserialise(json, hidden);
	return item;
};



MapItem.prototype = {
__construct:
	function(world)
	{
		this.world = world;
		this.inert = this.world.inert;
		this.editing = false;

		this.anchor = false;
		this.properties = {
			class_name: 'MapItem',
			uid: 0,
			user: {id: 0, username: 'Anonymous'},
			date: (new Date()).getTime(),
			public: false
		};
		this.data = {};
		this.sticky = false;
	},
toString:
	function()
	{
		return '[object MapItem]';
	},
serialise:
	function()
	{
		// Remove legacy properties
		if (this.properties.points && this.data.points) {
			delete this.properties.points;
		}

		return serialise({
			anchor: { latitude: this.anchor.lat(), longitude: this.anchor.lng() },
			properties: this.properties,
			data: this.data
		});
	},
unserialise:
	function(json, hidden)
	{
		// If json is a string, we need to evaluate it into an object
		if (typeof json == 'string') {
			try {
				var json = eval('(' + json + ')');
			} catch (err) {
				return false;
			}
		}

		if (!json.properties) {
			
			return false;
		}

		if (json.anchor) {
			this.anchor = new GLatLng(json.anchor[0], json.anchor[1]);
		} else if (json.properties.point instanceof Array) {
			this.anchor = new GLatLng(json.properties.point[0], json.properties.point[1]);
		}

		if (!json.properties.user) {
			json.properties.user = {id: 0, username: 'Anonymous'};
		}

		this.properties = json.properties;
		this.data = json.data;

		if (!hidden) {
			this.redraw();
		}
		return true;
	},
save:
	function(extra_post_data)
	{
		GEvent.trigger(this, 'beforesave');

		// Add user details
		this.properties.user = {id: this.world.user.uid, username: this.world.user.username};
		
		if (this.group != "") {
			this["data"]["group"] = this.group;
			var data = this.serialise();
		} else {
			var data = this.serialise();
		}
		
		

		var url = 'world/save_item/';
		if (this.properties.uid > 0) {
			url += this.properties.uid;
			if (this.properties.uid == 301864) {
				// Peter Campbells test Marker
				console.log(data);
			}
		}
		
		var post_string = 'json='+escape(data);
		if (extra_post_data) {
			post_string += '&' + extra_post_data;
		}
		
		
		
		RPC.postData(url, post_string, GEvent.callback(this,
			function(rpc) {
				
				// Update UID
				this.properties.uid = parseInt(rpc.responseText);

				// Add item to draw queue so it doesn't get loaded in twice
				var item = this.world.draw_queue.add_item(this);
				item.sticky = true; // Sticky is required, or the above item will just get deleted from the queue when the map updates

				this.end_editing();
				
				alert('Your map item has been saved.');
				item.activate();
				GEvent.trigger(this, 'aftersave');
				GEvent.trigger(this.world, 'itemsave', item);
			})
		);
		GEvent.trigger(this, 'save');
	},
load_data:
	function(force)
	{
		if (force) {
			this.clear_data();
		}

		GEvent.trigger(this, 'loaddatastart');
		if (!this.data_loaded) {
			RPC.getJSON('world/map_items/' + this.properties.uid + '/', GEvent.callback(this, 
				function(data)
				{
					this.unserialise(data);
					if (!this.data.points && this.properties.points) {
						this.data.points = this.properties.points;
					}
					this.data_loaded = true;
					GEvent.trigger(this, 'loaddataend');
				}
			));
		} else {
			this.redraw();
			GEvent.trigger(this, 'loaddataend');
		}
	},
clear_data:
	function()
	{
		this.data_loaded = false;
		this.data = {};
		GEvent.trigger(this, 'cleardata');
	},
destroy_options_element:
	function()
	{
		var el = this.get_options_element();
		if (el && el.parentNode) {
			el.parentNode.removeChild(el);
		}
	},
duplicate:
	function()
	{
		if (!this.properties) {
			
			return false;
		}
		
		var new_item = this.world.create_item(this.properties.class_name);

		// Copy properties
		if (!new_item.properties) {
			new_item.properties = {};
		}
		for (x in this.properties) {
			new_item.properties[x] = this.properties[x];
		}

		// Copy data
		if (this.data) {
			if (!new_item.data) {
				new_item.data = {};
			}
			for (x in this.data) {
				new_item.data[x] = this.data[x];
			}
		} else {
			
		}
		if (this.anchor) {
			new_item.anchor = this.anchor;
		} else {
			
		}

		new_item.properties.uid = 0;
		new_item.properties.user = {id: 0, username: 'Anonymous'};
		//new_item.start_editing();
		//new_item.redraw();
		return new_item;
	},
/**
 * Abstract methods
 */
inert:		// Prevents the item from receiving mouse events
	function()
	{
		GEvent.trigger(this, 'inert');
		this.is_inert = true;
	},
uninert:	// Opposite of inert()
	function()
	{
		GEvent.trigger(this, 'uninert');
		delete this.is_inert;
	},
start_editing:	// Start editing this item
	function()
	{
		if (this.editing) {
			return;
		}
		GEvent.trigger(this, 'startediting');
		this.editing = true;
		if (this.properties.uid) {
			this.fit_into_view();
			this.world.jump_to_point(this.anchor);
		}
		if (PopoutPanel._current_instance) {
			PopoutPanel._current_instance.close();
		}
	},
end_editing:		// Done editing this item
	function()
	{
		if (!this.editing) {
			return;
		}
		this.editing = false;
		GEvent.trigger(this, 'endediting');
	},
fit_into_view:	// Alter the position and zoom of the map to fit the item in
	function()
	{
		this.world.map.setZoom(16);
	},
delete_item:
	function()
	{
		if (this.properties.uid) {
			MapItem.delete_item(this.properties.uid);
		}
		this.destroy();
	},
rateItem:
	function(score)
	{
		// Check if logged int
		if (!this.world.user.is_authenticated) {
			this.world.show_login('You need to login before your rating can be counted.', GEvent.callback(this, function() { this.rateItem(score); }));
			return false;
		}
		var url = 'world/rate_item/' + this.properties.uid + '/';
		RPC.postData(url, 'score=' + parseInt(score, 10), GEvent.callback(this, function(rpc)
		{
			alert('You rated this item ' + score + ' out of 5.\n\nThe new score is ' + rpc.responseText);
			GEvent.trigger(this, 'rate', parseInt(score, 10), parseFloat(rpc.responseText)); 
		}));
	},
cancel_editing: function() {},	// Undoes any edits, or if it's a new item simply destroys it
get_options_title: function() {},	// *REQUIRED* Returns the title to go on the editing panel
get_options_element: function() {},	// *REQUIRED* Should return an HTML element that contains the options to appear in the editing panel
destroy: function() {},			// Called to remove item from the map
activate: function() {},		// Called when item is clicked, or some other event. Should show the route, or info balloon, etc.
deactivate: function() {},		// Should put item back into default state
hide: function() {},			// Hides the item, but doesn't destroy it
show: function() {},			// Shows the item after being hidden using hide()
redraw: function() {},			// Called when properties or data have changed
getPopoutContent: function() {}		// Content to show in the first tab of popout panel when item is activated
};


MarkerCategory = new Class();
MarkerCategory.prototype = {
	__construct: function(name, categories)
	{
		var options = options || {};
		var categories = categories || MARKER_CATEGORIES;
		var name = name || 'marker_category';
		this.element = T.div();
		this.element.className = 'marker_categories';
		this.element.appendChild(document.createElement('ul'));
		this.element.firstChild.className = 'selection_list';
		this.iconPreview = document.createElement('div');
		this.iconPreview.className = 'icon_preview';
		this.inputField = T.input({'type': 'hidden', 'name': name});
		this.element.appendChild(this.inputField);

		with (this.element.style) {
			height = '150px';
			position = 'relative';
		}
		with (this.element.firstChild.style) {
			height = '150px';
			marginRight = '80px';
			border = '1px solid #a5acb2';
			overflow = 'auto';
		}
		with (this.iconPreview.style) {
			textAlign = 'center';
			position = 'absolute';
			right = 0;
			top = 0;
			width = '70px';
			height = '70px';
			border = '1px solid #a5acb2';
		}

		for (x in categories) {
			if (categories[x].private) {
				continue;
			}
			var li = T.li();
			li.appendChild(T.a({'title': 'Click to select "'+categories[x].title+'" as your marker\'s category', 'href':'javascript:void(0)'}, [T.span(categories[x].title)]));
			li.marker = categories[x];
			li.markerID = x;

			// Listen for clicking on a category
			GEvent.addDomListener(li, 'click', GEvent.callbackArgs(this, function(li) {
				// remove "active" class
				for (var i=0; i<this.element.firstChild.childNodes.length; i++) {
					var n = this.element.firstChild.childNodes[i];
					$(n).removeClass('active');
				}
				// Add "active" class to selected item
				$(li).addClass('active');
				// Update input field for the form
				this.inputField.value = li.markerID;
				// Update the preview icon
				this.setPreviewIcon(li.marker.icon);
			}, li));
			this.element.firstChild.appendChild(li);
		}

		this.element.appendChild(this.iconPreview);
	},
	setValue: function(val)
	{
		for (var i=0; i<this.element.firstChild.childNodes.length; i++) {
			var n = this.element.firstChild.childNodes[i];
			if (n.markerID == val) {
				GEvent.trigger(n, 'click');
			}
		}
	},
	setPreviewIcon: function(icon)
	{
		if (!icon) {
			
			return;
		}
		if (typeof icon == 'string') {
			icon = Icons[icon];
		}

		if (!this.iconPreview.firstChild) {
			img = document.createElement('img');
			img.style.width = "70px";
			img.style.height = "70px";
			this.iconPreview.appendChild(img);
		}
		this.iconPreview.firstChild.src = icon.image;

	}
};
SidePanel = new Class;
SidePanel.prototype = {
	__construct: function(container, width)
	{
		this.toolbar_height = 0;
		this.grippy_width = 13;
		this.panels = [];
		this.container = container;
		this.width = width || 200;
		this.element = T.div({'class':'side_panel'});
		with (this.element.style) {
			height = (this.container.clientHeight - this.toolbar_height) +'px';
            //height = '100%';
			width = this.width +'px';
			position = 'absolute';
			left = 0;
			top = this.toolbar_height + 'px';
            overflow = 'hidden';
		}
		this.container.appendChild(this.element);

		// This is purely for IE6, the sidepanel disappears all the time without it.
		GEvent.bindDom(window, 'resize', this, this.fix_height);
        setTimeout(GEvent.callback(this, this.fix_height), 1);

		// Resizer
		this.grippy = T.div({'class':'grippy'});
		this.grippy.title = 'Close this side panel';
		with (this.grippy.style) {
			position = 'absolute';
			right = 0;
			top = 0;
			width = this.grippy_width + 'px';
			height = '100%';
			cursor = 'pointer';
		}
		this.element.appendChild(this.grippy);
		GEvent.bindDom(this.grippy, 'click', this, this.toggle);

		// Where the panels will be
		this.panel_container = T.div({'class':'panel_container'});
		with (this.panel_container.style) {
			position = 'absolute';
			right = (2+this.grippy_width) +'px';
			top = 0;
			width = (this.width - this.grippy_width -3) +'px';
			height = '100%';
			overflow = 'auto';
	//		marginTop = '30px';	// this should be dynamic, it's the height of the menu bar
		}
		this.element.appendChild(this.panel_container);


		this.setFullWidth(this.width);

		// This timer allows the rest of the function that instantiated this object
		// to continue before the resize event is triggered
		setTimeout(GEvent.callback(this, function() { this.setWidth(this.width); }), 1);
	},
	fix_height: function()
	{
		this.element.style.height = (this.container.clientHeight - this.toolbar_height) + 'px';
	},
	setFullWidth: function(width)
	{
		this.full_width = width;
	},
	setWidth: function(width)
	{
		this.width = width;
		this.element.style.width = width +'px';
		GEvent.trigger(this, 'resize', width);
	},
	getWidth: function()
	{
		return this.width;
	},
	addPanel: function(panel)
	{
		this.panels.push(panel);
		this.panel_container.appendChild(panel.initialize(this));
		return this.panels.length-1;
	},
	toggle: function()
	{
		this.hidden ? this.show() : this.hide();
	},
	hide: function(quick)
	{
		var quick = quick || IS_IE;
		this.hidden = true;
		this.grippy.className = 'grippy hidden_grippy';
		if (quick) {
			this.setWidth(this.grippy_width+1);
		} else {
			this.animate(this.grippy_width+1);
		}

		this.grippy.title = 'Open this side panel';
	},
	show: function(quick)
	{
		var quick = quick || IS_IE
		this.grippy.className = 'grippy';
		this.hidden = false;
		if (quick) {
			this.setWidth(this.full_width);
		} else {
			this.animate(this.full_width);
		}
		this.grippy.title = 'Close this side panel';
	},
	animate: function(target)
	{
		var speed = 20;
		var new_width = this.width;
		var hit_target = false;
		if (target < this.width) {
			new_width -= speed;
			if (new_width <= target) {
				new_width = target;
				hit_target = true;
			}
		} else {
			new_width += speed;
			if (new_width >= target) {
				new_width = target;
				hit_target = true;
			}
		}

		this.setWidth(new_width);

		if (!hit_target) {
			setTimeout(GEvent.callback(this, function() { this.animate(target); }), 1);
		} else {
			// TODO Trigger showend or hideend
		}
	},
	hidePanels: function()
	{
		for (var i=0; i<this.panels.length; i++) {
			var p = this.panels[i];
			p.element.style.display = 'none';
		}
	},
	showPanels: function()
	{
		for (var i=0; i<this.panels.length; i++) {
			var p = this.panels[i];
			p.element.style.display = 'block';
		}
	}
}
function httpGetString()
{
	var vals = location.search.substring(1, location.search.length).split('&');
	var gets = [];
	for (var i=0; i<vals.length; i++) {
		var t = vals[i].split('=');
		if (t[1]) {
			gets[t[0]] = t[1];
		} else {
			gets[t[0]] = true;
		}
	}

    // Bodge for injury clincs
	if (gets['m'] && !gets['item']) {
		gets['item'] = gets['m'];
	}
	return gets;
}
//	{{{ Function: createIcon
function createIcon(filename, width, height, anchor, hitMap, no_shadow, no_float)
{
	var icon = new GIcon();
	var filename = escape(filename);

	// == All the images ==
	var anchor_str = anchor.x+'x'+(height-anchor.y);	// Y has to be from the bottom
	// XXX: Image URLs must end in .png so the Google API knows to do the IE6 AlphaImageLoader stuff
	icon.image = STATIC_URL + 'icons/'+filename+'-main.png';
	icon.transparent = STATIC_URL + 'icons/'+filename+'-transparent.png';
	icon.printImage = STATIC_URL + 'icons/'+filename+'-print.gif';
	icon.mozPrintImage = STATIC_URL + 'icons/'+filename+'-mozprint.gif';
	if (!no_shadow) {
		icon.shadow = STATIC_URL + 'icons/'+filename+'-shadow.png';
		icon.printShadow = STATIC_URL + 'icons/'+filename+'-printshadow.gif';
	}
	icon.hoverImage = STATIC_URL + 'icons/'+filename+'-hover.png';
	icon.normalImage = icon.image;
	// Emblems
	icon.normalPhoto = STATIC_URL + 'icons/'+filename+'-main-photo.png';
	icon.normalVideo = STATIC_URL + 'icons/'+filename+'-main-video.png';
	icon.normalPhotoVideo = STATIC_URL + 'icons/'+filename+'-main-photo-video.png';
	icon.hoverPhoto = STATIC_URL + 'icons/'+filename+'-hover-photo.png';
	icon.hoverVideo = STATIC_URL + 'icons/'+filename+'-hover-video.png';
	icon.hoverPhotoVideo = STATIC_URL + 'icons/'+filename+'-hover-photo-video.png';



	icon.iconSize = new GSize(width, height);
	icon.shadowSize = new GSize(width*1.5, height);
	icon.iconAnchor = anchor;
	icon.infoWindowAnchor = new GPoint(9, 2);
	if (hitMap) {
		icon.imageMap = hitMap;
	}

	icon.no_float = no_float;
	if (no_float) {
		icon.maxHeight = 0.1; // disable drag floating 
		icon.dragCrossImage = ""; // hide drag cross 
		icon.dragCrossSize = new GSize(0,0); // hide drag cross 
	}

	return icon;
}
//	}}} Function: createIcon
//	{{{ Function: createMarker
function createMarker(point, options)
{
	if (!options.icon) {
		return false;
	}
	if (options.icon.no_float) {
		options.bouncy = false;
		options.dragCrossMove = true;
	}

	var marker = new GMarker(point, options);
	
	marker.updateIcon = function(type)
	{
		var type = type || 'normal';
		var image = this.icon[type + 'Image']
		if (this.has_video && this.has_photo) {
			image = this.icon[type + 'PhotoVideo'];
		} else if (this.has_video) {
			image = this.icon[type + 'Video'];
		} else if (this.has_photo) {
			image = this.icon[type + 'Photo'];
		} 
		try {
			this.setImage(image);
		} catch (e) {
		}
	};




	if (options.icon) {
		marker.icon = options.icon;
		GEvent.bind(marker, 'visibilitychanged', marker,
			function(vis)
			{
				if (!vis) {
					return;
				}
				this.updateIcon('normal');
			}
		);
		GEvent.bind(marker, 'mouseover', marker,
			function()
			{
				this.updateIcon('hover');
			}
		);
		GEvent.bind(marker, 'mouseout', marker,
			function()
			{
				this.updateIcon('normal');
			}
		);
		if (!options.icon.no_float) {
			GEvent.bind(marker, 'dragstart', marker,
				function()
				{
					this.updateIcon('normal');
				}
			);
			GEvent.bind(marker, 'dragend', marker,
				function()
				{
					this.updateIcon('hover');
				}
			);
		}
	}
	return marker;
}
//	}}} Function: createMarker
//	{{{ Namespace: PNG
PNG = {
	getSprite: function(png, img_w, img_h, x, y, w, h)
	{
		var x = x < 0 ? img_w+x : x;
		var y = y < 0 ? img_h+y : y;


		// Container to cut out the area we want
		var container = document.createElement('div');
		with (container.style) {
			width = w +'px';
			height = h +'px';
			overflow = 'hidden';
		}

		// Whole image which is moved to only show the correct section
		var newimg = this.createImage(png, img_w, img_h);
		with (newimg.style) {
			marginLeft = -x +'px';
			marginTop = -y +'px';
		}

		container.appendChild(newimg);
		return container;
	},
	createImage: function(png, width, height)
	{
		var img = document.createElement('div');
		img.style.width = width +'px';
		img.style.height = height +'px';
		img.style.overflow = 'hidden';

		if (IS_IE6) {
			img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+png+'", sizingMethod="crop")';
		} else {
			img.style.background = 'url("'+png+'") no-repeat';
		}
		return img;
	},
	setImage: function(png, img, scale)
	{
		if (scale) {
			if (IS_IE6) {
				img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+png+'", sizingMethod="scale")';
			} else {
				img.style.background = 'url("'+png+'") repeat';
			}
		} else {
			if (IS_IE6) {
				img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+png+'", sizingMethod="crop")';
			} else {
				img.style.background = 'url("'+png+'") no-repeat';
			}
		}
	}
};
//	}}} Namespace: PNG
//	{{{ Namespace: RPC
function RPC()
{
	if (typeof ActiveXObject != "undefined") {
		return ActiveXObject("Microsoft.XMLHTTP");
	} else if (window.XMLHttpRequest) {
		return XMLHttpRequest;
	}
}
RPC = {
	createXMLHTTP: function()
	{
		try {
			if (typeof ActiveXObject != "undefined") {
				return new ActiveXObject("Microsoft.XMLHTTP");
			} else if (window.XMLHttpRequest) {
				return new XMLHttpRequest;
			}
		} catch (e) {}
		return null;
	},
	/**
	 * Gets data from the server then send the RPC to the given callback function
	 * @param string script The server script to post to
	 * @param Function callback The callback function to send the RPC to once complete
	 */
	getData: function(script, callback)
	{
		
		var rpc = RPC.createXMLHTTP();

		// Cache buster is to stop IE6's stupid caching.
		var cache_buster = (new Date()).getTime();
		if (script.indexOf('?') > -1) {
			script += '&rnd=' + cache_buster;
		} else {
			script += '?rnd=' + cache_buster;
		}

		rpc.open("GET", BASE_URL + script, true);
		if (typeof callback == 'function') {
			rpc.onreadystatechange = function()
			{
				//try {
					if (rpc.readyState == 4 && (!rpc.status || rpc.status == 200)) {
						var error = false;
						// Check for error object
						try {
							//error = eval('('+rpc.responseText+')');
						} catch(e) {}

						if (!error.error_code) {
							callback(rpc);
						} else{
							switch (error.error_code) {
							case ERROR_LOGIN_REQUIRED:
								realWorld.user.logout();
								realWorld.user.login(false, callback);
								break;
							default:
								alert('An unknown error occured: '+error.error_code+'\n\n'+error.error_message);
								break;
							}
						}
						delete callback;
					} else if (rpc.readyState == 4 && rpc.status) {
						//alert('Error connecting to server: '+rpc.status);
						
						delete rpc;
						delete callback;
					}
				/*
				} catch (err) {
					
					delete rpc;
					delete callback;
				}
				*/
			};
		}
		rpc.send(null);
		return rpc;
	},
	getJSON: function(script, callback)
	{
		return this.getData(script,
			function(rpc) {
				try {
					var o = eval('('+rpc.responseText+')');
				} catch(e) {
                    
					return false;
				}
				callback(o);
			}
		);
	},
	/**
	 * Posts data to the server then sends the RPC to the given callback function
	 * @param string script The server script to post to
	 * @param string data The query string to post to the script
	 * @param Function callback The callback function to send the RPC to once complete
	 */
	postData: function(script, data, callback)
	{
		
		var rpc = RPC.createXMLHTTP();

		// Cache buster is to stop IE6's stupid caching.
		var cache_buster = (new Date()).getTime();
		if (script.indexOf('?') > -1) {
			script += '&rnd=' + cache_buster;
		} else {
			script += '?rnd=' + cache_buster;
		}

		rpc.open("POST", BASE_URL + script, true);
		if (typeof(rpc.setRequestHeader) != "undefined") {
			rpc.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
			rpc.setRequestHeader("Connection", "close");
		}
		if (typeof callback == 'function') {
			rpc.onreadystatechange = function()
			{
				try {
					if (rpc.readyState == 4 && (!rpc.status || rpc.status == 200)) {
						var error = false;
						// Check for error object
						try {
							error = eval('('+rpc.responseText+')');
						} catch(e) {}

						if (!error.error_code) {
							callback(rpc);
						} else{
							switch (error.error_code) {
								case ERROR_LOGIN_REQUIRED:
									realWorld.user.logout();
									alert('You are not logged in');
									break;
								default:
									alert('Error: ' + error.error_message);
									break;
							}
						}
						delete callback;
					} else if (rpc.readyState == 4 && rpc.status) {
						//alert('Error connecting to server: '+rpc.status);
						
						delete rpc;
						delete callback;
					}
				} catch (err) {
					
					delete rpc;
					delete callback;
				}
			};
		}
		rpc.send(data);
		return rpc;
	}
};	
//	}}} Namespace: RPC
//	{{{ Addons
GEvent.stop = function(e, burst)
{
	if (!e) {
		return;
	}
	if (e.preventDefault) {
		if (burst !== false) {
			e.preventDefault();
		}
		if (burst !== undefined) {
			e.stopPropagation();
		}
	} else {
		if (burst !== false) {
			e.returnValue = false;
		}
		if (burst !== undefined) {
			e.cancelBubble = true;
		}
	}
};
function setOpacity(el, op, notForIE)
{
	if (!notForIE && typeof el.style.filter == 'string') {
		el.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity='+(op*100)+')';
	} else {
		el.style.opacity = op;
	}
	el._style_opacity = op;
};
//	}}} Addons
CommentsView = new Class;
CommentsView.prototype = {
__construct:
	function(item_id)
	{
		var id = new Date().valueOf();
		this.item_id = item_id;

		//var jump_to_form = T.a({'class': 'add_comment_link', 'href': 'javascript:void(0)'}, 'Add a comment');
		var jump_to_form = T.div([new Button('Add a comment', GEvent.callback(this, 
			function()
			{
				var container = this.element.parentNode.parentNode.parentNode;
				container.scrollTop = container.scrollHeight;

				this.comment_form.getElementsByTagName('textarea')[0].focus();
			}), 120
		)]);
		with (jump_to_form.style) {
			display = 'none';
			cssFloat = 'right';
			styleFloat = 'right';
		}
		this.element = T.ol({'class': 'item_comments'}, [
			jump_to_form,
			this.comment_form = T.form({'class': 'comment_form'}, [
				T.label({'for': 'add_comment_'+id}, 'Enter your comment below:'),
				T.textarea({'id': 'add_comment_'+id}),
				new Button('Add comment', GEvent.callback(this, 
					function()
					{
						var txt = this.comment_form.getElementsByTagName('textarea')[0];
						if (txt.value == '') {
							alert('You must enter a comment first.');
							txt.focus();
							return;
						}

						this.save_comment(txt.value);
					}
				), 120)
			])
		]);

		this.comments = [];
		this._comment_count = 0;

		this.Comment = new Class;
		this.Comment.prototype = {
		__construct:
			function(message, date, user, profile_image)
			{
				var full_date = new Date();
				full_date.setTime(date * 1000);

				var formatted_date = full_date.getDate() + '/' + (full_date.getMonth() +1) + '/' + full_date.getFullYear() + ' - ' + full_date.getHours() + ':' + full_date.getMinutes();
				
				this.element = T.li({'class': 'item_comment'}, [
					T.div({'class': 'profile_image'}, [
						T.a({'href': 'javascript:void(0)'}, [
							T.img({'src': profile_image})
						])
					]),
					T.div({'class': 'header'}, [
						realWorld.user.profile_link(user.username),	// FIXME globals = ick
						T.span(' – '),
						T.span({'class': 'comment_date'}, formatted_date)
					]),
					T.div({'class': 'comment_message'}, [document.createParagraph(message)])
				]);
			}
		};
	},
refresh:
	function()
	{
		if (this._refresh_rpc) {
			GEvent.clearListeners(this._refresh_rpc);
			this._refresh_rpc.abort();
			delete this._refresh_rpc;
		}
		GEvent.trigger(this, 'startrefresh');
		this._refresh_rpc = RPC.getJSON('world/comments/' + this.item_id, GEvent.callback(this,
			function(data)
			{
				for (var i=0; i<data.length; i++) {
					var c = data[i]
					this.add_comment(c.id, c.message, c.date, c.user, c.profile_image);
				}
				delete this._refresh_rpc;
				GEvent.trigger(this, 'endrefresh');
			}
		));
	},
save_comment:
	function(message)
	{
		GEvent.trigger(this, 'beforesave', message);
		if (!realWorld.user.is_authenticated) {
			realWorld.show_login('You must login to post comments. After you login, your comment will be saved automatically.', GEvent.callback(this, function() { this.save_comment(message); }));
			return false;
		}
		RPC.postData('world/save_comment/', 'item=' + this.item_id + '&comment=' + escape(message), GEvent.callback(this,
			function(rpc)
			{
				GEvent.trigger(this, 'save', message);
				this.comment_form.getElementsByTagName('textarea')[0].value = '';
				this.refresh();
			}
		));
	},
add_comment:
	function(id, message, date, user, profile_image)
	{
		if (this.comments[id]) {
			// Comment already exists
			return;
		}
		this._comment_count++;

		// Show button if there's too many comments
		if (this._comment_count >= 1) {
			this.element.firstChild.style.display = 'block';
		}

		var user = user || 'Anonymous';
		var comment = new this.Comment(message, date, user, profile_image);
		this.comments[id] = comment;

		this.element.insertBefore(comment.element, this.comment_form);
		GEvent.trigger(this, 'addcomment');
	},
comment_count:
	function()
	{
		return this._comment_count;
	}
};
SmallTooltip = new Class(GOverlay);
SmallTooltip.prototype = {
	__construct: function(point, message, offset)
	{
		if (point instanceof Array) {
			var point = new GLatLng(point[0], point[1]);
		}
		this.point = point;
		this.offset = offset || new GSize(15, 0);
		this.message = message || '';
	},
	toString: function()
	{
		return '[object SmallTooltip]';
	},
	initialize: function(gmap)
	{
		this.gMap = gmap;
		this.element = document.createElement('div');

		with (this.element.style)
		{
			position = 'absolute';
			height = '27px';
			fontSize = '11px';
			lineHeight = '27px';
			whiteSpace = 'nowrap';
			opacity = '0.8';
			//filter = 'alpha(opacity=80)';
		}

		this.leftEnd = document.createElement('div');
		this.rightEnd = document.createElement('div');
		this.tile = document.createElement('div');

		this.leftEnd.style.position = 'absolute';
		this.leftEnd.style.width = '16px';
		this.leftEnd.style.height = '27px';
		this.leftEnd.style.left = 0;

		this.rightEnd.style.position = 'absolute';
		this.rightEnd.style.width = '16px';
		this.rightEnd.style.height = '27px';
		this.rightEnd.style.right = 0;

		this.tile.style.margin = '0 16px';
		this.tile.style.height = '27px';

		this.leftEnd.style.background = 'url('+SKIN_URL+'images/smalltooltip_arrow_left.png)';
		this.rightEnd.style.background = 'url('+SKIN_URL+'images/smalltooltip_end_right.png)';
		this.tile.style.background = 'url('+SKIN_URL+'images/smalltooltip_tile.png)';

		this.element.appendChild(this.leftEnd);
		this.element.appendChild(this.rightEnd);
		this.element.appendChild(this.tile);

		this.setMessage(this.message);
		
		this.gMap.getPane(G_MAP_FLOAT_PANE).appendChild(this.element);
	},
	remove: function()
	{
		this.element.parentNode.removeChild(this.element);
	},
	copy: function()
	{
		return new SmallTooltip(this.point, this.message);
	},
	redraw: function(force)
	{
		if (!force) return;

		if (!this.gMap) {
			
			return false;
		}
		var p = this.gMap.fromLatLngToDivPixel(this.point);
		this.element.style.left = (p.x + this.offset.width) +'px';
		this.element.style.top = (p.y + this.offset.height - 13) +'px';
	},
	setLatLng: function(point)
	{
		this.point = point;
		this.redraw(true);
	},
	getLatLng: function()
	{
		return this.point;
	},
	setMessage: function(message)
	{
		this.message = message;
		message = message.replace(/ /g, '\u00a0');

		if (this.tile) {
			while (this.tile.firstChild) {
				this.tile.removeChild(this.tile.firstChild);
			}
			this.tile.appendChild(document.createTextNode(message));
		}
	},
	getMessage: function()
	{
		return this.message;
	},
	show: function()
	{
		this.element.style.display = 'block';
	},
	hide: function()
	{
		this.element.style.display = 'none';
	}
};
ProfileView = new Class;
ProfileView.prototype = {
__construct:
	function(username)
	{
		this.username = username;
		this.element = T.div({'class': 'info_box'}, 'Loading profile...');
		this.data = {};
	},
toString:
	function()
	{
		return '[object ProfileView]';
	},
load_profile:
	function()
	{
		
		GEvent.trigger(this, 'beforeload');
		RPC.getJSON('world/profiles/' + escape(this.username) + '/', GEvent.callback(this, 
			function(data)
			{
				
				this.data = data;

				GEvent.trigger(this, 'load');
				this.loaded = true;
			})
		);
	},
create_profile:
	function()
	{
		// If it already exists, don't bother
		if (this.created) {
			return;
		}

		// If profile hasn't loaded yet, then load it.
		if (!this.loaded) {
			var _load_event = GEvent.bind(this, 'load', this,
				function()
				{
					this.create_profile();
					if (_load_event) {
						GEvent.removeListener(_load_event);
					}
				}
			);
			this.load_profile();
			return;
		}



		// Draw the profile
		
		document.emptyElement(this.element);

		// Generates a list of map items and makes them hyperlinks
		function generateItemList(items)
		{
			var item_list = T.ul({'class':'item_list'});
			for (var i=0; i<items.length; i++) {
				var item = items[i];
				var link = T.a({'href':'javascript:void(0)'}, item.title);
				GEvent.bindDom(link, 'click', item, function() { realWorld.activate_item(this.id); });
				item_list.appendChild(T.li([link]));
			}
			return item_list;
		}


		var member_since = new Date();
		member_since.setTime(this.data.member_since*1000);
		var formatted_date = member_since.getDate() + '/' + (member_since.getMonth() +1) + '/' + member_since.getFullYear() + ' - ' + member_since.getHours() + ':' + member_since.getMinutes();

		if (this.data.premium) {
			premium = T.span({'class': 'premium_membership_image_small float-left myp_membership_image', 'title': 'premium member'})
		} else {
			premium = T.span({});
		}
		this.element.appendChild(T.div([
			// Main author details
			//T.h2('Author'),
			T.table([T.tbody([
				T.tr([
					T.th('Username: '),
					T.td([T.span({'class': 'float-left'}, this.data.username), premium])
				]),
				T.tr([
					T.th('Member since: '),
					T.td(formatted_date)
				])
			])]),


			// Latest routes
			T.h2('Latest routes'),
			generateItemList(this.data.routes),

			// Latest markers
			T.h2('Latest markers'),
			generateItemList(this.data.markers)
		]));

		this.created = true;
	}
};

LabelBox = new Class();
LabelBox.prototype = {
	__construct: function(label, width, height)
	{
		var w = width || 110;
		var h = height || 24;
		this.box = new Box(SKIN_URL+'images/button.png', 512, 512, w, h, 4, 4, 4, 4);
		this.element = this.box.container;

		this.container = document.createElement('div');
		with (this.container.style) {
			position = 'relative';
			width = '100%';
			height = '100%';
			lineHeight = h +'px';
			textAlign = 'center';
		}
		if (label) {
			this.container.appendChild(document.createTextNode(label));
		}

		this.element.appendChild(this.container);
	}
};


Button = new Class(LabelBox);
Button.prototype = {
	__construct: function(label, action, width, height)
	{
        var width = width || 75;
		this.action = action || function() {};
		this._baseClass.__construct.call(this, false, width, height);

		this.button = document.createElement('button');
		this.button.setAttribute('type', 'button');
		this.element.className = 'button';
		with (this.button.style) {
			width = '100%';
			height = '100%';
			cursor = 'pointer';
			display = 'block';
			padding = 0;
			margin = 0;
			border = 0;
			background = 'transparent';
		}
		if (label) {
			this.button.appendChild(document.createTextNode(label));
		}
		this.container.appendChild(this.button);
		GEvent.bindDom(this.button, 'click', this, function() {
			if (!this.enabled) {
				return;
			}
			this.action();
		});
		this.enabled = true;
	},
	focus: function()
	{
		this.button.focus();
	},
	enable: function()
	{
		this.enabled = true;
		this.element.style.display = '';
		//this.element.style.visibility = 'visible';
		return this;
	},
	disable: function()
	{
		this.enabled = false;
		this.element.style.display = 'none';
		//this.element.style.visibility = 'hidden';
		return this;
	}
};
Menu = new Class();
Menu.prototype = {
	__construct: function(toolbar, list, hidden)
	{
		this.toolbar = toolbar;
		this.wrapper = T.div();
		this.is_list = list ? true : false;
		this.submenus = [];

		if (hidden) {
			this.hide();
		}

		if (list) {
			this.element = T.ul();
		} else {
			this.element = T.table([ T.tbody([T.tr()]) ]);
		}

		this.wrapper.appendChild(this.element);
		GEvent.bindDom(this.wrapper, 'mousedown', this, function(ev) { return GEvent.stop(ev, true); });

		this.toolbar.element.appendChild(this.wrapper);

		if (list) {
		} else {
			this.element = this.element.firstChild.firstChild;
		}
	},
	addLink: function(name, action, login_required)
	{
		var method;
		var e;
		if (this.is_list) {
			e = T.li([T.a({'href':'javascript:void(0)'}, name)]);
		} else {
			e = T.td([T.a({'href':'javascript:void(0)'}, name)]);
		}
		var event = 'click';

		for (var i=0; i<this.element.childNodes.length; i++) {
			$(this.element.childNodes[i]).removeClass('lastLink');
		}
		$(e).addClass('lastLink');

		switch (typeof action) {
			// URL
			case 'string':
				method = function() {
					this.toolbar.hideMenus();
					window.location = action;
				};
				break;
			// Submenu
			case 'object':
				this.submenus.push(action);
				// Firefox bug workaround
				if (e.tagName == 'TD') {
					action.wrapper.style.position = 'relative';
				}
				e.appendChild(action.wrapper);
				method = function(ev) {
					//var visible = action.toggle();
					action.show();
					this.hideSubMenus(action);
					//if (!visible) {
					//	GEvent.trigger(this, 'hidemenu');
					//}
					GEvent.stop(ev, true);
				};
				event = 'mouseover';
				break;
			// Function
			case 'function':
				method = function() {
					this.toolbar.hideMenus();
					// FIXME: Ick, a global!
					if (!login_required || realWorld.user.is_authenticated) {
						action.apply(this, arguments);
					} else {
						realWorld.user.login('You need to log in before you can use that feature.', GEvent.callback(this, function() { action.apply(this, arguments) }));
					}
				};
				break;
			// unknown
			case 'undefined':
				
				break;
			default:
				
				break;
		}


		if (method) {
			GEvent.bindDom(e, event, this, method);
		}

		this.addElement(e);
		return e;
	},
	addSubMenu: function(name, action, login_required)
	{
		var e = this.addLink(name, action, login_required);
		e.className = 'submenu';
		return e;
	},
	addToggle: function(name, action, checked)
	{
		var method;
		var e;
		if (this.is_list) {
			e = T.li([T.a({'href':'javascript:void(0)'}, name)]);
		} else {
			e = T.td([T.a({'href':'javascript:void(0)'}, name)]);
		}
		var event = 'click';
		$(e).addClass('menuToggle');
		if (checked) {
			$(e).addClass('checked');
		} else {
			$(e).addClass('unchecked');
		}

		for (var i=0; i<this.element.childNodes.length; i++) {
			$(this.element.childNodes[i]).removeClass('lastLink');
		}
		$(e).addClass('lastLink');

		if (action) {
			method = function() {
				if (action.apply(this, arguments)) {
					$(e).removeClass('checked');
					$(e).addClass('unchecked');
				} else {
					$(e).removeClass('unchecked');
					$(e).addClass('checked');
				}
			};

			GEvent.bindDom(e, event, this, method);
		}

		this.addElement(e);
		return e;
	},
	addElement: function(element)
	{
		this.element.appendChild(element);
	},
	hide: function()
	{
		this.hidden = true;
		this.wrapper.style.display = 'none';
		this.wrapper.style.visibility = 'hidden';	// IE bug work around
	},
	show: function()
	{
		this.hideSubMenus();
		this.hidden = false;
		this.wrapper.style.display = '';
		this.wrapper.style.visibility = '';	// IE bug work around
		this.toolbar.showBlocker();
	},
	toggle: function()
	{
		this.hidden ? this.show() : this.hide();
		return !this.hidden;
	},
	hideSubMenus: function(skip_menu)
	{
		for (var i=0; i<this.submenus.length; i++) {
			if (this.submenus[i] !== skip_menu) {
				this.submenus[i].hide();
			}
		}
		if (!skip_menu) {
			GEvent.trigger(this, 'hideallsubmenus');
		} else {
			GEvent.trigger(this, 'hidesubmenus');
		}
	}
};
Map = new Class();
Map.prototype = {
	__construct: function(container)
	{
		this.container = container;
		// Remove everything in main container
		while (container.firstChild) {
			container.removeChild(container.firstChild);
		}
		

		// Create map container
		this.element = document.createElement('div');
		this.element.className = 'map';
		this.container.appendChild(this.element);
		with (this.element.style) {
			width = '100%';
			height = '100%';
		}

		// Add in the Google map
		this.gMap = new GMap2(this.element);
		// XXX Must set a location before doing anything.
		if (getCookie('s_x') != null) {
			this.gMap.setCenter(new GLatLng(parseFloat(getCookie('s_y')), parseFloat(getCookie('s_x'))), parseInt(getCookie('s_z')));
		} else {
			this.gMap.setCenter(new GLatLng(_OPTIONS.lat, _OPTIONS.lng), parseInt(_OPTIONS.zoom));
		}
		// Add nifty new terrain support
		this.gMap.addMapType(G_PHYSICAL_MAP);
		//this.gMap.setMapType(G_PHYSICAL_MAP);

		// Setup some initial properties
		this.gMap.enableContinuousZoom();
		this.gMap.enableScrollWheelZoom();
		this.gMap.addControl(new GLargeMapControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(10, 40)));
		this.gMap.addControl(new GScaleControl());
		this.gMap.addControl(new GOverviewMapControl());
		//this.gMap.addControl(new GHierarchicalMapTypeControl(), new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(10, 40)));
		//this.gMap.addControl(new GMapTypeControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(10, 50)));
		//this._typeControl = new GMapTypeControl().initialize(this.gMap);
		this._typeControl = new GHierarchicalMapTypeControl().initialize(this.gMap);
		this._typeControl.className = 'map_type_selection';
		//this.gMap.addControl(this._typeControl, new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(10, 50)));
		
		// Add the marker manager XXX: This must be created after jumpTo or bad shit happens
		this.markerManager = new MarkerManager(this.gMap, {trackMarkers: true});
		this.polyManager = new PolyManager(this.gMap);

		// Event bindings
		GEvent.bindDom(document.body, 'mousemove', this, function(e)
		{
			try {
				var p = this.getMap().fromContainerPixelToLatLng(new GPoint(e.clientX -realLeft(this.element), e.clientY -realTop(this.element)));
				GEvent.trigger(this, 'mousemove', p);
			} catch (e) {}
		});
		GEvent.bind(this.gMap, 'click', this, function(m, p) { if (!p) return; GEvent.trigger(this, 'click', p); });
		//GEvent.bind(this.gMap, 'mousemove', this, function(e) { GEvent.trigger(this, 'mousemove', e); });
		GEvent.bindDom(this.gMap.getContainer(), 'mouseout', this, function(e) { GEvent.trigger(this, 'mouseout', e); });
		GEvent.bindDom(this.gMap.getContainer(), 'mouseover', this, function(e) { GEvent.trigger(this, 'mouseover', e); });
		GEvent.bind(this.gMap, 'mouseup', this, function(e) { GEvent.trigger(this, 'mouseup', e); });
		GEvent.bindDom(this.gMap.getContainer(), 'contextmenu', this, function(e) { GEvent.trigger(this, 'contextmenu', e); });
		GEvent.bind(this.gMap, 'movestart', this, function(e) { GEvent.trigger(this, 'movestart', e); });
		GEvent.bind(this.gMap, 'moveend', this, function(e) { GEvent.trigger(this, 'moveend', e); });
		GEvent.bind(this.gMap, 'move', this, function(e) { GEvent.trigger(this, 'move', e); });

		// Pass through clicks to overlays
		
		GEvent.bind(this.gMap, 'addoverlay', this.gMap,
			function(overlay)
			{
				overlay._clickHandler = GEvent.bind(overlay, 'click', this, function(point) {
					if (!overlay._passThruClick) {
						return;
					}
					if (overlay.getLatLng) {
						point = overlay.getLatLng();
					}
					if (point) {
						GEvent.trigger(this, 'click', false, point);
					}

				});
				if (overlay.updateIcon) {
					overlay.updateIcon();
				}
			}
		);
		
		GEvent.bind(this.gMap, 'removeoverlay', this.gMap,
			function(overlay)
			{
				if (overlay._clickHandler) {
					GEvent.removeListener(overlay._clickHandler);
				}
			}
		);



		try {
			pl.edit();
			alert('EDITING WORKS!');
		} catch (e) {
		}
	},
	saveLocation: function()
	{
		var p = this.gMap.getCenter();
		var y = p.lat();
		var x = p.lng();
		var z = this.gMap.getZoom();
		setCookie('s_x', x);
		setCookie('s_y', y);
		setCookie('s_z', z);
	},
	toString: function()
	{
		return '[object Map]';
	},
	jumpTo: function(lat, lon, zoom)
	{
		var lat = parseFloat(lat);
		var lng = parseFloat(lng);
		var zoom = parseInt(zoom);
		
		this.jumpToPoint(new GLatLng(lat, lon), zoom);
	},
	jumpToPoint: function(point, zoom)
	{
		this.gMap.panTo(new GLatLng(0, 0));
		this.gMap.panTo(point);

		if (zoom) {
			this.gMap.setZoom(zoom);
		}
	},
	jumpToLocation: function(place_name)
	{
		// Catch UK postcode
		var pc_reg = /[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? ?[0-9][A-Za-z]{2}/;
		if (place_name.match(pc_reg)) {
			return this.jumpToUKPostCode(place_name);
		}

		// Use Google's GeoCoder for everything else
		var geocoder = new GClientGeocoder();
		geocoder.setBaseCountryCode('GR');	// Default country is UK (ISO 3166-1)
		var _this = this;
		GEvent.trigger(this, 'startgeocode');
		geocoder.getLocations(place_name, GEvent.callback(this,
			function(response)
			{
				if (!response || response.Status.code != 200) {
					alert('Sorry, but we could not find "'+place_name+'".');
				} else {
					var place = response.Placemark[0];
					var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);

					GEvent.trigger(this, 'endgeocode');
					// work out zoom level based on accuracy of the result
					var zoom = (3*place.AddressDetails.Accuracy) + 2;
					this.jumpToPoint(point, zoom);
				}
			})
		);
	},
	jumpToUKPostCode: function(postcode)
	{
		RPC.getJSON('geocode/'+escape(postcode), GEvent.callback(this,
			function(data)
			{
				if (data.error_code) {
					alert('Sorry, but we could not find "'+postcode+'".');
					return;
				}

				var point = new GLatLng(data.lat, data.lng);
				GEvent.trigger(this, 'endgeocode');
				this.jumpToPoint(point, 16);
			}
		));
	},
	getMap: function()
	{
		return this.gMap;
	},
	fitToPoints: function(points)
	{
		var box = new GLatLngBounds();
		for (var i=0;i<points.length;i++) {
			box.extend(points[i]);
		}
		var lngCentre = (box.getNorthEast().lng() + box.getSouthWest().lng()) / 2;
		var latCentre = (box.getNorthEast().lat() + box.getSouthWest().lat()) / 2;
		var point = new GLatLng(latCentre,lngCentre);
		this.jumpToPoint(point, this.gMap.getBoundsZoomLevel(box));
		
	},
	getPointsZoomLevel: function(points)
	{
		var box = new GLatLngBounds();
		for (var i=0;i<points.length;i++) {
			box.extend(points[i]);
		}
		var lngCentre = (box.getNorthEast().lng() + box.getSouthWest().lng()) / 2;
		var latCentre = (box.getNorthEast().lat() + box.getSouthWest().lat()) / 2;
		var point = new GLatLng(latCentre,lngCentre);
		return this.gMap.getBoundsZoomLevel(box);
	},
	addOverlay: function(item)
	{
		this.gMap.addOverlay(item);
	},
	removeOverlay: function(item)
	{
		this.gMap.removeOverlay(item);
	},
	addMarker: function(item, minZoom, maxZoom)
	{
		var minZoom = isset(minZoom) ? minZoom : MIN_ZOOM;
		var maxZoom = isset(maxZoom) ? maxZoom : MAX_ZOOM;
		
		this.markerManager.addMarker(item, minZoom, maxZoom);
		if (!this._markers) {
			this._markers = []
		}
		this._markers.push(item);

		// updates it with the photo/video icon
		item.updateIcon();
	},
	addMarkers: function(items, minZoom, maxZoom)
	{
		var minZoom = isset(minZoom) ? minZoom : MIN_ZOOM;
		var maxZoom = isset(maxZoom) ? maxZoom : MAX_ZOOM;
		
		this.markerManager.addMarkers(items, minzoom, maxzoom);
		this.markerManager.refresh();
		if (!this._markers) {
			this._markers = []
		}
		this._markers = this._markers.concat(items);
	},
	removeMarker: function(item)
	{
		
		if (!item) {
			
			return false;
		}
		this.markerManager.removeMarker(item);
		if (this._markers) {
			var id = this._markers.find(item);
			if (id !== false) {
				this._markers.splice(id, 1);
			}
		}
	},
	inertAllMarkers: function()
	{
		if (!this._markers) {
			return;
		}

		for (var i=0; i<this._markers.length; i++) {
			var oldMarker = this._markers[i].activeMarker || this._markers[i];	// This prevents replacing the original marker with the inert version
			if (oldMarker.__hidden) {
				continue;
			}
			this._markers[i] = createMarker(oldMarker.getLatLng(), {icon: oldMarker.getIcon(), inert: true, draggable: false, clickable: false});
			this._markers[i].activeMarker = oldMarker;
			this.markerManager.removeMarker(oldMarker);
			this.markerManager.addMarker(this._markers[i], MIN_ZOOM, MAX_ZOOM);	// FIXME : Zoom levels need to be retained
		}
	},
	activateAllMarkers: function()
	{
		if (!this._markers) {
			return;
		}
		for (var i=0; i<this._markers.length; i++) {
			var oldMarker = this._markers[i].activeMarker;
			if (!oldMarker) {
				continue;
			}
			this.markerManager.removeMarker(this._markers[i]);
			this._markers[i] = oldMarker;
			this.markerManager.addMarker(this._markers[i], MIN_ZOOM, MAX_ZOOM);	// FIXME : Zoom levels need to be retained
		}
	},
	addPoly: function(item, minZoom, maxZoom)
	{
		var minZoom = isset(minZoom) ? minZoom : MIN_ZOOM;
		var maxZoom = isset(maxZoom) ? maxZoom : MAX_ZOOM;
		
		this.polyManager.addPoly(item, minZoom, maxZoom);
		if (!this._polys) {
			this._polys = []
		}
		this._polys.push(item);
	},
	removePoly: function(item)
	{
		
		this.polyManager.removePoly(item);
		if (this._polys) {
			var id = this._polys.find(item);
			if (id !== false) {
				this._polys.splice(id, 1);
			}
		}
	},
	hidePoly: function(item)
	{
		
		this.polyManager.hidePoly(item);
	},
	showPoly: function(item)
	{
		
		this.polyManager.showPoly(item);
	},
	disable: function()
	{
	},
	enable: function()
	{
	}
};
Panel = new Class;
Panel.prototype = {
	__construct: function(world)
	{
		this.world = world;
		this.parent = false;
		this.hidden = false;

		this.element = T.div({'class':'sidepanel_panel_open'});
		this.toggle_text = T.div({'class':'panel_toggle_text'}, 'close');
		this.title_bar = T.div({'class':'panel_title'});
		this.content = T.div({'class':'panel_content'});

		// When dragging a panel this will switch to "absolute", we force
		// "relative" so there's no surprises with regards to the layout of the
		// panel
		this.element.style.position = 'relative';
		this.content.style.position = 'relative';

		this.title_bar.appendChild(this.toggle_text);
		this.element.appendChild(this.title_bar);
		GEvent.bindDom(this.title_bar, 'click', this, this.toggle);
		this.element.appendChild(this.content);

		this.optionName = 'panel_' + this.getUID();

		if (O.getOption(this.optionName) && O.getOption(this.optionName).hidden) {
			this.hide();
		}
	},
getUID:
	function()
	{
		return false;
	},
toString:
	function()
	{
		return '[object Panel]';
	},
	initialize: function(side_panel)
	{
		this.set_parent(side_panel);
		return this.element;
	},
	toggle: function()
	{
		this.hidden ? this.show() : this.hide();
	},
busy:
	function(state)
	{
		this.is_busy = state;
		if (this.is_busy) {
			if (!this._busy_indicator) {
				this._busy_indicator = document.createElement('div');
				this._busy_indicator.appendChild(document.createElement('div'));
				this._busy_indicator.className = 'panel_busy_container';
				this._busy_indicator.firstChild.className = 'panel_busy_indicator';

				with (this._busy_indicator.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
				}
				with (this._busy_indicator.firstChild.style) {
					height = '100%';
					width = '100%';
					zIndex = 100000;
				}
				PNG.setImage(SKIN_URL+'images/busy_background.png', this._busy_indicator, true);
			}
			this.add_content(this._busy_indicator);
			this._busy_indicator.style.display = 'block';
		} else {
			if (this._busy_indicator) {
				this._busy_indicator.style.display = 'none';
			}
		}
	},
	show: function()
	{
		this.hidden = false;
		this.element.className = 'sidepanel_panel_open';
		this.element.style.display = 'block';
		this.content.style.display = 'block';
		document.emptyElement(this.toggle_text);
		this.toggle_text.appendChild(document.createTextNode('close'));
		O.unsetOption(this.optionName);
	},
	hide: function()
	{
		this.hidden = true;
		this.element.className = 'sidepanel_panel_closed';
		this.content.style.display = 'none';
		document.emptyElement(this.toggle_text);
		this.toggle_text.appendChild(document.createTextNode('open'));
		O.setOption(this.optionName, {hidden: true});
	},
	set_parent: function(parent)
	{
		this.parent = parent;
	},
	set_title: function(title)
	{
		document.emptyElement(this.title_bar);
		this.title_bar.appendChild(document.createTextNode(title));
		this.title_bar.appendChild(this.toggle_text);
	},
	set_content: function(node)
	{
		while (this.content.firstChild) {
			this.content.removeChild(this.content.firstChild);
		}
		this.add_content(node);
	},
add_content:
	function(node)
	{
		this.content.appendChild(node);
	}
}
var Icons = {
	
	'default': createIcon(
		'default',
		65, 65,
		new GPoint(32, 53),
		[32,11, 26,11, 20,15, 16,20, 14,27, 14,32, 16,39, 23,44, 32,45, 38,45, 44,42, 47,36, 50,32, 50,24, 46,18, 42,14, 36,11],
		0,
		0),
	
	'icon_2': createIcon(
		'icon_2',
		65, 65,
		new GPoint(32, 57),
		[21,10, 14,23, 19,23, 23,20, 33,31, 33,47, 22,49, 22,50, 46,50, 36,47, 36,31, 48,15, 29,15, 37,8],
		0,
		0),
	
	'start_flag': createIcon(
		'start_flag',
		32, 40,
		new GPoint(4, 39),
		[0,4 , 9,0 , 19,4 , 28,2 , 28,17 , 20,19 , 11,15 , 4,19 , 4,39],
		0,
		0),
	
	'end_flag': createIcon(
		'end_flag',
		32, 40,
		new GPoint(4, 39),
		[0,4 , 9,0 , 19,4 , 28,2 , 28,17 , 20,19 , 11,15 , 4,19 , 4,39],
		0,
		0),
	
	'route_handle': createIcon(
		'route_handle',
		11, 11,
		new GPoint(5, 5),
		null,
		1,
		1),
	
	'mini_start_flag': createIcon(
		'mini_start_flag',
		16, 20,
		new GPoint(1, 19),
		[0,0,  16,0,  16,20,  0,20],
		0,
		0),
	
	'mini_end_flag': createIcon(
		'mini_end_flag',
		16, 20,
		new GPoint(1, 19),
		[0,0,  16,0,  16,20,  0,20],
		0,
		0),
	
	'pubcrawl_handle': createIcon(
		'pubcrawl_handle',
		65, 67,
		new GPoint(32, 55),
		[20,10, 25,48, 41,48, 45,10],
		0,
		0),
	
	'icon_9': createIcon(
		'icon_9',
		65, 65,
		new GPoint(33, 52),
		[16,44, 20,12, 45,12, 50,45],
		0,
		0),
	
	'icon_10': createIcon(
		'icon_10',
		65, 60,
		new GPoint(37, 53),
		[7,27, 7,30, 12,36, 21,40, 23,42, 23,48, 27,49, 32,43, 44,43, 45,49, 49,49, 58,30, 57,20, 52,17, 44,13, 42,9, 37,8, 32,10, 32,14, 23,18, 18,15, 18,19, 14,19, 11,28],
		0,
		0),
	
	'icon_11': createIcon(
		'icon_11',
		65, 65,
		new GPoint(32, 54),
		[32,9, 43,13, 49,20, 52,32, 48,40, 39,47, 29,48, 17,41, 13,29, 18,15],
		0,
		0),
	
	'icon_12': createIcon(
		'icon_12',
		65, 65,
		new GPoint(33, 52),
		[18,45, 21,45, 21,49, 25,49, 25,45, 41,45, 41,49, 46,49, 46,45, 48,45, 48,24, 46,18, 43,13, 23,13, 18,24, 18,44],
		0,
		0),
	
	'icon_13': createIcon(
		'icon_13',
		65, 67,
		new GPoint(32, 53),
		[33,11, 31,17, 33,25, 17,25, 17,31, 22,43, 10,43, 21,47, 44,47, 54,43, 43,43, 46,40, 53,37, 55,33, 53,28, 48,25, 35,25, 37,2],
		0,
		0),
	
	'icon_14': createIcon(
		'icon_14',
		65, 65,
		new GPoint(32, 49),
		[9,43, 32,14, 56,43],
		0,
		0),
	
	'icon_15': createIcon(
		'icon_15',
		65, 67,
		new GPoint(32, 51),
		[20,22, 45,22, 51,37, 51,43, 49,51, 44,51, 44,45, 20,45, 20,51, 15,50, 13,40, 13,35],
		0,
		0),
	
	'icon_16': createIcon(
		'icon_16',
		65, 67,
		new GPoint(33, 58),
		[32,7, 21,14, 18,19, 23,51, 42,51, 47,18, 40,10],
		0,
		0),
	
	'icon_17': createIcon(
		'icon_17',
		65, 67,
		new GPoint(31, 52),
		[36,13, 27,13, 20,18, 17,23, 15,28, 15,33, 18,41, 23,45, 30,47, 36,47, 42,45, 47,38, 49,33, 49,26, 45,19, 40,15],
		0,
		0),
	
	'icon_18': createIcon(
		'icon_18',
		65, 67,
		new GPoint(25, 58),
		[25,54, 19,50, 33,7, 42,9, 42,36],
		0,
		0),
	
	'icon_19': createIcon(
		'icon_19',
		65, 65,
		new GPoint(33, 51),
		[44,13, 21,13, 26,47, 41,45],
		0,
		0),
	
	'icon_20': createIcon(
		'icon_20',
		65, 67,
		new GPoint(32, 52),
		[26,14, 26,25, 15,25, 15,34, 15,36, 27,36, 27,45, 38,45, 38,36, 49,36, 49,25, 37,25, 37,14],
		0,
		0),
	
	'icon_21': createIcon(
		'icon_21',
		65, 67,
		new GPoint(32, 50),
		[12,16, 54,16, 54,44, 12,44],
		0,
		0),
	
	'icon_22': createIcon(
		'icon_22',
		65, 65,
		new GPoint(31, 55),
		[4,42, 4,49, 61,49, 61,43, 61,42, 47,42, 52,29, 48,26, 48,18, 43,18, 43,14, 36,14, 36,9, 29,9, 29,15, 22,15, 22,18, 16,18, 16,27, 12,30, 17,42],
		0,
		0),
	
	'icon_23': createIcon(
		'icon_23',
		65, 65,
		new GPoint(31, 53),
		[17,10, 47,10, 47,46, 17,46],
		0,
		0),
	
	'icon_24': createIcon(
		'icon_24',
		65, 65,
		new GPoint(31, 55),
		[9,19, 32,8, 52,18, 48,22, 49,37, 55,49, 9,48, 14,38, 14,23],
		0,
		0),
	
	'icon_25': createIcon(
		'icon_25',
		65, 65,
		new GPoint(32, 49),
		[9,29, 24,15, 44,15, 56,29, 44,46, 20,46, 9,31],
		0,
		0),
	
	'icon_26': createIcon(
		'icon_26',
		65, 67,
		new GPoint(36, 57),
		[21,13, 18,12, 17,19, 13,25, 13,29, 22,32, 28,34, 29,44, 19,51, 53,52, 42,43, 35,29, 38,26, 38,19, 33,14, 29,9],
		0,
		0),
	
	'icon_27': createIcon(
		'icon_27',
		65, 67,
		new GPoint(36, 57),
		[21,13, 18,12, 17,19, 13,25, 13,29, 22,32, 28,34, 29,44, 19,51, 53,52, 42,43, 35,29, 38,26, 38,19, 33,14, 29,9],
		0,
		0),
	
	'icon_28': createIcon(
		'icon_28',
		65, 67,
		new GPoint(32, 52),
		[27,14, 37,14, 48,25, 48,34, 38,44, 26,44, 16,34, 16,25],
		0,
		0),
	
	'icon_29': createIcon(
		'icon_29',
		65, 67,
		new GPoint(32, 60),
		[46,5, 46,54, 19,54, 19,5],
		0,
		0),
	
	'icon_30': createIcon(
		'icon_30',
		65, 65,
		new GPoint(32, 53),
		[17,25, 33,11, 48,25, 48,46, 18,46],
		0,
		0),
	
	'icon_31': createIcon(
		'icon_31',
		65, 65,
		new GPoint(32, 53),
		[32,11, 44,16, 50,27, 46,41, 38,46, 23,45, 14,33, 14,22, 20,14],
		0,
		0),
	
	'icon_32': createIcon(
		'icon_32',
		65, 65,
		new GPoint(32, 53),
		[30,11, 44,15, 49,25, 49,32, 48,41, 45,46, 35,45, 25,44, 18,38, 15,29, 17,17],
		0,
		0),
	
	'icon_33': createIcon(
		'icon_33',
		65, 67,
		new GPoint(32, 54),
		[9,12, 56,12, 56,48, 9,48],
		0,
		0),
	
	'icon_34': createIcon(
		'icon_34',
		65, 65,
		new GPoint(32, 53),
		[19,46, 12,40, 23,17, 33,10, 44,17, 54,40, 45,46],
		0,
		0),
	
	'icon_35': createIcon(
		'icon_35',
		65, 65,
		new GPoint(28, 55),
		[17,8, 41,8, 42,39, 37,50, 19,49],
		0,
		0),
	
	'icon_36': createIcon(
		'icon_36',
		65, 65,
		new GPoint(33, 52),
		[30,11, 44,15, 49,25, 49,32, 47,39, 43,43, 35,45, 25,44, 18,38, 15,29, 17,17],
		0,
		0),
	
	'icon_37': createIcon(
		'icon_37',
		65, 65,
		new GPoint(34, 56),
		[14,48, 4,39, 18,8, 28,32, 55,32, 61,40, 61,48],
		0,
		0),
	
	'icon_38': createIcon(
		'icon_38',
		65, 67,
		new GPoint(32, 62),
		[5,41, 5,38, 15,31, 15,26, 24,21, 25,25, 29,22, 29,10, 32,5, 35,10, 35,22, 39,25, 40,21, 48,27, 49,32, 59,38, 59,41, 35,34, 35,49, 41,56, 23,56, 29,49, 29,34],
		0,
		0),
	
	'icon_39': createIcon(
		'icon_39',
		65, 65,
		new GPoint(33, 49),
		[14,43, 14,32, 21,15, 44,15, 52,32, 52,43],
		0,
		0),
	
	'icon_40': createIcon(
		'icon_40',
		65, 67,
		new GPoint(32, 54),
		[20,10, 25,48, 41,48, 45,10],
		0,
		0),
	
	'icon_41': createIcon(
		'icon_41',
		65, 65,
		new GPoint(33, 54),
		[33,10, 22,13, 17,19, 14,28, 16,37, 24,45, 33,46, 44,44, 50,36, 52,26, 47,17, 39,12],
		0,
		0),
	
	'icon_42': createIcon(
		'icon_42',
		65, 67,
		new GPoint(31, 55),
		[13,10, 50,10, 50,30, 41,44, 31,50, 19,42, 13,26],
		0,
		0),
	
	'icon_43': createIcon(
		'icon_43',
		65, 67,
		new GPoint(32, 52),
		[20,22, 45,22, 51,37, 51,43, 49,51, 44,51, 44,45, 20,45, 20,51, 15,50, 13,40, 13,35],
		0,
		0),
	
	'icon_44': createIcon(
		'icon_44',
		65, 67,
		new GPoint(34, 53),
		[35,18, 34,14, 12,20, 18,39, 27,44, 38,48, 47,43, 51,34, 53,22],
		0,
		0),
	
	'icon_45': createIcon(
		'icon_45',
		65, 67,
		new GPoint(32, 57),
		[17,49, 25,43, 21,43, 18,39, 18,16, 22,10, 43,10, 47,14, 47,39, 43,43, 40,43, 47,49],
		0,
		0),
	
	'icon_46': createIcon(
		'icon_46',
		65, 67,
		new GPoint(32, 57),
		[33,10, 26,14, 32,18, 22,18, 19,21, 19,46, 23,50, 26,50, 18,55, 46,55, 41,50, 44,50, 47,47, 47,22, 43,18, 34,18, 40,13],
		0,
		0),
	
	'icon_47': createIcon(
		'icon_47',
		65, 67,
		new GPoint(33, 51),
		[11,39, 11,20, 33,14, 55,20, 55,38, 33,44],
		0,
		0),
	
	'icon_48': createIcon(
		'icon_48',
		65, 67,
		new GPoint(32, 55),
		[4,30, 8,18, 19,12, 29,12, 50,20, 61,28, 61,39, 52,44, 40,49, 18,49, 7,41],
		0,
		0),
	
	'icon_49': createIcon(
		'icon_49',
		65, 65,
		new GPoint(32, 57),
		[15,14, 20,10, 38,7, 45,11, 46,18, 41,32, 39,37, 46,37, 50,43, 42,46, 24,52, 18,46, 20,32, 25,22, 15,18],
		0,
		0),
	
	'icon_50': createIcon(
		'icon_50',
		75, 80,
		new GPoint(36, 72),
		[38,62, 57,40, 60,28, 53,14, 42,8, 31,8, 20,16, 16,29, 19,40, 25,48],
		0,
		0),
	
	'icon_51': createIcon(
		'icon_51',
		75, 80,
		new GPoint(36, 62),
		null,
		0,
		0),
	
	'icon_52': createIcon(
		'icon_52',
		75, 80,
		new GPoint(47, 74),
		[48,69, 52,48, 63,37, 66,22, 52,9, 33,6, 15,16, 10,31, 18,44, 29,50, 38,51],
		0,
		0),
	
	'icon_53': createIcon(
		'icon_53',
		75, 80,
		new GPoint(49, 74),
		[48,69, 52,48, 63,37, 66,22, 52,9, 33,6, 15,16, 10,31, 18,44, 29,50, 38,51],
		0,
		0),
	
	'icon_54': createIcon(
		'icon_54',
		75, 80,
		new GPoint(49, 74),
		[48,69, 52,48, 63,37, 66,22, 52,9, 33,6, 15,16, 10,31, 18,44, 29,50, 38,51],
		0,
		0),
	
	'icon_55': createIcon(
		'icon_55',
		75, 80,
		new GPoint(49, 74),
		null,
		0,
		0),
	
	'icon_61': createIcon(
		'icon_61',
		65, 67,
		new GPoint(32, 50),
		[13,20, 51,20, 51,43, 13,43
],
		0,
		0),
	
	'icon_62': createIcon(
		'icon_62',
		65, 67,
		new GPoint(31, 54),
		[15,12, 50,12, 50,45, 15,45
],
		0,
		0),
	
	'icon_63': createIcon(
		'icon_63',
		65, 65,
		new GPoint(32, 51),
		[15,11, 50,11, 50,45, 15,45
],
		0,
		0),
	
	'fft_start_flag': createIcon(
		'fft_start_flag',
		65, 65,
		new GPoint(32, 55),
		[32,13, 23,8, 10,15, 10,26, 20,45, 46,45, 55,25, 55,14, 43,8
],
		0,
		0),
	
	'fot_start_flag': createIcon(
		'fot_start_flag',
		65, 65,
		new GPoint(32, 55),
		[32,13, 23,8, 10,15, 10,26, 20,45, 46,45, 55,25, 55,14, 43,8
],
		0,
		0),
	
	'icon_66': createIcon(
		'icon_66',
		65, 67,
		new GPoint(32, 60),
		[18,17, 32,12, 47,17, 47,55, 18,55],
		0,
		0),
	
	'green_flag': createIcon(
		'green_flag',
		40, 48,
		new GPoint(8, 42),
		[3,3,  36,3,  36,24,  10,24,  10,44,  3,44],
		0,
		0),
	
	'blue_flag': createIcon(
		'blue_flag',
		40, 48,
		new GPoint(8, 42),
		[3,3,  36,3,  36,24,  10,24,  10,44,  3,44],
		0,
		0),
	
	'red_flag': createIcon(
		'red_flag',
		40, 48,
		new GPoint(8, 42),
		[3,3,  36,3,  36,24,  10,24,  10,44,  3,44],
		0,
		0),
	
	'virgin_money': createIcon(
		'virgin_money',
		65, 65,
		new GPoint(32, 57),
		[11,6, 55,6, 55,50, 11,50],
		0,
		0),
	
	'route_add_handle': createIcon(
		'route_add_handle',
		20, 20,
		new GPoint(20, 20),
		[0,0, 0,18, 18,18, 18,0],
		0,
		1),
	
	'view_waypoint': createIcon(
		'view_waypoint',
		20, 20,
		new GPoint(9, 9),
		[0,0, 0,18, 18,18, 18,0],
		1,
		1),
	
	'split_route_handle': createIcon(
		'split_route_handle',
		11, 11,
		new GPoint(5, 5),
		null,
		1,
		1),
	
	'icon_75': createIcon(
		'icon_75',
		56, 78,
		new GPoint(29, 71),
		[4,4,88,4,88,123,4,123],
		0,
		0),
	
	'icon_76': createIcon(
		'icon_76',
		48, 71,
		new GPoint(21, 64),
		[5,2,97,2,97,131,5,131],
		0,
		0),
	
	'icon_77': createIcon(
		'icon_77',
		69, 51,
		new GPoint(35, 45),
		[26,43,15,35,8,28,4,16,11,6,58,3,62,17,59,32,52,38,38,43,28,40],
		0,
		0),
	
	'icon_78': createIcon(
		'icon_78',
		57, 50,
		new GPoint(29, 43),
		[6,4,53,4,53,45,6,45],
		0,
		0),
	
	'icon_79': createIcon(
		'icon_79',
		41, 81,
		new GPoint(24, 74),
		[7,5,38,5,38,78,7,78],
		0,
		0),
	
	'icon_80': createIcon(
		'icon_80',
		49, 62,
		new GPoint(24, 55),
		[5,5,98,5,98,130,5,130],
		0,
		0),
	
	'icon_81': createIcon(
		'icon_81',
		57, 75,
		new GPoint(28, 69),
		[23,66,5,43,8,4,50,6,49,42,35,70,21,65],
		0,
		0),
	
	'icon_82': createIcon(
		'icon_82',
		57, 62,
		new GPoint(28, 55),
		[4,5,62,5,62,74,4,74],
		0,
		0),
	
	'icon_83': createIcon(
		'icon_83',
		54, 68,
		new GPoint(30, 60),
		[23,60,6,21,5,-1,48,6,46,61,22,59],
		0,
		0),
	
	'icon_84': createIcon(
		'icon_84',
		53, 56,
		new GPoint(28, 50),
		[4,4,93,4,93,111,4,111],
		0,
		0),
	
	'icon_85': createIcon(
		'icon_85',
		81, 56,
		new GPoint(37, 49),
		[6,4,107,4,107,86,6,86],
		0,
		0),
	
	'icon_86': createIcon(
		'icon_86',
		40, 50,
		new GPoint(16, 43),
		[5,3,53,3,53,62,5,62],
		0,
		0),
	
	'icon_87': createIcon(
		'icon_87',
		49, 58,
		new GPoint(25, 52),
		[19,34,29,34,29,55,19,55],
		0,
		0),
	
	'icon_88': createIcon(
		'icon_88',
		58, 63,
		new GPoint(23, 55),
		[15,53,2,47,7,21,35,16,47,5,53,5,49,45,30,62,15,52],
		0,
		0),
	
	'icon_89': createIcon(
		'icon_89',
		39, 48,
		new GPoint(20, 41),
		[4,2,69,2,69,79,4,79],
		0,
		0),
	
	'icon_90': createIcon(
		'icon_90',
		49, 78,
		new GPoint(26, 71),
		[6,67,14,76,33,75,44,65,32,25,37,2,14,3,16,31,8,59],
		0,
		0),
	
	'icon_91': createIcon(
		'icon_91',
		60, 50,
		new GPoint(33, 43),
		[29,49,6,29,7,22,29,7,46,7,53,18,47,39,34,44],
		0,
		0),
	
	'icon_92': createIcon(
		'icon_92',
		53, 66,
		new GPoint(27, 60),
		[20,60,5,41,5,30,25,5,44,8,47,43,29,57,24,57],
		0,
		0),
	
	'icon_93': createIcon(
		'icon_93',
		55, 56,
		new GPoint(25, 50),
		[20,49,13,38,6,29,5,12,13,3,31,6,47,14,48,36,28,47,20,46],
		0,
		0),
	
	'icon_94': createIcon(
		'icon_94',
		67, 63,
		new GPoint(32, 55),
		[3,3,148,3,148,92,3,92],
		0,
		0),
	
	'icon_95': createIcon(
		'icon_95',
		44, 54,
		new GPoint(21, 47),
		[5,3,39,3,39,48,5,48],
		0,
		0),
	
	'icon_96': createIcon(
		'icon_96',
		64, 64,
		new GPoint(33, 54),
		[5,3,110,3,110,114,5,114],
		0,
		0),
	
	'icon_97': createIcon(
		'icon_97',
		54, 52,
		new GPoint(26, 45),
		[27,25,28],
		0,
		0),
	
	'icon_98': createIcon(
		'icon_98',
		78, 62,
		new GPoint(40, 56),
		[31,54,18,40,5,36,5,24,14,19,16,5,58,6,71,21,71,48,42,54,30,54],
		0,
		0),
	
	'icon_99': createIcon(
		'icon_99',
		76, 89,
		new GPoint(39, 82),
		[42,79,34,80,32,74],
		0,
		0),
	
	'icon_100': createIcon(
		'icon_100',
		61, 53,
		new GPoint(33, 46),
		[5,5,77,5,77,62,5,62],
		0,
		0),
	
	'icon_101': createIcon(
		'icon_101',
		54, 43,
		new GPoint(30, 36),
		[23,34,5,22,5,13,39,5,47,6,48,18,38,37,21,34],
		0,
		0),
	
	'icon_102': createIcon(
		'icon_102',
		83, 54,
		new GPoint(42, 47),
		[6,1,108,1,108,72,6,72],
		0,
		0),
	
	'icon_103': createIcon(
		'icon_103',
		49, 47,
		new GPoint(22, 40),
		[6,6,59,6,59,45,6,45],
		0,
		0),
	
	'icon_104': createIcon(
		'icon_104',
		44, 51,
		new GPoint(21, 45),
		[16,43,6,27,6,14,16,7,29,7,40,12,43,28,31,37,24,43,15,41],
		0,
		0),
	
	'icon_105': createIcon(
		'icon_105',
		48, 57,
		new GPoint(24, 50),
		[4,3,72,3,72,78,4,78],
		0,
		0),
	
	'icon_106': createIcon(
		'icon_106',
		58, 72,
		new GPoint(39, 65),
		[2,12,16,26,30,58,43,66,45,52,51,48,31,16,25,16,16,5,7,5,5,7],
		0,
		0),
	
	'icon_107': createIcon(
		'icon_107',
		43, 53,
		new GPoint(21, 46),
		[17,44,16,37,3,29,8,3,36,6,36,33,26,45,17,44],
		0,
		0),
	
	'icon_108': createIcon(
		'icon_108',
		58, 66,
		new GPoint(28, 59),
		[20,57],
		0,
		0),
	
	'icon_109': createIcon(
		'icon_109',
		59, 43,
		new GPoint(29, 36),
		[27,38,5,25,5,1,52,6,52,23,34,37],
		0,
		0),
	
	'icon_110': createIcon(
		'icon_110',
		40, 97,
		new GPoint(21, 90),
		[16,91,17,57,7,35,5,6,33,6,33,39,24,57,24,90,15,89],
		0,
		0),
	
	'icon_111': createIcon(
		'icon_111',
		61, 62,
		new GPoint(30, 51),
		[6,6,74,6,74,92,6,92],
		0,
		0),
	
	'icon_112': createIcon(
		'icon_112',
		31, 54,
		new GPoint(15, 47),
		[5,0,50,0,50,77,5,77],
		0,
		0),
	
	'icon_113': createIcon(
		'icon_113',
		110, 64,
		new GPoint(55, 56),
		[51,57,7,27,6,13,45,6,77,6,103,16,103,28,58,56,49,55],
		0,
		0),
	
	'icon_114': createIcon(
		'icon_114',
		44, 65,
		new GPoint(20, 58),
		[5,4,46,4,46,72,5,72],
		0,
		0),
	
	'icon_115': createIcon(
		'icon_115',
		45, 67,
		new GPoint(19, 60),
		null,
		0,
		0),
	
	'sport_relief': createIcon(
		'sport_relief',
		50, 56,
		new GPoint(26, 52),
		null,
		0,
		0),
	
	'sport\x2Drelief\x2Dregional\x2Dmile': createIcon(
		'sport\x2Drelief\x2Dregional\x2Dmile',
		50, 55,
		new GPoint(5, 5),
		null,
		0,
		0),
	
	'sport_relief_training': createIcon(
		'sport_relief_training',
		50, 55,
		new GPoint(26, 52),
		null,
		0,
		0)
	

};
G_DEFAULT_ICON = Icons['default'];
function setCookie(name, value, expires, path, domain, secure) {
	if (!expires) {
		var expires = new Date();
		expires.setTime(expires.getTime() + 3600000*24*365);
	}
    document.cookie= name + "=" + escape(value) +
        ((expires) ? "; expires=" + expires.toGMTString() : "") +
        ((path)    ? "; path=" + path : "") +
        ((domain)  ? "; domain=" + domain : "") +
        ((secure ) ? "; secure" : "");
}
function getCookie(name) {
    var dc = document.cookie;
    var prefix = name + "=";
    var begin = dc.indexOf("; " + prefix);
    if (begin == -1) {
        begin = dc.indexOf(prefix);
        if (begin != 0) return null;
    } else {
        begin += 2;
    }
    var end = document.cookie.indexOf(";", begin);
    if (end == -1) {
        end = dc.length;
    }
    return unescape(dc.substring(begin + prefix.length, end));
}
function deleteCookie(name, path, domain) {
    if (getCookie(name)) {
        document.cookie = name + "=" +
            ((path) ? "; path=" + path : "") +
            ((domain) ? "; domain=" + domain : "") +
            "; expires=Thu, 01-Jan-70 00:00:01 GMT";
    }
}
MarkerHighlight = new Class(GOverlay);
MarkerHighlight.prototype = {
	__construct: function(point)
	{
		this.point = point;
	},
	toString: function()
	{
		return '[object MarkerHighlight]';
	},
	initialize: function(gmap)
	{
		
		this.gMap = gmap;
		this.element = document.createElement('div');

		with (this.element.style)
		{
			position = 'absolute';
			width = '24px';
			height = '18px';
			marginLeft = '-12px';
			marginTop = '-9px';
		}

		this.element.style.background = 'url('+SKIN_URL+'images/marker_highlight.png)';
		this.gMap.getPane(G_MAP_MAP_PANE).appendChild(this.element);
	},
	remove: function()
	{
		this.element.parentNode.removeChild(this.element);
	},
	copy: function()
	{
		return new MarkerHighlight(this.point, this.message);
	},
	redraw: function(force)
	{
		if (!force) return;

		if (!this.gMap) {
			
			return false;
		}
		
		var p = this.gMap.fromLatLngToDivPixel(this.point);
		this.element.style.left = p.x +'px';
		this.element.style.top = p.y +'px';
	},
	setLatLng: function(point)
	{
		this.point = point;
		this.redraw(true);
	},
	getLatLng: function()
	{
		return this.point;
	},
	show: function()
	{
		this.element.style.display = 'block';
	},
	hide: function()
	{
		this.element.style.display = 'none';
	}
};



DockedPanel = new Class(FloatingPanel);
DockedPanel.prototype = {
	__construct: function(y, width, height)
	{
		
		// All these come from base class (FloatingPanel)
		var x = -10;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;

		if (!this.minimumSize) {
			this.minimumSize = {
				width: 200,
				height: 70
			};
		}


		if (!this.padding) {
			this.padding = {
				left: 3,
				top: 1,
				right: 11,
				bottom: 13
			};
		}

		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/panel_small.png', 1000, 1000, w, h, 8, 16, 16, 150);
		}
		FloatingPanel.prototype.__construct.call(this, x, y, width, height);

		this.movable(false);
		
		this.title.className = 'mini_panel_title';

		// Remove close button
		GEvent.clearInstanceListeners(this.closeButton);
		this.closeButton.parentNode.removeChild(this.closeButton);



		// Add in right side section
		this.toolbox = T.div({'class':'panel_toolbox'});
		with (this.toolbox.style) {
			background = '#fff url('+SKIN_URL+'images/panel_divider.gif) repeat-y';
			position = 'absolute';
			right = 0;
			top = 0;
			height = '100%';
			width = '100px';
			paddingLeft = '2px';
		}
		this.content.appendChild(this.toolbox);
		this.content.style.paddingRight = '120px';

		GEvent.bind(this, 'resize', this,
			function()
			{
				this.content.style.width = (this.width -this.padding.left -this.padding.right -120) +'px';
				if (this._busyIndicator) {
					this._busyIndicator.style.width = this.content.style.width;
				}
			}
		);

		// Add a hide button
		this.hideButton = document.createElement('div');
		this.hideButton.title = 'Toggle this panel';
		this.hideButton.className = 'panel_hide';
		with (this.hideButton.style) {
			position = 'absolute';
			left = '50%';
			marginLeft = '-10px';
			top = (this.padding.top +12) +'px';
			width = '20px';
			height = '20px';
			background = 'url('+SKIN_URL+'images/panel_hide.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}
		GEvent.bindDom(this.hideButton, 'mouseover', this,
			function(e)
			{
				var b = this.hideButton;
				if (typeof this._target_location == 'undefined' || this._target_location > -50) {
					b.style.backgroundPosition = '0 -20px';
				} else {
					b.style.backgroundPosition = '0 -60px';
				}
			}
		);
		GEvent.bindDom(this.hideButton, 'mouseout', this,
			function(e)
			{
				var b = this.hideButton;
				if (typeof this._target_location == 'undefined' || this._target_location > -50) {
					b.style.backgroundPosition = '0 0px';
				} else {
					b.style.backgroundPosition = '0 -40px';
				}
			}
		);
		GEvent.bindDom(this.hideButton, 'click', this, function() { this.toggle() });
		this.toolbox.appendChild(this.hideButton);

		var my_routes = new Button('my routes',
			function () 
			{
				if (!realWorld.user.is_authenticated) {
					realWorld.user.login('You need to login to view your routes.', GEvent.callback(this, function() { realWorld.modifyItem(Route); }));
				} else {
					realWorld.modifyItem(Route);
				}
			}, 85);
		with (my_routes.element.style) {
			position = 'relative';
			top = '40px';
			margin = '1em auto';
		}
		var my_markers = new Button('my markers',
			function ()
			{
				if (!realWorld.user.is_authenticated) {
					realWorld.user.login('You need to login to view your markers.', GEvent.callback(this, function() { realWorld.modifyItem(Marker); }));
				} else {
					realWorld.modifyItem(Marker);
				}
			}, 85);

		with (my_markers.element.style) {
			position = 'relative';
			top = '40px';
			margin = '1em auto';
		}

		this.toolbox.appendChild(my_routes.element);
		this.toolbox.appendChild(my_markers.element);


		
		/*
		GEvent.bindDom(this.hideButton, 'click', this,
			function()
			{
				this.toggle();
			}
		);
		*/



		/*
		this.closeButton.className = 'mini_panel_close';
		with (this.closeButton.style) {
			position = 'absolute';
			right = (this.padding.right+10) +'px';
			top = (this.padding.top +7) +'px';
			width = '12px';
			height = '12px';
			background = 'url('+SKIN_URL+'images/delete_upload.gif)';
			zIndex = '2000';
			cursor = 'pointer';
		}
		*/

		with (this.title.style) {
			height = '3px';
			overflow = 'hidden';
			background = '#e4e4e4';
		}

		this.resizable(false);

		GEvent.bind(this, 'show', this, function() {
			// FIXME : This timeout causes the panel to be sized properly in Safari
			setTimeout(GEvent.callback(this, function() { this.resize() }), 1);
		});
	},
	toggle: function(state)
	{
		if (typeof state == 'undefined') {
			var state = !this._state;
		}
		this._state = state;
		GEvent.trigger(this, 'dock', this._state);

		// This is where the panel will be when it's finished animating
		if (typeof this._target_location == 'undefined' || this._target_location > -50) {
			this.animate(-(this.getWidth()-110));
		} else {
			this.animate(-10);
		}

	},
	animate: function(target)
	{
		var speed = 10;
		if (typeof target != 'undefined') {
			this._target_location = target;
		}

		var new_x = this.getX();
		var new_y = false;
		
		if (this._target_location < -50) {
			new_x -= speed;
			if (new_x < this._target_location) {
				new_x = this._target_location;
			}
		} else {
			new_x += speed;
			if (new_x > this._target_location) {
				new_x = this._target_location;
			}
		}

		// Animation isn't complete
		if ((this.getX() > this._target_location && new_x > this._target_location) ||
		   (this.getX() < this._target_location && new_x < this._target_location)) {
			setTimeout(GEvent.callback(this, function() { this.animate(); }), 10);
		}

		this.move(new_x, new_y);
	}
};

function T(tag){
	 return function(){
		var e = document.createElement(tag);
		function setChild(a){
			if ("string,number".indexOf(typeof(a)) != -1) {
				e.appendChild(document.createTextNode(a.toString()));
			} else if (a instanceof Array) {
				for (var i=0; i<a.length; i++) {
					if (!a[i]) {
						continue;
					}
					if (a[i].element) {
						e.appendChild(a[i].element);
					} else {
						e.appendChild(a[i]);
					}
				}
			} else if (a == undefined) {
				return true;
			} else {
				return false;
			}
			return true;
		}

		if (!setChild(arguments[0])){
			if (tag == 'form') {
				e = document.createFormElement(arguments[0]);
			} else if (arguments[0]['name']) {
				e = document.createNamedElement(tag, arguments[0]['name']);
			}

			if (arguments[0]['className'] || arguments[0]['class']) {
				e.className = arguments[0]['className'] || arguments[0]['class'];
			}
			for (key in arguments[0]) {
				switch (key) {
				case 'checked':
					e.checked = arguments[0][key];
					break;
				case 'selected':
					e.selected = arguments[0][key];
					break;
				default:
					e.setAttribute(key=='className'?'class':key, arguments[0][key].toString());
					break;
				}
			}

			if (setChild(arguments[1])) {
				if (e.tabName == 'none') {
					return e.firstChild;
				}
				return e;
			}
			return null; // oops!
		} else {
			if (e.tabName == 'none') {
				return e.firstChild;
			}
			return e;
		}
	}
}
(function(tags){
	for (var i=0; i<tags.length; i++) {
		T[tags[i]] = T(tags[i]);
	}
	T['rating'] = function(options, buttons)
	{
		if (options['class']) {
			options['class'] += ' rating_options';
		} else {
			options['class'] = 'rating_options';
		}
		var n = T.div(options);
		var stars = T.ul({'class': 'rating_stars'});
		delete options['class'];
		options.type = 'radio';
		for (var i=1; i<=5; i++) {
			options.value = i;
			var c = T.input(options);
			c.style.display = 'none';
			n.appendChild(T.span([c]));
			var s = T.li([T.span('*')]);
			stars.appendChild(s);

			// show score on mouse over
			GEvent.bindDom(s, 'mouseover', c, function()
			{
				n.value = this.value;
				var li = stars.getElementsByTagName('li');
				for (var i=0; i<li.length; i++) {
					li[i].className = (i < this.value) ? 'active' : 'inactive';
				}
				this.checked = true;
			});
		}
		// unselect all ratings if mouse moved out
		GEvent.bindDom(stars, 'mouseout', n, function()
		{
			var inputs = this.getElementsByTagName('input');
			for (var i=0; i<inputs.length; i++) {
				inputs[i].checked = false;
			}
			var li = stars.getElementsByTagName('li');
			for (var i=0; i<li.length; i++) {
				li[i].className = 'inactive';
			}
		});
		// send rating when clicked
		GEvent.bindDom(stars, 'click', n, function()
		{
			GEvent.trigger(n, 'rate', n.value);
		});



		n.appendChild(stars);

		return n;
	};
	T['buttonRow'] = function(options, buttons)
	{
		var btns = [];
		if (options instanceof Array) {
			buttons = options;
			var options = {};
		}

		var options = options || {};
		for (var i=0; i<buttons.length; i++) {
			if (buttons[i].element) {
				btns.push(T.td([buttons[i].element]));
			} else {
				btns.push(T.td([buttons[i]]));
			}
		}
		var tbl = T.table(options, [T.tbody([T.tr(btns)])]);
		tbl.style.clear = 'both';
		return tbl;
	};
	T['video'] = function(options)
	{
		var params = [
			T.param({'name':'allowScriptAccess', 'value':'sameDomain'}),
			T.param({'name':'movie',	 'value':'/vodcasts/FlowPlayer.swf'}),
			T.param({'name':'quality',   'value':'high'}),
			T.param({'name':'scale',	 'value':'noScale'}),
			//T.param({'name':'wmode',	 'value':'transparent'}),
			T.param({'name':'flashvars', 'value':'config={videoFile: \''+options.src+'\', autoPlay: false, initialScale: \'fit\', overlayId: \'play\'}'})
		];
		var player = T.object({'type':'application/x-shockwave-flash', 'data':'/vodcasts/FlowPlayer.swf', 'width':'512', 'height':'384'});

		// This innerHTML crap is for IE, it doesn't allow appendChild on an <object> tag for a reason only Ballmer himself knows.
		var html = '<object type="application/x-shockwave-flash" data="/vodcasts/FlowPlayer.swf" width="512" height="384">';
		for (var i=0; i<params.length; i++) {
			if (params[i].outerHTML) {
				html += params[i].outerHTML;
			} else {
				player.appendChild(params[i]);
			}
		}
		html += '</object>';
		if (player.outerHTML) {
			player = T.div();
			player.innerHTML = html;
		}
		return player;
	};
})(['br', 'h1','h2','h3','h4','h5','h6','embed', 'select', 'optgroup', 'option', 'object', 'param', 'iframe', 'ul', 'ol',
	'li', 'dt', 'dl', 'dd', 'table', 'tbody', 'thead', 'tfoot', 'tr', 'td',
	'th', 'textarea', 'form', 'label', 'button', 'input', 'a', 'p', 'div',
	'span', 'em', 'sup', 'img', 'none']); 

DrawQueue = new Class;
DrawQueue.prototype = {
__construct:
	function(world)
	{
		this.world = world;

		// Items on the map
		this.items = {};

		this.started = false;

		this.queue = {};
		this._queue_ids = [];
		this._total_drawn = 0;
		this._current_item = 0;
	},
populate:
	function(items)
	{
		this.clear();
		this.add_to_queue(items);
	},
add_to_queue:
	function(items)
	{
		for (x in items) {
			this.queue[x] = items[x];
			this._queue_ids.push(x);
		}
	},
show_category:
	function(type, id)
	{
		if (!this.world.filters[type]) {
			this.world.filters[type] = {};		   
		}
		this.world.filters[type][id] = false;
		this.update_catetgory(type, id);
	},
/**
 * @param MapItem item This is a complete map item
 */
add_item:
	function(item)
	{
		if (this.items[item.properties.uid]) {
			return this.items[item.properties.uid];
		}
		this.items[item.properties.uid] = item;
		this.queue[item.properties.uid] = {anchor: [item.anchor.lat(), item.anchor.lng()], properties: item.properties, data: item.data};
		this._queue_ids.push(item.properties.uid);
		return this.items[item.properties.uid]; 
	},
hide_category:
	function(type, id)
	{
		if (!this.world.filters[type]) {
			this.world.filters[type] = {};		   
		}
		this.world.filters[type][id] = true;
		this.update_catetgory(type, id);
	},
update_catetgory:
	function(type, id)
	{
		for (x in this.items) {
			var i = this.items[x];
			if (i.properties.class_name == type && i.properties.category == id) {
				if (this.world.filters[type][id]) {
					i.hide();
				} else {
					i.show();
				}
			}
		}
	},
draw_item:
	function(id)
	{
		GEvent.trigger(this, 'drawitemstart');
		var item = this.queue[id];
		if (item && item.properties) {
			// If item isn't in cache...
			if (!this.items[item.properties.uid]) {
				// If item isn't cached, then create it

				var hidden = (!this.world.routes_shown && (item.properties.class_name == 'Route' || item.properties.class_name == 'PubCrawl'));
				hidden = hidden || this.world.is_filtered(item.properties.class_name, item.properties.category);

				var item_inst = MapItem.create_from_json(item, this.world, hidden);

				// Item created successfully
				if (item_inst) {
					this.items[item_inst.properties.uid] = item_inst;
				}
			} else {
				// Item is already cached
				//
				this.items[item.properties.uid].show();
				var item_inst = this.items[item.properties.uid];
			}
		} else {
			
			var item_inst = false;
		}
		if (item && !this.world.is_blocked(item.properties.class_name, item.properties.category)) {
			this._total_drawn++;
			GEvent.trigger(this, 'drawitemend', item_inst);
		}

		if (!item_inst) {
			return false;
		}


		return item_inst;
	},
draw_next:
	function(noevent)
	{
		var item = this.draw_item(this._queue_ids[this._current_item]);
		if (item.properties) {
			//
		}
		this._current_item++;

		if (this._current_item < this._queue_ids.length && this._current_item % ITEMS_PER_DRAW != 0) {
			this.draw_next(true);
		}

		if (noevent !== true) {
			GEvent.trigger(this, 'drawnextend', item);
		}
		return item;
	},
start:
	function()
	{
		GEvent.trigger(this, 'start');
		
		this._total_drawn = 0;
		this.started = true;
		this._tick_event = GEvent.bind(this, 'drawnextend', this,
			function()
			{
				if (this.started && this._current_item < this._queue_ids.length) {
					this._timer = setTimeout(GEvent.callback(this, this.draw_next), 1);
				} else if (this.started) {
					this.stop();
				}
			}
		);
		this._timer = setTimeout(GEvent.callback(this, this.draw_next), 1);
	},
stop:
	function()
	{
		this.started = false;
		if (this._timer) {
			clearTimeout(this._timer);
			delete this._timer;
		}
		if (this._tick_event) {
			GEvent.removeListener(this._tick_event);
			delete this._tick_event;
		}
		this.queue = {};
		GEvent.trigger(this, 'stop', this._total_drawn);
		
	},
clear:
	function(force)
	{
		this.stop();
		// TODO Intelligently work out which markers need hiding/removing
		for (x in this.items) {
			var item = this.items[x];
			if (!force && item.sticky) {
				continue;
			}
			if (CACHE_ITEMS) {
				item.hide();
			} else {
				item.destroy();
				delete this.items[x];
				delete item;
			}
		}

		this.queue = {};
		this._queue_ids = [];
		this._current_item = 0;
	}
};

var Icons = {
	
	'default': createIcon(
		'default',
		65, 65,
		new GPoint(32, 53),
		[32,11, 26,11, 20,15, 16,20, 14,27, 14,32, 16,39, 23,44, 32,45, 38,45, 44,42, 47,36, 50,32, 50,24, 46,18, 42,14, 36,11],
		0,
		0),
	
	'icon_2': createIcon(
		'icon_2',
		65, 65,
		new GPoint(32, 57),
		[21,10, 14,23, 19,23, 23,20, 33,31, 33,47, 22,49, 22,50, 46,50, 36,47, 36,31, 48,15, 29,15, 37,8],
		0,
		0),
	
	'start_flag': createIcon(
		'start_flag',
		32, 40,
		new GPoint(4, 39),
		[0,4 , 9,0 , 19,4 , 28,2 , 28,17 , 20,19 , 11,15 , 4,19 , 4,39],
		0,
		0),
	
	'end_flag': createIcon(
		'end_flag',
		32, 40,
		new GPoint(4, 39),
		[0,4 , 9,0 , 19,4 , 28,2 , 28,17 , 20,19 , 11,15 , 4,19 , 4,39],
		0,
		0),
	
	'route_handle': createIcon(
		'route_handle',
		11, 11,
		new GPoint(5, 5),
		null,
		1,
		1),
	
	'mini_start_flag': createIcon(
		'mini_start_flag',
		16, 20,
		new GPoint(1, 19),
		[0,0,  16,0,  16,20,  0,20],
		0,
		0),
	
	'mini_end_flag': createIcon(
		'mini_end_flag',
		16, 20,
		new GPoint(1, 19),
		[0,0,  16,0,  16,20,  0,20],
		0,
		0),
	
	'pubcrawl_handle': createIcon(
		'pubcrawl_handle',
		65, 67,
		new GPoint(32, 55),
		[20,10, 25,48, 41,48, 45,10],
		0,
		0),
	
	'icon_9': createIcon(
		'icon_9',
		65, 65,
		new GPoint(33, 52),
		[16,44, 20,12, 45,12, 50,45],
		0,
		0),
	
	'icon_10': createIcon(
		'icon_10',
		65, 60,
		new GPoint(37, 53),
		[7,27, 7,30, 12,36, 21,40, 23,42, 23,48, 27,49, 32,43, 44,43, 45,49, 49,49, 58,30, 57,20, 52,17, 44,13, 42,9, 37,8, 32,10, 32,14, 23,18, 18,15, 18,19, 14,19, 11,28],
		0,
		0),
	
	'icon_11': createIcon(
		'icon_11',
		65, 65,
		new GPoint(32, 54),
		[32,9, 43,13, 49,20, 52,32, 48,40, 39,47, 29,48, 17,41, 13,29, 18,15],
		0,
		0),
	
	'icon_12': createIcon(
		'icon_12',
		65, 65,
		new GPoint(33, 52),
		[18,45, 21,45, 21,49, 25,49, 25,45, 41,45, 41,49, 46,49, 46,45, 48,45, 48,24, 46,18, 43,13, 23,13, 18,24, 18,44],
		0,
		0),
	
	'icon_13': createIcon(
		'icon_13',
		65, 67,
		new GPoint(32, 53),
		[33,11, 31,17, 33,25, 17,25, 17,31, 22,43, 10,43, 21,47, 44,47, 54,43, 43,43, 46,40, 53,37, 55,33, 53,28, 48,25, 35,25, 37,2],
		0,
		0),
	
	'icon_14': createIcon(
		'icon_14',
		65, 65,
		new GPoint(32, 49),
		[9,43, 32,14, 56,43],
		0,
		0),
	
	'icon_15': createIcon(
		'icon_15',
		65, 67,
		new GPoint(32, 51),
		[20,22, 45,22, 51,37, 51,43, 49,51, 44,51, 44,45, 20,45, 20,51, 15,50, 13,40, 13,35],
		0,
		0),
	
	'icon_16': createIcon(
		'icon_16',
		65, 67,
		new GPoint(33, 58),
		[32,7, 21,14, 18,19, 23,51, 42,51, 47,18, 40,10],
		0,
		0),
	
	'icon_17': createIcon(
		'icon_17',
		65, 67,
		new GPoint(31, 52),
		[36,13, 27,13, 20,18, 17,23, 15,28, 15,33, 18,41, 23,45, 30,47, 36,47, 42,45, 47,38, 49,33, 49,26, 45,19, 40,15],
		0,
		0),
	
	'icon_18': createIcon(
		'icon_18',
		65, 67,
		new GPoint(25, 58),
		[25,54, 19,50, 33,7, 42,9, 42,36],
		0,
		0),
	
	'icon_19': createIcon(
		'icon_19',
		65, 65,
		new GPoint(33, 51),
		[44,13, 21,13, 26,47, 41,45],
		0,
		0),
	
	'icon_20': createIcon(
		'icon_20',
		65, 67,
		new GPoint(32, 52),
		[26,14, 26,25, 15,25, 15,34, 15,36, 27,36, 27,45, 38,45, 38,36, 49,36, 49,25, 37,25, 37,14],
		0,
		0),
	
	'icon_21': createIcon(
		'icon_21',
		65, 67,
		new GPoint(32, 50),
		[12,16, 54,16, 54,44, 12,44],
		0,
		0),
	
	'icon_22': createIcon(
		'icon_22',
		65, 65,
		new GPoint(31, 55),
		[4,42, 4,49, 61,49, 61,43, 61,42, 47,42, 52,29, 48,26, 48,18, 43,18, 43,14, 36,14, 36,9, 29,9, 29,15, 22,15, 22,18, 16,18, 16,27, 12,30, 17,42],
		0,
		0),
	
	'icon_23': createIcon(
		'icon_23',
		65, 65,
		new GPoint(31, 53),
		[17,10, 47,10, 47,46, 17,46],
		0,
		0),
	
	'icon_24': createIcon(
		'icon_24',
		65, 65,
		new GPoint(31, 55),
		[9,19, 32,8, 52,18, 48,22, 49,37, 55,49, 9,48, 14,38, 14,23],
		0,
		0),
	
	'icon_25': createIcon(
		'icon_25',
		65, 65,
		new GPoint(32, 49),
		[9,29, 24,15, 44,15, 56,29, 44,46, 20,46, 9,31],
		0,
		0),
	
	'icon_26': createIcon(
		'icon_26',
		65, 67,
		new GPoint(36, 57),
		[21,13, 18,12, 17,19, 13,25, 13,29, 22,32, 28,34, 29,44, 19,51, 53,52, 42,43, 35,29, 38,26, 38,19, 33,14, 29,9],
		0,
		0),
	
	'icon_27': createIcon(
		'icon_27',
		65, 67,
		new GPoint(36, 57),
		[21,13, 18,12, 17,19, 13,25, 13,29, 22,32, 28,34, 29,44, 19,51, 53,52, 42,43, 35,29, 38,26, 38,19, 33,14, 29,9],
		0,
		0),
	
	'icon_28': createIcon(
		'icon_28',
		65, 67,
		new GPoint(32, 52),
		[27,14, 37,14, 48,25, 48,34, 38,44, 26,44, 16,34, 16,25],
		0,
		0),
	
	'icon_29': createIcon(
		'icon_29',
		65, 67,
		new GPoint(32, 60),
		[46,5, 46,54, 19,54, 19,5],
		0,
		0),
	
	'icon_30': createIcon(
		'icon_30',
		65, 65,
		new GPoint(32, 53),
		[17,25, 33,11, 48,25, 48,46, 18,46],
		0,
		0),
	
	'icon_31': createIcon(
		'icon_31',
		65, 65,
		new GPoint(32, 53),
		[32,11, 44,16, 50,27, 46,41, 38,46, 23,45, 14,33, 14,22, 20,14],
		0,
		0),
	
	'icon_32': createIcon(
		'icon_32',
		65, 65,
		new GPoint(32, 53),
		[30,11, 44,15, 49,25, 49,32, 48,41, 45,46, 35,45, 25,44, 18,38, 15,29, 17,17],
		0,
		0),
	
	'icon_33': createIcon(
		'icon_33',
		65, 67,
		new GPoint(32, 54),
		[9,12, 56,12, 56,48, 9,48],
		0,
		0),
	
	'icon_34': createIcon(
		'icon_34',
		65, 65,
		new GPoint(32, 53),
		[19,46, 12,40, 23,17, 33,10, 44,17, 54,40, 45,46],
		0,
		0),
	
	'icon_35': createIcon(
		'icon_35',
		65, 65,
		new GPoint(28, 55),
		[17,8, 41,8, 42,39, 37,50, 19,49],
		0,
		0),
	
	'icon_36': createIcon(
		'icon_36',
		65, 65,
		new GPoint(33, 52),
		[30,11, 44,15, 49,25, 49,32, 47,39, 43,43, 35,45, 25,44, 18,38, 15,29, 17,17],
		0,
		0),
	
	'icon_37': createIcon(
		'icon_37',
		65, 65,
		new GPoint(34, 56),
		[14,48, 4,39, 18,8, 28,32, 55,32, 61,40, 61,48],
		0,
		0),
	
	'icon_38': createIcon(
		'icon_38',
		65, 67,
		new GPoint(32, 62),
		[5,41, 5,38, 15,31, 15,26, 24,21, 25,25, 29,22, 29,10, 32,5, 35,10, 35,22, 39,25, 40,21, 48,27, 49,32, 59,38, 59,41, 35,34, 35,49, 41,56, 23,56, 29,49, 29,34],
		0,
		0),
	
	'icon_39': createIcon(
		'icon_39',
		65, 65,
		new GPoint(33, 49),
		[14,43, 14,32, 21,15, 44,15, 52,32, 52,43],
		0,
		0),
	
	'icon_40': createIcon(
		'icon_40',
		65, 67,
		new GPoint(32, 54),
		[20,10, 25,48, 41,48, 45,10],
		0,
		0),
	
	'icon_41': createIcon(
		'icon_41',
		65, 65,
		new GPoint(33, 54),
		[33,10, 22,13, 17,19, 14,28, 16,37, 24,45, 33,46, 44,44, 50,36, 52,26, 47,17, 39,12],
		0,
		0),
	
	'icon_42': createIcon(
		'icon_42',
		65, 67,
		new GPoint(31, 55),
		[13,10, 50,10, 50,30, 41,44, 31,50, 19,42, 13,26],
		0,
		0),
	
	'icon_43': createIcon(
		'icon_43',
		65, 67,
		new GPoint(32, 52),
		[20,22, 45,22, 51,37, 51,43, 49,51, 44,51, 44,45, 20,45, 20,51, 15,50, 13,40, 13,35],
		0,
		0),
	
	'icon_44': createIcon(
		'icon_44',
		65, 67,
		new GPoint(34, 53),
		[35,18, 34,14, 12,20, 18,39, 27,44, 38,48, 47,43, 51,34, 53,22],
		0,
		0),
	
	'icon_45': createIcon(
		'icon_45',
		65, 67,
		new GPoint(32, 57),
		[17,49, 25,43, 21,43, 18,39, 18,16, 22,10, 43,10, 47,14, 47,39, 43,43, 40,43, 47,49],
		0,
		0),
	
	'icon_46': createIcon(
		'icon_46',
		65, 67,
		new GPoint(32, 57),
		[33,10, 26,14, 32,18, 22,18, 19,21, 19,46, 23,50, 26,50, 18,55, 46,55, 41,50, 44,50, 47,47, 47,22, 43,18, 34,18, 40,13],
		0,
		0),
	
	'icon_47': createIcon(
		'icon_47',
		65, 67,
		new GPoint(33, 51),
		[11,39, 11,20, 33,14, 55,20, 55,38, 33,44],
		0,
		0),
	
	'icon_48': createIcon(
		'icon_48',
		65, 67,
		new GPoint(32, 55),
		[4,30, 8,18, 19,12, 29,12, 50,20, 61,28, 61,39, 52,44, 40,49, 18,49, 7,41],
		0,
		0),
	
	'icon_49': createIcon(
		'icon_49',
		65, 65,
		new GPoint(32, 57),
		[15,14, 20,10, 38,7, 45,11, 46,18, 41,32, 39,37, 46,37, 50,43, 42,46, 24,52, 18,46, 20,32, 25,22, 15,18],
		0,
		0),
	
	'icon_50': createIcon(
		'icon_50',
		75, 80,
		new GPoint(36, 72),
		[38,62, 57,40, 60,28, 53,14, 42,8, 31,8, 20,16, 16,29, 19,40, 25,48],
		0,
		0),
	
	'icon_51': createIcon(
		'icon_51',
		75, 80,
		new GPoint(36, 62),
		null,
		0,
		0),
	
	'icon_52': createIcon(
		'icon_52',
		75, 80,
		new GPoint(47, 74),
		[48,69, 52,48, 63,37, 66,22, 52,9, 33,6, 15,16, 10,31, 18,44, 29,50, 38,51],
		0,
		0),
	
	'icon_53': createIcon(
		'icon_53',
		75, 80,
		new GPoint(49, 74),
		[48,69, 52,48, 63,37, 66,22, 52,9, 33,6, 15,16, 10,31, 18,44, 29,50, 38,51],
		0,
		0),
	
	'icon_54': createIcon(
		'icon_54',
		75, 80,
		new GPoint(49, 74),
		[48,69, 52,48, 63,37, 66,22, 52,9, 33,6, 15,16, 10,31, 18,44, 29,50, 38,51],
		0,
		0),
	
	'icon_55': createIcon(
		'icon_55',
		75, 80,
		new GPoint(49, 74),
		null,
		0,
		0),
	
	'icon_61': createIcon(
		'icon_61',
		65, 67,
		new GPoint(32, 50),
		[13,20, 51,20, 51,43, 13,43
],
		0,
		0),
	
	'icon_62': createIcon(
		'icon_62',
		65, 67,
		new GPoint(31, 54),
		[15,12, 50,12, 50,45, 15,45
],
		0,
		0),
	
	'icon_63': createIcon(
		'icon_63',
		65, 65,
		new GPoint(32, 51),
		[15,11, 50,11, 50,45, 15,45
],
		0,
		0),
	
	'fft_start_flag': createIcon(
		'fft_start_flag',
		65, 65,
		new GPoint(32, 55),
		[32,13, 23,8, 10,15, 10,26, 20,45, 46,45, 55,25, 55,14, 43,8
],
		0,
		0),
	
	'fot_start_flag': createIcon(
		'fot_start_flag',
		65, 65,
		new GPoint(32, 55),
		[32,13, 23,8, 10,15, 10,26, 20,45, 46,45, 55,25, 55,14, 43,8
],
		0,
		0),
	
	'icon_66': createIcon(
		'icon_66',
		65, 67,
		new GPoint(32, 60),
		[18,17, 32,12, 47,17, 47,55, 18,55],
		0,
		0),
	
	'green_flag': createIcon(
		'green_flag',
		40, 48,
		new GPoint(8, 42),
		[3,3,  36,3,  36,24,  10,24,  10,44,  3,44],
		0,
		0),
	
	'blue_flag': createIcon(
		'blue_flag',
		40, 48,
		new GPoint(8, 42),
		[3,3,  36,3,  36,24,  10,24,  10,44,  3,44],
		0,
		0),
	
	'red_flag': createIcon(
		'red_flag',
		40, 48,
		new GPoint(8, 42),
		[3,3,  36,3,  36,24,  10,24,  10,44,  3,44],
		0,
		0),
	
	'virgin_money': createIcon(
		'virgin_money',
		65, 65,
		new GPoint(32, 57),
		[11,6, 55,6, 55,50, 11,50],
		0,
		0),
	
	'route_add_handle': createIcon(
		'route_add_handle',
		20, 20,
		new GPoint(20, 20),
		[0,0, 0,18, 18,18, 18,0],
		0,
		1),
	
	'view_waypoint': createIcon(
		'view_waypoint',
		20, 20,
		new GPoint(9, 9),
		[0,0, 0,18, 18,18, 18,0],
		1,
		1),
	
	'split_route_handle': createIcon(
		'split_route_handle',
		11, 11,
		new GPoint(5, 5),
		null,
		1,
		1),
	
	'icon_75': createIcon(
		'icon_75',
		56, 78,
		new GPoint(29, 71),
		[4,4,88,4,88,123,4,123],
		0,
		0),
	
	'icon_76': createIcon(
		'icon_76',
		48, 71,
		new GPoint(21, 64),
		[5,2,97,2,97,131,5,131],
		0,
		0),
	
	'icon_77': createIcon(
		'icon_77',
		69, 51,
		new GPoint(35, 45),
		[26,43,15,35,8,28,4,16,11,6,58,3,62,17,59,32,52,38,38,43,28,40],
		0,
		0),
	
	'icon_78': createIcon(
		'icon_78',
		57, 50,
		new GPoint(29, 43),
		[6,4,53,4,53,45,6,45],
		0,
		0),
	
	'icon_79': createIcon(
		'icon_79',
		41, 81,
		new GPoint(24, 74),
		[7,5,38,5,38,78,7,78],
		0,
		0),
	
	'icon_80': createIcon(
		'icon_80',
		49, 62,
		new GPoint(24, 55),
		[5,5,98,5,98,130,5,130],
		0,
		0),
	
	'icon_81': createIcon(
		'icon_81',
		57, 75,
		new GPoint(28, 69),
		[23,66,5,43,8,4,50,6,49,42,35,70,21,65],
		0,
		0),
	
	'icon_82': createIcon(
		'icon_82',
		57, 62,
		new GPoint(28, 55),
		[4,5,62,5,62,74,4,74],
		0,
		0),
	
	'icon_83': createIcon(
		'icon_83',
		54, 68,
		new GPoint(30, 60),
		[23,60,6,21,5,-1,48,6,46,61,22,59],
		0,
		0),
	
	'icon_84': createIcon(
		'icon_84',
		53, 56,
		new GPoint(28, 50),
		[4,4,93,4,93,111,4,111],
		0,
		0),
	
	'icon_85': createIcon(
		'icon_85',
		81, 56,
		new GPoint(37, 49),
		[6,4,107,4,107,86,6,86],
		0,
		0),
	
	'icon_86': createIcon(
		'icon_86',
		40, 50,
		new GPoint(16, 43),
		[5,3,53,3,53,62,5,62],
		0,
		0),
	
	'icon_87': createIcon(
		'icon_87',
		49, 58,
		new GPoint(25, 52),
		[19,34,29,34,29,55,19,55],
		0,
		0),
	
	'icon_88': createIcon(
		'icon_88',
		58, 63,
		new GPoint(23, 55),
		[15,53,2,47,7,21,35,16,47,5,53,5,49,45,30,62,15,52],
		0,
		0),
	
	'icon_89': createIcon(
		'icon_89',
		39, 48,
		new GPoint(20, 41),
		[4,2,69,2,69,79,4,79],
		0,
		0),
	
	'icon_90': createIcon(
		'icon_90',
		49, 78,
		new GPoint(26, 71),
		[6,67,14,76,33,75,44,65,32,25,37,2,14,3,16,31,8,59],
		0,
		0),
	
	'icon_91': createIcon(
		'icon_91',
		60, 50,
		new GPoint(33, 43),
		[29,49,6,29,7,22,29,7,46,7,53,18,47,39,34,44],
		0,
		0),
	
	'icon_92': createIcon(
		'icon_92',
		53, 66,
		new GPoint(27, 60),
		[20,60,5,41,5,30,25,5,44,8,47,43,29,57,24,57],
		0,
		0),
	
	'icon_93': createIcon(
		'icon_93',
		55, 56,
		new GPoint(25, 50),
		[20,49,13,38,6,29,5,12,13,3,31,6,47,14,48,36,28,47,20,46],
		0,
		0),
	
	'icon_94': createIcon(
		'icon_94',
		67, 63,
		new GPoint(32, 55),
		[3,3,148,3,148,92,3,92],
		0,
		0),
	
	'icon_95': createIcon(
		'icon_95',
		44, 54,
		new GPoint(21, 47),
		[5,3,39,3,39,48,5,48],
		0,
		0),
	
	'icon_96': createIcon(
		'icon_96',
		64, 64,
		new GPoint(33, 54),
		[5,3,110,3,110,114,5,114],
		0,
		0),
	
	'icon_97': createIcon(
		'icon_97',
		54, 52,
		new GPoint(26, 45),
		[27,25,28],
		0,
		0),
	
	'icon_98': createIcon(
		'icon_98',
		78, 62,
		new GPoint(40, 56),
		[31,54,18,40,5,36,5,24,14,19,16,5,58,6,71,21,71,48,42,54,30,54],
		0,
		0),
	
	'icon_99': createIcon(
		'icon_99',
		76, 89,
		new GPoint(39, 82),
		[42,79,34,80,32,74],
		0,
		0),
	
	'icon_100': createIcon(
		'icon_100',
		61, 53,
		new GPoint(33, 46),
		[5,5,77,5,77,62,5,62],
		0,
		0),
	
	'icon_101': createIcon(
		'icon_101',
		54, 43,
		new GPoint(30, 36),
		[23,34,5,22,5,13,39,5,47,6,48,18,38,37,21,34],
		0,
		0),
	
	'icon_102': createIcon(
		'icon_102',
		83, 54,
		new GPoint(42, 47),
		[6,1,108,1,108,72,6,72],
		0,
		0),
	
	'icon_103': createIcon(
		'icon_103',
		49, 47,
		new GPoint(22, 40),
		[6,6,59,6,59,45,6,45],
		0,
		0),
	
	'icon_104': createIcon(
		'icon_104',
		44, 51,
		new GPoint(21, 45),
		[16,43,6,27,6,14,16,7,29,7,40,12,43,28,31,37,24,43,15,41],
		0,
		0),
	
	'icon_105': createIcon(
		'icon_105',
		48, 57,
		new GPoint(24, 50),
		[4,3,72,3,72,78,4,78],
		0,
		0),
	
	'icon_106': createIcon(
		'icon_106',
		58, 72,
		new GPoint(39, 65),
		[2,12,16,26,30,58,43,66,45,52,51,48,31,16,25,16,16,5,7,5,5,7],
		0,
		0),
	
	'icon_107': createIcon(
		'icon_107',
		43, 53,
		new GPoint(21, 46),
		[17,44,16,37,3,29,8,3,36,6,36,33,26,45,17,44],
		0,
		0),
	
	'icon_108': createIcon(
		'icon_108',
		58, 66,
		new GPoint(28, 59),
		[20,57],
		0,
		0),
	
	'icon_109': createIcon(
		'icon_109',
		59, 43,
		new GPoint(29, 36),
		[27,38,5,25,5,1,52,6,52,23,34,37],
		0,
		0),
	
	'icon_110': createIcon(
		'icon_110',
		40, 97,
		new GPoint(21, 90),
		[16,91,17,57,7,35,5,6,33,6,33,39,24,57,24,90,15,89],
		0,
		0),
	
	'icon_111': createIcon(
		'icon_111',
		61, 62,
		new GPoint(30, 51),
		[6,6,74,6,74,92,6,92],
		0,
		0),
	
	'icon_112': createIcon(
		'icon_112',
		31, 54,
		new GPoint(15, 47),
		[5,0,50,0,50,77,5,77],
		0,
		0),
	
	'icon_113': createIcon(
		'icon_113',
		110, 64,
		new GPoint(55, 56),
		[51,57,7,27,6,13,45,6,77,6,103,16,103,28,58,56,49,55],
		0,
		0),
	
	'icon_114': createIcon(
		'icon_114',
		44, 65,
		new GPoint(20, 58),
		[5,4,46,4,46,72,5,72],
		0,
		0),
	
	'icon_115': createIcon(
		'icon_115',
		45, 67,
		new GPoint(19, 60),
		null,
		0,
		0),
	
	'sport_relief': createIcon(
		'sport_relief',
		50, 56,
		new GPoint(26, 52),
		null,
		0,
		0),
	
	'sport\x2Drelief\x2Dregional\x2Dmile': createIcon(
		'sport\x2Drelief\x2Dregional\x2Dmile',
		50, 55,
		new GPoint(5, 5),
		null,
		0,
		0),
	
	'sport_relief_training': createIcon(
		'sport_relief_training',
		50, 55,
		new GPoint(26, 52),
		null,
		0,
		0)
	

};
G_DEFAULT_ICON = Icons['default'];


/**
 * All items on a map should extend from this. i.e. Route, Marker, PubCrawl, Flight, etc.
 */
MapItem = new Class;
/**
 * Static methods
 */
MapItem.create_from_json = function(json, world, hidden)
{
	// If json is a string, we need to evaluate it into an object
	if (typeof json == 'string') {
		try {
			var json = eval('(' + json + ')');
		} catch (err) {
			return false;
		}
	}



	try {
		var item = new window[json.properties.class_name](world);
	} catch (err) {
		
		return false;
	}
	item.unserialise(json, hidden);
	return item;
};



MapItem.prototype = {
__construct:
	function(world)
	{
		this.world = world;
		this.inert = this.world.inert;
		this.editing = false;

		this.anchor = false;
		this.properties = {
			class_name: 'MapItem',
			uid: 0,
			user: {id: 0, username: 'Anonymous'},
			date: (new Date()).getTime(),
			public: false
		};
		this.data = {};
		this.sticky = false;
	},
toString:
	function()
	{
		return '[object MapItem]';
	},
serialise:
	function()
	{
		// Remove legacy properties
		if (this.properties.points && this.data.points) {
			delete this.properties.points;
		}

		return serialise({
			anchor: { latitude: this.anchor.lat(), longitude: this.anchor.lng() },
			properties: this.properties,
			data: this.data
		});
	},
unserialise:
	function(json, hidden)
	{
		// If json is a string, we need to evaluate it into an object
		if (typeof json == 'string') {
			try {
				var json = eval('(' + json + ')');
			} catch (err) {
				return false;
			}
		}

		if (!json.properties) {
			
			return false;
		}

		if (json.anchor) {
			this.anchor = new GLatLng(json.anchor[0], json.anchor[1]);
		} else if (json.properties.point instanceof Array) {
			this.anchor = new GLatLng(json.properties.point[0], json.properties.point[1]);
		}

		if (!json.properties.user) {
			json.properties.user = {id: 0, username: 'Anonymous'};
		}

		this.properties = json.properties;
		this.data = json.data;

		if (!hidden) {
			this.redraw();
		}
		return true;
	},
save:
	function(extra_post_data)
	{
		GEvent.trigger(this, 'beforesave');

		// Add user details
		this.properties.user = {id: this.world.user.uid, username: this.world.user.username};
		
		if (this.group != "") {
			this["data"]["group"] = this.group;
			var data = this.serialise();
		} else {
			var data = this.serialise();
		}
		
		

		var url = 'world/save_item/';
		if (this.properties.uid > 0) {
			url += this.properties.uid;
			if (this.properties.uid == 301864) {
				// Peter Campbells test Marker
				console.log(data);
			}
		}
		
		var post_string = 'json='+escape(data);
		if (extra_post_data) {
			post_string += '&' + extra_post_data;
		}
		
		
		
		RPC.postData(url, post_string, GEvent.callback(this,
			function(rpc) {
				
				// Update UID
				this.properties.uid = parseInt(rpc.responseText);

				// Add item to draw queue so it doesn't get loaded in twice
				var item = this.world.draw_queue.add_item(this);
				item.sticky = true; // Sticky is required, or the above item will just get deleted from the queue when the map updates

				this.end_editing();
				
				alert('Your map item has been saved.');
				item.activate();
				GEvent.trigger(this, 'aftersave');
				GEvent.trigger(this.world, 'itemsave', item);
			})
		);
		GEvent.trigger(this, 'save');
	},
load_data:
	function(force)
	{
		if (force) {
			this.clear_data();
		}

		GEvent.trigger(this, 'loaddatastart');
		if (!this.data_loaded) {
			RPC.getJSON('world/map_items/' + this.properties.uid + '/', GEvent.callback(this, 
				function(data)
				{
					this.unserialise(data);
					if (!this.data.points && this.properties.points) {
						this.data.points = this.properties.points;
					}
					this.data_loaded = true;
					GEvent.trigger(this, 'loaddataend');
				}
			));
		} else {
			this.redraw();
			GEvent.trigger(this, 'loaddataend');
		}
	},
clear_data:
	function()
	{
		this.data_loaded = false;
		this.data = {};
		GEvent.trigger(this, 'cleardata');
	},
destroy_options_element:
	function()
	{
		var el = this.get_options_element();
		if (el && el.parentNode) {
			el.parentNode.removeChild(el);
		}
	},
duplicate:
	function()
	{
		if (!this.properties) {
			
			return false;
		}
		
		var new_item = this.world.create_item(this.properties.class_name);

		// Copy properties
		if (!new_item.properties) {
			new_item.properties = {};
		}
		for (x in this.properties) {
			new_item.properties[x] = this.properties[x];
		}

		// Copy data
		if (this.data) {
			if (!new_item.data) {
				new_item.data = {};
			}
			for (x in this.data) {
				new_item.data[x] = this.data[x];
			}
		} else {
			
		}
		if (this.anchor) {
			new_item.anchor = this.anchor;
		} else {
			
		}

		new_item.properties.uid = 0;
		new_item.properties.user = {id: 0, username: 'Anonymous'};
		//new_item.start_editing();
		//new_item.redraw();
		return new_item;
	},
/**
 * Abstract methods
 */
inert:		// Prevents the item from receiving mouse events
	function()
	{
		GEvent.trigger(this, 'inert');
		this.is_inert = true;
	},
uninert:	// Opposite of inert()
	function()
	{
		GEvent.trigger(this, 'uninert');
		delete this.is_inert;
	},
start_editing:	// Start editing this item
	function()
	{
		if (this.editing) {
			return;
		}
		GEvent.trigger(this, 'startediting');
		this.editing = true;
		if (this.properties.uid) {
			this.fit_into_view();
			this.world.jump_to_point(this.anchor);
		}
		if (PopoutPanel._current_instance) {
			PopoutPanel._current_instance.close();
		}
	},
end_editing:		// Done editing this item
	function()
	{
		if (!this.editing) {
			return;
		}
		this.editing = false;
		GEvent.trigger(this, 'endediting');
	},
fit_into_view:	// Alter the position and zoom of the map to fit the item in
	function()
	{
		this.world.map.setZoom(16);
	},
delete_item:
	function()
	{
		if (this.properties.uid) {
			MapItem.delete_item(this.properties.uid);
		}
		this.destroy();
	},
rateItem:
	function(score)
	{
		// Check if logged int
		if (!this.world.user.is_authenticated) {
			this.world.show_login('You need to login before your rating can be counted.', GEvent.callback(this, function() { this.rateItem(score); }));
			return false;
		}
		var url = 'world/rate_item/' + this.properties.uid + '/';
		RPC.postData(url, 'score=' + parseInt(score, 10), GEvent.callback(this, function(rpc)
		{
			alert('You rated this item ' + score + ' out of 5.\n\nThe new score is ' + rpc.responseText);
			GEvent.trigger(this, 'rate', parseInt(score, 10), parseFloat(rpc.responseText)); 
		}));
	},
cancel_editing: function() {},	// Undoes any edits, or if it's a new item simply destroys it
get_options_title: function() {},	// *REQUIRED* Returns the title to go on the editing panel
get_options_element: function() {},	// *REQUIRED* Should return an HTML element that contains the options to appear in the editing panel
destroy: function() {},			// Called to remove item from the map
activate: function() {},		// Called when item is clicked, or some other event. Should show the route, or info balloon, etc.
deactivate: function() {},		// Should put item back into default state
hide: function() {},			// Hides the item, but doesn't destroy it
show: function() {},			// Shows the item after being hidden using hide()
redraw: function() {},			// Called when properties or data have changed
getPopoutContent: function() {}		// Content to show in the first tab of popout panel when item is activated
};




Marker = new Class(MapItem);
Marker.prototype = {
__construct:
	function(world)
	{
		MapItem.prototype.__construct.apply(this, arguments);

		// Default data structures
		this.properties = {
			class_name: 'Marker',
			uid: 0,
			date: (new Date()).getTime(),
			title: '',
			category: 0,
			public: true,
			imported: false,
			tags: []
		};

		this.data = {
			sponsor: false,
			html: false,
			description: '',
			media: false
		};
	},
redraw:
	function()
	{
		// If we're editing, then pass stuff off to the editing redrawer
		if (this.editing) {
			return this.editing_redraw();
		}

		if (!this.anchor) {
			
			return;
		}

		if (!this.marker) {
			var marker_options = {
				title: this.properties.title,
				draggable: false,
				autoPan: false,
				clickable: !this.inert,
				inert: this.inert
			};
			try {
				marker_options.icon = Icons[MARKER_CATEGORIES[this.properties.category].icon];
			} catch (err) {
				//
				marker_options.icon = Icons['default'];
			}
			this.marker = createMarker(this.anchor, marker_options);
			this.marker.has_video = this.properties.has_video;
			this.marker.has_photo = this.properties.has_photo;
			GEvent.bind(this.marker, 'click', this, this.toggle_activation);
			this.world.map.addOverlay(this.marker);
			this.marker.updateIcon();
		}
	},
editing_redraw:
	function()
	{
	},
start_editing:
	function()
	{
		MapItem.prototype.start_editing.apply(this, arguments);
	
		// Watch for marker drags
		if (this.marker) {
			// Delete old cruddy marker
			this.world.map.removeOverlay(this.marker);
			GEvent.clearListeners(this.marker);
			delete this.marker;

			// This will create the marker again with the correct options
			this.set_point(this.anchor);
		}

		// Watch for map clicks
		var click_event = GEvent.bind(this.world.map, 'click', this, function (item, point) { this.set_point(point); });

		// Unbind the above event when we stop editing
		var unbind_event = GEvent.bind(this, 'endediting', this,
			function()
			{
				GEvent.removeListener(click_event);
				GEvent.removeListener(unbind_event);
				if (this._edit_marker_point) {
					GEvent.removeListener(this._edit_marker_point);
					delete this._edit_marker_point;
				}
			}
		);
	},
end_editing:
	function()
	{
		MapItem.prototype.end_editing.call(this);
		if (this.marker) {
			this.world.map.removeOverlay(this.marker);
			delete this.marker;
		}
	},
set_point:
	function(point)
	{
		this.anchor = point;

		if (!this.marker) {
			var marker_options = {
				icon: Icons['default'],
				title: 'Drag to reposition this marker',
				draggable: true,
				autoPan: true,
				clickable: false
			};
			try {
				marker_options.icon = Icons[MARKER_CATEGORIES[this.properties.category].icon];
			} catch (err) {
				marker_options.icon = Icons['default'];
			}
			this.marker = createMarker(this.anchor, marker_options);
			this.world.map.addOverlay(this.marker);
			this._edit_marker_point = GEvent.bind(this.marker, 'dragend', this, function() { this.set_point(this.marker.getPoint()); });
		} else {
			this.marker.setPoint(point);
		}
	},
show:
	function()
	{
		if (this.marker) {
			this.marker.show();
		}
		this.redraw();
	},
hide:
	function()
	{
		if (this.marker) {
			this.marker.hide();
		}
	},
destroy:
	function()
	{
		this.end_editing();
		if (this.marker) {
			this.world.map.removeOverlay(this.marker);
		}
		if (this._infoPanel) {
			this._infoPanel.close();
			delete this._infoPanel;
		}
	},
toggle_activation:
	function()
	{
		this.active ? this.deactivate() : this.activate();
		return this.active;
	},
activate:
	function()
	{
		this.active = true;
		this.sticky = true;
		//this._show_gallery = GEvent.bind(this, 'loaddataend', this, this.show_gallery);
		this.open_panel();
	},
deactivate:
	function()
	{
		this.active = false;
		this.sticky = false;
		this.close_panel();
	},
close_panel:
	function()
	{
		if (this._panel) {
			this._panel.close();
			delete this._panel;
		}
	},
open_panel:
	function()
	{
		if (!this.anchor) {
			// Can't show it without an anchor
			
			return false;
		}
		if (this._panel) {
			this._panel.close();
		}
		this._panel = new MapItemPopout(this);

		// Deactivate marker when panel is closed
		var closePanelEvent = GEvent.bind(this._panel, 'close', this,
			function()
			{
				this.deactivate();
				GEvent.removeListener(closePanelEvent);
				delete closePanelEvent;
			}
		);
		return this._panel;
	},
get_options_title:
	function()
	{
		return 'Editing a marker';
	},
get_options_element:
	function(panel)
	{
		var tabpanel = new TabContainer(this);

		// Pre-save code goes in here
		var save_and_close = function()
		{
			var data = {};

			// Popuplate the data object with values from the form
			var inputs = tabpanel.element.getElementsByTagName('input');
			for (var i=0; i<inputs.length; i++) {
				if (!inputs[i].name) {
					continue;
				}
				switch (inputs[i].type) {
				case 'radio':
					if (inputs[i].checked) {
						data[inputs[i].name] = inputs[i];
					}
					break;
				case 'checkbox':
					data[inputs[i].name] = inputs[i];
					break;
				default:
					data[inputs[i].name] = inputs[i];
					break;
				}
			}
			var inputs = tabpanel.element.getElementsByTagName('textarea');
			for (var i=0; i<inputs.length; i++) {
				data[inputs[i].name] = inputs[i];
			}


			// Validate input
			if (!this.anchor) {
				alert('Please click on the map to choose the location of your marker.');
				return;
			}
			if (!data['title'].value) {
				alert('Please enter a title for your marker.');
				tabpanel.setTab(0);
				data['title'].focus();
				return;
			}
			if (!data['description'].value) {
				alert('Please enter a description for your marker.');
				tabpanel.setTab(0);
				data['description'].focus();
				return;
			}
			if (!data['category'].value) {
				alert('Please select a category for your marker.');
				tabpanel.setTab(1);
				return;
			}


			// save
			this.properties.title = data['title'].value;
			this.properties.category = data['category'].value;
			if (this._uploaded_ids) {
				// gallery is an array for waypoints, marker is only 1 waypoint so it's just a 1 item array
				this.data.gallery = [this._uploaded_ids];
			}
			this.data.description = data['description'].value;
			this.properties.public = public.checked;
			
			// Need a check to see if gallery exists.
			// this.data.media = [{"user_id": this.properties.user["id"],"title": this.properties.title}];

			// Add image tags to post data
			var extra_post = ''
			if (this._uploaded_ids) {
				for (var i=0; i<this._uploaded_ids.length; i++) {
					var id = this._uploaded_ids[i];
					if (!id) {
						continue;
					}
					var tags = document.getElementsByName('tag_upload_' + id)[0].value;
					if (tags == 'Tag this photo/video') {
						tags = '';
					}
					if (extra_post != '') {
						extra_post += '&';
					}

					extra_post += 'tag_upload_' + escape(id) + '=' + escape(tags);
				}
			}

			
			this.save(extra_post);

			// clean up
			delete data;
			delete n;
			delete tabpanel;
			delete save_and_close;
		}

		// == STEP 1 ==
		tabpanel.addTab('description');
		var id = new Date().valueOf();
		var public;
		var private;
		var n = T.form({'className': 'panel_main_content'}, [
			T.label({'for': 'marker_title'+id}, 'Title of marker'),
			T.input({'id': 'marker_title'+id, 'type': 'text', 'name': 'title', 'value':this.properties.title}),

			T.label({'for': 'marker_description'+id}, 'Write your description here'),
			T.textarea({'id': 'marker_description'+id, 'rows': 5, 'name': 'description'}, this.data.description),

			T.div({'className': 'radio_field'}, [
				public = T.input({'id': 'marker_public'+id, 'type': 'radio', 'name': 'public', 'value': 'yes'}),
				T.label({'for': 'marker_public'+id}, 'Public'),

				private = T.input({'id': 'marker_private'+id, 'type': 'radio', 'name': 'public', 'value': 'no'}),
				T.label({'for': 'marker_private'+id}, 'Private')
			]),
			T.buttonRow({'className': 'button_row'}, [
				//new Button('save', GEvent.callback(this, save_and_close)),
				new Button('next', function() { tabpanel.setTab(1); })
			])
		]);
		setTimeout(GEvent.callback(this, function() { public.checked = this.properties.public; private.checked = !this.properties.public; }), 1);

		n.onsubmit = GEvent.stop;
		tabpanel.setContent(0, n);


		// == STEP 2 ==
		tabpanel.addTab('category');
		tabpanel.markerCategory = new MarkerCategory('category');
		var id = new Date().valueOf();
		var n = T.form({'className': 'panel_main_content'}, [
			T.label('Select a category'),
			tabpanel.markerCategory,

			T.buttonRow({'className': 'button_row'}, [
				//new Button('save', GEvent.callback(this, save_and_close)),
				new Button('back', function() { tabpanel.setTab(0); }),
				new Button('next', function() { tabpanel.setTab(2); })
			])
		]);
		tabpanel.markerCategory.setValue(this.properties.category);
		n.onsubmit = GEvent.stop;
		tabpanel.setContent(1, n);

		// == STEP 3 ==
		tabpanel.addTab('media');
		var id = new Date().valueOf();
		
		var upload_frame = T.iframe({'name':'marker_upload_frame'+id});
		with (upload_frame.style) {
			position = 'absolute';
			left = '-1000000px';
		}
		var upload_field = T.input({'class':'upload_field', 'id': 'marker_upload'+id, 'type': 'file', 'name': 'media_file'});
		this.upload_list = T.ul({'className': 'uploaded_file_list', 'id': 'marker_upload_list'+id});

		// TODO Update this to the new MediaUploader
		var uploader = new Uploader('http://www.realbuzz.com/photos-and-videos/applet-upload-myp/'+this.world.user.uid+'/', "100%", 26);

		// Add the iframe to the body tag
		document.body.appendChild(upload_frame);

		var n = T.div ([
			T.form({
					'method': 'post',
					'enctype': 'multipart/form-data',
					'action': 'http://www.realbuzz.com/photos-and-videos/applet-upload-myp/',
					'className': 'panel_main_content',
					'name':'marker_upload_form'+id,
					'target':'marker_upload_frame'+id
				}, [
				T.label({'for': 'marker_upload'+id}, 'Upload a photo or video'),
				//upload_field,
				uploader.element,
				T.label({'for': 'marker_upload_list'+id}, 'Successfully uploaded files'),
				this.upload_list,

				T.buttonRow({'className': 'button_row'}, [
					new Button('back', function() { tabpanel.setTab(1); }),
					new Button('save', GEvent.callback(this, save_and_close))
				])
			])
		]);

		// When a new image is selected, upload it
		GEvent.bindDom(upload_field, 'change', n,
			function()
			{
				this.getElementsByTagName('form')[0].submit();
				panel.busy(true);
			}
		);

		// Once uploaded do this stuff.
		//GEvent.bindDom(upload_frame, 'load', this, function() {
		GEvent.bindDom(uploader, 'uploadcomplete', this, function(data) {
			panel.busy(false);
			if (!data) {
				var json = upload_frame.contentWindow.document.body.innerHTML;
			}

			// This skips the first onload event which is called upon creating the iframe and loading about:blank
			if (json && upload_frame.contentWindow.location.href.indexOf('upload_media') == -1) {
				return;
			}
			try {
				if (json) {
					var data = eval('('+json+')');
				}
			} catch (e) {
				
				alert('There was an error processing your upload. Our development team has been notified of the problem.');
			}

			switch (data['action']) {
			case 'upload':	// We just uploaded some media
				switch (data.media['type']) {
				case 'image':
					this.properties.has_photo = true;
					break;
				case 'video':
					this.properties.has_video = true;
					break;
				}
				this.add_uploaded(data['media'].id, data['media'].filename);
				break;
			default:
				
				alert('Your uploaded photo or video is in an unsupported format.');
				break;
			}
			//panel.busy(false);
		});
		//GEvent.bindDom(upload_frame, 'stop', this, function() { alert('Upload canceled.'); panel.busy(false); });
		tabpanel.setContent(2, n);


		// Add uploaded media
		if (this.data.media && this.data.media[0]) {
			var media = this.data.media[0];
			for (var i=0; i<media.length; i++) {
				
				var row = this.add_uploaded(media[i].id, media[i].original_filename);
				// Populate tags text <input>
				if (media[i].tags) {
					row.tag_input.value = media[i].tags;
					row.tag_input.className = '';
				}
			}
		}

		return tabpanel.element;

	},
cancel_editing:
	function()
	{
		if (confirm('Are you sure you wish to cancel editing this marker? You will lose all unsaved changes.')) {
			this.end_editing();
			if (this.properties.uid == 0) {
				this.destroy();
			}
			return true;
		}
		return false;
	},
add_uploaded:
	function(id, filename)
	{
		if (!this.upload_list) {
			
			return false;
		}
		if (!this._uploaded_list) {
			this._uploaded_list = [];
		}
		if (!this._uploaded_ids) {
			this._uploaded_ids = [];
		}

		var delete_button = T.a({'href':'javascript:void(0)'}, [
			T.img({'className':'uploaded_delete', 'src':SKIN_URL+'images/delete_upload.gif'})
		]);
		GEvent.bindDom(delete_button, 'click', this, function() { this.delete_uploaded(id); });

		var tag_default_text = 'Tag this photo/video';
		var tag_input = T.input({'class': 'example', 'name': 'tag_upload_' + id, 'value': tag_default_text});
		tag_input.default_text = tag_default_text;

		var new_row = T.li([
			T.span({'className':'uploaded_filename'}, filename),
			delete_button,
			T.span({'class': 'uploaded_tags'}, [tag_input])
		]);
		new_row.tag_input = tag_input;
		this.upload_list.appendChild(new_row);

		GEvent.bindDom(tag_input, 'focus', tag_input,
			function(e)
			{
				if (this.value == this.default_text) {
					this.value = '';
					this.className = '';
				}
			}
		);
		GEvent.bindDom(tag_input, 'blur', tag_input,
			function(e)
			{
				if (this.value == '') {
					this.value = this.default_text;
					this.className = 'example';
				}
			}
		);

		this._uploaded_list[id] = new_row;
		this._uploaded_ids.push(id);
		return new_row
	},
delete_uploaded:
	function(id)
	{
		

		if (!this._uploaded_list || !this._uploaded_list[id]) {
			return false;
		}

		if (!confirm('Are you sure you wish to delete this file. This cannot be undone.')) {
			return;
		}

		// POST to delete item
		RPC.postData('action/delete_media/', 'id='+parseInt(id), GEvent.callback(this, function(rpc) {
			if (rpc.responseText == '1') {
				var l = this._uploaded_list[id];
				l.parentNode.removeChild(l);
				delete this._uploaded_list[id];
				this._uploaded_ids.splice(this._uploaded_ids.find(id), 1);
			} else {
				
				alert('There was an error deleting this media file.');
			}
		}));

		// TODO set has_photo/video
	},
// Appears in the popout panel when clicked
getPopoutContent:
	function()
	{
		var marker_url = SITE_URL + '?item='+this.properties.uid;
		var url_input;
		var description_node = T.div();
		if (this.data.html) {
			description_node.innerHTML = this.data.description;
		} else if (typeof this.data.description == 'string') {
			description_node.appendChild(document.createParagraph(this.data.description));
		} else {
			description_node.appendChild(this.data.description);
		}
		var description = T.div([
			description_node,
			T.div({'class':'info_box'}, [
				T.h2('Link to this marker'),
				T.ul({'class':'export_list'}, [
					T.li([
						url_input = T.input({'readonly':'readonly','value': marker_url})
					])
				])
			])
		]);
		var reset_url = function()
		{
			if (this.value != marker_url) {
				this.value = marker_url;
			}
			this.focus();
			this.select();
		};
		GEvent.bindDom(url_input, 'click', url_input, reset_url);
		//GEvent.bindDom(url_input, 'focus', url_input, reset_url);
		GEvent.bindDom(url_input, 'keydown', url_input, reset_url);
		GEvent.bindDom(url_input, 'keypress', url_input, reset_url);
		GEvent.bindDom(url_input, 'keyup', url_input, reset_url);
		GEvent.bindDom(url_input, 'change', url_input, reset_url);

		if (!this.properties.has_photo && !this.properties.has_video) {
			// this is just for IE, otherwise i'd have set it in the LI itself
			url_input.style.width = '290px';
			var element = description;
		} else {
			// this is just for IE, otherwise i'd have set it in the LI itself
			if (this.data.media) {
				url_input.style.width = '200px';
				var gallery = new Gallery(this.properties.user.username);
				gallery.set_description(description);
				try {
					gallery.add_media_from_array(this.data.media);
				} catch(err) {
					// Do nothing
				}
				var element = gallery.element;
			} else {
				url_input.style.width = '290px';
				var element = description;
			}
		}
		return element;
	}
};



SmallTooltip = new Class(GOverlay);
SmallTooltip.prototype = {
	__construct: function(point, message, offset)
	{
		if (point instanceof Array) {
			var point = new GLatLng(point[0], point[1]);
		}
		this.point = point;
		this.offset = offset || new GSize(15, 0);
		this.message = message || '';
	},
	toString: function()
	{
		return '[object SmallTooltip]';
	},
	initialize: function(gmap)
	{
		this.gMap = gmap;
		this.element = document.createElement('div');

		with (this.element.style)
		{
			position = 'absolute';
			height = '27px';
			fontSize = '11px';
			lineHeight = '27px';
			whiteSpace = 'nowrap';
			opacity = '0.8';
			//filter = 'alpha(opacity=80)';
		}

		this.leftEnd = document.createElement('div');
		this.rightEnd = document.createElement('div');
		this.tile = document.createElement('div');

		this.leftEnd.style.position = 'absolute';
		this.leftEnd.style.width = '16px';
		this.leftEnd.style.height = '27px';
		this.leftEnd.style.left = 0;

		this.rightEnd.style.position = 'absolute';
		this.rightEnd.style.width = '16px';
		this.rightEnd.style.height = '27px';
		this.rightEnd.style.right = 0;

		this.tile.style.margin = '0 16px';
		this.tile.style.height = '27px';

		this.leftEnd.style.background = 'url('+SKIN_URL+'images/smalltooltip_arrow_left.png)';
		this.rightEnd.style.background = 'url('+SKIN_URL+'images/smalltooltip_end_right.png)';
		this.tile.style.background = 'url('+SKIN_URL+'images/smalltooltip_tile.png)';

		this.element.appendChild(this.leftEnd);
		this.element.appendChild(this.rightEnd);
		this.element.appendChild(this.tile);

		this.setMessage(this.message);
		
		this.gMap.getPane(G_MAP_FLOAT_PANE).appendChild(this.element);
	},
	remove: function()
	{
		this.element.parentNode.removeChild(this.element);
	},
	copy: function()
	{
		return new SmallTooltip(this.point, this.message);
	},
	redraw: function(force)
	{
		if (!force) return;

		if (!this.gMap) {
			
			return false;
		}
		var p = this.gMap.fromLatLngToDivPixel(this.point);
		this.element.style.left = (p.x + this.offset.width) +'px';
		this.element.style.top = (p.y + this.offset.height - 13) +'px';
	},
	setLatLng: function(point)
	{
		this.point = point;
		this.redraw(true);
	},
	getLatLng: function()
	{
		return this.point;
	},
	setMessage: function(message)
	{
		this.message = message;
		message = message.replace(/ /g, '\u00a0');

		if (this.tile) {
			while (this.tile.firstChild) {
				this.tile.removeChild(this.tile.firstChild);
			}
			this.tile.appendChild(document.createTextNode(message));
		}
	},
	getMessage: function()
	{
		return this.message;
	},
	show: function()
	{
		this.element.style.display = 'block';
	},
	hide: function()
	{
		this.element.style.display = 'none';
	}
};

EventQueue = new Class;
EventQueue.prototype = {
__construct:
	function(obj, evnt)
	{
		this.object    = obj;
		this.event     = evnt;
		this.callbacks = [];
		this.length    = 0;
		GEvent.bind(this.object, this.event, this, this.eventTriggered);
	},
eventTriggered:
	function()
	{
		if (this.length < 1) {
			return;
		}
		var callback = this.callbacks.shift();
		
		callback.apply(this, arguments);
		this.length = this.callbacks.length;
		if (!this.length) {
			GEvent.trigger(this, 'complete');
		}
	},
addCallback:
	function(callback)
	{
		
		this.callbacks.push(callback);
		this.length = this.callbacks.length;
	}
};

function checkURL() {
	var url = window.location;
	url = url.toString();
	if (url.search('flmroutes') != -1) {
		return true;
	} else {
		return false;
	};
};

function get_tip() {
	var tips = 
	new Array('Talent and training aside, the right diet is one of the most important factors in improving you running performance.',
			  'Carbohydrates are the body’s primary source of fuel during exercise.',
			  'Fatigue occurs when glycogen (carbohydrates stored in the body) become depleted.',
			  'Starting a run with insufficient glycogen (carbohydrates stored in the body), is like taking a car out with a half empty fuel tank.',
			  'Taking on carbohydrates before and during prolonged performances has been shown to reduce the negative impact of glycogen (carbohydrates stored in the human body) depletion on performance.',
			  'A 2% reduction in body weight through sweating can significantly decrease performance.',
			  'Marathon runners have been reported to lose up to 8% of their body weight in warm conditions.',
			  'Fluid lost through sweat contains key electrolytes such as sodium and potassium which can be replaced by consuming a well formulated sports drink or other source of electrolytes.',
			  'Human urine is a great way of assessing hydration; when you are properly hydrated, your pee should be the colour of pale straw',
			  'If you want to maintain your bodyweight and energy levels you should increase your calories consumed relative to your training load by approx. 100Kcal per mile run.',
			  'Try and start each day with a high-carbohydrate breakfast and continue to top up your stores with high carbohydrate snacks such as energy bars, sports drinks and dried fruit.',
			  'Consume a high carbohydrate meal 3-4 hours before training and competing.',
			  'Stay well hydrated throughout the day by drinking a minimum of two pints of water little and often.',
			  'Consume a high carbohydrate meal or snack within 30mins of completing exercise. Aim for an intake of typically 1.0 – 1.2g of carbohydrate per kg of bodyweight per hour immediately after exercise.',
			  'After exercise try to consume approx. 1.5 litres of fluid per kg of bodyweight lost through sweating.',
			  'Include protein in your immediate post-race nutrition strategy to aid the generation process.',
			  'A daily carbohydrate intake of 7-10g/kg body weight is required to optimise carbohydrate stores for those competing in heavy training.',
			  'Ensure that you start the event suitably hydrated by drinking 500mls of an isotonic sports drink in the hour before you run and then drink whilst actually running.',
			  'Sweat rates could be 1-2 litres an hour or more, even in an air conditioned gym.',
			  'Exercise drinks like Lucozade Sport contain electrolytes which increase the rate at which water is absorbed from the small intestine.',
			  'An increase in work rate leads to an increase in core temperature. Sweat evaporating from the skin is a cooling mechanism to counteract this increase in core temperature.',
			  'Sweat loss varies for the individual and is dependent on the conditions you are exercising in.',
			  'Key symptoms of dehydration to watch out for are nausea; dizziness; headache; poor co-ordination; cramps; poor concentration; early fatigue and increased perception of effort.',
			  'Thirst is not a good indicator– you will already be dehydrated before feeling thirsty.',
			  'Always begin any form of exercise hydrated; drink 5-7ml of fluid per kilogram of body weight.',
			  'Caffeine can improve performance in both short and long term endurance events as well as short term, high intensity intermittent exercise.',
			  'Caffeine can improve many of the cognitive (mental processing) attributes important to sport such as alertness, concentration, reaction time and focus.',
			  'Caffeine can improve performance in exercise ranging between 3-120 min. The level of performance benefit will vary between individuals.',
			  'The benefits of caffeine are more apparent under situations of fatigue/physical stress.',
			  'Caffeine is mainly excreted from the body in the urine with the time to clear half of the ingested caffeine between 3-5 hours.',
			  'Carbohydrate is the fuel of choice for the brain, exercising muscle and central nervous system during exercise.',
			  'Sports nutrition guidelines are focused on strategies to enhance carbohydrate availability in the periods before, during and after exercise.',
			  'Correct participation is fundamental to an enjoyable race day experience and can make the difference between setting a new PB and finishing disappointed.',
			  'Pack your kit bag by ticking items off your check list. Make sure you pack an extra change of clothes, towel, drinks bottle, light snack and safety pins for your race number.',
			  'Take a drink and a snack for after the race – you’ll deserve it!',
			  'Don’t try anything new on race day – you may not like it or it may not like you!',
			  'During recovery, sufficient carbohydrate should be consumed to immediately replace what has just been used.',
			  'Carbohydrate can be consumed in either a series of snacks or large meals, although it is advised that a regular intake of smaller snacks may be helpful in overcoming the gastric discomfort often associated with eating large amounts of bulky high carbohydrate foods. For immediate recovery, consume moderate to high glycaemic index (GI) foods.',
			  'When the recovery time between exercise is short (0-4 h), carbohydrate consumption should begin immediately as this results in higher rates of muscle glycogen storage compared with delayed feeding.',
			  'A well thought out, and well stuck to training schedule is essential to not only surviving, but enjoying race day.',
			  'Arrive at the start line with enough time to relax and to be able to fully focus on the challenge ahead.',
			  'Allow plenty of time before the race to allow for traffic delays, time to drop your bag in the baggage area, put your race chip in your shoe and visit the toilet.',
			  '1/2 hour before the race begins top up your energy supplies with a small snack like a Lucozade Sport Energy Bar or banana or up to 500ml of Lucozade Sport.',
			  'Plan your route to the night before and be aware that roads may be blocked off.',
			  'Before race day, make sure you eat a couple of hours before you go to sleep.',
			  'During race week, gradually increase your carbohydrate intake to ensure you are storing the energy your muscles will need during the race. Foods such as potatoes, rice, pasta, bread, bananas, and jelly sweets are all high in carbohydrate and low in fat.',
			  'If training for over 60mins or to a high intensity you should take on fluid and carbohydrates.',
			  'If your event/sport is < 90 min in duration, ensure that you consume a high carbohydrate diet in the hours before and during exercise.',
			  'Performance in continuous or intermittent exercise (duration; ≥ 90 min) is generally improved by a high carbohydrate diet in the 1-7 days prior.',
			  'The provision of carbohydrate (glucose, sucrose, maltodextrins or alternative high glycaemic index carbohydrates) during exercise is normal practice during events/sports that are ≥ 60 min in duration and continuous (running/cycling) or high intensity and intermittent (team/racket sports).',
			  'During moderate to high intensity endurance exercise ≥ 60 min in duration, consume 30-60 g of carbohydrate per hour to maintain exercise intensity and delay fatigue.',
			  'If exercising for ≥ 60, consume 30-60 g of carbohydrate per hour (g/h) in small feedings every 10-30 min, or as allowed by the event/sport. Isotonic sports drinks (600-1200 ml) and/or carbohydrate gels, bars or confectionary provide suitable options.',
			  'Stay headstrong. Develop a strong mind and will. When it hurts – you can do it!',
			  'Marathon running is as much about the journey as it is about the actual race.'
			 );
	var index = Math.floor(Math.random()*tips.length);
	tip = tips[index];
	return tip
}



ROUTE_WIDTH = 3;
ROUTE_OPACITY = 0.7;
//ROUTE_SNAPPED_OPTIONS = {getPolyline: true, travelMode: G_TRAVEL_MODE_WALKING};
ROUTE_SNAPPED_OPTIONS = {getPolyline: true, travelMode: G_TRAVEL_MODE_DRIVING, avoidHighways: true};

RouteLine = new Class(GOverlay);
RouteLine.prototype = {
__construct:
	function(route)
	{
		this.route = route;

		//this.polyline = new GPolyline(points, this.colour, this.width, this.opacity, {clickable: false});

		var glatlngs = [];
		var points   = [];
		for (var i=0; i<this.route.data.points.length; i++) {
			// convert point arrays to GLatLngs
			var p = this.route.data.points[i];

			// TODO if we need a new segment, createpolyline and empty 'glatlngs'
			if (this.route.data.pointContent && this.route.data.pointContent[i]) {
				var content = this.route.data.pointContent[i];
			} else {
				var content = {type: 'normal'};
			}
			switch (content.type) {
			case 'snaptoroad':
				if (content.computedPoints) {
					points = points.concat(content.computedPoints);
					for (var j=0; j<content.computedPoints.length; j++) {
						var ll = content.computedPoints[j];
						glatlngs.push(new GLatLng(ll[0], ll[1]));
					}
					break;
				} // else carry on and do the default

			case 'normal':
			default:
				points.push(p);
				glatlngs.push(new GLatLng(p[0], p[1]));
				break;
			}

				/*
				// Snap to road lines are 'encoded' when saved. We just restore this to save asking google over and over.
				var l = new GPolyline(computedPoints, this.route.colour, ROUTE_WIDTH, ROUTE_OPACITY);
				lines.push(l);
				*/


			/*
			if (this.route.data.pointContent && this.route.data.pointContent[i]) {
				// Draw marker for waypoint content
				var marker_options = {
					icon: Icons['view_waypoint'],
					draggable: false,
					autoPan: false,
					clickable: true
				};
				var marker = createMarker(np, marker_options);
				this._point_markers.push(marker);
				this.world.map.addOverlay(marker);
				GEvent.addListener(marker, 'click', GEvent.callbackArgs(this, this.showWaypointContent, i));
			}
			*/
		}
		this.polyline = new GPolyline(glatlngs, this.route.colour, ROUTE_WIDTH, ROUTE_OPACITY);
		this.glatlngs = glatlngs;
		this.points = points;

	},
initialize:
	function(gmap)
	{
		this.gMap = gmap;
		//this.gMap.addOverlay(this.polyline);
		gmap.addOverlay(this.polyline);
	},
redraw:
	function(force)
	{
		if (!this.polyline) {
			return;
		}
		this.polyline.redraw(force);
	},
remove:
	function()
	{
		this.gMap.removeOverlay(this.polyline);
	},
getLength:
	function()
	{
		return this.polyline.getLength();
	},
toString:
	function()
	{
		return '[object RouteLine]';
	}
};

AddContentIcon = new Class(GOverlay);
AddContentIcon.prototype = {
__construct:
	function(point)
	{
		this.src = [
			STATIC_URL + 'images/addcontent.png',
			STATIC_URL + 'images/addcontent_hover.png'
		];
		this.element = T.img({'src': this.src[0]});
		this.size = 20;
		this.point = point;
		this.waypoint = 0;

		// This property is a big magic, when applied to GOverlays it prevents clicks causing new route points being added
		this.ignoreMap = true;

		GEvent.bindDom(this.element, 'mouseout',  this, function() { this.element.src = this.src[0]; });
		GEvent.bindDom(this.element, 'mouseover', this, function() { this.element.src = this.src[1]; });

		GEvent.bindDom(this.element, 'mouseout',  this, this.delayedHide);
		GEvent.bindDom(this.element, 'mouseover', this, this.cancelHide);

		// 'GEvent.stop' cancels any bubbling and thus prevents map clicks from triggering
		GEvent.bindDom(this.element, 'mousedown', this, function(e) { GEvent.trigger(this, 'mousedown'); GEvent.stop(e, true); });
		// FIXME this don't fire because we turned off bubbling. DUH!
		GEvent.bindDom(this.element, 'mouseup',   this, function(e) { GEvent.trigger(this, 'mouseup');   });
		GEvent.bindDom(this.element, 'click',     this, function(e) { GEvent.trigger(this, 'click'); });
	},
initialize:
	function(gmap)
	{
		this.gMap = gmap;
		with (this.element.style) {
			position = 'absolute';
			cursor = 'pointer';
			width  = this.size + 'px';
			height = this.size + 'px';
		}

		this.gMap.getPane(G_MAP_FLOAT_PANE).appendChild(this.element);
	},
setWaypoint:
	function(id)
	{
		this.waypoint = id;
	},
redraw:
	function(force)
	{
		if (!force) {
			return;
		}
		var p = this.gMap.fromLatLngToDivPixel(this.point);
		var x = (p.x - this.size -4);
		var y = (p.y - this.size -4);

		this.element.style.left = x + 'px';
		this.element.style.top  = y + 'px';
	},
setPoint:
	function(point)
	{
		this.point = point;
		this.redraw(true);
	},
hide:
	function()
	{
		this.cancelHide();
		this.element.style.display = 'none';
	},
show:
	function()
	{
		this.cancelHide();
		this.element.style.display = '';
	},
delayedHide:
	function()
	{
		this.cancelHide();
		this._hideTimer = setTimeout(GEvent.callback(this, this.hide), 500);
	},
cancelHide:
	function()
	{
		if (this._hideTimer) {
			clearTimeout(this._hideTimer);
			delete this._hideTimer;
		}
	},
remove:
	function()
	{
		this.element.parentNode.removeChild(this.element);
	}
};

RouteHandle = new Class(GOverlay);
RouteHandle.prototype = {
__construct:
	function(route, index, icon, add_icon)
	{
		this.route = route;
		if (!this.point) {
			var point = route.data.points[index];
			this.point = new GLatLng(point[0], point[1]);
			this.index = index;
		}

		if (!this.size) {
			this.size = 6;
		}

		this.element = T.div({'class':'route_handle'});
		this.dragElement = new GDraggableObject(this.element, {draggableCursor: 'pointer', draggingCursor: 'move'});

		this._events = [
			GEvent.bindDom(this.element,  'mouseover', this, function(e) { GEvent.trigger(this, 'mouseover', e); }),
			GEvent.bindDom(this.element,  'mouseout',  this, function(e) { GEvent.trigger(this, 'mouseout', e);  }),
			GEvent.bind(this.dragElement, 'dragstart', this, function(e) { GEvent.trigger(this, 'dragstart', e)  }),
			GEvent.bind(this.dragElement, 'drag',      this, function(e) { GEvent.trigger(this, 'drag', e);      }),
			GEvent.bind(this.dragElement, 'dragend',   this, function(e) { GEvent.trigger(this, 'dragend', e);   }),
			GEvent.bind(this, 'drag',      this, this.onDrag),
			GEvent.bind(this, 'dragend',   this, this.onDragEnd)
		];

		this.route.world.map.addOverlay(this);
		


		// Create the plus icon for adding waypoint content
		// NOTE this is static as we only want 1 of them to be shared between all handles
		if (!RouteHandle.addIcon) {
			RouteHandle.addIcon = new AddContentIcon(this.point);
			RouteHandle.addIcon.hide();
			this.route.world.map.addOverlay(RouteHandle.addIcon);
		}
		if (add_icon !== false) {
			this._events = this._events.concat([
				GEvent.addListener(this, 'mouseover', this.showPlus),
				GEvent.addListener(this, 'mouseout',  this.hidePlus)
			]);
		}

	},
createAddIcon:
	function()
	{
	},
initialize:
	function(gmap)
	{
		this.gMap = gmap;
		with (this.element.style) {
			position = 'absolute';
			width  = this.size + 'px';
			height = this.size + 'px';
			border = '3px solid #33c';
			background = '#fff';
			fontSize = '1px';
			/*
			opacity = '0.7';
			filter = 'alpha(opacity=70)';
			*/
			MozBorderRadius = '6px';
			WebkitBorderRadius = '6px';
			borderRadius = '6px';
		}

		this.gMap.getPane(G_MAP_MARKER_SHADOW_PANE).appendChild(this.element);
	},
setColour:
	function(colour) {
		var colour = colour || '#000000';
		this.element.style.borderColor = colour;
	},
onDrag:
	function()
	{
		this.refreshLatLng();
		//this.route.setPoint(this.index, this.getLatLng());
		this.disablePlus();
	},
onDragEnd:
	function()
	{
		this.route.setPoint(this.index, this.getLatLng());
		this.enablePlus();
	},
redraw:
	function(force)
	{
		if (!force) {
			return;
		}
		var p = this.gMap.fromLatLngToDivPixel(this.point);
		var x = (p.x - (this.element.offsetWidth /2));
		var y = (p.y  - (this.element.offsetHeight /2));

		this.dragElement.moveTo(new GPoint(x, y));
	},
disablePlus:
	function()
	{
		this._plusDisabled = true;
		RouteHandle.addIcon.hide();
	},
enablePlus:
	function()
	{
		this._plusDisabled = false;
		delete this._plusDisabled;
	},
showPlus:
	function()
	{
		if (this._plusDisabled) {
			return;
		}
		RouteHandle.addIcon.setPoint(this.point);
		// We don't want old listeners 
		GEvent.clearListeners(RouteHandle.addIcon);

		// Show the popout thingy when clicked
		GEvent.bind(RouteHandle.addIcon, 'click', this, function() { GEvent.trigger(this, 'plusclick'); });
		RouteHandle.addIcon.show();
	},
hidePlus:
	function()
	{
		RouteHandle.addIcon.delayedHide();

		// We don't want old listeners 
		GEvent.clearListeners(RouteHandle);
	},
destroy:
	function()
	{
		for (var i=0; i<this._events.length; i++) {
			var e = this._events[i];
			GEvent.removeListener(e);
		}
		this.route.world.map.removeOverlay(this);
	},
remove:
	function()
	{
		this.element.parentNode.removeChild(this.element);
	},
refreshLatLng:
	function()
	{
		this.point = this.gMap.fromDivPixelToLatLng(new GPoint(this.dragElement.left + (this.element.offsetWidth /2), this.dragElement.top + (this.element.offsetHeight /2)));
	},
getLatLng:
	function()
	{
		var ll = this.gMap.fromDivPixelToLatLng(new GPoint(this.dragElement.left + (this.element.offsetWidth /2), this.dragElement.top + (this.element.offsetHeight /2)));
		return ll;
	},
move_to:
	function(point)
	{
		if (point == this.marker._point) {
			return;
		}
		this.marker.setPoint(new GLatLng(point[0], point[1]));
		this.marker._point = point;
		if (this.addMarker) {
			this.addMarker.setPoint(new GLatLng(point[0], point[1]));
			this.addMarker._point = point;
		}
	},
get_point:
	function()
	{
		var p = this.marker.getLatLng(); 
		return [p.lat(), p.lng()];
	},
setIcon:
	function(icon)
	{
		var icon = icon || 'route_handle';
		var marker_options = {
			icon: Icons[icon],
			title: 'Drag to adjust the route',
			draggable: true,
			autoPan: true,
			clickable: false
		};
		var newMarker = createMarker(this.marker.getLatLng(), marker_options);
		this.route.world.map.removeOverlay(this.marker);
		GEvent.clearInstanceListeners(this.marker);

		this.marker = newMarker
		this.route.world.map.addOverlay(this.marker);

		GEvent.bind(this.marker, 'dragstart', this, function() { GEvent.trigger(this, 'dragstart'); });
		GEvent.bind(this.marker, 'drag', this, function() { GEvent.trigger(this, 'drag'); });
		GEvent.bind(this.marker, 'dragend', this, function() { GEvent.trigger(this, 'dragend'); });
		GEvent.bind(this.marker, 'mouseover', this, this.showPlus);
		GEvent.bind(this.marker, 'mouseout', this, this.hidePlus);
		// Fire when mouse over
		GEvent.bind(this.marker, 'mouseover', this, function() { GEvent.trigger(this, 'mouseover'); });
		GEvent.bind(this.marker, 'mouseout', this, function() { GEvent.trigger(this, 'mouseout'); });
	},
setPoint:
	function(point) {
		this.point = point;
		this.redraw(true);
		GEvent.trigger(this, 'setpoint', this.point);
	}
};

GhostHandle = new Class(RouteHandle);
GhostHandle.prototype = {
__construct:
	function(route, index)
	{
		var p1 = route.data.points[index-1];
		var p2 = route.data.points[index];
		var point = [(p1[0] + p2[0]) /2, (p1[1] + p2[1]) /2];
		this.point = new GLatLng(point[0], point[1]);
		this.index = index;
		this.size = 8;

		RouteHandle.prototype.__construct.call(this, route, false, false, false);

		this.element.title = 'Drag create new waypoint';

		// Updates the location of the ghost handle when the route is adjusted
		this._events.push(GEvent.bind(route, 'setpoint', this,
			function(id, point)
			{
				// If the updated point isn't around this, then we don't care.
				if (id != this.index && id != this.index -1) {
					return;
				}

				this.updateLocation();
			}
		));
	},
setColour:
	function(colour) {
		var colour = colour || '#000000';
		this.element.style.backgroundColor = colour;
	},
initialize:
	function(gmap)
	{
		RouteHandle.prototype.initialize.call(this, gmap);
		with (this.element.style) {
			border = 'none';
			MozBorderRadius = '3px';
			WebkitBorderRadius = '3px';
			borderRadius = '3px';
		}

		this.gMap.getPane(G_MAP_MARKER_SHADOW_PANE).appendChild(this.element);
		/*
		this.element.style.opacity = 0.6;
		this.element.style.filter = 'alpha(opacity=60)';
		*/
	},
onDrag:
	function(point)
	{
		if (!this._newPoint) {
			var newPointID = this.route.insertPoint(this.index, this.getLatLng());
			this._newPoint = this.route.getHandle(newPointID);
			this._newPoint.disablePlus();
		} else {
			this._newPoint.setPoint(this.getLatLng());
			this.route.setPoint(this.index -1, this.getLatLng());
		}

		if (this.route._distanceTooltip) {
			this.route._distanceTooltip.sticky = true;
			this.route.updateDistanceTooltip(this._newPoint.index);
		}
	},
onDragEnd:
	function()
	{
		this._newPoint.enablePlus();
		delete this._newPoint;
		this.updateLocation();
		if (this.route._distanceTooltip) {
			this.route._distanceTooltip.sticky = false;
		}
	},
updateLocation:
	function()
	{
		// Don't update location while in the process of creating a new point
		if (this._newPoint) {
			return;
		}
		var p1 = this.route.data.points[this.index-1];
		var p2 = this.route.data.points[this.index];
		var point = [(p1[0] + p2[0]) /2, (p1[1] + p2[1]) /2];

		this.setPoint(new GLatLng(point[0], point[1]));
	}
};

Route = new Class(MapItem);
Route.prototype = {
__construct:
	function(world)
	{
		MapItem.prototype.__construct.apply(this, arguments);

		// Load these from user's saved options
		this.options = {
			autocentre: O.getOption('route_autocentre'),
			snaptoroad: O.getOption('route_snaptoroad')
		};

		// Default data structures
		this.properties = {
			class_name: 'Route',
			uid: 0,
			date: (new Date()).getTime(),
			title: '',
			category: 1,
			public: true,
			imported: false,
			user: {id: 0, username: 'Anonymous'}
		};

		this.data = {
			points: [],			// The points that make up the route
			pointContent: [],	// Data stored with each points, usually a photos, description, and also the type such as 'relay change' or 'snap to road'
			media: false		// The photos and videos assigned to various points along this route
		};

		this.increment_unit = this.world.increment_unit;

		this.directions = new GDirections();

	},
getLength:
	function()
	{
		if (this.editing) {
			var distance = 0;
			if (this._editLines) {
				for (var i=0; i<this._editLines.length; i++) {
					if (!this._editLines[i]) {
						continue;
					}
					distance += this._editLines[i].getLength();
				}
			}
			if (this._snappedLines) {
				for (var i=0; i<this._snappedLines.length; i++) {
					if (!this._snappedLines[i]) {
						continue;
					}
					distance += this._snappedLines[i].getLength();
				}
			}
			return distance;
		} else if (this._route_polyline) {
			return this._route_polyline.getLength();
		}

		return 0;
	},
updateFields:
	function()
	{
		var metres = this.getLength();
		var miles  = Math.roundDP(metres / METRES2MILES, 2);
		var kms    = Math.roundDP(metres / METRES2KMS, 2);

		if (this._pointsField) {
			while (this._pointsField.firstChild) {
				this._pointsField.removeChild(this._pointsField.firstChild);
			}
			this._pointsField.appendChild(document.createTextNode(this.data.points.length));
		}
		if (this._distanceField) {
			while (this._distanceField.firstChild) {
				this._distanceField.removeChild(this._distanceField.firstChild);
			}
			this._distanceField.appendChild(document.createTextNode(miles + ' Miles'))
			this._distanceField.appendChild(T.br())
			this._distanceField.appendChild(document.createTextNode(kms + ' KMs'))
		}
	},
setEditLineColour:
	function(colour)
	{
		if (!this._editing_line) {
			return;
		}

		this._editing_line.setStrokeStyle({color: colour});
		for (var i=0; i<this._handles.length; i++) {
			var h = this._handles[i];
			var g = this._ghostHandles[i];

			h.setColour(colour);
			if (g) {
				g.setColour(colour);
			}
		}
	},
createHandleForPoint:
	function(id)
	{
		var p = this.data.points[id];
		var p2 = this.data.points[id-1];

		if (!this.colour) {
			this.colour = ROUTE_CATEGORIES.getNextColour(this.properties.category);
		}

		// Create handle for editing
		var h = new RouteHandle(this, id);
		h.setColour(this.colour);
		this._handles.push(h);

		// Update distance tooltip
		GEvent.bind(h, 'drag', this, function()
		{
			if (this._distanceTooltip) {
				this._distanceTooltip.sticky = true;
			}
			this.updateDistanceTooltip(h.index);
		});
		GEvent.bind(h, 'dragend', this, function()
		{
			if (this._distanceTooltip) {
				this._distanceTooltip.sticky = false;
			}
		});
		// Hide/show distance tooltip
		GEvent.bind(h, 'mouseover', this, function() { this.showPointDistance(h.index); });
		GEvent.bind(h, 'mouseout',  this, function() { this.hidePointDistance(h.index); });

		// Add items to way points when they click the [+] icon next to the handle
		GEvent.bind(h, 'plusclick', this, function() { this.addContentToWaypoint(h.index); });


		if (!this.data.pointContent) {
			this.data.pointContent = [];
		}
		var showGhostHandle = ((!this.data.pointContent[id] || this.data.pointContent[id].type != 'snaptoroad') && p && p2);
		// Create ghost handles
		if (showGhostHandle) {
			//var mid = [(p[0] + p2[0]) /2, (p[1] + p2[1]) /2];
			var g = new GhostHandle(this, id);
			this._ghostHandles.push(g);
			g.setColour(this.colour);
		}
	},
removeHandleForPoint:
	function(id)
	{
		for (var i=0; i<this._handles.length; i++) {
			var h = this._handles[i];
			if (h.index == id) {
				// Remove from map
				h.destroy();
				// Remove from array
				this._handles.splice(i, 1);
				break;
			}
		}
		for (var i=0; i<this._ghostHandles.length; i++) {
			var h = this._ghostHandles[i];
			if (h.index == id) {
				// Remove from map
				h.destroy();
				// Remove from array
				this._ghostHandles.splice(i, 1);
				break;
			}
		}
		this.updateEditFlags();
	},
redrawEditingRoute:
	function()
	{
		if (!this._handles) {
			this._handles = [];
			this._ghostHandles = [];
		}

		// Add missing handles
		for (var i=this._handles.length; i<this.data.points.length; i++) {
			this.createHandleForPoint(i);
		}

		this.updateFields();
	},
redraw:
	function(force)
	{
		// If we're editing, then pass stuff off to the editing redrawer
		if (this.editing) {
			return this.redrawEditingRoute();
		}

		if (!this._start_flag) {
			var flag_options = {
				title: 'Route: ' + this.properties.title,
				draggable: false,
				autoPan: false,
				clickable: !this.inert,
				inert: this.inert
			};
			// Pick the correct flag icon
			try {
				// If category has a specific flag use that
				if (ROUTE_CATEGORIES[this.properties.category].start_flag) {
					flag_options.icon = Icons[ROUTE_CATEGORIES[this.properties.category].start_flag];
				} else if (this.properties.user && this.properties.user.id
				&& this.properties.user.id == this.world.user.uid) {
				// Otherwise user's routes have a green flag
					flag_options.icon = Icons['green_flag'];
				} else {
				// everyone else has a blue one
					flag_options.icon = Icons['blue_flag'];
				}
			} catch (err) {
				
				// Broken routes are red, these should never appear.
				flag_options.icon = Icons['red_flag'];
			}

			if (!flag_options.icon) {
				
				flag_options.icon = G_DEFAULT_ICON;
			}
			
			// Add start flag
			this._start_flag = createMarker(this.anchor, flag_options);
			this.world.map.addOverlay(this._start_flag);
			GEvent.bind(this._start_flag, 'click', this, this.toggle_activation);


		}
		// Add end flag
		if (!this._end_flag && this.data && this.data.points && this.data.points.length > 1) {
			var endPoint = this.data.points[this.data.points.length-1];
			endPoint = new GLatLng(endPoint[0], endPoint[1]);
			this._end_flag = createMarker(endPoint, {'icon': Icons['end_flag']});
			this.world.map.addOverlay(this._end_flag);
			GEvent.bind(this._end_flag, 'click', this, this.toggle_activation);
		} else if (this._end_flag) {
			this._end_flag.show();
		}

		// If we're showing the route then redraw that too
		if (force && this.active) {
			this.show_route(true);
		}
	},
editing_redraw:
	function()
	{
		if (!this.data.points || this.data.points.length == 0) {
			//return;
		}

		if (!this._handles) {
			this._handles = [];
		}
		if (!this._sub_handles) {
			this._sub_handles = [];
		}
		
		if (this._handles.length > this.data.points.length) {
			// Delete any left over handles plus 1 extra to cause the end flag to be recreated
			while (this._handles.length >= this.data.points.length && this._handles.length > 0) {
				try {
					this._handles.pop().destroy();
					if (this._sub_handles.length > 0) {
						this._sub_handles.pop().destroy();
					}
				} catch(err) { delete err; }
			}
		}

		for (var i=0; i<this.data.points.length; i++) {
			var p = this.data.points[i];
			var p2 = this.data.points[i-1];
			if (!this._handles[i]) {
				var icon = false;

				// First point is the start flag
				if (i == 0) {
					icon = 'green_flag';
				} else if (i == this._handles.length) {
					if (this._handles.length >= 2) {
						// Set previous point to a normal waypoint
						this._handles[this._handles.length-1].setIcon(false);
					}
					icon = 'end_flag';
				}
				this._handles[i] = new RouteHandle(this, p, icon);
				// Show distance when hovering
				GEvent.addListener(this._handles[i], 'mouseover', GEvent.callbackArgs(this, this.showPointDistance, i));
				GEvent.addListener(this._handles[i], 'mouseout', GEvent.callbackArgs(this, this.hidePointDistance, i));

				// Update route's point when the handle is dragged
				GEvent.addListener(this._handles[i], 'drag', GEvent.callbackArgs(this,
					function(i)
					{
						// First point is the anchor
						if (i == 0) {
							this.anchor = this._handles[i].get_lat_lng();
						}

						var p1 = this.data.points[i-1];
						var p2 = this._handles[i].get_point();
						var p3 = this.data.points[i+1];
						this.setPoint(i, p2);

						if (p1) {
							// Add handle to middle of segment to allow splitting it
							var mid = [(p1[0] + p2[0]) /2, (p1[1] + p2[1]) /2];
							this._sub_handles[i].move_to(mid);
						}
						if (p3) {
							// Add handle to middle of segment to allow splitting it
							var mid = [(p2[0] + p3[0]) /2, (p2[1] + p3[1]) /2];
							this._sub_handles[i+1].move_to(mid);
						}

					}, i)
				);

				if (p2) {
					// Add handle to middle of segment to allow splitting it
					var mid = [(p[0] + p2[0]) /2, (p[1] + p2[1]) /2];
					this._sub_handles[i] = new RouteHandle(this, mid, 'split_route_handle', false);
					GEvent.addListener(this._sub_handles[i], 'dragstart', GEvent.callbackArgs(this,
						function(i)
						{
							
							var h = this._sub_handles[i];
							var p = h.get_point();
							this.data.points.splice(i, 0, p);
							this.data.pointContent.splice(i, 0, false);
							this._handles[i].disablePlus();

							// Bodge! Force drag event on extra handles to cause them to update properly
							for (var j=i; j<this._handles.length; j++) {
								GEvent.trigger(this._handles[j], 'drag');
							}
							GEvent.trigger(this, 'split', i);
						}, i)
					);
					GEvent.addListener(this._sub_handles[i], 'dragend', GEvent.callbackArgs(this,
						function(i)
						{
							this._handles[i].enablePlus();
						}, i)
					);
					
					GEvent.addListener(this._sub_handles[i], 'drag', GEvent.callbackArgs(this,
						function(i)
						{
							var h = this._sub_handles[i];
							var p = h.get_point();
							this.setPoint(i, p);
							GEvent.trigger(this._handles[i], 'drag');
							if (this._handles[i].addMarker) {
								this._handles[i].addMarker.hide();
							}
						}, i)
					);
					
				}

				// Update distance tooltip
				GEvent.addListener(this._handles[i], 'drag', GEvent.callbackArgs(this, this.updateDistanceTooltip, i));


				// Add items to way points when they click the [+] icon next to the handle
				GEvent.addListener(this._handles[i], 'plusclick', GEvent.callbackArgs(this, this.addContentToWaypoint, i));
			} else {
				this._handles[i].move_to(p);
			}
		}

		/*
		if (SEGMENTED_POLYLINE) {
			this.show_segmented_route();
		} else {
			this.show_route(true);
		}
		*/

		this.update_fields();
	},
toggle_activation:
	function()
	{
		this.active ? this.deactivate() : this.activate();
		return this.active;
	},
activate:
	function()
	{
		this.active = true;
		this.sticky = true;
		this.open_panel();

		/*
		this.show_info();


		//this._show_route_event = GEvent.bind(this, 'loaddataend', this, this.show_route);
		this.load_data();
		this.show();
		*/
	},
deactivate:
	function()
	{
		this.active = false;
		this.sticky = false;
		this.close_panel();
		this.hide_route();
		if (this._end_flag) {
			this._end_flag.hide();
		}

		/*
		if (this._show_route_event) {
			GEvent.removeListener(this._show_route_event);
		}
		this.hide_route();
		this.hide_info();
		*/
	},
open_panel:
	function()
	{
		if (this._panel) {
			this._panel.close();
		}
		this._panel = new MapItemPopout(this);
		return this._panel;


		if (this._panel) {
			this._panel.close();
		}
		this._panel = new TabbedPopoutPanel(this.world, this.anchor, 500, 320, true);
		//GEvent.bind(this._panel, 'close', this, this.deactivate);

		this._panel.set_title(this.properties.title);

		this._panel.busy(true);




		function populate_panel()
		{
			this._panel.busy(false);
			if (this._panel._populated) {
				return;
			}
			this._panel._populated = true;

			// Draw the route
			this.show_route();


			var miles = 0;
			var kms = 0;
			if (this._route_polyline) {
				if (this._route_polyline instanceof Array) {
					for (var i=0; i<this._route_polyline.length; i++) {
						miles += this._route_polyline[i].getLength() / METRES2MILES;
						kms   += this._route_polyline[i].getLength() / METRES2KMS;
					}
				} else {
					miles = this._route_polyline.getLength() / METRES2MILES;
					kms   = this._route_polyline.getLength() / METRES2KMS;
				}
			} else {
				
				miles = 0;
				kms = 0;
			}
			miles = Math.roundDP(miles, 2);
			kms = Math.roundDP(kms, 2);



			var url_input;
			var copy_route_link;
			var route_url = SITE_URL + '?item='+this.properties.uid;
			var n = T.div({'class':'mini_balloon_panel_content'}, [
				T.div({'class':'info_box'}, [
					// Stats, Distance, etc.
					T.h2('Statistics'),
					T.table([T.tbody([
						T.tr([
							T.th('Way points : '),
							this._pointsField = T.td(this.data.points.length)
						]),
						T.tr([
							T.th('Distance : '),
							this._distanceField = T.td( miles + ' Miles -- ' + kms + ' KMs')
						])
					])]),

					// About the author
					T.h2('Author'),
					T.table([T.tbody([
						T.tr([
							T.td({'rowspan':2}, [
								T.img({'width':80,'height':60,'src':'http://www.realbuzz.com/uploads/'+this.properties.user.id+'/system/profile80x60.jpg'})
							]),
							T.td([
								T.a({'href':'http://www.realbuzz.com/en-gb/users/' + escape(this.properties.user.username),'target':'_blank'}, [T.span(this.properties.user.username)])
							])
						]),
						T.tr([
							T.td([
						//		T.span('Stats?')
							])
						])
					])]),

					// Link to this route
					T.h2('Link to this route'),
					T.ul({'class':'export_list'}, [
						T.li([
							url_input = T.input({'readonly':'readonly','value': route_url})
						])
					]), 

					// Copy this route
					T.h2('Copy this route'),
					T.ul({'class':'export_list','style':'display:none'}, [
						T.li([
							copy_route_link = T.a({'href': 'javascript:void(0)'}, 'Copy and edit route')
						])
					]), 

					// Export route as GPX, KML, etc.
					T.h2('Export'),
					T.ul({'class':'export_list'}, [
						T.li([T.a({'href': BASE_URL + 'gpx_routes/' + this.properties.uid}, 'Export this route for GPS devices (.gpx)')])
						//T.li([T.a({'href':''}, 'Export this route for Google Earth (.kml)')])
					])
					
				])
			]);
			// this is just for IE, otherwise i'd have set it in the LI itself
			url_input.style.width = '270px';

			var reset_url = function()
			{
				if (this.value != route_url) {
					this.value = route_url;
				}
				this.focus();
				this.select();
			};
			GEvent.bindDom(url_input, 'click', url_input, reset_url);
			//GEvent.bindDom(url_input, 'focus', url_input, reset_url);
			GEvent.bindDom(url_input, 'keydown', url_input, reset_url);
			GEvent.bindDom(url_input, 'keypress', url_input, reset_url);
			GEvent.bindDom(url_input, 'keyup', url_input, reset_url);
			GEvent.bindDom(url_input, 'change', url_input, reset_url);


			GEvent.bindDom(copy_route_link, 'click', this, this.duplicate);

			// If it has a description, place that under the stats
			if (this.data.description) {
				n.firstChild.insertBefore(T.div([
					T.h2('Description'),
					document.createParagraph(this.data.description)
				]), n.firstChild.childNodes[2]);
			}

			//this._panel.set_content(n);

			this._panel.add_tab('Details', n);

			// Tab 2 -- Comments
			var comments = new CommentsView(this.properties.uid);
			GEvent.bind(this._panel, 'changetab', comments, function(id)
				{
					if (id == 1) {
						this.refresh();
					}
				}
			);
			if (isNaN(this.data.comment_count)) {
				this.data.comment_count = 0;
			}
			var comment_tab = this._panel.add_tab('Comments [' + parseInt(this.data.comment_count, 10) + ']', comments.element);
			// Update counter when new comments are added
			GEvent.bind(comments, 'addcomment', this, function()
				{
					var count = comments.comment_count();
					this._panel.set_tab_label(comment_tab, 'Comments [' + count + ']');
					this.data.comment_count = count;
				}
			);
			GEvent.bind(comments, 'startrefresh', this, function() { this._panel.tab_busy(comment_tab, true); });
			GEvent.bind(comments, 'endrefresh', this, function() { this._panel.tab_busy(comment_tab, false); });

			// Tab 3 -- Profile
			this._panel.add_tab('Author\'s profile', new ProfileView(this.properties.user.id).element);
		}	// populate_panel

		GEvent.bind(this, 'loaddataend', this, populate_panel);

		// Loading data must be last
		this.load_data();
	},
close_panel:
	function()
	{
		if (this._panel) {
			this._panel.close();
			delete this._panel;
		}
	},
get_lat_lng:
	function(id)
	{
		return new GLatLng(this.data.points[id][0], this.data.points[id][1]);
	},
hide:
	function()
	{
		if (this._start_flag) {
			this._start_flag.hide();
		}
		if (this._end_flag) {
			this._end_flag.hide();
		}
		this.hide_route();
	},
show:
	function()
	{
		// TODO Don't re-show things if they're already visible
		if (this._start_flag) {
			this._start_flag.show();
		}
		if (this._end_flag) {
			this._end_flag.show();
		}
		if (this.active) {
			this.show_route();
		}
		this.redraw();
	},
show_segmented_route:
	function()
	{
		// Remove non-segmented route
		if (!(this._route_polyline instanceof Array)) {
			this.hide_route();
		}

		// No colour? Need to delete the whole line and redraw, this is kinda slow.
		if (!this.colour) {
			this.colour = ROUTE_CATEGORIES.getNextColour(this.properties.category);
		}

		// Redraw from scratch if colour has changed.
		if (this._route_polyline && this._route_polyline._colour && this._route_polyline._colour != this.colour) {
			this.hide_route();
		}

		if (!this._route_polyline) {
			this._route_polyline = [];
		}

		this._route_polyline._colour = this.colour;

		for (var i=0; i<this.data.points.length -1; i++) {
			var p1 = this.data.points[i];
			var p2 = this.data.points[i+1];

			// Remove changed segments
			if (this._route_polyline[i]) {
				if (this._route_polyline[i].p1 == p1 && this._route_polyline[i].p2 == p2) {
					continue;
				}
				// Remove segment
				this.world.map.removeOverlay(this._route_polyline[i]);
				delete this._route_polyline[i];
			}
			var points = [new GLatLng(p1[0], p1[1]), new GLatLng(p2[0], p2[1])];
			this._route_polyline[i] = new GPolyline(points, this.colour, ROUTE_WIDTH, ROUTE_OPACITY, {clickable: false});
			this._route_polyline[i].p1 = p1;
			this._route_polyline[i].p2 = p2;
			this.world.map.addOverlay(this._route_polyline[i]);
		}
		// Delete any left over segments
		for (var i=this.data.points.length-1; i<this._route_polyline.length; i++) {
			if (this._route_polyline[i]) {
				this.world.map.removeOverlay(this._route_polyline[i]);
				delete this._route_polyline[i];
			}
		}
	},
show_route:
	function(force)
	{
		

		if (force) {
			this.hide_route();
		}

		if (this._route_polyline || !this.data || !this.data.points || this.data.points.length == 0) {
			return;
		}

		// Convert points to GLatLngs and draw waypoint icons
		var points = [];
		if (!this._point_markers) {
			this._point_markers = [];
		}
		for (var i=0; i<this.data.points.length; i++) {
			var p = this.data.points[i];
			var np = new GLatLng(p[0], p[1]);
			points.push(np);

			if (!this.editing) {
				if (this.data.pointContent && this.data.pointContent[i] && this.data.pointContent[i].title) {
					// Draw marker for waypoint content
					var marker_options = {
						icon: Icons['view_waypoint'],
						draggable: false,
						autoPan: false,
						clickable: true
					};
					var marker = createMarker(np, marker_options);
					this._point_markers.push(marker);
					this.world.map.addOverlay(marker);
					GEvent.addListener(marker, 'click', GEvent.callbackArgs(this, this.showWaypointContent, i));
				}
			}
		}

		// Get a colour
		if (!this.colour) {
			this.colour = ROUTE_CATEGORIES.getNextColour(this.properties.category);
		}

		this._route_polyline = new RouteLine(this);

		// Create line
		//this._route_polyline = new GPolyline(points, this.colour, ROUTE_WIDTH, ROUTE_OPACITY, {clickable: false});

		// Add line to map
		this.world.map.addOverlay(this._route_polyline);


		// Watch for increment changes
		if (!this._increment_change_event) {
			this._increment_change_event = GEvent.bind(this.world, 'incrementchange', this, this.refresh_increments);
		}

		// Draw increments
		if (!this.editing && this.increment_unit && this.increment_unit.value) {
			this.show_increments(this.increment_unit.value, this.increment_unit.label);
		} else {
			this.remove_increments();
		}
	},
refresh_increments:
	function(inc)
	{
		this.increment_unit = this.world.increment_unit;
		this.redraw(true);
	},
set_increment_unit:
	function(value, label)
	{
		if (!value && !this.increment_unit) {
			return;
		}
		if (!value) {
			delete this.increment_unit;
		}

		this.increment_unit = {value: value, label: label};
		this.redraw(true);
	},
show_info:
	function(point)
	{
		var point = point || this.anchor;
		// Show info bubble
		if (this.infoPanel) {
			this.infoPanel.close(true);
		}
		var infoPanel = new MiniBalloonPanel(0, 0, 500, 190, true);
		infoPanel.stickToMap(this.world.map);
		infoPanel.attachAnchor(point);
		infoPanel.setTitle(this.properties.title);
		infoPanel.setToolbar(T.div('test'));
		this.infoPanel = infoPanel;
		this.infoPanel.show();


		this.infoPanel.busy(true);

		// Über method to render the info panel as soon as we get the data
		var populate_panel = function()
		{
			// Draw the route
			this.show_route();
			if (!this.infoPanel) {
				return;
			}
			var miles = 0;
			var kms = 0;
			if (this._route_polyline) {
				if (this._route_polyline instanceof Array) {
					for (var i=0; i<this._route_polyline.length; i++) {
						miles += this._route_polyline[i].getLength() / METRES2MILES;
						kms +=  this._route_polyline[i].getLength() / METRES2KMS;
					}
				} else {
					miles = this._route_polyline.getLength() / METRES2MILES;
					kms =  this._route_polyline.getLength() / METRES2KMS;
				}
			} else {
				
				miles = 0;
				kms = 0;
			}
			miles = Math.roundDP(miles, 2);
			kms = Math.roundDP(kms, 2);



			var url_input;
			var copy_route_link;
			var route_url = SITE_URL + '?item='+this.properties.uid;
			var n = T.div({'class':'mini_balloon_panel_content'}, [
				T.div({'class':'info_box'}, [
					// Stats, Distance, etc.
					T.h2('Statistics'),
					T.table([T.tbody([
						T.tr([
							T.th('Way points : '),
							this._pointsField = T.td(this.data.points.length)
						]),
						T.tr([
							T.th('Distance : '),
							this._distanceField = T.td( miles + ' Miles -- ' + kms + ' KMs')
						])
					])]),

					// About the author
					T.h2('Author'),
					T.table([T.tbody([
						T.tr([
							T.td({'rowspan':2}, [
								T.img({'width':80,'height':60,'src':'http://www.realbuzz.com/uploads/'+this.properties.user.id+'/system/profile80x60.jpg'})
							]),
							T.td([
								T.a({'href':'http://www.realbuzz.com/en-gb/users/' + escape(this.properties.user.username),'target':'_blank'}, [T.span(this.properties.user.username)])
							])
						]),
						T.tr([
							T.td([
						//		T.span('Stats?')
							])
						])
					])]),

					// Link to this route
					T.h2('Link to this route'),
					T.ul({'class':'export_list'}, [
						T.li([
							url_input = T.input({'readonly':'readonly','value': route_url})
						])
					]), 

					// Copy this route
					T.h2('Copy this route'),
					T.ul({'class':'export_list','style':'display:none'}, [
						T.li([
							copy_route_link = T.a({'href': 'javascript:void(0)'}, 'Copy and edit route')
						])
					]), 

					// Export route as GPX, KML, etc.
					T.h2('Export'),
					T.ul({'class':'export_list'}, [
						T.li([T.a({'href': BASE_URL + 'gpx_routes/' + this.properties.uid}, 'Export this route for GPS devices (.gpx)')])
						//T.li([T.a({'href':''}, 'Export this route for Google Earth (.kml)')])
					])
					
				])
			]);
			// this is just for IE, otherwise i'd have set it in the LI itself
			url_input.style.width = '270px';

			var reset_url = function()
			{
				if (this.value != route_url) {
					this.value = route_url;
				}
				this.focus();
				this.select();
			};
			GEvent.bindDom(url_input, 'click', url_input, reset_url);
			//GEvent.bindDom(url_input, 'focus', url_input, reset_url);
			GEvent.bindDom(url_input, 'keydown', url_input, reset_url);
			GEvent.bindDom(url_input, 'keypress', url_input, reset_url);
			GEvent.bindDom(url_input, 'keyup', url_input, reset_url);
			GEvent.bindDom(url_input, 'change', url_input, reset_url);


			GEvent.bindDom(copy_route_link, 'click', this, this.duplicate);

			// If it has a description, place that under the stats
			if (this.data.description) {
				n.firstChild.insertBefore(T.div([
					T.h2('Description'),
					document.createParagraph(this.data.description)
				]), n.firstChild.childNodes[2]);
			}

			this.infoPanel.setContent(n);
			this.infoPanel.resizeToContent(true);
			this.infoPanel.busy(false);
		}	// populate_panel


		GEvent.bind(this, 'loaddataend', this, populate_panel);
	},
hide_info:
	function() {
		if (this.infoPanel) {
			this.infoPanel.close();
		}
	},
hide_route:
	function()
	{
		this.remove_increments();
		if (!this._route_polyline) {
			return;
		}
		if (this._route_polyline instanceof Array) {
			for (var i=0; i<this._route_polyline.length; i++) {
				if (this._route_polyline[i]) {
					this.world.map.removeOverlay(this._route_polyline[i]);
				}
			}
		} else {
			this.world.map.removeOverlay(this._route_polyline);
		}
		delete this._route_polyline;
		this.removePointMarkers();
	},
removePointMarkers:
	function()
	{
		if (this._point_markers) {
			for (var i=0; i<this._point_markers.length; i++) {
				this.world.map.removeOverlay(this._point_markers[i]);
				GEvent.clearInstanceListeners(this._point_markers[i]);
			}
			delete this._point_markers;
		}
	},
destroy:
	function()
	{
		this.end_editing();
		if (this._increment_change_event) {
			GEvent.removeListener(this._increment_change_event);
		}
		if (this._show_route_event) {
			GEvent.removeListener(this._show_route_event);
		}
		this.hide_route();
		if (this._start_flag) {
			GEvent.clearListeners(this._start_flag);
			this.world.map.removeOverlay(this._start_flag);
			delete this._start_flag;
		}
		if (this._end_flag) {
			GEvent.clearListeners(this._end_flag);
			this.world.map.removeOverlay(this._end_flag);
			delete this._end_flag;
		}

		this.remove_increments();
		this.removePointMarkers();
		
		// Remove all old events
		GEvent.clearListeners(this);
	},
set_category:
	function(cat)
	{
		this.properties.category = cat;
		// TODO change colour
	},
get_options_title:
	function()
	{
		return 'Editing a route';
	},
get_options_element:
	function()
	{
		// Generate the <select> TODO This could be a lot cleaner
		var routeCategorySelection = T.select({'name':'category'}, [T.option({'value':''}, '-- Select a category --')]);
		if (COMMUNITY_GROUPS != null) {
			var routeGroupSelection = T.select({'name':'group', 'id':'group'}, [T.option({'value':0}, 'Select a group (optional)')]);
		} else {
			var routeGroupSelection = T.p('You are not currently a member of any realbuzz group.');
		}
		var optGroups = [];
		for (var x in ROUTE_CATEGORIES) {
			if (x != parseInt(x) || !ROUTE_CATEGORIES[x]) {
				continue;
			}
			
			// Don't show private categories
			if(ROUTE_CATEGORIES[x].private){ continue; }

			// Don't show route categories hidden by the 'filters' option
			if (_OPTIONS['filters'] && _OPTIONS['filters']['Route']) {
				if (_OPTIONS['filters']['Route'].find(parseInt(x, 10)) === false) {
					continue;
				}
			}

			var opt = T.option({'value':x}, ROUTE_CATEGORIES[x].title);
			// Select the current category
			if (this.properties.category == x) {
				opt.selected = true;
			}

			
			if (ROUTE_CATEGORIES[x].parent) {
				if (typeof optGroups[ROUTE_CATEGORIES[x].parent] == 'undefined') {
					optGroups[ROUTE_CATEGORIES[x].parent] = [null];
				}
				optGroups[ROUTE_CATEGORIES[x].parent].push(opt);
			} else if (ROUTE_CATEGORIES[x].is_parent) {
				optGroups[x][0] = T.optgroup({'label': ROUTE_CATEGORIES[x].title});
				routeCategorySelection.appendChild(optGroups[x][0]);
			} else {
				routeCategorySelection.appendChild(opt);
			}
		}
		
		
		if (COMMUNITY_GROUPS != null) {
			for (var y in COMMUNITY_GROUPS) {
				if (y != parseInt(y) || !COMMUNITY_GROUPS[y]) {
					continue;
				}
				
				var cg_opt = T.option({'value':y}, COMMUNITY_GROUPS[y].title);
				routeGroupSelection.appendChild(cg_opt);
			}
		}
		
		for (var i=0; i<optGroups.length; i++) {
			if (!optGroups[i]) {
				continue;
			}
			var g = optGroups[i];
			for (var j=1; j<g.length; j++) {
				g[0].appendChild(g[j]);
			}
		}
		GEvent.bindDom(routeCategorySelection, 'change', this, function()
		{
			this.colour = ROUTE_CATEGORIES.getNextColour(routeCategorySelection.value);
			if (this.editing) {
				this.setEditLineColour(this.colour);
			}
			this.redraw();
		});
		// ========


		var n;
		var public;
		var private;
		var autocentre;
		var snaptoroad;
		this._pointsField = T.td('0');
		this._distanceField = T.td();
		this._distanceField.appendChild(document.createTextNode('0 Miles'))
		this._distanceField.appendChild(T.br())
		this._distanceField.appendChild(document.createTextNode('0 KMs'))
		this._pointsField.style.paddingRight = '20px';
		
		var el = T.div([
			T.p('Click on the map to start plotting your route.'),

			T.table({'class': 'route_stats'}, [
				T.tbody([
					T.tr([
						T.th('Way points:'),
						T.th('Distance:')
					]),
					T.tr([
						this._pointsField,
						this._distanceField
					])
				])
			]),
			

			n = T.form({'className': 'panel_main_content'}, [
				T.div({'className': 'radio_field'}, [
					autocentre = T.input({'checked':this.options.autocentre, 'id': 'route_centre', 'type': 'checkbox', 'name': 'autocentre', 'value': 'yes'}),
					T.label({'for': 'route_centre'}, 'Auto-centre map')
				]),
				T.div({'className': 'radio_field'}, [
					// snaptoroad = T.input({'checked':this.options.snaptoroad, 'id': 'route_snap', 'type': 'checkbox', 'name': 'snaptoroad', 'value': 'yes'}),
					snaptoroad = T.input({'checked':false, 'id': 'route_snap', 'type': 'checkbox', 'name': 'snaptoroad', 'value': 'yes'}),
					T.label({'for': 'route_snap'}, 'Snap route to road')
				]),
				T.label({'for': 'route_title'}, 'Title of route:'),
				T.input({'value':this.properties.title, 'id': 'route_title', 'type': 'text', 'name': 'title'}),

				T.label({'for': 'route_description'}, 'Write your description here:'),
				T.textarea({'id': 'route_description', 'rows': 6, 'name': 'description'}, this.data.description),

				T.label({'for': 'route_category'}, 'Category:'),
				routeCategorySelection,
				
				T.label({'for': 'groups'}, 'Group:'),
				routeGroupSelection,
				
				T.div({'className': 'radio_field'}, [
					public = T.input({'checked':this.properties.public, 'id': 'route_public', 'type': 'radio', 'name': 'public', 'value': 'yes'}),
					T.label({'for': 'route_public'}, 'Public'),

					private = T.input({'checked':!this.properties.public, 'id': 'route_private', 'type': 'radio', 'name': 'public', 'value': 'no'}),
					T.label({'for': 'route_private'}, 'Private')
				])
			]),

			T.buttonRow({'className': 'button_row'}, [
				new Button('undo last point', GEvent.callback(this, this.deletePoint), 115),
				new Button('save', GEvent.callback(this,
					// == SAVE FUNCTION ==
					function()
					{
						// Validate input
						var f = n['title']
						if (!f.value) {
							alert('Please enter a title for your route.');
							f.focus();
							return;
						}
						var f = n['description'];
						if (!f.value) {
							alert('Please enter a short description of your route.');
							f.focus()
							return;
						}
						var f = n['category'];
						if (!f.value) {
							alert('Please select a category to place your route in.');
							f.focus()
							return;
						}
						if (COMMUNITY_GROUPS != null) {
							this.group = n['group'].value;
						}
						this.properties.title = n['title'].value;
						this.data.description = n['description'].value;
						this.properties.category = n['category'].value;
						this.properties.public = public.checked;
						this.save();
						delete el;
						delete n;
					}
				), 60)
			])
		]);
		n.onsubmit = GEvent.stop;
		setTimeout(GEvent.callback(this, function() { public.checked = this.properties.public; private.checked = !this.properties.public; }), 1);

		// Auto centring checkbox
		GEvent.bindDom(autocentre, 'click', this, function() {
			this.options.autocentre = autocentre.checked;
			O.setOption('route_autocentre', autocentre.checked);
		});
		GEvent.bindDom(snaptoroad, 'click', this, function() {
			this.options.snaptoroad = snaptoroad.checked; 
			O.setOption('route_snaptoroad', snaptoroad.checked);
		});

		// IE requires this setting AFTER being added to the DOM. Thank you, Ballmer!
		autocentre.checked = this.options.autocentre;
		snaptoroad.checked = this.options.snaptoroad;
		return el;
	},
deletePoint:
	function(id)
	{
		if (id == undefined) {
			var id = this.data.points.length-1;
		}
		if (id < 0 || id > this.data.points.length -1) {
			return;
		}

		var content = this.data.pointContent[id];

		// TODO allow deletion of other points, currently only deletes the last one
		if (this.editing) {
			// If it's less than 2 verticies then just delete the lot
			if (this.data.points.length <= 1) {
				this.removeEditLine();
			} else {
				switch (content.type) {
				case 'snaptoroad':
					// Snapped to road
					// this._snappedLines[id];
					// this.world.map.removeOverlay(this._snappedLines[id]);
					// delete this._snappedLines[id];
					// break;
					var lastLine = this._editLines[this._editLines.length -1];
					var seg = this.getEditLineForPoint(id);
					/*
					if (seg.line.getVertexCount() > 2) {
						seg.line.deleteVertex(seg.id);
					*/
					// if (this._snappedLines[id].getVertexCount() > 2) {
					// 	this._snappedLines[id].deleteVertex(this._snappedLines[id].getVertexCount() -1);
					// 	console.log("vertex count > 2");
					// 	console.log(this._snappedLines[id].getVertexCount() -1);
					// 	this._editLines.pop();
					// } else {
					this.world.map.removeOverlay(this._snappedLines[id]);
					// console.log("vertex count <= 2");
					// console.log(this._snappedLines[id].getVertexCount() -1);
					this._editLines.pop();
					// }
					break;

				case 'normal':
				default:
					var lastLine = this._editLines[this._editLines.length -1];
					var seg = this.getEditLineForPoint(id);
					/*
					if (seg.line.getVertexCount() > 2) {
						seg.line.deleteVertex(seg.id);
					*/
					if (lastLine.getVertexCount() > 2) {
						lastLine.deleteVertex(lastLine.getVertexCount() -1);
					} else {
						this.world.map.removeOverlay(lastLine);
						this._editLines.pop();
					}
					break;
				}
			}
			this.removeHandleForPoint(id);
		}
		this.data.points.splice(id, 1);
		this.data.pointContent.splice(id, 1);
		this.updateEditFlags();
		this.redraw();
		if (this.editing) {
			this.updateEditFlags();
		}
	},
createEditLine:
	function(force, points)
	{
		if (force) {
			this.removeEditLine();
		} else if (this._editing_line) {
			return;
		}
		if (!this._editLines) {
			this._editLines = [];
		}


		if (!points) {
			var points = [];
			if (this.data.points.length > 1) {
				var p = this.data.points[this.data.points.length -2];
				points.push(new GLatLng(p[0], p[1]));
			}
		}
		/*
		for (var i=0; i<this.data.points.length; i++) {
			var p = this.data.points[i];
			points.push(new GLatLng(p[0], p[1]));
		}
		*/
		
		if (!this.colour) {
			this.colour = ROUTE_CATEGORIES.getNextColour(this.properties.category);
		}

		//this._editing_line = new GPolyline(points, this.colour, ROUTE_WIDTH, ROUTE_OPACITY, {clickable: false});
		var line = new GPolyline(points, this.colour, ROUTE_WIDTH, ROUTE_OPACITY, {clickable: false});
		this._editLines.push(line);
		this.world.map.addOverlay(line);
		if (!this._editing_flags) {
			this._editing_flags = {
				start: createMarker(new GLatLng(0, 0), {icon: Icons['start_flag'], draggable: true}),
				end:   createMarker(new GLatLng(0, 0), {icon: Icons['end_flag'], draggable: true})
			};
			this.world.map.addOverlay(this._editing_flags['start']);
			this.world.map.addOverlay(this._editing_flags['end']);
		}
		// FIXME need to update end flag to attach to correct point
		this.updateEditFlags();


		// == START FLAG == \\
		// Add even handler to update end points when flag is dragged
		GEvent.bind(this._editing_flags['start'], 'drag', this, function()
		{
			// Update start point
			var p = this._editing_flags['start'].getLatLng();
			var h = this.getHandle(0);

			h.disablePlus();
			this.setPoint(0, p);
			h.setPoint(p);
		});
		GEvent.bind(this._editing_flags['start'], 'dragend', this, function()
		{
			var h = this.getHandle(0);
			h.enablePlus();
		});
		// Show 'plus' when hoving over flags
		GEvent.bind(this._editing_flags['start'], 'mouseover', this, function()
		{
			var h = this.getHandle(0);
			h.showPlus();
		});
		GEvent.bind(this._editing_flags['start'], 'mouseout', this, function()
		{
			var h = this.getHandle(0);
			h.hidePlus();
		});
		//GEvent.bind(this._editing_flags['start'], 'mouseout',  h, h.delayedHide);
		

		// == END FLAG == \\
		GEvent.bind(this._editing_flags['end'], 'drag', this, function()
		{
			// Update end point
			var id = this.data.points.length-1;
			if (id < 1) {
				return;
			}
			var p = this._editing_flags['end'].getLatLng();
			var h = this.getHandle(id);
			h.disablePlus();
			this.setPoint(id, p);
			h.setPoint(p);
			this.updateDistanceTooltip(h.index);
		});
		GEvent.bind(this._editing_flags['end'], 'dragend', this, function()
		{
			var id = this.data.points.length-1;
			if (id < 1) {
				return;
			}
			var h = this.getHandle(id);
			h.enablePlus();
		});
		// Show 'plus' when hoving over flags
		GEvent.bind(this._editing_flags['end'], 'mouseover', this, function()
		{
			var id = this.data.points.length-1;
			if (id < 1) {
				return;
			}
			var h = this.getHandle(id);
			h.showPlus();
			this.showPointDistance(h.index);
		});
		GEvent.bind(this._editing_flags['end'], 'mouseout', this, function()
		{
			var id = this.data.points.length-1;
			if (id < 1) {
				return;
			}
			var h = this.getHandle(id);
			h.hidePlus();
			this.hidePointDistance(h.index);
		});



		// FIXME Show/hide handles when hovering
		// This only works with 'clickable: true' but that means you can't plot points on a route line
		/*
		GEvent.bind(this._editing_line, 'mouseover', this,
			function()
			{
				alert('meh');
			}
		);
		*/
		//return this._editLines;
		return line;
	},
updateEditFlags:
	function()
	{
		if (!this._editing_flags || this.data.points.length == 0) {
			return;
		}

		var p = this.data.points[0];
		var s = this._editing_flags['start'];
		s.setLatLng(new GLatLng(p[0], p[1]));

		var p = this.data.points[this.data.points.length-1];
		var s = this._editing_flags['end'];
		if (this.data.points.length > 1) {
			s.setLatLng(new GLatLng(p[0], p[1]));
			s.show();
		} else {
			s.hide();
		}
	},
removeEditLine:
	function()
	{
		if (!this._editLines) {
			return;
		}
		for (var i=0; i<this._editLines.length; i++) {
			GEvent.clearListeners(this._editLines[i]);
			this.world.map.removeOverlay(this._editLines[i]);
		}
		delete this._editLines;

		GEvent.clearListeners(this._editing_flags['start']);
		this.world.map.removeOverlay(this._editing_flags['start']);
		GEvent.clearListeners(this._editing_flags['end']);
		this.world.map.removeOverlay(this._editing_flags['end']);
		delete this._editing_flags;


		// Removed snapped lines
		if (this._snappedLines) {
			for (var i=0; i<this._snappedLines.length; i++) {
				if (!this._snappedLines[i]) {
					continue;
				}
				this.world.map.removeOverlay(this._snappedLines[i]);
			}
			delete this._snappedLines;
		}
	},
start_editing:
	function()
	{
		MapItem.prototype.start_editing.apply(this, arguments);
		//this.createEditLine(true);

		// Watch for map clicks
		var click_event = GEvent.bind(this.world.map, 'click', this,
			function (item, point)
			{
				// If the item has the ignoreMap property set, then we ignore
				// the click
				if (item && item.ignoreMap) {
					return;
				}

				if (!point) {
					var point = item.getLatLng();
				}
				//this.addPoint(point, 'snaptoroad');
				this.addPoint(point);
				if (this.options.autocentre) {
					this.world.jump_to_point(point);
				}
			}
		);

		// Unbind the above event when we stop editing
		var unbind_event = GEvent.bind(this, 'endediting', this,
			function()
			{
				GEvent.removeListener(click_event);
				GEvent.removeListener(unbind_event);
			}
		);

		// Editing existing item, so clean things up
		if (this.data.points.length > 0) {
			if (this._start_flag) {
				this.world.map.removeOverlay(this._start_flag);
				delete this._start_flag;
			}
			if (this._end_flag) {
				this.world.map.removeOverlay(this._end_flag);
				delete this._end_flag;
			}


			// Rebuild route ready for editing
			if (!this.data.pointContent) {
				this.data.pointContent = [];
			}
			var prevP = false;
			var prevPc = false;
			var lines = [];
			var line;
			//var linePoints = false;
			for (var i=0; i<this.data.points.length; i++) {
				var p = this.data.points[i];
				var pc = this.data.pointContent[i];
				if (!pc) {
					pc = {type: 'normal'};
					this.data.pointContent[i] = pc;
				} else if (!pc.type) {
					pc.type = 'normal';
					this.data.pointContent[i].type = 'normal';
				}

				if (!prevP || (prevPc.type != 'normal' && pc.type == 'normal')) {
					line = [];
					lines.push(line);
					//line = this.createEditLine();
				}
				line.push(new GLatLng(p[0], p[1]));

				//line.insertVertex(0, new GLatLng(p[0], p[1]));



				prevP = p;
				prevPc = pc;
			}
			for (var i=0; i<lines.length; i++) {
				this.createEditLine(false, lines[i]);
			}
			this.redraw();
		}

	},
end_editing:
	function()
	{
		MapItem.prototype.end_editing.call(this);

		// Remove editing handles
		if (this._handles) {
			for (var i=0; i<this._handles.length; i++) {
				if (this._handles[i]) {
					this._handles[i].destroy();
				}
				if (this._ghostHandles[i]) {
					this._ghostHandles[i].destroy();
				}
			}
			delete this._handles;
			delete this._ghostHandles;
		}
		this.removeEditLine();
	},
cancel_editing:
	function()
	{
		if (confirm('Are you sure you wish to cancel editing this route? You will lose all unsaved changes.')) {
			this.end_editing();
			if (this.properties.uid == 0) {
				this.destroy();
			}
			return true;
		}
		return false;
	},
insertPoint:
	function(id, point)
	{
		this.data.points.splice(id, 0, [point.lat(), point.lng()]);
		this.data.pointContent.splice(id, 0, {type: 'normal'});

		if (this.editing) {
			// if we're editing then we inject the new point
			var seg = this.getEditLineForPoint(id);
			seg.line.insertVertex(seg.id, point);

			
	
			// update handles' indexes
			for (var i=0; i<this._handles.length; i++) {
				var h = this._handles[i];
				if (h.index >= id) {
					h.index++;
				}
				var g = this._ghostHandles[i];
				if (g && g.index >= id) {
					g.index++;
				}
			}

			// Create a new handle
			this.createHandleForPoint(id);

			return id;
		} else {
			// TODO inserting point while not editing
		}
	},
getLastEditLine:
	function()
	{
		if (!this._editLines || this._editLines.length == 0) {
			return false;
		}

		return this._editLines[this._editLines.length -1];
	},
addPoint:
	function(point, type, fromQueue)
	{
		if (!this._addPointQueue) {
			this._addPointQueue = new EventQueue(this, 'addpoint');
		}

		// If there's stuff in the queue we need to add this to the queue
		// regardless of what type of point it is
		if (!fromQueue && (this._addPointQueue.length > 0 || this._snappingToRoad)) {
			this._addPointQueue.addCallback(GEvent.callbackArgs(this, this.addPoint, point, type, true));
			return;
		}


		if (!type) {
			var type = (this.options.snaptoroad) ? 'snaptoroad' : 'normal';
		}

		var prevPoint   = this.data.points[this.data.points.length-1];
		var prevContent = this.data.pointContent[this.data.pointContent.length-1];


		this.data.points.push([point.lat(), point.lng()]);
		this.data.pointContent.push({type: type});
		if (!this.anchor) {
			this.anchor = new GLatLng(this.data.points[0][0], this.data.points[0][1]);
		}

		// if we're editing then we inject the new point
		if (this.editing) {
			switch (type) {
			case 'normal':
				var line = (prevContent && prevContent.type == 'normal') ? this.getLastEditLine() : this.createEditLine();
				line.insertVertex(line.getVertexCount(), point);
				this.updateEditFlags();
				GEvent.trigger(this, 'addpoint');
				break;
			case 'snaptoroad':
				// if this is true, then all clicks afterwards will be queued
				this._snappingToRoad = true;
				// No previous point, so we just want to move this point to the road
				if (!prevPoint) {
					GEvent.clearListeners(this.directions);
					var evnt = GEvent.bind(this.directions, 'load', this,
						function()
						{
							var point = this.directions.getPolyline().getVertex(0);
							this.createEditLine().insertVertex(this.data.points.length, point);
							this.anchor = point;
							this.data.points[0] = [point.lat(), point.lng()];
							this._snappingToRoad = 0;
							GEvent.removeListener(evnt);
							GEvent.trigger(this, 'addpoint');
							this.getHandle(0).setPoint(point);
							this.updateFields();
							this.updateSnappedPoints();
							this.updateEditFlags();
						}
					);

					this.directions.loadFromWaypoints([point, point], ROUTE_SNAPPED_OPTIONS);
				} else {
					var prevPoint = new GLatLng(prevPoint[0], prevPoint[1]);
					this.directions.loadFromWaypoints([prevPoint, point], ROUTE_SNAPPED_OPTIONS);

					// TODO This code is mostly duplicated in Route.setPoint
					var evnt = GEvent.bind(this.directions, 'load', this,
						function()
						{
							
							if (!this._snappedLines) {
								this._snappedLines = [];
							}

							var poly = this.directions.getPolyline();
							poly.setStrokeStyle({color: this.colour, weight: ROUTE_WIDTH, opacity: ROUTE_OPACITY});
							this.world.map.addOverlay(poly);
							this._snappedLines[this.data.points.length-1] = poly;

							var last = poly.getVertex(poly.getVertexCount() -1);
							this.getHandle(this.data.points.length -1).setPoint(last);

							this._snappingToRoad = false;
							GEvent.removeListener(evnt);
							GEvent.trigger(this, 'addpoint');

							// Store computed line
							var verts = [];
							var vcount = poly.getVertexCount();
							for (var i=0; i<vcount; i++) {
								var v = poly.getVertex(i);
								verts.push([v.lat(), v.lng()]);
							}
							this.data.pointContent[this.data.pointContent.length-1].computedPoints = verts;
							this.updateFields();
							this.updateSnappedPoints();
							this.updateEditFlags();
						}
					);
				}
				this.updateEditFlags();
				break;
			default:
				
				break;
			}
		}
		this.redraw(true);
	},
addPoints:
	function(points)
	{
		if (!this.data.points) {
			this.data.points = [];
		}
		this.data.points = this.data.points.concat(points);
		if (!this.anchor && this.data.points.length > 0) {
			this.anchor = new GLatLng(this.data.points[0][0], this.data.points[0][1]);
		}
		this.redraw(true);
	},
getHandle:
	function(pointID)
	{
		if (!this._handles) {
			return false;
		}
		for (var i=0; i<this._handles.length; i++) {
			if (this._handles[i].index == pointID) {
				return this._handles[i];
			}
		}
		return false;
	},
setPoint:
	function(id, point, moveHandle)
	{
		if (point instanceof GLatLng) {
			var ll = point;
			var point = [ll.lat(), ll.lng()];
		} else {
			var ll = new GLatLng(point[0], point[1]);
		}


		// 'id' is incremented to update snapped lines, so we need to keep a
		// copy of the original
		var original_id = id;

		/*
		if (point == this.data.points[id]) {
			return;
		}
		*/


		// All these vars are set after the updateNextPoint() function, but
		// need to be inside the closure.
		var poly;
		var vcount;
		var lastPoint;
		var nextPoint;
		var prevPoint;


		// TODO This code is mostly duplicated in Route.addPoint
		// Load new snapped route data
		var evnt = GEvent.bind(this.directions, 'load', this,
			function()
			{
				
				if (!this._snappedLines) {
					this._snappedLines = [];
				}
				poly = this.directions.getPolyline();
				var p = this._snappedLines[id];
				if (p) {
					this.world.map.removeOverlay(p);

					poly.setStrokeStyle({color: this.colour, weight: ROUTE_WIDTH, opacity: ROUTE_OPACITY});
					this.world.map.addOverlay(poly);
					this._snappedLines[id] = poly;
				}
				
				// Update handle
				var lastVert = poly.getVertex(poly.getVertexCount() -1);
				this.getHandle(id).setPoint(lastVert);
				// First handle has no line before it so we need to update it here
				if (id == 1) {
					this.getHandle(0).setPoint(poly.getVertex(0));
				}


				/*
				var last = poly.getVertex(poly.getVertexCount() -1);
				this.getHandle(this.data.points.length -1).setPoint(last);
				*/

				this._snappingToRoad = false;

				// Store computed line
				var verts = [];
				vcount = poly.getVertexCount();
				for (var i=0; i<vcount; i++) {
					var v = poly.getVertex(i);
					verts.push([v.lat(), v.lng()]);
				}
				this.data.pointContent[id].computedPoints = verts;

				this.updateSnappedPoints();
				this.updateEditFlags();

				updateNextPoint.call(this);

			}
		);

		function updateNextPoint()
		{
			// Update next point too
			if (!lastPoint && nextPoint) {
				if (this.data.pointContent[id +1].type == 'snaptoroad') {
					lastPoint = true;	// Prevents this running over and over...
					id++;
					var waypoints = [ll, nextPoint];
					this.directions.loadFromWaypoints(waypoints, ROUTE_SNAPPED_OPTIONS);
				} else if (this.data.pointContent[id].type == 'snaptoroad' && this.data.pointContent[id +1].type == 'normal') {
					// connecting line is a normal one, so adjust the first point
					var seg = this.getEditLineForPoint(id+1);
					seg.line.insertVertex(0, poly.getVertex(vcount-1));
					seg.line.deleteVertex(1);
					if (evnt) {
						GEvent.removeListener(evnt);
					}
				} else if (evnt) {
					GEvent.removeListener(evnt);
				}
			} else if (evnt) {
				GEvent.removeListener(evnt);
			}
		}


		this.data.points[id] = point;
		prevPoint = this.data.points[id -1];
		if (prevPoint) {
			prevPoint = new GLatLng(prevPoint[0], prevPoint[1]);
		}

		nextPoint = this.data.points[id +1];
		if (nextPoint) {
			nextPoint = new GLatLng(nextPoint[0], nextPoint[1]);
		}

		if (this.editing) {
			switch (this.data.pointContent[id].type) {
			case 'snaptoroad':
				this._snappingToRoad = true; // Forces all points to be queued until we get a response

				if (!prevPoint && !nextPoint) {
					// TODO snap point to road, but no polyline
					var waypoints = [ll, ll];
				} else if (!prevPoint) {
					// Skip this point and just update next line
					var waypoints = [ll, nextPoint];
					id++;
					lastPoint = true;
				} else {
					var waypoints = [prevPoint, ll];
				}
				this.directions.loadFromWaypoints(waypoints, ROUTE_SNAPPED_OPTIONS);


				

				break;
			case 'normal':
			default:
				var segment = this.getEditLineForPoint(id);
				segment.line.insertVertex(segment.id, ll);
				segment.line.deleteVertex(segment.id+1);

				updateNextPoint.call(this);

				// If next point is snaptoroad, update that too

				break;
			}
			this.updateEditFlags();
		}
		this.redraw();
		if (moveHandle) {
			var h = this.getHandle(id);
			h.setPoint(ll);
		}
		GEvent.trigger(this, 'setpoint', original_id, ll);
	},
getEditLineForPoint:
	function(id)
	{
		var offset = 0;
		var lineCount = 0;

		for (var i=0; i<=id; i++) {
			var pc = this.data.pointContent[i];
			var ppc = this.data.pointContent[i -1];
			if (i > 0 && pc.type != 'normal') {
				offset++;
			}
			if (ppc && ppc.type != 'normal' && pc.type == 'normal') {
				lineCount++;
			}
		}

		// loop over previous lines and add vertex count to offset
		for (var i=1; i<lineCount; i++) {
			offset += this._editLines[i].getVertexCount() -1;
		}

		return {
			line: this._editLines[lineCount],
			id: id - offset
		};

		return false;
	},
show_increments:
	function(distance, label)
	{
		this._current_increment = distance;
		this._increment_label = label;
		var label = label || ' mile';


		if (!this._incremental_markers) {
			this._incremental_markers = {};
		}


		if (this._incremental_markers[distance]) {
			return;
		} else {
			this._incremental_markers[distance] = [];
		}

		var points = this.get_increments(distance);

		// TODO delete left over markers

		var plural = false;
		for (var i=this._incremental_markers[distance].length; i<points.length; i++) {
			if (!plural && i >= 1) {
				plural = true;
				label += 's';
			}
		//	var m = createMarker(points[i], {icon: Icons['route_handle'], inert: true, draggable: false, clickable: false});
			//this.map.addMarker(m);
			var m = new IncrementMarker(points[i], (i+1) + label, false, i);
			this.world.map.addOverlay(m);
			this._incremental_markers[distance].push(m);
		}
	},
remove_increments:
	function(distance)
	{
		if (!this._incremental_markers) {
			return;
		}
		if (distance && !this._incremental_markers[distance]) {
			return;
		}


		if (distance) {
			for (var i=0; i<this._incremental_markers[distance].length; i++) {
				//this.map.removeMarker(this._incremental_markers[i]);
				this.world.map.removeOverlay(this._incremental_markers[distance][i]);
			}
			delete this._incremental_markers[distance];
		} else {
			for (j in this._incremental_markers) {
				if (isNaN(j)) {
					continue;
				}
				for (var i=0; i<this._incremental_markers[j].length; i++) {
					this.world.map.removeOverlay(this._incremental_markers[j][i]);
				}
			}
			delete this._incremental_markers;
		}
	},
// TODO this needs a lot of improving, accuracy is poor without doing the cheeky little zoom
get_increments:
	function(inc)
	{
		var distance = 0;
		var lastUnit = 0;
		var markerCount = 0;
		var unitPoints = [];
		var leaveLoop = false;


		var points = this._route_polyline.points;

		// Zoom in for best accuracy
		var oldZoom = this.world.map.getZoom();
		this.world.map.setZoom(17);

		for (var i=1; i<points.length && !leaveLoop; i++) {
			var p1 = new GLatLng(points[i-1][0], points[i-1][1]);
			var p2 = new GLatLng(points[i][0], points[i][1]);
			var dp1 = this.world.map.fromLatLngToDivPixel(p1);
			var dp2 = this.world.map.fromLatLngToDivPixel(p2);
			var distanceInMetres = p1.distanceFrom(p2);
			distance += distanceInMetres;

			var distanceInPixels = Math.sqrt(Math.pow((dp2.x - dp1.x), 2) + Math.pow((dp2.y - dp1.y), 2));
			var unitsPerPixel = distanceInPixels / (distanceInMetres / inc);



			// Calculate where point should be along the line
			var adj = dp2.x - dp1.x;
			var opp = dp2.y - dp1.y;
			var rad = Math.atan2(adj, opp);



			if (unitsPerPixel-lastUnit < distanceInPixels) {
				var speed = unitsPerPixel-lastUnit;	// the distance to move
				if (speed < 0) {
					speed = -speed;
				}
				while (speed >= 0 && speed < distanceInPixels && !leaveLoop) {
					var unitX = dp1.x + (Math.sin(rad) * speed);
					var unitY = dp1.y + (Math.cos(rad) * speed);

					var unit = new GPoint(unitX, unitY);
					dp1 = unit;
					distanceInPixels = Math.sqrt(Math.pow((dp2.x - dp1.x), 2) + Math.pow((dp2.y - dp1.y), 2));
					unit = this.world.map.fromDivPixelToLatLng(unit);

					unitPoints.push(unit);
					lastUnit = distanceInPixels;
					speed = unitsPerPixel;
					if (unitPoints.length >= 100) {
						leaveLoop = true;
					}
				}
			} else {
				lastUnit += distanceInPixels;
			}
		}
		// remove extra marker created by rounding errors
		if (unitPoints.length > distance / inc) {
			unitPoints.pop();
		}
		this.world.map.setZoom(oldZoom);
		return unitPoints;
	},
	update_fields: function()
	{
		var miles = 0;
		var kms = 0;
		if (this._pointsField) {
			while (this._pointsField.firstChild) {
				this._pointsField.removeChild(this._pointsField.firstChild);
			}
			this._pointsField.appendChild(document.createTextNode(this.data.points.length));
		}
		if (this._distanceField) {
			if (this._route_polyline) {
				if (this._route_polyline instanceof Array) {
					for (var i=0; i<this._route_polyline.length; i++) {
						miles += this._route_polyline[i].getLength() / METRES2MILES;
						kms +=  this._route_polyline[i].getLength() / METRES2KMS;
					}
				} else {
					miles = this._route_polyline.getLength() / METRES2MILES;
					kms =  this._route_polyline.getLength() / METRES2KMS;
				}
			}
			miles = Math.roundDP(miles, 2);
			kms = Math.roundDP(kms, 2);
			while (this._distanceField.firstChild) {
				this._distanceField.removeChild(this._distanceField.firstChild);
			}
			this._distanceField.appendChild(document.createTextNode(miles + ' Miles'))
			this._distanceField.appendChild(T.br())
			this._distanceField.appendChild(document.createTextNode(kms + ' KMs'))
		}
	},
duplicate:
	function()
	{
		var item = MapItem.prototype.duplicate.call(this);
		// Force redraw of line
		//item.createEditLine(true);
		// Updates the 'distance' and 'waypoints' in sidepanel
		//item.updateFields();
		this.world.edit_item(item);
		//item.start_editing();
		alert("The route has been copied, you can make changes and save it to your items using the panel on the left.");
		return item;
	},
getPopoutContent:
	function()
	{
		// TODO Draw the route. I'm not sure this should be called here
		this.show_route();

		var miles = 0;
		var kms = 0;
		if (this._route_polyline) {
			if (this._route_polyline instanceof Array) {
				for (var i=0; i<this._route_polyline.length; i++) {
					miles += this._route_polyline[i].getLength() / METRES2MILES;
					kms +=  this._route_polyline[i].getLength() / METRES2KMS;
				}
			} else {
				miles = this._route_polyline.getLength() / METRES2MILES;
				kms =  this._route_polyline.getLength() / METRES2KMS;
			}
		} else {
			
			miles = 0;
			kms = 0;
		}
		miles = Math.roundDP(miles, 2);
		kms = Math.roundDP(kms, 2);
		
		var url_input;
		var copy_route_link;
		var route_url = SITE_URL + '?item='+this.properties.uid;
		
		// get a lucozade tip at random
		var tip = get_tip();
		
		// Need this check to see if we are on flm map page (as opposed to realbuzz)- but repeating bits of code here.
		if (checkURL() == true) {
			var n = T.div({'class':'mini_balloon_panel_content'}, [
				T.div({'class':'info_box'}, [
					// Stats, Distance, etc.
					//T.h2('Statistics'),

					// Show random lucozade tip on the FLM map only
					// T.div({'id':'tip-box-container'}, [
					// 	T.div({'id':'tip-box-top'}),
					// 	T.div({'id':'tip-box-middle'}, [
					// 		T.p({'class':'tip-text'}, tip),
					// 		T.p({'class':'tip-text'}, [T.a({'href':'http://adserver.realbuzz.com/click/59/','target':'_blank','name':'lucozade'}, 'Learn more')]),
					// 	]),
					// 	T.div({'id':'tip-box-bottom'}),
					// ]),
					// 
					// T.br(),
					T.table([T.tbody([
						T.tr([
							T.th('Way points : '),
							this._pointsField = T.td(this.data.points.length)
						]),
						T.tr([
							T.th('Distance : '),
							this._distanceField = T.td( miles + ' Miles -- ' + kms + ' KMs')
						])
					])]),

					/*
					// About the author
					T.h2('Author'),
					T.table([T.tbody([
						T.tr([
							T.td({'rowspan':2}, [
								T.img({'width':80,'height':60,'src':'http://www.realbuzz.com/uploads/'+this.properties.user.id+'/system/profile80x60.jpg'})
							]),
							T.td([
								T.a({'href':'http://www.realbuzz.com/en-gb/users/' + escape(this.properties.user.username),'target':'_blank'}, [T.span(this.properties.user.username)])
							])
						]),
						T.tr([
							T.td([
						//		T.span('Stats?')
							])
						])
					])]),
					*/

					// Copy this route
					//T.h2('Copy this route'),
					T.ul({'class':'export_list','style':'display:none'}, [
						T.li([
							copy_route_link = T.a({'href': 'javascript:void(0)'}, 'Copy and edit route')
						])
					]), 

					// Export route as GPX, KML, etc.
					//T.h2('Export'),
					T.ul({'class':'export_list'}, [
						T.li([T.a({'href': BASE_URL + 'gpx_routes/' + this.properties.uid}, 'Export this route for GPS devices (.gpx)')])
						//T.li([T.a({'href':''}, 'Export this route for Google Earth (.kml)')])
					]),

					// Link to this route
					T.h2('Link to this route'),
					T.ul({'class':'export_list'}, [
						T.li([
							url_input = T.input({'readonly':'readonly','value': route_url})
						])
					]),
					
				])
			]);
		} else {
			var n = T.div({'class':'mini_balloon_panel_content'}, [
				T.div({'class':'info_box'}, [
					// Stats, Distance, etc.
					//T.h2('Statistics'),
					
					// T.a({'href':'#lucozade'}, [T.img({'width':294,'height':23,'src':'http://www.realbuzz.com/static/images/view-tip.gif'})]),
					
					// Show random lucozade tip on the MYP map but with different learn more link
					// T.div({'id':'tip-box-container'}, [
					// 	T.div({'id':'tip-box-top'}),
					// 	T.div({'id':'tip-box-middle'}, [
					// 		T.p({'class':'tip-text'}, tip),
					// 		T.p({'class':'tip-text'}, [T.a({'href':'http://adserver.realbuzz.com/click/60/','target':'_blank','name':'lucozade'}, 'Learn more')]),
					// 	]),
					// 	T.div({'id':'tip-box-bottom'}),
					// ]),
					// T.br(),
					
					T.table([T.tbody([
						T.tr([
							T.th('Way points : '),
							this._pointsField = T.td(this.data.points.length)
						]),
						T.tr([
							T.th('Distance : '),
							this._distanceField = T.td( miles + ' Miles -- ' + kms + ' KMs')
						])
					])]),

					/*
					// About the author
					T.h2('Author'),
					T.table([T.tbody([
						T.tr([
							T.td({'rowspan':2}, [
								T.img({'width':80,'height':60,'src':'http://www.realbuzz.com/uploads/'+this.properties.user.id+'/system/profile80x60.jpg'})
							]),
							T.td([
								T.a({'href':'http://www.realbuzz.com/en-gb/users/' + escape(this.properties.user.username),'target':'_blank'}, [T.span(this.properties.user.username)])
							])
						]),
						T.tr([
							T.td([
						//		T.span('Stats?')
							])
						])
					])]),
					*/

					// Copy this route
					//T.h2('Copy this route'),
					T.ul({'class':'export_list','style':'display:none'}, [
						T.li([
							copy_route_link = T.a({'href': 'javascript:void(0)'}, 'Copy and edit route')
						])
					]), 

					// Export route as GPX, KML, etc.
					//T.h2('Export'),
					T.ul({'class':'export_list'}, [
						T.li([T.a({'href': BASE_URL + 'gpx_routes/' + this.properties.uid}, 'Export this route for GPS devices (.gpx)')])
						//T.li([T.a({'href':''}, 'Export this route for Google Earth (.kml)')])
					]),

					// Link to this route
					T.h2('Link to this route'),
					T.ul({'class':'export_list'}, [
						T.li([
							url_input = T.input({'readonly':'readonly','value': route_url})
						])
					]),
				])
			]);
			
		};
		// this is just for IE, otherwise i'd have set it in the LI itself
		url_input.style.width = '270px';

		var reset_url = function()
		{
			if (this.value != route_url) {
				this.value = route_url;
			}
			this.focus();
			this.select();
		};
		GEvent.bindDom(url_input, 'click', url_input, reset_url);
		//GEvent.bindDom(url_input, 'focus', url_input, reset_url);
		GEvent.bindDom(url_input, 'keydown', url_input, reset_url);
		GEvent.bindDom(url_input, 'keypress', url_input, reset_url);
		GEvent.bindDom(url_input, 'keyup', url_input, reset_url);
		GEvent.bindDom(url_input, 'change', url_input, reset_url);


		GEvent.bindDom(copy_route_link, 'click', this, this.duplicate);

		// If it has a description, place that under the stats
		if (this.data.description) {
			n.firstChild.insertBefore(T.div([
				T.h2('Description'),
				document.createParagraph(this.data.description)
			]), n.firstChild.childNodes[2]);
		}

		//this._panel.set_content(n);

		return n;
	},
addContentToWaypoint:
	function(id)
	{
		
		var popout = new TabbedPopoutPanel(this.world, this.data.points[id], 400, 300, true);
		popout.set_title('Add content to waypoint ' + (id +1));

		if (!this.data.pointContent) {
			this.data.pointContent = [];
		}
		if (!this.data.pointContent[id]) {
			this.data.pointContent[id] = {
				type: 'normal'
			};
		}

		var waypointTitle = this.data.pointContent[id].title || '';
		var waypointDesc  = this.data.pointContent[id].description || '';
		var waypointType  = this.data.pointContent[id].type || 'normal';

		// Description tab
		var titleInput;
		var descriptionInput;
		popout.add_tab('Description', T.div({'class': 'panel_main_content'}, [
			T.div('You can add a title, description as well as photos and video to this waypoint.'),

			T.label({'for': 'waypoint_title'+id}, 'Title of waypoint'),
			titleInput = T.input({'id': 'waypoint_title'+id, 'type': 'text', 'name': 'title', 'value': waypointTitle}),

			T.label({'for': 'waypoint_description'+id}, 'Write your description here'),
			descriptionInput = T.textarea({'id': 'waypoint_description'+id, 'rows': 6, 'name': 'description'}, waypointDesc),

			T.buttonRow({'className': 'button_row'}, [
				//new Button('save', GEvent.callback(this, save_and_close)),
				new Button('next', function() { popout.show_tab(1); })
			])
		]));

		// Media uploader
		var uploader = new MediaUploader("100%", 26);
		// Add previously uploaded files to the list
		if (this.data.media && this.data.media[id]) {
			var media = this.data.media[id];
			for (var i=0; i<media.length; i++) {
				var row = uploader.uploadList.addItem(media[i].id, media[i].original_filename, media[i].tags);
				// TODO add 'tags'
			}
			delete media;
		}

		// Media tab content
		var form = T.form({'class': 'panel_main_content'}, [
			uploader,
			T.buttonRow({'className': 'button_row'}, [
				new Button('back', function() { popout.show_tab(0); }),
				new Button('save', GEvent.callback(this, function() {
					// Save tags on uploaded files
					uploader.saveTags();

					// Save waypoint content
					this.saveWaypointContent(id, {
						title: titleInput.value,
						description: descriptionInput.value,
						media: uploader.getUploadedIds(),
						type: waypointType
					});

					popout.close();
					delete popout;
				}))
			])
		]);
		GEvent.addDomListener(form, 'submit', GEvent.stop);
		popout.add_tab('Photos & videos', form);

	},
saveWaypointContent:
	function(id, data)
	{
		
		var media = data.media;
		delete data.media;
		this.data.pointContent[id] = data;
		if (!this.data.gallery) {
			this.data.gallery = [];
		}
		this.data.gallery[id] = media;
	},
showWaypointContent:
	function(id)
	{
		var p = this.data.pointContent[id];
		var media = this.data.media ? this.data.media[id] : false;

		
		var popout = new PopoutPanel(this.world, this.data.points[id], 480, 240, true);

		popout.set_title(p.title);
		popout.set_content(this.getWaypointPopoutContent(id));
		if (media && media.length > 0) {
			popout.resize(666, 266);
			popout.centre_on_map();
		}
	},
getWaypointPopoutContent:
	function(id)
	{
		var p = this.data.pointContent[id];
		var media = this.data.media ? this.data.media[id] : false;
		var description = T.div([document.createParagraph(p.description)]);

		if (media && media.length > 0) {
			var gallery = new Gallery(this.properties.user.username);
			gallery.set_description(description);
			gallery.add_media_from_array(media);
			var element = gallery.element;
		} else {
			var element = description;
		}
		return element;
	},
getDistanceToPoint:
	function(id)
	{
		var points = this.data.points.slice(0, id+1);
		var latlngs = [];
		for (var i=0; i<points.length; i++) {
			if (this._snappedLines && this.data.pointContent[i].type == 'snaptoroad') {
				var line = this._snappedLines[i];
				if (line) {
					var vertCount = line.getVertexCount();
					for (var j=0; j<vertCount; j++) {
						latlngs.push(line.getVertex(j));
					}
				} else {
					latlngs.push(new GLatLng(points[i][0], points[i][1]));
				}
			} else {
				latlngs.push(new GLatLng(points[i][0], points[i][1]));
			}
		}
		var line = new GPolyline(latlngs);
		return line.getLength();
	},
showPointDistance:
	function(id)
	{
		// Sticky tooltips shouldn't get hidden or replaced
		if (this._distanceTooltip && this._distanceTooltip.sticky) {
			return;
		}
		this.hidePointDistance();
		if (!id) {
			return;
		}

		this._distanceTooltip = new SmallTooltip(this.data.points[id], '');
		this._distanceTooltip._pointID = id;
		this.world.map.addOverlay(this._distanceTooltip);
		
		this.updateDistanceTooltip(id);
	},
updateDistanceTooltip:
	function(id)
	{
		if (this._distanceTooltip && this._distanceTooltip._pointID == id) {
			var distance = this.getDistanceToPoint(id);
			var miles = Math.roundDP(distance / METRES2MILES, 2);
			var kms = Math.roundDP(distance / METRES2KMS, 2);

			if (!distance) {
				return;
			}

			this._distanceTooltip.setLatLng(this.getHandle(id).getLatLng());
			var message = miles + ' mile' + (miles != 1 ? 's' : '');
			message += ' / ' + kms + ' km' + (kms != 1 ? 's' : '');
			this._distanceTooltip.setMessage(message);
		}
	},
hidePointDistance:
	function()
	{
		if (this._distanceTooltip && !this._distanceTooltip.sticky) {
			this.world.map.removeOverlay(this._distanceTooltip);
			delete this._distanceTooltip;
		}
	},
fitToView:
	function()
	{
		this.world.map.setZoom(14);
	},
fit_into_view:
	function() 
	{
		this.fitToView();
	},
updateSnappedPoints:
	function()
	{
		if (!this._snappedLines) {
			return;
		}
		for (var i=0; i<this._snappedLines.length; i++) {
			var l = this._snappedLines[i];
			if (!l) {
				continue;
			}

			if (this.data.pointContent[i -1].type == 'snaptoroad') {
				var p = l.getVertex(0);
				this.data.points[i -1] = [p.lat(), p.lng()];
				if (i == 1) {
					this.anchor = p.copy();
				}

				if (this.data.points[i]) {
					var p = l.getVertex(l.getVertexCount() -1);
					this.data.points[i] = [p.lat(), p.lng()];
				}
			} else {
				//
			}
		}
	}
};




PubCrawl = new Class(Route);
PubCrawl.prototype = {
__construct:
	function(world)
	{
		Route.prototype.__construct.apply(this, arguments);
	}
};


MiniWorld = new Class;

MiniWorld.prototype = {
__construct:
	function(position, zoomlevel, container)
	{
		this.position = position;
		this.zoomlevel = zoomlevel;
		this.container = container;
		this.element = T.div();
		this.element.className = 'miniworld';
		this.container.appendChild(this.element);

		// Bodge in realworld to hide routes at start up
		this.routes_shown = true;
		// Makes all markers unclickable
		this.inert = true;


		with (this.element.style) {
			width = '100%';
			height = '100%';
		}

		// Queue for drawing markers
		this.draw_queue = new DrawQueue(this);

		// The google map
		this.map = this.initalize_map(this.element);

		// When you start dragging the map
		GEvent.bind(this.map, 'movestart', this,
			function()
			{
				this._dragging_map = true;
				if (this._update_timeout) {
					clearTimeout(this._update_timeout);
					delete this._update_timeout;
				}
			}
		);
		// When you finish dragging the map
		GEvent.bind(this.map, 'moveend', this,
			function()
			{
				delete this._dragging_map;
				if (this._routeNavigator) {
					this._routeNavigator.busy(true);
				}

				// Only request data after short delay.
				if (this._update_timeout) {
					clearTimeout(this._update_timeout);
					delete this._update_timeout;
				}
				this._update_timeout = setTimeout(GEvent.callback(this, this.update_map_items), 1000);	// 1s
			}
		);

		this.update_map_items();
	},
initalize_map:
	function(container)
	{
		var map = new GMap2(container);

		map.addMapType(G_PHYSICAL_MAP);
		map.enableContinuousZoom();
		map.enableScrollWheelZoom();

		map.setCenter(this.position, this.zoomlevel);

		return map;
	},
is_filtered:
	function()
	{
		false;
	},
update_map_items:
	function(bounds, page, no_delay)
	{

		var delay = no_delay ? 1 : 1000;

		this.cancel_update_map_items();

		this._update_delay = setTimeout(GEvent.callbackArgs(this,
			function(bounds, page) {
				if (!bounds || !(bounds instanceof GLatLngBounds)) {
					var bounds = this.map.getBounds()
				}
				var page = parseInt(page) || 0;



				var sw = bounds.getSouthWest();
				var ne = bounds.getNorthEast();


				var url = 'world/minimap_items/' + ne.toUrlValue(6) + '/' + sw.toUrlValue(6) + '/' + page;
				this._update_map_items_rpc = RPC.getJSON(url, GEvent.callback(this,
					function(data)
					{
						
						

						this.draw_queue.clear();
						this.draw_queue.add_to_queue(data.routes);
						this.draw_queue.add_to_queue(data.items);
						this.draw_queue.start();

						GEvent.trigger(this, 'getitemsend', data.page, data.length, data.total_items);

						// Must delete this, otherwise client will call abort() on it later and trigger a "readystatechange" causing this method to run again.
						delete this._update_map_items_rpc;

						
					}
				));
			},
		bounds, page), delay);
	},
cancel_update_map_items:
	function()
	{
		if (this._update_delay) {
			clearTimeout(this._update_delay);
			delete this._update_delay;
		}
		if (this._update_map_items_rpc) {
			
			GEvent.trigger(this._update_map_items_rpc, 'abort');
			GEvent.clearListeners(this._update_map_items_rpc);
			this._update_map_items_rpc.abort();
			delete this._update_map_items_rpc;
		}
	}
};



MiniPanel = new Class(FloatingPanel);
MiniPanel.prototype = {
	__construct: function(x, y, width, height, skin)
	{
		
		// All these come from base class (FloatingPanel)
		var x = x || 100;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;

		if (!this.minimumSize) {
			this.minimumSize = {
				width: 200,
				height: 70
			};
		}


		if (!this.padding) {
			this.padding = {
				left: 3,
				top: 1,
				right: 11,
				bottom: 13
			};
		}

		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/panel_small.png', 1000, 1000, w, h, 8, 16, 16, 150);
		}
		FloatingPanel.prototype.__construct.call(this, x, y, width, height);
		
		this.title.className = 'mini_panel_title';

		GEvent.clearListeners(this.closeButton, 'mouseover');
		GEvent.clearListeners(this.closeButton, 'mouseout');
		this.closeButton.className = 'mini_panel_close';
		with (this.closeButton.style) {
			position = 'absolute';
			right = (this.padding.right+10) +'px';
			top = (this.padding.top +7) +'px';
			width = '12px';
			height = '12px';
			background = 'url('+SKIN_URL+'images/delete_upload.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}

		this.title.style.height = '28px';

		this.resizable(false);

		GEvent.bind(this, 'show', this, function() {
			// FIXME : This timeout causes the panel to be sized properly in Safari
			setTimeout(GEvent.callback(this, function() { this.resize() }), 1);
		});
	}
};







var ROUTE_EDIT_WIDTH = 4;
var ROUTE_VIEW_WIDTH = 2;
var MIN_ZOOM = 11;
var MAX_ZOOM = 17;
var MEDIA_URL = "http://www.realbuzz.com/static/uploads/";
//var SKIN = 'realbuzz';
var SKIN = 'realbuzz';

var BASE_URL = '/mapyourpassion/';
//var FULL_URL = BASE_URL;
var FULL_URL = location.protocol + '//' + location.host + BASE_URL;
var STATIC_URL = '/static/maps/';
var SKIN_URL =  STATIC_URL + 'skins/' + SKIN + '/';
var SITE_URL = _OPTIONS.site_url || (location.protocol+'//'+location.host+location.pathname);
var CACHE_ITEMS = false;
var ITEMS_PER_DRAW = 3;
var SEGMENTED_POLYLINE = (IS_IE || IS_KHTML);
//var SMOOTH_PAN = (!IS_IE);
var SMOOTH_PAN = false;



RealWorld = new Class();
RealWorld.prototype = {
__construct:
	function(container)
	{
		this.container = container;
		this.container.style.position = 'relative';
		this.element = T.div();
		this.element.className = 'realworld';
		this.container.appendChild(this.element);

		// Options manager, keeps track of user preferences and saves them to a cookie for later
		this.optionsManager = new OptionsManager();
		this.optionsManager.loadFromCookie();
		window.O = this.optionsManager;


		with (this.element.style) {
			position = 'absolute';
			left = 0;
			top = 0;
			width = '100%';
			height = '100%';
		}


		this.content = T.div({'class':'world_container'});
		with (this.content.style) {
			position = 'relative';
			overflow = 'hidden';
			height = '100%';
		}
		this.element.appendChild(this.content);

		this.filters = {
			'Route': {},
			'Marker': {43: true}	// 43 = blog posts
		};

		if (_OPTIONS['unchecked_filters']) {
			for (var x in _OPTIONS['unchecked_filters']) {
				if (this.filters[x]) {
					var filters = _OPTIONS['unchecked_filters'][x];
					if (filters) {
						for (var i=0; i<filters.length; i++) {
							var f = filters[i];
							this.filters[x][f] = true;
						}
					}
				}
			}
		}

		this.map_container = T.div({'class':'map_container'});
		with (this.map_container.style) {
			height = '100%';
		}
		this.content.appendChild(this.map_container);



		// Our fancy side panel
		this.side_panel = new SidePanel(this.element, 250);
		// Check when the panel resizes and adjust the map size
		GEvent.bind(this.side_panel, 'resize', this, this.update_content_size);
		this.update_content_size(this.side_panel.width);


		// The google map
		this.map = this.initalize_map(this.map_container);

		// Drawing queue, used to keep IE ticking over rather than freezing up when drawing a lot of markers
		this.draw_queue = new DrawQueue(this);

		// The user
		this.user = new User();

		this.toolbar = new Toolbar(this);	// Toolbar must come after user

		//this._typeControl = new GHierarchicalMapTypeControl().initialize(this.map);
		this._typeControl = new ExtMapTypeControl({showTraffic: true, showTrafficKey: true}).initialize(this.map);
		this._typeControl.className = 'map_type_selection';
		// Fix to render ExtMapTypeControl properly
		GEvent.trigger(this.map, 'maptypechanged');

		this.toolbar.menu.addElement(T.td({'className':'toolbar_end'}, [this._typeControl]));


		// Set the default units for route incremental markers
		this.set_increment_unit(METRES2MILES, ' mile');

		// Stuff to do when user starts dragging the map around
		GEvent.bind(this.map, 'movestart', this,
			function()
			{
				
				this.moving = true;

				// Cancel any previous requests to get new items
				this.cancel_update_map_items();
			}
		);

		// Get new items when map has finished moving to a poision
		GEvent.bind(this.map, 'moveend', this,
			function()
			{
				
				this.moving = false;
				// TODO Clear all items not on screen, but not those on screen.
				if (this.map.getZoom() >= 11) {
					// TODO Start timer to pull in new items
					this.update_map_items();
				}
			}
		);

		// Stuff to do when user has changed zoom level
		GEvent.bind(this.map, 'zoomend', this,
			function(old_level, new_level)
			{
				console.log('zoom end');
				// Cancel any previous requests to get new items
				this.cancel_update_map_items();

				if (new_level >= 11) {
					this.update_map_items(this.map.getBounds());
				} else {
					GEvent.trigger(this, 'toofarout');
					this.draw_queue.clear();
				}
			}
		);

		// Trigger move event to cause it to update the map
		GEvent.trigger(this.map, 'zoomend');

		if (PAGE_PARAMS['item']) {
			this.activate_item(PAGE_PARAMS['item']);
		}
		if (_OPTIONS.fullscreen || PAGE_PARAMS['fullscreen']) {
			this.full_screen(true);
		}

		// Activate users options
		if (O.getOption('showroutes') || _OPTIONS['show_routes']) {
			this.show_routes();
		}

		// Add in the panels
		if (_OPTIONS['show_account_panel']) {
			this.side_panel.addPanel(new AccountPanel(this));
		}
		// this.side_panel.addPanel(new LocalWeatherPanel(this)); // Functionality not yet implemented! See LocalWeather.js
		this.side_panel.addPanel(new ShowHideFullScreenPanel(this));
		this.side_panel.addPanel(new NavigationPanel(this));
		this.side_panel.addPanel(new CreatePanel(this));
		this.side_panel.addPanel(new MyItemsPanel(this));
		this.side_panel.addPanel(new ItemsInAreaPanel(this));
		this.side_panel.addPanel(new FiltersPanel(this));
		this.side_panel.addPanel(new GPSPanel(this));
		/*
		
		this.side_panel.addPanel(new FavouritePlacesPanel(this));
		this.side_panel.addPanel(new FavouriteItemsPanel(this));
		*/

	},
delete_item:
	function(uid)
	{
		if (typeof uid == 'object' && uid.properties) {
			var uid = uid.properties.uid;
		}
		if (uid) {
			RPC.postData('action/delete_item/', 'id='+parseInt(uid), GEvent.callback(this,
				function(rpc) {
					if (rpc.responseText == '1') {
						alert('Item deleted succesfully.');
					} else {
						alert('There was an error deleting your item.');
					}
					this.update_map_items();
				}
			));
		}
	},
show_routes:
	function()
	{
		
		this.routes_shown = true;
		this.update_map_items();
		O.setOption('showroutes', true);
	},
hide_routes:
	function()
	{
		
		this.routes_shown = false;	
		this.update_map_items();
		O.setOption('showroutes', false);
	},
set_increment_unit:
	function(value, label)
	{
		this.increment_unit = {value: value, label: label};
		GEvent.trigger(this, 'incrementchange', this.route_increment_unit);
	},
activate_item:
	function(id)
	{
		RPC.getJSON('world/map_items/' + escape(id) + '/', GEvent.callback(this, this.activate_item_from_json));
	},
activate_item_from_json:
	function(item)
	{
		// Check if item is already on the map
		var my_item = this.draw_queue.items[item.properties.uid];

		// It's not on the map, so we'll add it on ourselves
		if (!my_item) {
			// Add to draw queue if it's missing
			if (!this.draw_queue.queue[item.properties.uid]) {
				this.draw_queue.queue[item.properties.uid] = item;
			}
			// Draw it
			my_item = this.draw_queue.draw_item(item.properties.uid);
		}

		// Fit item into view
		my_item.fit_into_view();
		// Turn it on
		my_item.activate();
		return my_item;
	},
get_item:
	function(id, callback)
	{
	},
full_screen:
	function(state)
	{
		if (state) {
			document.body.appendChild(this.element);
		} else {
			this.container.appendChild(this.element);
		}
		this.side_panel.fix_height();
	},
show_login:
	function(message, callback)
	{
		this.user.login(message, callback)
	},
is_blocked:
	function(type, id)
	{
		// Items that should be hidden everywhere
		if ((_OPTIONS['filters'] && _OPTIONS['filters'][type])
		&& (_OPTIONS['filters'][type].find(parseInt(id, 10)) === false)) {
			return true;
		}
		return false;
	},
is_filtered:
	function(type, id)
	{
		return (this.is_blocked(type, id) || (this.filters[type] && this.filters[type][id]))
	},
set_filter:
	function(type, id, state)
	{
		
		if (state) {
			this.draw_queue.hide_category(type, id);
		} else {
			this.draw_queue.show_category(type, id);
		}
		GEvent.trigger(this, 'changefilter', type, id, state);
		O.setOption('filter_' + type + '_' + id, state);
	},
get_filters:
	function(type)
	{
		var groups = {}
		var categories = window[type.toUpperCase() + '_CATEGORIES'];

		for (var x in categories) {
			if (isNaN(x) || !categories[x] || categories[x].private) {
				continue;
			}

			if (!this.filters[type]) {
				this.filters[type] = {};
			}

			var cat = {
				id: x,
				title: categories[x].title,
				parent: categories[x].parent,
				enabled: (!!this.filters[type][x]),
				children: []
			};

			// Create this category's group if it wasn't created by one of it's children
			if (typeof groups[x] == 'undefined') {
				groups[x] = cat;
			} else {
				cat.children = groups[x].children;
				groups[x] = cat;
			}

			if (cat.parent) {
				// create empty parent category if it's missing
				if (typeof groups[cat.parent] == 'undefined') {
					groups[cat.parent] = {children: []};
				}
				// Add this category's ID to the parent category
				groups[cat.parent].children.push(groups[x]);
			}
		}


		return groups;
	},
initalize_map:
	function(container)
	{
		var url = window.location.href;
		var realbuzz = "http://www.realbuzz.com/mapyourpassion/";
		var virgin = "http://www.realbuzz.com/route/virginlondonmarathon/map.html";
		var realbuzzStage = "http://stage.realbuzz.local/mapyourpassion/";
		
		var realbuzzMatch = url.search(realbuzz);
		var virginMatch = url.search(virgin);
		var realbuzzStage = url.search(realbuzzStage)
		
		// Only enable google search bar for our specificied urls.
		if (realbuzzMatch != -1 || virginMatch != -1 || realbuzzStage != -1) {
			if (realbuzzMatch != -1 || realbuzzStage != -1) {
				var channel_id = "7679207629";
			} else if (virginMatch != -1) {
				var channel_id = "3683734973";
			}
			
			var publisher_id = "pub-5505630038923151";
			
			// Google search bar options, passed into GMap2 constructor
			var mapOptions = {
				googleBarOptions : {
					style : "new",
					adsOptions: {
						client: publisher_id,
						channel: channel_id,
						adsafe: "high",
						language: "en"
					} 
				}
			}
			
			// var adsManagerOptions = {
			// 	maxAdsOnMap : 1,
			// 	style: 'adunit',
			// 	// The channel field is optional - replace this field with a channel number 
			// 	// for Google AdSense tracking
			// 	// channel: channel_id  
			// };
			var map = new GMap2(container,mapOptions);
			// adsManager = new GAdsManager(map, publisher_id, adsManagerOptions);
			// adsManager.enable();
		} else {
			var map = new GMap2(container);
		}

        if (!_OPTIONS['dont_save_location']) {
            var startLocation = O.getOption('startLocation', [_OPTIONS.lat, _OPTIONS.lng]);
            var startZoom = O.getOption('startZoom', _OPTIONS.zoom);
        } else {
            var startLocation = [_OPTIONS.lat, _OPTIONS.lng];
            var startZoom = _OPTIONS.zoom;
        }

		// Set to previous location
		map.setCenter(new GLatLng(parseFloat(startLocation[0]), parseFloat(startLocation[1])), parseInt(startZoom));
		
		
		// Only enable google search bar for our specificied urls.
		if (realbuzzMatch != -1 || virginMatch != -1 || realbuzzStage != -1) {
			map.enableGoogleBar();
		}
		
		map.addMapType(G_PHYSICAL_MAP);
		map.enableContinuousZoom();
		map.enableScrollWheelZoom();
		
		if (realbuzzMatch != -1 || virginMatch != -1 || realbuzzStage != -1) {
			map.addControl(new GLargeMapControl3D(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(8, 35)));
		} else {
			map.addControl(new GLargeMapControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(8, 35)));
		}
		
		map.addControl(new GScaleControl(), new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(8, 35)));
		
		this.overview = new GOverviewMapControl();
		map.addControl(this.overview);

		// This fixes the arrow getting stuck when the browser is resized
		this.overview.hide();
		this.overview.show();
		//map.addControl(new GHierarchicalMapTypeControl());

		//map.addOverlay(new GTrafficOverlay());

		return map;
	},
update_map_items:
	function(bounds, page, no_delay)
	{

		var delay = no_delay ? 1 : 1000;

		this.cancel_update_map_items();

		// We're editing stuff, ignore updates
		if (this.editing_item) {
			return;
		}

		if (this.map.getZoom() < MIN_ZOOM) {
			return;
		}
		GEvent.trigger(this, 'getitemsstart');

		this._update_delay = setTimeout(GEvent.callbackArgs(this,
			function(bounds, page) {
				if (!bounds || !(bounds instanceof GLatLngBounds)) {
					var bounds = this.map.getBounds()
				}
				var page = parseInt(page) || 0;

				var sw = bounds.getSouthWest();
				var ne = bounds.getNorthEast();

				var url = 'world/map_items/' + ne.toUrlValue(6) + '/' + sw.toUrlValue(6) + '/' + page;

				// Assemble route and item filter
				markers = _OPTIONS.filters.Marker;
				
				url_opts='routes=';

				for(x in _OPTIONS.filters.Route){
					if(typeof(_OPTIONS.filters.Route[x])=='function'){ break; }
					url_opts +=_OPTIONS.filters.Route[x]+',';
				}
				
				url_opts = url_opts.substr(0,url_opts.length-1)+"&items=";
				
				for(x in _OPTIONS.filters.Marker){
					if(typeof(_OPTIONS.filters.Marker[x])=='function'){ break; }
					url_opts +=_OPTIONS.filters.Marker[x]+',';
				}
				
				url_opts = url_opts.substr(0,url_opts.length-1);
				
				// TODO The zoom level should be a constant
				if (this.map.getZoom() > 12) {
					url += '?sponsors&'+url_opts;
				} else {
					url += '?'+url_opts
				}

				// parameter routes and markers which is a list of encoded markers and routes to return.
				this._update_map_items_rpc = RPC.getJSON(url, GEvent.callback(this,
					function(data)
					{
						
						

						this.draw_queue.clear();
						this.draw_queue.add_to_queue(data.routes);
						this.draw_queue.add_to_queue(data.items);
						this.draw_queue.start();

						GEvent.trigger(this, 'getitemsend', data.page, data.length, data.total_items);

						// Must delete this, otherwise client will call abort() on it later and trigger a "readystatechange" causing this method to run again.
						delete this._update_map_items_rpc;

						
					}
				));
			},
		bounds, page), delay);
	},
cancel_update_map_items:
	function()
	{
		if (this._update_delay) {
			clearTimeout(this._update_delay);
			delete this._update_delay;
		}
		if (this._update_map_items_rpc) {
			
			GEvent.trigger(this._update_map_items_rpc, 'abort');
			GEvent.clearListeners(this._update_map_items_rpc);
			this._update_map_items_rpc.abort();
			delete this._update_map_items_rpc;
		}
	},
update_content_size:
	function(width)
	{

		this.content.style.marginLeft = width + 'px';
		if (this.map) {
			this.map.checkResize();
		}
	},
get_visible_items:
	function()
	{
		// FIXME This will return an incomplete list if the map hasn't finished drawing
		return this.draw_queue.items;
	},
inert_everything:
	function()
	{
		var items = this.get_visible_items();
		for (x in items) {
			var item = items[x];
			item.inert();
		}
	},
uninert_everything:
	function()
	{
		var items = this.get_visible_items();
		for (x in items) {
			var item = items[x];
			item.uninert();
		}
	},
create_item:
	function(proto)
	{
		// If proto is a string then find the prototype with that name
		if (typeof proto == 'string') {
			
			proto = window[proto];
		}
		if (!proto) {
			
			return false;
		}

		// Are you logged in?
		if (!this.user.is_authenticated) {
			this.show_login('You need to log in if you wish to create new map items.', GEvent.callback(this, function() { this.create_item(proto); }));
			return false;
		}

		// Close any open popout panels
		if (BalloonPanel._single_instance_panel) {
			BalloonPanel._single_instance_panel.close(true);
		}


		// Are we already editing something?
		if (this.editing_item) {
			// TODO show confirmation to cancel editing existing item
			this.editing_item.destroy();
		}

		// Disable everything on the map
		//this.inert_everything();
		this.draw_queue.stop();
		this.draw_queue.clear(true);

		this.cancel_update_map_items();

		var item = new proto(this);
		item.start_editing();

		this.editing_item = item;

		GEvent.trigger(this, 'createitemstart', item);
		// Delete reference once we're done editing it
		GEvent.bind(item, 'endediting', this, function() { delete this.editing_item; this.update_map_items(); });
		return item;
	},
edit_item:
	function(item)
	{
		
		GEvent.trigger(this, 'startedititem', item);
		this.draw_queue.stop();
		this.draw_queue.clear(true);
		this.cancel_update_map_items();

		this.draw_queue.queue[item.properties.uid] = item;
		var item_inst = this.draw_queue.draw_item(item.properties.uid);

		// When the data has loaded, start editing
		GEvent.bind(item_inst, 'loaddataend', this,
			function()
			{
				
				GEvent.clearListeners(item_inst);
				this.editing_item = item_inst;
				item_inst.start_editing();
				GEvent.trigger(this, 'edititemstart', item_inst);
				GEvent.bind(item_inst, 'endediting', this, function() { delete this.editing_item; this.update_map_items(); });
				delete item_inst;
			}
		);
		item_inst.load_data();
		return item;
	},
jump_to_location:
	function(place_name)
	{
		// Catch UK postcode
		var pc_reg = /[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? ?[0-9][A-Za-z]{2}/;
		if (place_name.match(pc_reg)) {
			return this.jump_to_UK_postcode(place_name);
		}

		// Use Google's GeoCoder for everything else
		var geocoder = new GClientGeocoder();
		geocoder.setBaseCountryCode('GB');	// Default country is UK (ISO 3166-1)
		GEvent.trigger(this, 'startgeocode');
		geocoder.getLocations(place_name, GEvent.callback(this,
			function(response)
			{
				if (!response || response.Status.code != 200) {
					alert('Sorry, but we could not find "'+place_name+'".');
				} else {
					var place = response.Placemark[0];
					var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);

					GEvent.trigger(this, 'endgeocode');
					// work out zoom level based on accuracy of the result
					var zoom = (3*place.AddressDetails.Accuracy) + 2;
					this.jump_to_point(point, zoom);
				}
			})
		);
	},
jump_to_UK_postcode:
	function(postcode)
	{
		RPC.getJSON('action/geocode/'+escape(postcode), GEvent.callback(this,
			function(data)
			{
				if (data.error_code) {
					alert('Sorry, but we could not find "'+postcode+'".');
					return;
				}

				var point = new GLatLng(data.lat, data.lng);
				GEvent.trigger(this, 'endgeocode');
				this.jump_to_point(point ,16);
			}
		));
	},
jump_to_point:
	function(point, zoom)
	{
		if (typeof zoom != 'undefined') {
			this.map.setZoom(zoom);
		}
		if (SMOOTH_PAN) {
			this.map.panTo(point);
		} else {
			this.map.setCenter(point);
		}
	},
fit_to_points:
	function(points)
	{
		var box = new GLatLngBounds();
		for (var i=0;i<points.length;i++) {
			var p = points[i];
			if (p instanceof Array) {
				var p = new GLatLng(p[0], p[1]);
			}
			box.extend(p);
		}
		var lngCentre = (box.getNorthEast().lng() + box.getSouthWest().lng()) / 2;
		var latCentre = (box.getNorthEast().lat() + box.getSouthWest().lat()) / 2;
		var point = new GLatLng(latCentre,lngCentre);
		this.jump_to_point(point, this.map.getBoundsZoomLevel(box));
	},
save_location:
	function()
	{
        if (_OPTIONS['dont_save_location']) {
            return;
        }
		var p = this.map.getCenter();

		O.setOption('startLocation', [p.lat(), p.lng()]);
		O.setOption('startZoom', this.map.getZoom());

		this.map.savePosition();
	},



	// -- old code --
	toString: function()
	{
		return '[object RealWorld]';
	},
	showTerms: function()
	{
		var p = new ModalPanel(640, 480);
		p.resizable(false);
		p.setTitle('terms & conditions');
		var n = T.div();
		with (n.style) {
			width = '100%';
			height = '100%';
			overflow = 'auto';
		}
		n.innerHTML = '<div style="padding: 1px 20px">' + _TERMS + '</div>';
		p.setContent(n);
		p.show();
	},
	showHelp: function()
	{
		var p = new ModalPanel(640, 480);
		p.resizable(false);
		p.setTitle('how to use');
		var n = T.div();
		with (n.style) {
			width = '100%';
			height = '100%';
			overflow = 'auto';
		}
		n.innerHTML = '<div style="padding: 1px 20px">' + _HELP + '</div>';
		p.setContent(n);
		p.show();
	},
	updateMapSize: function(width)
	{
		if (!this.map) {
			return;
		}
		this.map_container.style.marginLeft = width + 'px';
		this.map.gMap.checkResize();
	},
	uploadGPS: function()
	{
		if (this.map._editing && confirm('Uploading GPS data will cancel editing the current item.\n\nDo you wish to continue?')) {
			this.map._editing.discard();
			delete this.map._editing;
		} else if (this.map._editing) {
			return;
		}

		var p = new ModalPanel(340, 100);

		var upload_frame;
		var upload_field;
		var n = T.div({'class':'panel_main_content'}, [
			upload_frame = T.iframe({'name':'gps_upload_frame', 'style': 'display:none;visibility:hidden;position:absolute;left:-10000px;'}),
			T.p('Browse to the .GPX file generated by your GPS device, it will upload automatically and create the route for you.'),
			T.form({
					'method': 'post',
					'enctype': 'multipart/form-data',
					'action': BASE_URL + 'action/upload_gps/',
					'className': 'panel_main_content',
					'name':'gps_upload_form',
					'target':'gps_upload_frame'
				}, [
				T.label({'for': 'gps_upload'}, 'GPX file'),
				upload_field = T.input({'id': 'gps_upload', 'type': 'file', 'name': 'gps_file'})
			])
		]);
		with (upload_frame.style) {
			position = 'absolute';
			left = '-1000000px';
		}

		GEvent.bindDom(upload_field, 'change', n, function() { p.busy(true); this.getElementsByTagName('form')[0].submit(); });
		// Process data uploaded
		GEvent.bindDom(upload_frame, 'load', this, function() { 
			var json = upload_frame.contentWindow.document.body.innerHTML;

			// This skips the first onload event which is called upon creating the iframe and loading about:blank
			if (upload_frame.contentWindow.location.href.indexOf('upload_gps') == -1) {
				return;
			}
			try {
				var data = eval('('+json+')');
				var points = [];
				for (var i=0; i<data.length; i++) {
					points.push(new GLatLng(data[i][0], data[i][1]));
				}
				if (points[0]) {
					var item = this.getItem(this.createItem(Route));
					this.fit_to_points(points);
					item.addLatLngs(points);
					alert('Your route has been created, enter your route details then click "save route"');
					p.close();
					delete p;
				} else {
					alert('No route data was found in your GPS file');
				}
			} catch (e) {
				
			}
			if (p) {
				p.busy(false);
			}
		});

		p.setTitle('upload gps data');
		p.setContent(n);
		p.resizable(false);
		p.show();
		p.resizeToContent(true);
	},
	/**
	 * This will return false if not found, but may also return 0 if it's the first item. So use if (foo !== false) to be sure.
	 */
	getItemID: function(item)
	{
		var found = false;
		for (var i=0; i<this.items.length; i++) {
			if (this.items[i] == item) {
				found = i;
				break;
			}
		}
		return found;
	},
	getItem: function(id)
	{
		return this.items[id];
	},
	selectItem: function(item)
	{
		if (typeof item == 'number') {
			// Item is a number, so use it as an index
			var item = this.items[item];
			if (!item) {
				return false;
			}
		} else {
			// Check item exists in our array
			if (this.getItemID(item) === false) {
				
				return false;
			}
		}
		var o = this.currentItem;
		this.currentItem = item;
		return o;
	},
	editItem: function(item)
	{
		
		for (var i=0; i<this.items.length; i++) {
			if (!this.items[i] || this.items[i] == item) {
				continue;
			}
			this.items[i].disableEditing();
		}
		item.enableEditing();
	},
	addItem: function(item)
	{
		
		var id = this.getItemID(item);
		if (id !== false) {
			
			return id;
		}
		this.items.push(item);
		id = this.getItemID(item);
		GEvent.trigger(this, 'itemadd', id);
		return id;
	},
	createItem: function(item)
	{
		if (this.map._editing && confirm('Creating a new item will cancel editing the current one.\n\nDo you wish to continue?')) {
			this.map._editing.discard();
			delete this.map._editing;
		} else if (this.map._editing) {
			return;
		}
		
		item = this.addItem(new item(this.map));
		this.selectItem(item);
		this.editItem(this.getItem(item));
		return item;
	},
	deleteItem: function(item)
	{
		
		var id = this.getItemID(item);
		if (id === false) {
			
			return false;
		}
		
		item.destroy();
		delete this.items[id];
	},
	modifyItem: function(type)
	{
		if (!type.showEditPanel) {
			
			return false;
		}

		type.showEditPanel();
	},
	handleMapClick: function(point)
	{
		
		/*
		if (!this.currentItem) return;

		this.currentItem.addLatLng(point);
		*/
	},
	/* DELETEME
	createRouteNavigator: function()
	{
		this._routeNavigator = new DockedPanel(40, 500, 150);

		this._routeNavigator = new MiniPanel(20, 40, 350, 180);
		this._routeNavigator.setTitle('recent routes');
		this._routeNavigator.closeButton.parentNode.removeChild(this._routeNavigator.closeButton);

		var searchBox;
		var incrementLink;
		var legendLink;
		var n = T.form({'className': 'mini_panel_content'}, [
			T.p('Click the flags to turn the routes on and off.'),
			T.buttonRow({'className': 'button_row'}, [
				this.map._routePrev = new Button('previous', GEvent.callback(this, function() { 
					this.updateVisibleMap(--this.page);
				}), 75).disable(),
				this.map._routeTally = T.span('Loading routes...'),
				this.map._routeNext = new Button('next', GEvent.callback(this, function() { this.updateVisibleMap(++this.page);
				}), 75).disable()
			]),
			T.div([
				T.label({'for': 'location_search'}, 'Jump to location:'),
				searchBox = T.input({'id': 'location_search', 'value':_OPTIONS.location_example})
			]),
			T.div({'class':'incremental_link'}, [
				incrementLink = T.a({'href':'javascript:void(0)'}, [
					incrementLinkText = T.span('Switch to "kms"')
				]),
				legendLink = T.a({'href':'javascript:void(0)'}, [ T.span('Show marker legend') ]),
			])

		]);
		this.map._routeTally.parentNode.style.padding = '8px 0';

		GEvent.bindDom(searchBox, 'focus', searchBox,
			function(e)
			{
				if (this.value == _OPTIONS.location_example) {
					this.value = '';
				}
			}
		);
		GEvent.bindDom(searchBox, 'blur', searchBox,
			function(e)
			{
				if (this.value == '') {
					this.value = _OPTIONS.location_example;
				}
			}
		);
		GEvent.bindDom(searchBox, 'keypress', this, function(e) {
			var e = e || window.event;
			if (e.keyCode == 13) {
				this.map.jumpToLocation(searchBox.value);
				GEvent.stop(e, true);
			}
		});
		GEvent.bindDom(incrementLink, 'click', this,
			function()
			{
				this.toggleIncrementUnit();
				switch (this._increment_unit) {
				case METRES2KMS:
					var label = 'miles';
					break;
				case METRES2MILES:
				default:
					var label = 'kms';
					break;
				}
				while (incrementLinkText.firstChild) {
					incrementLinkText.removeChild(incrementLinkText.firstChild);
				}
				incrementLinkText.appendChild(document.createTextNode('Switch to "'+label+'"'));
			}
		);
		GEvent.bindDom(legendLink, 'click', this,
			function()
			{
				this.showMarkerLegend();	
			}
		);

		this._routeNavigator.addContent(n);
		this._routeNavigator.show();
		this.setIncrementUnit(METRES2MILES);
	},
	*/
	toggleIncrementUnit: function()
	{
		 var unit = (this._increment_unit == METRES2MILES) ? METRES2KMS : METRES2MILES;
		 this.setIncrementUnit(unit);
	},
	setIncrementUnit: function(unit)
	{
		this._increment_unit = unit;

		// Redraw visible routes
		for (var x in SceneItem.items) {
			var i = SceneItem.items[x];
			if (i.properties.class_name != 'Route' || !i.enabled) {
				continue;
			}
			i.removeIncrements();
			switch (this._increment_unit) {
			case METRES2KMS:
				var label = ' km';
				break;
			case METRES2MILES:
			default:
				var label = ' mile';
				break;
			}
			i.drawIncrements(this._increment_unit, label);
		}
	},
	updateVisibleMap: function(page)
	{
		GEvent.trigger(this, 'updatestart');
		var page = page || 1;
		var map = this.map;

		/* DELETEME
		// Update the route navigation box
		if (!this._routeNavigator) {
			this.createRouteNavigator();
		}


		// We're zoomed out too far.
		if (map.gMap.getZoom() > MAX_ZOOM || map.gMap.getZoom() < MIN_ZOOM) {
			while (map._routeTally.firstChild) {
				map._routeTally.removeChild(map._routeTally.firstChild);
			}
			map._routeTally.appendChild(document.createTextNode('Zoom in closer'));
			this._routeNavigator.busy(false);
			return;
		}
		this._routeNavigator.busy(true);
		var route_nav = this._routeNavigator;
		*/

		if (map.gMap.getZoom() > MAX_ZOOM || map.gMap.getZoom() < MIN_ZOOM) {
			GEvent.trigger(this, 'updateend');
			return;
		}

		var sw = map.gMap.getBounds().getSouthWest();
		var ne = map.gMap.getBounds().getNorthEast();
		
		// FIXME TODO the MY_ITEMS filter is actually backwards, i.e. False shows only my items, True shows all items. It's a BODGE
		var myItems = '';
		if (!this.activeFilters['Route'][MY_ITEMS]) {
			if (myItems) {
				myItems += ',';
			}
			myItems += 'Route';
		}
		if (!this.activeFilters['Marker'][MY_ITEMS]) {
			if (myItems) {
				myItems += ',';
			}
			myItems += 'Marker';
		}
		if (!this.activeFilters['PubCrawl'][MY_ITEMS]) {
			if (myItems) {
				myItems += ',';
			}
			myItems += 'PubCrawl';
		}


		this._update_ident = (new Date()).getTime() + '_' + Math.round(10000*Math.random());
		if (this._show_single_item) {
			var url = 'scene/'+parseInt(page)+'/?sponsors&ident=' + this._update_ident + '&my_items='+myItems+'&sw='+sw.lat()+','+sw.lng()+'&ne='+ne.lat()+','+ne.lng();
		} else {
			var url = 'scene/'+parseInt(page)+'/?ident=' + this._update_ident + '&my_items='+myItems+'&sw='+sw.lat()+','+sw.lng()+'&ne='+ne.lat()+','+ne.lng();
		}
		// Request scene data from server
		RPC.getJSON(url, GEvent.callback(this,
			function(data)
			{

				this._current_page = data.page;
				this._per_page = data.perpage;
				this._total_routes = data.total;
				if (this._dragging_map) {
					return;
				}
				/* DELETEME
				route_nav.busy(false);
				*/
				SceneItem.unserialise(data, map, this._update_ident);
				this.map.markerManager.refresh();
				if (this._show_single_item) {
					this._show_single_item.show();
				}
				GEvent.trigger(this, 'updateend');
			}
		));

		this.page = parseInt(page);
	},
	/**
	 * @param type The type of SceneItem (Marker, Route, etc)
	 * @param cat The category ID to filter
	 * @param status True to hide items in that category, false to show them
	 * @param bounds TODO A GLatLngBounds area to only apply filters to
	 */
	setFilter: function(type, cat, status, bounds)
	{
		
		if (status) {
			this.activeFilters[type][cat] = true;
		} else {
			this.activeFilters[type][cat] = false;
			delete this.activeFilters[type][cat];
		}
		for (var x in SceneItem.items) {
			var i = SceneItem.items[x];
			if (i.properties.class_name == type && i.properties.category == cat) {
				
				status ? i.hide() : i.show();
			}
		}
		this.map.polyManager.refresh();
		this.map.markerManager.refresh();

		// Show only my items
		if (cat == MY_ITEMS) {
			this.updateVisibleMap();
		}
	},
	reapplyFilters: function(bounds)
	{
		var types = ['Marker', 'Route'];
		for (var t=0; t<types.length; t++) {
			for (var i=0; i<this.activeFilters[types[t]].length; i++) {
				if (this.activeFilters[types[t]][i]) {
					this.setFilter(types[t], i, true, bounds);
				}
			}
		}
	},
	toggleFilter: function(type, cat, bounds)
	{
		this.setFilter(type, cat, this.activeFilters[type][cat] != true);
		return this.activeFilters[type][cat] == true;
	},
	activateItem: function(item)
	{
		// TODO if item is already in memory jump to it and activate it
		// TODO if not in memory, request it and on returning jump to it and activate it
		RPC.getData('items/'+item+'/', GEvent.callback(this,
			function(rpc)
			{
				this.map.gMap.setZoom(13);
				var uid = SceneItem.unserialise(rpc.responseText, this.map);
				var item = SceneItem.items[uid];
				this.map.gMap.checkResize();
				this.updateVisibleMap();
				item.activate();
			}
		));
	},
	showMarkerLegend: function()
	{
		if (this._marker_legend) {
			this._marker_legend.close();
			delete this._marker_legend;
		}
		this._marker_legend = new MarkerLegend();
	},
	getVisibleItems: function(type)
	{
		if (type) {
			return SceneItem.visibleItems[type];
		}
		return SceneItem.visibleItems;
	}
};
function T(tag){
	 return function(){
		var e = document.createElement(tag);
		function setChild(a){
			if ("string,number".indexOf(typeof(a)) != -1) {
				e.appendChild(document.createTextNode(a.toString()));
			} else if (a instanceof Array) {
				for (var i=0; i<a.length; i++) {
					if (!a[i]) {
						continue;
					}
					if (a[i].element) {
						e.appendChild(a[i].element);
					} else {
						e.appendChild(a[i]);
					}
				}
			} else if (a == undefined) {
				return true;
			} else {
				return false;
			}
			return true;
		}

		if (!setChild(arguments[0])){
			if (tag == 'form') {
				e = document.createFormElement(arguments[0]);
			} else if (arguments[0]['name']) {
				e = document.createNamedElement(tag, arguments[0]['name']);
			}

			if (arguments[0]['className'] || arguments[0]['class']) {
				e.className = arguments[0]['className'] || arguments[0]['class'];
			}
			for (key in arguments[0]) {
				switch (key) {
				case 'checked':
					e.checked = arguments[0][key];
					break;
				case 'selected':
					e.selected = arguments[0][key];
					break;
				default:
					e.setAttribute(key=='className'?'class':key, arguments[0][key].toString());
					break;
				}
			}

			if (setChild(arguments[1])) {
				if (e.tabName == 'none') {
					return e.firstChild;
				}
				return e;
			}
			return null; // oops!
		} else {
			if (e.tabName == 'none') {
				return e.firstChild;
			}
			return e;
		}
	}
}
(function(tags){
	for (var i=0; i<tags.length; i++) {
		T[tags[i]] = T(tags[i]);
	}
	T['rating'] = function(options, buttons)
	{
		if (options['class']) {
			options['class'] += ' rating_options';
		} else {
			options['class'] = 'rating_options';
		}
		var n = T.div(options);
		var stars = T.ul({'class': 'rating_stars'});
		delete options['class'];
		options.type = 'radio';
		for (var i=1; i<=5; i++) {
			options.value = i;
			var c = T.input(options);
			c.style.display = 'none';
			n.appendChild(T.span([c]));
			var s = T.li([T.span('*')]);
			stars.appendChild(s);

			// show score on mouse over
			GEvent.bindDom(s, 'mouseover', c, function()
			{
				n.value = this.value;
				var li = stars.getElementsByTagName('li');
				for (var i=0; i<li.length; i++) {
					li[i].className = (i < this.value) ? 'active' : 'inactive';
				}
				this.checked = true;
			});
		}
		// unselect all ratings if mouse moved out
		GEvent.bindDom(stars, 'mouseout', n, function()
		{
			var inputs = this.getElementsByTagName('input');
			for (var i=0; i<inputs.length; i++) {
				inputs[i].checked = false;
			}
			var li = stars.getElementsByTagName('li');
			for (var i=0; i<li.length; i++) {
				li[i].className = 'inactive';
			}
		});
		// send rating when clicked
		GEvent.bindDom(stars, 'click', n, function()
		{
			GEvent.trigger(n, 'rate', n.value);
		});



		n.appendChild(stars);

		return n;
	};
	T['buttonRow'] = function(options, buttons)
	{
		var btns = [];
		if (options instanceof Array) {
			buttons = options;
			var options = {};
		}

		var options = options || {};
		for (var i=0; i<buttons.length; i++) {
			if (buttons[i].element) {
				btns.push(T.td([buttons[i].element]));
			} else {
				btns.push(T.td([buttons[i]]));
			}
		}
		var tbl = T.table(options, [T.tbody([T.tr(btns)])]);
		tbl.style.clear = 'both';
		return tbl;
	};
	T['video'] = function(options)
	{
		var params = [
			T.param({'name':'allowScriptAccess', 'value':'sameDomain'}),
			T.param({'name':'movie',	 'value':'/vodcasts/FlowPlayer.swf'}),
			T.param({'name':'quality',   'value':'high'}),
			T.param({'name':'scale',	 'value':'noScale'}),
			//T.param({'name':'wmode',	 'value':'transparent'}),
			T.param({'name':'flashvars', 'value':'config={videoFile: \''+options.src+'\', autoPlay: false, initialScale: \'fit\', overlayId: \'play\'}'})
		];
		var player = T.object({'type':'application/x-shockwave-flash', 'data':'/vodcasts/FlowPlayer.swf', 'width':'512', 'height':'384'});

		// This innerHTML crap is for IE, it doesn't allow appendChild on an <object> tag for a reason only Ballmer himself knows.
		var html = '<object type="application/x-shockwave-flash" data="/vodcasts/FlowPlayer.swf" width="512" height="384">';
		for (var i=0; i<params.length; i++) {
			if (params[i].outerHTML) {
				html += params[i].outerHTML;
			} else {
				player.appendChild(params[i]);
			}
		}
		html += '</object>';
		if (player.outerHTML) {
			player = T.div();
			player.innerHTML = html;
		}
		return player;
	};
})(['br', 'h1','h2','h3','h4','h5','h6','embed', 'select', 'optgroup', 'option', 'object', 'param', 'iframe', 'ul', 'ol',
	'li', 'dt', 'dl', 'dd', 'table', 'tbody', 'thead', 'tfoot', 'tr', 'td',
	'th', 'textarea', 'form', 'label', 'button', 'input', 'a', 'p', 'div',
	'span', 'em', 'sup', 'img', 'none']); 



ModalPanel = new Class(FloatingPanel);
ModalPanel.prototype = {
	__construct: function(width, height)
	{
		

		var x = document.body.clientWidth/2 - width/2;
		var y = document.body.clientHeight/2 - height/2;

		y = -100;

		FloatingPanel.prototype.__construct.call(this, x, y, width, height);

		this.movable(false);

		this.blocker = document.createElement('div');
		with (this.blocker.style) {
			position = 'absolute';
			top = 0;
			left = 0;
			width = '100%';
			height = this.container.parentNode.scrollHeight +'px';
			background = '#fff';
			zIndex = 9999999;
		}
		this.container.parentNode.parentNode.appendChild(this.blocker);
		setOpacity(this.blocker, 0.5);

		// Reposition panel if it's resized
		GEvent.bind(this, 'resize', this, this.fixPosition);

		// Reposition window if browser is resized
		GEvent.bindDom(window, 'resize', this, this.fixPosition);


		// move it's element into the body tag
		document.body.appendChild(this.container);
		this.fixPosition();

	},
	__destruct: function()
	{
		FloatingPanel.prototype.__destruct.call(this);
		if (this.blocker.parentNode) {
			this.blocker.parentNode.removeChild(this.blocker);
		}
	},
	setZ: function()
	{
		// Always on top!
		FloatingPanel.prototype.setZ.call(this, 10000000);
	},
	fixPosition: function()
	{
		try { 
			if (!this.container || !this.container.parentNode) {
				return;
			}
			var x = (document.documentElement.clientWidth/2)  - (this.getWidth()/2)  + document.body.scrollLeft;
			var y = (document.documentElement.clientHeight/2) - (this.getHeight()/2) + document.body.scrollTop;
			this.blocker.style.height = this.blocker.parentNode.scrollHeight +'px';
			this.move(x, y);
		} catch(e) {}
	}
};
Box = new Class();
Box.prototype = {
	__construct: function(png, img_w, img_h, w, h, top, left, bottom, right)
	{
		this.sizes = {
			left: left,
			top: top,
			right: right,
			bottom: bottom
		};
		// TODO these numbers will be in a skin config file
		this.segments = {};
		this.segments.top_left = PNG.getSprite(png, img_w, img_h,   0, 0, left, top);
		this.segments.top = PNG.getSprite(png, img_w, img_h,  left, 0, w-(left+right), top);
		this.segments.top_right = PNG.getSprite(png, img_w, img_h, -right, 0, right, top);
		if (bottom) {
			this.segments.mid_left = PNG.getSprite(png, img_w, img_h,   0, top, left, h-(top+bottom));
			this.segments.mid = PNG.getSprite(png, img_w, img_h,  left, top, w-(left+right), h-(top+bottom));
			this.segments.mid_right = PNG.getSprite(png, img_w, img_h, -right, top, right, h-(top+bottom));
			this.segments.bot_left = PNG.getSprite(png, img_w, img_h,   0, -bottom, left, bottom);
			this.segments.bot = PNG.getSprite(png, img_w, img_h,  left, -bottom, w-(left+right), bottom);
			this.segments.bot_right = PNG.getSprite(png, img_w, img_h, -right, -bottom, right, bottom);
		}

		this.container = document.createElement('div');
		with (this.container.style) {
			position = 'relative';
			width = w +'px';
			height = h +'px';
		}


		for (x in this.segments) {
			var s = this.segments[x];
			s.style.position = 'absolute';
			switch (x) {
			case 'top_left':
				s.style.left = 0;
				s.style.top = 0;
				break;
			case 'top':
				s.style.left = left +'px';
				s.style.top = 0;
				break;
			case 'top_right':
				s.style.right = 0;
				s.style.top = 0;
				break;
			case 'mid_left':
				s.style.left = 0;
				s.style.top = top+'px';
				break;
			case 'mid':
				s.style.left = left+'px';
				s.style.top = top+'px';
				break;
			case 'mid_right':
				s.style.right = 0;
				s.style.top = top+'px';
				break;
			case 'bot_left':
				s.style.left = 0;
				s.style.bottom = 0;
				break;
			case 'bot':
				s.style.left = left+'px';
				s.style.bottom = 0;
				break;
			case 'bot_right':
				s.style.right = 0;
				s.style.bottom = 0;
				break;
			}

			this.container.appendChild(s);
		}
	},
	resize: function(width, height)
	{
		this.container.style.width = width +'px';
		this.container.style.height = height +'px';

		this.segments.top.style.width = (width-(this.sizes.left+this.sizes.right)) +'px';
		if (this.sizes.bottom) {
			this.segments.mid.style.width = (width-(this.sizes.left+this.sizes.right)) +'px';
			this.segments.bot.style.width = (width-(this.sizes.left+this.sizes.right)) +'px';

			this.segments.mid_left.style.height = (height-(this.sizes.top+this.sizes.bottom)) +'px';
			this.segments.mid.style.height = (height-(this.sizes.top+this.sizes.bottom)) +'px';
			this.segments.mid_right.style.height = (height-(this.sizes.top+this.sizes.bottom)) +'px';
		}
	},
	setImage: function(png)
	{
		for (x in this.segments) {
			var p = this.segments[x];
			PNG.setImage(png, p.firstChild);
		}
	}
};

Uploader = new Class;
Uploader.instances = [];
Uploader.java_upload_callback = function(data)
{
	try {
		var data = eval('(' + data + ')');
		if (data.error_code) {
			
			alert('There was an error uploading your file: \n\n' + data.error_message);
			return;
		}
	} catch (err) {
		
		alert('The server generated an error');
		return;
	}

	if (isNaN(data.uploader_id)) {
		
		return;
	}
	
	var inst = this.instances[0];
	if (!inst) {
		
		return;
	}

	inst.upload_complete(data);
	GEvent.trigger(inst, 'uploadcomplete', data);


	
};
Uploader.prototype = {
__construct:
	function(target, width, height)
	{
		this.id = Uploader.instances.push(this) -1;
		this.uploadedFiles = [];
		this.use_java   = true;
		this.width	  = width  || "100%";
		this.height	 = height || 180;
		this.target_url = target || '';
		this.target_url += (this.target_url.indexOf('?') == -1) ? '?' : '&';
		this.target_url += 'uploader_id=' + this.id + '&java_uploader&sid=' + getCookie('PHPSESSID');


		this.element = this.use_java ? this.create_java_uploader() : this.create_html_uploader();
	},
upload_complete:
	function(data)
	{
		
		this.uploadedFiles = this.uploadedFiles.concat(data.media);
	},
create_java_uploader:
	function()
	{
		var element;
		element = T.div([T.object(
			{
				'name': 'uploader',
				'classid': 'java:com.thinfile.upload.ThinImageUpload.class',
				'type': 'application/x-java-applet',
				'archive': STATIC_URL + 'realbuzz_uploader.jar',
				'width': this.width, 'height': this.height
			}, [
			T.param({'name': 'archive', 'value': STATIC_URL + 'realbuzz_uploader.jar'}),
			T.param({'name': 'code', 'value': 'com.thinfile.upload.ThinImageUpload'}),
			T.param({'name': 'MAYSCRIPT', 'value': 'yes'}),
			T.param({'name': 'name', 'value': 'Realbuzz Uploader'}),
			T.param({'name': 'scriptable', 'value': 'true'}),
			T.param({'name': 'message', 'value': 'Drag files here to upload them.'}),
			T.param({'name': 'url', 'value': this.target_url}),
			T.param({'name': 'callback', 'value': 'Uploader.java_upload_callback'}),
			T.param({'name': 'props_file', 'value': 'http://www.realbuzz.com/static/thinupload.properties'})
		])]);
		if (IS_IE) {
			element.innerHTML = '\
				<object name="uploader" classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" width="' + this.width + '" height="' + this.height + '" codebase="http://java.sun.com/update/1.5.0/jinstall-1_5-windows-i586.cab#version=1,4,1"> \
					<param name="code" value="com.thinfile.upload.ThinImageUpload" /> \
					<param name="archive" value="' + STATIC_URL + 'realbuzz_uploader.jar" /> \
					<param name="MAYSCRIPT" value="yes" /> \
					<param name="name" value="Realbuzz Uploader" /> \
					<param name="scriptable" value="true"/> \
					<param name="message" value="Drag files here to upload them."/> \
					<param name="url" value="' + this.target_url + '"/> \
					<param name="callback" value="Uploader.java_upload_callback"/> \
					<param name="props_file" value="http://www.realbuzz.com/static/thinupload.properties"/> \
				</object>';
		}

		return element;
	},
create_html_uploader:
	function()
	{
	},
destroy:
	function()
	{
		try {
			for (var i=0; i<Uploader.instances.length; i++) {
				if (Uploader.instances[i] == this) {
					delete Uploader.instances[i];
					break;
				}
			}
			this.element.innerHTML = '';
		} catch(err) {}
	},
getUploadedIds:
	function()
	{
		var ids = [];
		for (var i=0; i<this.uploadedFiles.length; i++) {
			var f = this.uploadedFiles[i];
			if (f && f.id) {
				ids.push(this.uploadedFiles[i].id);
			}
		}
		return ids;
	}
};


MediaUploader = new Class(Uploader);
MediaUploader.prototype = {
__construct:
	function(width, height)
	{
		Uploader.prototype.__construct.call(this, 'http://www.realbuzz.com/static/thinupload.properties', width, height);
		//Uploader.prototype.__construct.call(this, FULL_URL + 'hotshots/upload/', width, height);
		this.uploadList = new MediaUploadList(this);
		this.element = T.div([
			T.label({'for': 'media_upload_'+this.id}, 'Upload a photo or video'),
			this.element,

			T.label({'for': 'media_uploadList_'+this.id}, 'Successfully uploaded files'),
			this.uploadList
		]);

		GEvent.bind(this, 'uploadcomplete', this.uploadList, function(data) {
			this.addItem(data['media'].id, data['media'].filename);
		});
	},
saveTags:
	function()
	{
		var tags = this.uploadList.getTags();
		
		if (!tags) {
			
			return;
		}
		GEvent.trigger(this, 'beforetagsave');

		var data = escape(serialise(tags));

		RPC.postData('hotshots/save_tags/', 'tags=' + data, this, this.saveTagsCallback);
	},
saveTagsCallback:
	function(rpc)
	{
		
		GEvent.trigger(this, 'tagsave');
	}
};

MediaUploadList = new Class;
MediaUploadList.prototype = {
__construct:
	function(uploader)
	{
		this.defaultTagText = 'Tag this photo/video';
		this.uploader = uploader;
		this._uploaded_ids = [];
		this._uploaded_list = [];
		this.element = T.ul({'className': 'uploaded_file_list', 'id': 'media_uploadList'+this.uploader.id});
	},
addItem:
	function(id, filename, tags)
	{
		var delete_button = T.a({'href':'javascript:void(0)'}, [
			T.img({'className':'uploaded_delete', 'src':SKIN_URL+'images/delete_upload.gif'})
		]);
		GEvent.bindDom(delete_button, 'click', this, function() { this.deleteItem(id); });

		var tagText = this.defaultTagText;
		if (tags) {
			// Convert tag array to string
			if (tags instanceof Array) {
				tagText = tags.join(' ');
			} else {
				tagText = tags;
			}
		}


		var tag_input = T.input({'class': (tags ? '' : 'example'), 'name': 'tag_media_upload_' + id, 'value': tagText});
		tag_input.default_text = this.defaultTagText;

		var new_row = T.li([
			T.span({'className':'uploaded_filename'}, filename),
			delete_button,
			T.span({'class': 'uploaded_tags'}, [tag_input])
		]);
		new_row.tag_input = tag_input;
		this.element.appendChild(new_row);

		GEvent.bindDom(tag_input, 'focus', tag_input,
			function(e)
			{
				if (this.value == this.default_text) {
					this.value = '';
					this.className = '';
				}
			}
		);
		GEvent.bindDom(tag_input, 'blur', tag_input,
			function(e)
			{
				if (this.value == '') {
					this.value = this.default_text;
					this.className = 'example';
				}
			}
		);

		this._uploaded_list[id] = new_row;
		this._uploaded_ids.push(id);
		return new_row;
	},
deleteItem:
	function(id)
	{
		if (!confirm('Are you sure you wish to delete this file. This cannot be undone.')) {
			return;
		}

		// POST to delete item
		RPC.postData('hotshots/delete/', 'id='+parseInt(id), GEvent.callback(this, function(rpc) {
			if (rpc.responseText == '1') {
				var l = this._uploaded_list[id];
				l.parentNode.removeChild(l);
				delete this._uploaded_list[id];
				this._uploaded_ids.splice(this._uploaded_ids.find(id), 1);
			} else {
				
				alert('There was an error deleting this media file.');
			}
		}));
	},
getTags:
	function()
	{
		if (this._uploaded_ids.length == 0) {
			return false;
		}
		var tags = {};

		for (var i=0; i<this._uploaded_ids.length; i++) {
			var id = this._uploaded_ids[i];
			var tagInput = this._uploaded_list[id].getElementsByTagName('input')[0];
			var rawTags = tagInput.value;
			// Ignore those without tags
			if (!rawTags || rawTags == this.defaultTagText) {
				tags[id] = '';
				continue;
			}
			// Replace spaces and commas with single space
			rawTags = rawTags.replace(/[ ,]+/g, ' ');
			// Remove whitespace from edges
			rawTags = rawTags.replace(/^[ ]+/g, '');
			rawTags = rawTags.replace(/[ ]+$/g, '');

			tags[id] = rawTags.split(' ');
		}
		return tags;
	}
};
Toolbar = new Class();
Toolbar.prototype = {
	__construct: function(world)
	{
		this.world = world;
		this.container = world.map_container;
		this.element = document.createElement('div');
		this.element.className = 'gmnoprint toolbar';
		this.element.style.zIndex = 10001;

		// This blocks the whole page and catches clicks to hide the menus
		// The GMap doens't register a mousedown event, that's why this is required.
		this.blocker = document.createElement('div');
		with (this.blocker.style) {
			background = 'white';
			position = 'absolute';
			left = 0;
			top = 0;
			width = '100%';
			height = '100%';
			zIndex = 10000;
			display = 'none';
		}
		setOpacity(this.blocker, 0.01);
		GEvent.bindDom(this.blocker, 'mousedown', this, function(ev) { this.hideMenus(); GEvent.stop(ev); });
		this.container.appendChild(this.blocker);


		this.container.appendChild(this.element);

		// == Main menu ==
		this.menu = new Menu(this);
		GEvent.bind(this.menu, 'hideallsubmenus', this, this.hideBlocker);
		GEvent.bind(this.menu, 'hidemenu', this, this.hideBlocker);

		// == Sub menus ==
		/*
		var mapsMenu		  = new Menu(this, false, true);
		var routesMenu		= new Menu(this, true,  true);
		var markersMenu	   = new Menu(this, true,  true);
		var filtersMenu	   = new Menu(this, false, true);
		var routesFilterMenu  = new Menu(this, true,  true);
		var markersFilterMenu = new Menu(this, true,  true);
		*/

		// == Main menu items ==
		/*
		this.menu.addLink('Home', _OPTIONS.home_link);
		var login_link = this.menu.addLink('Login', GEvent.callback(this.world, function() { this.user.login(); }));
		var logout_link = this.menu.addLink('Logout', GEvent.callback(this.world, function() { this.user.logout(); }));
		this.menu.addLink('Maps', mapsMenu);
		this.menu.addLink('Filters', filtersMenu);
		this.menu.addLink('GPS', GEvent.callback(this.world, function() { this.uploadGPS(); }), true);
		*/
		if (_OPTIONS['show_terms']) {
			this.menu.addLink('Terms', GEvent.callback(this.world, function() { this.showTerms(); }));
		}
		this.menu.addLink('How to use', GEvent.callback(this.world, function() { this.showHelp(); }));
		//this.menu.addElement(T.td({'className':'toolbar_end'}, [this.world.map._typeControl]));

		/*
		var checkUID = function(uid)
		{
			if (uid > 0) {
				login_link.style.display = 'none';
				logout_link.style.display = '';
			} else {
				login_link.style.display = '';
				logout_link.style.display = 'none';
			}
		};

		// Hide login link when user logs in
		GEvent.addListener(this.world.user, 'setuid', checkUID);
		checkUID(this.world.user.uid);


		// == mapsMenu items ==
		mapsMenu.addLink('Routes', routesMenu);
		mapsMenu.addLink('Markers', markersMenu);
		//mapsMenu.addLink('Advertise');

		// == routesMenu items ==
		//routesMenu.addLink('Search');
		routesMenu.addLink('Create', GEvent.callback(this.world, function () { this.createItem(Route); }), true);
		routesMenu.addLink('Edit', GEvent.callback(this.world, function () { this.modifyItem(Route); }), true);
		//routesMenu.addLink('Delete');

		// == markersMenu items ==
		//markersMenu.addLink('Search');
		markersMenu.addLink('Create', GEvent.callback(this.world, function () { this.createItem(Marker); }), true);
		markersMenu.addLink('Edit', GEvent.callback(this.world, function () { this.modifyItem(Marker); }), true);
		//markersMenu.addLink('Delete');

		// == filtersMenu items ==
		filtersMenu.addLink('Routes', routesFilterMenu);
		filtersMenu.addLink('Markers', markersFilterMenu);

		// If PubCrawls are enabled, show them
		if (_OPTIONS.show_pub_crawl) {
			var pubCrawlMenu = new Menu(this, true, true);
			mapsMenu.addLink('Pub Crawls', pubCrawlMenu);
			// == pubCrawlMenu items ==
			//pubCrawlMenu.addLink('Search');
			pubCrawlMenu.addLink('Create', GEvent.callback(this.world, function () { this.createItem(PubCrawl); }), true);
			pubCrawlMenu.addLink('Edit', GEvent.callback(this.world, function () { this.modifyItem(PubCrawl); }), true);
			//pubCrawlMenu.addLink('Delete');
		}



		// == routesFiltersMenu items ==
		var subMenus = [];
		var link = routesFilterMenu.addToggle('Show only my routes', GEvent.callback(this.world,
			function()
			{
				return this.toggleFilter('Route', MY_ITEMS);
			}
		));
		link.firstChild.style.fontWeight = 'bold';
		for (var x in ROUTE_CATEGORIES) {
			if (parseInt(x) != x) {
				continue;
			}
			var c = ROUTE_CATEGORIES[x];

			// Cant filter this category
			if (!c.filterable) {
				continue;
			}


			if (!c.parent) {
				// If it has no parent, it's a top level item
				
				if (c.is_parent) {
					if (!subMenus[x]) { 
						// If this is a parent and the subMenu hasn't been created, then do so...
						subMenus[x] = new Menu(this, true, true);
					}
					// If it's a parent, then attach it's submenu
					routesFilterMenu.addSubMenu(c.title, subMenus[x]);
				} else {
					// Add a checkbox link
					var menuItem = routesFilterMenu.addToggle(c.title, GEvent.callback([this.world, x], function() {
						return this[0].toggleFilter('Route', this[1]);
					}), !this.world.activeFilters['Route'][x]);	// True  =  checked by default
				}
			} else {
				// It has a parent so add it to the submenu.
				if (!subMenus[c.parent]) {
					subMenus[c.parent] = new Menu(this, true, true);
				}
				// Add a checkbox link
				subMenus[c.parent].addToggle(c.title, GEvent.callback([this.world, x], function() {
					return this[0].toggleFilter('Route', this[1]);
				}), true);	// True  =  checked by default
			}
		}

		// == markersFiltersMenu items ==
		var link = markersFilterMenu.addToggle('Show only my markers', GEvent.callback(this.world,
			function()
			{
				return this.toggleFilter('Marker', MY_ITEMS);
			}
		));
		link.firstChild.style.fontWeight = 'bold';
		for (var x in MARKER_CATEGORIES) {
			if (parseInt(x) != x) {
				continue;
			}

			// Cant filter this category
			if (!MARKER_CATEGORIES[x].filterable) {
				continue;
			}
			markersFilterMenu.addToggle(MARKER_CATEGORIES[x].title, GEvent.callback([this.world, x],
				function() {
					return this[0].toggleFilter('Marker', this[1]);
				}
			), !this.world.activeFilters['Marker'][x]);	// True  =  checked by default
		}
		markersFilterMenu.element.className = 'marker_filters';
		*/



	},
	toString: function()
	{
		return '[object Toolbar]';
	},
	showBlocker: function()
	{
		this.blocker.style.display = 'block';
	},
	hideBlocker: function()
	{
		this.blocker.style.display = 'none';
	},
	hideMenus: function()
	{
		this.menu.hideSubMenus();
		this.hideBlocker();
	}
};
TabContainer = new Class();
TabContainer.prototype = {
	__construct: function(panel)
	{
		this.tabs = [];
		this.activeTab = false;
		this.tabContent = document.createElement('div');
		this.tabBar = T.table([T.thead([T.tr()])]);
		this.tabBar.className = 'tab_bar';
		this.element = document.createElement('div');
		this.element.className = 'tab_panel';

		this.element.appendChild(this.tabBar);
		this.element.appendChild(this.tabContent);

		this.tabBar = this.tabBar.firstChild.firstChild;	// set tabbar to the <TR>

		this.setPanel(panel);
	},
	setPanel: function(panel)
	{
		this.panel = panel;
		if (this._resizeListeners) {
			for (var i=0; i<this._resizeListeners.length; i++) {
				GEvent.removeListener(this._resizeListeners[i]);
				delete this._resizeListeners[i];
			}
			delete this._resizeListeners;
		}
		if (!panel) return;

		// Listen for resizes
		if (panel) {
			this._resizeListeners = [
				GEvent.bind(panel, 'resize', this, this.updateSize),
				GEvent.bind(panel, 'beforeresizetocontent', this, function() {
					this.element.style.height = 'auto';
				})
			];
		}
	},
	addTab: function(name)
	{
		// Create the tab
		var tab = document.createElement('th');
		tab.appendChild(document.createElement('a'));
		tab.firstChild.appendChild(document.createTextNode(name));
		this.tabBar.appendChild(tab);

		// Create the tab content container
		tab.content = document.createElement('div');
		tab.content.className = 'tab_content';
		this.tabContent.appendChild(tab.content);

		// Add to tabs array
		this.tabs.push(tab);

		if (this.tabs.length == 1) {
			// If this is the first tab, then select it
			this.setTab(0);
		} else {
			// If not then hide it
			this.setTab(this.activeTab);
		}

		var tabID = this.tabContent.childNodes.length -1;
		
		// Add click event to tab
		GEvent.bindDom(tab.firstChild, 'click', this, function() { this.setTab(tabID); });

		this.updateSize();

		GEvent.trigger(this, 'addtab', this, tabID);

		tab.tabID = tabID;

		// Return it's ID
		return tabID;
	},
	setTab: function(id)
	{
		for (var i=0; i<this.tabs.length; i++) {
			this.tabs[i].className = 'tab_inactive';
			this.tabs[i].content.style.display = 'none';
		}
		this.tabs[id].className = 'tab_active';
		this.tabs[id].content.style.display = 'block';
		this.activeTab = id;

		GEvent.trigger(this, 'tabchange', this, id);
		if (this.panel && this.panel.resizeToContent) {
			this.panel.resizeToContent(true);
			// FIXME: the +80 is a hack, it needs to cacluate the true height required
			//this.panel.resize(false, this.tabs[id].content.scrollHeight+80, true);
		}
	},
	setContent: function(id, node)
	{
		while (this.tabs[id].content.firstChild) {
			this.tabs[id].content.removeChild(this.tabs[id].content.firstChild);
		}
		this.tabs[id].content.appendChild(node);
	},
	addContent: function(id, node)
	{
		this.tabs[id].content.appendChild(node);
	},
	updateSize: function(panel)
	{
		var panel = panel || this.panel;
        if (!panel.getContentWidth) {
            return;
        }
		

		//this.element.style.background = 'blue';

		// Set width
		var width = panel.getContentWidth();
		this.element.style.width = width+'px';

		// Calculate tabSize (we can't use percentages because IE sucks at rounding them)
		var tabSize = Math.floor(width/this.tabs.length);
		var remainder = width-(tabSize*this.tabs.length);
		for (var i=0; i<this.tabs.length; i++) {
			var thisTab = tabSize;
			if (remainder-- > 0) thisTab++;
			this.tabs[i].style.width = thisTab +'px';
		}

		// Height is set after width incase tha tabs height changes due to word wrapping
		var height = panel.getContentHeight() - this.tabBar.offsetHeight;
		if (height < 48) {
			height = 48;
		}
		this.element.style.height = height+'px';
	}
};
/**
 * This class is just to help the interface, it does not replace any server
 * side user authentication as the client side can easily be hax0red
 */
User = new Class();
User.prototype = {
	__construct: function(uid)
	{
		this.setUID(uid);
		this.username = '';
	},
	toString: function()
	{
		return '[object User]';
	},
	login: function(message, callback)
	{
		if (!this.loginPanel) {
			this.loginPanel = new ModalPanel(300, 100);
			GEvent.bind(this.loginPanel, 'close', this, function() { delete this.loginPanel; });

			this.loginPanel.setTitle('log in...');

			if (message) {
				var pmessage = T.p(message);
			}


			var submitForm = function() {
				this.loginPanel.busy(true);
				this.do_login(username_field.value, password_field.value, callback, message);
			};
		
			var username_field = T.input({'id': 'login_user', 'type': 'text', 'name': 'username'});
			// Jump to password field on return key
			username_field.onkeypress = GEvent.callback(this, function(e) {
				var e = e || window.event;
				if (e.keyCode == 13) {
					password_field.focus();
				}
			});

			var password_field = T.input({'id': 'login_pass', 'type': 'password', 'name': 'password'});
			// Submit form on return key
			password_field.onkeypress = GEvent.callback(this, function(e) {
				var e = e || window.event;
				if (e.keyCode == 13) {	// Return
					submitForm.call(this);
				}
			});
			var n = T.form({'className': 'panel_main_content'}, [
				pmessage,
				T.p('Enter your username and password below. If you don\'t have an account click "sign up" to create one.'),
				T.label({'for': 'login_user'}, 'Username'),
				username_field,
				T.label({'for': 'login_pass'}, 'Password'),
				password_field,
				T.buttonRow({'className': 'button_row'}, [
					new Button('Log in', GEvent.callback(this, submitForm)),
					new Button('Sign up', function() { window.open(_OPTIONS.register_url); })
				])
			]);
			n.onsubmit = GEvent.stop;
			this.loginPanel.setContent(n);
		}

		this.loginPanel.show();
		username_field.focus();

		this.loginPanel.resizeToContent(true);
	},
	logout: function()
	{
		this.setUID(0);
		RPC.postData('logout/');
		alert('You have been logged out.');
		GEvent.trigger(this, 'logout');
	},
do_login:
	function(username, password, callback, message)
	{
		
		RPC.postData('action/login/', 'username='+escape(username)+'&password='+escape(password), GEvent.callback(this,
			function(rpc)
			{
				if (parseInt(rpc.responseText) > 0) {
					this.setUID(parseInt(rpc.responseText));
					// Login was successful
					GEvent.trigger(this, 'login');
					if (typeof callback == 'function') {
						callback(this.uid);
					}
				} else {
				var error = new ModalDialogue('Error', 'Your username or password was incorrect, please try again.');
					if (this.loginPanel) {
						error.addButton('Cancel', GEvent.callback(this, function() { error.close(); }));
						error.addButton('Try again', GEvent.callback(this, function() { error.close(); this.login(message, callback); }), true);
					} else {
						error.addButton('Close', GEvent.callback(this, function() { error.close(); }), true);
					}
					error.show();
					GEvent.trigger(this, 'loginfail');
				}
				if (this.loginPanel) {
					this.loginPanel.close(true);
				}
			}
		));
	},
	setUID: function(uid)
	{
		this.uid = uid || false;
		this.is_authenticated = (this.uid > 0);
		GEvent.trigger(this, 'setuid', uid);
	},
show_profile:
	function(username)
	{
		
		var popup = new ModalPanel(534, 475);
		var profile = new ProfileView(username);
		popup.setTitle(username + '\'s profile');
		popup.setContent(T.div({'class': 'profile_view'}, [profile.element]));
		popup.show();
		profile.create_profile();
	},
profile_link:
	function(username)
	{
		var username = username || this.username;
		var el = T.a({'href': 'javascript:void(0)', 'title': 'View ' + username + '\'s profile'}, username);

		GEvent.bindDom(el, 'click', this,
			function()
			{
				this.show_profile(username);
			}
		);
		return el;
	}
};
PostItPanel = new Class;
PostItPanel.panels = [];
PostItPanel.prototype = {
__construct:
	function(x, y, w, h, title, message)
	{
		PostItPanel.panels.push(this);
		var pad = 8;

		this.element = T.div({'class': 'post_it'});
		this.close_button   = T.div({'class': 'post_it_close'}, 'X');
		this.title   = T.div({'class': 'post_it_title'});
		this.content = T.div({'class': 'post_it_content'});
		this.element.appendChild(this.title);
		this.element.appendChild(this.close_button);
		this.element.appendChild(this.content);

		this.real_x = x;
		this.real_y = y;

		GEvent.bindDom(this.element, 'click', this, this.show);
		GEvent.bindDom(this.close_button, 'click', this,
			function(e)
			{
				this.destroy();
				GEvent.stop(e, true);
			}
		);

		with (this.element.style) {
			position = 'absolute';
			right    = x + 'px';
			bottom   = y + 'px';
			width    = w + 'px';
			height   = h + 'px';
			display  = 'none';
			zIndex   = 99999;
		}
		with (this.close_button.style) {
			position = 'absolute';
			right     = pad + 'px';
			top      = pad + 'px';
			width    = '16px';
			height   = '16px';
		}
		with (this.title.style) {
			position = 'absolute';
			left     = pad + 'px';
			top      = pad + 'px';
			width    = (w -(pad *2)) + 'px';
			height   = '20px';
			overflow = 'auto';
		}
		with (this.content.style) {
			position = 'absolute';
			left     = pad + 'px';
			top      = (pad + 20) +'px';
			width    = (w -(pad *2)) + 'px';
			height   = (h -(pad *2) -20) + 'px';
			overflow = 'auto';
		}


		if (title) {
			this.set_title(title);
		}
		if (message) {
			this.set_content(message);
		}
	},
set_content:
	function(content)
	{
		while (this.content.firstChild) {
			this.content.removeChild(this.content.firstChild);
		}

		this.content.appendChild(content);
	},
set_title:
	function(title)
	{
		while (this.title.firstChild) {
			this.title.removeChild(this.title.firstChild);
		}

		this.title.appendChild(document.createTextNode(title));
	},
show:
	function()
	{
		this.element.style.zIndex = 99999;
		this.element.style.display = 'block';
		this.element.style.right = this.real_x +'px';
		this.element.style.bottom = this.real_y +'px';

		for (var i=0; i<PostItPanel.panels.length; i++) {
			var p = PostItPanel.panels[i];
			if (!p || p == this) {
				continue;
			}
			p.element.style.right = (p.real_x + 16) + 'px';
			p.element.style.bottom = (p.real_y - 16) + 'px';
			p.element.style.zIndex = 99998;
		}
	},
hide:
	function()
	{
		this.element.style.display = 'none';
	},
destroy:
	function()
	{
		if (this.element.parentNode) {
			this.element.parentNode.removeChild(this.element);
		}
		for (var i=0; i<PostItPanel.panels.length; i++) {
			var p = PostItPanel.panels[i];
			if (!p || p == this) {
				continue;
			}
			p.show();
			break;
		}
		for (var i=0; i<PostItPanel.panels.length; i++) {
			var p = PostItPanel.panels[i];
			if (!p || p != this) {
				continue;
			}
			PostItPanel.panels.splice(i, 1);
		}
		GEvent.trigger(this, 'destroy');
	}
};
Gallery_old = new Class();
Gallery_old.prototype = {
	__construct: function(panel, id) {
		this.element = document.createElement('div');
		this.element.className = 'gallery_panel no_media';

		if (panel) {
			this.setPanel(panel);
		}

		if (id) {
			this.loadMarkerGallery(id);
		}
	},
	setPanel: function(panel)
	{
		this.panel = panel;
		if (this._resizeListener) {
			GEvent.removeListener(this._resizeListener);
			delete this._resizeListener;
		}
		if (!panel) {
			return;
		}

		// Listen for resizes
		if (panel) {
			this._resizeListener = GEvent.bind(panel, 'resize', this, this.updateSize);
		}
	},
	updateSize: function(panel)
	{
		var panel = panel || this.panel;
		if (!panel) {
			return false;
		}
	},
	loadMarkerGallery: function(id)
	{
		if (!id) {
			
			return false;
		}
		this.panel.busy(true);
		RPC.getData('action/get_gallery/'+id+'/', GEvent.callback(this, function(rpc) {
			this.buildGallery();
			try {
				var data = eval('(' + rpc.responseText + ')');
			} catch(e) {
				
				alert('There was an error loading this gallery');
				
				this.panel.busy(false);
				return false;
			}

			

			// FIXME: Some of this is just hacked in rather than using proper methods
			if (data && data['media']) {
				// Marker has some images to show
				for (var i=0; i<data['media'].length; i++) {
					var double_size = (data['media'].length == 1);
					if (data['media'][i].type == 'IMAGE' && double_size) {
						data['media'][i].thumb_filename = data['media'][i].large_filename;
					}
					this.addMedia(data['media'][i].type, data['media'][i].thumb_filename, data['media'][i].large_filename, data['user'].username, double_size);
				}
			} else {
				// Marker lacks images
			}
            this.panel.sponsor = data['sponsor'];

			if (!data['sponsor']) {
				// Set userlink
				while (this.userlink.firstChild) {
					this.userlink.removeChild(this.userlink.firstChild);
				}
				this.userlink.appendChild(T.span('By '));
				this.userlink.appendChild(T.a({'href':'http://realbuzz.com/en-gb/users/'+escape(data['user'].username),'target':'_blank'}, data['user'].username));
			}
			// Set description
			while (this.description.firstChild) {
				this.description.removeChild(this.description.firstChild);
			}
			if (data.html) {
				// FIXME: Inner HMTL is evil!
				this.description.innerHTML = data.description;
				GEvent.trigger(this, 'addhtml');
			} else {
				this.description.appendChild(document.createParagraph(data.description));
			}
			this.panel.busy(false);
		}));
	},
	setDescription: function(desc)
	{
		if (!this.description) {
			this.buildGallery();
		}
		while (this.description.firstChild) {
			this.description.removeChild(this.description.firstChild);
		}
		this.description.appendChild(document.createParagraph(desc));
	},
	buildGallery: function()
	{
		this.userlink = T.div({'className':'gallery_user'}, '');
		this.element.appendChild(this.userlink);
		this.description = T.div({'className':'gallery_description'}, 'Description goes here');
		this.element.appendChild(this.description);
		this.media = T.ul({'className':'gallery_media'});
		this.element.appendChild(this.media);
		if (this.panel) {
			this.panel.content.style.overflow = 'auto';
		}
	},
	addMedia: function(type, src, big_src, username, double_size)
	{
		if (!this.media) {
			
			return false;
		}
		GEvent.trigger(this, 'addmedia');
		if (this.element.className != 'gallery_panel has_media') {
			this.element.className = 'gallery_panel has_media';
		}
		var img_class = double_size ? ' double_size' : ' normal_size';
		this.media.className += img_class;

		var img = T.li([
			T.a({'href':'javascript:void(0)'}, [
				T.img({'src':MEDIA_URL+src})
			])
		]);

		// Triggered when media is clicked, shows the item in a popup
		GEvent.bindDom(img.firstChild, 'click', this,
			function() {
				switch (type) {
				case 'IMAGE':
					var media = T.div({'className':'gallery_image'}, [
							T.img({'src':MEDIA_URL+big_src})
						]);
					break;
				case 'VIDEO':
					var media = T.div({'className':'gallery_image'}, [
							T.video({'src':MEDIA_URL+big_src})
						]);
					break;
				default:
					
					break;
				}
				var popup = new ModalPanel(534, 475);
				var content = T.div({'className':'gallery_view'}, [
					media,
					T.div({'className':'gallery_user'}, [
						T.span('By '),
						T.a({'href':'http://realbuzz.com/en-gb/users/'+escape(username),'target':'_blank'}, username)
					])
				]);
				popup.setContent(content);
				popup.setTitle('');
				popup.resizable(false);
				popup.show();

				// XXX This hack is for IE, without it the flash video player
				// doesn't get destroyed and will play in the background forever
				GEvent.bind(popup, 'close', this,
					function() {
						try { 
							media.innerHTML = '';
						} catch(e) {}
						delete media;
					}	// function
				);
			}	// function
		);
		this.media.appendChild(img);
	}
};
DrawQueue = new Class;
DrawQueue.prototype = {
__construct:
	function(world)
	{
		this.world = world;

		// Items on the map
		this.items = {};

		this.started = false;

		this.queue = {};
		this._queue_ids = [];
		this._total_drawn = 0;
		this._current_item = 0;
	},
populate:
	function(items)
	{
		this.clear();
		this.add_to_queue(items);
	},
add_to_queue:
	function(items)
	{
		for (x in items) {
			this.queue[x] = items[x];
			this._queue_ids.push(x);
		}
	},
show_category:
	function(type, id)
	{
		if (!this.world.filters[type]) {
			this.world.filters[type] = {};		   
		}
		this.world.filters[type][id] = false;
		this.update_catetgory(type, id);
	},
/**
 * @param MapItem item This is a complete map item
 */
add_item:
	function(item)
	{
		if (this.items[item.properties.uid]) {
			return this.items[item.properties.uid];
		}
		this.items[item.properties.uid] = item;
		this.queue[item.properties.uid] = {anchor: [item.anchor.lat(), item.anchor.lng()], properties: item.properties, data: item.data};
		this._queue_ids.push(item.properties.uid);
		return this.items[item.properties.uid]; 
	},
hide_category:
	function(type, id)
	{
		if (!this.world.filters[type]) {
			this.world.filters[type] = {};		   
		}
		this.world.filters[type][id] = true;
		this.update_catetgory(type, id);
	},
update_catetgory:
	function(type, id)
	{
		for (x in this.items) {
			var i = this.items[x];
			if (i.properties.class_name == type && i.properties.category == id) {
				if (this.world.filters[type][id]) {
					i.hide();
				} else {
					i.show();
				}
			}
		}
	},
draw_item:
	function(id)
	{
		GEvent.trigger(this, 'drawitemstart');
		var item = this.queue[id];
		if (item && item.properties) {
			// If item isn't in cache...
			if (!this.items[item.properties.uid]) {
				// If item isn't cached, then create it

				var hidden = (!this.world.routes_shown && (item.properties.class_name == 'Route' || item.properties.class_name == 'PubCrawl'));
				hidden = hidden || this.world.is_filtered(item.properties.class_name, item.properties.category);

				var item_inst = MapItem.create_from_json(item, this.world, hidden);

				// Item created successfully
				if (item_inst) {
					this.items[item_inst.properties.uid] = item_inst;
				}
			} else {
				// Item is already cached
				//
				this.items[item.properties.uid].show();
				var item_inst = this.items[item.properties.uid];
			}
		} else {
			
			var item_inst = false;
		}
		if (item && !this.world.is_blocked(item.properties.class_name, item.properties.category)) {
			this._total_drawn++;
			GEvent.trigger(this, 'drawitemend', item_inst);
		}

		if (!item_inst) {
			return false;
		}


		return item_inst;
	},
draw_next:
	function(noevent)
	{
		var item = this.draw_item(this._queue_ids[this._current_item]);
		if (item.properties) {
			//
		}
		this._current_item++;

		if (this._current_item < this._queue_ids.length && this._current_item % ITEMS_PER_DRAW != 0) {
			this.draw_next(true);
		}

		if (noevent !== true) {
			GEvent.trigger(this, 'drawnextend', item);
		}
		return item;
	},
start:
	function()
	{
		GEvent.trigger(this, 'start');
		
		this._total_drawn = 0;
		this.started = true;
		this._tick_event = GEvent.bind(this, 'drawnextend', this,
			function()
			{
				if (this.started && this._current_item < this._queue_ids.length) {
					this._timer = setTimeout(GEvent.callback(this, this.draw_next), 1);
				} else if (this.started) {
					this.stop();
				}
			}
		);
		this._timer = setTimeout(GEvent.callback(this, this.draw_next), 1);
	},
stop:
	function()
	{
		this.started = false;
		if (this._timer) {
			clearTimeout(this._timer);
			delete this._timer;
		}
		if (this._tick_event) {
			GEvent.removeListener(this._tick_event);
			delete this._tick_event;
		}
		this.queue = {};
		GEvent.trigger(this, 'stop', this._total_drawn);
		
	},
clear:
	function(force)
	{
		this.stop();
		// TODO Intelligently work out which markers need hiding/removing
		for (x in this.items) {
			var item = this.items[x];
			if (!force && item.sticky) {
				continue;
			}
			if (CACHE_ITEMS) {
				item.hide();
			} else {
				item.destroy();
				delete this.items[x];
				delete item;
			}
		}

		this.queue = {};
		this._queue_ids = [];
		this._current_item = 0;
	}
};
LabelBox = new Class();
LabelBox.prototype = {
	__construct: function(label, width, height)
	{
		var w = width || 110;
		var h = height || 24;
		this.box = new Box(SKIN_URL+'images/button.png', 512, 512, w, h, 4, 4, 4, 4);
		this.element = this.box.container;

		this.container = document.createElement('div');
		with (this.container.style) {
			position = 'relative';
			width = '100%';
			height = '100%';
			lineHeight = h +'px';
			textAlign = 'center';
		}
		if (label) {
			this.container.appendChild(document.createTextNode(label));
		}

		this.element.appendChild(this.container);
	}
};



var METRES2MILES = 1609.344;
var METRES2KMS = 1000;
var IS_IE = (navigator.userAgent.indexOf('MSIE') != -1 && navigator.userAgent.indexOf('Opera') == -1);
var IS_IE6 = (navigator.userAgent.indexOf('MSIE 6') != -1 && navigator.userAgent.indexOf('Opera') == -1);
var IS_IE7 = (navigator.userAgent.indexOf('MSIE 7') != -1 && navigator.userAgent.indexOf('Opera') == -1);
var IS_KHTML = (navigator.userAgent.indexOf('KHTML') != -1);
var IS_GECKO = (navigator.userAgent.indexOf('Gecko') != -1);



var MY_ITEMS = -1;
var PAGE_PARAMS = httpGetString();
//		{{{ Enum: RW_MODE
var RW_MODE_BROWSE = 0;
var RW_MODE_CREATE = 1;
var RW_MODE_EDIT   = 2;
var RW_MODE_ADMIN  = 3;
//		}}} Enum: RW_MODE
//		{{{ Enum: RW_ROUTE
var RW_ROUTE_RUNNING = 1;
var RW_ROUTE_CYCLING = 2;
var RW_ROUTE_WALKING = 3;
var RW_ROUTE_HIKING  = 4;
//		}}} Enum: RW_ROUTE
//		{{{ Enum: ERROR
var ERROR_LOGIN_REQUIRED = 1;
var ERROR_ALREADY_RATED  = 5;
//		}}} Enum: ERROR
//		{{{ MARKER_CATEGORIES
var MARKER_CATEGORIES = {
	
	54: {
		filterable: true,
		private: false,
		icon: 'icon_75',
		title: 'Activity centres',
		parent: 0
	},
	
	32: {
		filterable: true,
		private: false,
		icon: 'icon_38',
		title: 'Aeroplane',
		parent: 0
	},
	
	55: {
		filterable: true,
		private: false,
		icon: 'icon_76',
		title: 'Alternative medicine',
		parent: 0
	},
	
	4: {
		filterable: true,
		private: false,
		icon: 'icon_9',
		title: 'Ambulance',
		parent: 0
	},
	
	56: {
		filterable: true,
		private: false,
		icon: 'icon_77',
		title: 'Athletics',
		parent: 0
	},
	
	5: {
		filterable: true,
		private: false,
		icon: 'icon_10',
		title: 'Bank',
		parent: 0
	},
	
	2: {
		filterable: true,
		private: false,
		icon: 'icon_2',
		title: 'Bar',
		parent: 0
	},
	
	43: {
		filterable: true,
		private: false,
		icon: 'icon_49',
		title: 'Blog post',
		parent: 0
	},
	
	57: {
		filterable: true,
		private: false,
		icon: 'icon_78',
		title: 'Boxing',
		parent: 0
	},
	
	6: {
		filterable: true,
		private: false,
		icon: 'icon_11',
		title: 'Bureau de Change',
		parent: 0
	},
	
	7: {
		filterable: true,
		private: false,
		icon: 'icon_12',
		title: 'Bus',
		parent: 0
	},
	
	8: {
		filterable: true,
		private: false,
		icon: 'icon_13',
		title: 'Cafe',
		parent: 0
	},
	
	9: {
		filterable: true,
		private: false,
		icon: 'icon_14',
		title: 'Campsite',
		parent: 0
	},
	
	10: {
		filterable: true,
		private: false,
		icon: 'icon_15',
		title: 'Car',
		parent: 0
	},
	
	11: {
		filterable: true,
		private: false,
		icon: 'icon_16',
		title: 'Cinema',
		parent: 0
	},
	
	12: {
		filterable: true,
		private: false,
		icon: 'icon_17',
		title: 'Club',
		parent: 0
	},
	
	13: {
		filterable: true,
		private: false,
		icon: 'icon_18',
		title: 'College / University',
		parent: 0
	},
	
	58: {
		filterable: true,
		private: false,
		icon: 'icon_79',
		title: 'Cricket',
		parent: 0
	},
	
	59: {
		filterable: true,
		private: false,
		icon: 'icon_80',
		title: 'Cycling',
		parent: 0
	},
	
	60: {
		filterable: true,
		private: false,
		icon: 'icon_81',
		title: 'Dance studio',
		parent: 0
	},
	
	14: {
		filterable: true,
		private: false,
		icon: 'icon_19',
		title: 'Dentist',
		parent: 0
	},
	
	61: {
		filterable: true,
		private: false,
		icon: 'icon_82',
		title: 'Diet meetings',
		parent: 0
	},
	
	15: {
		filterable: true,
		private: false,
		icon: 'icon_20',
		title: 'Doctor',
		parent: 0
	},
	
	62: {
		filterable: true,
		private: false,
		icon: 'icon_83',
		title: 'Dojo',
		parent: 0
	},
	
	63: {
		filterable: true,
		private: false,
		icon: 'icon_84',
		title: 'Dry ski',
		parent: 0
	},
	
	16: {
		filterable: true,
		private: false,
		icon: 'icon_21',
		title: 'Event',
		parent: 0
	},
	
	64: {
		filterable: true,
		private: false,
		icon: 'icon_85',
		title: 'Extreme sports',
		parent: 0
	},
	
	65: {
		filterable: true,
		private: false,
		icon: 'icon_86',
		title: 'Farmers market',
		parent: 0
	},
	
	17: {
		filterable: true,
		private: false,
		icon: 'icon_22',
		title: 'Ferry',
		parent: 0
	},
	
	18: {
		filterable: true,
		private: false,
		icon: 'icon_23',
		title: 'Fire service',
		parent: 0
	},
	
	66: {
		filterable: true,
		private: false,
		icon: 'icon_87',
		title: 'Football',
		parent: 0
	},
	
	1: {
		filterable: true,
		private: false,
		icon: 'default',
		title: 'General / Miscellaneous',
		parent: 0
	},
	
	67: {
		filterable: true,
		private: false,
		icon: 'icon_88',
		title: 'Golf',
		parent: 0
	},
	
	19: {
		filterable: true,
		private: false,
		icon: 'icon_24',
		title: 'Government',
		parent: 0
	},
	
	20: {
		filterable: true,
		private: false,
		icon: 'icon_25',
		title: 'Gym',
		parent: 0
	},
	
	68: {
		filterable: true,
		private: false,
		icon: 'icon_89',
		title: 'Healthy eating',
		parent: 0
	},
	
	69: {
		filterable: true,
		private: false,
		icon: 'icon_90',
		title: 'Hockey',
		parent: 0
	},
	
	21: {
		filterable: true,
		private: false,
		icon: 'icon_27',
		title: 'Holiday / Travel',
		parent: 0
	},
	
	46: {
		filterable: false,
		private: true,
		icon: 'icon_52',
		title: 'Holiday Inn',
		parent: 0
	},
	
	48: {
		filterable: false,
		private: true,
		icon: 'icon_54',
		title: 'Holiday Inn \x2D Crowne Plaza',
		parent: 0
	},
	
	49: {
		filterable: false,
		private: true,
		icon: 'icon_55',
		title: 'Holiday Inn \x2D InterContinental',
		parent: 0
	},
	
	47: {
		filterable: false,
		private: true,
		icon: 'icon_53',
		title: 'Holiday Inn Express',
		parent: 0
	},
	
	70: {
		filterable: true,
		private: false,
		icon: 'icon_91',
		title: 'Horse racing',
		parent: 0
	},
	
	22: {
		filterable: true,
		private: false,
		icon: 'icon_28',
		title: 'Hospital',
		parent: 0
	},
	
	23: {
		filterable: true,
		private: false,
		icon: 'icon_29',
		title: 'Hotel/Hostel',
		parent: 0
	},
	
	24: {
		filterable: true,
		private: false,
		icon: 'icon_30',
		title: 'House',
		parent: 0
	},
	
	71: {
		filterable: true,
		private: false,
		icon: 'icon_92',
		title: 'Ice rink',
		parent: 0
	},
	
	45: {
		filterable: false,
		private: false,
		icon: 'icon_51',
		title: 'Injury Clinic',
		parent: 0
	},
	
	26: {
		filterable: true,
		private: false,
		icon: 'icon_32',
		title: 'Internet',
		parent: 0
	},
	
	51: {
		filterable: true,
		private: false,
		icon: 'icon_62',
		title: 'Landmark',
		parent: 0
	},
	
	27: {
		filterable: true,
		private: false,
		icon: 'icon_33',
		title: 'Library',
		parent: 0
	},
	
	72: {
		filterable: true,
		private: false,
		icon: 'icon_93',
		title: 'Netball',
		parent: 0
	},
	
	73: {
		filterable: true,
		private: false,
		icon: 'icon_94',
		title: 'Park',
		parent: 0
	},
	
	28: {
		filterable: true,
		private: false,
		icon: 'icon_34',
		title: 'Park / Forest',
		parent: 0
	},
	
	29: {
		filterable: true,
		private: false,
		icon: 'icon_35',
		title: 'Petrol station',
		parent: 0
	},
	
	94: {
		filterable: true,
		private: false,
		icon: 'icon_20',
		title: 'Pharmacy',
		parent: 0
	},
	
	50: {
		filterable: true,
		private: false,
		icon: 'icon_61',
		title: 'Photo',
		parent: 0
	},
	
	31: {
		filterable: true,
		private: false,
		icon: 'icon_37',
		title: 'Picnic site',
		parent: 0
	},
	
	33: {
		filterable: true,
		private: false,
		icon: 'icon_39',
		title: 'Police',
		parent: 0
	},
	
	74: {
		filterable: true,
		private: false,
		icon: 'icon_95',
		title: 'Pool',
		parent: 0
	},
	
	34: {
		filterable: true,
		private: false,
		icon: 'icon_40',
		title: 'Public house',
		parent: 0
	},
	
	44: {
		filterable: false,
		private: true,
		icon: 'icon_50',
		title: 'Puma Stores',
		parent: 0
	},
	
	75: {
		filterable: true,
		private: false,
		icon: 'icon_96',
		title: 'Railway',
		parent: 0
	},
	
	35: {
		filterable: true,
		private: false,
		icon: 'icon_41',
		title: 'Restaurant',
		parent: 0
	},
	
	76: {
		filterable: true,
		private: false,
		icon: 'icon_97',
		title: 'Rugby',
		parent: 0
	},
	
	77: {
		filterable: true,
		private: false,
		icon: 'icon_98',
		title: 'Running',
		parent: 0
	},
	
	78: {
		filterable: true,
		private: false,
		icon: 'icon_99',
		title: 'Sailing',
		parent: 0
	},
	
	79: {
		filterable: true,
		private: false,
		icon: 'icon_100',
		title: 'Salad bars',
		parent: 0
	},
	
	36: {
		filterable: true,
		private: false,
		icon: 'icon_42',
		title: 'School',
		parent: 0
	},
	
	53: {
		filterable: true,
		private: false,
		icon: 'icon_66',
		title: 'Shopping',
		parent: 0
	},
	
	80: {
		filterable: true,
		private: false,
		icon: 'icon_101',
		title: 'Skate boarding',
		parent: 0
	},
	
	81: {
		filterable: true,
		private: false,
		icon: 'icon_102',
		title: 'Ski',
		parent: 0
	},
	
	82: {
		filterable: true,
		private: false,
		icon: 'icon_103',
		title: 'Spas',
		parent: 0
	},
	
	42: {
		filterable: true,
		private: false,
		icon: 'icon_48',
		title: 'Sport',
		parent: 0
	},
	
	95: {
		filterable: true,
		private: false,
		icon: 'sport_relief',
		title: 'Sport Relief',
		parent: 0
	},
	
	97: {
		filterable: true,
		private: false,
		icon: 'sport\x2Drelief\x2Dregional\x2Dmile',
		title: 'Sport Relief Local Mile',
		parent: 0
	},
	
	83: {
		filterable: true,
		private: false,
		icon: 'icon_104',
		title: 'Sports centre',
		parent: 0
	},
	
	84: {
		filterable: true,
		private: false,
		icon: 'icon_105',
		title: 'Sports practitioner',
		parent: 0
	},
	
	85: {
		filterable: true,
		private: false,
		icon: 'icon_106',
		title: 'Sports shop',
		parent: 0
	},
	
	86: {
		filterable: true,
		private: false,
		icon: 'icon_107',
		title: 'Squash',
		parent: 0
	},
	
	87: {
		filterable: true,
		private: false,
		icon: 'icon_108',
		title: 'Surfing',
		parent: 0
	},
	
	88: {
		filterable: true,
		private: false,
		icon: 'icon_109',
		title: 'Swimming',
		parent: 0
	},
	
	37: {
		filterable: true,
		private: false,
		icon: 'icon_43',
		title: 'Taxi',
		parent: 0
	},
	
	30: {
		filterable: true,
		private: false,
		icon: 'icon_36',
		title: 'Telephone',
		parent: 0
	},
	
	89: {
		filterable: true,
		private: false,
		icon: 'icon_110',
		title: 'Tennis',
		parent: 0
	},
	
	3: {
		filterable: false,
		private: true,
		icon: 'start_flag',
		title: 'Test flag',
		parent: 0
	},
	
	38: {
		filterable: true,
		private: false,
		icon: 'icon_44',
		title: 'Theatre',
		parent: 0
	},
	
	52: {
		filterable: true,
		private: false,
		icon: 'icon_63',
		title: 'Toilet',
		parent: 0
	},
	
	25: {
		filterable: true,
		private: false,
		icon: 'icon_31',
		title: 'Tourist information / General information',
		parent: 0
	},
	
	39: {
		filterable: true,
		private: false,
		icon: 'icon_45',
		title: 'Train',
		parent: 0
	},
	
	40: {
		filterable: true,
		private: false,
		icon: 'icon_46',
		title: 'Tram',
		parent: 0
	},
	
	90: {
		filterable: true,
		private: false,
		icon: 'icon_111',
		title: 'Underground',
		parent: 0
	},
	
	91: {
		filterable: true,
		private: false,
		icon: 'icon_112',
		title: 'Vitamin stores',
		parent: 0
	},
	
	92: {
		filterable: true,
		private: false,
		icon: 'icon_113',
		title: 'Water parks',
		parent: 0
	},
	
	41: {
		filterable: true,
		private: false,
		icon: 'icon_47',
		title: 'WiFi',
		parent: 0
	},
	
	93: {
		filterable: true,
		private: false,
		icon: 'icon_114',
		title: 'Yoga',
		parent: 0
	}
	
};
//		}}} MARKER_CATEGORIES
//		{{{ ROUTE_CATEGORIES
var ROUTE_CATEGORIES = {
	getNextColour: function(id) {
		if (!this[id]) {
			return '#ff0000';
		}
		var colour = this[id].colours[this[id].index];

		this[id].index++;
		while (this[id].index >= this[id].colours.length) {
			this[id].index -= this[id].colours.length;
		}

		return colour;
	},
	
	11: {
		filterable: true,
		private: false,
		colours: ('#FF0000,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Sport Relief Training Run',
		parent: 0,
		is_parent: false,
		start_flag: 'sport_relief_training',
		end_flag: 'mini_end_flag',
		index: 0
	},
	
	10: {
		filterable: true,
		private: true,
		colours: ('#FF0000,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Sport Relief',
		parent: 0,
		is_parent: false,
		start_flag: 'sport_relief',
		end_flag: 'mini_end_flag',
		index: 0
	},
	
	5: {
		filterable: true,
		private: false,
		colours: ('#0000FF,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Running',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	1: {
		filterable: true,
		private: false,
		colours: ('#0000FF,#0000AA,#550055,#2A4895,#0A59CF,#072A5F,#353FC5,#031366,#12579D').split(','),
		title: 'Other running',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	9: {
		filterable: true,
		private: false,
		colours: ('#FF6600').split(','),
		title: 'General travel',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	8: {
		filterable: true,
		private: false,
		colours: ('#FFFF00').split(','),
		title: 'Backpacking',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	6: {
		filterable: true,
		private: false,
		colours: ('#4AD840,#17970D,#04621C,#36C55B,#41792D,#6FE645,#048C2D,#45974A,#215116').split(','),
		title: 'Walking',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	},
	
	7: {
		filterable: true,
		private: false,
		colours: ('#E40D0D,#B90404,#D2373E,#7F191E,#F1000B,#C2301D,#5A130A,#CA1224').split(','),
		title: 'Cycling',
		parent: 0,
		is_parent: false,
		start_flag: '',
		end_flag: '',
		index: 0
	}
	
};
//		}}} ROUTE_CATEGORIES
//      {{{ COMMUNITY_GROUPS
if ('' != "None") {
	var COMMUNITY_GROUPS = {
		
	}
} else {
	var COMMUNITY_GROUPS = null;
}function checkURL() {
	var url = window.location;
	url = url.toString();
	if (url.search('flmroutes') != -1) {
		return true;
	} else {
		return false;
	};
};

function get_tip() {
	var tips = 
	new Array('Talent and training aside, the right diet is one of the most important factors in improving you running performance.',
			  'Carbohydrates are the body’s primary source of fuel during exercise.',
			  'Fatigue occurs when glycogen (carbohydrates stored in the body) become depleted.',
			  'Starting a run with insufficient glycogen (carbohydrates stored in the body), is like taking a car out with a half empty fuel tank.',
			  'Taking on carbohydrates before and during prolonged performances has been shown to reduce the negative impact of glycogen (carbohydrates stored in the human body) depletion on performance.',
			  'A 2% reduction in body weight through sweating can significantly decrease performance.',
			  'Marathon runners have been reported to lose up to 8% of their body weight in warm conditions.',
			  'Fluid lost through sweat contains key electrolytes such as sodium and potassium which can be replaced by consuming a well formulated sports drink or other source of electrolytes.',
			  'Human urine is a great way of assessing hydration; when you are properly hydrated, your pee should be the colour of pale straw',
			  'If you want to maintain your bodyweight and energy levels you should increase your calories consumed relative to your training load by approx. 100Kcal per mile run.',
			  'Try and start each day with a high-carbohydrate breakfast and continue to top up your stores with high carbohydrate snacks such as energy bars, sports drinks and dried fruit.',
			  'Consume a high carbohydrate meal 3-4 hours before training and competing.',
			  'Stay well hydrated throughout the day by drinking a minimum of two pints of water little and often.',
			  'Consume a high carbohydrate meal or snack within 30mins of completing exercise. Aim for an intake of typically 1.0 – 1.2g of carbohydrate per kg of bodyweight per hour immediately after exercise.',
			  'After exercise try to consume approx. 1.5 litres of fluid per kg of bodyweight lost through sweating.',
			  'Include protein in your immediate post-race nutrition strategy to aid the generation process.',
			  'A daily carbohydrate intake of 7-10g/kg body weight is required to optimise carbohydrate stores for those competing in heavy training.',
			  'Ensure that you start the event suitably hydrated by drinking 500mls of an isotonic sports drink in the hour before you run and then drink whilst actually running.',
			  'Sweat rates could be 1-2 litres an hour or more, even in an air conditioned gym.',
			  'Exercise drinks like Lucozade Sport contain electrolytes which increase the rate at which water is absorbed from the small intestine.',
			  'An increase in work rate leads to an increase in core temperature. Sweat evaporating from the skin is a cooling mechanism to counteract this increase in core temperature.',
			  'Sweat loss varies for the individual and is dependent on the conditions you are exercising in.',
			  'Key symptoms of dehydration to watch out for are nausea; dizziness; headache; poor co-ordination; cramps; poor concentration; early fatigue and increased perception of effort.',
			  'Thirst is not a good indicator– you will already be dehydrated before feeling thirsty.',
			  'Always begin any form of exercise hydrated; drink 5-7ml of fluid per kilogram of body weight.',
			  'Caffeine can improve performance in both short and long term endurance events as well as short term, high intensity intermittent exercise.',
			  'Caffeine can improve many of the cognitive (mental processing) attributes important to sport such as alertness, concentration, reaction time and focus.',
			  'Caffeine can improve performance in exercise ranging between 3-120 min. The level of performance benefit will vary between individuals.',
			  'The benefits of caffeine are more apparent under situations of fatigue/physical stress.',
			  'Caffeine is mainly excreted from the body in the urine with the time to clear half of the ingested caffeine between 3-5 hours.',
			  'Carbohydrate is the fuel of choice for the brain, exercising muscle and central nervous system during exercise.',
			  'Sports nutrition guidelines are focused on strategies to enhance carbohydrate availability in the periods before, during and after exercise.',
			  'Correct participation is fundamental to an enjoyable race day experience and can make the difference between setting a new PB and finishing disappointed.',
			  'Pack your kit bag by ticking items off your check list. Make sure you pack an extra change of clothes, towel, drinks bottle, light snack and safety pins for your race number.',
			  'Take a drink and a snack for after the race – you’ll deserve it!',
			  'Don’t try anything new on race day – you may not like it or it may not like you!',
			  'During recovery, sufficient carbohydrate should be consumed to immediately replace what has just been used.',
			  'Carbohydrate can be consumed in either a series of snacks or large meals, although it is advised that a regular intake of smaller snacks may be helpful in overcoming the gastric discomfort often associated with eating large amounts of bulky high carbohydrate foods. For immediate recovery, consume moderate to high glycaemic index (GI) foods.',
			  'When the recovery time between exercise is short (0-4 h), carbohydrate consumption should begin immediately as this results in higher rates of muscle glycogen storage compared with delayed feeding.',
			  'A well thought out, and well stuck to training schedule is essential to not only surviving, but enjoying race day.',
			  'Arrive at the start line with enough time to relax and to be able to fully focus on the challenge ahead.',
			  'Allow plenty of time before the race to allow for traffic delays, time to drop your bag in the baggage area, put your race chip in your shoe and visit the toilet.',
			  '1/2 hour before the race begins top up your energy supplies with a small snack like a Lucozade Sport Energy Bar or banana or up to 500ml of Lucozade Sport.',
			  'Plan your route to the night before and be aware that roads may be blocked off.',
			  'Before race day, make sure you eat a couple of hours before you go to sleep.',
			  'During race week, gradually increase your carbohydrate intake to ensure you are storing the energy your muscles will need during the race. Foods such as potatoes, rice, pasta, bread, bananas, and jelly sweets are all high in carbohydrate and low in fat.',
			  'If training for over 60mins or to a high intensity you should take on fluid and carbohydrates.',
			  'If your event/sport is < 90 min in duration, ensure that you consume a high carbohydrate diet in the hours before and during exercise.',
			  'Performance in continuous or intermittent exercise (duration; ≥ 90 min) is generally improved by a high carbohydrate diet in the 1-7 days prior.',
			  'The provision of carbohydrate (glucose, sucrose, maltodextrins or alternative high glycaemic index carbohydrates) during exercise is normal practice during events/sports that are ≥ 60 min in duration and continuous (running/cycling) or high intensity and intermittent (team/racket sports).',
			  'During moderate to high intensity endurance exercise ≥ 60 min in duration, consume 30-60 g of carbohydrate per hour to maintain exercise intensity and delay fatigue.',
			  'If exercising for ≥ 60, consume 30-60 g of carbohydrate per hour (g/h) in small feedings every 10-30 min, or as allowed by the event/sport. Isotonic sports drinks (600-1200 ml) and/or carbohydrate gels, bars or confectionary provide suitable options.',
			  'Stay headstrong. Develop a strong mind and will. When it hurts – you can do it!',
			  'Marathon running is as much about the journey as it is about the actual race.'
			 );
	var index = Math.floor(Math.random()*tips.length);
	tip = tips[index];
	return tip
}IncrementMarker = new Class(GOverlay);
IncrementMarker.prototype = {
	__construct: function(point, message, offset, index)	// BODGE index is bodged in so it knows if it should be hidden when zoomed out
	{
		this.index = index;
		this.point = point;
		this.offset = offset || new GSize(0, 0);
		this.message = message || '';
	},
	toString: function()
	{
		return '[object IncrementMarker]';
	},
	initialize: function(gmap)
	{
		
		this.gMap = gmap;
		this.element = document.createElement('div');


		GEvent.bind(gmap, 'zoomend', this,
			function()
			{
				if (gmap.getZoom() < 10 && !this.hidden) {
					this.hide();
				} else if (gmap.getZoom() >= 10 && this.hidden) {
					this.show();
				}
			}
		);

		with (this.element.style)
		{
			position = 'absolute';
			height = '21px';
			fontSize = '10px';
			fontWeight = 'bold';
			lineHeight = '18px';
			whiteSpace = 'nowrap';
		}

		this.leftEnd = document.createElement('div');
		this.rightEnd = document.createElement('div');
		this.tile = document.createElement('div');

		this.leftEnd.style.position = 'absolute';
		this.leftEnd.style.width = '19px';
		this.leftEnd.style.height = '21px';
		this.leftEnd.style.left = 0;

		this.rightEnd.style.position = 'absolute';
		this.rightEnd.style.width = '8px';
		this.rightEnd.style.height = '21px';
		this.rightEnd.style.right = 0;

		this.tile.style.margin = '0 8px 0 19px';
		this.tile.style.height = '21px';

		this.leftEnd.style.background = 'url('+SKIN_URL+'images/milemarker_arrow_left.png)';
		this.rightEnd.style.background = 'url('+SKIN_URL+'images/milemarker_end_right.png)';
		this.tile.style.background = 'url('+SKIN_URL+'images/milemarker_tile.png)';

		this.element.appendChild(this.leftEnd);
		this.element.appendChild(this.rightEnd);
		this.element.appendChild(this.tile);

		this.setMessage(this.message);
		
		this.gMap.getPane(G_MAP_MAP_PANE).appendChild(this.element);
	},
	remove: function()
	{
		GEvent.clearListeners(this);
		this.element.parentNode.removeChild(this.element);
	},
	copy: function()
	{
		return new IncrementMarker(this.point, this.message, this.offset, this.index);
	},
	redraw: function(force)
	{
		if (!force) return;

		if (!this.gMap) {
			
			return false;
		}
		var p = this.gMap.fromLatLngToDivPixel(this.point);
		this.element.style.left = (p.x + this.offset.width - 3) +'px';
		this.element.style.top = (p.y + this.offset.height - 8) +'px';
        this.show();
	},
	setLatLng: function(point)
	{
		this.point = point;
		this.redraw(true);
	},
	getLatLng: function()
	{
		return this.point;
	},
	setMessage: function(message)
	{
		
		this.message = message;
		message = message.replace(/ /g, '\u00a0');	// Replace space with &nbsp;

		if (this.tile) {
			while (this.tile.firstChild) {
				this.tile.removeChild(this.tile.firstChild);
			}
			this.tile.appendChild(document.createTextNode(message));
		}
	},
	getMessage: function()
	{
		return this.message;
	},
	show: function()
	{
		// BODGE to reduce number of markers show at low zoom levels
        var interval = 1;
        switch (this.gMap.getZoom()) {
            case 12:
            case 11:
                interval = 2;
                break;
            case 10:
            case 9:
                interval = 3;
                break;
            case 8:
            case 7:
                interval = 4;
                break;
            case 6:
            case 5:
                interval = 5;
                break;
            case 4:
            case 3:
                interval = 6;
                break;
            case 2:
            case 1:
            case 0:
                interval = 10;
                break;
        }

		
		if (this.index % interval == 0) {
			//this.show();
			this.hidden = false;
			this.element.style.display = 'block';
		} else {
			this.hide();
		}
	},
	hide: function()
	{
		this.hidden = true;
		this.element.style.display = 'none';
	}
};




ModalPanel = new Class(FloatingPanel);
ModalPanel.prototype = {
	__construct: function(width, height)
	{
		

		var x = document.body.clientWidth/2 - width/2;
		var y = document.body.clientHeight/2 - height/2;

		y = -100;

		FloatingPanel.prototype.__construct.call(this, x, y, width, height);

		this.movable(false);

		this.blocker = document.createElement('div');
		with (this.blocker.style) {
			position = 'absolute';
			top = 0;
			left = 0;
			width = '100%';
			height = this.container.parentNode.scrollHeight +'px';
			background = '#fff';
			zIndex = 9999999;
		}
		this.container.parentNode.parentNode.appendChild(this.blocker);
		setOpacity(this.blocker, 0.5);

		// Reposition panel if it's resized
		GEvent.bind(this, 'resize', this, this.fixPosition);

		// Reposition window if browser is resized
		GEvent.bindDom(window, 'resize', this, this.fixPosition);


		// move it's element into the body tag
		document.body.appendChild(this.container);
		this.fixPosition();

	},
	__destruct: function()
	{
		FloatingPanel.prototype.__destruct.call(this);
		if (this.blocker.parentNode) {
			this.blocker.parentNode.removeChild(this.blocker);
		}
	},
	setZ: function()
	{
		// Always on top!
		FloatingPanel.prototype.setZ.call(this, 10000000);
	},
	fixPosition: function()
	{
		try { 
			if (!this.container || !this.container.parentNode) {
				return;
			}
			var x = (document.documentElement.clientWidth/2)  - (this.getWidth()/2)  + document.body.scrollLeft;
			var y = (document.documentElement.clientHeight/2) - (this.getHeight()/2) + document.body.scrollTop;
			this.blocker.style.height = this.blocker.parentNode.scrollHeight +'px';
			this.move(x, y);
		} catch(e) {}
	}
};


ModalDialogue = new Class(ModalPanel);
ModalDialogue.prototype = {
	__construct: function(title, body)
	{
		
		this.buttons = [];

		var width = 368;
		var height = 143;

		ModalPanel.prototype.__construct.call(this, width, height);
		this.resizable(false);

		var content = document.createElement('div');
		content.className = 'modal_content';
		content.appendChild(document.createTextNode(body));
		content.style.padding = '10px';

		this.setTitle(title);
		this.setContent(content);
	},
	addButton: function(label, action, focus)
	{
		var btn = new Button(label, GEvent.callback(this, function () {
            if (action) {
                action();
            }
            this.close();
		}));

		
		// Forced to use a table :(
		if (!this.buttonContainer) {
			var tbl = document.createElement('table');
			tbl.appendChild(document.createElement('tbody'));
			with (tbl.style) {
				borderCollapse = 'collapse';
			}
			tbl.className = 'button_container';
			this.buttonContainer = document.createElement('tr');
			tbl.firstChild.appendChild(this.buttonContainer);
			this.content.appendChild(tbl);
		}
		var cell = document.createElement('td');
		cell.className = 'button_cell';
		cell.appendChild(btn.element);

		this.buttonContainer.appendChild(cell); 
		if (focus && this.visible) {
			btn.focus();
		} else if (focus) {
            this._focus_on_show = GEvent.bind(this, 'show', this,
                function()
                {
                    GEvent.removeListener(this._focus_on_show);
                    btn.focus();
                    delete this._focus_on_show;
                }
            );
        }
	}
};
EventQueue = new Class;
EventQueue.prototype = {
__construct:
	function(obj, evnt)
	{
		this.object    = obj;
		this.event     = evnt;
		this.callbacks = [];
		this.length    = 0;
		GEvent.bind(this.object, this.event, this, this.eventTriggered);
	},
eventTriggered:
	function()
	{
		if (this.length < 1) {
			return;
		}
		var callback = this.callbacks.shift();
		
		callback.apply(this, arguments);
		this.length = this.callbacks.length;
		if (!this.length) {
			GEvent.trigger(this, 'complete');
		}
	},
addCallback:
	function(callback)
	{
		
		this.callbacks.push(callback);
		this.length = this.callbacks.length;
	}
};
PolyManager = new Class();
PolyManager.prototype = {
	__construct: function(map, options)
	{
		this.map = map;
		this.polies = [];
		
		GEvent.bind(this.map, 'zoomend', this, this.refresh);
		GEvent.bind(this.map, 'dragend', this, this.refresh);
	},
	addPoly: function(poly, minZoom, maxZoom)
	{
		this.polies.push({
			poly: poly,
			minZoom: minZoom,
			maxZoom: maxZoom,
			hidden: false			// This sets if the poly should ALWAYS be hidden
		});
		this.map.addOverlay(poly);
		this.refresh();
	},
	refresh: function()
	{
		
		var zoom = this.map.getZoom();
		for (var i=0; i<this.polies.length; i++) {
			var p = this.polies[i];
			// TODO Check if it's in viewport
			if (!p.poly.isHidden() && (zoom > p.maxZoom || zoom < p.minZoom)) {
				
				p.poly.hide();
			} else if (!p.hidden && !p.poly.__hidden && p.poly.isHidden() && zoom <= p.maxZoom && zoom >= p.minZoom) {
				
				p.poly.show();
			}
		}
		
	},
	hidePoly: function(poly)
	{
		var p = this.findPoly(poly);
		if (p === false) {
			return false;
		}

		this.polies[p].hidden = true;
		this.polies[p].poly.hide();
	},
	showPoly: function(poly)
	{
		var p = this.findPoly(poly);
		if (p === false) {
			return false;
		}

		this.polies[p].hidden = false;
		this.polies[p].poly.show();		// FIXME: this will show the poly even if it should't (calling refresh is better, but slower)
	},
	removePoly: function(poly)
	{
		var i = this.findPoly(poly);
		if (i === false) {
			return false;
		}
		this.map.removeOverlay(poly);
		this.polies.splice(i, 1);
		return;
	},
	findPoly: function(poly)
	{
		for (var i=0; i<this.polies.length; i++) {
			if (this.polies[i].poly === poly) {
				return i;
			}
		}
		return false;
	}
};
MarkerLegend = new Class();
MarkerLegend.prototype = {
	__construct: function()
	{
		this.panel = new FloatingPanel(20, 40, 450, 150);
		this.panel.resizable(false);
		this.panel.setTitle('marker legend');

		var n = T.table();
		var tbody = T.tbody();
		n.appendChild(tbody);


		var odd = false;
		for (var x in MARKER_CATEGORIES) {
			if (!odd) {
				var row = T.tr();
				tbody.appendChild(row);
			}
			if (parseInt(x) != x) {
				continue;
			}
			
			var m = MARKER_CATEGORIES[x];
			if (m.private) {
				continue;	// Skip sponsors
			}
			row.appendChild(T.td([T.img({'style':'vertial-align:middle', 'src':Icons[m.icon].image})]));
			if (odd) {
				row.appendChild(T.td([T.span(m.title)]));
			} else {
				row.appendChild(T.td({'style':'border-right:1px solid #e3e3e3'}, [T.span(m.title)]));
			}

				

			odd = !odd;
		}
		n = T.div([n]);
		n.className = 'marker_legend';
		this.panel.setContent(n);
		this.panel.show();
		this.panel.resizeToContent(true);
	},
	close: function()
	{
		if (this.panel) {
			this.panel.close();
		}
		delete this.panel;
	}
};

PopoutPanel = new Class(GOverlay);
PopoutPanel.prototype = {
__construct:
	function(world, anchor, width, height, single_instance)
	{
		if (single_instance) {
			if (PopoutPanel._current_instance) {
				PopoutPanel._current_instance.close();
			}
			PopoutPanel._current_instance = this;
			GEvent.addListener(this, 'close', function() { delete PopoutPanel._current_instance; });
		}
		this.world = world;

		if (anchor instanceof Array) {
			var anchor = new GLatLng(anchor[0], anchor[1]);
		}
		this.anchor = anchor;

		var w = width || 400;
		var h = height || 300;
		this.width = w;
		this.height = h;

		this.status_bar_size = 0;

		if (!this.padding) {
			this.padding = {
				left: 8,
				top: 14,
				right: 149,
				bottom: 24
			};
		}

		if (!this.attach_point) {
			this.attach_point = {
				x: -18,	// x is from the right
				y: 144  // y is from the top
			};
		}

		if (!this.box) {
			this.box = new Box(SKIN_URL + 'images/balloon_panel.png', 930, 580, w, h, 32, 32, 48, 180);
			with (this.box.container.style) {
				position = 'absolute';
				left = 0;
				top = 0;
			}
		}



		if (!this.element) {
			this.element = T.div({'class': 'popout_panel'}, [this.box.container]);
		}

		

		with (this.element.style) {
			position = 'absolute';
			width = w + 'px';
			height = h + 'px';
		}

		// Title bar area
		this.titlebar = T.div({'class': 'popout_title'}, [T.div({'class': 'title_content'})]);
		with (this.titlebar.style) {
			cursor = 'auto';
			position = 'absolute';
			left = this.padding.left + 'px';
			top = this.padding.top + 'px';
			width = (w - this.padding.left - this.padding.right) + 'px';
		}
	
		this.element.appendChild(this.titlebar);

		// Close button
		this.close_button = T.a({'href': 'javascript:void(0)'});
		this.close_button.className = 'panel_close';
		with (this.close_button.style) {
			position = 'absolute';
			right = '5px';
			top = '1px';
			width = '20px';
			height = '20px';
			background = 'url('+SKIN_URL+'images/panel_close.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}
		GEvent.bindDom(this.close_button, 'mouseover', this.close_button, function(e) { this.style.backgroundPosition = '0 20px'; });
		GEvent.bindDom(this.close_button, 'mouseout', this.close_button, function(e) { this.style.backgroundPosition = '0 0'; });
		GEvent.bindDom(this.close_button, 'click', this, function() { this.close() });
		this.titlebar.appendChild(this.close_button);


		// Main content area
		this.content_container = T.div({'class': 'panel_content_container'});
		this.content = T.div({'class': 'panel_content'});
		this.content_container.appendChild(this.content);
		with (this.content_container.style) {
			position = 'absolute';
			left = this.padding.left + 'px';
			top = this.padding.top + 'px';
			width = (w - this.padding.left - this.padding.right) + 'px';
			height = (h - this.padding.top - this.padding.bottom) + 'px';
			overflow = 'auto';
			cursor = 'auto';
		}
		with (this.content.style) {
			zoom = 1;
			overflow = 'hidden';
		}
		try {
			this.content_container.style.overflowX = 'hidden';
		} catch(err) { }

		
		// Prevent default map events
		this._map_events = [
			'click',
			'dblclick',
			'mousedown',
			'mouseup',
			'mousewheel',
			'contextmenu',
			'DOMMouseScroll'
		];
		for (x in this._map_events) {
			GEvent.addDomListener(this.titlebar, this._map_events[x], function(e) { GEvent.stop(e, false); } );
			GEvent.addDomListener(this.content_container, this._map_events[x], function(e) { GEvent.stop(e, false); } );
		}
		
		this.element.appendChild(this.content_container);

		this.world.map.addOverlay(this);
		this.centre_on_map();
	},
set_content:
	function(content, html)
	{
		document.emptyElement(this.content);
		if (html) {
			this.content.innerHTML = content;
		} else {
			if (typeof content == 'string') {
				var content = document.createParagraph(content);
			}
			this.content.appendChild(content);
		}
	},
set_title:
	function(title)
	{
		document.emptyElement(this.titlebar.firstChild);
		this.titlebar.firstChild.appendChild(document.createTextNode(title));
		this.fix_size();
	},
resize_to_content:
	function(max_width, max_height)
	{
		this.content_container.style.height = '30000px';
		if (this.content_container.scrollHeight >= 20000) {
			this.content_container.style.height = 'auto';
		}
		this.content_container.style.width = '30000px';
		if (this.content_container.scrollWidth >= 20000) {
			this.content_container.style.width = 'auto';
		}
		var width = this.content_container.scrollWidth + this.padding.left + this.padding.right;
		var height = this.content_container.scrollHeight + this.titlebar.scrollHeight + this.padding.top + this.padding.bottom;
		if (max_width && width > max_width) {
			width = max_width;
		}
		if (max_height && height > max_height) {
			height = max_height;
		}

		this.resize(width, height);
	},
resize:
	function(width, height)
	{

		if (!this.element) {
			
			return false;
		}
		this.status_bar_size = this.status_bar ? 24 : 0;

		var width = width || this.width;
		var height = height || this.height;

		this.width = width;
		this.height = height;

		this.box.resize(this.width, this.height + this.status_bar_size);

		with (this.element.style) {
			width = this.width + 'px';
			height = (this.height + this.status_bar_size) + 'px';
		}
		with (this.titlebar.style) {
			width = this.get_container_width() + 'px';
		}
		with (this.content_container.style) {
			width = this.get_container_width() + 'px';
			height = this.get_container_height() + 'px';
		}
		this.fix_size();
		this.redraw();
		GEvent.trigger(this, 'resize');
	},
get_centre_latlng:
	function()
	{
		var left = this.element.offsetLeft + this.padding.left;
		var right = this.element.offsetLeft + this.width;
		var top = this.element.offsetTop + this.padding.top;
		var bottom = this.element.offsetTop + this.height;
		var width = right - left;
		var height = bottom - top;

		var centre_x = left + (width/2);
		var centre_y = top + (height/2);

		var point = this.world.map.fromDivPixelToLatLng(new GPoint(centre_x, centre_y));
		return point;
	},
centre_on_map:
	function()
	{
		this.world.jump_to_point(this.get_centre_latlng());
	},
fix_size:
	function()
	{
		var t = (this.padding.top + this.titlebar.offsetHeight);
		var h = this.height - t - this.padding.bottom;
		with (this.content_container.style) {
			top = t + 'px';
			height = h + 'px';
		}
	},
initialize:
	function(map)
	{
		map.getPane(G_MAP_FLOAT_PANE).appendChild(this.element);
		this.redraw();
	},
remove:
	function()
	{
		this.world.map.getPane(G_MAP_FLOAT_PANE).removeChild(this.element);
	},
copy:
	function()
	{
	},
close:
	function()
	{
		if (!this.element) {
			return false;
		}
		this.world.map.removeOverlay(this);
		delete this.element;
		GEvent.trigger(this, 'close');
	},
redraw:
	function(force)
	{
		var p = this.world.map.fromLatLngToDivPixel(this.anchor);
		this.element.style.left = (p.x - this.width - this.attach_point.x) +'px';
		this.element.style.top = (p.y - this.attach_point.y) +'px';
		this.fix_size();
	},
busy:
	function(state)
	{
		if (this.is_busy == status || status === undefined) {
			return this.is_busy;
		}
		
		this.is_busy = state;

		switch (this.is_busy) {
		case true:
			if (!this._busy_indicator) {
				this._busy_indicator = document.createElement('div');
				this._busy_indicator.appendChild(document.createElement('div'));
				this._busy_indicator.className = 'panel_busy_container';
				this._busy_indicator.firstChild.className = 'panel_busy_indicator';


				this.content_container.appendChild(this._busy_indicator);
				with (this._busy_indicator.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
					zIndex = 1000;
				}
				with (this._busy_indicator.firstChild.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
				}
				PNG.setImage(SKIN_URL + 'images/busy_background.png', this._busy_indicator, true);
			}
			this.content_container.style.overflow = 'hidden';
			this._busy_indicator.style.display = 'block';
			break;
		case false:
			this.content_container.style.overflow = 'auto';
			this._busy_indicator.style.display = 'none';
			break;
		}
	},
get_container_width:
	function()
	{
		return this.width - this.padding.left - this.padding.right;
	},
get_container_height:
	function()
	{
		return this.height - this.padding.top - this.padding.bottom;
	},
set_status_bar:
	function(el)
	{
		if (!this.status_bar) {
			this.status_bar = T.div({'class': 'panel_status_bar'});
			this.element.appendChild(this.status_bar);
			with (this.status_bar.style) {
				position = 'absolute';
				bottom = this.padding.bottom + 'px';
				left = this.padding.left + 'px';
				width = this.get_container_width() + 'px';
			}

			// Prevent events hitting the map
			for (x in this._map_events) {
				GEvent.addDomListener(this.status_bar, this._map_events[x], function(e) { GEvent.stop(e, false); } );
			}
		} else {
			document.emptyElement(this.status_bar);
		}
		this.status_bar.appendChild(el);
		
		this.resize();	// resize adds room for status bar if it exists
	}
};


TabbedPopoutPanel = new Class(PopoutPanel);
TabbedPopoutPanel.prototype = {
__construct:
	function(world, anchor, width, height, single_instance)
	{
		PopoutPanel.prototype.__construct.apply(this, arguments);

		this.tab_bar = new TabBar();
		this.element.appendChild(this.tab_bar.element);
		this.tab_elements = [];

		// Catch map events from bubbling from tabbar
		var map_events = [
			'click',
			'dblclick',
			'mousedown',
			'mousewheel',
			'contextmenu',
			'DOMMouseScroll'
		];
		for (x in map_events) {
			GEvent.addDomListener(this.tab_bar.element, map_events[x], function(e) { GEvent.stop(e, false); } );
		}

		GEvent.bind(this.tab_bar, 'changetab', this, function(id) { GEvent.trigger(this, 'changetab', id); });
		GEvent.bind(this.tab_bar, 'changetab', this, this.show_tab);
	},
fix_size:
	function()
	{
		PopoutPanel.prototype.fix_size.call(this);
	},
tab_busy:
	function(id, status)
	{
		this.tab_bar.tab_busy(id, status);
	},
set_tab_label:
	function(id, label)
	{
		this.tab_bar.set_tab_label(id, label);
	},
add_tab:
	function(name, content, html)
	{
		var tab_id = this.tab_bar.add_tab(name);

		this.add_tab_element(content, html);

		// If it's the first tab, activate it automagically
		if (tab_id == 0) {
			this.tab_bar.set_tab(tab_id);
		}
		return tab_id;
	},
add_tab_element:
	function(content, html)
	{
		var node = T.div({'class': 'tab_content'});
		node.style.display = 'none';

		this.tab_elements.push(node);
		var tab_id = this.tab_elements.length -1;

		this.content.appendChild(node);


		if (html) {
			node.innerHTML = content;
		} else {
			if (typeof content == 'string') {
				var content = document.createParagraph(content);
			}
			node.appendChild(content);
		}
		return tab_id;
	},
show_tab:
	function(tab)
	{
		// If the tab isn't active, then activate it.
		if (tab !== this.tab_bar.active_tab) {
			this.tab_bar.set_tab(tab);
			return;
		}
		

		for (var i=0; i<this.tab_elements.length; i++) {
			this.tab_elements[i].style.display = 'none';
		}
		this.tab_elements[tab].style.display = 'block';
	},
hide_tabs:
	function()
	{
		this.tab_bar.element.style.display = 'none';
	}
};


TabBar = new Class;
TabBar.prototype = {
__construct:
	function()
	{
		this.height = 26;

		this.element = T.div({'class': 'tab_bar'}, [T.ul()]);
		with (this.element.style) {
			position = 'absolute';
			top = (-this.height +4) + 'px';
			left = '28px';
		}
		this.tabs = [];


		// Tab class
		this.Tab = new Class;
		this.Tab.prototype = {
		__construct:
			function(tab_bar, name)
			{
				this.tab_bar = tab_bar;
				this.element = T.li([T.a({'href': 'javascript:void(0)'}, name)]);
				with (this.element.style) {
					position = 'relative';
					cssFloat = 'left';
					styleFloat = 'left';
					height = this.tab_bar.height + 'px';
					overflow = 'hidden';
				}
				with (this.element.firstChild.style) {
					//display = 'block';
					height = '100%';
					padding = '0 8px';
				}


				GEvent.bindDom(this.element.firstChild, 'click', this, function() { GEvent.trigger(this, 'click'); });
				this.deactivate();
			},
		busy:
			function(status)
			{
				if (status === undefined) {
					return this._busy;
				}
				if (this._busy == status) return;
				this._busy = status;

				if (!this._busyIndicator) {
					this._busyIndicator = T.div({'class': 'tab_busy_indicator'});
					with (this._busyIndicator.style) {
						position = 'absolute';
						right = '4px';
						top = '50%';
					}
					this.element.appendChild(this._busyIndicator);
				}
				if (this._busy) {
					this.element.style.paddingRight = '20px';
					this._busyIndicator.style.display = 'block';
				} else {
					this.element.style.paddingRight = 0;
					this._busyIndicator.style.display = 'none';
				}
			},
		set_label:
			function(label)
			{
				document.emptyElement(this.element.firstChild);
				this.element.firstChild.appendChild(T.none(label));
				if (this._busyIndicator) {
					this.element.appendChild(this._busyIndicator);
				}
			},
		activate:
			function()
			{
				this.element.className = 'tab active_tab';
				with (this.element.style) {
					top = '1px';
					height = this.tab_bar.height + 'px';
				}
			},
		deactivate:
			function()
			{
				this.element.className = 'tab inactive_tab';
				with (this.element.style) {
					top = '3px';
					height = (this.tab_bar.height -4) + 'px';
				}
			}
		};
	},
tab_busy:
	function(id, status)
	{
		this.tabs[id].busy(status);
	},
set_tab_label:
	function(id, label)
	{
		var tab = this.tabs[id];
		tab.set_label(label);
	},
add_tab:
	function(name)
	{
		var tab = new this.Tab(this, name);
		this.tabs.push(tab);
		var tab_id = this.tabs.length - 1;

		tab.element.style.zIndex = 500 - this.tabs.length;

		this.element.firstChild.appendChild(tab.element);

		GEvent.addListener(tab, 'click', GEvent.callbackArgs(this, this.set_tab, tab_id));

		return tab_id;
	},
set_tab:
	function(id)
	{
		this.active_tab = id;
		for (var i=0; i<this.tabs.length; i++) {
			(id == i) ? this.tabs[i].activate() : this.tabs[i].deactivate();
		}

		GEvent.trigger(this, 'changetab', id);
	}
};
/*

*/
TabPanel = new Class();
TabPanel.prototype = {
	__construct: function(panel)
	{
		this.tabs = [];
		this.activeTab = false;
		this.tabContent = document.createElement('div');
		this.tabBar = document.createElement('ul');
		this.tabBar.className = 'tab_bar';
		this.element = document.createElement('div');
		this.element.className = 'tab_panel';

		this.element.appendChild(this.tabBar);
		this.element.appendChild(this.tabContent);

		this.setPanel(panel);
	},
	setPanel: function(panel)
	{
		this.panel = panel;
		if (this._resizeListeners) {
			for (var i=0; i<this._resizeListeners.length; i++) {
				GEvent.removeListener(this._resizeListeners[i]);
				delete this._resizeListeners[i];
			}
			delete this._resizeListeners;
		}
		if (!panel) return;

		// Listen for resizes
		if (panel) {
			this._resizeListeners = [
				GEvent.bind(panel, 'resize', this, this.updateSize),
				GEvent.bind(panel, 'beforeresizetocontent', this, function() {
					this.element.style.height = 'auto';
				})
			];
		}
	},
	addTab: function(name)
	{
		// Create the tab
		var tab = document.createElement('li');
		tab.appendChild(document.createElement('a'));
		tab.firstChild.appendChild(document.createTextNode(name));
		this.tabBar.appendChild(tab);

		// Create the tab content container
		tab.content = document.createElement('div');
		tab.content.className = 'tab_content';
		this.tabContent.appendChild(tab.content);

		// Add to tabs array
		this.tabs.push(tab);

		if (this.tabs.length == 1) {
			// If this is the first tab, then select it
			this.setTab(0);
		} else {
			// If not then hide it
			this.setTab(this.activeTab);
		}

		var tabID = this.tabContent.childNodes.length -1;
		
		// Add click event to tab
		GEvent.bindDom(tab.firstChild, 'click', this, function() { this.setTab(tabID); });

		this.updateSize();

		GEvent.trigger(this, 'addtab', this, tabID);

		tab.tabID = tabID;

		// Return it's ID
		return tabID;
	},
	setTab: function(id)
	{
		for (var i=0; i<this.tabs.length; i++) {
			this.tabs[i].className = 'tab_inactive';
			this.tabs[i].content.style.display = 'none';
		}
		this.tabs[id].className = 'tab_active';
		this.tabs[id].content.style.display = 'block';
		this.activeTab = id;

		GEvent.trigger(this, 'tabchange', this, id);
		if (this.panel && this.panel.resizeToContent) {
			this.panel.resizeToContent(true);
			// FIXME: the +80 is a hack, it needs to cacluate the true height required
			//this.panel.resize(false, this.tabs[id].content.scrollHeight+80, true);
		}
	},
	setContent: function(id, node)
	{
		while (this.tabs[id].content.firstChild) {
			this.tabs[id].content.removeChild(this.tabs[id].content.firstChild);
		}
		this.tabs[id].content.appendChild(node);
	},
	addContent: function(id, node)
	{
		this.tabs[id].content.appendChild(node);
	},
	updateSize: function(panel)
	{
		var panel = panel || this.panel;
        if (!panel.getContentWidth) {
            return;
        }
		

		//this.element.style.background = 'blue';

		// Set width
		var width = panel.getContentWidth();
		this.element.style.width = width+'px';

		// Calculate tabSize (we can't use percentages because IE sucks at rounding them)
		var tabSize = Math.floor(width/this.tabs.length);
		var remainder = width-(tabSize*this.tabs.length);
		for (var i=0; i<this.tabs.length; i++) {
			var thisTab = tabSize;
			if (remainder-- > 0) thisTab++;
			this.tabs[i].style.width = thisTab +'px';
		}

		// Height is set after width incase tha tabs height changes due to word wrapping
		var height = panel.getContentHeight() - this.tabBar.offsetHeight;
		if (height < 48) {
			height = 48;
		}
		this.element.style.height = height+'px';
	}
};
PreviewLine = new Class(GOverlay);
PreviewLine.prototype = {
	__construct: function(points, colour, weight, opacity)
	{
		this.points = points || [];
		this.colour = colour || '#ff0000';
		this.weight = weight || 3;
		this.opacity = opacity || 0.7;
	},
	initialize: function(map)
	{
		this.map = map;
		this.line = document.createElement('div');
		this.line.style.position = 'absolute';
		for (var i=0; i<this.points.length-1; i++) {
			var con = document.createElement('div');
			con.style.position = 'absolute';
			setOpacity(con, this.opacity);
			this.line.appendChild(con);
		}

		this.map.getPane(G_MAP_MAP_PANE).appendChild(this.line);
	},
	setPoints: function(points)
	{
		var oldPoints = this.points;
		this.points = points;
		if (oldPoints.length != points.length) {
			this.remove();
			this.initialize(this.map);
		}
		this.redraw(true);
	},
	setColour: function(colour)
	{
		if (!colour || this.colour == colour) {
			return;
		}
		this.colour = colour;
		if (!this.line || !this.line.childNodes) {
			return;
		}
		var lines = this.line.childNodes;
		for (var i=0; i<lines.length; i++) {
			var dots = lines[i].childNodes;
			for (var j=0; j<dots.length; j++) {
				dots[j].style.backgroundColor = this.colour;
			}
		}
	},
	remove: function()
	{
		this.line.parentNode.removeChild(this.line);
	},
	copy: function()
	{
		return new PreviewLine(this.points, this.colour, this.weight, this.opacity);
	},
	redraw: function(force)
	{
		if (!force) {
			return;
		}

		var points = [];
		for (var i=0; i<this.points.length; i++) {
			points[i] = this.map.fromLatLngToDivPixel(this.points[i]);
		}

		for (var j=0; j<points.length-1; j++) {
			var p1 = points[j];
			var p2 = points[j+1];

			// Position the box surrounding the dots (IE requires this to draw properly)
			var line = this.line.childNodes[j];
			var bL = Math.min(p1.x, p2.x) - this.weight;
			var bT = Math.min(p1.y, p2.y) - this.weight;
			var bW = Math.max(p1.x, p2.x) - bL + (this.weight*2);
			var bH = Math.max(p1.y, p2.y) - bT + (this.weight*2);
		
			line.style.left = bL +'px';
			line.style.top = bT +'px';
			line.style.width = bW +'px';
			line.style.height = bH +'px';

			var dist = Math.sqrt( ((p2.x - p1.x)*(p2.x - p1.x)) + ((p2.y - p1.y)*(p2.y - p1.y)) );
			var step = (this.weight*2);
			if (step < 5) {
				step = 5;
			}
			var numDots = Math.floor(dist/step);
			var extraDots = numDots - line.childNodes.length;

			if (extraDots > 0) {
				for (var i=0; i<extraDots; i++) {
					var newDot = document.createElement('div');
					with (newDot.style) {
						position = 'absolute';
						background = this.colour;
						width = this.weight +'px';
						height = this.weight +'px';
						MozBorderRadius = Math.round(this.weight/2) +'px';
						WebkitBorderRadius = Math.round(this.weight/2) +'px';
						borderRadius = Math.round(this.weight/2) +'px';
						fontSize = '1%';
						display = 'none';
					}
					line.appendChild(newDot);
				}
			} else if (extraDots < 0) {
				for (var i=0; i>extraDots; i--) {
					line.removeChild(line.firstChild);
				}
			}

			var xS = (p1.x - p2.x) / numDots;
			var yS = (p1.y - p2.y) / numDots;
			for (var i=0; i<line.childNodes.length; i++) {
				var d = line.childNodes[i];
				with (d.style) {
					if (display == 'none') {
						display = 'block';
					}
					left = (p1.x - bL - Math.round(this.weight/2) - xS*i) +'px';
					top = (p1.y - bT -  Math.round(this.weight/2) - yS*i) +'px';
				}	// with
			}	// for (var i)
		}	// for (var j)
	}	// function
};
FloatingPanel = new Class();
FloatingPanel.numberOfPanels = 0;
FloatingPanel.openPanels = [];
FloatingPanel.prototype = {
	__construct: function(x, y, width, height, skin)
	{
		
		var x = x || 100;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;
		var skin = skin || 'realbuzz';
		this.width = w;
		this.height = h + 40;   // 40 is to take into account the large panel edge

		this.skin = skin;

		if (!this.minimumSize) {
			this.minimumSize = {
				width: 200,
				height: 200
			};
		}

		if (!this.padding) {
			this.padding = {
				left: 6,
				top: 9,
				right: 21,
				bottom: 48
			};
		}

		// Anchor is used when zooming the map
		if (!this.anchor) {
			this.anchor = {
				x: 0,
				y: 0
			};
		}


		// Add to static array of all open panels
		// TODO Get index so it can be deleted on destrction
		FloatingPanel.openPanels.push(this);
		FloatingPanel.numberOfPanels++;

		// Check if box was created in an extended class
		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/panel.png', 800, 580, this.width, this.height, 32, 32, 64, 64);
		}
		this.container = this.box.container;
		this.container.className = 'gmnoprint panel';
		with (this.container.style) {
			position = 'absolute';
			left = x +'px';
			top = y +'px';
			zIndex = '1000';
		}

		this.closeButton = document.createElement('div');
		this.closeButton.className = 'panel_close';
		with (this.closeButton.style) {
			position = 'absolute';
			right = (this.padding.right+9) +'px';
			top = (this.padding.top +8) +'px';
			width = '20px';
			height = '20px';
			background = 'url('+SKIN_URL+'images/panel_close.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}
		GEvent.bindDom(this.closeButton, 'mouseover', this.closeButton, function(e) { this.style.backgroundPosition = '0 20px'; });
		GEvent.bindDom(this.closeButton, 'mouseout', this.closeButton, function(e) { this.style.backgroundPosition = '0 0'; });
		GEvent.bindDom(this.closeButton, 'click', this, function() { this.close() });
		this.container.appendChild(this.closeButton);

		this.title = document.createElement('div');
		this.title.appendChild(document.createElement('div'));
		this.title.className = 'panel_title';
		with (this.title.style) {
			position = 'absolute';
			overflow = 'hidden';
			top = this.padding.top+'px';
			left = this.padding.left+'px';
			//whiteSpace = 'nowrap';
		}
		this.title.firstChild.style.padding = '4px 30px 4px 15px';
		this.setTitle('Untitled panel');
		this.container.appendChild(this.title);
		GEvent.bindDom(this.title, 'mousedown', this, this.startDrag);

		this.content = document.createElement('div');
		this.content.appendChild(document.createElement('div'));
		this.content.className = 'panel_content';
		with (this.content.style) {
			position = 'absolute';
			left = this.padding.left+'px';
			top = 0;
			overflow = 'hidden';
		}

		this.container.appendChild(this.content);


		// Resizer
		this.resizer = PNG.createImage(SKIN_URL+'images/resize.png', 12, 12);
		with (this.resizer.style) {
			position = 'absolute';
			bottom = (this.padding.bottom+1)+'px';
			right = (this.padding.right+1)+'px';
			cursor = 'se-resize';
			display = 'none';
		}
		this.container.appendChild(this.resizer);
		GEvent.bindDom(this.resizer, 'mousedown', this, this.startResize);
		GEvent.bindDom(this.container, 'mousedown', this, this.bringToFront);

		this.resizable(true);
		this.movable(true);

		this.container.style.visibility = 'hidden';


		// Add panel into DOM
		realWorld.content.appendChild(this.container);

		this.setZ(FloatingPanel.numberOfPanels);

		GEvent.trigger(FloatingPanel, 'created', this);

		this.resize(this.width, this.height);

	},
	__destruct: function()
	{
		this.endDrag();
		if (this.container && this.container.parentNode) {
			this.container.parentNode.removeChild(this.container);
		}
		for (x in FloatingPanel.openPanels) {
			var p = FloatingPanel.openPanels[x];
			if (p == this) {
				delete FloatingPanel.openPanels[x];
				delete p;
				FloatingPanel.numberOfPanels--;
				break;
			}
		}
        GEvent.clearListeners(this);
	},
	setTitle: function(title)
	{
		while (this.title.firstChild.firstChild) {
			this.title.firstChild.removeChild(this.title.firstChild.firstChild);
		}
		this.title.firstChild.appendChild(document.createTextNode(title));
	},
	setToolbar: function(node)
	{
		if (!this.toolbar) {
			this.toolbar = T.div('test');
			this.container.insertBefore(this.toolbar, this.content);
		}
	},
	setContent: function(node)
	{
		while (this.content.firstChild) {
			this.content.removeChild(this.content.firstChild);
		}
		this.content.appendChild(node);
	},
	addContent: function(node)
	{
		this.content.appendChild(node);
	},
	resizable: function(status)
	{
		if (status === undefined) {
			return this._resizable;
		}
		this._resizable = status;
		if (status) {
			this.resizer.style.display = 'block';
		} else {
			this.resizer.style.display = 'none';
		}
	},
	movable: function(status)
	{
		if (status === undefined) {
			return this._movable;
		}
		this._movable = status;
		if (this._movable) {
			this.title.style.cursor = 'move';
		} else {
			this.title.style.cursor = 'default';
		}
	},
	resize: function(width, height, animated, aniStep)
	{
		var aniStep = aniStep || 20;
		if (animated) {
			if (this._aniResizeTimer) {
				clearTimeout(this._aniResizeTimer);
			}
			this._targetWidth = width || this.width;
			this._targetHeight = height || this.height;
			this.aniResize(width<this.width?-aniStep:aniStep, height<this.height?-aniStep:aniStep);
		} else {
			this.width = width || this.width;
			this.height = height || this.height;

			
			this.title.style.width = (this.width -this.padding.left -this.padding.right) +'px';
			this.content.style.top = (this.title.offsetHeight +this.padding.top) +'px';
			this.content.style.width = (this.width -this.padding.left -this.padding.right) +'px';
			this.content.style.height = (this.height -this.title.offsetHeight -this.padding.top - this.padding.bottom) +'px';
			this.box.resize(this.width, this.height);

			// Resize busy indicator
			if (this._busyIndicator) {
				this._busyIndicator.style.top = this.content.style.top;
				this._busyIndicator.style.left = this.content.style.left;
				this._busyIndicator.style.width = this.content.style.width;
				this._busyIndicator.style.height = this.content.style.height;
			}
		}
		GEvent.trigger(this, 'resize', this);
	},
	aniResize: function(w_inc, h_inc)
	{
		var newW = this.width + w_inc;
		var newH = this.height + h_inc;
		if (w_inc < 0 && newW < this._targetWidth) {
			newW = this._targetWidth;
		} else if (w_inc > 0 && newW > this._targetWidth) {
			newW = this._targetWidth;
		}
		if (h_inc < 0 && newH < this._targetHeight) {
			newH = this._targetHeight;
		} else if (h_inc > 0 && newH > this._targetHeight) {
			newH = this._targetHeight;
		}

		this.resize(newW, newH);

		if (newW != this._targetWidth || newH != this._targetHeight) {
			if (newW == this._targetWidth) {
				w_inc = 0;
			}
			if (newH == this._targetHeight) {
				h_inc = 0;
			}

			this._aniResizeTimer = setTimeout(GEvent.callback(this, function() {
				this.aniResize(w_inc, h_inc);
			}), 10);
		}
	},
	resizeToContent: function(animated)
	{
		GEvent.trigger(this, 'beforeresizetocontent');
		// FIXME : This ugly height hack is because IE sucks and can't obtain the correct scrollHeight
		this.content.style.height = '30000px';
		if (this.content.scrollHeight >= 20000) {
			this.content.style.height = 'auto';
		}
		var height = this.content.scrollHeight+this.title.scrollHeight+this.padding.top+this.padding.bottom;
        this.content.scrollTop = 0; // Fix for firefox chopping off the top
		this.resize(false, height, animated);
	},
	move: function(left, top)
	{
		if (left !== false) {
			this.container.style.left = left +'px';
		}
		if (top !== false) {
			this.container.style.top = top +'px';
		}
	},
	getX: function()
	{
		return this.container.offsetLeft;
	},
	getY: function()
	{
		return this.container.offsetTop;
	},
	getZ: function()
	{
		return this._z;
	},
	setZ: function(z)
	{
		this._z = z;
		this.container.style.zIndex = z;
	},
	getWidth: function()
	{
		return this.container.offsetWidth;
	},
	getHeight: function()
	{
		return this.container.offsetHeight;
	},
	getContentWidth: function()
	{
		return this.content.offsetWidth;
	},
	getContentHeight: function()
	{
		return this.content.offsetHeight;
	},
	startDrag: function(e)
	{
		if (!this._movable) {
			return;
		}
		this.endDrag();
		this.endResize();
		this._dragOffsetX = this.getX() - e.clientX;
		this._dragOffsetY = this.getY() - e.clientY;

		this._dragHandler = GEvent.bindDom(document.documentElement, 'mousemove', this, this.doDrag);
		this._dragEndHandler = GEvent.bindDom(document.documentElement, 'mouseup', this, this.endDrag);
		setOpacity(this.container, 0.8, true);
		GEvent.trigger(this, 'dragstart', this);
		GEvent.stop(e);
	},
	doDrag: function(e)
	{
		var left = (e.clientX + this._dragOffsetX);
		var top = (e.clientY + this._dragOffsetY);

		// Only restrict positioning if the panel isn't stuck to the map
		if (!this.sticky) {
			if (left < -this.getWidth() /2) {
				left = -this.getWidth() /2;
			} else if (left > document.body.clientWidth - this.getWidth() /2) {
				left = document.body.clientWidth - this.getWidth() /2;
			}
			if (top < 24) {
				top = 24;
			} else if (top > this.container.parentNode.clientHeight - 26) {
				top = this.container.parentNode.clientHeight - 26;
			}
		}
		this.move(left, top);
		GEvent.trigger(this, 'drag', this);
		GEvent.stop(e);
	},
	endDrag: function(e)
	{
		if (this._dragHandler) {
			GEvent.removeListener(this._dragHandler);
			delete this._dragHandler;
		}
		if (this._dragEndHandler) {
			GEvent.removeListener(this._dragEndHandler);
			delete this._dragEndHandler;
		}
		if (e) {
			if (this._stickyPoint) {
				delete this._stickyPoint;
				this.stickToMap();
			}
			GEvent.trigger(this, 'dragend', this);
			setOpacity(this.container, 1, true);
		}
	},
	startResize: function(e)
	{
		this.endDrag();
		this.endResize();
		this._resizeOffsetX = (this.getX()+this.getWidth()) - e.clientX;
		this._resizeOffsetY = (this.getY()+this.getHeight()) - e.clientY;

		this._resizeHandler = GEvent.bindDom(document.documentElement, 'mousemove', this, this.doResize);
		this._resizeEndHandler = GEvent.bindDom(document.documentElement, 'mouseup', this, this.endResize);
		GEvent.trigger(this, 'resizestart', this);
		GEvent.stop(e);
	},
	doResize: function(e)
	{
		var width = e.clientX - this.getX() +this._resizeOffsetX;
		var height = e.clientY - this.getY() +this._resizeOffsetY;
		if (width < this.minimumSize.width) {
			width = this.minimumSize.width;
		} else if (width > 1000) {
			width = 1000;
		}
		if (height < this.minimumSize.height) {
			height = this.minimumSize.height;
		} else if (height > 1000) {
			height = 1000;
		}
		this.resize(width, height);
		GEvent.stop(e);
	},
	endResize: function(e)
	{
		if (this._resizeHandler) {
			GEvent.removeListener(this._resizeHandler);
			delete this._resizeHandler;
		}
		if (this._resizeEndHandler) {
			GEvent.removeListener(this._resizeEndHandler);
			delete this._resizeEndHandler;
		}
		if (e) {
			GEvent.trigger(this, 'resizeend', this);
		}
	},
	bringToFront: function()
	{
		for (var i=0; i<FloatingPanel.openPanels.length; i++) {
			var p = FloatingPanel.openPanels[i];
			if (!p || p == this) {
				continue;
			}

			//p.box.setImage('skins/'+p.skin+'/panel/frame-dull.png');
			if (p.getZ() > this.getZ()) {
				p.setZ(p.getZ()-1);
			}
		}

		//this.box.setImage('skins/'+this.skin+'/panel/frame.png');
		this.setZ(FloatingPanel.numberOfPanels);
	},
	hide: function()
	{
		this.visible = false;
		// IE can't change the opacity of PNGs properly, so don't bother
		if (typeof this.container.style.filter != 'string') {
			this._opacity = 1;
			setOpacity(this.container, 1);
			setTimeout(GEvent.callback(this, this.fadeOut), 10);
		} else {
			GEvent.trigger(this, 'hide', this);
			this.container.style.visibility = 'hidden';
		}
	},
	show: function()
	{
		this.visible = true;
		// IE can't change the opacity of PNGs properly, so don't bother
		if (typeof this.container.style.filter != 'string') {
			this._opacity = 0;
			setOpacity(this.container, 1);
			//setTimeout(GEvent.callback(this, this.fadeIn), 10);
		}
		this.container.style.visibility = 'visible';
		GEvent.trigger(this, 'show');

		if (this.sticky) {
			this.panIntoView();
		}
	},
	panIntoView: function()
	{
		if (!this.sticky) {
			return;
		}

		var map = this._stickyMap;
		if (!map) {
			return;
		}

		this.attachAnchor(this._attachedAnchor);

		// Calculate the screen X/Y pixel of the map
		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		var corner = new GPoint(this.getX() + sw.x, this.getY() + ne.y);
		var centre = new GPoint(this.getX() + sw.x + (this.getWidth()/2), this.getY() + ne.y + (this.getHeight()/2));
		corner = map.fromDivPixelToLatLng(corner);
		centre = map.fromDivPixelToLatLng(centre);

		// If the top left corner isn't on screen, then we need to pan to see the panel
		//if (!map.getBounds().containsLatLng(corner)) {
			map.setCenter(centre);
			//map.panTo(centre);
		//}

	},
	fadeIn: function()
	{
		this._opacity += 0.25;
		// Round off due to floating point errors
		this._opacity = Math.roundDP(this._opacity, 2);

		if (this._opacity > 1) {
			this._opacity = 1;
		}
		setOpacity(this.container, this._opacity);
		if (this._opacity < 1) {
			setTimeout(GEvent.callback(this, this.fadeIn), 10);
		}
	},
	fadeOut: function()
	{
		this._opacity -= 0.25;
		// Round off due to floating point errors
		this._opacity = Math.roundDP(this._opacity, 2);

		if (this._opacity < 0) {
			this._opacity = 0;
		}
		setOpacity(this.container, this._opacity);
		if (this._opacity > 0) {
			setTimeout(GEvent.callback(this, this.fadeOut), 10);
		} else {
			GEvent.trigger(this, 'hide', this);
			this.container.style.visibility = 'hidden';
		}
	},
	close: function(force)
	{
		if (this.busy() && !force) {
			//return;
		}
		GEvent.trigger(this, 'beforeclose', this);
		if (this.preventClosing) {
			return;
		}

		// Hide to cause fade out, and kill window once hidden
		GEvent.bind(this, 'hide', this, function() {
			GEvent.trigger(this, 'close', this);
			this.__destruct();
		});
		this.hide();
	},
	updateStickyPanelPosition: function(map)
	{
		var map = map || this._stickyMap;
		if (!map) {
			return false;
		}
		// Calculate the screen X/Y pixel of the map
		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		if (!this._stickyPoint) {
			this._stickyPoint = new GPoint(this.getX()+sw.x, this.getY()+ne.y);
		}

		var point = new GPoint(this._stickyPoint.x, this._stickyPoint.y);
		point.x -= sw.x;
		point.y -= ne.y;
		this.move(point.x, point.y);
	},
	stickToMap: function(map)
	{
		var map = map || this._stickyMap;
		
		if (!map) {
			
			return;
		}
		this.sticky = true;
		this._stickyMap = map;
		if (!this._stickyEventHandlers) {
			this._stickyEventHandlers = [
				GEvent.bind(map, 'move', this, this.updateStickyPanelPosition),
				GEvent.bind(map, 'moveend', this, this.updateStickyPanelPosition),
				GEvent.bind(map, 'moveend', this, function() { this.attachAnchor(this._attachedAnchor); })
			];
		}

		this.updateStickyPanelPosition();


        // Clean up to avoid memory leaks
        GEvent.bind(this, 'close', this,
            function()
            {
                this.unstickFromMap();
                this.detachAnchor();
            }
        );

	},
	unstickFromMap: function()
	{
		this.sticky = false;
		delete this._stickyPoint;
		delete this._stickyMap;
        if (this._stickyEventHandlers) {
            for (var i=0; i<this._stickyEventHandlers.length; i++) {
                GEvent.removeListener(this._stickyEventHandlers[i]);
            }
            delete this._stickyEventHandlers;
        }
	},
	getAnchorPoint: function(map)
	{
		var map = map || this._stickyMap;
		if (!map) {
			return false;
		}

		// Calculate the screen X/Y pixel of the map
		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		var point = new GPoint(this.getX()+sw.x+this.anchor.x, this.getY()+ne.y+this.anchor.y);
		return point;
	},
	getAnchorLatLng: function(map)
	{
		return map.fromDivPixelToLatLng(this.getAnchorPoint(map));
	},
	attachAnchor: function(point, map)
	{
		var map = map || this._stickyMap;
		if (!map) {
			return false;
		}

		var sw = map.fromLatLngToDivPixel(map.getBounds().getSouthWest());
		var ne = map.fromLatLngToDivPixel(map.getBounds().getNorthEast());

		if (point instanceof GLatLng) {
			this._attachedAnchor = point;
			point = map.fromLatLngToDivPixel(point);
		} else {
			this._attachedAnchor = map.fromDivPixelToLatLng(point);
		}

		point.x -= sw.x;
		point.y -= ne.y;

		

		this.move(point.x-this.anchor.x, point.y-this.anchor.y);
		if (this._stickyPoint) {
			delete this._stickyPoint;
			this.stickToMap();
		}
		this.resizable(false);
		this.movable(false);

		// Listen for zooms to adjust position
		if (!this._anchorZoomListener) {
			this._anchorZoomListener = GEvent.bind(map, 'zoomend', this, function() {
					this.attachAnchor(this._attachedAnchor);
			});
		}
	},
	detachAnchor: function()
	{
		if (this._anchorZoomListener) {
			GEvent.removeListener(this._anchorZoomListener);
			delete this._anchorZoomListener;
		}
		delete this._attachedAnchor;
	},
	busy: function(status)
	{
		if (status === undefined) {
			return this._busy;
		}
		if (this._busy == status) return;

		this._busy = status;
		if (this._busy) {
			if (!this._busyIndicator) {
				this._busyIndicator = document.createElement('div');
				this._busyIndicator.appendChild(document.createElement('div'));
				this._busyIndicator.className = 'panel_busy_container';
				this._busyIndicator.firstChild.className = 'panel_busy_indicator';


				this.container.appendChild(this._busyIndicator);
				with (this._busyIndicator.style) {
					position = 'absolute';
				}
				with (this._busyIndicator.firstChild.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
				}
				PNG.setImage(SKIN_URL+'images/busy_background.png', this._busyIndicator, true);
			}
			this._busyIndicator.style.display = 'block';
		} else {
			if (this._busyIndicator) {
				this._busyIndicator.style.display = 'none';
			}
		}
		// XXX Forces the busyIndicator box to be resized correctly in some browsers
		this.resize();
	}
};
SceneItem = new Class();
SceneItem.items = {};
SceneItem.getItems = function(type, bounds)
{
	var items = [];
	for (var i in SceneItem.items) {
		var si = SceneItem.items[i];
		if (si.properties.class_name != type) {
			continue;
		}
		// If it's within the bounds, keep it
		if (si.bounds.intersects(bounds)) {
			items.push(si);
		}
	}
	return items;
};
SceneItem.inertAllItems = function(state)
{
	SceneItem.all_inert = state;
	for (var i in SceneItem.items) {
		var si = SceneItem.items[i];
		si.inert(state);
	}
	GEvent.trigger(SceneItem, 'inertall', state);
};
SceneItem.hideItems = function(map, type, bounds)
{
	for (var i in SceneItem.items) {
		var si = SceneItem.items[i];
		if (si.properties.class_name != type) {
			continue;
		}
		if (!si.editing && !si.enabled) {
			si.hide();
		}
	}
};
SceneItem.hideRoutes = function(map, bounds)
{
	for (var i in SceneItem.items) {
		var si = SceneItem.items[i];
		if (si.properties.class_name != 'Route') {
			continue;
		}
		if (bounds) {
			if (!si.editing && !si.enabled && si.points.length > 0 && bounds.contains(si.points[0])) {
				si.hide();
			}
		} else {
			if (si.enabled) {
				si.activate();
			}
			si.hide();
		}
		// If it's within the bounds, hide it
		/*
		if (si.bounds.intersects(bounds)) {
			si.hide();
		}
		*/
	}
};
SceneItem.unserialise = function(o, map, ident)
{
	if (!o) {
		
		return false;
	}
	if (ident && o.ident != ident) {
		return;
	}
	
	
	var total = o.total;
	var page = o.page;
	var perpage = o.perpage;
	var ne = o.ne;
	var sw = o.sw;
	if (o.items) {
		o = o.items;
	} else if (!(o instanceof Array)) {
		
		o = [o];
	}

	

	/** DELETEME
	// Update route navigation box. FIXME this shouldn't be in this method
	if (perpage && map._routeTally) {
		var from = ((perpage*(page-1))+1);
		var to = (perpage*page);
		if (to > total) {
			to = total;
		}

		var str = '';
		if (map.gMap.getZoom() < MIN_ZOOM) {
			// We're zoomed too far out to show anything
			str = 'Zoom in closer to see the routes';
			total = 0;
		} else if (!total) {
			// There's no routes here :(
			str =  'There are no routes';
		} else if (from > to) {
			// We're off the end of the list, reload from page 1
			map._world.page = 1;
			map._world.updateVisibleMap(1);
			str =  'Loading routes...';
		} else {
			str =  from + ' to ' + to + ' of ' + total;
		}



		while (map._routeTally.firstChild) {
			map._routeTally.removeChild(map._routeTally.firstChild);
		}
		map._routeTally.appendChild(document.createTextNode(str));
		map._routeTally.appendChild(T.div('in this area'));

		// Disable buttons as required
		if (!total || from == 1) {
			map._routePrev.disable();
		} else {
			map._routePrev.enable();
		}
		if (!total || to >= total) {
			map._routeNext.disable();
		} else {
			map._routeNext.enable();
		}
	}
	*/

	// Hide all routes on screen (only those sent by the server will be reshown later
	if (sw && ne) {
		SceneItem.hideRoutes(map, new GLatLngBounds(new GLatLng(sw[0], sw[1]), new GLatLng(ne[0], ne[1])));
		SceneItem.hideItems(map, 'Marker', new GLatLngBounds(new GLatLng(sw[0], sw[1]), new GLatLng(ne[0], ne[1])));
	}

	// Keep track of all items on screen
	SceneItem.visibleItems = {
		'Marker': [],
		'Route': [],
		'PubCrawl': []
	};

	// Unserialise each object
	for (var i=0; i<o.length; i++) {
		if (!o[i].class_name) {
			continue;
		}
		var item = SceneItem.unserialise_object(o[i], map);
		SceneItem.visibleItems[o[i].class_name].push(item);
	}
	

	if (o.length > 0) {
		return o[o.length-1].uid;
	} else {
		return false;
	}
};
SceneItem.unserialise_object = function(obj, map)
{
	try {
		
		// TODO Hide visible items that aren't in the incoming data
		// Item is already on the map, so show it rather than rebuild it
		if (SceneItem.items[obj.uid]) {
			// Check if it's filtered out, no need to show it then
			if (SceneItem.items[obj.uid].show && !realWorld.activeFilters[obj.class_name][obj.category]) {
				SceneItem.items[obj.uid].show();
			}

			
			return SceneItem.items[obj.uid];
		}
		

		// Create the item
		var newItem = eval('new '+obj.class_name);	// FIXME: eval() == evil()
		newItem.setMap(map);
		newItem.updateProperties(obj);
		
		// add it to our list using it's unique ID as the key
		SceneItem.items[obj.uid] = newItem;
		realWorld.addItem(newItem);
		// Set it inert if required
		newItem.inert(SceneItem.all_inert);
		
		return SceneItem.items[obj.uid];
	} catch(e) {
		
		
	}
};
SceneItem.prototype = {
	__construct: function(map)
	{
		this.map = map || false;
		this.editing = false;
		this._editEventHandlers = [];
		// Propeties are loaded as soon as the item appears in the view port
		this.properties = {};

		// Data is only loaded when needed, e.g. onclick
		this.data = {};

		// Pretty much just for makers, this is a list of media id's
		this.gallery = [];
	},
	toString: function()
	{
		return '[object SceneItem]';
	},
	setMap: function(map)
	{
		this.map = map;
	},
	inert: function()
	{
	},
	destroy: function()
	{
		if (this.properties.uid) {
			RPC.postData('delete_item/', 'id='+parseInt(this.properties.uid), GEvent.callback(this,
				function(rpc) {
					if (rpc.responseText == '1') {
						delete SceneItem.items[this.properties.uid];
					} else {
						alert('There was an error deleting your item.');
					}
				}
			));
		}
	},
	addLatLng: function(point)
	{
	},
	discard: function()
	{
	},
	enableEditing: function()
	{
		if (this.editing) {
			return;
		}
		this.map._editing = this;
		
		SceneItem.inertAllItems(true);
		this.editing = true;
		this._editEventHandlers = [
			GEvent.bind(this.map, 'click', this,
				function(point) {
					if (point) {
						this.addLatLng(point);
					}
				}
			)
		];
	},
	disableEditing: function()
	{
		if (!this.editing) {
			return;
		}
		delete this.map._editing;
		
		SceneItem.inertAllItems(false);
		this.editing = false;
		for (var i=0; i<this._editEventHandlers.length; i++) {
			GEvent.removeListener(this._editEventHandlers[i]);
		}

		if (this.closeCreatePanel) {
			this.closeCreatePanel();
		}
	},
	view: function()
	{
	},
	hide: function()
	{
		this._hidden = true;
	},
	show: function()
	{
		this._hidden = false;
	},
	redraw: function()
	{
	},
	serialise: function()
	{
		if (!this.properties) {
			return false;
		}
		return {
			properties: serialise(this.properties),
			data: serialise(this.data),
			gallery: serialise(this.gallery)
		};
	},
	unserialise: function(JSON)
	{
		this.properties = eval('('+JSON+')');
		return this;
	},
	isSceneItem: function()
	{
		return true;
	},
	save: function()
	{
		
		var json = this.serialise();
		RPC.postData('save_scene/', 'properties='+escape(json['properties'])+'&data='+escape(json['data'])+'&gallery='+escape(json['gallery']), GEvent.callback(this, function(rpc) {
			//alert(rpc.responseText);
			SceneItem.items[rpc.responseText] = this;
			this.properties.uid = rpc.responseText;
		}));
	},
	activate: function()
	{
	}
};
SceneItem.visibleItems = {
	'Marker': [],
	'Route': [],
	'PubCrawl': []
};
PopoutPanel = new Class(GOverlay);
PopoutPanel.prototype = {
__construct:
	function(world, anchor, width, height, single_instance)
	{
		if (single_instance) {
			if (PopoutPanel._current_instance) {
				PopoutPanel._current_instance.close();
			}
			PopoutPanel._current_instance = this;
			GEvent.addListener(this, 'close', function() { delete PopoutPanel._current_instance; });
		}
		this.world = world;

		if (anchor instanceof Array) {
			var anchor = new GLatLng(anchor[0], anchor[1]);
		}
		this.anchor = anchor;

		var w = width || 400;
		var h = height || 300;
		this.width = w;
		this.height = h;

		this.status_bar_size = 0;

		if (!this.padding) {
			this.padding = {
				left: 8,
				top: 14,
				right: 149,
				bottom: 24
			};
		}

		if (!this.attach_point) {
			this.attach_point = {
				x: -18,	// x is from the right
				y: 144  // y is from the top
			};
		}

		if (!this.box) {
			this.box = new Box(SKIN_URL + 'images/balloon_panel.png', 930, 580, w, h, 32, 32, 48, 180);
			with (this.box.container.style) {
				position = 'absolute';
				left = 0;
				top = 0;
			}
		}



		if (!this.element) {
			this.element = T.div({'class': 'popout_panel'}, [this.box.container]);
		}

		

		with (this.element.style) {
			position = 'absolute';
			width = w + 'px';
			height = h + 'px';
		}

		// Title bar area
		this.titlebar = T.div({'class': 'popout_title'}, [T.div({'class': 'title_content'})]);
		with (this.titlebar.style) {
			cursor = 'auto';
			position = 'absolute';
			left = this.padding.left + 'px';
			top = this.padding.top + 'px';
			width = (w - this.padding.left - this.padding.right) + 'px';
		}
	
		this.element.appendChild(this.titlebar);

		// Close button
		this.close_button = T.a({'href': 'javascript:void(0)'});
		this.close_button.className = 'panel_close';
		with (this.close_button.style) {
			position = 'absolute';
			right = '5px';
			top = '1px';
			width = '20px';
			height = '20px';
			background = 'url('+SKIN_URL+'images/panel_close.gif)';
			zIndex = '2000';
			cursor = 'pointer'
		}
		GEvent.bindDom(this.close_button, 'mouseover', this.close_button, function(e) { this.style.backgroundPosition = '0 20px'; });
		GEvent.bindDom(this.close_button, 'mouseout', this.close_button, function(e) { this.style.backgroundPosition = '0 0'; });
		GEvent.bindDom(this.close_button, 'click', this, function() { this.close() });
		this.titlebar.appendChild(this.close_button);


		// Main content area
		this.content_container = T.div({'class': 'panel_content_container'});
		this.content = T.div({'class': 'panel_content'});
		this.content_container.appendChild(this.content);
		with (this.content_container.style) {
			position = 'absolute';
			left = this.padding.left + 'px';
			top = this.padding.top + 'px';
			width = (w - this.padding.left - this.padding.right) + 'px';
			height = (h - this.padding.top - this.padding.bottom) + 'px';
			overflow = 'auto';
			cursor = 'auto';
		}
		with (this.content.style) {
			zoom = 1;
			overflow = 'hidden';
		}
		try {
			this.content_container.style.overflowX = 'hidden';
		} catch(err) { }

		
		// Prevent default map events
		this._map_events = [
			'click',
			'dblclick',
			'mousedown',
			'mouseup',
			'mousewheel',
			'contextmenu',
			'DOMMouseScroll'
		];
		for (x in this._map_events) {
			GEvent.addDomListener(this.titlebar, this._map_events[x], function(e) { GEvent.stop(e, false); } );
			GEvent.addDomListener(this.content_container, this._map_events[x], function(e) { GEvent.stop(e, false); } );
		}
		
		this.element.appendChild(this.content_container);

		this.world.map.addOverlay(this);
		this.centre_on_map();
	},
set_content:
	function(content, html)
	{
		document.emptyElement(this.content);
		if (html) {
			this.content.innerHTML = content;
		} else {
			if (typeof content == 'string') {
				var content = document.createParagraph(content);
			}
			this.content.appendChild(content);
		}
	},
set_title:
	function(title)
	{
		document.emptyElement(this.titlebar.firstChild);
		this.titlebar.firstChild.appendChild(document.createTextNode(title));
		this.fix_size();
	},
resize_to_content:
	function(max_width, max_height)
	{
		this.content_container.style.height = '30000px';
		if (this.content_container.scrollHeight >= 20000) {
			this.content_container.style.height = 'auto';
		}
		this.content_container.style.width = '30000px';
		if (this.content_container.scrollWidth >= 20000) {
			this.content_container.style.width = 'auto';
		}
		var width = this.content_container.scrollWidth + this.padding.left + this.padding.right;
		var height = this.content_container.scrollHeight + this.titlebar.scrollHeight + this.padding.top + this.padding.bottom;
		if (max_width && width > max_width) {
			width = max_width;
		}
		if (max_height && height > max_height) {
			height = max_height;
		}

		this.resize(width, height);
	},
resize:
	function(width, height)
	{

		if (!this.element) {
			
			return false;
		}
		this.status_bar_size = this.status_bar ? 24 : 0;

		var width = width || this.width;
		var height = height || this.height;

		this.width = width;
		this.height = height;

		this.box.resize(this.width, this.height + this.status_bar_size);

		with (this.element.style) {
			width = this.width + 'px';
			height = (this.height + this.status_bar_size) + 'px';
		}
		with (this.titlebar.style) {
			width = this.get_container_width() + 'px';
		}
		with (this.content_container.style) {
			width = this.get_container_width() + 'px';
			height = this.get_container_height() + 'px';
		}
		this.fix_size();
		this.redraw();
		GEvent.trigger(this, 'resize');
	},
get_centre_latlng:
	function()
	{
		var left = this.element.offsetLeft + this.padding.left;
		var right = this.element.offsetLeft + this.width;
		var top = this.element.offsetTop + this.padding.top;
		var bottom = this.element.offsetTop + this.height;
		var width = right - left;
		var height = bottom - top;

		var centre_x = left + (width/2);
		var centre_y = top + (height/2);

		var point = this.world.map.fromDivPixelToLatLng(new GPoint(centre_x, centre_y));
		return point;
	},
centre_on_map:
	function()
	{
		this.world.jump_to_point(this.get_centre_latlng());
	},
fix_size:
	function()
	{
		var t = (this.padding.top + this.titlebar.offsetHeight);
		var h = this.height - t - this.padding.bottom;
		with (this.content_container.style) {
			top = t + 'px';
			height = h + 'px';
		}
	},
initialize:
	function(map)
	{
		map.getPane(G_MAP_FLOAT_PANE).appendChild(this.element);
		this.redraw();
	},
remove:
	function()
	{
		this.world.map.getPane(G_MAP_FLOAT_PANE).removeChild(this.element);
	},
copy:
	function()
	{
	},
close:
	function()
	{
		if (!this.element) {
			return false;
		}
		this.world.map.removeOverlay(this);
		delete this.element;
		GEvent.trigger(this, 'close');
	},
redraw:
	function(force)
	{
		var p = this.world.map.fromLatLngToDivPixel(this.anchor);
		this.element.style.left = (p.x - this.width - this.attach_point.x) +'px';
		this.element.style.top = (p.y - this.attach_point.y) +'px';
		this.fix_size();
	},
busy:
	function(state)
	{
		if (this.is_busy == status || status === undefined) {
			return this.is_busy;
		}
		
		this.is_busy = state;

		switch (this.is_busy) {
		case true:
			if (!this._busy_indicator) {
				this._busy_indicator = document.createElement('div');
				this._busy_indicator.appendChild(document.createElement('div'));
				this._busy_indicator.className = 'panel_busy_container';
				this._busy_indicator.firstChild.className = 'panel_busy_indicator';


				this.content_container.appendChild(this._busy_indicator);
				with (this._busy_indicator.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
					zIndex = 1000;
				}
				with (this._busy_indicator.firstChild.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
				}
				PNG.setImage(SKIN_URL + 'images/busy_background.png', this._busy_indicator, true);
			}
			this.content_container.style.overflow = 'hidden';
			this._busy_indicator.style.display = 'block';
			break;
		case false:
			this.content_container.style.overflow = 'auto';
			this._busy_indicator.style.display = 'none';
			break;
		}
	},
get_container_width:
	function()
	{
		return this.width - this.padding.left - this.padding.right;
	},
get_container_height:
	function()
	{
		return this.height - this.padding.top - this.padding.bottom;
	},
set_status_bar:
	function(el)
	{
		if (!this.status_bar) {
			this.status_bar = T.div({'class': 'panel_status_bar'});
			this.element.appendChild(this.status_bar);
			with (this.status_bar.style) {
				position = 'absolute';
				bottom = this.padding.bottom + 'px';
				left = this.padding.left + 'px';
				width = this.get_container_width() + 'px';
			}

			// Prevent events hitting the map
			for (x in this._map_events) {
				GEvent.addDomListener(this.status_bar, this._map_events[x], function(e) { GEvent.stop(e, false); } );
			}
		} else {
			document.emptyElement(this.status_bar);
		}
		this.status_bar.appendChild(el);
		
		this.resize();	// resize adds room for status bar if it exists
	}
};



BalloonPanel = new Class(FloatingPanel);
BalloonPanel.prototype = {
	__construct: function(x, y, width, height, single_instance)
	{

		if (single_instance) {
			if (BalloonPanel._single_instance_panel) {
				BalloonPanel._single_instance_panel.close(true);
			}
			BalloonPanel._single_instance_panel = this;

			// Delete reference to this panel when it's closed
			GEvent.addListener(this, 'close',
				function()
				{
					if (BalloonPanel._single_instance_panel == this) {
						delete BalloonPanel._single_instance_panel;
					}
				}
			);
		}
		
		// All these come from base class (FloatingPanel)
		var x = x || 100;
		var y = y || 100;
		var w = width || 400;
		var h = height || 300;


		if (!this.padding) {
			this.padding = {
				left: 3,
				top: 1,
				right: 147,
				bottom: 16
			};
		}

		if (!this.anchor) {
			this.anchor = {
				x: w+5,
				y: 210
			};
		}

		if (!this.box) {
			this.box = new Box(SKIN_URL+'images/balloon_panel.png', 1000, 1000, w, h, 8, 16, 16, 150);
		}
		FloatingPanel.prototype.__construct.call(this, x, y, width, height);

		// TODO: Make this a method: This panel is printable
		$(this.container).removeClass('gmnoprint');

		this.updateAnchor();
		GEvent.bind(this, 'resize', this, this.updateAnchor);
	},
	updateAnchor: function()
	{
		this.anchor.x = this.getWidth()+5;
	}
};
Gallery = new Class;
Gallery.prototype = {
__construct:
	function(username)
	{
		this.username    = username || '';
		this.element     = T.div({'class': 'gallery_container'});
		this.description = T.div({'class': 'gallery_description'});
		this.media       = T.div({'class': 'gallery_media'}, [T.ul()]);

		this.element.appendChild(this.description);
		this.element.appendChild(this.media);
	},
set_description:
	function(description, html)
	{
		document.emptyElement(this.description);
		if (html) {
			this.description.innerHTML = description;
		} else {
			if (typeof description == 'string') {
				description = document.createParagraph(description);
			}
			this.description.appendChild(description);
		}
	},
add_media_from_array:
	function(media)
	{
		for (var i=0; i<media.length; i++) {
			this.add_media(
				media[i].type,
				media[i].thumb_filename,
				media[i].large_filename
			);
		}
	},
add_media:
	function(type, thumb, src)
	{
		var img = T.li([
			T.a({'href':'javascript:void(0)'}, [
				T.img({'src': MEDIA_URL + src})
			])
		]);
		this.media.firstChild.appendChild(img);

		if (this.media.firstChild.childNodes.length == 1) {
			this.element.className = 'gallery_container single_media';
		} else {
			this.element.className = 'gallery_container multi_media';
		}




		// Triggered when media is clicked, shows the item in a popup
		GEvent.bindDom(img.firstChild, 'click', this,
			function() {
				switch (type) {
				case 'IMAGE':
					var media = T.div({'className':'gallery_image'}, [
							T.img({'src':MEDIA_URL + src})
						]);
					break;
				case 'VIDEO':
					var media = T.div({'className':'gallery_image'}, [
							T.video({'src':MEDIA_URL + src})
						]);
					break;
				default:
					
					break;
				}
				var popup = new ModalPanel(534, 475);
				var content = T.div({'className':'gallery_view'}, [
					media,
					T.div({'className':'gallery_user'}, [
						T.span('By '),
						T.a({'href':'http://realbuzz.com/en-gb/users/' + escape(this.username),'target':'_blank'}, this.username)
					])
				]);
				popup.setContent(content);
				popup.setTitle('');
				popup.resizable(false);
				popup.show();

				// XXX This hack is for IE, without it the flash video player
				// doesn't get destroyed and will play in the background forever
				GEvent.bind(popup, 'close', this,
					function() {
						try { 
							media.innerHTML = '';
						} catch(e) {}
						delete media;
					}	// function
				);
			}	// function
		);
	}
};




TabbedPopoutPanel = new Class(PopoutPanel);
TabbedPopoutPanel.prototype = {
__construct:
	function(world, anchor, width, height, single_instance)
	{
		PopoutPanel.prototype.__construct.apply(this, arguments);

		this.tab_bar = new TabBar();
		this.element.appendChild(this.tab_bar.element);
		this.tab_elements = [];

		// Catch map events from bubbling from tabbar
		var map_events = [
			'click',
			'dblclick',
			'mousedown',
			'mousewheel',
			'contextmenu',
			'DOMMouseScroll'
		];
		for (x in map_events) {
			GEvent.addDomListener(this.tab_bar.element, map_events[x], function(e) { GEvent.stop(e, false); } );
		}

		GEvent.bind(this.tab_bar, 'changetab', this, function(id) { GEvent.trigger(this, 'changetab', id); });
		GEvent.bind(this.tab_bar, 'changetab', this, this.show_tab);
	},
fix_size:
	function()
	{
		PopoutPanel.prototype.fix_size.call(this);
	},
tab_busy:
	function(id, status)
	{
		this.tab_bar.tab_busy(id, status);
	},
set_tab_label:
	function(id, label)
	{
		this.tab_bar.set_tab_label(id, label);
	},
add_tab:
	function(name, content, html)
	{
		var tab_id = this.tab_bar.add_tab(name);

		this.add_tab_element(content, html);

		// If it's the first tab, activate it automagically
		if (tab_id == 0) {
			this.tab_bar.set_tab(tab_id);
		}
		return tab_id;
	},
add_tab_element:
	function(content, html)
	{
		var node = T.div({'class': 'tab_content'});
		node.style.display = 'none';

		this.tab_elements.push(node);
		var tab_id = this.tab_elements.length -1;

		this.content.appendChild(node);


		if (html) {
			node.innerHTML = content;
		} else {
			if (typeof content == 'string') {
				var content = document.createParagraph(content);
			}
			node.appendChild(content);
		}
		return tab_id;
	},
show_tab:
	function(tab)
	{
		// If the tab isn't active, then activate it.
		if (tab !== this.tab_bar.active_tab) {
			this.tab_bar.set_tab(tab);
			return;
		}
		

		for (var i=0; i<this.tab_elements.length; i++) {
			this.tab_elements[i].style.display = 'none';
		}
		this.tab_elements[tab].style.display = 'block';
	},
hide_tabs:
	function()
	{
		this.tab_bar.element.style.display = 'none';
	}
};


TabBar = new Class;
TabBar.prototype = {
__construct:
	function()
	{
		this.height = 26;

		this.element = T.div({'class': 'tab_bar'}, [T.ul()]);
		with (this.element.style) {
			position = 'absolute';
			top = (-this.height +4) + 'px';
			left = '28px';
		}
		this.tabs = [];


		// Tab class
		this.Tab = new Class;
		this.Tab.prototype = {
		__construct:
			function(tab_bar, name)
			{
				this.tab_bar = tab_bar;
				this.element = T.li([T.a({'href': 'javascript:void(0)'}, name)]);
				with (this.element.style) {
					position = 'relative';
					cssFloat = 'left';
					styleFloat = 'left';
					height = this.tab_bar.height + 'px';
					overflow = 'hidden';
				}
				with (this.element.firstChild.style) {
					//display = 'block';
					height = '100%';
					padding = '0 8px';
				}


				GEvent.bindDom(this.element.firstChild, 'click', this, function() { GEvent.trigger(this, 'click'); });
				this.deactivate();
			},
		busy:
			function(status)
			{
				if (status === undefined) {
					return this._busy;
				}
				if (this._busy == status) return;
				this._busy = status;

				if (!this._busyIndicator) {
					this._busyIndicator = T.div({'class': 'tab_busy_indicator'});
					with (this._busyIndicator.style) {
						position = 'absolute';
						right = '4px';
						top = '50%';
					}
					this.element.appendChild(this._busyIndicator);
				}
				if (this._busy) {
					this.element.style.paddingRight = '20px';
					this._busyIndicator.style.display = 'block';
				} else {
					this.element.style.paddingRight = 0;
					this._busyIndicator.style.display = 'none';
				}
			},
		set_label:
			function(label)
			{
				document.emptyElement(this.element.firstChild);
				this.element.firstChild.appendChild(T.none(label));
				if (this._busyIndicator) {
					this.element.appendChild(this._busyIndicator);
				}
			},
		activate:
			function()
			{
				this.element.className = 'tab active_tab';
				with (this.element.style) {
					top = '1px';
					height = this.tab_bar.height + 'px';
				}
			},
		deactivate:
			function()
			{
				this.element.className = 'tab inactive_tab';
				with (this.element.style) {
					top = '3px';
					height = (this.tab_bar.height -4) + 'px';
				}
			}
		};
	},
tab_busy:
	function(id, status)
	{
		this.tabs[id].busy(status);
	},
set_tab_label:
	function(id, label)
	{
		var tab = this.tabs[id];
		tab.set_label(label);
	},
add_tab:
	function(name)
	{
		var tab = new this.Tab(this, name);
		this.tabs.push(tab);
		var tab_id = this.tabs.length - 1;

		tab.element.style.zIndex = 500 - this.tabs.length;

		this.element.firstChild.appendChild(tab.element);

		GEvent.addListener(tab, 'click', GEvent.callbackArgs(this, this.set_tab, tab_id));

		return tab_id;
	},
set_tab:
	function(id)
	{
		this.active_tab = id;
		for (var i=0; i<this.tabs.length; i++) {
			(id == i) ? this.tabs[i].activate() : this.tabs[i].deactivate();
		}

		GEvent.trigger(this, 'changetab', id);
	}
};


MapItemPopout = new Class(TabbedPopoutPanel);
MapItemPopout.prototype = {
__construct:
	function(item)
	{
		this.item = item;
		TabbedPopoutPanel.prototype.__construct.call(this, item.world, item.anchor, 480, 240, true);

		// Set title and add animated busy thingy
		this.set_title(item.properties.title);
		this.busy(true);

		// Populate panel when item data has loaded
		GEvent.bind(item, 'loaddataend', this, this.populate_panel);
		item.load_data();
	},
populate_panel:
	function()
	{
		this.busy(false);
		if (this._populated) {
			return;
		}
		this._populated = true;

		var item = this.item;
		// Tab 1 -- Details
		this.add_tab('Details', item.getPopoutContent());

		// Tab 2 -- Comments
		var comments = new CommentsView(item.properties.uid);
		GEvent.bind(this, 'changetab', comments, function(id)
			{
				if (id == 1) {
					this.refresh();
				}
			}
		);
		if (isNaN(item.data.comment_count)) {
			item.data.comment_count = 0;
		}
		var comment_tab = this.add_tab('Comments [' + parseInt(item.data.comment_count, 10) + ']', comments.element);
		// Update counter when new comments are added
		GEvent.bind(comments, 'addcomment', this, function()
			{
				var count = comments.comment_count();
				this.set_tab_label(comment_tab, 'Comments [' + count + ']');
				this.item.data.comment_count = count;
			}
		);
		GEvent.bind(comments, 'startrefresh', this, function() { this.tab_busy(comment_tab, true); });
		GEvent.bind(comments, 'endrefresh',   this, function() { this.tab_busy(comment_tab, false); });

		
		// Tab 3 -- Profile
		var profileView = new ProfileView(item.properties.user.username);
		var profile_tab = this.add_tab('Author\'s profile', profileView.element);
		// Load profile when switching to tab
		GEvent.bind(this, 'changetab', profileView, function(id)
			{
				if (id == 2) {
					this.create_profile();
				}
			}
		);
		GEvent.bind(profileView, 'beforeload', this, function() { this.tab_busy(profile_tab, true); });
		GEvent.bind(profileView, 'load',       this, function() { this.tab_busy(profile_tab, false); });

		
		// BODGE! Resize panel if required
		if (!item.properties.sponsor && (item.data.html || item.properties.has_video || item.properties.has_photo)) {
			//this.resize_to_content(700, 330);
			this.resize(666, 266);
			this.centre_on_map();
		}

		// Sponsors don't have tabs
		if (item.properties.sponsor) {
			this.hide_tabs();
		} else {
			// Create the bottom bar of the popout
			var rating = T.rating({'name': 'rating', 'title': 'Click to rate this item'});
			GEvent.bind(rating, 'rate', item, item.rateItem);
			rating.style.display = 'none';

			// == RATING ==
			// TODO this should be moved to it's own method for easy updating
			// Current rating
			var currentRating = T.div({'class':'current_rating'}, [T.div()]);
			with (currentRating.style) {
				width = '80px'; // 16pixels * 5stars
				height = '16px';
			}
			var scoreWidth = (item.data.rating * 16);
			with (currentRating.firstChild.style) {
				width =  scoreWidth + 'px';
				height = '16px';
				overflow = 'hidden';
			}

			// Update score when someone votes
			GEvent.bind(item, 'rate', this, function(score, total)
			{
				// Rating is set to what the *user* voted, not the real score. This is how youtube works.
				item.data.rating = total;
				item.data.user_rating = score;
				item.data.rated = true;
				currentRating.firstChild.style.width = (score * 16) + 'px';
			});

			var userRating = item.data.rated ? 'You rated: ' + item.data.user_rating : '';
			var ratingWrapper = T.div({'class': 'rating_wrapper'}, [rating, currentRating, T.div({'class': 'user_rating'}, userRating)]);

			// Swap current score for rating widget when hovering
			GEvent.addDomListener(ratingWrapper, 'mouseover', function()
			{
				// Don't allow rating if already rated
				if (item.data.rated) {
					return;
				}
				currentRating.style.display = 'none';
				rating.style.display = 'block';
			});
			GEvent.addDomListener(ratingWrapper, 'mouseout', function()
			{
				rating.style.display = 'none';
				currentRating.style.display = 'block';
			});
			if (item.properties.user.premium) {
				premium = T.span({'class': 'premium_membership_image_small float-right myp_membership_image', 'title': 'premium member'})
			} else {
				premium = T.span({});
			}
			this.set_status_bar(T.div({'class': 'author_status_bar'}, [
				ratingWrapper,
				premium
				,T.span('Added by ')
				,item.world.user.profile_link(item.properties.user.username)

			]));
		}
	}
};
Uploader = new Class;
Uploader.instances = [];
Uploader.java_upload_callback = function(data)
{
	try {
		var data = eval('(' + data + ')');
		if (data.error_code) {
			
			alert('There was an error uploading your file: \n\n' + data.error_message);
			return;
		}
	} catch (err) {
		
		alert('The server generated an error');
		return;
	}

	if (isNaN(data.uploader_id)) {
		
		return;
	}
	
	var inst = this.instances[0];
	if (!inst) {
		
		return;
	}

	inst.upload_complete(data);
	GEvent.trigger(inst, 'uploadcomplete', data);


	
};
Uploader.prototype = {
__construct:
	function(target, width, height)
	{
		this.id = Uploader.instances.push(this) -1;
		this.uploadedFiles = [];
		this.use_java   = true;
		this.width	  = width  || "100%";
		this.height	 = height || 180;
		this.target_url = target || '';
		this.target_url += (this.target_url.indexOf('?') == -1) ? '?' : '&';
		this.target_url += 'uploader_id=' + this.id + '&java_uploader&sid=' + getCookie('PHPSESSID');


		this.element = this.use_java ? this.create_java_uploader() : this.create_html_uploader();
	},
upload_complete:
	function(data)
	{
		
		this.uploadedFiles = this.uploadedFiles.concat(data.media);
	},
create_java_uploader:
	function()
	{
		var element;
		element = T.div([T.object(
			{
				'name': 'uploader',
				'classid': 'java:com.thinfile.upload.ThinImageUpload.class',
				'type': 'application/x-java-applet',
				'archive': STATIC_URL + 'realbuzz_uploader.jar',
				'width': this.width, 'height': this.height
			}, [
			T.param({'name': 'archive', 'value': STATIC_URL + 'realbuzz_uploader.jar'}),
			T.param({'name': 'code', 'value': 'com.thinfile.upload.ThinImageUpload'}),
			T.param({'name': 'MAYSCRIPT', 'value': 'yes'}),
			T.param({'name': 'name', 'value': 'Realbuzz Uploader'}),
			T.param({'name': 'scriptable', 'value': 'true'}),
			T.param({'name': 'message', 'value': 'Drag files here to upload them.'}),
			T.param({'name': 'url', 'value': this.target_url}),
			T.param({'name': 'callback', 'value': 'Uploader.java_upload_callback'}),
			T.param({'name': 'props_file', 'value': 'http://www.realbuzz.com/static/thinupload.properties'})
		])]);
		if (IS_IE) {
			element.innerHTML = '\
				<object name="uploader" classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" width="' + this.width + '" height="' + this.height + '" codebase="http://java.sun.com/update/1.5.0/jinstall-1_5-windows-i586.cab#version=1,4,1"> \
					<param name="code" value="com.thinfile.upload.ThinImageUpload" /> \
					<param name="archive" value="' + STATIC_URL + 'realbuzz_uploader.jar" /> \
					<param name="MAYSCRIPT" value="yes" /> \
					<param name="name" value="Realbuzz Uploader" /> \
					<param name="scriptable" value="true"/> \
					<param name="message" value="Drag files here to upload them."/> \
					<param name="url" value="' + this.target_url + '"/> \
					<param name="callback" value="Uploader.java_upload_callback"/> \
					<param name="props_file" value="http://www.realbuzz.com/static/thinupload.properties"/> \
				</object>';
		}

		return element;
	},
create_html_uploader:
	function()
	{
	},
destroy:
	function()
	{
		try {
			for (var i=0; i<Uploader.instances.length; i++) {
				if (Uploader.instances[i] == this) {
					delete Uploader.instances[i];
					break;
				}
			}
			this.element.innerHTML = '';
		} catch(err) {}
	},
getUploadedIds:
	function()
	{
		var ids = [];
		for (var i=0; i<this.uploadedFiles.length; i++) {
			var f = this.uploadedFiles[i];
			if (f && f.id) {
				ids.push(this.uploadedFiles[i].id);
			}
		}
		return ids;
	}
};
GMarker.prototype.getImageTags = function()
{
	if (this.img_tags) {
		return this.img_tags;
	}
	var imgs = [];
	for (x in this) {
		if (!this[x]) {
			continue;
		}
		if (this[x].tagName == 'IMG') {
			imgs.push(this[x]);
		} else {
			for (y in this[x]) {
				if (!this[x][y]) {
					continue;
				}
				if (this[x][y].tagName == 'IMG') {
					imgs.push(this[x][y]);
				}
			}
		}
	}
	this.img_tags = imgs;
	if (imgs.length == 0) {
		return false;
	}
	return imgs;
};
GMarker.prototype.setZIndex = function(z)
{
	var imgs = this.getImageTags();
	for (var i=0; i<imgs.length; i++) {
		imgs[i].style.zIndex = z+i;
	}
	return z+i;
};

if (!("console" in window) || !("firebug" in console)) {
(function()
{
    window.console = 
    {
        log: function()
        {
            logFormatted(arguments, "");
        },
        
        debug: function()
        {
            logFormatted(arguments, "debug");
        },
        
        info: function()
        {
            logFormatted(arguments, "info");
        },
        
        warn: function()
        {
            logFormatted(arguments, "warning");
        },
        
        error: function()
        {
            logFormatted(arguments, "error");
        },
        
        assert: function(truth, message)
        {
            if (!truth)
            {
                var args = [];
                for (var i = 1; i < arguments.length; ++i)
                    args.push(arguments[i]);
                
                logFormatted(args.length ? args : ["Assertion Failure"], "error");
                throw message ? message : "Assertion Failure";
            }
        },
        
        dir: function(object)
        {
            var html = [];
                        
            var pairs = [];
            for (var name in object)
            {
                try
                {
                    pairs.push([name, object[name]]);
                }
                catch (exc)
                {
                }
            }
            
            pairs.sort(function(a, b) { return a[0] < b[0] ? -1 : 1; });
            
            html.push('<table>');
            for (var i = 0; i < pairs.length; ++i)
            {
                var name = pairs[i][0], value = pairs[i][1];
                
                html.push('<tr>', 
                '<td class="propertyNameCell"><span class="propertyName">',
                    escapeHTML(name), '</span></td>', '<td><span class="propertyValue">');
                appendObject(value, html);
                html.push('</span></td></tr>');
            }
            html.push('</table>');
            
            logRow(html, "dir");
        },
        
        dirxml: function(node)
        {
            var html = [];
            
            appendNode(node, html);
            logRow(html, "dirxml");
        },
        
        group: function()
        {
            logRow(arguments, "group", pushGroup);
        },
        
        groupEnd: function()
        {
            logRow(arguments, "", popGroup);
        },
        
        time: function(name)
        {
            timeMap[name] = (new Date()).getTime();
        },
        
        timeEnd: function(name)
        {
            if (name in timeMap)
            {
                var delta = (new Date()).getTime() - timeMap[name];
                logFormatted([name+ ":", delta+"ms"]);
                delete timeMap[name];
            }
        },
        
        count: function()
        {
            this.warn(["count() not supported."]);
        },
        
        trace: function()
        {
            this.warn(["trace() not supported."]);
        },
        
        profile: function()
        {
            this.warn(["profile() not supported."]);
        },
        
        profileEnd: function()
        {
        },
        
        clear: function()
        {
            consoleBody.innerHTML = "";
        },

        open: function()
        {
            toggleConsole(true);
        },
        
        close: function()
        {
            if (frameVisible)
                toggleConsole();
        }
    };
 
    // ********************************************************************************************
       
    var consoleFrame = null;
    var consoleBody = null;
    var commandLine = null;
    
    var frameVisible = false;
    var messageQueue = [];
    var groupStack = [];
    var timeMap = {};
    
    var clPrefix = ">>> ";
    
    var isFirefox = navigator.userAgent.indexOf("Firefox") != -1;
    var isIE = navigator.userAgent.indexOf("MSIE") != -1;
    var isOpera = navigator.userAgent.indexOf("Opera") != -1;
    var isSafari = navigator.userAgent.indexOf("AppleWebKit") != -1;

    // ********************************************************************************************

    function toggleConsole(forceOpen)
    {
        frameVisible = forceOpen || !frameVisible;
        if (consoleFrame)
            consoleFrame.style.visibility = frameVisible ? "visible" : "hidden";
        else
            waitForBody();
    }

    function focusCommandLine()
    {
        toggleConsole(true);
        if (commandLine)
            commandLine.focus();
    }

    function waitForBody()
    {
        if (document.body)
            createFrame();
        else
            setTimeout(waitForBody, 200);
    }    

    function createFrame()
    {
        if (consoleFrame)
            return;
        
        window.onFirebugReady = function(doc)
        {
            window.onFirebugReady = null;

            var toolbar = doc.getElementById("toolbar");
            toolbar.onmousedown = onSplitterMouseDown;

            commandLine = doc.getElementById("commandLine");
            addEvent(commandLine, "keydown", onCommandLineKeyDown);

            addEvent(doc, isIE || isSafari ? "keydown" : "keypress", onKeyDown);
            
            consoleBody = doc.getElementById("log");
            layout();
            flush();
        }

        var baseURL = getFirebugURL();

        consoleFrame = document.createElement("iframe");
        consoleFrame.setAttribute("src", baseURL+"/firebug.html");
        consoleFrame.setAttribute("frameBorder", "0");
        consoleFrame.style.visibility = (frameVisible ? "visible" : "hidden");    
        consoleFrame.style.zIndex = "2147483647";
        consoleFrame.style.position = "fixed";
        consoleFrame.style.width = "100%";
        consoleFrame.style.left = "0";
        consoleFrame.style.bottom = "0";
        consoleFrame.style.height = "200px";
        document.body.appendChild(consoleFrame);
    }
    
    function getFirebugURL()
    {
        var scripts = document.getElementsByTagName("script");
        for (var i = 0; i < scripts.length; ++i)
        {
            if (scripts[i].src.indexOf("firebug.js") != -1)
            {
                var lastSlash = scripts[i].src.lastIndexOf("/");
                return scripts[i].src.substr(0, lastSlash);
            }
        }
    }
    
    function evalCommandLine()
    {
        var text = commandLine.value;
        commandLine.value = "";

        logRow([clPrefix, text], "command");
        
        var value;
        try
        {
            value = eval(text);
        }
        catch (exc)
        {
        }

        console.log(value);
    }
    
    function layout()
    {
        var toolbar = consoleBody.ownerDocument.getElementById("toolbar");
        var height = consoleFrame.offsetHeight - (toolbar.offsetHeight + commandLine.offsetHeight);
        consoleBody.style.top = toolbar.offsetHeight + "px";
        consoleBody.style.height = height + "px";
        
        commandLine.style.top = (consoleFrame.offsetHeight - commandLine.offsetHeight) + "px";
    }
    
    function logRow(message, className, handler)
    {
        if (consoleBody)
            writeMessage(message, className, handler);
        else
        {
            messageQueue.push([message, className, handler]);
            waitForBody();
        }
    }
    
    function flush()
    {
        var queue = messageQueue;
        messageQueue = [];
        
        for (var i = 0; i < queue.length; ++i)
            writeMessage(queue[i][0], queue[i][1], queue[i][2]);
    }

    function writeMessage(message, className, handler)
    {
        var isScrolledToBottom =
            consoleBody.scrollTop + consoleBody.offsetHeight >= consoleBody.scrollHeight;

        if (!handler)
            handler = writeRow;
        
        handler(message, className);
        
        if (isScrolledToBottom)
            consoleBody.scrollTop = consoleBody.scrollHeight - consoleBody.offsetHeight;
    }
    
    function appendRow(row)
    {
        var container = groupStack.length ? groupStack[groupStack.length-1] : consoleBody;
        container.appendChild(row);
    }

    function writeRow(message, className)
    {
        var row = consoleBody.ownerDocument.createElement("div");
        row.className = "logRow" + (className ? " logRow-"+className : "");
        row.innerHTML = message.join("");
        appendRow(row);
    }

    function pushGroup(message, className)
    {
        logFormatted(message, className);

        var groupRow = consoleBody.ownerDocument.createElement("div");
        groupRow.className = "logGroup";
        var groupRowBox = consoleBody.ownerDocument.createElement("div");
        groupRowBox.className = "logGroupBox";
        groupRow.appendChild(groupRowBox);
        appendRow(groupRowBox);
        groupStack.push(groupRowBox);
    }

    function popGroup()
    {
        groupStack.pop();
    }
    
    // ********************************************************************************************

    function logFormatted(objects, className)
    {
        var html = [];

        var format = objects[0];
        var objIndex = 0;

        if (typeof(format) != "string")
        {
            format = "";
            objIndex = -1;
        }

        var parts = parseFormat(format);
        for (var i = 0; i < parts.length; ++i)
        {
            var part = parts[i];
            if (part && typeof(part) == "object")
            {
                var object = objects[++objIndex];
                part.appender(object, html);
            }
            else
                appendText(part, html);
        }

        for (var i = objIndex+1; i < objects.length; ++i)
        {
            appendText(" ", html);
            
            var object = objects[i];
            if (typeof(object) == "string")
                appendText(object, html);
            else
                appendObject(object, html);
        }
        
        logRow(html, className);
    }

    function parseFormat(format)
    {
        var parts = [];

        var reg = /((^%|[^\\]%)(\d+)?(\.)([a-zA-Z]))|((^%|[^\\]%)([a-zA-Z]))/;    
        var appenderMap = {s: appendText, d: appendInteger, i: appendInteger, f: appendFloat};

        for (var m = reg.exec(format); m; m = reg.exec(format))
        {
            var type = m[8] ? m[8] : m[5];
            var appender = type in appenderMap ? appenderMap[type] : appendObject;
            var precision = m[3] ? parseInt(m[3]) : (m[4] == "." ? -1 : 0);

            parts.push(format.substr(0, m[0][0] == "%" ? m.index : m.index+1));
            parts.push({appender: appender, precision: precision});

            format = format.substr(m.index+m[0].length);
        }

        parts.push(format);

        return parts;
    }

    function escapeHTML(value)
    {
        function replaceChars(ch)
        {
            switch (ch)
            {
                case "<":
                    return "&lt;";
                case ">":
                    return "&gt;";
                case "&":
                    return "&amp;";
                case "'":
                    return "&#39;";
                case '"':
                    return "&quot;";
            }
            return "?";
        };
        return String(value).replace(/[<>&"']/g, replaceChars);
    }

    function objectToString(object)
    {
        try
        {
            return object+"";
        }
        catch (exc)
        {
            return null;
        }
    }

    // ********************************************************************************************

    function appendText(object, html)
    {
        html.push(escapeHTML(objectToString(object)));
    }

    function appendNull(object, html)
    {
        html.push('<span class="objectBox-null">', escapeHTML(objectToString(object)), '</span>');
    }

    function appendString(object, html)
    {
        html.push('<span class="objectBox-string">&quot;', escapeHTML(objectToString(object)),
            '&quot;</span>');
    }

    function appendInteger(object, html)
    {
        html.push('<span class="objectBox-number">', escapeHTML(objectToString(object)), '</span>');
    }

    function appendFloat(object, html)
    {
        html.push('<span class="objectBox-number">', escapeHTML(objectToString(object)), '</span>');
    }

    function appendFunction(object, html)
    {
        var reName = /function ?(.*?)\(/;
        var m = reName.exec(objectToString(object));
        var name = m ? m[1] : "function";
        html.push('<span class="objectBox-function">', escapeHTML(name), '()</span>');
    }
    
    function appendObject(object, html)
    {
        try
        {
            if (object == undefined)
                appendNull("undefined", html);
            else if (object == null)
                appendNull("null", html);
            else if (typeof object == "string")
                appendString(object, html);
            else if (typeof object == "number")
                appendInteger(object, html);
            else if (typeof object == "function")
                appendFunction(object, html);
            else if (object.nodeType == 1)
                appendSelector(object, html);
            else if (typeof object == "object")
                appendObjectFormatted(object, html);
            else
                appendText(object, html);
        }
        catch (exc)
        {
        }
    }
        
    function appendObjectFormatted(object, html)
    {
        var text = objectToString(object);
        var reObject = /\[object (.*?)\]/;

        var m = reObject.exec(text);
        html.push('<span class="objectBox-object">', m ? m[1] : text, '</span>')
    }
    
    function appendSelector(object, html)
    {
        html.push('<span class="objectBox-selector">');

        html.push('<span class="selectorTag">', escapeHTML(object.nodeName.toLowerCase()), '</span>');
        if (object.id)
            html.push('<span class="selectorId">#', escapeHTML(object.id), '</span>');
        if (object.className)
            html.push('<span class="selectorClass">.', escapeHTML(object.className), '</span>');

        html.push('</span>');
    }

    function appendNode(node, html)
    {
        if (node.nodeType == 1)
        {
            html.push(
                '<div class="objectBox-element">',
                    '&lt;<span class="nodeTag">', node.nodeName.toLowerCase(), '</span>');

            for (var i = 0; i < node.attributes.length; ++i)
            {
                var attr = node.attributes[i];
                if (!attr.specified)
                    continue;
                
                html.push('&nbsp;<span class="nodeName">', attr.nodeName.toLowerCase(),
                    '</span>=&quot;<span class="nodeValue">', escapeHTML(attr.nodeValue),
                    '</span>&quot;')
            }

            if (node.firstChild)
            {
                html.push('&gt;</div><div class="nodeChildren">');

                for (var child = node.firstChild; child; child = child.nextSibling)
                    appendNode(child, html);
                    
                html.push('</div><div class="objectBox-element">&lt;/<span class="nodeTag">', 
                    node.nodeName.toLowerCase(), '&gt;</span></div>');
            }
            else
                html.push('/&gt;</div>');
        }
        else if (node.nodeType == 3)
        {
            html.push('<div class="nodeText">', escapeHTML(node.nodeValue),
                '</div>');
        }
    }

    // ********************************************************************************************
    
    function addEvent(object, name, handler)
    {
        if (document.all)
            object.attachEvent("on"+name, handler);
        else
            object.addEventListener(name, handler, false);
    }
    
    function removeEvent(object, name, handler)
    {
        if (document.all)
            object.detachEvent("on"+name, handler);
        else
            object.removeEventListener(name, handler, false);
    }
    
    function cancelEvent(event)
    {
        if (document.all)
            event.cancelBubble = true;
        else
            event.stopPropagation();        
    }

    function onError(msg, href, lineNo)
    {
        var html = [];
        
        var lastSlash = href.lastIndexOf("/");
        var fileName = lastSlash == -1 ? href : href.substr(lastSlash+1);
        
        html.push(
            '<span class="errorMessage">', msg, '</span>', 
            '<div class="objectBox-sourceLink">', fileName, ' (line ', lineNo, ')</div>'
        );
        
        logRow(html, "error");
    };

    function onKeyDown(event)
    {
        if (event.keyCode == 123)
            toggleConsole();
        else if ((event.keyCode == 108 || event.keyCode == 76) && event.shiftKey
                 && (event.metaKey || event.ctrlKey))
            focusCommandLine();
        else
            return;
        
        cancelEvent(event);
    }

    function onSplitterMouseDown(event)
    {
        if (isSafari || isOpera)
            return;
        
        addEvent(document, "mousemove", onSplitterMouseMove);
        addEvent(document, "mouseup", onSplitterMouseUp);

        for (var i = 0; i < frames.length; ++i)
        {
            addEvent(frames[i].document, "mousemove", onSplitterMouseMove);
            addEvent(frames[i].document, "mouseup", onSplitterMouseUp);
        }
    }
    
    function onSplitterMouseMove(event)
    {
        var win = document.all
            ? event.srcElement.ownerDocument.parentWindow
            : event.target.ownerDocument.defaultView;

        var clientY = event.clientY;
        if (win != win.parent)
            clientY += win.frameElement ? win.frameElement.offsetTop : 0;
        
        var height = consoleFrame.offsetTop + consoleFrame.clientHeight;
        var y = height - clientY;
        
        consoleFrame.style.height = y + "px";
        layout();
    }
    
    function onSplitterMouseUp(event)
    {
        removeEvent(document, "mousemove", onSplitterMouseMove);
        removeEvent(document, "mouseup", onSplitterMouseUp);

        for (var i = 0; i < frames.length; ++i)
        {
            removeEvent(frames[i].document, "mousemove", onSplitterMouseMove);
            removeEvent(frames[i].document, "mouseup", onSplitterMouseUp);
        }
    }
    
    function onCommandLineKeyDown(event)
    {
        if (event.keyCode == 13)
            evalCommandLine();
        else if (event.keyCode == 27)
            commandLine.value = "";
    }
    
    window.onerror = onError;
    addEvent(document, isIE || isSafari ? "keydown" : "keypress", onKeyDown);
    
    if (document.documentElement.getAttribute("debug") == "true")
        toggleConsole(true);
})();
}

if (!("console" in window) || !("firebug" in console))
{
    var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml",
    "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];

    window.console = {};
    for (var i = 0; i < names.length; ++i)
        window.console[names[i]] = function() {}
}
Panel = new Class;
Panel.prototype = {
	__construct: function(world)
	{
		this.world = world;
		this.parent = false;
		this.hidden = false;

		this.element = T.div({'class':'sidepanel_panel_open'});
		this.toggle_text = T.div({'class':'panel_toggle_text'}, 'close');
		this.title_bar = T.div({'class':'panel_title'});
		this.content = T.div({'class':'panel_content'});

		// When dragging a panel this will switch to "absolute", we force
		// "relative" so there's no surprises with regards to the layout of the
		// panel
		this.element.style.position = 'relative';
		this.content.style.position = 'relative';

		this.title_bar.appendChild(this.toggle_text);
		this.element.appendChild(this.title_bar);
		GEvent.bindDom(this.title_bar, 'click', this, this.toggle);
		this.element.appendChild(this.content);

		this.optionName = 'panel_' + this.getUID();

		if (O.getOption(this.optionName) && O.getOption(this.optionName).hidden) {
			this.hide();
		}
	},
getUID:
	function()
	{
		return false;
	},
toString:
	function()
	{
		return '[object Panel]';
	},
	initialize: function(side_panel)
	{
		this.set_parent(side_panel);
		return this.element;
	},
	toggle: function()
	{
		this.hidden ? this.show() : this.hide();
	},
busy:
	function(state)
	{
		this.is_busy = state;
		if (this.is_busy) {
			if (!this._busy_indicator) {
				this._busy_indicator = document.createElement('div');
				this._busy_indicator.appendChild(document.createElement('div'));
				this._busy_indicator.className = 'panel_busy_container';
				this._busy_indicator.firstChild.className = 'panel_busy_indicator';

				with (this._busy_indicator.style) {
					position = 'absolute';
					left = 0;
					top = 0;
					height = '100%';
					width = '100%';
				}
				with (this._busy_indicator.firstChild.style) {
					height = '100%';
					width = '100%';
					zIndex = 100000;
				}
				PNG.setImage(SKIN_URL+'images/busy_background.png', this._busy_indicator, true);
			}
			this.add_content(this._busy_indicator);
			this._busy_indicator.style.display = 'block';
		} else {
			if (this._busy_indicator) {
				this._busy_indicator.style.display = 'none';
			}
		}
	},
	show: function()
	{
		this.hidden = false;
		this.element.className = 'sidepanel_panel_open';
		this.element.style.display = 'block';
		this.content.style.display = 'block';
		document.emptyElement(this.toggle_text);
		this.toggle_text.appendChild(document.createTextNode('close'));
		O.unsetOption(this.optionName);
	},
	hide: function()
	{
		this.hidden = true;
		this.element.className = 'sidepanel_panel_closed';
		this.content.style.display = 'none';
		document.emptyElement(this.toggle_text);
		this.toggle_text.appendChild(document.createTextNode('open'));
		O.setOption(this.optionName, {hidden: true});
	},
	set_parent: function(parent)
	{
		this.parent = parent;
	},
	set_title: function(title)
	{
		document.emptyElement(this.title_bar);
		this.title_bar.appendChild(document.createTextNode(title));
		this.title_bar.appendChild(this.toggle_text);
	},
	set_content: function(node)
	{
		while (this.content.firstChild) {
			this.content.removeChild(this.content.firstChild);
		}
		this.add_content(node);
	},
add_content:
	function(node)
	{
		this.content.appendChild(node);
	}
}


FavouriteItemsPanel = new Class(Panel);
FavouriteItemsPanel.prototype = {
getUID: function() { return 'faveitems'; },
	__construct: function(world)
	{
		Panel.prototype.__construct.call(this, world);
		this.set_title('Favourite items');
	}
}



FavouritePlacesPanel = new Class(Panel);
FavouritePlacesPanel.prototype = {
getUID: function() { return 'faveplaces'; },
	__construct: function(world)
	{
		Panel.prototype.__construct.call(this, world);
		this.set_title('Favourite places');
	}
}



MyItemsPanel = new Class(Panel);
MyItemsPanel.prototype = {
getUID: function() { return 'myitems'; },
__construct:
	function(world)
	{
		Panel.prototype.__construct.call(this, world);
		this.set_title('My items');

		var item_select;
		var content = T.div({'class':'items_list_panel my_items_panel'}, [
			item_select = T.select([
				T.option({'value':''}, 'All items'),
				T.option({'value':'Route'}, 'Routes'),
				T.option({'value':'Marker'}, 'Markers'),
				//T.option({'value':'PubCrawl'}, 'Pub crawls')
			]),

			T.div({'class':'items_list'}, [ this.item_list = T.ul() ])
		]);

		GEvent.bindDom(item_select, 'change', this, function() { this.get_my_items(item_select.value); });
		GEvent.bindDom(this.world.user, 'login', this, function() { this.get_my_items(item_select.value); });
		GEvent.bindDom(this.world.user, 'logout', this, function() { this.get_my_items(item_select.value); });
		GEvent.bindDom(this.world, 'itemsave', this, function() { this.get_my_items(item_select.value); });


		this.set_content(content);
		this.get_my_items();
	},
get_my_items:
	function(type)
	{
		this._last_type = type;

		if (!this.world.user.is_authenticated) {
			document.em