/*
  Utility functions 
*/
/** 
 * Function to  warp an object API calls into embed object, without overriding 
 * exisiting function. Example : for a class A that have a method 'setup()', B 
 * has a memeber 'a' that is an instance of 'A'. If you call 
 * objectWrapper(A.prototype, "B.prototype", "a"); all calls of 'setup()' on 
 * a 'B' instance will be evualated just as if called it through B.a.setup(). 
 * So it makes B.setup() behave exactly as B.a.setup().
 * 
 * @param {Prototype} prototypeSource prototype of the embeded object
 * @param {String} prototypeDest string that will return the destination prototype object if evaluate by 'eval()' (just quote the code to access the prototype)
 * @param {String} embeded name of the object emebeded in prototypeDest class
 */
function objectWrapper(prototypeSource, prototypeDest, embeded) {
  for (var i in prototypeSource) {
    try {
      if (typeof(prototypeSource[i]) == "function" && eval(prototypeDest + "." + i) == undefined){
        // 'i' must be greater than 2, because in gmap objects, short properties are hidden ones
        if (i.length > 2) {
          // generating parameters string
          paramsCount = prototypeSource[i].length;
          params = "";
          paramString = "p";
          if (paramsCount > 0) {
            params = paramString;
          }
          for (p = 1; p < paramsCount; p++) {
            paramString += "p";
            params += ", " + paramString;
          } 
        
          eval(prototypeDest + "." + i + " = function ("+ params +") { return " + embeded + "." + i +"("+params+");}");
        }
      }
    } catch(e) {
      // fails silently (goosh)
    }
  }
}

/**
 * Makes copies of all the properties of an object to another, so that the dest
 * object has all the behaviours of source object (beware of overriding) plus its own methods and members
 *
 * @param {Object} objectSource Source Object
 * @param {Object} objectDest Destination Object
 */
function copyProperties(objectSource, objectDest) {
  for (var i in objectSource) {
    objectDest[i] = objectSource[i];
  }
}

/**
 * Generate a sequence of QRST that match the tile requested by the parameters. The parameters are in the google maps tile numbering style, that differs from the geogarage tile numbering.
 * 
 * @param {Number} x tile coordinate (unit is the tile)
 * @param {Number} y tile coordinate (unit is the tile)
 * @param {Number} zoom zoom level
 * @returns The qrst that identifies that tile
 * @type String
 */

function genQrst(x, y, zoom) {
	var inc = 1 << zoom-1;
	var qrstString = "t";
	for (var i = zoom; i > 0; i--) {
	  if ((x & inc) && (y & inc)) {
      qrstString += "s";
    } else if  ((x & inc) && ! (y & inc)) {
      qrstString += "r";
    } else if  (!(x & inc) && ! (y & inc)) {
      qrstString += "q";
    } else if  (!(x & inc) && (y & inc)) {
      qrstString += "t";
    }
    inc = inc >> 1;
 	}
 	return qrstString;
}

/**
 * Generate a path where a file corresponding to the string should located, in GeoGarage quadtree storage structure. 
 * 
 * @param {String} qrstString the qrst string corresponding to a file.
 * @returns The corresponding directory path
 * @type String
 */
function genPathFromQrst(qrstString) {
	var directoryElem = new Array();
	var strLength = qrstString.length;
	for (var i = 0; (i + 1) * 6 <= strLength; i++) {
    directoryElem.push(qrstString.substr(i * 6, 6));
	}
	var result = "";
	for (var i = 0; i < directoryElem.length; i++) {
	  result += directoryElem[i] + "/";
	}
	return result + qrstString + ".png";
}

/*
  GLOBAL_CONSTANTS
*/
// IE version dection
/** @ignore */
var arVersion = navigator.appVersion.split("MSIE");
/**
 * Get and sotre the microsoft internet explorer version.
 * @final
 */
var IE_VERSION =  parseFloat(arVersion[1]);
if (! arVersion[1]) {
  IE_VERSION = null;
}

/**
 * True if the microsoft internet explorer needs the image alpha workarounds
 * @final
 */
var IE_NEEDS_ALPHA_CODE = (IE_VERSION > 5) && (IE_VERSION < 7);

/** 
 * Give alpha layer to image fo microsoft IE 5.5 and 6. And transparent .gif file with same path (except extention) should be present on server
 * 
 * @param {Node} img Image DOM Node
 */
function loadImageWithAlphaLayer(img) {
  img.style.filter='progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+ img.src +'", sizingMethod="scale")';
  img.src = img.src.replace(".png", ".gif");
}

/**
 * Stores the decimal seperator
 * @final
 */
DECIMAL_SEP = (1.1).toString().split("1").join("");

/** For safari workarounds
 * @final
 */
BROWSER_IS_SAFARI = navigator.appVersion.indexOf("AppleWebKit") == -1 ? false : true;

/** Opacity Levels 
 * @final
 */
OPACITY_LEVELS = [0.33, 0.66, 1];

/** grab cursor 
 * @final
 */
CURSOR_GRAB = null;
if (! IE_VERSION) {
	// We assume mozilla, others browser have not cursor support anyway.
	CURSOR_GRAB = "-moz-grab";
}

// using null for 'CURSOR_GRAB' in ie makes it keep current cursor
// that should be the correct behavior most of the time 

/**
  GEvent hack to keep original google maps API behaviour

*/
 
// we need to use an temporary array because IE7
// continues to iterate in the for (<prop> in <object>) loop if 
// new properties are added in <object> (damned ie devs).
var propsArray = new Array();

for (s in GEvent) {
    propsArray.push(s);
}
 
while (s = propsArray.pop()) {
  GEvent["_old_" + s] = GEvent[s];
  
  arglist = ""
  for (var i = 1; i < GEvent[s].length; i++) {
     arglist += ", arg" + i;
  }
  
  eval(" GEvent."+s+" = function (arg0 "+ arglist +") { if (arg0 && arg0.gmap != undefined) { arg0 = arg0.gmap; } GEvent._old_"+ s +"(arg0 " +arglist+ "); } ")
}

/*
  Core architecture : Layers classes and main class
*/

if(typeof geogarage == "undefined") var geogarage = new Object();

/**
 * Generates a GMap Type composed of the tile layer list and with the given name.
 * @param {String} name Name of the map type
 * @param {Array} tileLayerList Layers to group in the map type
 * @returns The map type ready to be added in GMap2
 * @type GMapType
 */
geogarage.makeComposedMapType = function(name, tileLayerList) {
  var maxZoom = 0;
  for (var i in tileLayerList) {
    if (tileLayerList[i].maxResolution() + 1 > maxZoom) {
      maxZoom = tileLayerList[i].maxResolution() + 1;
    }
  }
  return new GMapType(tileLayerList, new GMercatorProjection(maxZoom), name);
}

/**
 * @class Abstract layer source, with common methods
 * 
 */
geogarage.LayerSource = function(enabled) {
  this.name = "";
  if (enabled != undefined) {
    this.enabed = enabled;
  } else {
    this.enabled = true; 
  }
  this.mOpacity = 1;
}

/**
 * Generate a array of layers with the given object. Depending of the layer type 'onTransparentMapOverlay', 'onMagicOverlay' and 'onSimpleOverlay' are called on handler (with the corresponding LayerSource object) if a specific behaviour is needed for this tile. handler accepts or refuse the layer by returning true or false.
 * @param sourceLayersArray structured object that describe the layers
 * @param handler a handler if a specific behaviour is required with some kind of source
 * @return An array with the created layer list
 * @type Array
 */
 generateLayerList = function(sourceLayersArray, handler) {
  if (! handler) handler = {};
  var layerList = new Array();
  for (var i in sourceLayersArray) {
    var activateMagicPath = false;
    var accept = true;
    switch(sourceLayersArray[i].type) {
      case "GoogleMapsTransparent" : {
        var tmapOverlay = new geogarage.LayerSourceGoogleTransparentMap(sourceLayersArray[i].name, sourceLayersArray[i].id, sourceLayersArray[i].enabled);
        if (handler.onTransparentMapOverlay) { accept = handler.onTransparentMapOverlay(tmapOverlay); }
        if (accept) { layerList.unshift(tmapOverlay); }
        break;
      }
      case "Magic" : {
        activateMagicPath = true;
        if (handler.onMagicOverlay) { accept = handler.onMagicOverlay(magicOverlay); }
        // We don't break here
      }
      case "Simple" : {
        var curLayer = sourceLayersArray[i];
        var simpleOverlay = new geogarage.LayerSourceMagic(curLayer.name, curLayer.id, curLayer.url, curLayer.top_qrst, 
                                            curLayer.max_zoom, curLayer.bounds,curLayer.copyrights, curLayer.enabled, activateMagicPath);
        // we forward accept from previous case (accept = accept && handler.onS...() )
        if (handler.onSimpleOverlay) { accept = accept && handler.onSimpleOverlay(simpleOverlay); }
        if (accept) { layerList.unshift(simpleOverlay); }
        break;
      }
    }
  }
  return layerList;
}



geogarage.LayerSource.prototype = {
/**
 * Set the new opacity for the layer, the visual will be updated when overlay will be (re)loaded 
 * @param {Number} newVal opacity value between 0 (invisible) and 1 (opaque).
 */
  setOpacity: function(newVal) {
    var theName = this.name;
    this.mOpacity = newVal;
    var opacity = this.mOpacity;
    this.gTileLayer.getOpacity = function () { return opacity;}
  },

/**
 * Get the GTileLayer corresponding to this tile source.
 * @returns The GTileLayer instance
 * @type GTileLayer
 */ 
  getTileLayer: function() {
    if (! this.gTileLayer) {
      this.generateGTileLayerOverlay();
    }
    return this.gTileLayer;
  },

/**
 * Get the GTileLayerOverlay corresponding to this tile source.
 * @returns The GTileLayerOverlay instance
 * @type GTileLayerOverlay
 */   
  getTileLayerOverlay: function() {
    if (! this.gTileLayerOverlay) {
      this.generateGTileLayerOverlay();
    }
    return this.gTileLayerOverlay;
  }, 

/**
 * Creates a GMapType for this tile source.
 * @param {Array} othersLayers layers 
 * @returns The GMapType instance
 * @type GMapType
 */   
  makeMapType: function(othersLayers) {
    if (!othersLayers) {othersLayers = new Array()};
    return new GMapType(othersLayers.concat(this.getTileLayer()),
                        new GMercatorProjection(this.getTileLayer().maxResolution() + 1),
                        this.name);
  },
  
/**
 * Initialize the instance 
 * @returns The GTileLayerOverlay instance
 * @type GTileLayerOverlay
 */  
  generateGTileLayerOverlay: function(){
    GLog.write("Do call this on LayerSource abstract class");
  }
}

/**
 * @class Concrete implementation of TileSource for Google's transparent maps
 * @constructor
 * @param {String} name name for the tile source
 * @param {String} id an identifier for programatical use
 */
geogarage.LayerSourceGoogleTransparentMap = function(name, id, enabled) {
  if (enabled != undefined) {
    this.enabled = enabled;
  } else {
    this.enabled = true; 
  }
  this.name = name;
  this.id = id;
}

geogarage.LayerSourceGoogleTransparentMap.prototype = new geogarage.LayerSource;

var layerSourceGoogleTransparentMapPrototype = {
/**
 * Initialize the instance 
 * @member geogarage.LayerSourceGoogleTransparentMap
 * @returns The GTileLayerOverlay instance
 * @type GTileLayerOverlay
 */
  generateGTileLayerOverlay: function(){
    this.gTileLayer = G_HYBRID_MAP.getTileLayers()[1];
    this.gTileLayerOverlay =  new GTileLayerOverlay(this.gTileLayer);
    return this.gTileLayerOverlay;
  }
}

copyProperties(layerSourceGoogleTransparentMapPrototype, geogarage.LayerSourceGoogleTransparentMap.prototype);

/**
 * @class Concrete implementation of TileSource for SimpleQrst and Magic Qrst source
 *
 * @param {String} name name for the tile source
 * @param {String} id an identifier for programatical use
 * @param {String} url the base url for the tile source
 * @param {String} topQrst the highest qrst that this tile source has
 * @param {String} maxZoom the biggest zoom level this tile source can display (actually qrst tree depth)
 * @param {GLatLngBounds} bounds the area covered by this tile source
 * @param {String} copyrights defines the copyrights holder for this source
 * @param {String} genPath true or false if wether the tile tree is stored in a directory tree or in a flat (single) directory
 */

geogarage.LayerSourceMagic = function(name, id, url, topQrst, maxZoom, bounds, copyrights, enabled, genPath) {
  /** @private */
  this.enabled = undefined;
  /** @private */
  this.name = undefined;
  /** @private */
  this.id = undefined;
  /** @private */
  this.mLeftPart = undefined;
  /** @private */
  this.mRightPart = undefined;
  /** @private */
  this.mMinRand = undefined;
  /** @private */
  this.mMaxRand = undefined;
  /** @private */
  this.mRandomized = undefined;
  /** @private */
  this.mUrl = undefined;
  /** @private */
  this.mBounds = undefined;
  /** @private */
  this.mTopQrst = undefined;
  /** @private */
  this.mMaxZoom = undefined;
  /** @private */
  this.mCopyrights = undefined;
  /** @private */
  this.gTileLayerOverlay = undefined;
  /** @private */
  this.genPath = undefined;
  
  this.name = name;
  this.id = id;
  var testRand = url.split("%");
  if (testRand.length == 3) {
    this.mLeftPart = testRand[0];
    this.mRightPart = testRand[2];
    var numbers = testRand[1].split("-");
    this.mMinRand = parseInt(numbers[0]);
    this.mMaxRand = parseInt(numbers[1]);
    this.mRandomized = true;
  } else {
  	this.mRandomized = false;
    this.mUrl = url;
  }
  
  this.mBounds = bounds;
  if (! this.mBounds) {
       this.mBounds = new GLatLngBounds(new GLatLng(-180, -90), new GLatLng(180, 90));
  }
  this.mTopQrst = topQrst;
  this.mMaxZoom = maxZoom;
  this.mCopyrights = copyrights;
  this.gTileLayerOverlay = null;
  if (genPath) {
    this.genPath = true;
  } else {
    this.genPath = false;
  }
  
  if (enabled != undefined) {
    this.enabed = enabled;
  } else {
    this.enabled = true; 
  }
  
}

geogarage.LayerSourceMagic.prototype = new geogarage.LayerSource;


var layerSouceMagicPrototype = {
/**
 * Initialize the instance 
 * @member geogarage.LayerSourceMagic
 * @returns The GTileLayerOverlay instance
 * @type GTileLayerOverlay
 */
  generateGTileLayerOverlay: function() {  
    var leftURLPart = this.mLeftPart;
    var rightURLPart = this.mRightPart;
    var minRand = this.mMinRand;
    var maxRand = this.mMaxRand;
    var url = this.mUrl;
    var randomized = this.mRandomized;
   
    var maxZoom = this.mMaxZoom;
    var topQrst = this.mTopQrst;
    var copyrights = new GCopyrightCollection("Image:");
    copyrights.addCopyright(
      new GCopyright(
        Math.floor(Math.random() * 10000),
        this.mBounds,
        topQrst.length,
        this.mCopyrights
      )
    );
    var pathneeded = this.genPath;
   	this.gTileLayer = new GTileLayer(copyrights, topQrst.length, maxZoom);
   	this.gTileLayer.getTileUrl = function (tile,  zoom) {
      var qrst = genQrst(tile.x, tile.y, zoom);
      // avoid attempts to load something where there is no tiles
      if ( ! BROWSER_IS_SAFARI && qrst.indexOf(topQrst) == -1) { return ""; }
      var localurl = url;
      if (randomized) {
        var selectNumber = 0;
        var startNumber = qrst.length;
        startNumber = startNumber > 4 ? 4 : startNumber;
        for (var i = startNumber; i < qrst.length; i++) {
          selectNumber += qrst.charCodeAt(i) - 113;
        }
        selectNumber %= (maxRand - minRand + 1);
        localurl = leftURLPart + (selectNumber + minRand) + rightURLPart;
      }
      return localurl + (pathneeded ? genPathFromQrst(qrst) : qrst);
    }
    
    this.gTileLayer.isPng = function () { return true;}
    var opacity = this.mOpacity;
    this.gTileLayer.getOpacity = function () {return opacity;}
    this.gTileLayerOverlay = new GTileLayerOverlay(this.gTileLayer);
    return this.gTileLayerOverlay;
  }
}

copyProperties(layerSouceMagicPrototype, geogarage.LayerSourceMagic.prototype);

/**
 * @class Global class that enable using easily GMap2 with the GeoGarage. geogarage.Maps warps entirely methods of GMap2, and can be used seamlessly instead of a GMap2 instance. For an extended use of geogarage.Maps, read the <a href="http://www.google.com/apis/maps/documentation/reference.html">Google Maps API reference</a>
 *
 * @param {Node} elem the div elem that should contain the map
 * @param {Object} layersDefs the layers definition, to handle GeoGarage layers easily
 * @param {Object} opts <a href="http://www.google.com/apis/maps/documentation/reference.html#GMapOptions">GMap2 options</a> merged with geogarage.Maps options
 */
geogarage.Maps = function(elem, layersDefs, opts) {
  /** @private */
  this.elem = undefined;
  /** @private */
  this.gmap = undefined;
  /** @private */
  this.layersList = undefined;
  
  if ( ! opts) opts = {};
  this.elem = elem;
  this.layersList = new Array();
  if (! opts.mapTypes ) {
    opts.mapTypes = [G_NORMAL_MAP, G_SATELLITE_MAP];
  }
  if (GBrowserIsCompatible()) {
    this.gmap = new GMap2(elem, opts);
  }
  if(layersDefs) {
    this.loadLayersWithSetup(layersDefs, opts);
  }
}

geogarage.Maps.IMAGES_PATH = "ggimages/"
geogarage.Maps.setImagesPath = function (path) {
    geogarage.Maps.IMAGES_PATH = path;
    updatePositionStyle();
}

geogarage.Maps.prototype = {
/**
 * Defines the behavior when used with {@link #generateLayerList}
 * @private
 * @param overlay the overlay encountered : shoulf be a transparent map overlay
 * @returns true
 * @type Boolean
 */
  onTransparentMapOverlay: function(overlay) {
    var gmapInst = this.gmap;
    var geogmap = this;
    // We disable the transparent map overlay if type switched to NORMAL_MAP or HYBRID
    GEvent.addListener(this, "maptypechanged", function() {
      var currentType = gmapInst.getCurrentMapType();
      // We hide transparent map if a map will be displayed
      if (currentType == G_NORMAL_MAP || currentType == G_HYBRID_MAP) {
        if (overlay.enabled) {
          overlay.enabled = false;
          overlay.wasEnabled = true;
          geogmap.reloadLayers();
        }
      } else {
          if (overlay.wasEnabled) {
          overlay.enabled = true;
          overlay.wasEnabled = false;
          geogmap.reloadLayers();
        }
      }
    });
    return true;
  },
  
/**
 * Loads the layers defined in given object
 *
 * @param {Object} sourceLayersDef the object that defines the layer
 */
  loadLayers: function(sourceLayersDef) {
    if (!this.layersList) {
      this.layersList = new Array();
    }
    this.center_lat = sourceLayersDef.globals.center_lat;
    this.center_lon = sourceLayersDef.globals.center_lon;
    this.init_zoom = sourceLayersDef.globals.init_zoom;
    
    this.gmap.setCenter(new GLatLng(this.center_lat, this.center_lon), this.init_zoom);
    this.layersList = this.layersList.concat( generateLayerList(sourceLayersDef.layers, this));

  },
 /**
 * Removes all the layers enabled, loaded and shown with {@link geogarage.Maps#displayLayers}
 */ 
  clearLayers: function() {
    for (var i in this.layersList) {
      this.gmap.removeOverlay(this.layersList[i].gTileLayerOverlay);
      this.gmap.removeControl(this.layersList[i].control);
    }
  },
  
 /**
 * Displays the layers enabled and loaded with {@link geogarage.Maps#loadLayers} 
 */ 
  displayLayers: function() {
    for (var i in this.layersList) {
      if (! this.layersList[i].gTileLayerOverlay) {
        // we generate the layerOverlay objects and control
        this.layersList[i].generateGTileLayerOverlay();
        this.layersList[i].control = new OverlayController( this.layersList[i].name, this.layersList[i],  this.layersList[i].enabled, this);
      }
      var layer = this.layersList[i];
      this.gmap.addControl(layer.control);
      layer.control.setButtonEnabled(layer.enabled);
      if (layer.enabled) {
        this.gmap.addOverlay(layer.gTileLayerOverlay);
      }
    }
  },
 
 /**
 * Reloads the layers enabled and loaded with {@link geogarage.Maps#loadLayers} and displayed
 */ 
  reloadLayers : function() {
    this.clearLayers();
    this.displayLayers();
  },
  
  
 /**
 * Setup a default layout to display the layers
 *
 * @param {Object} layersObject an object that define the layers in a human readable way
 * @param {Object} opts choose some display parameters with the options
 */ 
  loadLayersWithSetup: function(layersObject, opts) {
    if ( ! opts) opts = {};
    var overlay = opts.method && opts.method == "overlay" ? true : false;
    var underlying = opts.underlying && opts.underlying instanceof Array ? opts.underlying : G_SATELLITE_MAP.getTileLayers();
    var tmaplayer = new geogarage.LayerSourceGoogleTransparentMap("Carte", "TMAP").getTileLayer();
    var over = opts.over && opts.over instanceof Array ? 
               opts.over : [tmaplayer];
    var maponly = opts.nonormalmap ? undefined : G_NORMAL_MAP.getTileLayers(); 
    
    this.loadLayers(layersObject);
    if (overlay) {
      OverlayController.setInitOffset(35);
      this.displayLayers();
      maptype = G_SATELLITE_MAP;
    } else {
      var layers = this.getAllTileLayers();
      for (var i in layers) {
        if (layers[i] == tmaplayer) {
          layers.splice(i, 1);
        }
      }
      var maptype = geogarage.makeComposedMapType("hybride", underlying.concat(layers.concat(over)));

      this.addMapType(maptype);
      this.removeMapType(G_NORMAL_MAP);
      this.removeMapType(G_SATELLITE_MAP);
      if (maponly) {
        this.addMapType(geogarage.makeComposedMapType("carte", maponly.concat(layers) ) );
      }
    }
    
    this.addControl(new GLargeMapControl());
    this.addControl(new GScaleControl());
    this.addControl(new GOverviewMapControl());
    this.addControl(new geogarage.CenterPosition());
    this.addControl(new geogarage.Reticle());
    this.addControl(new GMapTypeControl());
    this.enableContinuousZoom();
    this.enableDoubleClickZoom();        

    this.setMapType(maptype);
  },
  
/**
 * Retrieves a copy of the array containing all the layers loaded
 *
 * @return the array of layers
 * @type Array
 */  
  getAllLayers: function() {
    return new Array().concat(this.layersList);
  },
  
/**
 * Creates a copy of all the GTileLayer, to create GMapTypes easily
 *
 * @return the array of GTileLayers
 * @type GTileLayer Array
 */    
  getAllTileLayers: function() {
    var layers = new Array();
    for (var i in this.layersList) {
      layers.push(this.layersList[i].getTileLayer());
    }
    return layers;
  }
	
}

// We warp the Gmap API calls into embed object, without overriding existing functions
objectWrapper(GMap2.prototype, "geogarage.Maps.prototype", "this.gmap");


/* 
  GUI things : Overlay controllers buttons
*/

/* Alpha controler : three levels of opacity managed by three buttons (kind of radio buttons) */
/**
  @private
*/
function AlphaController(switchButton, overlay, geogaragemap) {
  this.controledOverlay = overlay;
  this.button = switchButton;
  this.map = geogaragemap;
  this.levelSelected = 2;
  this.position = new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(switchButton.offsetX, switchButton.offsetY + 4))
}


AlphaController.prototype = new GControl;

var alphaControllerPrototype = {
  formatControl: function(elem) { 
    var leftImg = document.createElement("img");
    var centerImg = document.createElement("img");
    var rightImg = document.createElement("img");

    leftImg.setAttribute("src", geogarage.Maps.IMAGES_PATH + "ableft.png");
    centerImg.setAttribute("src", geogarage.Maps.IMAGES_PATH + "abcenter.png");
    rightImg.setAttribute("src", geogarage.Maps.IMAGES_PATH + "abright.png");
    
    leftImg.setAttribute("title", "niveau d'opacité à " + (OPACITY_LEVELS[0] * 100) + "%");
    centerImg.setAttribute("title", "niveau d'opacité à " + (OPACITY_LEVELS[1] * 100) + "%");
    rightImg.setAttribute("title", "niveau d'opacité à " + (OPACITY_LEVELS[2] * 100) + "%");
    
    if (IE_NEEDS_ALPHA_CODE) {
      loadImageWithAlphaLayer(leftImg);
      loadImageWithAlphaLayer(centerImg);
      loadImageWithAlphaLayer(rightImg);
    } 
    
    var alphaController = this;
    rightImg.alphaLevel = 2;
    centerImg.alphaLevel = 1;
    leftImg.alphaLevel = 0;
    
    rightImg.setSelectedView = centerImg.setSelectedView = leftImg.setSelectedView = function(selection) {
      if (selection) {
        this.src = this.src.replace(".png", "sel.png");
      } else {
        this.src = this.src.replace("sel.png", ".png");
      }
    }
    
    rightImg.onMouseClickView = centerImg.onMouseClickView = leftImg.onMouseClickView = function () {
      this.mSelected = !this.mSelected;
      alphaController.levelSelected = this.alphaLevel;
    }
    
    function onMouseOverEvenHandler() {
      if (!this.mSelected) {
        this.setSelectedView(true);
      }
    }
    
    function onMouseOutEvenHandler() {
      if (!this.mSelected) {
        this.setSelectedView(false);
      }
    }
    
    GEvent.addDomListener(leftImg, "mouseover", onMouseOverEvenHandler); 
    GEvent.addDomListener(centerImg, "mouseover", onMouseOverEvenHandler); 
    GEvent.addDomListener(rightImg, "mouseover", onMouseOverEvenHandler); 
    
    GEvent.addDomListener(leftImg, "mouseout", onMouseOutEvenHandler); 
    GEvent.addDomListener(centerImg, "mouseout", onMouseOutEvenHandler); 
    GEvent.addDomListener(rightImg, "mouseout", onMouseOutEvenHandler); 
   
    if (this.levelSelected == leftImg.alphaLevel) {
      leftImg.mSelected = true;
      centerImg.mSelected = false;
      rightImg.mSelected = false;
      leftImg.setSelectedView(true);
    }
    if (this.levelSelected == centerImg.alphaLevel) {
      leftImg.mSelected = false;
      centerImg.mSelected = true;
      rightImg.mSelected = false;
      centerImg.setSelectedView(true);
    }
    if (this.levelSelected == rightImg.alphaLevel) {
      leftImg.mSelected = false;
      centerImg.mSelected = false;
      rightImg.mSelected = true;
      rightImg.setSelectedView(true);
    }
    
    
    rightImg.style.cursor = centerImg.style.cursor = leftImg.style.cursor = "pointer";
    
    elem.appendChild(leftImg);
    elem.appendChild(centerImg);
    elem.appendChild(rightImg);
    
    this.left = leftImg;
    this.center = centerImg;
    this.right = rightImg;
    return elem;
  },

  createSimpleDiv: function() {
      var button = document.createElement("div");
      return button;
  },

  createAlphaControl: function() {
      var control = this.createSimpleDiv();
      return this.formatControl(control);
  },

  initialize: function(googlemap) {
        var control = this.createSimpleDiv();
        var map = this.map;
     
        googlemap.getContainer().appendChild(control);
        // Our reference is still valid : we can modify button aspect
        this.formatControl(control);
        
        var overlay = this.controledOverlay;
        function onClickHandler() {
          overlay.setOpacity(OPACITY_LEVELS[this.alphaLevel]);
          this.onMouseClickView();
          map.reloadLayers();
        }
        
        GEvent.addDomListener(this.left, "click", onClickHandler);
        GEvent.addDomListener(this.center, "click", onClickHandler);
        GEvent.addDomListener(this.right, "click", onClickHandler);
        
        this.control = control;
        return control;
  },

  getDefaultPosition: function() {
        return this.position;
  }
}

copyProperties(alphaControllerPrototype, AlphaController.prototype);

/*  General overlay code (enable and disable overlay, add/remove the alpha control)  */

/**
  @private
*/
function OverlayController(title, overlayer, was_enabled, geogaragemap) {
  this.controledOverlay = overlayer;
  this.map = geogaragemap;
  if (was_enabled) {
      this.controledOverlay.enabled = true;
  } else {
      this.controledOverlay.enabled = false;
  }
  this.button_title = title;
  this.button = null;
  
  if (! this.map.overlayController_count) this.map.overlayController_count = 0;
  this.map.overlayController_count++;
  this.offsetX = 5;
  this.offsetY = 5 + 26 * (this.map.overlayController_count - 1) + OverlayController.INIT_Y_OFFSET ;
  var alphaControlSpace = IE_NEEDS_ALPHA_CODE ? 0 : 70;
  this.position = new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(this.offsetX + alphaControlSpace, this.offsetY));
}

OverlayController.setInitOffset = function(val) {
  OverlayController.INIT_Y_OFFSET = val;
}

OverlayController.prototype = new GControl;
OverlayController.count = 0;
OverlayController.INIT_Y_OFFSET = 0;

var overlayControllerPrototype = {
  formatButton: function(elem, text) {
    var leftImg = document.createElement("img");
    var contentSpan = document.createElement("span");
    var rightImg = document.createElement("img");
    
    // we copy attributes from the given elements, to keep properties such as non selectable content

    leftImg.setAttribute("src", geogarage.Maps.IMAGES_PATH + "bgleft.png");
    rightImg.setAttribute("src", geogarage.Maps.IMAGES_PATH + "bgright.png");
    rightImg.style.verticalAlign = leftImg.style.verticalAlign = "middle";
    rightImg.style.cursor = leftImg.style.cursor = "pointer";
   
    if (IE_NEEDS_ALPHA_CODE) {
      loadImageWithAlphaLayer(leftImg);
      loadImageWithAlphaLayer(rightImg);
    }
    
    var contentStyle = {
      background: "transparent url("+geogarage.Maps.IMAGES_PATH +"bgcontent.png) repeat-x scroll center",
      padding: "6px",
      fontSize: "10px",
      fontFamily: "arial, sans-sherif",
      verticalAlign: "middle",
      cursor: "pointer",
      color: "black",
      overlflow: "auto"
    }
    
    copyProperties(contentStyle, contentSpan.style);
    contentSpan.innerHTML = text;
    elem.innerHTML = "";
    elem.appendChild(leftImg);
    elem.appendChild(contentSpan);
    elem.appendChild(rightImg);
    elem.setAttribute("title", "activer/désactiver le calque");
    elem.textNodeNumber = 1; // make sure to correct that if you change 'contentSpan' node position in elem;
    return elem;
  },

  createButton: function(text) {
    var button = document.createElement("div");
    this.formatButton(button, text);
    return button;
  },

  createSimpleDiv: function(text) {
    var button = document.createElement("div");
    button.innerHTML = text;
    return button;
  },

  selectable: function() {return false;},

  createAlphaController: function() {
    if (IE_NEEDS_ALPHA_CODE) { return; }
    if (this.alphacontrol) { this.map.gmap.removeControl(this.alphacontrol); }
    if (!this.alphacontrol) { this.alphacontrol = new AlphaController(this, this.controledOverlay, this.map); }
    this.map.gmap.addControl(this.alphacontrol);
  },

  initialize: function(googlemap) {
    var button = this.createSimpleDiv(this.button_title);
    var map = this.map;
    button.innerHTML = this.button_title;
    var overlay = this.controledOverlay;
    if (overlay) {
      GEvent.addDomListener(button, "click", function() {
        if (overlay.enabled) {
          overlay.enabled = false;
        } else {
          overlay.enabled = true;
        }
        map.reloadLayers();
      });
    }
    googlemap.getContainer().appendChild(button);
    // Our reference is still valid : we can modify button aspect
    this.formatButton(button, button.innerHTML);
    this.button = button;
    this.createAlphaController();
    return button;
  },

  setButtonEnabled: function(enabled) {
    var textNode = this.button.childNodes[this.button.textNodeNumber];
    if (enabled) {
      textNode.style.textDecoration = "none";
    } else {
      textNode.style.textDecoration = "line-through";
    }
  },

  getDefaultPosition: function() {
      return this.position;
  }

}

copyProperties(overlayControllerPrototype, OverlayController.prototype);

/* utility classes,  function  && Variables */
/**
 * Parameters are all optional
 * @class define some ways to format Lat/Lng degrees for displaying latitudes and longitudes.
 *
 * @param {Number} prec precision, in number of decimals
 * @param {Boolean} displayCards true or false wether if the N/S/E/W cadinal points should be shown or use negative values
 *
 */
function DegreeFormater(prec, displayCards) {
  /** the precision to display */
  this.precision = undefined;
  /** true if cardinal points caracters (N/S/E/W) should be displayed */
  this.displayCards = undefined;
  if (prec) {
    this.precision = prec;
  } else {
    this.precision = 0;
  }
  if (displayCards) {
    this.displayCards = true;
  } else {
    this.displayCards = false; 
  }
}

DegreeFormater.prototype = {
/**
 * Returns the cardinal caracter to use, wether if the number is positive/negative and is a latitude/longitude
 * @private
 * @param {Boolean} positive true if the value is positive
 * @param {Boolean} lat true if the value is a latitude
 */
  getCardString: function(positive, lat) {
    if (lat) {
      return positive ? "N" : "S";
    } else{
      return positive ? "E" : "W";
    }
  },

/**
 * Formats the value in a &lt;deg&gt;&deg; &lt;min&gt;' &lt;sec&gt;" format (with seconds precision defined in constructor)
 * @param {Number} value a degree value (unbounded, but should be limited to -180/+180
 * @param {Boolean} lat true if the value is a latitude
 */  
  formatToDegMinSec : function(value, lat) {
    var prec = this.precision;
    var precMul = Math.pow(10, prec);
    var neg = value < 0 ? true : false;
    if (neg) value = -value;
    var degs = Math.floor(value);
    value -= degs;
    value *= 60;
    degs = degs.toString();
    if (neg && ! this.displayCards) degs = "-"+degs;
    var mins = Math.floor(value);
    value -= mins;
    value *= 60;
    mins = mins.toString();
    var secs = value.toFixed(prec).toString();
    if (prec > 0) prec++;
    while(degs.length < 3) {
      degs = " " + degs;
    }
    while(mins.length < 2) {
      mins = " " + mins;
    }
    while(secs.length < 2 + prec) {
      secs = " " + secs;
    }
    return degs + '&deg;' + mins + "'" + secs + '"' + (this.displayCards ? this.getCardString(!neg, lat) : "");
  },
  
/**
 * Formats the value in a &lt;deg&gt;&deg; &lt;min&gt;' format (with minutes precision defined in constructor)
 * @param {Number} value a degree value (unbounded, but should be limited to -180/+180
 * @param {Boolean} lat true if the value is a latitude
 */    
  formatToDegMin: function (value, lat) {
    var prec = this.precision;
    var precMul = Math.pow(10, prec);
    var neg = value < 0 ? true : false;
    if (neg) value = -value;
    var degs = Math.floor(value);
    value -= degs;
    value *= 60;
    degs = degs.toString();
    if (neg && ! this.displayCards) degs = "-"+degs;
    var mins = value.toFixed(prec).toString();
    if (prec > 0) prec++;
    while(degs.length < 3) {
      degs = " " + degs;
    }
    while(mins.length < 2 + prec) {
      mins = " " + mins;
    }
    return degs + '&deg;' + mins + "'" + (this.displayCards ? this.getCardString(!neg, lat) : "");
  },

  
/**
 * Formats the value in a &lt;deg&gt;&deg; format (with degree precision defined in constructor)
 * @param {Number} value a degree value (unbounded, but should be limited to -180/+180
 * @param {Boolean} lat true if the value is a latitude
 */    
  formatToDec: function (value, lat) {
    var prec = this.precision;
    var precMul = Math.pow(10, prec);
    var neg = value < 0 ? true : false;
    if (neg) value = -value;
    var degs = value.toFixed(prec).toString();
    if (neg && ! this.displayCards) degs = "-"+degs;
    if (prec > 0) prec++;
    while(degs.length < 3 + prec) {
      degs = " " + degs;
    }
    return degs + '&deg;' + (this.displayCards ? this.getCardString(!neg, lat) : "");
  },
  
/**
 * This method is here to define a interface for the use of a default format ({@link DegreeFormater#formatToDegMinSec}  here) <br />
 * It should be overridden by copying other methods reference : 
 * <pre>var myFormat = new DegreeFormater(5, false);<br />myformat.format = myformat.formatToDec; // default format is now a decimal value</pre>
 *
 * @param {Number} value a degree value (unbounded, but should be limited to -180/+180
 * @param {Boolean} lat true if the value is a latitude
 */  
  format: function(value, lat) {
    return this.formatToDegMinSec(value, lat);
  }
  
}

/** an instance for &lt;deg&gt;&deg; &lt;min&gt;' &lt;sec&gt;" format, one decimal precision for the seconds, with cardinal points caracters */
DegreeFormater.DEFAULT_DEG_MIN_SEC = new DegreeFormater(1, true);
/** an instance for &lt;deg&gt;&deg; &lt;min&gt;' format, four decimal precision for the minutes, with cardinal points caracters */
DegreeFormater.DEFAULT_DEG_MIN = new DegreeFormater(4, true);
DegreeFormater.DEFAULT_DEG_MIN.format = DegreeFormater.prototype.formatToDegMin;
/** an instance for &lt;deg&gt;&deg; &lt;min&gt;' format, seven decimal precision for the degrees, without cardinal points caracters */
DegreeFormater.DEFAULT_DEG = new DegreeFormater(7, false);
DegreeFormater.DEFAULT_DEG.format = DegreeFormater.prototype.formatToDec;

/** an array of defaults format, usefull for cylcing throught different formats */
DegreeFormater.DEFAULT_FORMATS = [DegreeFormater.DEFAULT_DEG_MIN_SEC, DegreeFormater.DEFAULT_DEG_MIN, DegreeFormater.DEFAULT_DEG];

/**
 * @private
 * defines where and how CenterPosition and MousePosition instances should be placed
 */

geogarage.POSITIONS = {STYLE: {}, ANCHOR_BOTTOM_LEFT: 30, ANCHOR_BOTTOM_RIGHT: 7, 
                                  ANCHOR_TOP_RIGHT: 7, ANCHOR_TOP_LEFT: 7};

function updatePositionStyle() {  
  geogarage.POSITIONS.STYLE = {color: "white", 
                               fontFamily:  '"Andale Mono", "Lucida Console", "Courier New", monospace', 
                               fontSize: "10px", 
                               whiteSpace: "pre",
                               padding: "2px"}
  if (IE_NEEDS_ALPHA_CODE) {
    geogarage.POSITIONS.STYLE.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod="scale", src=" '+ geogarage.Maps.IMAGES_PATH  +'black50.png")';
  } else {
    geogarage.POSITIONS.STYLE.backgroundImage = "url("+geogarage.Maps.IMAGES_PATH +"black50.png)";
  }
}

updatePositionStyle()


/**
  @class An instance of this class, when added to a GMap2, will display the latitude and longitude of the center of the map. It supports cycling through different formats.
*/
geogarage.CenterPosition = function() {
  this.currentFormat = 1;
  this.formater = DegreeFormater.DEFAULT_FORMATS[this.currentFormat];
}

geogarage.CenterPosition.prototype = new GControl(true, true);

var centerPositionPrototype = {
/**
  Update the displayed value to current center lat/lng
  @private
  @member geogarage.CenterPosition
  @param {GMAP2} googlemap the GMap2 object
  @param {Node} textDiv the text div where the text should be displayed
  @
*/
  updateValue: function(googlemap, textDiv) {
    var latlon  = googlemap.getCenter();
    textDiv.innerHTML = this.formater.format(latlon.lat(), true) + "," + this.formater.format(latlon.lng(), false);
  },

/**
  Configures what events should be listened and how handle it
  @private
  @member geogarage.CenterPosition
  @param {GMAP2} googlemap the GMap2 object
  @param {Node} textDiv the text div where the text should be displayed
  @
*/
  setUpEventListening: function(googlemap, text) {
    var ctl = this;
    function updateFieldValue() {
      ctl.updateValue(googlemap, text);
    }
    GEvent.addListener(googlemap, "drag", updateFieldValue);
    if (window.attachEvent) {
      window.attachEvent("onresize", updateFieldValue);
    } else {
      window.addEventListener("resize", updateFieldValue, false);
    }

  },

/**
  The GControl initialize function implementation
  @private
  @member geogarage.CenterPosition
  @param {GMAP2} googlemap the GMap2 object
  @
*/
  initialize: function(googlemap) {
    this.mapInstance = googlemap;
    var textField = document.createElement("div");
    copyProperties(geogarage.POSITIONS.STYLE, textField.style);
    googlemap.getContainer().appendChild(textField);
    var switcher = document.createElement("img");
    switcher.title = "Choisir entre les différents formats";
    switcher.style.verticalAlign = "-15%";
    switcher.style.marginRight = "5px";
    switcher.src = geogarage.Maps.IMAGES_PATH + "loopswitch.png";
    if (IE_NEEDS_ALPHA_CODE) {loadImageWithAlphaLayer(switcher)}
    switcher.style.cursor = "pointer";
    textField.appendChild(switcher);
    var text = document.createElement("span");
    textField.appendChild(text);
    
    var ctl = this;
    GEvent.addDomListener(switcher, "click", function() {
      ctl.currentFormat++;
      ctl.currentFormat %= DegreeFormater.DEFAULT_FORMATS.length;
      ctl.formater = DegreeFormater.DEFAULT_FORMATS[ctl.currentFormat];
      ctl.updateValue(googlemap, text);
    });
    
    this.setUpEventListening(googlemap, text);
   
    this.updateValue(googlemap, text);
    return textField;
  },

/**
  The GControl getDefaultPosition function implementation
  @private
  @member geogarage.CenterPosition
  @param {GMAP2} googlemap the GMap2 object
  @
*/
  getDefaultPosition: function() {
    if (this.mapInstance) {
      if(! this.mapInstance.POSITIONS) this.mapInstance.POSITIONS = {ANCHOR_BOTTOM_LEFT: geogarage.POSITIONS.ANCHOR_BOTTOM_LEFT};
      this.mapInstance.POSITIONS.ANCHOR_BOTTOM_LEFT += 15; 
    }
    return new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(5, this.mapInstance.POSITIONS.ANCHOR_BOTTOM_LEFT));
  }

}

copyProperties(centerPositionPrototype, geogarage.CenterPosition.prototype);


/**
  @class An instance of this class, when added to a GMap2, will display the latitude and longitude of the mouse pointer. It supports cycling through different formats.
*/
geogarage.MousePosition = function () {
  this.currentFormat = 1;
  this.formater = DegreeFormater.DEFAULT_FORMATS[1];
}

geogarage.MousePosition.prototype = new geogarage.CenterPosition;

var mousePositionPrototype = {
/**
  Update the displayed value to current mouse lat/lng (overrides base class implementation)
  @private
  @member geogarage.MousePosition
  @param {GMAP2} googlemap the GMap2 object
  @param {Node} textDiv the text div where the text should be displayed
  @
*/
  updateValue: function(googlemap, textDiv, latlng) {
    if (!latlng) return;
    textDiv.innerHTML = this.formater.format(latlng.lat(), true) + "," + this.formater.format(latlng.lng(), false);
  },
  
/**
  Configures what events should be listened and how handle it (overrides base class implementation)
  @private
  @member geogarage.MousePosition
  @param {GMAP2} googlemap the GMap2 object
  @param {Node} textDiv the text div where the text should be displayed
  @
*/
  setUpEventListening: function(googlemap, text) {
    var ctl = this;
    GEvent.addListener(googlemap, "mousemove", function(latlng) {
      ctl.updateValue(googlemap, text, latlng);
    });
  }
}

copyProperties(mousePositionPrototype, geogarage.MousePosition.prototype);

/** @class An instance of this class, when added to a GMap2, will display a reticle (crosschair) at the center of the screen */
geogarage.Reticle = function () {
  this.position = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(0, 0));
}

geogarage.Reticle.prototype = new GControl(true, false);

var reticlePrototype = {
/**
  The GControl initialize function implementation
  @private
  @member geogarage.Reticle
  @param {GMAP2} googlemap the GMap2 object
  @
*/
  initialize : function(googlemap) {
    var reticle = document.createElement("img");
    reticle.src = geogarage.Maps.IMAGES_PATH + "reticle.png";
    reticle.style.cursor = CURSOR_GRAB;
    
    if (IE_NEEDS_ALPHA_CODE) {loadImageWithAlphaLayer(reticle)}
    var container = googlemap.getContainer();
    container.appendChild(reticle);

    var point = {x:container.clientWidth/2, y:container.clientHeight/2};
    var position = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(point.x - 8, point.y - 8));
    position.apply(reticle);

    function windowResize() {
      var newpoint = {x:container.clientWidth/2, y:container.clientHeight/2};
      var newposition = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(newpoint.x - 8, newpoint.y - 8));
      newposition.apply(reticle);
    }

    if (window.attachEvent) {
      window.attachEvent("onresize", windowResize );
    } else {
      window.addEventListener("resize", windowResize, false);
    }

    var dbleClkCtl = {lastClk: 0}
    GEvent.addDomListener(reticle, "dblclick", function() {
      googlemap.zoomIn(googlemap.getCenter(), true, true);
    });
    
    GEvent.addDomListener(reticle, "mousedown", function() {
      reticle.style.cursor = "-moz-grabbing";
    });
    GEvent.addDomListener(reticle, "mouseup", function() {
      reticle.style.cursor = "-moz-grab";
    });
    this.position = position; 

    return reticle;
  },
  
/**
  The GControl printable function implementation
  @private
  @member geogarage.Reticle
  @param {GMAP2} googlemap the GMap2 object
  @
*/
  printable: function () {return false;},
/**
  The GControl selectable function implementation
  @private
  @member geogarage.Reticle
  @param {GMAP2} googlemap the GMap2 object
  @
*/
  selectable: function () {return false;},

/**
  The GControl getDefaultPosition function implementation
  @private
  @member geogarage.Reticle
  @param {GMAP2} googlemap the GMap2 object
  @
*/
  getDefaultPosition: function() {
    return this.position;
  }

}

copyProperties(reticlePrototype, geogarage.Reticle.prototype);


