/*  MovableMap, javascript for map-in-viewport applications, version 0.1
 *  (c) 2008 Jason LaPier
 *
 *  MovableMap is freely distributable under the terms of an MIT-style license.
 *  For details and example usage, see the Off the Line web site: 
 *  http://offtheline.net/javascript
 *
 *  Requires:
 *    Prototype version 1.6+, http://www.prototypejs.org/ 
 *    Script.aculo.us effects.js version 1.8+, http://script.aculo.us
 *    Low Pro version 0.5+, http://lowprojs.com
 *--------------------------------------------------------------------------*/

var MoveableMap = Class.create({
  
  // initialize with inner block element (the one that's clipped),
  // container block element, and a number of pixels to move
  initialize: function(innerMap, mapContainer, numPixels) {
    this.innerMap = innerMap;
    this.mapContainer = mapContainer;
    this.numPixels = numPixels;

    // left and top are really the stored offsets, which is why we abs() them
    // in actual positioning, they will be negative
    this.left = parseInt(this.innerMap.getStyle('left')).abs();
    this.top  = parseInt(this.innerMap.getStyle('top')).abs();
    
    // these are the maximum postion offsets (that will keep us from over- 
    // sliding and getting blank space on the sides)
    this.maxLeft = this.innerMap.getWidth() -  parseInt(this.mapContainer.getStyle('width'));
    this.maxTop =  this.innerMap.getHeight() - parseInt(this.mapContainer.getStyle('height'));
  },

  // slides the map in the compass direction specified
  // maximum makes the map move to the extreme offset for that direction
  slideMap: function(dir, maximum) {
    toTop = this.top; toLeft = this.left;
    if (maximum) {
      switch(dir) {
        case 'N':  toTop = 0;                                  break;
        case 'NE': toTop = 0;           toLeft = this.maxLeft; break;
        case 'NW': toTop = 0;           toLeft = 0;            break;
        case 'S':  toTop = this.maxTop;                        break;
        case 'SE': toTop = this.maxTop; toLeft = this.maxLeft; break;
        case 'SW': toTop = this.maxTop; toLeft = 0;            break;
        case 'E':                       toLeft = this.maxLeft; break;
        case 'W':                       toLeft = 0;            break;
      }
    } else {
      switch(dir) {
        case 'N':  toTop -= this.numPixels;                           break;
        case 'NE': toTop -= this.numPixels; toLeft += this.numPixels; break;
        case 'NW': toTop -= this.numPixels; toLeft -= this.numPixels; break;
        case 'S':  toTop += this.numPixels;                           break;
        case 'SE': toTop += this.numPixels; toLeft += this.numPixels; break;
        case 'SW': toTop += this.numPixels; toLeft -= this.numPixels; break;
        case 'E':                           toLeft += this.numPixels; break;
        case 'W':                           toLeft -= this.numPixels; break;
      }
    }
    this.moveMapToCoords(toLeft, toTop);
  },

  // move the map to the specific top/left coordinates 
  // without pushing map beyond it's borders
  moveMapToCoords: function(toLeft, toTop, noslide) {
    this.left = [[toLeft, this.maxLeft].min(), 0].max();
    this.top  = [[toTop,  this.maxTop ].min(), 0].max();
    if (noslide) {
      this.innerMap.setStyle({left:'-'+this.left+'px', top:'-'+this.top+'px'});
    } else { 
      this.innerMap.morph({left:'-'+this.left+'px', top:'-'+this.top+'px'});
    }
  },
  
  // instead of jumping to a position, just bump the offsets by an (x,y) pair
  // this is mainly used for the dragging feature
  nudgeMap: function(x, y) {
    this.left = [[this.left + x, this.maxLeft].min(), 0].max();
    this.top  = [[this.top + y,  this.maxTop ].min(), 0].max();
    this.innerMap.setStyle({left:'-'+this.left+'px', top:'-'+this.top+'px'});
  },
  
  setCursor: function(cursorStyle) { 
    this.innerMap.setStyle({cursor: cursorStyle});
  },
  
  
  /* helper function: returns dimensional offsets in the form [left, top] */
  getCenterOfElement: function(element) {
    _offset = element.positionedOffset();
    _left = _offset[0]; _top = _offset[1];
    container_dims = this.mapContainer.getDimensions();
    e_dims = element.getDimensions();
    centerLeft = _left + (e_dims.width  / 2) - (container_dims.width  / 2);
    centerTop  = _top  + (e_dims.height / 2) - (container_dims.height / 2);
    return [centerLeft, centerTop];
  },

  /* TODO: test this
  */
  moveMapToElement: function(element, noslide) { 
    _center = this.getCenterOfElement(element);
    myLeft = _center[0]; myTop = _center[1];
    this.moveMapToCoords( myLeft, myTop, noslide );
  }
}); // end MoveableMap class

var MAPDIRS = new Array('NE','N','NW','E','W','SE','S','SW');

/*  MapMover: this behavior can be used for clickable directional arrows
 *  if you add a 'mapdir' attribute to the elements that will be clicked
 *  eg:  <td class="map_arrow" mapdir="NE"></td>
 *  the mapdir must be one of the compass direction abbreviations:
 *  N, NE, NW, W, E, S, SE, SW
 *  If the element has no mapdir, then the map will be centered on the element
 *  itself (NOTE: if the element is not inside the map, this will break; 
 *  use MapJumper for links outside the map)    */
 
var MapMover = Behavior.create({
  direction: null,
  mapToMove: null,
  myLeft: null,
  myTop: null,
  initialize: function(mapToMove) {
    this.mapToMove = mapToMove;
    this.direction = this.element.readAttribute('mapdir');
  },

  // nudge us one increment in the compass direction of the element
  // if the element is not a direction button, then center in on the element
  onclick: function() {
    if (this.direction) {
      this.mapToMove.slideMap(this.direction);
    } else {
      _center = this.mapToMove.getCenterOfElement(this.element);
      myLeft = _center[0]; myTop = _center[1];
      this.mapToMove.moveMapToCoords( myLeft, myTop );
    }
  },

  // double-click on a direction and it will jump to the edge of that 
  // corner or side
  ondblclick: function() {
    this.mapToMove.slideMap(this.direction, true);
  } 
}); // end MapMover Behavior


/*  MapJumper: this behavior will center the map on the target element
 *  identified by HTML attribute targetelement
 *  e.g. <span class="target" targetelement="map-point-A">Center on A!</span>
 *  (NOTE: the target element must be inside the map or this will break)    */

var MapJumper = Behavior.create({
  mapToMove: null,
  targetElement: null,
  toLeft: 0,
  toTop: 0,
  
  initialize: function(mapToMove) {
    this.mapToMove = mapToMove;
    this.targetElement = $(this.element.readAttribute('targetelement'));
    _center = this.mapToMove.getCenterOfElement(this.targetElement);
    this.toLeft = _center[0]; this.toTop = _center[1]
  },

  // jump to position of the target element
  onclick: function() {
    this.mapToMove.moveMapToCoords( this.toLeft, this.toTop );
  }
}); // end MapJumper Behavior


/* MapDrag: This behavior is what we use to drag the whole map around. It 
   should be attached to the main map element                     */
   
var MapDrag = Behavior.create({
  mapToMove: null,
  mousedown: 0,
  start_m_x: 0,
  start_m_y: 0,
  mainElement: null,
  
  initialize: function(mapToMove) {
    this.mapToMove = mapToMove;
    this.mainElement = this.element;
  },

  onmousedown: function(event) {
    this.mousedown = 1;
    this.mapToMove.setCursor('move');
    this.start_m_x = Event.pointerX(event);
    this.start_m_y = Event.pointerY(event);
    return false;
  },
  
  onmouseup: function() {
    this.mousedown = 0;
    this.mapToMove.setCursor('default');
  },
  
  onmouseout: function(event) {
    // have to check for target element (ie moused-into element) to make sure 
    // it's not in the same table or something annoying like that
    target = event.toElement || event.relatedTarget;
    if( target != null && 
      !(target == this.mainElement || target.descendantOf(this.mainElement))) {
      this.mousedown = 0;
      this.mapToMove.setCursor('default');
    }
  },
  
  onselectstart: function() { return false; },
  
  onmousemove: function(event) {
    if (this.mousedown == 1) {
      // calculate the change and reassign our saved values for start x and y
      // otherwise we'll get a weird acceleration effect
      new_m_x = Event.pointerX(event); new_m_y = Event.pointerY(event);
      change_m_x = this.start_m_x - new_m_x;
      change_m_y = this.start_m_y - new_m_y;
      this.start_m_x = new_m_x; this.start_m_y = new_m_y;
      this.mapToMove.nudgeMap(change_m_x, change_m_y);
    }
    return true;
  }
}); // end MapDrag behavior




