/**
 * @name gpsmap
 * @version 1.0
 * @author Ernest Delgado
 * @copyright (c) 2008 Ernest Delgado
 * @fileoverview Emulates a 3D Car Navigation Map similar to the GPS ones
 *     based on the Super Mario Kart Demo in Canvas.
 *
 * Code licensed under the MIT License:
 * http://ernestdelgado.com/public-tests/mit-license.txt
 * http://www.opensource.org/licenses/mit-license.php
 */

/*
 * The original MarioKart() function is from the Super Mario Kart
 * created by Jacob Seidelin (blog.nihilogic.dk)
 * http://blog.nihilogic.dk/2008/05/javascript-super-mario-kart.html
 */

/*
 * The YUI Slider at the bottom of this file and some of the helper
 * functions are from the YUI Library which is released under the
 * BSD license.
 * http://developer.yahoo.com/yui/license.html
 */




/**
 * Helper functions
 *
 */
 
/**
 * Returns image DOM element with the given source url.
 * @method createImgEl
 * @param {String} src The source url of the image.
 * @return {HTMLElement} image DOM Element.
 */
function createImgEl(src) {
  var imgEl = document.createElement('img');
  imgEl.src = src;
  return imgEl;
}

/**
 * Asserts that the event is an triggered by an arrow key.
 * @method isArrowKey
 * @param {Event} e The event.
 * @return {Boolean} image DOM Element.
 */
function isArrowKey(e) {
  return (e.keyCode == 37 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 40);
}

/**
 * Alias for getElementById.
 * @method get_
 * @param {String} id The id string.
 * @return {HTMLElement} DOM Element.
 */
function get_(id) {
  return document.getElementById(id);
}

/**
 * Returns the active DOM element for the given event.
 * @method getTarget
 * @param {Event} e The event.
 * @return {Node} The current target.
 */
function getTarget(e) {
  var node = e.target || e.srcElement;
  if (node && 3 == node.nodeType) {
    return node.parentNode;
  } 
  else {
    return node;
  }
}

/**
 * Facade of necessary functions to cancel the default
 * event, valid for all browsers.
 * @method stopEvent
 * @param {Event} ev The event.
 * @return {Void}
 */
var stopEvent = function(e) {
  if (e.stopPropagation) {
      e.stopPropagation();
  } else {
      e.cancelBubble = true;
  }
  if (e.preventDefault) {
      e.preventDefault();
  } else {
    e.returnValue = false;
  }
}

// Browser detection for specific filters
var ua = navigator.userAgent;
var isSafari = ua.indexOf('WebKit') != -1;
var isGecko = navigator.product == 'Gecko' && !isSafari;


/**
 * Singleton that displays 3x3 grid with a small representation of the map.
 * The tiles are pulled from openstreetmap.org using images with
 * formatted source as http://c.tile.openstreetmap.org/16/10479/25329.png
 * [SF downtown by default]
 * The number in the url repressent the geographic coordinates:
 * http://c.tile.openstreetmap.org/[Z]/[X]/[Y].png
 * Once the 9 tile images are loaded then they are merged in a hidden canvas
 * which will be the image source for the 3D map. The modified function from 
 * Jacob {@link MarioKartControl} will take care of spliting the image in strips
 * to create the 3d effect.
 * @namespace window
 * @class tileGrid
 */
window.TileGrid = (function() {
  /**
   * Array that contains the images of the grid.
   * @property gridImages
   * @private
   * @type Array
   */
  var gridImages = [];
  
  /**
   * Whether or not is an infinte map.
   * An infinite map will update the images on the grid
   * dynamically as the user moves on the map.
   * The non infinite map will have boundaries the user
   * won't be able to go beyond.
   * @property infiniteMap
   * @private
   * @type Boolean
   */  
  var infiniteMap = true;  

  /**
   * Whether or not the tile images are being loaded.
   * @property loadingImageTiles
   * @private
   * @type Boolean
   */
  var loadingImageTiles = false;
  
  /**
   * The width of the orignial downloaded map tile
   * @property tileWidth
   * @private
   * @type Number
   */
  var tileWidth = 256;

  /**
   * Array of purple markers.
   * There are two by default with their initial position. 
   * @property purpleMarkers
   * @private
   * @type Array
   */        
  var purpleMarkers = [
    {x : 384, y : 360},
    {x : 334, y : 480}
  ];

  /**
   * These factors are meant to match the coordinates of the
   * red like with the actual position in the 3d map
   * since the tileGrid width has been reduced defined in px 
   * on the css of the main html.
   * @property tileGridSizeFactor
   * @private
   * @type Object
   */
  var tileGridSizeFactor = {x: 4.5, y: 4.3};

  /**
   * Keeps track of the user movements from the initial
   * position. Values will change for every time new tiles
   * are loaded.
   * @property tileOffset
   * @private
   * @type Object
   */    
  var tileOffset = { x:0, y:0 };

  /**
   * Whether or not the tile images are loaded and copied
   * to the merged canvas so the map can use it as a source.
   * @property tilesReady
   * @private
   * @type Boolean
   */  
  var tilesReady = false;

  /**
   * Creates images elements and places them in the grid.
   * @method initTileGrid_
   * @private
   * @return {Void}
   */
  function initTileGrid_() {  
    var gridCellWidth = '58px';
    var gridCellOpacity = '0.7';
    var tableElId = 'tiles-grid';
  
    // for now, assume table is on the DOM already
    var tableEl = get_(tableElId);
    var gridCells = tableEl.getElementsByTagName('td');
  
    // create one image DOM element for each cell and
    // add it to the gridImages property.
    for (var i = 0, len = gridCells.length; i < len; ++i) {
      var imageEl = createImgEl('');
      imageEl.style.width = gridCellWidth;
      gridCells[i].appendChild(imageEl);
      gridImages.push(imageEl);
      // put opacity on the tiles except the central one
      if (i != 4 && infiniteMap) gridCells[i].style.opacity = gridCellOpacity;
    }
  }

  /**
   * The canvas element which is placed
   * as the overlay of the tileGrid in order to draw
   * the red line that keeps track of the map cursor. 
   * @method initTileGridOverlay_
   * @private
   * @return {Void}
   */
  function initTileGridOverlay_() {
    var tileGridOverlay = document.createElement("canvas");
    tileGridOverlay.id = 'tileGridOverlay';
    tileGridOverlay.width = 178;
    tileGridOverlay.height = 178;
    tileGridOverlay.style.width = '178px';
    tileGridOverlay.style.height = '178px';
    tileGridOverlay.style.position = 'absolute';
    tileGridOverlay.style.top = '0';
    tileGridOverlay.style.right = '0';

    document.getElementById('tiles-preview-container').appendChild(tileGridOverlay);
  }

  /**
   * Creates the necessary DOM elements. i.e. the loading label
   * and the hidden 'clipboards' canvas both for the merged tiles and
   * for the red line.
   * @method addDOMElements_
   * @private
   * @return {Void}
   */
  function addDOMElements_() {
    var loadingMsg = document.createElement("div");
    loadingMsg.id = 'loadingMsg';
    loadingMsg.style.color = '#fff';
    loadingMsg.innerHTML = 'Loading tile images...';
    loadingMsg.style.position = 'absolute';
    loadingMsg.style.top = '0';
    loadingMsg.style.right = '0';
    loadingMsg.style.zIndex = '2000';
    loadingMsg.style.backgroundColor = 'red';
    document.getElementById('gps-canvas').appendChild(loadingMsg);
  
    var mergedTiles = document.createElement("canvas");
    mergedTiles.id = 'mergedTiles';
    mergedTiles.width = 1000;
    mergedTiles.height = 1000;
    mergedTiles.style.width = '1000px';
    mergedTiles.style.height = '1000px';
    mergedTiles.style.position = 'absolute';
    mergedTiles.style.top = '-2000px';
    mergedTiles.style.border = '5px solid red';
    document.body.appendChild(mergedTiles);

    var redLineClipboard = document.createElement("canvas");
    redLineClipboard.id = 'redLineClipboard';
    redLineClipboard.width = 1000;
    redLineClipboard.height = 1000;
    redLineClipboard.style.width = '1000px';
    redLineClipboard.style.height = '1000px';
    redLineClipboard.style.position = 'absolute';
    redLineClipboard.style.top = '-2000px';
    redLineClipboard.style.border = '5px solid red';
    document.body.appendChild(redLineClipboard);
    
    // set state of the 'infinite' checkbox
    document.getElementById('infinite').checked = infiniteMap;
  }
  
  /**
   * Attaches the necessary listeners to the infinite checkbox.
   * Also to the tile grid; clicking on it will determine whether
   * the user is adding or removing markers.
   * @method attachListeners_
   * @private
   * @return {Void}
   */
  function attachListeners_() {
    YAHOO.util.Event.on(get_('infinite'), "click", function(e) {
      var tileGridOverlayCtx = get_('tileGridOverlay').getContext('2d');
      tileGridOverlayCtx.clearRect(0,0,1000,1000);
      switchTilesOpacity_(this.checked);
    });
    YAHOO.util.Event.on(get_('tiles-preview-container'), "click", function(e) {
      setMarkers_(e);
    });
  }

  /**
   * Changes the opacity of the tiles depending on whether or not
   * the infinite map is active.
   * @method switchTilesOpacity_
   * @private
   * @return {Void}
   */
  function switchTilesOpacity_(transparency) {
    var tableEl = document.getElementById('tiles-grid');
    var gridCells = tableEl.getElementsByTagName('td');    
    for (var i = 0; i < gridCells.length; i++) {
      if (i != 4) {
        gridCells[i].style.opacity = (transparency) ? '0.7' : '1';
      }
      else { // we leave the central tile as it is
        gridCells[i].style.opacity = '1';
      }
    }
  }

  /**
   * Add or removes markers from the TileGrid.
   * It calls the necessary functions to update
   * both the tileGrid and the 3d map.
   * @method setMarkers_
   * @private
   * @param {Event}
   * @return {Void}
   */
  function setMarkers_(e) {
    stopEvent(e);
    var target = getTarget(e);
    // that means we have clicked on an existing marker
    // then we proceed to remove it
    if (target.nodeName.toLowerCase() == 'img') {
      // focus the whole map adding border and blocking scroll
      MarioKartControl.setArrowsBlocked(true);
      get_('gps-container').style.border = '3px solid yellow';
      
      // extract the number from the clicked marker
      // (the original format is i[n] (e.g.'i9'))
      var markerId = target.id.match(/\w/g)[1]
      // delete it from the purpleMarkers array
      purpleMarkers.splice(markerId,1);
      // delete it from the 3d map
      MarioKartControl.deleteMarker(markerId);
    }
    // when click on the preview mini map then we add a new purple marker
    else {
      var newMarker = {
        x: (e.clientX - YAHOO.util.Dom.getX(target)) * tileGridSizeFactor.x, 
        y: (e.clientY - YAHOO.util.Dom.getY(target)) * tileGridSizeFactor.y
      };
      purpleMarkers.push({
        x: (e.clientX - YAHOO.util.Dom.getX(target)) * tileGridSizeFactor.x - (tileOffset.x * tileWidth), 
        y: (e.clientY - YAHOO.util.Dom.getY(target)) * tileGridSizeFactor.y - (tileOffset.y * tileWidth)
      });
      MarioKartControl.addMarker(newMarker);
    }
    // rerender
    drawGridPurpleMarkers_();
  }
  
  /**
   * Merges the nine tiles to the temporary hidden canvas that will be used
   * as the source of the 3d map. This action happens both at the beginning
   * and after every update. Since the updated render will happen in fractions
   * of a second we already update all the UI components at the end of this
   * function. i.e. update the red line, remove loading label and call the
   * function to update the purple markers.
   * @method mergeTiles_
   * @private
   * @return {Void}
   */
  function mergeTiles_(updatedMarkersOffset) {
    var mergedTiles = document.getElementById('mergedTiles');
    var mergedTilesCtx = mergedTiles.getContext('2d');
    for (var i = 0; i < gridImages.length; ++i) {
      mergedTilesCtx.save();
      mergedTilesCtx.translate(tileWidth * (i%3), tileWidth * Math.floor(i/3));
      mergedTilesCtx.drawImage(gridImages[i],0,0);
      mergedTilesCtx.restore();
    }
    if (typeof MarioKartControl !== 'undefined') {
      var mapCursor = MarioKartControl.oMarker;
      // Here we update teh position of the user to the central tile.
      // We rescue the user from whatever the position beyond the central
      // tile was as the new tile images were being loaded.
      // Thus we avoid to see a jump on the rendering of the 3d map.
      if (!tilesReady && typeof mapCursor === 'object') { 
        // we get here if tiles are ready and the player has ever been set.
      
        // if x coords are beyond the central tile then update 
        if (mapCursor.x > tileWidth*2 || mapCursor.x < tileWidth) {
          mapCursor.x = mapCursor.x%tileWidth+tileWidth;
        }
        // if y coords are beyond the central tile then update
        if (mapCursor.y > tileWidth*2 || mapCursor.y < tileWidth) {
          mapCursor.y = mapCursor.y%tileWidth+tileWidth;
        }
      }
    }
    tilesReady = true;
    TileGrid.setLoadingVisibility(false);
    drawGridPurpleMarkers_();
    
    if (updatedMarkersOffset) {
      MarioKartControl.updateCanvasMarkers(updatedMarkersOffset[0], updatedMarkersOffset[1], tileWidth);
      MarioKartControl.updateRedLine(updatedMarkersOffset[0], updatedMarkersOffset[1], tileWidth);
    }
  }

  /**
   * Loads the images into the grid and fills the debug box
   * with the requests calls.
   * @method fillGrid_
   * @private
   * @param {Function} callback function to merge the tiles when
   *    the images are finally loaded.
   * @return {Void}
   */
  function fillGrid_(callback) {
    var XYZ = TileGrid.XYZ;
    if (loadingImageTiles) return;
      loadingImageTiles = true;
      var imagesLoaded = 0;
    // put these lines in a for loop using modulo
    var XYZ_ = {};
    // clone XYZ to XYZ_
    for (var i in XYZ) {
      XYZ_[i] = XYZ[i];
    }
    var requestDebug = document.getElementById('requests');
    requestDebug.innerHTML = '';
    
    // attach handler to execute only when all the images are load
    for (var i = 0, len = gridImages.length; i < len; ++i) {
      gridImages[i].onload = (function() {
        imagesLoaded++;                
        requestDebug.appendChild(document.createTextNode(this.src + ' ...OK\n'))
        if (imagesLoaded == len) {
            callback.call(this);
            loadingImageTiles = false;
        }
      });
    }
    
    // Load images
    for (var y = -1, i = 0; y < 2; y++) {
      for (var x = -1; x < 2; x++, i++) {
       gridImages[i].src = createTileUrl_(XYZ_.x + x, XYZ_.y + y, XYZ_.z);
      }
    }        
  }

  /**
   * Create the openstreetmap url base on its base path
   * and xyz coords.
   * @method createTileUrl_
   * @private
   * @param {Object} xyz relative coords of the map.
   * @return {Void}
   */
  function createTileUrl_(x, y, z) {
    var tileUrl = 'http://c.tile.openstreetmap.org/';
    return tileUrl+z+'/'+x+'/'+y+'.png';
  }

  /**
   * Create the openstreetmap url base on its base path
   * and xyz coords.
   * @method createTileUrl_
   * @private
   * @param {Object} xyz relative coords of the map.
   * @return {Void}
   */
  function drawGridPurpleMarkers_() {
    var purpleMarkerSize = {x: 10, y: 16};
    var tableContainer = document.getElementById('tiles-preview-container');
    var imgs = Array.prototype.slice.call(tableContainer.getElementsByTagName('IMG'));
    
    // remove all the markers. Tile images should not be touched.
    for (var i = 0, el; el = imgs[i]; i++) { 
      if (imgs[i].className == 'marker') {
        tableContainer.removeChild(imgs[i]);
      }
    }    
    // add all the markers based on the position data of purpleMarkers
    for (var i = 0; i < purpleMarkers.length; i++) {
      var oImg = new Image();
      oImg.id = 'i' + i;
      oImg.className = 'marker';
      oImg.src = 'purple_marker_s.png';
      oImg.style.position = 'absolute';
      oImg.style.top = purpleMarkers[i].y/tileGridSizeFactor.y - purpleMarkerSize.y + (tileOffset.y * tileWidth)/tileGridSizeFactor.y + 'px';
      oImg.style.right = ((tileWidth * 3) - purpleMarkers[i].x - tileOffset.x * tileWidth)/tileGridSizeFactor.x + 'px';
      tableContainer.appendChild(oImg);
    }
  }

  
  return {        
    /**
     * Position of the initial maps tiles (central)
     * @property XYZ
     * @public
     * @type Object
     */
    XYZ: {
      x: 10479,
      y: 25329,
      z: 16 // zoom level
    },
    
    /**
     * Starts singleton instance.
     * @method init
     * @public
     * @return {Void}
     */    
    init: function() {
      initTileGrid_();
      initTileGridOverlay_();
      addDOMElements_();
      fillGrid_(mergeTiles_);
      attachListeners_();
    },

    /**
     * Returns private tileWidth.
     * @method getTileWidth
     * @public
     * @return {Number}
     */
    getTileWidth: function() {
      return tileWidth;
    },

    /**
     * Returns private purpleMarkers.
     * @method getPurpleMarkers
     * @public
     * @return {Array}
     */
    getPurpleMarkers: function() {
      return purpleMarkers;
    },

    /**
     * Returns private tileGridSizeFactor.
     * @method getTileGridSizeFactor
     * @public
     * @return {Object}
     */
    getTileGridSizeFactor: function() {
      return tileGridSizeFactor;
    },

    /**
     * Returns private tilesReady.
     * @method getTilesReady
     * @public
     * @return {Object}
     */
    getTilesReady: function() {
      return tilesReady;
    },

    /**
     * Sets tilesReady value.
     * @method setTilesReady
     * @public
     * @return {Object}
     */
    setTilesReady: function(newTilesReady) {
      tilesReady = newTilesReady;
    },
    
    /**
     * Sets the visibility of the loading label.
     * @method setLoadingVisibility
     * @public
     * @return {Void}
     */
    setLoadingVisibility: function(visible) {
      if (visible) {
        document.getElementById('loadingMsg').style.display = 'block';
      }
      else {
        document.getElementById('loadingMsg').style.display = 'none';
      }
    },
    
    /**
     * Triggers new tile images to be loaded then tiles are merged
     * on a temporary canvas so the 3d map uses that merged image
     * as the source on the next render.
     * Called from MarioKartControl
     * @method reloadTileGrid.render()
     * @public
     * @return {Void}
     */
    reloadTileGrid: function() {
      var mapCursor = MarioKartControl.oMarker;
      var XYZ = this.XYZ;
      var oX = tileOffset.x;
      var oY = tileOffset.y;
      if (mapCursor.y < tileWidth) { // up
        XYZ.y -= 1;
        tileOffset.y += 1;
      }
      else if (mapCursor.x < tileWidth) { // left
        XYZ.x -= 1;
        tileOffset.x += 1;    
      }
      else if (mapCursor.x > tileWidth*2) { // right
        XYZ.x += 1;
        tileOffset.x -= 1;
      }
      else if (mapCursor.y > tileWidth*2) { // down
        XYZ.y += 1;
        tileOffset.y -= 1;
      }
      fillGrid_(function() {
        mergeTiles_([tileOffset.x - oX, tileOffset.y - oY]);
      });
    }
  }
})(); // end of tileGrid singleton
  

// Modified function of the Mario Kart Demo
// http://blog.nihilogic.dk/2008/05/javascript-super-mario-kart.html
function MarioKart() {
  var oMaps = {
    "map1" : {
      "texture" : "mktest2.png",
      "width" : 768,
      "height" : 768,
      "collision" : [],
      "startposition" : {
        x : 384,
        y : 460
      },
      "aistartpositions" : [
        {x : 384, y : 460},
        {x : 334, y : 480}
      ],
      "startrotation" : 180,
      "aipoints" : [
        [400, 200]
      ]
    }
  }

  // array of available maps
  var aAvailableMaps = ["map1","map2"];

  // render modes:
  // 0: One screen canvas
  // 1: One canvas per horizontal screen line
  var iRenderMode = 0;


  // size of div wrapper and the canvas. the final size will depend on the value of the screenscale
  var iWidth = 80; 
  var iHeight = 38;
  // scale of the canvas. if iwidth = 80 and iscreenscale = 4 then final width is 160
  var iScreenScale = 8;

  // quality
  var iQuality = 1;  // 1 = best, 2 = half as many lines, etc.
  var bSmoothSprites = true;
  var bMusic = false;

  function setRenderMode(iValue) 
  {
    if (bCounting) return;

    iRenderMode = iValue;
    if (bRunning)
      resetScreen();
  }

  function setScreenScale(iValue) 
  {
    if (bCounting) return;

    iScreenScale = iValue;
    if (bRunning)
      resetScreen();
  }

  function setQuality(iValue) 
  {
    if (bCounting) return;

    iQuality = iValue;
    if (bRunning)
      resetScreen();
  }


  var oMap;
  var oHills;
  var oTrees;
  var aPlayers = ["mario", "luigi"];
  var strPlayer = "";
  var oPlayer;
  var arrowsBlocked = false;
  var isInfiniteMap;

  var iMapWidth;
  var iMapHeight;

  var oMapImg;

  // strPlayer = aPlayers[0];
  strMap = aAvailableMaps[0];


  function resetGame(strMap) {
    oMap = oMaps[strMap];
    loadMap(oMap);
  }

  function loadMap() {
      // oMapImg = new Image();
      // oMapImg.onload = startGame;
      // oMapImg.src = oMap.texture;
    
      // (ernest): changed the source of the map image for a the merged canvas
      iMapWidth = oMap.width;
      iMapHeight = oMap.height;
      oMapImg = document.getElementById('mergedTiles');
      try {
          render();
      } catch(e) {
        // throw new Error("");
      }
  }


  var fMaxSpeed = 6; 
  var fMaxRotInc = 6;
  var fMaxRotTimer = 0;

  var aKarts = [];
  var bRunning = false;
  var bCounting = false;

  function resetPlayers(action, el) {
    var purpleMarkers = TileGrid.getPurpleMarkers();
    if (action == 'remove') {
      aKarts.splice(++el,1)[0].sprite.remove();
    }
    else if (action == 'add') {
      var oEnemy = {
        x : el.x,
        y : el.y,
        speed : 0,
        speedinc : 0,
        rotation : oMap.startrotation,
        rotincdir : 0,
        rotinc : 0,
        sprite : new Sprite('marker'),
        cpu : true,
        aipoint : 0
      };
      aKarts.push(oEnemy);      
    }
    else {
      aKarts = [];
      aKarts.push(oPlayer);

      var iAI = 0;
      for (var i=0;i<purpleMarkers.length;i++) {
        var oEnemy = {
          x : purpleMarkers[i].x,
          y : purpleMarkers[i].y,
          speed : 0,
          speedinc : 0,
          rotation : oMap.startrotation,
          rotincdir : 0,
          rotinc : 0,
          sprite : new Sprite('marker'),
          cpu : true,
          aipoint : 0
        };
        aKarts.push(oEnemy);
        iAI++;    
      }  
    }
  }

  function startGame() {
    resetScreen();

    oPlayer = {
      x : oMap.startposition.x,
      y : oMap.startposition.y,
      speed : 0,
      speedinc : 0,
      rotation : oMap.startrotation,
      rotincdir : 0,
      rotinc : 0,
      sprite : new Sprite(strPlayer),
      cpu : false
    }
    resetPlayers();
    render();

    bCounting = true;

    var oCount = document.createElement("div");
    var oCntStyle = oCount.style;
    oCntStyle.position = "absolute";
    oCntStyle.width = 12*iScreenScale+"px";
    oCntStyle.height = 12*iScreenScale+"px";
    oCntStyle.overflow = "hidden";
    oCntStyle.top = 4*iScreenScale+"px";
    oCntStyle.left = 8*iScreenScale+"px";

    var oCountImg = document.createElement("img");
    oCountImg.src = "countdown.png";
    oCountImg.style.position = "absolute";
    oCountImg.style.left = "0px";
    oCountImg.height = 12*iScreenScale;

    oCount.appendChild(oCountImg);
    // oContainer.appendChild(oCount);

    var iCntStep = 1;

    oCount.scrollLeft = 0;

        cycle();
        bRunning = true;
  }


  var oMusicEmbed;
  var bMusicPlaying = false;

  function startMusic() {
    bMusicPlaying = true;
    oMusicEmbed = document.createElement("embed");
    oMusicEmbed.src = strMap + ".mid";
    oMusicEmbed.setAttribute("loop", "true");
    oMusicEmbed.setAttribute("autostart", "true");
    oMusicEmbed.style.position = "absolute";
    oMusicEmbed.style.left = '-1000px';
    document.body.appendChild(oMusicEmbed);
  }

  function stopMusic() {
    if (!bMusicPlaying) {
      return;
    }
    bMusicPlaying = false;
    document.body.removeChild(oMusicEmbed);
  }


  // size of the sprite (caret, car) proportional to the screeen (canvas) size
  var fSpriteScale = 0;
  // height of the strip. It seems to give the same result both incresing it and reducing it
  var fLineScale = 0;

  // div that wraps the canvas
  var oContainer = document.getElementById("gps-canvas")
  // oContainer.appendChild(document.createTextNode('Loading map...'));
  oContainer.tabindex = 1;
  // style of the div
  var oCtrStyle = oContainer.style;


  // canvas element
  var oScreenCanvas = document.createElement("canvas");
  // canvas context
  var oScreenCtx = oScreenCanvas.getContext("2d");
  // canvas element style
  var oScrStyle = oScreenCanvas.style;
  oScrStyle.position = "absolute";
  oContainer.appendChild(oScreenCanvas);

  // strip container. a div on top of the canvas
  var oStripCtr = document.createElement("div");
  oStripCtr.style.position = "absolute";
  oContainer.appendChild(oStripCtr);

  // array for screen strip descriptions
  var aStrips = [];

  var iCamHeight = 24; 
  var iCamDist = 32;
  var iViewHeight = -20;
  var iViewDist = 0;
  var fFocal = 1 / Math.tan(Math.PI*Math.PI / 360);

  function resetScreen() {
    
    // size of the sprite (caret, car) proportional to the screeen (canvas) size
    fSpriteScale = iScreenScale / 4;
    // height of the strip.
    // the more quality the shorter the strip will be -> less blur
    fLineScale = 1/iScreenScale * iQuality;

      // array of strips
    aStrips = [];
    // text to put inside the canvas?
    oStripCtr.innerHTML = "";

    // change dimensions of main container
    // width * scale . 80 * 4 = 160px final
    oCtrStyle.width = iWidth*iScreenScale+"px";
    oCtrStyle.height = iHeight*iScreenScale+"px";

      // remove any existing backgrounds
    if (oHills)
      oContainer.removeChild(oHills.div);
    if (oTrees)
      oContainer.removeChild(oTrees.div);

    // change dimensions of screen canvas
    oScreenCanvas.width = iWidth/fLineScale;
    oScreenCanvas.height = iHeight/fLineScale - 53;
    oScrStyle.width = iWidth*iScreenScale+iScreenScale+"px";
    oScrStyle.left = -(iScreenScale/2)+"px";
    oScrStyle.top = iScreenScale+"px";
    oScrStyle.height = iHeight*iScreenScale+"px";

    oStripCtr.style.width = iWidth*iScreenScale+iScreenScale+"px";
    oStripCtr.style.left = -(iScreenScale/2)+"px";

    var fLastZ = 0;

    // create horizontal strip descriptions
    for (var iViewY=0;iViewY<iHeight;iViewY+=fLineScale) {
      var iTotalY = iViewY + iViewHeight; // total height of point (on view) from the ground up
      var iDeltaY = iCamHeight - iTotalY; // height of point relative to camera
      var iPointZ = (iTotalY/(iDeltaY / iCamDist)); // distance to point on the map

      var fScaleRatio = fFocal / (fFocal + iPointZ);
      var iStripWidth = Math.floor(iWidth/fScaleRatio);
      if (fScaleRatio > 0 && iStripWidth < iViewCanvasWidth) {

        if (iViewY == 0)
          fLastZ = iPointZ - 1;

        var oCanvas;
        if (iRenderMode == 1) {
          var oCanvas = document.createElement("canvas");
          oCanvas.width = iStripWidth;
          oCanvas.height = 1;
          var oStyle = oCanvas.style;
          oStyle.position = "absolute";
          oStyle.width = (iWidth*iScreenScale)+iScreenScale+"px";
          oStyle.height = (iScreenScale*fLineScale)+iScreenScale*0.5+'px';
          oStyle.left = (-iScreenScale/2)+"px";
          oStyle.top = Math.round((iHeight-iViewY)*iScreenScale)+"px";
          oStripCtr.appendChild(oCanvas);
        }

        aStrips.push(
          {
            canvas : oCanvas || null,
            viewy : iViewY,
            mapz : iPointZ,
            scale : fScaleRatio,
            stripwidth : iStripWidth,
            mapzspan : iPointZ - fLastZ
          }
        )
        fLastZ = iPointZ;
      }
    }

    oHills = new BGLayer("hills", 360);
    oTrees = new BGLayer("trees", 720);
  }



  // setup canvas for holding the currently visible portion of the map
  // this is the canvas used to draw from when rendering
  var iViewCanvasHeight = 90; // these height, width and y-offset values 
  var iViewCanvasWidth = 456; // have been adjusted to work with the current camera setup
  var iViewYOffset = 10;
  var oViewCanvas = document.createElement("canvas");
  var oViewCtx = oViewCanvas.getContext("2d");
  oViewCanvas.width=iViewCanvasWidth;
  oViewCanvas.height=iViewCanvasHeight;


  function Sprite(strSprite) 
  { 
    var oImg = new Image();
    if (strSprite == '') {
      oImg.style.height = '15px';
      oImg.style.width = '32px';
      oImg.src = "arrow.gif";
    }
    else {
      oImg.src = "sprite_purple_marker.png";;    
    }
    oImg.style.position = "absolute";
    oImg.style.left = "0px";


    var oSpriteCtr = document.createElement("div");
    oSpriteCtr.style.width = '32px';
    oSpriteCtr.style.height = ((strSprite == '') ? '15px' : '32px');
    oSpriteCtr.style.position = "absolute";
    oSpriteCtr.style.overflow = "hidden";
    // oSpriteCtr.style.zIndex = 10000;

    oSpriteCtr.style.display = "none";

    oSpriteCtr.appendChild(oImg);
    oContainer.appendChild(oSpriteCtr);

    var iActiveState = 0;

    this.draw = function(iX, iY, fScale) {
      var bDraw = true;

      if (iY > iHeight * iScreenScale || iY < 1 * iScreenScale) {
        bDraw = false;
      }

      if (!bDraw) {
        oSpriteCtr.style.display = "none";
        return;
      }

      oSpriteCtr.style.display = "block";

      var fSpriteSize = 32 * fSpriteScale * fScale;

      oSpriteCtr.style.left = iX - fSpriteSize/2 + 'px';
      oSpriteCtr.style.top = iY - fSpriteSize/2 + 'px';

      oImg.style.height = ((strSprite == '') ? 24 : fSpriteSize) + 'px';

      oSpriteCtr.style.width = fSpriteSize + 'px';
      oSpriteCtr.style.height = ((strSprite == '') ? 30 : fSpriteSize) + 'px';

      oImg.style.left = -(fSpriteSize * iActiveState)+"px";
    }

    this.setState = function(iState) {
      iActiveState = iState;
    }
    
    this.remove = function() {
      oContainer.removeChild(oSpriteCtr);
    }
    
    this.div = oSpriteCtr;

  }



  function BGLayer(strImage, iLayerWidth) {
    var oLayer = document.createElement("div");
    oLayer.style.height = 10 * iScreenScale + 'px';
    oLayer.style.width = iWidth * iScreenScale + 'px';
    oLayer.style.position = "absolute";
    oLayer.style.overflow = "hidden";

    var oImg1 = new Image();
    oImg1.height = 20;
    oImg1.width = iLayerWidth;
    oImg1.style.position = "absolute";
    oImg1.style.left = "0px";

    var oImg2 = new Image();
    oImg2.height = 20;
    oImg2.width = iLayerWidth;
    oImg2.style.position = "absolute";
    oImg2.style.left = "0px";

    var oCanvas1 = document.createElement("canvas");
    oCanvas1.width = iLayerWidth;
    oCanvas1.height = 20;

    oImg1.onload = function() {
      oCanvas1.getContext("2d").drawImage(oImg1, 0, 0);
    }
    oImg1.src = "bg_" + strImage + ".png";

    oCanvas1.style.width = Math.round(iLayerWidth/2 * iScreenScale + iScreenScale)+"px"
    oCanvas1.style.height = (10 * iScreenScale)+"px";

    oCanvas1.style.position = "absolute";
    oCanvas1.style.left = "0px";

    var oCanvas2 = document.createElement("canvas");
    oCanvas2.width = iLayerWidth;
    oCanvas2.height = 20;
    oImg2.onload = function() {
      oCanvas2.getContext("2d").drawImage(oImg2, 0, 0);
    }
    oImg2.src = "bg_" + strImage + ".png";

    oCanvas2.style.width = Math.round(iLayerWidth/2 * iScreenScale)+"px";
    oCanvas2.style.height = (10 * iScreenScale)+"px";

    oCanvas2.style.position = "absolute";
    oCanvas2.style.left = Math.round(iLayerWidth * iScreenScale)+"px";

    oLayer.appendChild(oCanvas1);
    oLayer.appendChild(oCanvas2);

    oContainer.appendChild(oLayer);

    return {
      draw : function(fRotation) {
        // something is wrong in here. For now, it looks fine due to fortunate hill placement
        var iRot = -Math.round(fRotation);
        while (iRot < 0)
          iRot += 360;
        while (iRot > 360)
          iRot -= 360;

        // iRot is now between 0 and 360

        var iScaledWidth = (iLayerWidth/2 * iScreenScale);

        // one degree of rotation equals x width units:
        var fRotScale = iScaledWidth / 360;

        var iScroll = iRot * fRotScale;

        var iLeft1 = -iScroll;
        var iLeft2 = -iScroll + iScaledWidth;

        oCanvas1.style.left = Math.round(iLeft1) + 'px';
        oCanvas2.style.left = Math.round(iLeft2) + 'px';
      },
      div : oLayer
    }
  }

  var tileGridOverlayCtx = get_('tileGridOverlay').getContext('2d');
  
  function render() {
    var tileGridSizeFactor = TileGrid.getTileGridSizeFactor();
    var tileWidth = TileGrid.getTileWidth();
    var redMarkWidth = 4;
    // iCamHeight = 40;
    // (posx, posy) should be at (iViewCanvasWidth/2, iViewCanvasHeight - iViewYOffset) on view canvas
    oViewCanvas.width = oViewCanvas.width;
    // oViewCtx.fillStyle = "green";
    // oViewCtx.fillRect(0,0,oViewCanvas.width,oViewCanvas.height);
    oViewCtx.save();
    oViewCtx.translate(iViewCanvasWidth/2,iViewCanvasHeight-iViewYOffset);
    oViewCtx.rotate((180 + oPlayer.rotation) * Math.PI / 180);
    if (typeof (iViewCanvasWidth/2,iViewCanvasHeight-iViewYOffset) !== 'number') {
      console.log(iViewCanvasWidth/2,iViewCanvasHeight-iViewYOffset)
    }
    oViewCtx.drawImage(
      oMapImg,
      -oPlayer.x,-oPlayer.y
    );

    // (ernest): draw red line
    tileGridOverlayCtx.fillStyle = "red";
    if (isInfiniteMap && TileGrid.getTilesReady()) {
      if (oPlayer.x > tileWidth*2 || oPlayer.x < tileWidth ||
          oPlayer.y > tileWidth*2 || oPlayer.y < tileWidth) {
          TileGrid.reloadTileGrid();
          TileGrid.setLoadingVisibility(true);
          TileGrid.setTilesReady(false);
      }
    }
    tileGridOverlayCtx.fillRect(oPlayer.x/tileGridSizeFactor.x,oPlayer.y/tileGridSizeFactor.y,redMarkWidth,redMarkWidth);    
    
    oViewCtx.restore();

    oScreenCanvas.width = oScreenCanvas.width;
    oScreenCtx.fillStyle = "green";
    //oScreenCtx.fillRect(0,0,oScreenCanvas.width,oScreenCanvas.height);

    for (var i=0;i<aStrips.length;i++) {

      var oStrip = aStrips[i];

      if (iRenderMode == 0) {
        try {
        oScreenCtx.drawImage(
          oViewCanvas,
          iViewCanvasWidth/2 - (oStrip.stripwidth/2),
          //Math.floor(((iViewCanvasHeight-iViewYOffset) - oStrip.mapz)),
          ((iViewCanvasHeight-iViewYOffset) - oStrip.mapz)-1,
          oStrip.stripwidth,
          oStrip.mapzspan,

          0,(iHeight-oStrip.viewy)/fLineScale,iWidth/fLineScale,1
        );
        } catch(e) {};

      }

      if (iRenderMode == 1) {
        var iStripHeight = Math.max(3,oStrip.mapzspan);
        //oStrip.canvas.width=oStrip.canvas.width;
        oStrip.canvas.height = iStripHeight;
        oStrip.canvas.getContext("2d").clearRect(0,0,oStrip.stripwidth,1);
        try {
          oStrip.canvas.getContext("2d").drawImage(
            oViewCanvas,
            iViewCanvasWidth/2 - (oStrip.stripwidth/2),
            ((iViewCanvasHeight-iViewYOffset) - oStrip.mapz)-1,
            oStrip.stripwidth,
            oStrip.mapzspan,
            0,0,oStrip.stripwidth,iStripHeight
        );
        } catch(e) {};
      }

    }

    var iOffsetX = (iWidth/2)*iScreenScale;
    var iOffsetY = (iHeight - iViewYOffset)*iScreenScale;

    for (var i=0;i<aKarts.length;i++) {
      var oKart = aKarts[i];
      if (oKart.cpu) {
        var fCamX = -(oPlayer.x - oKart.x);
        var fCamY = -(oPlayer.y - oKart.y);

        var fRotRad = oPlayer.rotation * Math.PI / 180;

        var fTransX = fCamX * Math.cos(fRotRad) - fCamY * Math.sin(fRotRad);
        var fTransY = fCamX * Math.sin(fRotRad) + fCamY * Math.cos(fRotRad);

        var iDeltaY = -iCamHeight;
        var iDeltaX = iCamDist + fTransY;

        var iViewY = ((iDeltaY / iDeltaX) * iCamDist + iCamHeight) - iViewHeight;
        var fViewX = -(fTransX / (fTransY + iCamDist)) * iCamDist;

        var fAngle = oPlayer.rotation - oKart.rotation;
        while (fAngle < 0)
          fAngle += 360;
        while (fAngle > 360)
          fAngle -= 360;

        var iAngleStep = Math.round(fAngle / (360 / 22));
        if (iAngleStep == 22) iAngleStep = 0;

        oKart.sprite.setState(iAngleStep);

        // oKart.sprite.div.style.zIndex = Math.round(10000 - fTransY);
        
        oKart.sprite.draw(
          ((iWidth/2) + fViewX) * iScreenScale, 
          (iHeight - iViewY) * iScreenScale,
          fFocal / (fFocal + (fTransY))
        );

      }
    }

    oPlayer.sprite.div.style.zIndex = '10000';
    oPlayer.sprite.draw(iOffsetX,iOffsetY,1);

    // oHills.draw(oPlayer.rotation);
    // oTrees.draw(oPlayer.rotation);
  }

  function canMoveTo(iX, iY) {
    if (isInfiniteMap) return true;
    if (iX > iMapWidth-5 || iY > iMapHeight-5) return false;
    if (iX < 4 || iY < 4) return false;

    for (var i=0;i<oMap.collision.length;i++) {
      var oBox = oMap.collision[i];
      if (iX > oBox[0] && iX < oBox[0] + oBox[2]) {
        if (iY > oBox[1] && iY < oBox[1] + oBox[3]) {
          return false;
        }
      }
    }
    return true;
  }


  function move(oKart) {
    if (oKart.rotincdir) {
      oKart.rotinc += 2 * oKart.rotincdir;
    } else {
      if (oKart.rotinc < 0) {
        oKart.rotinc = Math.min(0, oKart.rotinc + 1);
      }
      if (oKart.rotinc > 0) {
        oKart.rotinc = Math.max(0, oKart.rotinc - 1);
      }
    }

    oKart.rotinc = Math.min(oKart.rotinc, fMaxRotInc);
    oKart.rotinc = Math.max(oKart.rotinc, -fMaxRotInc);

    if (oKart.speed) {
      oKart.rotation += (oKart.speedinc < 0 || (oKart.speedinc == 0 && oKart.speed < 0)) ? -oKart.rotinc : oKart.rotinc;
    }
    if (oKart.rotation < 0)
      oKart.rotation += 360;
    if (oKart.rotation > 360)
      oKart.rotation -= 360;

    if (!oKart.cpu) {
      if (oKart.rotincdir == 0) {
        // oKart.sprite.setState(0);
      } else {
        if (oKart.rotincdir < 0) {
          // if (oKart.rotinc == -fMaxRotInc && fMaxRotTimer > 0 && (new Date().getTime() - fMaxRotTimer) > 800)
            // oKart.sprite.setState(26);
          // else
            // oKart.sprite.setState(24);
        } else {
          // if (oKart.rotinc == fMaxRotInc && fMaxRotTimer > 0 && (new Date().getTime() - fMaxRotTimer) > 800)
            // oKart.sprite.setState(27);
          // else
            // oKart.sprite.setState(25);
        }
      }

      if (Math.abs(oKart.rotinc) != fMaxRotInc) {
        fMaxRotTimer = 0;
      } else if (fMaxRotTimer == 0) {
        fMaxRotTimer = new Date().getTime();
      }
    }

    
    oKart.speed += oKart.speedinc;

    var fMaxKartSpeed = fMaxSpeed;
    if (oKart.cpu) fMaxKartSpeed = 0;

    if (oKart.speed > fMaxKartSpeed)
      oKart.speed = fMaxKartSpeed;
    if (oKart.speed < -fMaxKartSpeed/4)
      oKart.speed = -fMaxKartSpeed/4;

    // move position
    var fMoveX = oKart.speed * Math.sin(oKart.rotation * Math.PI / 180);
    var fMoveY = oKart.speed * Math.cos(oKart.rotation * Math.PI / 180);

    var fNewPosX = oKart.x + fMoveX;
    var fNewPosY = oKart.y + fMoveY;

    if (canMoveTo(Math.round(fNewPosX), Math.round(fNewPosY))) {
      oKart.x = fNewPosX;
      oKart.y = fNewPosY;
    } else {
      oKart.speed *= -1;
    }

    // decrease speed
    oKart.speed *= 0.9;
  }

  function ai(oKart) {
    var aCurPoint = oMap.aipoints[oKart.aipoint];

    // first time, get the point coords
    if (!oKart.aipointx)
      oKart.aipointx = aCurPoint[0];
    if (!oKart.aipointy)
      oKart.aipointy = aCurPoint[1];

    var iLocalX = oKart.aipointx - oKart.x;
    var iLocalY = oKart.aipointy - oKart.y;

    iRotatedX = iLocalX * Math.cos(oKart.rotation * Math.PI / 180) - iLocalY * Math.sin(oKart.rotation * Math.PI / 180);
    iRotatedY = iLocalX * Math.sin(oKart.rotation * Math.PI / 180) + iLocalY * Math.cos(oKart.rotation * Math.PI / 180);

    var fAngle = Math.atan2(iRotatedX,iRotatedY) / Math.PI * 180;

    if (Math.abs(fAngle) > 10) {
      if (oKart.speed == fMaxSpeed) oKart.speedinc = -0.5;
      oKart.rotincdir = fAngle > 0 ? 1 : -1;
    } else {
      oKart.rotincdir = 0;
    }

    oKart.speedinc = 1;

    var fDist = Math.sqrt(iLocalX*iLocalX + iLocalY*iLocalY);
    if (fDist < 40) {
      oKart.aipoint++;
      if (oKart.aipoint >= oMap.aipoints.length)
        oKart.aipoint = 0;

      var oNewPoint = oMap.aipoints[oKart.aipoint];
      oKart.aipointx = oNewPoint[0] + (Math.random()-0.5) * 10;
      oKart.aipointy = oNewPoint[1] + (Math.random()-0.5) * 10;
    }
  }

  function cycle() {

    for (var i=0;i<aKarts.length;i++) {
      if (aKarts[i].cpu)
        ai(aKarts[i]);
      move(aKarts[i]);
    }

    setTimeout(cycle, 1000 / 15);

    render();
  }
  
  document.onmousedown = function(e) {
    var target = getTarget(e);
    var container = document.getElementById('gps-container');
    if (target.nodeName.toLowerCase() == 'canvas') {
      arrowsBlocked = true;
      container.style.border = '3px solid yellow';
    }
    else {
      arrowsBlocked = false;
      container.style.border = '3px solid #FFF';    
    }
  }
  
  var keydownEvent = isSafari ? 'onkeydown' : 'onkeypress';
  document[keydownEvent] = function(e) {
    if (arrowsBlocked && isArrowKey(e)) {
      stopEvent(e);
      if (!bRunning) return;
      isInfiniteMap = document.getElementById('infinite').checked;
      switch (e.keyCode) {
        case 38: // up
          oPlayer.speedinc = 1;
          break;
        case 37: // left
          oPlayer.rotincdir = 1;
          break;
        case 39: // right
          oPlayer.rotincdir = -1;
          break;
        case 40: // down
          oPlayer.speedinc -= 0.2;
          break;
      }
    }
  }

  document.onkeyup = function(e) {
    if (arrowsBlocked && isArrowKey(e)) {
      stopEvent(e);
      if (!bRunning) return;
      switch (e.keyCode) {
        case 38: // up
          oPlayer.speedinc = 0;
          break;
        case 37: // left
          oPlayer.rotincdir = 0;
          break;
        case 39: // right
          oPlayer.rotincdir = 0;
          break;
        case 40: // down
          oPlayer.speedinc = 0;
          break;
      }
    }
  }
  
  // pre fetching
  var oImg = new Image();
  oImg.src = "sprite_purple_marker.png";
  
  init();
  function init() {        
    // reset the game
    resetGame(strMap);
    // pre fetching arrow image
    var oImg = new Image();
    oImg.src = "arrow.gif";
    // try {
      startGame();
    // } catch(e) {
      // throw new Error("simulasjsd g asdf.");
    // }
  }
  

// public hooks to the internal funcitons
window.MarioKartControl = {
    oMarker: oPlayer,
    setArrowsBlocked: function(status) {
      arrowsBlocked = status;
    },
    updateCanvasMarkers: function(mOffX, mOffY, tileWidth) {
      for (var i=0;i<aKarts.length;i++) {
        var oKart = aKarts[i];
        if (oKart.cpu) {
          oKart.x += mOffX * tileWidth;
          oKart.y += mOffY * tileWidth;
        }
      }
    },
    
    updateRedLine: function(mOffX, mOffY, tileWidth) {
      var tileGridSizeFactor = TileGrid.getTileGridSizeFactor();
      var redLineClipboardCtx = get_('redLineClipboard').getContext('2d');
      redLineClipboardCtx.drawImage(get_('tileGridOverlay'), 0, 0);
       tileGridOverlayCtx.clearRect(0,0,1000,1000);
      tileGridOverlayCtx.drawImage(get_('redLineClipboard'), 
        mOffX * tileWidth/tileGridSizeFactor.x,
        mOffY * tileWidth/tileGridSizeFactor.y);
      redLineClipboardCtx.clearRect(0,0,1000,1000);
    },
    addMarker: function(el) {
      resetPlayers('add', el);
    },
    
    deleteMarker: function(el) {
      resetPlayers('remove', el);
    },
    
    setiCamHeight: function(height) {
      iCamHeight = height;
    },
    
    sliderResetScreen: function() {
      resetScreen();
    },
    
    setRenderMode : function(iValue) {
       setRenderMode(iValue);
    },
    setQuality : function(iValue) {
       setQuality(iValue);
    },
    setScreenScale : function(iValue) {
       setScreenScale(iValue);
    },
    setMusic : function(iValue) {
      bMusic = !!iValue;
      if (bMusic && !bMusicPlaying && bRunning) {
        startMusic();
      }
      if (!bMusic && bMusicPlaying) {
        stopMusic();
      }
    }

  };

}

window.onload = function() {
  TileGrid.init();
  MarioKart();    
}



/*
 *
 * Slider code from YUI
 *
 */
 
// var Event = YAHOO.util.Event,
//     Dom   = YAHOO.util.Dom,
//     lang  = YAHOO.lang,
//     slider, 
//     bg="slider-bg", thumb="slider-thumb", 
//     valuearea="slider-value", textfield="slider-converted-value"
// 
// // The slider can move 0 pixels up
// var topConstraint = 0;
// 
// // The slider can move 200 pixels down
// var bottomConstraint = 150;
// 
// // Custom scale factor for converting the pixel offset into a real value
// var scaleFactor = 1.5;
// 
// // The amount the slider moves when the value is changed with the arrow
// // keys
// var keyIncrement = 20;
// 
// Event.onDOMReady(function() {
// 
//     slider = YAHOO.widget.Slider.getVertSlider(bg, 
//                      thumb, topConstraint, bottomConstraint);
// 
//     slider.getRealValue = function() {
//         return Math.round(this.getValue() * scaleFactor);
//     }
// 
//     slider.subscribe("change", function(offsetFromStart) {
// 
//         var valnode = Dom.get(valuearea);
//         var fld = Dom.get(textfield);
// 
//         // Display the pixel value of the control
//         valnode.innerHTML = offsetFromStart;
// 
//         // use the scale factor to convert the pixel offset into a real
//         // value
//         var actualValue = slider.getRealValue();
// 
//         // update the text box with the actual value
//         fld.value = actualValue;
//         // hook mariokart values here
//         MarioKartControl.setiCamHeight(actualValue);
//         MarioKartControl.sliderResetScreen();
//          
//         // Update the title attribute on the background.  This helps assistive
//         // technology to communicate the state change
//         // Dom.get(bg).title = "slider value = " + actualValue;
// 
//     });
// 
//     slider.subscribe("slideStart", function() {
//             YAHOO.log("slideStart fired", "warn");
//         });
// 
//     slider.subscribe("slideEnd", function() {
//             YAHOO.log("slideEnd fired", "warn");
//         });
// 
//     // set an initial value
//     slider.setValue(20);
// 
//     // Listen for keystrokes on the form field that displays the
//     // control's value.  While not provided by default, having a
//     // form field with the slider is a good way to help keep your
//     // application accessible.
//     Event.on(textfield, "keydown", function(e) {
// 
//         // set the value when the 'return' key is detected
//         if (Event.getCharCode(e) === 13) {
//             var v = parseFloat(this.value, 10);
//             v = (lang.isNumber(v)) ? v : 0;
// 
//             // convert the real value into a pixel offset
//             slider.setValue(Math.round(v/scaleFactor));
//         }
//     });
// 
//     // Use setValue to reset the value to white:
//     Event.on("putval", "click", function(e) {
//         slider.setValue(100, false); //false here means to animate if possible
//     });
// 
//     // Use the "get" method to get the current offset from the slider's start
//     // position in pixels.  By applying the scale factor, we can translate this
//     // into a "real value
//     Event.on("getval", "click", function(e) {
//         YAHOO.log("Current value: "   + slider.getValue() + "\n" + 
//                   "Converted value: " + slider.getRealValue(), "info", "example"); 
//     });
//     Dom.get(bg).title = "Drag the red button (literal) at your own risk";
// });
