/**
 * @fileoverview Mapserv class provides support functions for advanced
 * web clients using the MapServer.  Performs coordinate management,
 * layer management, and url construction services.
 */

// Support functions for advanced web clients using the
// MapServer. Original coding 02-25-2000. - SDL -
//
// Re-write for MapServer 3.6+ and DHTML standardization July 2002. - SDL -
// Simplified layer handling with the addition of DHTML legend containers (11/4/2004). - SDL -
// Big-time re-write to de-couple from dbox.js etc... (04/10/2006). - SDL - 

var MAPSERV_UNITS_METERS = 0; // unit types
var MAPSERV_UNITS_FEET = 1;
var MAPSERV_UNITS_MILES = 2;
var MAPSERV_UNITS_KILOMETERS = 3;
var MAPSERV_UNITS_DD = 4;

var MAPSERV_DRAW = 0; // callback types
var MAPSERV_QUERY = 1;

/**
 * Construct a new Mapserv object.
 * @class This is the basic Mapserv class.
 * 
 * @constructor
 * @param {String} mapserver The url for the MapServer instance that will create maps.
 * @param {String} mapfile The mapfile to be used.
 * @param {Double} minx The minimum x coordinate for the initial (default) map extent.
 * @param {Double} miny The minimum y coordinate for the initial (default) map extent.
 * @param {Double} maxx The maximum x coordinate for the initial (default) map extent.
 * @param {Double} maxy The maximum y coordinate for the initial (default) map extent.
 * @param {Integer} width The width (in pixels) of the maps to be created.
 * @param {Integer} height The height (in pixels) of the maps to be created.
 * @return A new mapserv object
 */
function Mapserv(mapserver, mapfile, minx, miny, maxx, maxy, width, height)
{  
  /**
   * mode in which the MapServer executable will be called.

   * <p>Valid values are map, query and nquery although it is certainly possible
   * to create more complex queries outside of the Mapserv object.</p>

   * @type String
   */
  this.mode = 'map'; 

  /**
   * a complete url for a map draw or query operation.
   * @type String
   */
  this.url = '';

  /**
 
   * an associative array of layer status, keyed by layer name; values
   * are booleans.

   * @type Array
   */
  this.layers = new Array(); 

  /**
   * the url for the MapServer executable that will create maps.
   * @type String
   */
  this.mapserver = mapserver;

  /**
   * the url for the MapServer executable that will handle queries.
   * @type String
   */
  this.queryserver = mapserver;

  /**
   * the MapServer configuration file for map draws.
   * @type String
   */
  this.mapfile = mapfile;

  /**
   * the MapServer configuration file for query operations.
   * @type String
   */
  this.queryfile = mapfile;

  /**

   * a four-member array of Doubles defining the rectangular extent of
   * the map (in map coordinates).

   * <p>
   * <ul>
   *  <li>extent[0] - minimum x map coordinate</li>
   *  <li>extent[1] - minimum y map coordinate</li>
   *  <li>extent[2] - maximum x map coordinate</li>
   *  <li>extent[3] - maximum y map coordinate</li>
   * </ul>
   * </p>

   * @type Array
   */
  this.extent = new Array(minx, miny, maxx, maxy);

  /**
   * a two-member array of Doubles defining a point of interest (in map
   * coordinates) to the current operation (typically from a user's
   * mouse click).

   * <p>
   * <ul>
   *  <li>point[0] is x</li>
   *  <li>point[1] is y</li>
   * </ul>
   * </p>

   * @type Array
   */
  this.point = new Array(-1, -1); 

  /**

   * a four-member array of Doubles defining a rectangular extent (in
   * map coordinates) for a query operation.

   * <p>
   * <ul>
   *  <li>queryextent[0] is minimum x</li>
   *  <li>queryextent[1] is minimum y</li>
   *  <li>queryextent[2] is maximum x</li>
   *  <li>queryextent[3] is maximum y</li>
   * </ul>
   * </p>

   * @type Array
   */
  this.queryextent = new Array(-1, -1, -1, -1);

  /**

   * a two-member array of Doubles defining a point of interest (in
   * image coordinates) to the current query operation (typically from
   * a user's mouse click).

   * <p>
   * <ul>
   *  <li>querypoint[0] is x</li>
   *  <li>querypoint[1] is y</li>
   * </ul>
   * </p>

   * @type Array
   */
  this.querypoint = new Array(-1, -1);

  /**
   * the width of the map image (in pixels).
   * @type Integer
   */
  this.width = width;

  /**
   * the height of the map image (in pixels).
   * @type Integer
   */
  this.height = height;

  /**

   * a url query string ("&this=that&some=other") used to pass any
   * application-specific options for a map draw operation that are
   * not covered in the {@link #draw} method.

   * @type String
   */
  this.options = '';

  /**

   * a url query string ("&this=that&some=other") used to store any
   * application-specific options for a query operation that are not
   * covered in the {@link #query} method.

   * @type String
   */
  this.queryoptions = '';

  /**
   * a reference map object.
   * @type Mapserv
   */
  this.referencemap = null;
  
  /**
   * calculated length of a pixel side in map units.
   * @type Double
   */
  this.cellsize = adjustExtent(this.extent, this.width, this.height);

  /**
   * the extent that was passed to this Mapserv instance at construction.
   * @type Array
   */
  this.defaultextent = this.extent;

  /**

   * the zoom factor to be applied on zoom-in operations (the inverse
   * is taken for zoom-out operations).

   */
  this.zoomsize = 2;

  /**
   * direction of zoom.

   * <p>
   * <ul>
   *  <li>-1 = out</li>
   *  <li>0 = none (pan)</li>
   *  <li>1 = in</li>
   * </ul>
   * </p>

   * @type Integer
   */
  this.zoomdir = 0; // pan to start

  /**
   * minimum scale to allow for map draws.

   * <p>value used is denominator of the representative fraction
   * (1:<b>x</b>)</p>

   * <p>value of -1 indicates no minimum</p>

   * @type Integer
   */
  this.minscale = -1;


  /**
   * maximum scale to allow for map draws.

   * <p>value used is denominator of the representative fraction
   * (1:<b>x</b>)</p>

   * <p>value of -1 indicates no maximum</p>

   * @type Integer
   */
  this.maxscale = -1;

  /**
   * amount by which to move the map in automatic pan operations.
   * <p>0 &lt; pansize &lt 1</p>
   * @type Float
   */
  this.pansize = .8;

  /** 
   * a value used in scale calculations.

   * <p>default: 72 (typical monitor resolution)</p>
   * @type Integer
   */
  this.pixelsPerInch = 72;

  /**
   * number of inches in the current map unit.
   * <p>default: 39.3701 (inches in a meter)</p>
   * @type Float
   */
  this.inchesPerMapUnit = 39.3701;

  // handlers/callbacks

  /**
   * function to be called at map draw time.

   * <p>Must be defined and set (via {@link #setHandler}) elsewhere in
   * the application, or nothing happens at draw time.</p>

   * <p>A minimal setup, handling nothing but the map draw, might be:<br>
   * <font size="-1">(note the "ms" variable below is a Mapserv object
   * instance, and "function" is intentionally mispelled to avoid
   * confusing our documentation generator)</font><p>

   * <pre>
   * function map_draw() {
   *   main.setImage(ms.url);
   * }
   * ms.setHandler(MAPSERV_DRAW, map_draw);
   * </pre>

   * <p>Typically, the function would be more complex, handling
   * reference map, scalebar, and legend drawing as well.  Developers
   * are free to implement essentially any desired functionality
   * here.</p>

   * @type Function
   */
  this.drawHandler = null;

  /**
   * function to be called at query time.

   * <p>Must be defined and set (via {@link #setHandler}) elsewhere in
   * the application, or nothing happens at query time.</p>

   * <p>A setup to display query results in a new window might be:<br>
   * <font size="-1">(note the "ms" variable below is a Mapserv object
   * instance, and "function" is intentionally mispelled to avoid
   * confusing our documentation generator)</font><p>

   * <pre>
   * function do_query() {
   *   querywin = window.open(ms.url, 'querywin');
   *   querywin.focus();
   * }
   * ms.setHandler(MAPSERV_QUERY, do_query);
   * </pre>

   * <p>Developers are free to implement essentially any desired
   * functionality here.</p>

   * @type Function
   */
  this.queryHandler = null;

  /**
   * object reference to avoid "this" conflicts with callbacks and event handlers
   * @private
   * @type Mapserv
   */
  var self = this; 

  //
  // private functions
  //

  /**

   * adjust values of given extent to fit {@link #width} and {@link
   * #height}

   * @private
   * @param {Array} extent extent Array to be adjusted
   * @param {Integer} width width of map image in pixels
   * @param {Integer} height of map image in pixels
   * @returns {Double} ground resolution of pixels in map image
   */
  function adjustExtent(extent, width, height) {
    var cellsize = Math.max((extent[2] - extent[0])/width, (extent[3] - extent[1])/height);

    if(cellsize > 0) {
      var ox = Math.max((width - (extent[2] - extent[0])/cellsize)/2,0);
      var oy = Math.max((height - (extent[3] - extent[1])/cellsize)/2,0);

      extent[0] = extent[0] - ox*cellsize;
      extent[1] = extent[1] - oy*cellsize;
      extent[2] = extent[2] + ox*cellsize;
      extent[3] = extent[3] + oy*cellsize;
    }	

    return(cellsize);
  }

  /**
   * convert an extent array to a polygon array.
   * @private
   * @param {Array} extent extent array to be converted
   * @returns {Array} polygon array (essentially a set of five
   * coordinate pairs)
   */
  function extentToPolygon(extent) {
    var polygon = new Array(10);

    polygon[0] = extent[0];
    polygon[1] = extent[3];
    polygon[2] = extent[2];
    polygon[3] = extent[3];
    polygon[4] = extent[2];
    polygon[5] = extent[1];
    polygon[6] = extent[0];
    polygon[7] = extent[1];
    polygon[8] = extent[0];
    polygon[9] = extent[3];

    return(polygon);
  }

  //
  // function prototypes follow (potential callbacks use 'self' instead of 'this')
  //

  /**
   * set the map units.

   * @param {Enumerated} type constant for desired units type.  Valid
   * type values are:

   * <ul>
   * <li>MAPSERV_UNITS_METERS (0)</li>
   * <li>MAPSERV_UNITS_FEET (1)</li>
   * <li>MAPSERV_UNITS_MILES (2)</li>
   * <li>MAPSERV_UNITS_KILOMETERS (3)</li>
   * <li>MAPSERV_UNITS_DD (4)</li>
   * </ul>
   */
  this.setUnits = function(type) {
    if (type == MAPSERV_UNITS_METERS)
      self.inchesPerMapUnit = 39.3701;
		else if (type == MAPSERV_UNITS_FEET)
      self.inchesPerMapUnit = 12.0;
    else if (type == MAPSERV_UNITS_MILES)
      self.inchesPerMapUnit = 63360.0;
    else if (type == MAPSERV_UNITS_KILOMETERS)
      self.inchesPerMapUnit = 39370.1;
    else if (type == MAPSERV_UNITS_DD)
      self.inchesPerMapUnit = 4374754; // at the equator
  }

  /**
   * set a handler function for draw/query operations.

   * @param {Enumerated} type constant for handler type to be set.
   * Valid type values are:

   * <ul>
   * <li>MAPSERV_DRAW (0)</li>
   * <li>MAPSERV_QUERY (1)</li>
   * </ul>


   * @param {Function} handler function to be called

   * @see #drawhandler
   * @see #queryhandler

   */
  this.setHandler = function(type, handler) {
    if (type == MAPSERV_DRAW)
      self.drawHandler = handler;
    else if (type == MAPSERV_QUERY)
      self.queryHandler = handler;    
  }

  /**

   * convert an image coordinate pair to a map coordinate pair, and
   * store in the {@link #point} field.

   * @param {Integer} x the x image coordinate
   * @param {Integer} y the y image coordinate
   */
  this.imageToMap = function(x, y) {
    var dx, dy;

    dx = this.extent[2] - this.extent[0];
    dy = this.extent[3] - this.extent[1];
    this.point[0] = this.extent[0] + this.cellsize*x;
    this.point[1] = this.extent[3] - this.cellsize*y;
  }

  /**
   * set the draw/query status of a layer.
   * @param {String} name The name of the layer to set status for
   * @param {Boolean} status <b>0/false</b> - off; <b>1/true</b> - on
   */
  this.setLayer = function(name, status) {
    self.layers[name] = status;
  }

  /**
   * Set draw/query status for all layers to off.
   */
  this.layersOff = function() {
    self.layers = new Array();
  }

  /**
   * Get the list of currently active layers.
   * @param {String} delimiter the character(s) to use to delimit the returned list
   * @returns A delimited string of active layers
   * @type String
   */
  this.getLayers = function(delimeter) {
    var list = new Array();
    var keys;

    for (key in this.layers)
      if(this.layers[key]) list.push(key);

    return list.join(delimeter);
  }

  /**

   * convert a box defined in image coordinates to an extent defined in
   * map coordinates and store in the {@link #extent} field.

   * @param {Integer} minx minimum x image coordinate
   * @param {Integer} miny minimum y image coordinate
   * @param {Integer} maxx maximum x image coordinate
   * @param {Integer} maxy maximum y image coordinate

   */
  this.applyBox = function(minx, miny, maxx, maxy) {
    var temp = new Array(4);

    temp[0] = this.extent[0] + this.cellsize*minx;
    temp[1] = this.extent[3] - this.cellsize*maxy;
    temp[2] = this.extent[0] + this.cellsize*maxx;	
    temp[3] = this.extent[3] - this.cellsize*miny;

    this.extent = temp;
 
    this.cellsize = adjustExtent(this.extent, this.width, this.height);

    if(this.minscale != -1 && this.getScale() < this.minscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.minscale);
    }
    if(this.maxscale != -1 && this.getScale() > this.maxscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.maxscale);
    }
  }

  /**

  * calculate a new map extent based on {@link #zoomdir}, {@link
  * #zoomsize}, and the passed point (in image coordinates), and store
  * in the {@link #extent} field.

  * @param {Integer} x image coordinate x
  * @param {Integer} y image coordinate y
  */
  this.applyZoom = function(x, y) {
    var dx, dy, mx, my;
    var zoom;

    if(this.zoomdir == 1 && this.zoomsize != 0)
      zoom = this.zoomsize;
    else if(this.zoomdir == -1 && this.zoomsize != 0)
      zoom = 1/this.zoomsize;
    else
      zoom = 1;

    dx = this.extent[2] - this.extent[0];
    dy = this.extent[3] - this.extent[1];
    mx = this.extent[0] + this.cellsize*x; // convert *click* to map coordinates
    my = this.extent[3] - this.cellsize*y;

    this.extent[0] = mx - .5*(dx/zoom);
    this.extent[1] = my - .5*(dy/zoom);
    this.extent[2] = mx + .5*(dx/zoom);
    this.extent[3] = my + .5*(dy/zoom);

    this.cellsize = adjustExtent(this.extent, this.width, this.height);

    if(this.minscale != -1 && this.getScale() < this.minscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.minscale);
    }
    if(this.maxscale != -1 && this.getScale() > this.maxscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.maxscale);
    }
  }


  /**

  * calculate a new map extent to be centered on an image coordinate
  * from the reference map image, and store in the {@link #extent}
  * field.

  * @param {Integer} x reference map image coordinate x value
  * @param {Integer} y reference map image coordinate y value

  */
  this.applyReference = function(x,y) {
    var mx, my;
    var dx, dy;

    if(!this.referencemap) return;

    dx = this.extent[2] - this.extent[0];
    dy = this.extent[3] - this.extent[1];
    mx = this.referencemap.extent[0] + this.referencemap.cellsize*x;
    my = this.referencemap.extent[3] - this.referencemap.cellsize*y;

    this.extent[0] = mx - .5*dx;
    this.extent[1] = my - .5*dy;
    this.extent[2] = mx + .5*dx;
    this.extent[3] = my + .5*dy;

    this.cellsize = adjustExtent(this.extent, this.width, this.height);
  }

  /**

   * construct a four-member query extent array in image coordinates
   * and store in the {@link #queryextent} field.

   * @param {Integer} minx minimum x image coordinate
   * @param {Integer} miny minimum y image coordinate
   * @param {Integer} maxx maximum x image coordinate
   * @param {Integer} maxy maximum y image coordinate
   */
  this.applyBoxQuery = function(minx, miny, maxx, maxy) {
    var temp = new Array(4);

    // convert to map coordinates
    // temp[0] = this.extent[0] + this.cellsize*minx;
    // temp[1] = this.extent[3] - this.cellsize*maxy;
    // temp[2] = this.extent[0] + this.cellsize*maxx;	
    // temp[3] = this.extent[3] - this.cellsize*miny;

    // leave in pixel coordinates
    temp[0] = minx;
    temp[1] = miny;
    temp[2] = maxx;
    temp[3] = maxy;

    this.queryextent = temp;
  }

  /**

   * construct a two-member query point array in image coordinates
   * and store in the {@link #querypoint} field.

   * @param {Integer} x image coordinate x value
   * @param {Integer} y image coordinate y value
   */
  this.applyPointQuery = function(x,y) {
    var dx, dy;

    // convert to map coordinates
    // dx = this.extent[2] - this.extent[0];
    // dy = this.extent[3] - this.extent[1];
    // this.querypoint[0] = this.extent[0] + this.cellsize*x;
    // this.querypoint[1] = this.extent[3] - this.cellsize*y;

    // leave in pixel coordinates
    this.querypoint[0] = x;
    this.querypoint[1] = y;
  }

  /**

  * construct a query url using the current settings of various
  * fields, store in the {@link #url} field, and call the {@link
  * #queryHandler}.

  * <p>Fields/methods used in assembling the url are:</p>

  * <ul>
  * <li>{@link #queryserver}</li>
  * <li>(mode=) {@link #mode}</li>
  * <li>(map=) {@link #queryfile}</li>
  * <li>(imgext=) {@link #extent}</li>
  * <li>(imgxy=) {@link #querypoint}</li>
  * <li>(imgbox=) {@link #queryextent}</li>
  * <li>(imgsize=) {@link #width} {@link #height}</li>
  * <li>(layers=) {@link #getLayers}</li>
  * <li>{@link #queryoptions}</li>
  * </ul>

  */
  this.query = function() {  
    var layerlist = this.getLayers('+');

    // point or box based queries 
    this.url = this.queryserver +
               '?mode=' + this.mode +
               '&map=' + this.queryfile +
	       '&imgext=' +  this.extent.join('+') +
               '&imgxy=' +  this.querypoint.join('+') +            
               '&imgbox=' + this.queryextent.join('+') +
               '&imgsize=' + this.width + '+' + this.height;
 
    if(layerlist) this.url += '&layers=' + layerlist;
    if(this.queryoptions) this.url += this.queryoptions;	   

    if (self.queryHandler) self.queryHandler();

    return;
  }

  /**

  * construct a pair of urls (one for the main map and one for the
  * reference map, if applicable) using the current settings of
  * various fields, store them in the {@link #url} and {@link
  * #referencemap}.url fields, respectively, and call the {@link
  * #drawHandler}.

  * <p>Fields/methods used in assembling the url for the main map
  * are:</p>

  * <ul>
  * <li>{@link #mapserver}</li>
  * <li>(map=) {@link #mapfile}</li>
  * <li>(mapext=) {@link #extent}</li>
  * <li>(mapsize=) {@link #width}+{@link #height}</li>
  * <li>(layers=) {@link #getLayers}</li>
  * <li>{@link #options}</li>
  * </ul>

  * <p>Fields/methods used in assembling the url for the reference map
  * are:</p>

  * <ul>
  * <li>{@link #mapserver}</li>
  * <li>(map=) {@link #referencemap}.mapfile</li>
  * <li>(mapext=) {@link #extent}</li>
  * <li>(mapsize=) {@link #width}+{@link #height}</li>
  * </ul>

  */
  this.draw = function() {
    var layerlist = this.getLayers('+');
 
    if(this.referencemap)
      this.referencemap.url = this.mapserver +
                              '?mode=reference' +
                              '&map=' + this.referencemap.mapfile +
                              '&mapext=' + this.extent.join('+') +
                              '&mapsize=' + this.width + '+' + this.height;  

    this.url = this.mapserver +
               '?mode=map' + 
               '&map=' + this.mapfile +
               '&mapext=' + this.extent.join('+') +
               '&mapsize=' + this.width + '+' + this.height +
               '&layers=' + layerlist +
	       this.options;

    if (self.drawHandler) self.drawHandler();
  }

  /** 

  * redraw the map at the default extent.

  * @see #defaultextent
  */
  this.zoomDefault = function() {
    this.mode = map;
    this.extent = this.defaultextent;
    this.cellsize = adjustExtent(this.extent, this.width, this.height);
    this.draw();
  }

  /**
   * set {@link #extent} using the given values.
   * @param {Double} minx minimum x coordinate
   * @param {Double} miny minimum y coordinate
   * @param {Double} maxx maximum x coordinate
   * @param {Double} maxy maximum y coordinate
   */
  this.setExtent = function(minx, miny, maxx, maxy) {
    this.extent[0] = minx;
    this.extent[1] = miny;
    this.extent[2] = maxx;
    this.extent[3] = maxy;

    this.cellsize = adjustExtent(this.extent, this.width, this.height);

    if(this.minscale != -1 && this.getScale() < this.minscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.minscale);    
    }
    if(this.maxscale != -1 && this.getScale() > this.maxscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.maxscale);
    }
  }

  /**

  * set {@link #extent} to be centered on the given point and minimally
  * fit the circle defined by the point and supplied radius.

  * @private
  * @param {Double} x map coordinate x value
  * @param {Double} y map coordinate y value

  * @param {Double} radius desired distance from given point to edge of map

  */
  this.setExtentFromRadius = function(x, y, radius) {
    this.extent[0] = x - radius;
    this.extent[1] = y - radius;
    this.extent[2] = x + radius;
    this.extent[3] = y + radius;

    this.cellsize = adjustExtent(this.extent, this.width, this.height);

    if(this.minscale != -1 && this.getScale() < this.minscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.minscale);    
    }
    if(this.maxscale != -1 && this.getScale() > this.maxscale) {
      x = (this.extent[2] + this.extent[0])/2;
      y = (this.extent[3] + this.extent[1])/2;
      this.setExtentFromScale(x, y, this.maxscale);
    }
  }


  /**

  * draw a new map centered on the given point, with an {@link #extent}
  * that minimally fits the circle defined by the point and supplied radius.

  * @param {Double} x map coordinate x value
  * @param {Double} y map coordinate y value

  * @param {Double} radius desired distance from given point to edge of map

  */
  this.zoomRadius = function(x, y, radius) {
    this.setExtentFromRadius(x, y, radius);
    this.draw();
  }

  /**
  * get the nominal scale based on current settings.
  * @returns {Double} scale the nominal scale
  */
  this.getScale = function() {
    var gd, md;
  
    md = (this.width-1)/(this.pixelsPerInch*this.inchesPerMapUnit);
    gd = this.extent[2] - this.extent[0];

    return(gd/md);
  }

  /**

  * set {@link #extent} centered on the given point using the given
  * scale.

  * @param {Double} x map coordinate x value
  * @param {Double} y map coordinate y value

  * @param {Integer} scale desired scale, given as denominator of the
  * representative fraction (1:<b>x</b>)

  */
  this.setExtentFromScale = function(x, y, scale) {
    // remove leading 1: and any commas
    // var scale = 1.0*scale.replace(/,|1:/g,"");

    if((this.minscale != -1) && (scale < this.minscale))
      scale = this.minscale;

    if((this.maxscale != -1) && (scale > this.maxscale))
      scale = this.maxscale;

    this.cellsize = (scale/this.pixelsPerInch)/this.inchesPerMapUnit;

    this.extent[0] = x - this.cellsize*this.width/2.0;
    this.extent[1] = y - this.cellsize*this.height/2.0;
    this.extent[2] = x + this.cellsize*this.width/2.0;
    this.extent[3] = y + this.cellsize*this.height/2.0;

    this.cellsize = adjustExtent(this.extent, this.width, this.height);
  }

  /**
   * recenter the map at the given point, using the current scale.
   * @param {Double} x map coordinate x value
   * @param {Double} y map coordinate y value
   */
  this.recenter = function(x, y) { 
    this.setExtentFromScale(x, y, this.getScale());  
    this.draw();
  }

  /**
   * recenter the map at the given point, using the given scale.
   * @param {Double} x map coordinate x value
   * @param {Double} y map coordinate y value

   * @param {Integer} scale desired scale, given as denominator of the
   * representative fraction (1:<b>x</b>)

   */
  this.zoomScale = function(x, y, scale) {  
    this.setExtentFromScale(x, y, scale);  
    this.draw();
  }

  /**

  * zoom in centered on the given point, using the current {@link
  * #zoomsize} setting.

  * @param {Double} x map coordinate x value
  * @param {Double} y map coordinate y value
  */
  this.zoomIn = function(x,y) {
    this.zoomdir = 1;
    this.applyZoom(x,y);  
    this.draw();
    this.zoomdir = 0;
  }

  /**

  * zoom out centered on the given point, using the inverse of the
  * current {@link #zoomsize} setting.

  * @param {Double} x map coordinate x value
  * @param {Double} y map coordinate y value
  */
  this.zoomOut = function(x,y) {
    this.zoomdir = -1;
    this.applyZoom(x,y);
    this.draw();
    this.zoomdir = 0;
  }

  /**

   * pan map in the given direction, using the current {@link
   * #pansize} setting.

   * @param {Enumerated} direction direction to pan. Specified as a
   * lowercase abbreviation of one of the eight cardinal/primary
   * intercardinal directions:
   * <p>
   * <ul>
   * <li>n</li>
   * <li>ne</li>
   * <li>e</li>
   * <li>se</li>
   * <li>s</li>
   * <li>sw</li>
   * <li>w</li>
   * <li>nw</li>
   * </ul>
   * </p>

   */
  this.pan = function(direction) {
    this.zoomdir = 0;

    if(direction == 'n') {
      x = (this.width-1)/2.0;
      y = 0 - this.height*this.pansize + this.height/2.0;
    } else if(direction == 'nw') {
      x = 0 - this.width*this.pansize + this.width/2.0;
      y = 0 - this.height*this.pansize + this.height/2.0;
    } else if(direction == 'ne') {
      x = (this.width-1) + this.width*this.pansize - this.width/2.0;
      y = 0 - this.height*this.pansize + this.height/2.0;
    } else if(direction == 's') {
      x = (this.width-1)/2.0;
      y = (this.height-1) + this.height*this.pansize - this.height/2.0;
    } else if(direction == 'sw') {
      x = 0 - this.width*this.pansize + this.width/2.0;
      y = (this.height-1) + this.height*this.pansize - this.height/2.0;
    } else if(direction == 'se') {
      x = (this.width-1) + this.width*this.pansize - this.width/2.0;
      y = (this.height-1) + this.height*this.pansize - this.height/2.0;
    } else if(direction == 'e') {
      x = (this.width-1) + this.width*this.pansize - this.width/2.0;
      y = (this.height-1)/2.0;
    } else if(direction == 'w') {
      x = 0 - this.width*this.pansize + this.width/2.0;
      y = (this.height-1)/2.0;
    }

    this.applyZoom(x,y);
    this.draw();
  }
}
