// $Id: alignMap.js,v 1.25 2007/08/24 19:12:39 sherrimn Exp $
// $Source: /cm/cvs/webApps/shared/plugins/alignMap/alignMap.js,v $

var MAP_DEBUG = false;
if (MAP_DEBUG) {
  window.loadFirebugConsole();//DEBUG
  console.log('DEBUG is on');
}


var AlignMap = Class.create(AjaxPlugin, {
  initialize: function($super, element_id, session_object, app_name, app_style)
  {
    // Call the superclass constructor:
    $super();
    this.map_id = 'map'+this.instance_id+'_';// A unique prefix for all element IDs
    
    // Remember our attributes:
    this.appName = app_name;
    if (!app_style) app_style = 'GA'; // Default is GA style
    this.appStyle = ''+app_style;

    //this.serverpath = gMapServerPath; // this was deduced above
    this.serverpath = this.plugins_server_path + 'alignMap09/';
    
    // The default help file:
    this.helpPath = this.serverpath + 'help/maphelp0.html';

    // Remember our host element
    this.host_element_id = element_id;
    this.host_element = $(this.host_element_id);
    if (this.host_element == null) throw('alignMap host element is null');
    
    // Create a map of host elements to map instances:
    if (!AlignMap.prototype.instance_map) { AlignMap.prototype.instance_map = $H(); }
    AlignMap.prototype.instance_map.set(this.host_element_id, this);
    
    //this._matchHostWidth();
    this._setupHostElement();
    // Set a global var to point to this map instance.
    // This prevents multiple maps on a page.  (Can we eliminate this??)
    //alignMapPtr = this;
    
    // Remember our session object:
    this.sessionPtr = session_object;
    
    this.pan_widths = 3;
    
    // an associative array to hold our event handler pointers
    this.eventHandlerMap = []; 
    
    this.selectedItemsHash  = $H({});
    
    // Which optional features shall we show?
    this.includedFeaturesVal = "";
    this.labels = ""; // The labels to enable
    
    this.sessionData = {
                         mapID:         '',
                         mapType:       '',
                         species:       '',
                         chromosome:    '',
                         chr_left:       0,
                         chr_right:      0,
                         selectedItems: '',
                         overlayFeatures: $H({}),
                         tied_exons:      $H({})
                       };

    // Include IE-specific styles if nec:
    if (Prototype.Browser.IE) {
      var ie_sheet = $link({ type: "text/css", rel: "stylesheet", href: this.serverpath+"css/alignMap_IE.css" });
      document.getElementsByTagName("head")[0].appendChild(ie_sheet);
    }
    
    // Keep track of whither or not the shift key is down:
    Event.observe(document, 'keydown', function(e) {
      if (e.keyCode == 16) this.shiftKeyPressed = true;
    }.bind(this));
    Event.observe(document, 'keyup', function(e) {
      if (e.keyCode == 16) this.shiftKeyPressed = false;
    }.bind(this));

    // Set a hook to pan when user gets near the map edge: 
    this.set_event_hook('panStop', function(e) {

      var pan_pos = this.getPanPos();
      if (pan_pos < 50) {  // close to left or right?
        this.pan.bind(this).defer('left');
      } else if (pan_pos > (this.map_width * ((this.pan_widths - 1) * 0.98))) {
        this.pan.bind(this).defer('right');
      }
    }.bind(this));
    
    this.firstLoad = true;
    this.reload_in_progress = false;
  },
  
  setAppName: function(app_name)
  {
    this.appName = app_name;
  },
  
  // Allow the map caller to set the helpFile path:
  setHelpFile: function(helpPath)
  {
    // Try to handle absolute and relative URLs: 
    if (helpPath.match(/^http/) || helpPath.match(/^\//)) {
      this.helpPath = helpPath;
    } else { // relative:
      this.helpPath = this.serverpath + helpPath;
    }
      
    // If we already have a helpBalloon instance, set it to the spec'd file:
//    var helpIcon = $(this.map_id+'mapHelp');
//    if (helpIcon && helpIcon.firstDescendant())
//    {
//      helpIcon.firstDescendant().writeAttribute({href:this.helpPath});
//    }
    
    // modify our helpBalloon with the desired page
    if (this.helpBalloon) {
      this.helpBalloon.options.dataURL = this.helpPath;
    }

  },
  
  // Get a list of entity IDs of a certain type:
  // Scope can be "ALL", "INVISIBLE", "VISIBLE"(default)
  getEntityIds: function(entity_type, scope)
  {
    var elems = [],
    // A CSS selector used to find the entity_type as an ID prefix:
    selector = 'span[id='+this.map_id+'PanSpan] span[id^='+this.map_id+entity_type+'_]',
    // A regex used to strip off the prefix and underscore:
    prefix_regex = new RegExp(this.map_id+entity_type+'_'),
    panPos = this.getPanPos();
    // Find 'em, strip 'em, and return 'em:
    $$(selector).each(function(elem){
      if (scope == "ALL") // versus just visible
      {
        elems.push(elem.id.replace(prefix_regex, ''));
        return;
      }
      
      // Determine if this element is in the visible part of the map:
      var left = elem.positionedOffset().left,
           right = left+elem.getWidth(),
           visible = (right > panPos && left < (panPos + this.map_width));

      if (!scope || scope == 'VISIBLE') // they want visible
      {
        if (visible) elems.push(elem.id.replace(prefix_regex, ''));
      }
      else // else they want invisible:
      if (!visible) elems.push(elem.id.replace(prefix_regex, ''));
          
    }.bind(this));
    return elems;
  },
  
  _saveSessionData: function()
  {
    if (this.sessionPtr)
    {
      this.sessionPtr.setParam("alignMapSessionData", 
                               Object.toJSON(this.sessionData));
    }
  },

  _keepSelectedElementsSelected: function ()
  {
    // Iterate through the selectedItemsHash and draw as selected:
    this.selectedItemsHash.keys().each(function(key){
      // Draw it as selected
      this._drawSelected(key);
    }.bind(this));
  },

  _populateMapDataStruct: function()
  {
    var map_data = $(this.map_id+"alignMap_data");
    
    if (map_data)
    {
      var map_data_json = map_data.firstChild.nodeValue;
      this.map_data = map_data_json.evalJSON();
      if (this.map_data.app != null)
      {
        this.appName = this.map_data.app;
      }
    }
  },
  
  _setupHostElement: function ()
  {
    this._matchHostWidth();
    
    if (!this.host_element || $(this.map_id+'mapHeader') || !this.map_width) return;
    
    this._addHeader();
    
    // Add the body span
    this.map_body = $span({'id':this.map_id+'mapBody',
    'class':this.appStyle+'_map_body',style:'width:'+this.map_width+'px;'});
    this.host_element.appendChild(this.map_body);
    
    this._addFooter();
    
    this.host_element.makePositioned();
    this.host_element.setStyle({'overflow-x':'hidden'});
  },
  
  _addHeader: function()
  {
    // Add the header span
    this.map_header = $span({'id':this.map_id+'mapHeader',
        'class':this.appStyle+'_map_header',style:'width:'+this.map_width+'px;'});
    this.host_element.appendChild(this.map_header);
    
    // Add a build placeholder:
    var mapHelp = $span({'id': this.map_id+'mapBuildLabel', 'class':this.appStyle+'_map_build_label'});
    this.map_header.appendChild(mapHelp);

    // Add the help icon
    var mapHelp = $span({'id': this.map_id+'mapHelp', 'class':this.appStyle+'_map_help'}, 'help');
    this.map_header.appendChild(mapHelp);
    // Tweak the left of help icon:
    var help_width = parseInt(mapHelp.getStyle('width'));

    mapHelp.setStyle({'left':this.map_width-help_width+'px'});
//    this.host_element.appendChild(header);
    
    //var mapHelp = $(this.map_id+'mapHelp');
    
    if (this.appStyle == 'GA' && typeof(HelpBalloon) != 'undefined' &&
        (Prototype.Browser.IE || // IE OK
         Prototype.Browser.Gecko)) // Gecko(Firefox) OK
    {
      if (this.helpBalloon == null) 
      {
        this.helpBalloon = new HelpBalloon({
            returnElement: true,
            dataURL: this.helpPath,
            //icon:    this.serverpath + 'images/icon_question.gif',
            //icon:     $(this.map_id+'mapHelp'),
            icon:    this.serverpath + 'images/null.gif',
            altText: 'Click to view map help',
            guid:    GA_UTIL.getUID() //'33333'
        });
      }

      mapHelp.appendChild(this.helpBalloon._elements.icon);
    }
    
    mapHelp.observe('click', function(){
        if (this.helpBalloon) {
          this.helpBalloon.show();
        } else if (typeof(popUpHelp) != 'undefined') {
          popUpHelp(this.helpPath);
        } else {
//        window.open(this.helpPath, 'mapHelp', 'toolbar=0,scrollbars=1,location=0,statusbar=0,menubar=0,resizable=1,width=500,height=625,left = 500,top = 100');
          window.open(this.helpPath, 'mapHelp');
        }
    }.bind(this));

  },
  
  _addFooter: function()
  {
    // Add the footer span
    this.map_footer = $span({'id':this.map_id+'mapFooter',
        'class':this.appStyle+'_'+this.appName+'_map_footer '+
                this.appStyle+'_map_footer',style:'width:'+this.map_width+'px;'});
    //var footer_img = $img({'src':this.serverpath+'help/images/'+this.appName+'Legend.png'});
    //this.map_footer.appendChild(footer_img);
    this.host_element.appendChild(this.map_footer);

    if (this.appName == 'GeneX' || this.appName == 'BeamerGE' || this.appName == 'siRNA' ||
        this.appName == 'ncRNA')
    {
      // Add a switch to toggle non-refseqs
      var refseq_switch = $span({'class':this.appStyle+'_map_RefSeqSwitch'})
      var val = (GA_UTIL.getCookieVal("show_mRNAs") == "allmRNA")? 'checked="checked"':'';

      refseq_switch.innerHTML = '<input id="'+this.map_id+'Switch_refSeq" type="checkbox" '+
            val+' /><span class="mapSwitchLabel"> - show GenBank mRNAs</span>';
      this.map_footer.appendChild(refseq_switch);
      $(this.map_id+'Switch_refSeq').observe('click', function() {
          this.toggleGBmRNAs();
      }.bind(this));
    }
    
    if (this.appName == 'siRNA' || this.appName == 'workflow')
    { // Add the siRNA switches
      // Toggle silencers:
      var silencer_switch = $span({'class':this.appStyle+'_map_SilencerSwitch'})
      var val = (GA_UTIL.getCookieVal("show_silencersiRNAs") == "yes")? 'checked="checked"':'';

      silencer_switch.innerHTML = '<input id="'+this.map_id+'Switch_silencers" type="checkbox" '+
            val+' /><span class="mapSwitchLabel"> - show <i>Silencer</i>&reg; siRNAs</span>';
      this.map_footer.appendChild(silencer_switch);
      $(this.map_id+'Switch_silencers').observe('click', function(e) {
          if (e.element().checked)
            this.showSilencersiRNAs();
          else
            this.hideSilencersiRNAs();
          this.reloadMap();
      }.bind(this));

      // Toggle si_sels:
      var sisel_switch = $span({'class':this.appStyle+'_map_SiSelSwitch'})
      var val = (GA_UTIL.getCookieVal("show_siSelectsiRNAs") == "allsiRNA")? 'checked="checked"':'';

      sisel_switch.innerHTML = '<input id="'+this.map_id+'Switch_sisels" type="checkbox" '+
            val+' /><span class="mapSwitchLabel"> - show all <i>Silencer</i>&reg; Select siRNAs</span>';
      this.map_footer.appendChild(sisel_switch);
      $(this.map_id+'Switch_sisels').observe('click', function(e) {
          if (e.element().checked)
            this.showAdditionalSiSelectsiRNAs();
          else
            this.hideAdditionalSiSelectsiRNAs();
          this.reloadMap();
      }.bind(this));
      
    }
    
  },
  
  _matchHostWidth: function ()
  {
      // Set the *map* width, to match the container:
    this.map_width = parseInt(this.host_element.getStyle('width'));
    if (this.map_width == 0 || isNaN(this.map_width)) {
      this.map_width = 600; 
    }
  },
  
  _setContainerHeight: function()
  {
    // Set the height, if possible:
    if (typeof(this.map_data) != 'undefined')
    {
      var footer_height = parseInt(this.map_footer.getStyle('height'));
      var legend_padding = 23;
      this.map_height = parseInt(this.map_data.features_bottom);
      if (this.map_height < 50) {  this.map_height = 50; }
      this.map_body.setStyle({height: this.map_height + 'px'});
      this.host_element.setStyle({height: (this.map_height + legend_padding + footer_height) + 'px'});
      this.map_footer.setStyle({top: (this.map_height + legend_padding) + 'px'});
    }
  },

  _addFeatureLabels: function()
  {  
    if (!this.map_data) return;
    
    // Set the build label specified by the map
    if ($(this.map_id+'mapBuildLabel')) {
      $(this.map_id+'mapBuildLabel').update(this.map_data.build_label);
    }
    
    // Draw selected tier labels here:
    var tier_labels = $H(this.map_data.tier_labels);

    tier_labels.each(function(label_data){
      var text = label_data[0],
           y = label_data[1];

      if (text.match(/siRNA/)) {
        var label = $span({'class':'mapTierLabel', style:'top:'+y+'px;'}, text);
        label.update(text.replace(/Silencer/, '<i>Silencer</i>'));
        this.map_body.appendChild(label);
      }
    }.bind(this));
  },
  
  _addChromosome: function()
  {
    if (!this.map_data) return;
    // Call the server to fetch a chrom image with a tickmark at our position
    var round_factor = 100000.0;//Lettuce round to roughly 1 pixel worth
    if (!$(this.map_id+'chromosome_overlay')) {
      this.map_header.appendChild($span({'id':this.map_id+'chromosome_overlay',
      'class':this.appStyle+'_map_chrom'}));
    }
    var chrom_width = $(this.map_id+'chromosome_overlay').getWidth();
    
    var url = this.serverpath + 'chrom.rb';
    
    var params = $H({
            wanted: 'json', 
            server: this.serverpath,
            instance: this.map_id,
            species: this.map_data.ncbi_node_id,
            chrom: this.map_data.chromosome,
            width: chrom_width,
            left:  Math.round(this.map_data.chr_left/round_factor)*round_factor, 
            right: Math.round(this.map_data.chr_right/round_factor)*round_factor
    });
    
    // Fetch the chromosome markup as json:
    GA_UTIL.jsonPRequest(url, params.toQueryString(), function(chrom_markup) {
        // Put the chromosome markup into the container:
        $(this.map_id+'chromosome_overlay').innerHTML = chrom_markup;
    }.bind(this));
  },
  
  //////////////////////////////////////////////////////////////////////////////
  // id_type can be "gene_id", "transcript_id", or "assay_id"
  showMap: function(id_type, id_val)
  { 
//console.log(this.map_id+' showMap with '+id_type+' '+id_val);
    if (!id_type || !id_val) {return;}
    
    this.firstLoad = true;
    
    this.resetFlankData(); // clear flank maps
    
    // Show an hourglass while we fetch the map
    this.show_progress();

    this._fetchMap(id_type, id_val, this._setup_incoming_map.bind(this));
  },

  _fetchMap: function(id_type, id_val, handler)
  {     
    
    //this._matchHostWidth();
    
    var cookieParams = this.getCookieParams(),
        URL = this.serverpath + 'alignMap.rb?wanted=json'+
             '&input_type='     + id_type +
             '&acc='            + id_val +
             '&include_features='+ this.includedFeaturesVal +
             '&labels='          + this.labels +
             '&app='             + this.appName +
             '&map_style='       + this.appStyle +
             '&map_id='          + this.map_id +
             '&server='          + this.serverpath +
             '&map_width='       + this.map_width +
             cookieParams        +
             '&cachestopper='    + Math.random();

    this.raise_event('onLoading');
    this._mapAjaxCall(URL, handler);
    
    // Remember the current id and id_type in a browser cookie
    this.sessionData.mapID   = id_val;
    this.sessionData.mapType = id_type;
    this._saveSessionData();
  },

  
  //////////////////////////////////////////////////////////////////////////////
  // 
  showMapRegion: function (species, chromosome, left, right, acc, acc_type, orient)
  {
//console.log(this.map_id+' showMapRegion with '+left+','+right);
    if (typeof(left)  == 'string') left = parseInt(left);
    if (typeof(right) == 'string') right = parseInt(right);
    this.firstLoad = true; // First load of a new map
    
    this.resetFlankData(); // clear flanks
    
    // Show an hourglass while we fetch the map
    this.show_progress();
    
    var handler = this._setup_incoming_map.bind(this);

    // add a margin (for CADT):
    var margin = (right - left) * 0.05;
    left  -= margin;
    right += margin;
    
    this._fetchMapRegion(species, chromosome, left, right, acc, acc_type, orient, handler);
  },
  
  _fetchMapRegion: function (species, chromosome, left, right, acc, acc_type, orient, handler)
  {
    if (chromosome == '' && typeof(this.map_data) != 'undefined') 
    {
      chromosome = this.map_data.chromosome;
    }

    //this._matchHostWidth();

    this.m_current_species = species;
    this.m_current_chrom   = chromosome;
    
    var cookieParams = this.getCookieParams(),
        URL = this.serverpath + 'alignMap.rb?wanted=json'+
             '&input_type=region' +
             '&acc='            + acc +
             '&acc_type='       + acc_type +
             '&species='        + species +
             '&chrom='          + chromosome +
             '&left='           + Math.round(left) +
             '&right='          + Math.round(right) +
             '&orient='          + orient +
             '&include_features='+ this.includedFeaturesVal +
             '&labels='          + this.labels +
             '&app='             + this.appName +
             '&map_style='       + this.appStyle +
             '&map_id='          + this.map_id +
             '&server='          + this.serverpath +
             '&map_width='       + this.map_width +
             cookieParams        +
             '&cachestopper='    + Math.random();

    this.raise_event('onLoading');

    this._mapAjaxCall(URL, handler);
  },

  _mapAjaxCall: function(url, callback)
  {
    GA_UTIL.jsonPRequest(url, '', callback);
  },
  
  reloadMap: function()
  {
    if (!this.map_data) 
      return; // Don't try to draw if we have no info:
    
    if (this.map_data.map_type == 'region')
    {
      var map_left = this.getPanPos(),
          map_right = map_left + this.map_width,
          chr_left = this.mapCoordToChrom(map_left),
          chr_right = this.mapCoordToChrom(map_right);
      
      this.showMapRegion(this.map_data.species, 
                         this.map_data.chromosome,
                         chr_left,
                         chr_right,
                         this.map_data.acc,
                         this.map_data.acc_type,
                         this.map_data.orientation);
    }
    else if (this.map_data.acc != '')
    {
      this.showMap(this.map_data.map_type, this.map_data.acc);
    }

  },
  
  resizeStandaloneMap: function(new_width)
  {
    if (new_width < 600 || new_width == this.map_width) return;
    
    this.map_width = new_width;
    //$('map_header').setStyle({width:new_width+'px'});
    this.map_header.setStyle({width:new_width+'px'});
    this.map_footer.setStyle({width:new_width+'px'});
    this.map_body.setStyle({width:new_width+'px'});
    
    var mapHelp = $(this.map_id+'mapHelp');
    mapHelp.setStyle({left:(new_width-parseInt(mapHelp.getStyle('width')))+'px'});
    
    // Don't reload more than once at a time:
    if (this.reload_in_progress) {
      this.queue_reload = true;
    } else {
      this.reload_in_progress = true;
      this.reloadMap(); 
    }
  },

  //////////////////////////////////////////////////////////////////////////////
  // feature_string tells the map what optional features to show
  // It should be a comma-delimited list like "miRNA,CNV"
  // This is deprecated in favor of setting app_name
  setIncludedFeatures: function(feature_string)
  {
    this.includedFeaturesVal = feature_string;
  },
  
  setLabels: function(feature_string)
  {
    this.labels = feature_string;
  },

  show_progress: function()
  {
    var progress_id = this.map_id+'map_progress';
    if ($(progress_id)) { $(progress_id).show(); }
    else
    {
      var progress_img = $img({src:this.serverpath + '/images/progress.gif',
      'class':'map_progress', 'id':progress_id});
      this.map_body.appendChild(progress_img);
    }
  },
  hide_progress: function()
  {
    var progress_id = this.map_id+'map_progress';
    if ($(progress_id)) { $(progress_id).hide(); }
  },    
  
  
  _empty_trash_can: function()
  {
    this._trash_can_contents.each(function(elem){
      elem.stopObserving();
      //elem.remove();
    });
    this._trash_can_contents = [];
  },
  
  _setup_incoming_map: function (map_html) 
  {
    // Handle multiple reloads, as in IE resizing:
    if (this.reload_in_progress) {
      this.reload_in_progress = false;
      if (this.queue_reload) {
        this.queue_reload = false;
        this.reloadMap();
        return;
      }
    }

    if (typeof(map_html) == 'undefined' || map_html === null || map_html == '')
    {
      map_html = 'Error: No data from alignMap';
    }

    // Removing the observers from these elems fixes mem leak
    this._trash_can_contents = this.map_body.descendants();
    this._empty_trash_can.bind(this).delay(2);
    
    // Insert map HTML into the host element:
    this.map_html = map_html;
    this.map_body.innerHTML = map_html;//myHtml;
    
    this.show_progress();//ie. keep showing it until we're done loading
    var panSpan = $(this.map_id+'PanSpan');
    
    panSpan.appendChild($span({'id':this.map_id+'temp_features_overlay'}));
    panSpan.appendChild($span({'id':this.map_id+'selected_features_overlay'}));
    panSpan.appendChild($span({'id':this.map_id+'colorBar_overlay'}));
    panSpan.appendChild($span({'id':this.map_id+'exon_tie_overlay'}));

    // Grab the map metadata hidden in the HTML:
    this._populateMapDataStruct();
    
    // We have deferred this from the constructor to here:
    this._setContainerHeight();
    this._setupHostElement();
    this._addFeatureLabels();
    this._addChromosome.bind(this).defer();

    
    // Show any overlayed features
    var temp_features = $H({}),
        selected_features = $H({});
        
    this.sessionData.overlayFeatures.values().each(function(feature){
        
      var feature_id = this.map_id+feature.type+'_'+feature.name;
      // Split the features between the overlayers by selected v unselected:
      if (this.selectedItemsHash.get(feature_id))
      {
        selected_features.set(feature_id, feature);
      }
      else
      {
        temp_features.set(feature_id, feature);
      }
    }.bind(this));
    
    // Now draw the overlay features
    this.addOverlayedFeatures(selected_features,this.map_id+'selected_features_overlay', null, 'nopulsate');
    this.addOverlayedFeatures(temp_features,this.map_id+'temp_features_overlay', null, 'nopulsate');
    
    this._keepSelectedElementsSelected();
    
    // Redraw any exon_ties:
    this.sessionData.exon_ties = $H(this.sessionData.exon_ties);
    this.sessionData.exon_ties.each(function(tie_pair){
      this.draw_exon_tie(tie_pair.key, tie_pair.value);
    }.bind(this));
    
    this._initializePanning();//.bind(this).defer();
    //this.initializeZoomDrag();
    this._initializeZoomBar();//.bind(this).defer();

    this._initializeMapEntityEvents("ALL");//.bind(this).defer("ALL");//VISIBLE");
    this.hide_progress();
    //this._initializeMapEntityEvents.bind(this).delay(2, "INVISIBLE");
    
    // Add a help balloon:
    //this._initializeHelpButton();
    
    if (this.firstLoad)
    {
      this.firstLoad = false;
      this.raise_event.bind(this).defer('onFirstLoaded', this.map_data);
      this.raise_event.bind(this).defer('onLoaded', this.map_data);
    }
    else
    {
      this.raise_event.bind(this).defer('onLoaded', this.map_data);
    }
  },


  mapCoordToChrom: function(map_coord)
  {
    return Math.round(this.map_data.chr_left + 
            (map_coord / this.map_data.image_width) * this.map_data.chr_range);
  },
  
  chromCoordToMap: function(chrom_coord)
  {
    return Math.round(((chrom_coord - this.map_data.chr_left) * this.map_data.image_width) /
                        this.map_data.chr_range);
  },
  
  zoom: function(direction, factor)
  {
    var old_chr_range,
        new_chr_left,
        new_chr_right;
    
    if (direction == 'in') { factor = factor * -1;}
    var added_width = this.map_data.chr_range * (1.0/this.pan_widths) * factor / 2.0,
//    var added_pixels = this.map_data.image_width * 0.2 * factor * (-1.0);// / 2.0;
        added_pixels = this.map_data.image_width * 0.35 * factor * (-1.0);// / 2.0;
    
    var map_left = this.getPanPos(),
        map_right = map_left + this.map_width;
    new_chr_left = this.mapCoordToChrom(map_left) - added_width;
    new_chr_right = this.mapCoordToChrom(map_right) + added_width;
    
    // Handle some boundary cases
    if (new_chr_left < 0) new_chr_left = 0;
    if (new_chr_right - new_chr_left > 10000000) return;
    
    // try to animate the image:
    if (direction == 'in') { added_pixels = added_pixels * 2.0; }// % in vs. % out??
    var px_width = parseInt(this.map_data.image_width) + added_pixels,
        pan_ratio = (this.getPanPos() + (this.map_width/2.0)) / (this.map_data.image_width / 2.0),
        px_shift = (added_pixels * pan_ratio)/(-2.0),
        mapImage = $(this.map_id+'image'),
        height = mapImage.getStyle('height');
    // Turn off overlays, since they won't animate:
    $(this.map_id+'selected_features_overlay').hide();
    $(this.map_id+'temp_features_overlay').hide();
    $(this.map_id+'exon_tie_overlay').hide();
    $(this.map_id+'colorBar_overlay').hide();
    // Hide any selected items:
    this.selectedItemsHash.each(function(element_id) {
      var elem = $(element_id[0]);
      if (elem) elem.setStyle({opacity: 0});
    });
    
    if (!Prototype.Browser.IE) {
      // Style dims must be correct to start:
      mapImage.setStyle({height:height,width:this.map_data.image_width+'px'});
      new Effect.Morph(mapImage, {style:'width:'+px_width+'px;height:'+
          height+';left:'+px_shift+'px', duration:0.5});
    }
    
    this.show_progress();//.bind(this).defer();

    this.resetFlankData(); // clear flanks
    this._fetchMapRegion(this.map_data.species,//bind(this).defer(this.map_data.species,
                              this.map_data.chromosome,
                              new_chr_left, new_chr_right,
                              '',//this.map_data.acc,//clear the acc, to view more
                              this.map_data.acc_type,
                              this.map_data.orientation,
                              this._setup_incoming_map.bind(this));
  },
  
  _initializePanning: function()
  {
    var pan_span = $(this.map_id+'PanSpan');
    
    if (!pan_span) {return 0;}
    
    // we'll make the img itself grabbable
    var mapImage = $(this.map_id+'image'),

    // Set limits to panning:
        min_x = -(this.map_width * (this.pan_widths - 1)),
        max_x = 0,

    // [obj, true] below means obj is grabbable:
//       drag = new Drag([[pan_span, false],[mapImage, true]], null, min_x, max_x, 0, 0,
        drag = new Drag(pan_span, mapImage, null, min_x, max_x, 0, 0,
                         false, false, null, function(y){return y;});
    
    // Set up events for drag start/stop:
    drag.setCallbacks({'mousedown': function(e){this.raise_event('panStart', e);}.bind(this),
                         'mouseup': function(e){this.raise_event('panStop', e);}.bind(this)});
    
    // Pre-fetch flank maps if nec:
    if (!this.leftFlankData)
    {
      this.preFetchFlank.bind(this).delay(3, 'left');//defer('left');
    }
    
    if (!this.rightFlankData)
    {
      this.preFetchFlank.bind(this).delay(3, 'right');//defer('right');
    }
    
    return;
  },
  
  pan: function(direction, handler)
  {
      if (!this.map_data) return;
      
      var current_range = this.map_data.chr_right - this.map_data.chr_left,
          new_chr_left,
          new_chr_right,
          need_to_fetch = true;
      if (direction == 'left')
      {
        if (this.leftFlankData)
        {
          need_to_fetch = false;
          var new_html = this.leftFlankData;
          this.leftFlankData = null;
          this.rightFlankData = this.map_html;
          this._setup_incoming_map.bind(this).defer(new_html);
        }
        else
        {
          new_chr_left = this.map_data.chr_left;
          new_chr_right = this.map_data.chr_left + (1.0/this.pan_widths) * current_range;
        }
      }
      else
      {
        if (this.rightFlankData)
        {
          need_to_fetch = false;
          var new_html = this.rightFlankData;
          this.rightFlankData = null;
          this.leftFlankData = this.map_html;
          this._setup_incoming_map.bind(this).defer(new_html);
        }
        else
        {
          new_chr_left = this.map_data.chr_right - (1.0/this.pan_widths) * current_range;
          new_chr_right = this.map_data.chr_right;
        }
      }

      if (need_to_fetch)// Didn't have the flank map already? fetch the new map then:
      {
        if (!handler) {
          this.show_progress();
          handler = this._setup_incoming_map.bind(this);
        }
        
        this._fetchMapRegion(this.map_data.species,
                              this.map_data.chromosome,
                              new_chr_left, new_chr_right,
                              this.map_data.acc,
                              this.map_data.acc_type,
                              this.map_data.orientation,
                              handler);
      }
  },
  
  preFetchFlank: function(direction)
  {
    if (direction == 'left') {
      if (!this.leftFlankData) {
        this.pan('left', function(flank_data){
          this.leftFlankData = flank_data;
        }.bind(this));
      }
    }
    else
    {
      if (!this.rightFlankData) {
        this.pan('right', function(flank_data){ 
          this.rightFlankData = flank_data;
        }.bind(this));
      }
    }
  },
  
  resetFlankData: function()
  {
    // Clear flank data:
    this.leftFlankData = null;
    this.rightFlankData = null;
  },
  
  getPanPos: function()
  {
    var panSpan = $(this.map_id+'PanSpan');
    if (!panSpan) return 0;
    var xy = panSpan.positionedOffset();
    return  -xy.left;
  },

  getMapEdgesChrPos: function()
  {
    var left = this.getPanPos();
    var right = left + this.map_width;
    return ([this.mapCoordToChrom(left),this.mapCoordToChrom(right)]);
  },
  
  clearZoomStack: function() {},// deprecated, not used
  setEventHook: function(subject, callback) // deprecated, use set_event_hook()
  {
    this.set_event_hook(subject, callback);
  },
  
  _initializeZoomBar: function()
  {
    if ($(this.map_id+'ZoomBorder')) return; // Return if done already
    
    var bar_border = $span({'id':this.map_id+'ZoomBorder','class': this.appStyle+'_map_ZoomBarBorder'});
//        style:'height:'+(this.map_data.features_bottom-30)+'px;'});
    
    this.map_header.appendChild(bar_border);
    
    var plus  = $span({'class': this.appStyle+'_map_zoom_in',
                title: 'Click to zoom in'}),
        minus = $span({'class': this.appStyle+'_map_zoom_out',
//        style:'top:'+(this.map_data.features_bottom-50)+'px;',
                title: 'Click to zoom out'}),
        handle = $span({'class': this.appStyle+'mapZoomHandle'});

    bar_border.appendChild(plus);
    bar_border.appendChild(minus);
    bar_border.appendChild(handle);
    
    minus.observe('click',function(){ this.zoom('out', 1.5); }.bind(this));
    plus.observe('click',function(){ this.zoom('in', 1.5); }.bind(this));
    
    // This line does something mystical.  Delete it and the map goes blank
    // when zooming in!
    //this.map_body.getStyle('left');
  },

  getCookieParams: function()
  {
    var params = '&show_mRNAs=' + GA_UTIL.getCookieVal("show_mRNAs") +
                 '&show_silencersiRNAs=' + GA_UTIL.getCookieVal("show_silencersiRNAs") +
                 '&show_siSelectsiRNAs=' + GA_UTIL.getCookieVal("show_siSelectsiRNAs") +
                 '&show_assays=' + GA_UTIL.getCookieVal("show_assays");
                 
    return params;
  },
  
  _elementClicked: function (element_type, element_acc)
  {
    if (element_type == "Switch")
    {
      if (element_acc == "mRNASwitch")
      {
         this.toggleGBmRNAs();
      }
      else if (element_acc == "silencersiRNASwitch")
      {
        if (GA_UTIL.getCookieVal("show_silencersiRNAs") == "yes")
        {
          this.hideSilencersiRNAs();
          this.raise_event('display_flag_set', {'silencersiRNAs': 'no'});
        }
        else
        {
          this.showSilencersiRNAs();
          this.raise_event('display_flag_set', {'silencersiRNAs': 'yes'});
        }

        this.reloadMap();
      }
      else if (element_acc == "siSelectsiRNASwitch")
      {
        if (GA_UTIL.getCookieVal("show_siSelectsiRNAs") == "allsiRNA")
        {
          this.hideAdditionalSiSelectsiRNAs();
          this.raise_event('display_flag_set', {'siSelectsiRNAs': 'topsiRNAs'});
        }
        else
        {
          this.showAdditionalSiSelectsiRNAs();
          this.raise_event('display_flag_set', {'siSelectsiRNAs': 'allsiRNAs'});
        }

        this.reloadMap();
      }
      else if (element_acc == "AssaySwitch")
      {
        if (GA_UTIL.getCookieVal("show_assays") == "allAssays")
        {
          this.hideAdditionalGeXassays();
          this.raise_event('display_flag_set', {'GeXassays': 'BestGex'});
        }
        else
        {
          this.showAdditionalGeXassays();
          this.raise_event('display_flag_set', {'GeXassays': 'allAssays'});
        }
    
        this.reloadMap();    
      }
      else
      {
        alert ("unknown Switch type clicked");
      }
    }
    else // If it wasn't a Switch, it must be a feature:
    {
      // Raise the Event with the element type as subject
      // (The subject might ought to be "alignMap_<type>_clicked"...)
      this.raise_event(element_type, element_acc);
    }
    
  }, //
  
  showAdditionalGeXassays: function()
  {
    document.cookie = "show_assays=allAssays;path=/";
  },
  
  hideAdditionalGeXassays: function()
  {
    document.cookie = "show_assays=BestGex;path=/";
  },
  
  showSilencersiRNAs: function()
  {
    document.cookie = "show_silencersiRNAs=yes;path=/";
  },
  
  hideSilencersiRNAs: function()
  {
    document.cookie = "show_silencersiRNAs=no;path=/";
  },
  
  showAdditionalSiSelectsiRNAs: function()
  {
    document.cookie = "show_siSelectsiRNAs=allsiRNA;path=/";
  },
  
  hideAdditionalSiSelectsiRNAs: function()
  {
    document.cookie = "show_siSelectsiRNAs=topsiRNA;path=/";
  },
  
  toggleGBmRNAs: function()
  {
    if (GA_UTIL.getCookieVal("show_mRNAs") == "allmRNA")
    {
      GA_UTIL.setCookieVal('show_mRNAs','refseq');
      $(this.map_id+'Switch_refSeq').checked = false;
    }
    else
    {
      GA_UTIL.setCookieVal('show_mRNAs','allmRNA');
      $(this.map_id+'Switch_refSeq').checked = true;
    }

    this.reloadMap();  
  },
  
  
  // This method plops a marker on the map (start/stop position)
  showMarker: function(name, color, chrom_pos, vert_offset)
  {
    // If it's not defined, or off the visible map, put it in the middle:
    if (!chrom_pos || chrom_pos < this.mapCoordToChrom(this.getPanPos()) || //map_data.chr_left ||
        chrom_pos > this.mapCoordToChrom(this.getPanPos()+this.map_width)) //map_data.chr_right)
    { // If they don't give us a position, put it mid-map:
      var pan_pos = this.getPanPos();
      chrom_pos = this.mapCoordToChrom(300 + pan_pos);
      // Give our caller a way to know the position we made up:
      this.raise_event('MarkerMoved', {name:name,pos:chrom_pos}); 
    }
    var markerID = this.map_id+name+'_marker';
    var markerLeft = this.chromCoordToMap(chrom_pos);
    var markerBorder= '1px solid #777';
    var markerStyle = 'position:absolute;left:'+(markerLeft)+'px;'+
              'px;top:0px;';
    var markerContainer = $span({id:markerID, 
                               style:markerStyle});
    if ($(markerID)) { $(markerID).remove(); }//remove any previously existing
    //$(this.map_id+'PanSpan').appendChild(markerContainer);
    $(this.map_id+'temp_features_overlay').appendChild(markerContainer);
    
    var boxID = this.map_id+name + '_box';
    var bottom = this.map_data.features_bottom - 15;
    if (vert_offset == null) vert_offset = 0; // Handle lack of vert_offset parm
    var boxTop = bottom - 40 + vert_offset;
    var boxLeft = -35;
    var boxStyle = 'position:absolute;top:'+boxTop+'px;left:'+boxLeft+'px;height:18px;'+
              'width:70px;border:'+markerBorder+';background:'+color+
              ';cursor:e-resize;color:#fff;padding:1px;text-align:center;'+
              'background-image:url('+this.serverpath+'images/handle.gif);'+
              'background-position:bottom;background-repeat:repeat-x;';
    var box = $span({id: boxID,
                           style: boxStyle}, ''+chrom_pos); 
    $(markerID).appendChild(box);

    var barID = this.map_id+name + '_bar';
    var barTop = 30;
    var barHeight =  boxTop - barTop;
    var barStyle = 'position:absolute;left:0px;height:'+barHeight+
              'px;top:'+barTop+'px;width:1px;border:'+markerBorder+
              ';border-bottom:none;background:'+color+
              ';cursor:e-resize;';
    var verticalBar = $span({id: barID,
                           style: barStyle}); 
    $(markerID).appendChild(verticalBar);

    var dragger = new Drag(markerContainer, null, null, null, null, 0, 0);
    dragger.setCallbacks($H({'mouseup':this.handleMarkerStop.bind(this),
                               'mousemove':this.handleMarkerMove.bind(this)}));
    // Make dragging more reliable:
    this.moving_marker_name = null;
    
    return 0;
  },
  
  handleMarkerMove: function(e)
  {
    var elem = Event.element(e);
    //var elem = event.element();
    var elem_id = elem.id;
//    var marker_name = elem_id.replace(/_[\w]*/, '');
    var marker_name = elem_id.replace(/_box/, '');
    
    // Save the moving marker name, in case the mousemove goes over mapImage:
    if (this.moving_marker_name == null && marker_name != (this.map_id+'image'))
    {
      this.moving_marker_name = marker_name;
    }
    //If the mouse slips off the marker, keep dragging the previous marker:
    if (!$(this.marker_name+'_marker'))
    {
      marker_name = this.moving_marker_name;
    }
    
    var marker_id = marker_name+'_marker';
    var marker = $(marker_id);
    
    // If we still don't have an element, just return:
    if (!marker) {return;}
    
    var marker_pos = marker.positionedOffset()[0];
    var chrom_pos = this.mapCoordToChrom(marker_pos);
    $(marker_name+'_box').innerHTML = '' + chrom_pos;
    
    return {name:marker_name, pos:chrom_pos};
  },
  
  handleMarkerStop: function(e)
  {
    var marker_data = this.handleMarkerMove(e);
    
    // Forget we're dragging this marker:
    this.moving_marker_name = null;
    
    this.raise_event('MarkerMoved', marker_data); 
  },
  
  removeMarker: function(name)
  {
    $(this.map_id+name+'_marker').innerHTML = '';
  },
  
  // This highlight is mainly for rollovers:
  _highlight: function (element_id)
  {
    if (!(element = $(element_id))) { return; }
    
    //highlightID tells it to highlight another element in addition
    if ((highlightID = element.readAttribute('highlightID')))
    {
      element.setStyle({opacity: 0.5});
      element = $(this.map_id+highlightID);
    }
    
    element.setStyle({opacity: 0.5, background:'#ee2'});

    var related = element.readAttribute('relatedIDs');
    if (related !== null)
    {
      related.split(',').each(function(elem_id){
        var elem = null;
        elem_id = this.map_id + elem_id;
        if ((elem = $(elem_id)) !== null)
          elem.setStyle({opacity: 0.5, background:'#ee2'});
      }.bind(this));
    }
    
    this.raise_event("entity_highlighted", element_id);
  },

  // This unhighlight is mainly for rollovers:
  _unHighlight: function (element_id)
  {
    var element  = $(element_id);
    
    if (element === null) { return; }
    
    //highlightID tells it to (un)highlight another element in addition
    if ((highlightID = element.readAttribute('highlightID')) !== null)
    {
      this._unHighlight(this.map_id+highlightID);
    }
    
    // Don't unHighlight *selected* items, they should stay highlighted
    if (!this.selectedItemsHash.get(element_id))
    {
      element.setStyle({opacity: 0});
    }
    else // it is selected, so keep it so
    {
      this._drawSelected(element_id);
    }
    
    
    var related = element.readAttribute('relatedIDs');
    if (related !== null)
    {
      related.split(',').each(function(elem_id){
        var elem = null;
        elem_id = this.map_id + elem_id;
        if ((elem = $(elem_id)) !== null)
        {
          if (!this.selectedItemsHash.get(elem_id))
          {
            elem.setStyle({opacity: 0});
          }
          else // it is selected, so keep it so
          {
            this._drawSelected(elem_id);
          }
        }
      }.bind(this));
    }
    
    this.raise_event("entity_unhighlighted", element_id);
  },

  // A wee wrapper:
  getFeature: function(feature_id)
  {
    return $(this.map_id+feature_id);
  },
  
  unselectAll: function()
  {
    this.selectedItemsHash.keys().each(function(key) {
        this.selectedItemsHash.unset(key);
        this._unHighlight(key);
        this._moveFeatureOverlay(key, this.map_id+'selected_features_overlay',
                                        this.map_id+'temp_features_overlay');
    }.bind(this));
        
    this.selectedItemsHash = $H({});
    this.sessionData.selectedItems = "";
    this._saveSessionData();
  },

  
  unselect: function(element_id)
  {
    element_id = this.map_id+element_id;
    
    this.selectedItemsHash.unset(element_id);
    this._unHighlight(element_id);
    this._moveFeatureOverlay(element_id, this.map_id+'selected_features_overlay',
                                          this.map_id+'temp_features_overlay');

    this._saveSessionData();
  },
  
  // Set, not toggle:
  setSelected: function (element_ids)
  {
    element_ids.each(function(element_id){
        
      element_id = this.map_id+element_id;
      
      if (this.selectedItemsHash.get(element_id))
      {
        return; // Do nothing, it's already selected
      }

      // Otherwise, select it:
      this.selectedItemsHash.set(element_id, 1);

      // If this element_id isn't on the map:
      if ($(element_id) === null)
      {
        // Try showing additional siRNAs or GeXassays:
        var switch_changed = false;
        if (element_id.match(/siRNA/))
        {
          this.showAdditionalSiSelectsiRNAs();
          this.showSilencersiRNAs();
          switch_changed = true;
        }
        else if (element_id.match(/GeXassay/))
        {
          this.showAdditionalGeXassays();
          switch_changed = true;
        }

        if (switch_changed)
          this.reloadMap();

        return;
      }
      
      this._drawSelected(element_id);
      
      // If this is selecting a temp_overlay feature, move it 
      // to the selected overlay:
      this._moveFeatureOverlay(element_id, this.map_id+'temp_features_overlay',
                                            this.map_id+'selected_features_overlay');

    }.bind(this));

    // Set a cookie to reflect the current status
    var cookie_text = Object.toJSON(this.selectedItemsHash);
    
    this.sessionData.selectedItems = cookie_text;
    this._saveSessionData();
  },


  toggleSelected: function (element_id)
  {
    element_id = this.map_id+element_id;
    
    // If this element ID is one of our selected ones...
    if (this.selectedItemsHash.get(element_id))
    {
      // Remove it and unhighlight it
      this.selectedItemsHash.unset(element_id);
      this._unHighlight (element_id);
    }
    else // If it is not selected, then make it so:
    {
      this.selectedItemsHash.set(element_id, 1);
      this._drawSelected(element_id);
    }

    // Set a cookie to reflect the current status
    var cookie_text = Object.toJSON(this.selectedItemsHash);
    
    this.sessionData.selectedItems = cookie_text;
    this._saveSessionData();
  },

  _drawSelected: function (element_id)
  {
    var element = $(element_id);

    if (element === null)
    {
      return;
    }
    
    element.setStyle({
                      border:'1px solid #777',
                      background:"#1CDBE6",//"#99ffff",
                      opacity: 0.5});
  },

  // Return an array of all of the map's selected items:
  getSelectedEntities: function()
  {
    return this.sessionData.selectedItems.keys();
  },
  
  
  addOverlayedFeatures: function (feature_list, overlay_name, purgeOld, nopulsate)
  // The structure for the feature_list is:
  // [
  //   {id:,species:,chromosome:,start:,stop:,fill_color:,border_color:},
  //   . . .
  // ]
  //
  {
    // This is a hassle (but where else to set it?):
    var feature_top = 10;//29;
    
    // We will maintain the list of elements, and redraw when we get a new map
    if (feature_list.keys().size() === 0) 
    {
      return;
    }
    
    if (overlay_name == null || overlay_name === '')
    {
      overlay_name = this.map_id+'temp_features_overlay';
    }
    
    var overlay = $(overlay_name);
    overlay.innerHTML = "";
    
    // Remove any non-selected features from the overlayFeatures:
    if (purgeOld == 'purgeOld')
    {
      this.sessionData.overlayFeatures.each(function(entry){
          if (typeof(this.selectedItemsHash.get(entry.key)) == 'undefined')
            this.sessionData.overlayFeatures.unset(entry.key);
      }.bind(this));
    }
    
    var html = '';
    var newSymbolIDs = new Array();
    
    // Draw each feature
    $H(feature_list).values().each(function(feature)
      {
        if (feature.chromosome == null || feature.chromosome == '') // If blank...
        { // then just assume it's on the current map chromosome
          feature.chromosome = this.map_data.chromosome;
        }
        if (feature.chromosome != this.map_data.chromosome)
        { // We don't do this
          return;
        }
        
        var feature_name = this.map_id + feature.type+'_'+feature.name;
        var map_left  = this.chromCoordToMap(feature.start);
        var map_right = this.chromCoordToMap(feature.stop);
        
        // Don't draw items outside the map
        if (map_left < 0) {map_left = 0;}
        if (map_right < 0) {return;}
        if (map_left > this.map_data.image_width) {return;}
        if (map_right > this.map_data.image_width) {map_right = (this.map_data.image_width - 3);}
        
        var feature_height = 5;
        var feature_width = Math.round(map_right - map_left) + 1;
        var feature_style = "position:absolute;top:" +
                    feature_top + "px;left:" + map_left +
                    "px;height:" + feature_height +
                    "px;width:" + feature_width + "px;" +
                    "background:" + feature.fill_color + 
                    ";border:1px solid "+feature.outline_color +
                    "; opacity:0.8;font-size: 0px;z-index:0;";
        var center_xoffset = Math.round(((map_right - map_left) / 2) - 3);
        var my_span = $span({
            id:    feature_name + "_shape",
            style: feature_style}
        );
        
        overlay.appendChild(my_span);
        
        var image_path = "../shared/images/target.png"; // Default
        if (feature.image_path != null && feature.image_path != '')
        {
          image_path = feature.image_path;
        }
        
        var img_id = feature_name + "_img";
        var my_img = $img({
                id:    img_id,
                style: "position:absolute;top:"+(feature_top-2)+
                       "px;left:"+(map_left+center_xoffset)+
                       "px;z-index:1;",
                src: image_path
            });
        
        overlay.appendChild(my_img);
        
        newSymbolIDs.push(img_id);
        
       var highlight_style = "position:absolute;top:" +
                    (feature_top - 3) + "px;left:" + (map_left - 3) +
                    "px;height:" + (feature_height + 6) +
                    "px;width:" + (feature_width + 6) + 
                    "px;z-index:2;cursor:pointer;";
    
        var highlight = $span({id: feature_name, title: feature.name,
                    'class':'highlight',style:highlight_style});
        
        overlay.appendChild(highlight);
        
        //var highlight = $(feature_name);
        highlight.observe('mouseover',function(event) { this._highlight(feature_name); }.bind(this)); 
        highlight.observe('mouseout',function(event) { this._unHighlight(feature_name); }.bind(this)); 
        highlight.observe('click',function(event) { this._overlayFeatureClicked(feature_name); }.bind(this)); 

        // Make it draggable via domdrag.js
        //Drag.init($("map_overlay_" + region[0]), null, 0,600,feature_top,feature_top);
        this.sessionData.overlayFeatures.set(feature_name, feature);
                                              
      }.bind(this)
    );
    
    // Draw attention to the new symbols (if there are not too many):
    var delay = 0.0; var MAX_TO_ANIMATE = 20;
    if (nopulsate != 'nopulsate' && newSymbolIDs.size() <= MAX_TO_ANIMATE)
    {
      newSymbolIDs.each(function(myImgID){
          Effect.Pulsate(myImgID, {pulses: 2, duration: 0.7, delay: delay});
          delay += 0.05;
      });
    }
    
    // Add a label for the targets (ideally passed in?):
    var label = $span({'class':'mapTierLabel', style:'top:'+(feature_top-4)+'px;'},
      'Custom Targets:');
    this.map_body.appendChild(label);

    
    // Save our list of overlay features in the session
    this._saveSessionData();
    
  },

  // This function looks up the overlay feature and sends it in the event
  _overlayFeatureClicked: function(feature_name)
  {
    var dater = this.sessionData.overlayFeatures.get(feature_name);
    this.raise_event('OverlayFeatureClicked',
                     this.sessionData.overlayFeatures.get(feature_name));
    
    // If they click on a temp_overlay feature, move it 
    // to the selected overlay:
    if ($(feature_name).childOf(this.map_id+'temp_features_overlay'))
    {
      this._moveFeatureOverlay(feature_name, this.map_id+'temp_features_overlay',
                                              this.map_id+'selected_features_overlay');
    }
    else if ($(feature_name).childOf(this.map_id+'selected_features_overlay')) 
    {
      this._moveFeatureOverlay(feature_name, this.map_id+'selected_features_overlay',
                                              this.map_id+'temp_features_overlay');
    }
    
  },
  
  _moveFeatureOverlay: function(feature_name, from_overlay, to_overlay)
  {
    if ($(feature_name) == null) return;
    // Move the features from one overlay to the other:
    if ($(feature_name).childOf(from_overlay))
    {
      $(to_overlay).appendChild($(feature_name).remove());
      $(to_overlay).appendChild($(feature_name + "_shape").remove());
      $(to_overlay).appendChild($(feature_name + "_img").remove());
    }
  },

  
  showColorBar: function (caption, left, right, colors)
  {
    colors = $A(colors);
    var chrom_width = right - left;
    var bar_start = this.chromCoordToMap(left);
    var bar_end   = this.chromCoordToMap(right);
    var bar_top   = this.map_data.features_bottom;
    var caption_top=bar_top - 22;
    var bar_width = bar_end - bar_start;
    var div_width = bar_width / colors.length;
    var index = 0;
    var colorBar_container = $(this.map_id+'colorBar_overlay'); 
    colorBar_container.innerHTML = '';
    
    var caption_span = $span({'class':'mapTierLabel', style:'top:'+caption_top+
    'px;'},  caption);
    this.map_body.appendChild(caption_span);
    
    colors.each(function (color){
     
     var bar_left = bar_start + (index * div_width);
     var highlight_style = "position:absolute;top:" +
                  (bar_top - 7) + "px;left:" + bar_left +
                  "px;height:4px;width:" + div_width + 
                  "px;z-index:2;font-size:2px;background:#"+color+";";
                  
     var color_block = $span({
         style:        highlight_style
         });
      
      colorBar_container.appendChild(color_block);
      
      index++;
    });
  },
  
  restorePreviousMapAndSelections: function ()
  {
  // If the user has session data for selected items, load them up again on reload:
    var sessionDataString = this.sessionPtr.getParam("alignMapSessionData");
    if (sessionDataString !== null && sessionDataString !== '')
    {
      this.sessionData = sessionDataString.evalJSON();
      
      // Upgrade gracefully:
      // Add exon_ties hash to any old sessionData:
      if (this.sessionData.exon_ties == null) {this.sessionData.exon_ties = $H({});}
    }
    // Prototype-ize this
    this.sessionData.overlayFeatures = $H(this.sessionData.overlayFeatures);
    
    // Whoa, JSON inside of JSON:
    var selected_items = this.sessionData.selectedItems;
    if (selected_items !== null && selected_items !== "")
    {
      this.selectedItemsHash = $H(selected_items.evalJSON());
    }
    
      // Remove unselected features
    this.sessionData.overlayFeatures.values().each(function(feature){
      var feature_id = feature.type+'_'+feature.name;
      if (!this.selectedItemsHash.get(feature_id))
      {
        this.sessionData.overlayFeatures.unset(feature_id);
      }
    }.bind(this));

  },
  
  _tie_exons: function(transcript_id, exon_ids)
  {
    // Get the existing tie on this transcript:
    var tied_exons = $H(this.sessionData.exon_ties.get(transcript_id));

    // Here's a wrinkle: singles should toggle, multiples should not:
    if (exon_ids.size() == 1)
    {
      // Is it already tied?
      if (tied_exons.get(exon_ids.first()) != null) {
        // If so, toggle it off, store the list, and we're done:
        tied_exons.unset(exon_ids.first());
        this.sessionData.exon_ties.set(transcript_id, tied_exons);
        return;
      }
    }

    exon_ids.each(function(exon_id){
      var exon = $(this.map_id+exon_id);
  
      // Handle exon IDs without the prefix too:
      if (exon == null) { exon_id = 'Exon_'+exon_id; exon = $(this.map_id+exon_id);}
      if (exon == null) return; // If it wasn't found, give up
      
      // Figure out the exon's chrom pos:
      var exon_pos = exon.positionedOffset();
      var exon_width = exon.getWidth();
      var exon_map_x = exon_pos[0] + exon_width / 2.0;
      // Get this exon's midpoint's chrom pos:
      var exon_chrom_x = this.mapCoordToChrom(exon_map_x);
      //alert('exon mid x is '+exon_chrom_x);
      
      tied_exons.set(exon_id, exon_chrom_x);
      
    }.bind(this));
    
    // Set the exons as selected:
    this.setSelected(exon_ids);
      
    // Store the list
    this.sessionData.exon_ties.set(transcript_id, tied_exons);

  },
  
  // tie_exon will draw a bracket between exons
  tie_exon: function(exon_id)
  {
    var matches = exon_id.match(/Exon_([\w]+)\.(\d)_exon_([\d]+)/);
    if (!matches) throw 'malformed exon ID:'+exon_id;

    var transcript_id = matches[1] + '.' + matches[2];
    var this_exon_no = matches[3];

    // A list of the exons involved:
    var exons = [];
    
    // If the shift key is down, try to select a RANGE of exons:
    if (this.shiftKeyPressed && this.last_exon_clicked != null) {
      var matches = this.last_exon_clicked.match(/Exon_([\w]+)\.(\d)_exon_([\d]+)/);
      if (!matches) { throw 'malformed exon ID'+this.last_exon_clicked; }
      
      var last_trans = matches[1] + '.' + matches[2];
      var last_exon_no = matches[3];

      if (last_trans == transcript_id) {
        // figure out the exon range
        var two_exons = [parseInt(last_exon_no), parseInt(this_exon_no)];
        var id_base_str = 'Exon_'+transcript_id+'_exon_';
        // add each exon in the range:
        $R(two_exons.min(), two_exons.max()).each(function(this_exon_id){
          exons.push(id_base_str + this_exon_id);
        });
      }
      else // not same transcript, so treat as individual click
      {
        exons.push(exon_id);
      }
    }
    else // no shift-click, so treat as individual click
    {
      exons.push(exon_id);
    }
    
    // Now store the selected exon(s):
    this._tie_exons(transcript_id, exons); // encapsulates exon calculations 
    
    // Draw the tie bracket:
    this.draw_exon_tie(transcript_id);
    
    // Remember the last exon clicked, in case the next click has 'shift' on
    this.last_exon_clicked = exon_id;
  },
  
  draw_exon_tie: function(transcript_id)
  {
    var tied_exons = $H(this.sessionData.exon_ties.get(transcript_id));
    
    var tie_span_id = this.map_id+transcript_id+'_exon_tie';
    var tie_span = $(tie_span_id);
    // If this transcript has no tie span, create one and append it:
    if (tie_span == null) {
      tie_span = $span({id: tie_span_id,
                       'class': 'mapExonTie',
                        title: 'Click to accept these target exons'});
      $(this.map_id+'exon_tie_overlay').appendChild(tie_span);
      tie_span.observe('click', this.select_exon_tie.bind(this));
      tie_span.observe('mouseover', this.exon_tie_mouseover.bind(this));
      tie_span.observe('mouseout', this.exon_tie_mouseout.bind(this));
    }
    
    if (tied_exons.keys().size() == 0) { // Handle zero exons:
      tie_span.remove();
      return;
    }
    
    // Draw bracket over the exon(s):
    var bracket_height = 5; // px
    var bracket_topmargin = 3;
    var tie_html = '';
    
    // Calculate bracket top
    var trans = $(this.map_id+'Trans_'+transcript_id);
    if (!trans) return;//If trans is off the map, don't try to draw
    var trans_pos = trans.positionedOffset();
    var bracket_top = (trans_pos[1]-bracket_height) - bracket_topmargin;

    var far_left=null;
    var far_right=null;
    
    if (tied_exons.keys().size() == 1) { // Single exon:
      
      var exon_id = tied_exons.keys().first();// and only
      
      var exon = $(this.map_id+exon_id);
      if (!exon) return;// exon off the screen? nothing to draw
      var exon_pos = exon.positionedOffset();
      var exon_width = exon.getWidth();
      
      bracket_top = (exon_pos[1]-bracket_height) - bracket_topmargin;
      far_left = exon_pos[0] - 1;
      far_right = far_left + exon_width;
      
      tie_html += '<span id="'+exon_id+'_tie" class="mapExonTieBracket" style="left:0px;'+
                 'top:'+bracket_topmargin+
                 'px;width:'+exon_width+'px;height:'+bracket_height+'px;"></span>';
    } else { // If there's > 1 exon, tie them in pairs:
    
      var left_exon_id = null;
      
      // Iterate through the exons, sorted by chrom pos (which is the hash value):
      tied_exons.keys().sort(function(key_a, key_b){
             return tied_exons.get(key_a) - tied_exons.get(key_b);
      }).each(function(exon_id){
        if (left_exon_id == null) {
          
          left_exon_id = exon_id;
          var left_exon = $(this.map_id+left_exon_id);
          
          if (!left_exon) { // calculate the left pos
            var exon_chrom_pos = tied_exons.get(exon_id);
            far_left = this.chromCoordToMap(exon_chrom_pos);
            //If the left exon is off the screen:
            if (far_left < 0)  far_left = 0;
          } 
          else // Calculate top/left of whole bracket based on left exon:
          {
            var left_pos = left_exon.positionedOffset();
            far_left = left_pos[0] + left_exon.getWidth() / 2.0;
          }
          
          return;
        }

        // Figure out bracket_left:
        var left_exon = $(this.map_id+left_exon_id);
        var bracket_left;

        if (left_exon == null)
        { // If the left exon is off the screen, use far_left:
          bracket_left = far_left;
          if (bracket_left > this.map_width) return;// ALL off to right
        }
        else // if on the screen, get its midpoint:
        {
          var left_width = left_exon.getWidth();
          var left_pos = left_exon.positionedOffset();
          bracket_left = left_pos[0] + left_width / 2.0;
        }
        
        // Figure out bracket_right:
        var right_exon = $(this.map_id+exon_id);
        var bracket_right;
        
        if (!right_exon) { // off the screen, but left or right?
          var exon_chrom_pos = tied_exons.get(exon_id);
          bracket_right = this.chromCoordToMap(exon_chrom_pos);
          if (bracket_right < 0) return;// off to left, nothing to draw
          if (bracket_right > this.map_width) bracket_right = this.map_width - 1; // always true?
        }
        else // if one the screen, get its midpoint:
        {
          var right_width = right_exon.getWidth();
          var right_pos = right_exon.positionedOffset();
          bracket_right = right_pos[0] + right_width / 2.0;
        }
        var bracket_width = (bracket_right - bracket_left) - 1;
        tie_html += '<span id="'+this.map_id+left_exon_id+'_tie" class="mapExonTieBracket" style="cursor:pointer;font-size:0px;left:'+
                   (bracket_left-far_left) + 'px;top:'+bracket_topmargin+
                  'px;width:'+bracket_width+'px;height:'+bracket_height+'px;"></span>';
        
        left_exon_id = exon_id;
        far_right = bracket_right;
      }.bind(this));
    }
    
    tie_span.innerHTML = tie_html;
    // Set the extents of the bracket container:
    tie_span.setStyle({top: bracket_top+'px', left: far_left+'px', 
        width: (far_right - far_left)+'px',height: (bracket_height+bracket_topmargin)+'px'});
  },
  
  select_exon_tie: function(event)
  {
    var tie_id = event.element().up().id;
    // Sometimes they click on the tie, sometimes the bracket, so deal with it:
    if (tie_id == (this.map_id+'exon_tie_overlay')) { tie_id = event.element().id;}
    
    // strip this off the end:
    var transcript_id = tie_id.replace(/_exon_tie/,'');
    // strip the map_id off the front:
    transcript_id = transcript_id.replace(new RegExp(this.map_id), '');
    var tied_exon_ids = this.sessionData.exon_ties.get(transcript_id);
    
    // Extract the exon numbers (ick!):
    var tied_exons = tied_exon_ids.collect(function(exon_id){
        var matches = exon_id[0].match(/_exon_([\d]+)/);
        return matches[1];
    }.bind(this));
    
    var tie_data = $H({});
    tie_data.set(transcript_id, tied_exons);
    this.raise_event('ExonTieClicked', tie_data);
  },
  
  clear_exon_tie: function(transcript_id)
  {
    var tied_exon_ids = this.sessionData.exon_ties.get(transcript_id);
    
    // De-select the exons:
    var tied_exons = tied_exon_ids.collect(function(exon_id){
        this.unselect(exon_id[0]);
    }.bind(this));
    
    // Clear out this exon_tie set:
    this.sessionData.exon_ties.set(transcript_id, $H({}));
    this.draw_exon_tie(transcript_id, $H({}));
  },
  
  exon_tie_mouseover: function(event)
  {
    var tie_id = event.element().up().id;
    
    // Sometimes they click on the tie, sometimes the bracket, so deal with it:
    if (tie_id == (this.map_id+'exon_tie_overlay')) { tie_id = event.element().id;}
    
    $(tie_id).setStyle({background:'#ff8'});//, border:'1px solid #ccc'});
  },
  
  exon_tie_mouseout: function(event)
  {
    var tie_id = event.element().up().id;
    
    // Sometimes they click on the tie, sometimes the bracket, so deal with it:
    if (tie_id == (this.map_id+'exon_tie_overlay')) { tie_id = event.element().id;}
    
    $(tie_id).setStyle({background:'#fff'});//, border:'none'});
  },
  
  // It is hoped that this event method will manage memory better than static HTML:
  // vis_or_invis used to initialize visible/invis separately
  _initializeMapEntityEvents: function(vis_or_invis)
  {
    var types = ['Gene','GeXassay','GTassay','SNP','Exon','Trans','CNVA','DGV','siRNA','miRNA','miRNAAssay','Mature'];

    types.each(function(entity_type) {
        var entities = this.getEntityIds(entity_type, vis_or_invis);
        //if (MAP_DEBUG) { console.log(entity_type + entities.size()); }
        entities.each(function(entity_id) {
            var element_id = this.map_id + entity_type + '_' + entity_id;
            var entity = $(element_id);
            entity.stopObserving();// clear any existing observers
            entity.observe('mouseover',function(event) { this._highlight(element_id); }.bind(this)); 
            entity.observe('mouseout',function(event) { this._unHighlight(element_id); }.bind(this));
            entity.observe('click',function(event) { this._elementClicked(entity_type, entity_id); }.bind(this)); 
        }.bind(this));
    }.bind(this));

  },
  
  displaySwitches: function()
  {
    //if (this.app_name
  }
  
});// End of AlignMap plugin

