// copyright(c) 2008-2009 The Allen Institute for Brain Science.
var _zapviewers = new Object();

function zapviewer(container_id, image_service, tile_service, info_service, options) {

// public properties
    this.id = container_id;
    this.container = $(container_id).makePositioned();
    this.strip = null;
    this.zapimg = null;
    this.scalebar = null;

    this.zap_tier = options.zap_tier ? options.zap_tier : null;
    this.max_tier = options.max_tier ? options.max_tier : 8;
    this.precalculated_path = options.precalculated_path ? options.precalculated_path : false;
    this.src_key = options.src_key ? options.src_key : 'path';
    this.title = options.title ? options.title : "";
    this.initialized = false;

// startup image option values
    this.start_id = options.start_id ? options.start_id : undefined;
    this.start_x = options.start_x ? options.start_x : undefined;
    this.start_y = options.start_y ? options.start_y : undefined;
    this.start_tier = options.start_tier ? options.start_tier : undefined;

// scalebar options values
    this.sb_create = options.sb_create ? options.sb_create : undefined;
    this.sb_resolution = options.sb_resolution ? options.sb_resolution : undefined;
    this.sb_units = options.sb_units ? options.sb_units : undefined;
    this.sb_opacity = options.sb_opacity ? options.sb_opacity : undefined;
    this.sb_position = options.sb_position ? options.sb_position : undefined;
    this.sb_h_image = options.sb_h_image ? options.sb_h_image : undefined;
    this.sb_v_image = options.sb_v_image ? options.sb_v_image : undefined;

// side panel options values
    this.side_panel_width = options.side_panel_width ? options.side_panel_width : undefined;

// event hooks
    this.onLoad = options.load ? eval(options.load) : null;
    this.onViewerCreated = options.created ? eval(options.created) : null;
    this.onSelect = options.select ? eval(options.select) : null;
    this.onMove = options.move ? eval(options.move) : null;
    this.onZoom = options.zoom ? eval(options.zoom) : null;
    this.onImageChange = options.change ? eval(options.change) : null;

//  event hook arrays (from original event handling)
    this.onLoadArray = new Array();
    this.onViewerCreatedArray = new Array();
    this.onSelectArray = new Array();
    this.onMoveArray = new Array();
    this.onZoomArray = new Array();
    this.onImageChangeArray = new Array();
    this.onKeyBeforeArray = new Array();
    this.onKeyAfterArray = new Array();

//  from newer event handling (specified in zap viewer :event_options)
    if (options.onLoadFncs)          this.onLoadArray = options.onLoadFncs.split(",");
    if (options.onViewerCreatedFncs) this.onViewerCreatedArray = options.onViewerCreatedFncs.split(",");
    if (options.onSelectFncs)        this.onSelectArray = options.onSelectFncs.split(",");
    if (options.onMoveFncs)          this.onMoveArray = options.onMoveFncs.split(",");
    if (options.onZoomFncs)          this.onZoomArray = options.onZoomFncs.split(",");
    if (options.onImageChangeFncs)   this.onImageChangeArray = options.onImageChangeFncs.split(",");
    if (options.onKeyBeforeFncs)     this.onKeyBeforeArray = options.onKeyBeforeFncs.split(",");
    if (options.onKeyAfterFncs)      this.onKeyAfterArray = options.onKeyAfterFncs.split(",");


// public methods

    // Users can call this to inject key events into the system
    this.doKey = function(evt) {
        evt.fromZapViewer = true;
        doKey(evt);
    };

    this.setResolution = function(res) {
        this.sb_resolution = res;
        if (self.scalebar !== null) {
            this.scalebar.setResolution(res);
        }
    };
    this.getResolution = function() {
        return this.sb_resolution;
    };

    //select an image in the strip and show it in the viewer
    this.select = function(id) {
        this.strip.select(id);
    };

    // scroll to an image in the strip.  Note that the image is not selected,
    // just scrolled into visibility
    this.moveToImage = function(id) {
        this.strip.moveToImage(id);
    };

    // returns the arrray of images shown in the strip
    this.getImages = function() {
        return(this.strip.getImages());
    };

    // sets which field in the image record is used to provide the path to the image.
    // center - optional arg, set true to center image
    this.setSrcKey = function(newSrcKey, includeStrip, center) {
        var centerImg = (center === undefined) ? false : center;
        this.zapimg.setSrcKey(newSrcKey, center);
        if(includeStrip)
            this.strip.setSrcKey(newSrcKey);
    };

    // sets the filter that's applied to the zap image.  The image is refreshed
    // with the filter applied.
    this.setFilter = function(filterName, filterVals, includeStrip) {
        this.zapimg.setFilter(filterName, filterVals);
	    if (includeStrip) {
	        this.strip.setFilter(filterName, filterVals);
        }
    };

    this.getFilter = function() {
	return(this.zapimg.getFilter());
    };

    // returns the image record for the currently zapped image.
    this.getImageInfo = function() {
        return(this.zapimg.getImageInfo());
    };

    this.getImage = function(id) {
        return(this.strip.getImage(id));
    };

    this.getImagePosition = function() {
        return(this.zapimg.getPosition());
    };

    this.getZoom = function() {
        return(this.zapimg.getTier());
    };

    this.setCenterPoint = function(imgX, imgY) {
        return(this.zapimg.setCenterPoint(imgX, imgY));
    };

    // returns a hash with keys 'x' and 'y', whose values are numbers.
    this.getCenterPoint = function() {
        return(this.zapimg.getCenterPoint());
    };

    this.centerImage = function() {
        return(this.zapimg.centerImage());
    };

    this.setZoom = function(newZoom) {
        return(this.zapimg.setTier(newZoom));
    };

    this.setPosition = function(coord_system, tier, left, top) {
        this.zapimg.setPosition(coord_system, tier, left, top);
    };

    // returns the tier, top & left coordinates for the curent zap view.
    this.getPosition = function() {
        return(this.zapimg.getPosition());
    };

    this.addImage = function(image) {
        this.strip.addImage(image);
    };

    // center - optional arg, set true to center image
    this.setImage = function(image, center) {
        var centerImg = (center == null) ? true : center;
        this.zapimg.setImage(image, centerImg);
    };

    this.setControls = function(controlHTML) {
        if(controls_div) {
            controls_div.update(controlHTML);
        }
    };

    this.setTitle = function(newTitle) {
        this.title = newTitle;
        titletext_div.update(this.title);
    };

    this.getPanel = function(panel_id)  {
        return(panels[panel_id]);
    };

    this.addPanel = function(panel_id, class_name) {
        add_panel(panel_id, class_name);
    };

    this.add_side_panel = function() {
        return (add_side_panel());
    };

    this.removePanel = function(panel_id) {
        var panel = panels[panel_id];
        if(panel) {
            panel.foreground.remove();
            panel.remove();
            panels[panel_id] = null;
        }
    };

    this.showPanel = function(panel_id, show) {
        var panel = panels[panel_id];
        if(panel) {
            if(show) {
                panel.show();
                panel.foreground.show();
            } else {
                panel.foreground.hide();
                panel.hide();
            }
        }
    };

    this.init = function() {
        init_viewer();
    };

// private properties
    var self = this;
    // felixl - next line below, there is code that loads a "class_name"
    // option from zap_image_tag inputs (there is class_name under :style_options,
    // but that is used to create a style and is not passed on).
    // So class_name *always* ends up with the value "stripViewer", which is
    // used in the framework <div>. Looks like this in practice:
    //   <div id="zapviewer78178648" class="stripViewer" style="width: 100%; height: 100%;">
    // Somehow this doesn't seem right?

    var class_name = options.class_name ? options.class_name : "stripViewer";
    var defer_init = options.defer_init == true ? true : false;
    var width = options.width ? options.width : "100%";
    var height = options.height ? options.height : "100%";
    var field = options.field ? options.field : "";
    var image_service = image_service;
    var tile_service = tile_service;
    var info_service = info_service;
    var controls_div = null;
    var titletext_div = null;
    var panels = new Object();

// private methods
    function init_viewer() {
      if(defer_init)
        Event.stopObserving(window, 'load', init_viewer);

      buildFramework();
      setupKeyListener();
      setupWheelListener(self.container);

      // make sure this is called after buildFramework() so all objects are
      // created before attaching event hooks
      initEventObservers();

      self.zapimg.onImageChange = doImageChange;
      self.zapimg.onZoomChange = doZoomChange;
      self.zapimg.onMove = doMove;
      self.strip.onSelect = doSelect;

      // onKeyBefore event hook(s) exist
      if (self.onKeyBeforeArray.length > 0) {
          self.zapimg.zapWantsKey = true;
          self.strip.zapWantsKey = true;
      }

      // onKeyAfter event hook(s) exist
      if (self.onKeyAfterArray.length > 0) {
          self.zapimg.zapWantsKey = true;
          self.strip.zapWantsKey = true;
      }

      //we're trying to maintain a global list of viewers in this scope.
      _zapviewers[self.container.id] = self;

      // felixl - there is currently no code that accepts or generates "initial_image" option,
      // so code below never executes
      if(options.initial_image)
          self.strip.selectImage(options.initial_image);

      self.initialized = true;

      // onLoad fnc below executes the gynormous fnc, which looks similar to 'load_zapviewer78178648',
      // which then also calls the fnc, 'user_load_viewer'
      //
      if(self.onLoad) {
          // zapviewer:load event must fire off before the gynormous function is called, so that any
          // user-defined panels are created. Otherwise, the initialization functions
          // in the gynormous function will fire 'imagechange' events which will
          // update the contents of the panels before they even exist.
          $(self.id).fire("zapviewer:load", {viewer: self});

          self.onLoad(self);

          // zapviewer creation/instantiation complete
          $(self.id).fire("zapviewer:viewercreated", {viewer: self});
      }
    }

    function buildFramework() {
        // Note: the container is a <div> with id=/imageid/, and class="zapviewer"
        var containerHeight = self.container.getHeight();
        var imgHeight = (containerHeight-24)*0.8;
        var stripHeight = (containerHeight-24)*0.2;

        // wrapper element allows zapviewer (class='stripViewer') and side panel to exist side-by-side in same container
        var wrapper_div = new Element('div',{id:'zapwrapper'+self.container.id, className:'zapWrapper', style:'position:relative; width:100%;height:' + height + ';'});
        var framework_div = new Element('div',{id:'zapviewer'+self.container.id, className:class_name, style:'float:left; display:inline; width:' + width + '; height:100%'});
        var titlebar_div = new Element('div',{id:'zaptitle'+self.container.id, className:'viewTitleBar', style:'width:100%;height:24px;background-color:#4D6D86;'});
        titletext_div = new Element('div',{id:'zaptitletext'+self.container.id, className:'viewTitleText', style:'width:50%;height:1em;color:#fff;font-weight:bold;padding:3px;cursor:move'});
        controls_div = new Element('div',{className:'viewControls'});

        var field_light = ";";
        if(field.length > 0) {
           if(field == 'dark')
              field_light = ";background-color:#000;";
           else if(field == 'bright')
              field_light = ";background-color:#fff;";
        }
        var zap_div = new Element('div',{id:'zapimg'+self.container.id, className:'zapimg', style:'overflow:hidden;width:100%;height:'+ imgHeight + 'px' + field_light});
        var strip_div = new Element('div',{id:'simstrip'+self.container.id, className:'simpleStrip', style:'width:100%;height:'+ stripHeight + 'px'});

        self.container.appendChild(wrapper_div);
        wrapper_div.appendChild(framework_div);

        framework_div.appendChild(titlebar_div);
        titlebar_div.appendChild(titletext_div);
        titletext_div.update(self.title);
        titlebar_div.appendChild(controls_div);
        framework_div.appendChild(zap_div);
        framework_div.appendChild(strip_div);

        self.zapimg = new zapimage(zap_div.id, tile_service, info_service, {src_key:self.src_key, path:'', filter:options.filter, filter_vals:options.filter_vals}, false);
        self.strip = new simpleStrip(strip_div.id, 'h', image_service, self.max_tier, {src_key:self.src_key, defer_init:false, precalculated_path:self.precalculated_path});

        // scale bar integration
        if (self.sb_create === true || self.sb_create === "true") {
            var scalebar_div = new Element('div',{id:'scalebar'+self.container.id, className:'scalebar', style:'display:none,position:absolute;top:0px;left:0px;width:110px;text-align:center;font-family:arial,sans-serif;font-size:small;color:black;background:white;z-index:3000'});
            zap_div.appendChild(scalebar_div);
            self.scalebar = new zapscalebar(scalebar_div.id, zap_div.id, {resolution:self.sb_resolution, units:self.sb_units, opacity:self.sb_opacity, position:self.sb_position, hImage:self.sb_h_image, vImage:self.sb_v_image});
        }
    }

    // creates a side panel (right side), returning the <div> id for the panel as a string
    function add_side_panel() {
        var wrapper_div = $('zapwrapper'+self.container.id);
        var sidepanel_div = new Element('div',{id:'zapsidepanel'+self.container.id, className:'zapSidePanel', style:'float:left; display:inline; overflow:auto; border-style:solid; border-width:1px; width:' + self.side_panel_width + '; height:100%'});
        wrapper_div.appendChild(sidepanel_div);

        return ('zapsidepanel'+self.container.id);
    }

    // create a translucent horizontal panel just below the Zap Viewer title bar
    function add_panel(panel_id, class_name) {
        var panel = new Element('div',{id:panel_id+self.id, className:class_name, style:'position:absolute;z-index:2000;top:0;left:0;'});
        var foreground = new Element('div',{id:panel_id+'_foreground_'+self.id, className:class_name+'_foreground', style:'position:absolute;z-index:2001;top:0;left:0;'});
        panel.setOpacity(0.15);
        foreground.setOpacity(1);
        panel.foreground = foreground;
        panels[panel_id] = panel;
        self.zapimg.container.appendChild(panel);
        self.zapimg.container.appendChild(foreground);
    }

    function doImageChange(image) {
        $(self.id).fire("zapviewer:imagechange", {viewer: self, image: image});
        if(self.onImageChange) {
            self.onImageChange(self, image);
        }
    }

    function doZoomChange(image, position) {
        settingZoom = true;
        $(self.id).fire("zapviewer:zoom", {viewer: self, image: image, position: position});

        if(self.onZoom)
            self.onZoom(self, image, position);
    }

    function doSelect(image) {
        // the zapimg will show the largest tier that fits by default;
        // If the user wants a specific tier, set it, otherwise delete the tier field
        // so the zap does its default thing, then restore the original value.
        var orig_tier = image.tier;
        if(self.zap_tier) {
            image.tier = self.zap_tier;
        }
        else
            delete image['tier'];
        self.zapimg.setImage(image, true);
        image['tier'] = orig_tier;

        $(self.id).fire("zapviewer:select", {viewer: self, image: image});
        if(self.onSelect)
            self.onSelect(self, image);
    }

    function setupKeyListener() {
        //listen for key strokes for zoom & pan
        if(Prototype.Browser.WebKit) {
            // In Safari (& all webkit-based browsers) a <div> is not focusable,
            // so will never report keystrokes.  This adds an invisable text
            // element that will receive focus and process keyboard events.
            setupWebKitKeyListener();
        }
        else
            Event.observe(self.container, 'keydown', doKey, true);
    }

    var key_catcher;
    function setupWebKitKeyListener() {
        if(Prototype.Browser.WebKit) {
            key_catcher = new Element('input',{id:'zapviewerkey'+self.container.id, type:'text', className:'key_catcher', style:'outline:none;background-color:transparent;font-size:0pt;border:0px;width:0px;height:0px'});
            self.container.appendChild(key_catcher);
            Event.observe(self.container, 'click', catchWebkitKeys, true);
            key_catcher.focus();
            Event.observe(key_catcher, 'keydown', doKey, true);
        }
    }

    function catchWebkitKeys() {
        key_catcher.focus();
    }

    // this function redirects keyboard commands bubbling up from a child object who is
    // not responsible for handling the event. Once Zap Viewer gets the command, it
    // redirects it to the appropriate child object.
    // For example, arrow key pan events from the image strip are bubbled up to Zap Viewer,
    // who redirects it to zap image.
    // If we get to this function via the user calling the public doKey function, we don't want to
    // set off any keyBefore or keyAfter notification events.
    function doKey(evt) {

        if (self.onKeyBeforeArray.length > 0 && evt.fromZapViewer !== true) {
            // if we fire off keyBefore, then we don't want to handle the key event here
            $(self.id).fire("zapviewer:keybefore", {viewer: self, keyCode: evt.keyCode});
            return;
        }

         switch(evt.keyCode) {
            case Event.KEY_RIGHT:
                self.zapimg.doKey(evt);
                break;
            case Event.KEY_LEFT:
                 self.zapimg.doKey(evt);
                break;
            case Event.KEY_UP:
                self.zapimg.doKey(evt);
                break;
            case Event.KEY_DOWN:
                self.zapimg.doKey(evt);
                break;
            case 69://"e" select 1st image
                self.strip.doKey(evt);
                break;
            case 82://"r" select last image
                self.strip.doKey(evt);
                break;
            case 68://"d" select previous image
                self.strip.doKey(evt);
                break;
            case 70://"f" select next image
                self.strip.doKey(evt);
                break;
            case 65://"a" to be consistent with SIV, zoom in
                self.zapimg.doKey(evt);
                break;
            case 90://"z" to be consistent with SIV, zoom out
                self.zapimg.doKey(evt);
                break;
            case 107://"+"
                self.zapimg.doKey(evt);
                break;
            case 109://"-"
                self.zapimg.doKey(evt);
                break;
            case 187://"+" on IE & Safari number row
                self.zapimg.doKey(evt);
                break;
            case 189://"-" on IE & Safari number row
                self.zapimg.doKey(evt);
                break;
            case 61://"+" on FFv2 number row
                self.zapimg.doKey(evt);
                break;
            default:
                break;
        }

        $(self.id).fire("zapviewer:keyafter", {viewer: self, keyCode: evt.keyCode});
    }

    function setupWheelListener(element) {
        //listen to mouse wheel events. (Credit http://www.ogonek.net/mousewheel/demo.html)
        Object.extend(Event, {
                wheel:function (event){
                        var delta = 0;
                        if (!event) event = window.event;
                        if (event.wheelDelta) {
                                delta = event.wheelDelta/120;
                                if (window.opera) delta = -delta;
                        } else if (event.detail) {
                                delta = -event.detail/3;
                        }
                        return Math.round(delta);
                }
        });
        Event.observe(element, 'mousewheel', wheelZoom, false);
        Event.observe(element, 'DOMMouseScroll', wheelZoom, false);
    }

    // catches wheel zoom events that bubble up from image strip,
    // then applies them to the zapimage object
    function wheelZoom(evt) {
        Event.stop(evt);

        var delta = Event.wheel(evt);

        // ensure that 1 event correlates to one unit of scroll
        var increment = 1;
        if (delta < 0) {
            increment = -1;
        }

        var curTier = self.zapimg.getTier();
        self.zapimg.setTier(curTier + increment);
    }

    function inspect(prefix, obj) {
        var s = "";
        for(key in obj) {
            if((typeof obj[key]) == "object" && obj[key] !== null & key != "element")
                s += "\n" + prefix + "key: " + key + inspect(prefix + "    ", obj[key]);
            else
                s += "\n" + prefix + "key: " + key + ",   value: " + obj[key];
        }
        return(s);
    }

    function doMove(image, position) {
        $(self.id).fire("zapviewer:move", {viewer: self, image: image, position: position});
        if(self.onMove)
            self.onMove(self, image, position);
    }

    // setup custom event hooks for custom events
    function initEventObservers() {
        // used if initial image has a particular initial pan position
        $(self.id).observe('zapviewer:imagechange', setStartupCenterPoint);

        // used if a scale bar is being created
        if (self.scalebar !== null) {
            $(self.id).observe('zapviewer:imagechange', self.scalebar.updateScaleBar)
            $(self.id).observe('zapviewer:zoom', self.scalebar.updateScaleBar)
        }

        var fName;
        var i;
        for (i = 0; i < self.onLoadArray.length; ++i) {
            fName = self.onLoadArray[i];
            $(self.id).observe('zapviewer:load', eval(fName));
        }
        for (i = 0; i < self.onViewerCreatedArray.length; ++i) {
            fName = self.onViewerCreatedArray[i];
            $(self.id).observe('zapviewer:viewercreated', eval(fName));
        }
        for (i = 0; i < self.onImageChangeArray.length; ++i) {
            fName = self.onImageChangeArray[i];
            $(self.id).observe('zapviewer:imagechange', eval(fName));
        }
        for (i = 0; i < self.onZoomArray.length; ++i) {
            fName = self.onZoomArray[i];
            $(self.id).observe('zapviewer:zoom', eval(fName));
        }
        for (i = 0; i < self.onMoveArray.length; ++i) {
            fName = self.onMoveArray[i];
            $(self.id).observe('zapviewer:move', eval(fName));
        }
        for (i = 0; i < self.onSelectArray.length; ++i) {
            fName = self.onSelectArray[i];
            $(self.id).observe('zapviewer:select', eval(fName));
        }
        for (i = 0; i < self.onKeyBeforeArray.length; ++i) {
            fName = self.onKeyBeforeArray[i];
            $(self.id).observe('zapviewer:keybefore', eval(fName));
        }
        for (i = 0; i < self.onKeyAfterArray.length; ++i) {
            fName = self.onKeyAfterArray[i];
            $(self.id).observe('zapviewer:keyafter', eval(fName));
        }
    }

    /*
     * Event hook to set initial image centering coords and zoom. Responds to
     * 'zapviewer:imagechange' event and is only processed if valid
     * values of 'start_x' and 'start_y' are specified.
     *
     * Once in this function, the very first thing to do is call
     * Event.stopObserving so that you won't call this function for
     * subsequent image changing, like when displaying other
     * images in the series. This hook is only called after the
     * imageproperties is returned from the .aff file, this
     * necessary because otherwise it is impossible to execute the
     *setCenterPoint() fnc below.
    */
    function setStartupCenterPoint(evt) {
        var viewer = evt.memo['viewer'];
        var image = evt.memo['image'];

        var startId = viewer.start_id;
        var xCenter = parseFloat(viewer.start_x);
        var yCenter = parseFloat(viewer.start_y);
        var zoom = parseInt(viewer.start_tier);

        // only process if changeimage event is for the image we are loading,
        // this won't happen unless someone changes up the loading code :)
        if (image.id.toString() == startId) {
            $(viewer.id).stopObserving("zapviewer:imagechange", setStartupCenterPoint);
            if (!isNaN(xCenter) && !isNaN(yCenter)) {
                self.setCenterPoint(xCenter, yCenter);
                self.zapimg.refresh();
            }

            // must call *after* setCenterPoint, 'cause it uses the centerpoint values!
            if (!isNaN(zoom)) {
                self.setZoom(zoom);
            }
        }

        return;
    }

    //initialization
    if(defer_init == true) {
        Event.observe(window, 'load', init_viewer);
    }
    else {
        init_viewer();
    }


}

// Given an html table populated with zapviewer objects, this code allows
// dragging & dropping the viewers from one cell to another as well as
// dynamically loading new viewers and removing existing.
//
// params:
//
// table_id:           the html ID of the table to be wrapped.
// new_viewer_url:     the url of the method that will deliver the ajax
//                     response for a new viewer request.
function viewer_table(table_id, new_viewer_url, size, busy_icon_url)
{
//public properties
    this.size = size;
    this.busy_icon_url = busy_icon_url;

//event hooks
    // called if the attempt to request a new viewer fails
    this.on_ajax_error = null;
    // called if the response from a viewer request is invalid
    this.on_response_error = null;


//public methods
    this.addViewer = function(image_Series_id) {
      //find how many open spots there are in the first row
      var openCells = 0;
      var first_row = the_table.rows[0];
      for(var i = 0; i < first_row.cells.length; i++) {
        var viewer = getByClass(first_row.cells[i], 'div', 'zapviewer');
        if(!viewer.length)
           openCells++;
      }

      if(openCells == 0)
        prependRow(the_table);

      var cell = nextOpenCell();
      if(cell !== null) {
         if(this.busy_icon_url)
            cell.innerHTML = "<img class='busyImg' src='" + this.busy_icon_url + "' />"
         else
            cell.innerHTML = "<img class='busyImg' src='ajax-loader.gif' />"
         fetchViewer(image_Series_id, cell);
      }
    };

    this.removeViewer = function(viewer_id) {
        removeViewer(viewer_id);
    };

    /* define a class method that we can call without instantiating viewer_table
     * This allows us to set up valid event hooks before the viewer_table is created.
     * It duplicates the functionality of user_load_viewer, but allows us to predefined
     * the event hook as part of the :event_options hash.
     * Note: cannot call user_load_viewer from inside this method, because
     * user_load_viewer is not a class method.
     */
    viewer_table.makeViewerDraggable = function(evt) {
        var viewer = evt.memo.viewer;
        new Draggable(viewer.id, {handle:'zaptitletext' + viewer.id,
                                          scroll:window,
                                          scrollSensitivity:50,
                                          revert:'failure',
                                          onStart: function() {
                                                Event.observe(document.body, "selectstart", function() { Event.stop(event); return false; } );
                                          }
                                   }
                     );
    };

    // This function is called when the zap_viewer_tag initialization script is complete.
    // Here we're making each viewer draggable, with the title bar the handle.
    this.user_load_viewer = function (viewer) {
        new Draggable(viewer.id, {handle:'zaptitletext' + viewer.id,
                                          scroll:window,
                                          scrollSensitivity:50,
                                          revert:'failure',
                                          onStart: function() {
                                                Event.observe(document.body, "selectstart", function() { Event.stop(event); return false; } );
                                          }
                                   }
                     );
    };

//private properties
    var viewer = this;
    var ref_cell_sequence = 0;
    var the_table = $(table_id);
    var ajax_viewer_url = new_viewer_url;

//private methods
    function doDrop(draggable, droppable, evt) {
        if(draggable.parentNode.id != droppable.id) {
          var viewer = droppable.select('div.zapviewer')[0];
          if(viewer) {
             moveViewer(viewer, draggable.parentNode);
          }
        }
        moveViewer(draggable, droppable);
    }

    function moveViewer(viewer, target_cell) {
        var the_view = viewer.parentNode.removeChild(viewer);
        target_cell.appendChild(the_view);
        viewer.setStyle('top:0px;left:0px;');
    }

    function setupTable() {
        //make each cell in the table a drop target
        var the_cells = the_table.getElementsByTagName('td');
        for(var i = 0; i < the_cells.length; i++) {

            if(!the_cells[i].id)
                the_cells[i].id = ref_cell_sequence++;

              Droppables.add(the_cells[i].id, {accept:'zapviewer',
                                          hoverclass:'testHover',
                                          overlap:'vertical',
                                          onDrop:doDrop}
                            );
        }
    }

    function getByClass(container, tag_name, class_name) {
        var list = new Array();
        var count = 0;
        var kids = container.getElementsByTagName(tag_name);
        for(var i = 0; i < kids.length; i++) {
            if(kids[i].className == class_name) {
                list[count++] = kids[i];
            }
        }
        return(list);
    }

    function fetchViewer(image_series_id, container) {
          new Ajax.Request(ajax_viewer_url + image_series_id + "/" + viewer.size,
          {
              method:'get',
              asynchronous:false,
              onSuccess: function(response) { loadViewer(response, container);   },
              onFailure: function(response) { doFetchError(response, container); }
          });
    }

    // Use the response from an ajaxy call to create a new viewer object
    function loadViewer(response, container) {
        if(!response.responseText) {
            if(viewer.on_response_error)
                viewer.on_response_error(response, container);
            return;
        }

         // the response contains a <div> to host the control...
         container.innerHTML = response.responseText.match(/<div[\d|\D]*<\/div>/)[0];
         // ..as well as a <script> that loads the images and initializes the control
         script_content = response.responseText.match(/<script>([\d|\D]*)<\/script>/)[1];
         eval(script_content);

        //add the new viewer to the set of draggables.
        var viewer = $(container.firstChild);
        var titlebar_div = viewer.select('div.viewTitleBar');
        new Draggable(viewer.id, {handle:titlebar_div[0].id,
                                          scroll:window,
                                          scrollSensitivity:50,
                                          revert:'failure'}
                     );
    }

    function doFetchError(response, container) {
        if(!response.responseText) {
            if(viewer.on_ajax_error)
              viewer.on_ajax_error("request FAILED, with no error text.", container);
            return;
        }

        if(viewer.on_ajax_error)
           viewer.on_ajax_error("FAIL!\n" + response.responseText, container);
    }

    function newCell(row) {
       var c = row.insertCell(-1);
       c.id = "ref" + ref_cell_sequence++;
       Droppables.add(c.id, {accept:'zapviewer',
                                      hoverclass:'testHover',
                                      overlap:'vertical',
                                      onDrop:doDrop}
                        );
       return(c);
    }

    function prependRow(the_table) {
       var new_row = the_table.insertRow(0);
       for(var i = 0; i < the_table.rows[1].cells.length; i++) {
          newCell(new_row);
       }
    }

    function nextOpenCell() {
        var cols = the_table.rows[0].cells.length;
        for(var i = 0; i < the_table.rows.length; i++) {
            for(var j = 0; j < cols; j++) {
                if(getByClass(the_table.rows[i].cells[j], 'div', 'zapviewer').length == 0)
                    if(!the_table.rows[i].cells[j].pending) {
                        the_table.rows[i].cells[j].pending = true;
                        return(the_table.rows[i].cells[j]);
                     }
            }
        }
        return(null);
    }

    function removeViewer(viewer_id) {
        delete _zapviewers[viewer_id];
        var viewer = $(viewer_id)
        viewer.parentNode.pending = false;
        viewer.remove();
    }


// Wait until the page is fully loaded before setting up
    Event.observe(window, 'load', setupTable);
}

