// PotoSIG -- SIG extensions for Rails. Written by Javier Goizueta
// IvyGIS --- Mapserver and PostGIS on Rails.  Written by Robert S. Thau.
// Copyright 2006, Japan Spatial Information Technology, Inc.
//
// Distributed without warranty under the terms of the GNU General Public
// License, version 2.  

var PotoMap = Class.create();

// "Class method" to find the PotoMap object containing an element.
// Note that we avoid storing pointers to the PotoMap directly on
// DOM nodes to avoid IE space leak issues; instead there's a map
// from element ID's (unique within the document) to map objects.

PotoMap.forElementId = new Object();

PotoMap.containingElement = function (elt) {
  elt = $(elt);
  while (elt != null) {
    if (elt.id != null && this.forElementId[elt.id] != null)
      return this.forElementId[elt.id];
    else
      elt = elt.parentNode;
  }
  return null;
};

// "Class method" to create a positioned overlay, keyed to
// the given element.  Delegates to the instance method of the
// same name, which see for details.

PotoMap.create_pos_div = function (parent, contents_url, position,
                                  offsets, options)
{
  PotoMap.containingElement(parent).
    create_pos_div(parent, contents_url, position, offsets, options);
};

// "Class method" to move a map so that a given element is entirely
// visible.  The element must be a positioned element on the map,
// or a child of one.  Again, delegates to an instance method.

PotoMap.move_to_show = function (elt, margin)
{
  PotoMap.containingElement(elt).move_to_show(elt, margin);
};

Object.extend (PotoMap.prototype, Notifier.prototype);
Object.extend (PotoMap.prototype, 
{
  // Note that some "options" aren't optional

  initialize: function(element, maps, options) 
  {
    this.viewport = element;
    this.maps = maps;
    this.current_map_tag = null;
    this.current_zoom_level = 0;
    this.remembered_positions = {};
    this.last_posn = null;
    this.overlays = {};
    this.overlay_count = 0;
    this.initialize_as_notifier();
    this.pan_overlap = 0.2;

    this.chooser_elt = options.chooser;
    this.overlay_ui_div = options.overlay_ui_div;

    if (this.chooser_elt != null)
      $(this.chooser_elt).onchange = function () { map.init_selected_map() };
    else
      this.map_tag = options.map_tag;
      
    this.common_pos = options.common_pos; // mantain position across map change

    this.scale_selector = options.scale_selector;

    var map = this;

    map['fixed'] = (options['left_margin']==null);
    ['left_margin', 'right_margin', 'top_margin', 'bottom_margin'].each
      (function(key) { map[key] = options[key] });

    // Create the "drop zone"

    var drop_zone = this.create_div($(element));
    drop_zone.className = "poto_map_dropzone";

    this.drop_zone = drop_zone.id;
    
    Droppables.add (this.drop_zone, 
                    {onDrop: function (elt) { 
                        map.broadcast ("drop", elt) }});

    // Create the base div ...

    var contents_pane = this.create_div($(element));
    contents_pane.className = "poto_map_overlay";
    contents_pane.style.left = 0;
    contents_pane.style.top = 0;

    this.contents_pane = contents_pane.id;

    new Draggable 
    (this.contents_pane,
     { zindex: false,
         change: function() { map.note_pan (); },
         starteffect: null,
         endeffect: function() { map.redisplay (); }});

    // Make ourselves findable from the DOM...

    this.dom_associate(this.viewport);

    // Hook on events we care about... mainly page-layout handling.

    if (!map.fixed)
      {
        Event.observe (window, "resize", function() {
                         map.resize_to_margins ();
                         map.redisplay ()});
      }
    // Create manager for "positioned objects"

    this.pos_manager = new PotoPosObjectManager(this);

    // Defer final setup...

    Event.observe (window, "load", function() {
                     map.create_overlays();
                     map.resize_to_margins();
                     map.setup_overlay_ui();
                     map.init_selected_map(options.initial);
                   });
  },

  // Making one of these objects (or even a handler which closes
  // over one of them) a DOM node property risks storage leaks in
  // IE.  Hence the "class method" above, and this...

  dom_associate: function (elt_id) {
    PotoMap.forElementId[elt_id] = this;
  },

  // The map can have listeners which get notified when the map's
  // position or extents change (or when we load a new map).
  //
  // There are actually two types of events to note map motion.
  // When the user is dragging the map, we may want to, say, reload
  // tiles as the map is moved, but postpone other sorts of server
  // interaction until the drag is finished.  So, we broadcast "pan"
  // events while the map is moving, and "extents_changed" when the
  // user lets go.  (We use Effect_doMapRedisplays to arrange this 
  // sort of thing in conjunction with programmed motion, q.v.).
  //
  // (To further avoid wasting time, we only broadcast pan events
  // when the map has moved at least 50px).

  note_pan: function ()
  {
    var contents_posn = Position.page ($(this.contents_pane));
    if (this.last_posn == null
        || Math.abs (this.last_posn[0] - contents_posn[0]) > 50
        || Math.abs (this.last_posn[1] - contents_posn[1]) > 50)
      {
        this.last_posn = contents_posn;
        this.broadcast ("pan");
      }
  },

  redisplay: function () 
  {
    this.last_posn = null;
    this.broadcast ("extents_changed");
  },

  scale_label: function ()
  {
    var label = "";
    if (this.x_scale && this.current_map) {
      label = "1:"+Math.round(this.x_scale*this.current_map.scales_factor);
    }
	var rgx = /(\d+)(\d{3})/;
	while (rgx.test(label)) {
		label = label.replace(rgx, '$1' + '.' + '$2');
	}    
    return label;
  },

  // Panes for subcomponents to draw on.  (Everything is handled as
  // a map subcomponent, including the tiles).

  create_overlay: function () 
  {
    var elt = this.create_div($(this.contents_pane));
    $(elt).className = "poto_map_overlay";
    return elt.id;
  },

  create_div: function (parent) 
  {
    var elt = document.createElement("div");
    parent.appendChild (elt);
    elt.id = this.viewport + "_subdiv_" + this.overlay_count++;
    return elt;
  },

  // Initialization.

  create_overlays: function() {
    for (var i = 0; i < this.maps.length; ++i) {
      var map_spec = this.maps[i];
      var map_tag = map_spec.tag;
      this.overlays[map_tag] = this.overlays[map_tag] || [];
      new PotoMapTiler (this, map_tag, map_spec.base_format);
      var overlays = map_spec.overlays;
      for (var j = 0; j < overlays.length; ++j) {
        if (overlays[j].type == null)
          this.overlays[map_tag].
            push(new PotoMapTiler (this, map_spec.tag,
                                  map_spec.overlay_format,
                                  overlays[j].tag,
                                  overlays[j].name,
                                  overlays[j].default_on));
        else {
          alert ("Map " + map_tag + " has overlay " +
                 overlays[j].tag +
                 " of unknown type " + overlays[j].type);
        }
      }
    }
  },

  add_overlay: function (map_tag, overlay_object) {
    this.overlays[map_tag] = this.overlays[map_tag] || [];
    this.overlays[map_tag].push(overlay_object);
  },

  init_selected_map: function (initial_override) 
  {
    // First, remember position on the map we're deselecting...

    if (this.current_map_tag != null) {
      if (this.common_pos) {
        var current_common_view = this.viewport_world_coordinates();
      }
      else {      
          var remembered_position = this.viewport_center_world_coordinates();
          remembered_position.level = this.current_level;
          this.remembered_positions[this.current_map_tag] = remembered_position;
      }      
    }

    // Find new map

    var map_tag = this.chooser_elt ? $(this.chooser_elt).value : this.map_tag;
    var map;

    for (var i = 0; i < this.maps.length; ++i)
      if (this.maps[i].tag == map_tag) {
        map = this.maps[i];
        break;
      }

    if (map == null) {
      alert ("Huh?  Map " + map_tag + " not found!");
      return;
    }

    // Set up the geometry...

    var old_map_tag = this.current_map_tag;

    this.current_map_tag = map_tag;
    this.current_map = map;
    this.world_xmin = map.world_extent.min_x
    this.world_ymin = map.world_extent.min_y
    this.world_xmax = map.world_extent.max_x
    this.world_ymax = map.world_extent.max_y
    
    var initial = map.initial;
    var initial_rect = (initial.x==undefined); 
    if (initial_rect) {
      this.wx0 = 0.5*(initial.xmin + initial.xmax);
      this.wy0 = 0.5*(initial.ymin + initial.ymax);
    }
    else {
      this.wx0 = map.initial.x;
      this.wy0 = map.initial.y;
    }
    if (initial_override!=undefined) {
      initial = initial_override;
      initial_rect = (initial.x==undefined); 
    }

    
    this.setup_overlay_ui ();

    this.broadcast ('new_map', this.current_map_tag, this.old_map_tag);

    // .. and warp to remembered position if we have it, initial if not.

    if (this.common_pos && current_common_view) {
        this.wrect_zoom_corners(current_common_view.min_longitude,current_common_view.min_latitude,current_common_view.max_longitude,current_common_view.max_latitude,-0.1);
    }
    else {
        var remembered_position = this.remembered_positions[this.current_map_tag];
        if (remembered_position != null) 
          this.setup_zoom_level (remembered_position.level, remembered_position);
        else {
          if (initial_rect)
            this.wrect_zoom_corners(initial.xmin, initial.ymin, initial.xmax, initial.ymax, 0.0);
          else
            this.setup_zoom_level(map.initial.zoom_level,
                                {longitude:map.initial.x, latitude:map.initial.y});
        }
    }
  },

  setup_zoom_level: function (new_level, world_posn,smooth)
  {
  	new_level = parseInt(new_level);
    if (new_level < 0 || new_level >= this.current_map.scales.length)
      return false;

    var scale = this.current_map.scales[new_level]
    this.current_level = new_level;
  
    this.x_quantum = this.current_map.tile_width;
    this.y_quantum = this.current_map.tile_height;
    this.x_scale   = scale;
    this.y_scale   = scale;
    this.recenter_map (world_posn,smooth);

    this.broadcast ("zoom_changed");
    this.broadcast ("extents_changed");
    if (this.scale_selector) {
      $('potosig_scale_selector_'+new_level).selected = 'selected';
    }
    return true;
  },

  // Auto-generated "overlay panel"

  setup_overlay_ui: function() {
    var the_div = $(this.overlay_ui_div);
    if (the_div == null) return;

    // Clear out old contents

    while (the_div.firstChild != null)
      the_div.removeChild (the_div.firstChild);

    // Put in new contents

    var layers = this.overlays[this.current_map_tag] || [];

    if (layers.length == 0) {
      the_div.appendChild (
        document.createTextNode("Este mapa no tiene capas adicionales."));
      return;
    }

    for (var i = 0; i < layers.length; ++i) {

      var elt_div = document.createElement("div");
      the_div.appendChild (elt_div);

      elt_div.appendChild (this.tied_checkbox(layers[i]));
      elt_div.appendChild (document.createTextNode (layers[i].name+' '));
      if (layers[i].overlay_inset) // (layers[i].format)
        elt_div.appendChild (this.legend_link(layers[i].overlay_inset));
    }

      var elt_div = document.createElement("div");
      the_div.appendChild (elt_div);
      elt_div.appendChild (this.legend_link(0));

      if (this.scale_selector) {
	      var scales_div = document.createElement("div");
	      the_div.appendChild (scales_div);
		    scales_div.appendChild(document.createTextNode("Escala: "));
		    var scales_select = document.createElement("select"); 
		    scales_select.id = 'potosig_scale_selector';
	
	      scales_select.onchange = function () { map.setup_zoom_level(this.value,map.viewport_center_world_coordinates()) };
	
	      for (i=0; i<this.current_map.scales.length; i++) {
          var scales_option = document.createElement("option"); 
          var label = "1:"+Math.round(this.current_map.scales[i]*this.current_map.scales_factor);
	   	    var rgx = /(\d+)(\d{3})/;
	  	    while (rgx.test(label)) {
            label = label.replace(rgx, '$1' + '.' + '$2');
          }
	 	      scales_option.appendChild(document.createTextNode(label));
			    scales_option.value = i;
			    scales_option.id = 'potosig_scale_selector_'+i;
		      scales_select.appendChild(scales_option);	  	
		    }	  	  	  	  	  
	      scales_div.appendChild (scales_select);
	    }
    
  },

  tied_checkbox: function(layer) {
    var input_elt = document.createElement("input");
    input_elt.type = "checkbox";
    input_elt.checked = layer.active;
    input_elt.defaultChecked = layer.active;
    input_elt.onclick = function() {
      layer.set_active(this.checked);
    };
    return input_elt;
  },

  legend_link: function(layer_tag) {
    var map_tag = this.current_map_tag;
    var input_elt = document.createElement("a");
    input_elt.href = "/legend/"+map_tag+"/l"+layer_tag+".png";
    input_elt.target = "leyenda"
    input_elt.appendChild (document.createTextNode('leyenda'));
    return input_elt;
  },


  // Hook for map displays to provide their own...

  overlay_check: function(map_tag, ovl_tag, checkbox) {
    this.find_overlay(map_tag, ovl_tag).set_active(checkbox.checked);
  },

  find_overlay: function (map_tag, ovl_tag) {
    var layers = this.overlays[map_tag];
    if (layers != null) {
      for (var i = 0; i < layers.length; ++i)
        if (layers[i].overlay_tag == ovl_tag)
          return layers[i];
    }

    // A "can't happen" thing... indicates a bug in the caller.

    alert ("Map " + map_tag + " has no overlay tagged " + ovl_tag);
  },

  // Other UI actions...

  zoom_in: function () {
    if (!this.setup_zoom_level (this.current_level - 1, 
                                this.viewport_center_world_coordinates()))
      alert ("can't zoom in");
  },

  zoom_out: function () {
    if (!this.setup_zoom_level (this.current_level + 1, 
                                this.viewport_center_world_coordinates()))
      alert ("can't zoom out");
  },

  can_zoom_in: function() { return (this.current_level > 0); },
  can_zoom_out: function() { 
    return (this.current_level < this.current_map.scales.length - 1); 
  },

  pan_E: function (smooth) {
    var overlap = this.pan_overlap;
    var ext = this.viewport_world_coordinates();
    var x0 = ext['max_longitude']*(0.5+1-overlap)+ext['min_longitude']*(0.5-1+overlap);
    var y0 = (ext['max_latitude']+ext['min_latitude'])*0.5;
    this.setup_zoom_level(this.current_level, {longitude: x0, latitude: y0},smooth);
  },
  pan_W: function (smooth) {
    var overlap = this.pan_overlap;
    var ext = this.viewport_world_coordinates();
    var x0 = ext['max_longitude']*(0.5-1+overlap)+ext['min_longitude']*(0.5+1-overlap);
    var y0 = (ext['max_latitude']+ext['min_latitude'])*0.5;
    this.setup_zoom_level(this.current_level, {longitude: x0, latitude: y0},smooth);
  },
  pan_N: function (smooth) {
    var overlap = this.pan_overlap;
    var ext = this.viewport_world_coordinates();
    var y0 = ext['max_latitude']*(0.5+1-overlap)+ext['min_latitude']*(0.5-1+overlap);
    var x0 = (ext['max_longitude']+ext['min_longitude'])*0.5;
    this.setup_zoom_level(this.current_level, {longitude: x0, latitude: y0},smooth);
  },
  pan_S: function (smooth) {
    var overlap = this.pan_overlap;
    var ext = this.viewport_world_coordinates();
    var y0 = ext['max_latitude']*(0.5-1+overlap)+ext['min_latitude']*(0.5+1-overlap);
    var x0 = (ext['max_longitude']+ext['min_longitude'])*0.5;
    this.setup_zoom_level(this.current_level, {longitude: x0, latitude: y0},smooth);
  },


  resize_to_margins: function () {
    var window_width, window_height;

    if (this.fixed)
      {
        var viewport = $(this.viewport);
        // this.viewport_width = viewport.style.width; // convert to number
        // this.viewport_height= viewport.style.height; // convert to number     
         this.viewport_width= $(this.viewport).offsetWidth;
         this.viewport_height= $(this.viewport).offsetHeight;
        return;
      }

    if (window.innerWidth) {
      // Mozilla variants (i.e., Gecko-based browsers, e.g. Firefox)
      window_width  = window.innerWidth;
      window_height = window.innerHeight;
    }
    else if (document.body.parentNode.clientWidth != 0) {
      // IE standards mode
      window_width  = document.body.parentNode.clientWidth;
      window_height = document.body.parentNode.clientHeight;
      ie_lossage = true;
    }
    else {
      // IE quirks mode
      window_width  = document.body.clientWidth;
      window_height = document.body.clientHeight;
    }
    
    var viewport = $(this.viewport);
    viewport.style.top = this.top_margin + "px";
    viewport.style.left = this.left_margin + "px";
    viewport.style.width = 
      window_width - this.left_margin - this.right_margin + "px";
    viewport.style.height = 
      window_height - this.top_margin - this.bottom_margin + "px";

    this.viewport_width = window_width - this.left_margin - this.right_margin;
    this.viewport_height= window_height - this.top_margin - this.bottom_margin;
  },

  // Programmatically moving the map...

  // Move the map so that the given 'elt' is entirely on screen,
  // ideally with 'margin' pixels between it and the edge on all sides.

  move_to_show: function (elt, margin)
  {
    var contents_pane = $(this.contents_pane);
    var viewport = $(this.viewport);
    elt = $(elt);
    margin = margin || 10; // want elt this far from viewport edge if possible

    var vp_posn = Position.page(viewport);
    var vp_top  = vp_posn[1];
    var vp_left = vp_posn[0];
    var vp_right = vp_left + viewport.offsetWidth;
    var vp_bottom = vp_top + viewport.offsetHeight;
  
    var elt_posn = Position.page(elt);
    var elt_top = elt_posn[1];
    var elt_left = elt_posn[0];
    var elt_right = elt_left + elt.offsetWidth;
    var elt_bottom = elt_top + elt.offsetHeight;

    var dx = 0, dy = 0;

    if (elt_top < vp_top + margin)
      dy = (vp_top + margin) - elt_top;
    else if (elt_bottom > vp_bottom - margin)
      dy = (vp_bottom - margin) - elt_bottom;

    if (elt_left < vp_left + margin)
      dx = (vp_left + margin) - elt_left;
    else if (elt_right > vp_right - margin)
      dx = (vp_right - margin) - elt_right;

    if (dx != 0 || dy != 0)
      new Effect.Parallel
        ([new Effect.MoveBy (contents_pane, dy, dx, { duration: 5 }),
          new Effect_doMapRedisplays(this)]);
  },

  recenter_map: function (world_posn,smooth) 
  {
    var screen_posn = this.world_centered_on_screen(world_posn);
    var contents = $(this.contents_pane);

    if (smooth) {
       var dx = -(screen_posn.x) - parseInt(contents.style.left);    
       var dy = -(screen_posn.y) - parseInt(contents.style.top);    
      new Effect.Parallel
        ([new Effect.MoveBy (contents, dy, dx, { duration: 3 }),
          new Effect_doMapRedisplays(this)]);  
    }
    else {
    contents.style.left = -(screen_posn.x) + "px";
    contents.style.top  = -(screen_posn.y) + "px";
    }
  },

  // Dealing with positions...

  viewport_screen_coordinates: function () {
    var elt     = $(this.contents_pane);
    var width   = $(this.viewport).offsetWidth;
    var height  = $(this.viewport).offsetHeight;
    var min_x   = -parseInt (elt.style.left);
    var min_y   = -parseInt (elt.style.top);
    return {min_x: min_x, max_x: min_x + width,
        min_y: min_y, max_y: min_y + height }
  },

  viewport_dimensions: function () {

    // Don't trust viewport offsetWidth and offsetHeight to be
    // reported here correctly... Mozilla XHTML glitch?

    return { width: this.viewport_width,
             height: this.viewport_height };
  },

  viewport_center_world_coordinates: function() {
    var width    = $(this.viewport).offsetWidth;
    var height   = $(this.viewport).offsetHeight;

    var elt = $(this.contents_pane);
    var posn = 
     this.screen_to_world ({x: (-parseInt (elt.style.left) + width / 2),
                            y: (-parseInt (elt.style.top)  + height / 2)});

    return posn;
  },
  
  viewport_world_coordinates: function() {
    var elt     = $(this.contents_pane);
    var width   = $(this.viewport).offsetWidth;
    var height  = $(this.viewport).offsetHeight;
    var min_pos = 
      this.screen_to_world ({x: -parseInt (elt.style.left),
                             y: -parseInt (elt.style.top)});
    var max_pos = 
      this.screen_to_world ({x: -parseInt (elt.style.left) + width,
                             y: -parseInt (elt.style.top) + height});

    // Note here that latitudes and screen y coordinates increase
    // in opposite directions...

    return { min_longitude: min_pos.longitude, min_latitude: max_pos.latitude,
             max_longitude: max_pos.longitude, max_latitude: min_pos.latitude}
  },

  screen_to_world: function (arg) {
    return { longitude: arg.x * this.x_scale+this.wx0, 
             latitude:  -arg.y * this.y_scale+this.wy0 };
  },

  world_to_screen: function (arg) {
    return { x: Math.floor ((arg.longitude-this.wx0) / this.x_scale),
             y: -Math.floor ((arg.latitude-this.wy0)  / this.y_scale) };
  },

  world_centered_on_screen: function (arg) {
    var centered_point = this.world_to_screen (arg);
    centered_point.x -= $(this.viewport).offsetWidth/2;
    centered_point.y -= $(this.viewport).offsetHeight/2;
    return centered_point;
  },

  vp_form_args: function (include_overlays) {

    // Collect coordinate info...

    var vp_world  = this.viewport_world_coordinates();
    var vp_center = this.viewport_center_world_coordinates();

    var vp_dx = vp_world.max_longitude - vp_world.min_longitude;
    var vp_dy = vp_world.max_latitude  - vp_world.min_latitude;

    var distance = Math.max(vp_dx, vp_dy);
    
    var active_overlays = '';
    
    if (include_overlays) {
      var layers = this.overlays[this.current_map_tag];
      for (var i = 0; i < layers.length; ++i) {
        if (layers[i].active && typeof(layers[i].overlay_tag)!='undefined') {
          if (active_overlays!='') active_overlays += ",";
          active_overlays += layers[i].overlay_tag;
        }
      }
      if (active_overlays!='')
        active_overlays = "&overlays=" + active_overlays;
    }    

    return "scale=" + this.x_scale +
      "&lat=" + vp_center.latitude + "&lon=" + vp_center.longitude +
      "&x_origin=" + vp_world.min_longitude +
      "&y_origin=" + vp_world.max_latitude +
      "&x_opposite=" + vp_world.max_longitude +
      "&y_opposite=" + vp_world.min_latitude +
      "&distance=" + distance + 
      "&map_tag=" + this.current_map_tag +
      active_overlays;
  },

  evt_screen_posn: function(evt) {
    var contents_posn = Position.page($(this.contents_pane));
    return {x: Event.pointerX(evt) - contents_posn[0],
            y: Event.pointerY(evt) - contents_posn[1]};
  },

  // Positioned objects.  Delegated to our PotoPosObjectManager...

  // Insert a DOM object at the given (world *or* screen) position,
  // offset by the given absolute pixel offsets (e.g., {x:10, y:0}).
  // keyed to the element given as "parent", which must be a child
  // element of a <div> created by create_overlay above.  Tooltips
  // over that element will be automatically disabled until someone
  // calls delete_pos_object on the result (or reenables them
  // directly by invoking PotoTooltips.enable_over);
  //
  // We also arrange to reposition the object when the zoom level
  // changes, and to hide and restore it appropriately when the
  // user switches maps, and switches back.
  //
  // If the position is an object with 'latitude' and 'longitude' 
  // attributes, these will be taken to be the corresponding world
  // position.  If it has 'x' and 'y' attributes, these will be
  // taken to be a screen position --- which will be converted
  // to a world position, so the object is still properly placed
  // when the map is rescaled.  Otherwise, we try to treat it as
  // a mouse event, and extract a screen position from it.

  register_pos_object: function(elt, parent, position, offsets) {
    return this.pos_manager.register_pos_object(elt,parent,position,offsets);
  },

  // Creates and returns a fresh <div>, which is installed as a positioned
  // object (as above) at the given position and offsets, on the given parent
  // overlay.  An inner <div> will be initialized as containing the given
  // contents (which must be HTML).
  //
  // Last argument is a hash of miscellaneous options.  Two are now
  // supported, both having to do with the panel of widgets, including
  // the 'x' close box, displayed on the top edge of the <div>.  They are:
  //
  //    {close_handler: function(elt) {...},
  //     widget_specs: [spec, spec, ... ]}
  //
  // The close_handler, if specified, will be invoked when the user hits
  // the 'x', before the created <div> is actually removed.  If it returns
  // false, the <div> will *not* be removed; in this case, you should
  // probably display an alert() to tell the user what's going on.
  //
  // The widget_specs allow you to add additional widgets, like the bookmark
  // widget for poto_notes.  They are an array of objects of the form:
  //
  //  {css_class: 'foo_widget', char: '^', title: 'Blatherize',
  //   handler: function() { ... }}
  //
  // This creates a widget in the <div>'s widget bar, with the given
  // title, visible character, CSS class, and handler.  
  
  create_pos_div: function(parent, contents_url, position, 
                           offsets, options) {
    return this.pos_manager.create_pos_div(parent, contents_url, position, 
                                           offsets, options);
  },

  // Stop managing the argument element as a positioned object,
  // and remove it from the screen.

  delete_pos_object: function(elt) {
    this.pos_manager.delete_pos_object(elt);
  },
  
  
  // Convert screen rectangle to world coordinates
  
    rect_to_world: function(left,top,width,height) {
    var min_pos = 
      this.screen_to_world ({x: -left, y: -top});
    var max_pos = 
      this.screen_to_world ({x: -left + width, y: top + height});

    // Note here that latitudes and screen y coordinates increase
    // in opposite directions...

    return { min_longitude: min_pos.longitude, min_latitude: max_pos.latitude,
             max_longitude: max_pos.longitude, max_latitude: min_pos.latitude}
  },
  
    // zoom by word rectangle: defined by center and dimensions
    wrect_zoom_center_dim: function(world_center, world_width, world_height,ext) {
      if (ext==null)
        ext = 0.0;
      world_width *= (1.0+ext);
      world_height *= (1.0+ext);
      var vpdim = this.viewport_dimensions();
      //alert("ww:"+world_width+" wh:"+world_height+" vx:"+vpdim.x+" vy:"+vpdim.y);
      var n = this.current_map.scales.length;
      var scale_level = n-1;
      for (var i=0; i<n-1; i++) {
        scale = this.current_map.scales[i];
        //alert("s:"+scale+"sx="+scale*vpdim.x+"sy="+scale*vpdim.y);
        if (scale*vpdim.width>=world_width && scale*vpdim.height>=world_height) {
          scale_level = i;
          break;
        }        
      }      
      return this.setup_zoom_level(scale_level, world_center);
    },
    // defined by corners
    wrect_zoom_corners: function(wx0, wy0, wx1, wy1,ext) {
      var vpdim = this.viewport_dimensions();
      var world_center = { longitude: 0.5*(wx0+wx1), latitude: 0.5*(wy0+wy1) };
      var world_width = Math.abs(wx1-wx0);
      var world_height = Math.abs(wy1-wy0);
      return this.wrect_zoom_center_dim(world_center, world_width, world_height,ext);
    }  
 });

PotoMapTiler = Class.create();
PotoMapTiler.prototype = {

  initialize: function (map, map_tag, format, overlay_tag, overlay_name, default_on) {
    this.map = map;
    this.map_tag = map_tag;
    this.overlay_tag = overlay_tag || "base";
    this.overlay_inset = (overlay_tag == null)? "0" : "_" + overlay_tag + "_";
    this.format = format;
    this.layer = map.create_overlay();
    this.name = overlay_name;
    this.map_active = false;
    this.active = (overlay_tag == null) || default_on || false;  // base only to begin with
    map.add_listener (this);
  },

  // Event handling...

  on_new_map: function (map_tag) {
    this.map_active = (map_tag == this.map_tag);
    if (!this.map_active)
      this.clear_tiles();
  },

  on_zoom_changed: function () {
    if (this.active && this.map_active)
      this.clear_tiles();
  },

  on_extents_changed: function () {
    if (this.active && this.map_active)
      this.clear_offscreen_tiles (this.load_tiles ());
  },

  on_pan: function () {
    if (this.active && this.map_active)
      this.load_tiles ();
  },

  // Activation...

  set_active: function(active) {
    this.active = active;
    if (active && this.map_active)
      this.load_tiles();
    else
      this.clear_tiles();
  },

  // Tile naming conventions...

  tile_posn_tag: function (x, y) {
    return "l" + this.map.current_level + "x" + x + "y" + y;
  },

  tile_tag: function (x, y) {
    return this.map.current_map_tag + this.overlay_inset + 
      "_" + this.tile_posn_tag (x, y)
  },

  tile_url: function (x, y) {
    return "/tile/" + this.map.current_map_tag + "/c" + this.overlay_inset +
      this.tile_posn_tag (x, y) + "." + this.format;
  },

  // Loading tiles...

  load_tiles: function () 
  {
    var cur_tiles = new Array();
    var pending_tiles = {};
    var v = this.viewport_tile_range();

    for (var tile_x = v.x_lo; tile_x < v.x_hi; tile_x += this.map.x_quantum)
      for (var tile_y = v.y_lo; tile_y < v.y_hi; tile_y += this.map.y_quantum){
        cur_tiles [this.tile_tag (tile_x, tile_y)] = 1;
        this.enqueue_tile (pending_tiles, tile_x, tile_y);
      }

    this.load_queued_tiles(pending_tiles);
    return cur_tiles;
  },

  viewport_tile_range: function ()
  {
    var map_posn = this.map.viewport_screen_coordinates();

    var result = {};

    result.x_lo = this.map.x_quantum*
      (Math.floor (map_posn.min_x / this.map.x_quantum));
    result.y_lo = this.map.y_quantum*
      (Math.floor (map_posn.min_y / this.map.y_quantum));
    
    var dims = this.map.viewport_dimensions();

    result.x_hi = result.x_lo + dims.width  + this.map.x_quantum;
    result.y_hi = result.y_lo + dims.height + this.map.y_quantum;

    return result;
  },

  // As a hint to the browser, we install the tiles near the center
  // of the viewport first...

  enqueue_tile: function (pending_tiles, x, y) 
  {
    var tag = this.tile_tag (x, y);

    if (document.getElementById (tag)) return;
    if (pending_tiles[tag]) return;

    pending_tiles[tag] = { x: x, y: y };
  },

  load_queued_tiles: function (pending_tiles) 
  {
    while (1) {

      // We want to load the pending tile whose center is closest
      // to the center of the viewport.  However, we have positions
      // of the tiles' top left corners.  So, we shift the viewport
      // reference by half the size of a tile to compensate.

      var v = this.viewport_tile_range();
      var x_ctr = (v.x_lo + v.x_hi - this.map.x_quantum) / 2.0;
      var y_ctr = (v.y_lo + v.y_hi - this.map.y_quantum) / 2.0;

      var best_tile_tag = null;
      var best_dist = 1000000000;

      for (var tile_tag in pending_tiles) 
        {
          var posn = pending_tiles[tile_tag];

          if (posn != null && posn.x != null && posn.y != null)
            {
              var distx = posn.x - x_ctr;
              var disty = posn.y - y_ctr;
              var dist = distx * distx + disty * disty;

              if (dist < best_dist) {
                best_tile_tag = tile_tag;
                best_dist = dist;
              }
            }
        }

      if (best_tile_tag == null) {
        return;
      }

      // Have found a tile.  Delete it from the pending list, and
      // start it loading...

      var posn = pending_tiles [best_tile_tag];

      pending_tiles [best_tile_tag] = null;

      var loading_tile = document.createElement ("img");

      loading_tile.id = best_tile_tag;
      loading_tile.className = "bgtile hidden";
      loading_tile.onload = function () { this.className = "bgtile" };
      loading_tile.src = this.tile_url (posn.x, posn.y);

      loading_tile.style.left = posn.x + "px";
      loading_tile.style.top  = posn.y + "px";

      $(this.layer).appendChild (loading_tile);
    }
  },

  clear_offscreen_tiles: function (cur_tiles)
  {
    var tile_layer = $(this.layer);
    tile_layer.select('.bgtile').each
    (function (tile) {
      if (!cur_tiles[tile.id]) {
        tile_layer.removeChild (tile);
        tile.src="/plugin_assets/potosig_engine/images/blank.gif";
      }});
  },

  clear_tiles: function ()
  {
    // When changing zoom levels, we want to wipe displayed tiles completely.
    // So, no tile is current.

    this.clear_offscreen_tiles (new Array());
  }
};

PotoPosObjectManager = Class.create();
PotoPosObjectManager.prototype = {

  initialize: function (map) {
    this.map = map;
    this.managed_objects = {};
    this.managed_obj_count = 0;
    this.map.add_listener(this);
    Event.observe(window, 'unload', function() { this.managed_objects = {} });
  },

  on_map_changed: function (new_map_tag, old_map_tag) 
  {
    var old_elts = this.managed_objects[old_map_tag];
    for (var i = 0; old_elts != null && i < old_elts.length; ++i) {
      var elt = $(old_elts[i].elt);
      if (elt.parentNode) elt.parentNode.removeChild(elt);
    }

    var new_elts = this.managed_objects[new_map_tag];
    for (var i = 0; new_elts != null && i < new_elts.length; ++i) {
      this.reposition(new_elts[i]);
      var elt = $(new_elts[i].elt);
      var parent = $(new_elts[i].layer);
      parent.appendChild (elt);
    }
  },

  on_zoom_changed: function() {
    var elts = this.managed_objects[this.map.current_map_tag];
    for (var i = 0; elts != null && i < elts.length; ++i)
      this.reposition(elts[i]);
  },

  reposition: function(record) {
    var elt = record.elt;
    var posn = this.map.world_to_screen(record.position);
    elt.style.left = posn.x + record.offsets.x + "px";
    elt.style.top  = posn.y + record.offsets.y + "px";
  },

  find_parent: function(elt, className) {
    var saved_elt = elt;

    for (elt = $(elt); elt; elt = elt.parentNode) {
      if (elt.className == className)
        return elt;
      else if (elt.className != null && elt.className.split != null) {
        var names = elt.className.split(' ');
        for (var i = 0; i < names.length; ++i)
          if (className == names[i])
            return elt;
      }
    }

    alert ("Couldn't find parent of class " + className + " for " + saved_elt);
  },

  // Basic function to register a positioned object --- caller
  // is assumed to have set it up to be absolutely positioned.

  register_pos_object: function(elt, handle, position, offsets) 
  {
    // For messy reasons involving IE DOM bugs, we may find
    // ourselves trying to register something that's already
    // displayed.  If so, do nothing.

    var records = this.managed_objects[this.map.current_map_tag];
    for (var i = 0; records != null && i < records.length; ++i)
      if (records[i].elt == elt) {
        return;
      }

    // Find overlay to place the thing on.

    var parent = this.find_parent (handle, "poto_map_overlay");

    if (parent == null)
      alert ("Couldn't find overlay to place positioned element...");

    position = this.convert_pos_object_pos(position);

    offsets = offsets || {x: 0, y: 0};

    var map_tag = this.map.current_map_tag;
    var record = {elt: elt, layer: $(parent).id, handle: handle,
                  position: position, offsets: offsets,
                  map_tag: map_tag};

    // Mouse down on these should *not* start a drag...

    elt.onmousedown = function(evt){(evt || window.event).cancelBubble=true;};

    if (!this.managed_objects[map_tag])
      this.managed_objects[map_tag] = [];

    this.managed_objects[map_tag].push(record);
    this.reposition(record);

    if (elt.parentNode != parent)
      parent.appendChild(elt);
  },

  delete_pos_object: function(elt) 
  {
    elt = $(elt);

    var records = this.managed_objects[this.map.current_map_tag];
    for (var i = 0; records != null && i < records.length; ++i)
      if (records[i].elt == elt) {
        PotoTooltips.enable_over(records[i].handle);
        elt.parentNode.removeChild(elt);
        records.splice(i, 1);
        return;
      }

    alert ("Couldn't find record for positioned object " + elt);
  },

  create_pos_div: function(parent, contents_url, position, offsets, options)
  {
    if (!options) options = {};

    PotoTooltips.disable_over(parent);

    var elt = document.createElement("div");
    elt.className = "msgdisplay";

    var widget_panel = document.createElement("div");
    widget_panel.className = "msgdisplay_widgets";
    elt.appendChild(widget_panel);

    var manager = this;
    var user_close_handler = options.close_handler;

    var real_close_handler = function () { 
      elt = manager.find_parent(this,"msgdisplay");
      if (user_close_handler && !user_close_handler(elt))
        return;
      manager.delete_pos_object (elt); 
      return false;
    };

    this.create_widget (widget_panel,
                        {css_class:'close_widget', char:'x', title:'close',
                         handler: real_close_handler});

    var widget_specs = options.widget_specs || [];

    for (var i = 0; i < widget_specs.length; ++i)
      this.create_widget (widget_panel, (widget_specs[i]));

    var inner_div = document.createElement("div");
    inner_div.className = "msgdisplay_inner";
    inner_div.id = this.map.viewport + "_mobj_" + this.managed_obj_count++;

    position = this.convert_pos_object_pos(position);

    if (contents_url.indexOf('?') >= 0)
      contents_url += '&located_div_id=' + inner_div.id;
    else
      contents_url += '?located_div_id=' + inner_div.id;

    contents_url += '&located_lon=' + position.longitude;
    contents_url += '&located_lat=' + position.latitude;

    var map = this.map;
    elt.appendChild(inner_div);
    new Ajax.Updater (inner_div, contents_url,
                      { onComplete: function() { map.move_to_show(elt); }});

    // Clicking links on these should *not* start a drag...

    this.register_pos_object(elt, parent, position, offsets);
    return elt;
  },
  
  convert_pos_object_pos: function(position) 
  {
    if (position.button != null) {
      // Assume it's a mouse event...
      position = this.map.evt_screen_posn(position);
      position = this.map.screen_to_world(position);
    }
    else if (position.x) {
      position = this.map.screen_to_world(position);
    }
    return position;
  },

  create_widget: function (widget_panel, spec) 
  {
    var widget = document.createElement("a");
    widget.className = "widget " + spec.css_class;
    widget.href = "#";
    widget.title = spec.title;

    var manager = this;
    
    widget.onclick = spec.handler;
    widget_panel.appendChild(widget);
    widget.appendChild(document.createTextNode(spec.char));
  }
};

// If you're moving the map around programmatically, you need
// to periodically do redisplays.  That can be arranged by putting
// it in an Effect.Parallel with this:

var Effect_doMapRedisplays = Class.create();
Object.extend (Effect_doMapRedisplays.prototype, Effect.Base.prototype);
Object.extend (Effect_doMapRedisplays.prototype,
               { initialize: function (map) { 
                   this.map = map;
                   this.start ({fps: 1}) ;
                 },
                 setup: function () { this.map.note_pan () },
                 update: function () { this.map.note_pan () },
                 finish: function () { this.map.redisplay () }});

