<commit_before>/*
 * Copyright [1999-2014] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.ImageMap = Ensembl.Panel.Content.extend({
  constructor: function (id, params) {
    this.base(id, params);
    
    this.dragging         = false;
    this.clicking         = true;
    this.dragCoords       = {};
    this.dragRegion       = {};
    this.highlightRegions = {};
    this.areas            = [];
    this.draggables       = [];
    this.speciesCount     = 0;
    this.minImageWidth    = 500;
    
    function resetOffset() {
      delete this.imgOffset;
    }
    
    Ensembl.EventManager.register('highlightImage',     this, this.highlightImage);
    Ensembl.EventManager.register('mouseUp',            this, this.dragStop);
    Ensembl.EventManager.register('hashChange',         this, this.hashChange);
    Ensembl.EventManager.register('changeFavourite',    this, this.changeFavourite);
    Ensembl.EventManager.register('imageResize',        this, function () { if (this.xhr) { this.xhr.abort(); } this.getContent(); });
    Ensembl.EventManager.register('windowResize',       this, resetOffset);
    Ensembl.EventManager.register('ajaxLoaded',         this, resetOffset); // Adding content could cause scrollbars to appear, changing the offset, but this does not fire the window resize event
    Ensembl.EventManager.register('changeWidth',        this, function () { this.params.updateURL = Ensembl.updateURL({ image_width: false }, this.params.updateURL); Ensembl.EventManager.trigger('queuePageReload', this.id); });
    Ensembl.EventManager.register('highlightAllImages', this, function () { if (!this.align) { this.highlightAllImages(); } });
  },
  
  init: function () {
    var panel   = this;
    var species = {};
    
    this.base();
    
    this.imageConfig        = $('input.image_config', this.el).val();
    this.lastImage          = Ensembl.images.total > 1 && this.el.parents('.image_panel')[0] === Ensembl.images.last;
    this.hashChangeReload   = this.lastImage || $('.hash_change_reload', this.el).length;
    this.zMenus             = {};
    
    this.params.highlight   = (Ensembl.images.total === 1 || !this.lastImage);
    
    this.elLk.container     = $('.image_container',   this.el);
    this.elLk.drag          = $('.drag_select',       this.elLk.container);
    this.elLk.map           = $('map',                this.elLk.container);
    this.elLk.areas         = $('area',               this.elLk.map);
    this.elLk.exportMenu    = $('.iexport_menu',      this.elLk.container).appendTo('body').css('left', this.el.offset().left);
    this.elLk.resizeMenu    = $('.image_resize_menu', this.elLk.container).appendTo('body').css('left', this.el.offset().left);
    this.elLk.img           = $('img.imagemap',       this.elLk.container);
    this.elLk.hoverLabels   = $('.hover_label',       this.elLk.container);
    this.elLk.boundaries    = $('.boundaries',        this.elLk.container);
    this.elLk.toolbars      = $('.image_toolbar',     this.elLk.container);
    this.elLk.popupLinks    = $('a.popup',            this.elLk.toolbars);

    this.vertical = this.elLk.img.hasClass('vertical');
    this.multi    = this.elLk.areas.hasClass('multi');
    this.align    = this.elLk.areas.hasClass('align');
    
    this.makeImageMap();
    this.makeHoverLabels();
    this.initImageButtons()
    
    if (!this.vertical) {
      this.makeResizable();
    }
    
    species[this.id] = this.getSpecies();
    $.extend(this, Ensembl.Share);
    this.shareInit({ species: species, type: 'image', positionPopup: this.positionToolbarPopup });
    
    if (this.elLk.boundaries.length) {
      Ensembl.EventManager.register('changeTrackOrder', this, this.externalOrder);
      
      if (this.elLk.img[0].complete) {
        this.makeSortable();
      } else {
        this.elLk.img.on('load', function () { panel.makeSortable(); });
      }
    }
    
    if (typeof FileReader !== 'undefined') {
      this.dropFileUpload();
    }
    
    $('a',         this.elLk.toolbars).helptip({ track: false });
    $('a.iexport', this.elLk.toolbars).data('popup', this.elLk.exportMenu);
    $('a.resize',  this.elLk.toolbars).data('popup', this.elLk.resizeMenu);
    
    this.elLk.popupLinks.on('click', function () {
      var popup = $(this).data('popup');
      
      panel.elLk.popupLinks.map(function () { return ($(this).data('popup') || $()).filter(':visible')[0]; }).not(popup).hide();
      
      if (popup && !popup.hasClass('share_page')) {
        panel.positionToolbarPopup(popup, this).toggle();
      }
      
      popup = null;
      
      return false;
    });

    $('a.image_resize', this.elLk.resizeMenu).on('click', function () {
      if (!$(this).has('.current').length) {
        panel.resize(parseInt($(this).text(), 10) || Ensembl.imageWidth());
      }
      
      return false;
    });
  },

  initImageButtons: function() {
    var panel = this;

    this.el.find('._reset').on('click', function(e) {
      e.preventDefault();
      $.ajax({
        context: panel,
        url: this.href,
        type: 'post',
        success: function() {
          this.getContent();
        },
        data: {
          image_config: panel.imageConfig
        }
      });
    });
  },

  hashChange: function (r) {
    var reload = this.hashChangeReload;
    
    this.params.updateURL = Ensembl.urlFromHash(this.params.updateURL);
    
    if (Ensembl.images.total === 1) {
      this.highlightAllImages();
    } else if (!this.multi && this.highlightRegions[0]) {
      var range = this.highlightRegions[0][0].region.range;
      r = r.split(/\W/);
      
      if (parseInt(r[1], 10) < range.start || parseInt(r[2], 10) > range.end || range.chr !== r[0]) {
        reload = true;
      }
    }
    
    if (reload) {
      this.base();
    }
    
    if (this.align) {
      Ensembl.EventManager.trigger('highlightAllImages');
    }
  },
  
  getContent: function () {
    // If the panel contains an ajax loaded sub-panel, this function will be reached before ImageMap.init has been completed.
    // Make sure that this doesn't cause an error.
    if (this.imageConfig) {
      this.elLk.exportMenu.add(this.elLk.labelLayers).add(this.elLk.hoverLayers).add(this.elLk.resizeMenu).remove();
    
      for (var id in this.zMenus) {
        Ensembl.EventManager.trigger('destroyPanel', id);
      }
   
      this.removeShare();
    }
    
    this.base.apply(this, arguments);
    
    this.xhr.done(function (html) {
      if (!html) {
        delete Ensembl.images[this.imageNumber];
        Ensembl.EventManager.trigger('highlightAllImages');
      }
    });
  },
  
  makeImageMap: function () {
    var panel = this;
    
    var highlight = !!(window.location.pathname.match(/\/Location\//) && !this.vertical);
    var rect      = [ 'l', 't', 'r', 'b' ];
    var speciesNumber, c, r, start, end, scale;
    
    this.elLk.areas.each(function () {
      c = { a: this };
      
      if (this.shape && this.shape.toLowerCase() !== 'rect') {
        c.c = [];
        $.each(this.coords.split(/[ ,]/), function () { c.c.push(parseInt(this, 10)); });
      } else {
        $.each(this.coords.split(/[ ,]/), function (i) { c[rect[i]] = parseInt(this, 10); });
      }
      
      panel.areas.push(c);
      
      if (this.className.match(/drag/)) {
        // r = [ '#drag', image number, species number, species name, region, start, end, strand ]
        r        = c.a.href.split('|');
        start    = parseInt(r[5], 10);
        end      = parseInt(r[6], 10);
        scale    = (end - start + 1) / (this.vertical ? (c.b - c.t) : (c.r - c.l)); // bps per pixel on image
        
        c.range = { chr: r[4], start: start, end: end, scale: scale, vertical: this.vertical };
        
        panel.draggables.push(c);
        
        if (highlight === true) {
          r = this.href.split('|');
          speciesNumber = parseInt(r[1], 10) - 1;
          
          if (panel.multi || !speciesNumber) {
            if (!panel.highlightRegions[speciesNumber]) {
              panel.highlightRegions[speciesNumber] = [];
              panel.speciesCount++;
            }
            
            panel.highlightRegions[speciesNumber].push({ region: c });
            panel.imageNumber = parseInt(r[2], 10);
            
            Ensembl.images[panel.imageNumber] = Ensembl.images[panel.imageNumber] || {};
            Ensembl.images[panel.imageNumber][speciesNumber] = [ panel.imageNumber, speciesNumber, parseInt(r[5], 10), parseInt(r[6], 10) ];
          }
        }
      }
    });
    
    if (Ensembl.images.total) {
      this.highlightAllImages();
    }
    
    this.elLk.drag.on({
      mousedown: function (e) {

        if (!e.which || e.which === 1) { // Only draw the drag box for left clicks.
          panel.dragStart(e);
        }
        
        return false;
      },
      mousemove: function(e) {

        if (panel.dragging !== false) {
          return;
        }
        
        var area = panel.getArea(panel.getMapCoords(e));
        var tip;

        // change the cursor to pointer for clickable areas
        $(this).toggleClass('drag_select_pointer', !(!area || $(area.a).hasClass('label') || $(area.a).hasClass('drag') || $(area.a).hasClass('hover')));

        // Add helptips on navigation controls in multi species view
        if (area && area.a && $(area.a).hasClass('nav')) {
          if (tip !== area.a.alt) {
            tip = area.a.alt;
            
            if (!panel.elLk.navHelptip) {
              panel.elLk.navHelptip = $('<div class="ui-tooltip helptip-bottom"><div class="ui-tooltip-content"></div></div>');
            }
            
            panel.elLk.navHelptip.children().html(tip).end().appendTo('body').position({
              of: { pageX: panel.imgOffset.left + area.l + 10, pageY: panel.imgOffset.top + area.t - 48, preventDefault: true }, // fake an event
              my: 'center top'
            });
          }
        } else {
          if (panel.elLk.navHelptip) {
            panel.elLk.navHelptip.detach().css({ top: 0, left: 0 });
          }
        }
      },
      mouseleave: function(e) {
        if (e.relatedTarget) {

          if (panel.elLk.navHelptip) {
            panel.elLk.navHelptip.detach();
          }

        }
      },
      click: function (e, e2) {
        if (panel.clicking) {
          panel.makeZMenu(e2 || e, panel.getMapCoords(e2 || e));
        } else {
          panel.clicking = true;
        }
      }
    });
  },
  
  makeHoverLabels: function () {
    var panel = this;

    this.elLk.labelLayers = $();
    this.elLk.hoverLayers = $();

    $.each(this.areas, function() {

      if (!this.a) {
        return;
      }

      var $a = $(this.a);

      if ($a.hasClass('label')) {
        var hoverLabel = panel.elLk.hoverLabels.filter('.' + this.a.className.replace(/label /, ''));

        if (hoverLabel.length) {

          // add a div layer over the label, and append the hover menu to the layer. Hover menu toggling is controlled by CSS.
          panel.elLk.labelLayers = panel.elLk.labelLayers.add(
            $('<div class="label_layer">').append('<div class="label_layer_bg">').append(hoverLabel).appendTo(document.body).data({area: this})
          );
        }

        hoverLabel = null;

      } else if ($a.hasClass('hover')) {

        panel.elLk.hoverLayers = panel.elLk.hoverLayers.add(
          $('<div class="hover_layer">').appendTo(document.body).data({area: this}).on('click', function(e) {
            panel.clicking = true;
            panel.elLk.drag.triggerHandler('click', e);
          }
        ));
      }

      $a = null;
    });

    // apply css positions to the hover layers
    this.positionLayers();

    this.elLk.hoverLabels.each(function() {

      // position hover menus to the right of the layer and init the tab styled icons inside the hover menus
      $(this).css('left', function() { return $(this.parentNode).width(); }).find('._hl_icon').tabs($(this).find('._hl_tab'));

    // init config tab, fav icon and close icon
    }).find('a.config').on('click', function () {
      var config  = this.rel;
      var update  = this.href.split(';').reverse()[0].split('='); // update = [ trackId, renderer ]
      var fav     = '';
      var $this   = $(this);

      if ($this.hasClass('favourite')) {
        fav = $this.hasClass('selected') ? 'off' : 'on';
        Ensembl.EventManager.trigger('changeFavourite', update[0], fav === 'on');
      } else {
        $this.parents('.label_layer').addClass('hover_label_spinner');
      }

      $.ajax({
        url: this.href + fav,
        dataType: 'json',
        success: function (json) {
          if (json.updated) {
            Ensembl.EventManager.triggerSpecific('changeConfiguration', 'modal_config_' + config, update[0], update[1]);
            Ensembl.EventManager.trigger('reloadPage', panel.id);
          }
        }
      });
      
      $this = null;

      return false;
    }).end().find('input._copy_url').on('click focus blur', function(e) {
      $(this).val(this.defaultValue).select().parents('.label_layer').toggleClass('hover', e.type !== 'blur');
    });
  },

  positionLayers: function() {
    var offset = this.elLk.img.offset();
    var right  = this.draggables[0].l; // label ends where the drag region starts

    this.elLk.labelLayers.each(function() {
      var $this = $(this);
      var area  = $this.data('area');

      $this.css({
        left:   offset.left + area.l,
        top:    offset.top + area.t,
        height: area.b - area.t,
        width:  right - area.l
      });

      area = $this = null;
    });

    this.elLk.hoverLayers.each(function() {
      var $this = $(this);
      var area  = $this.data('area');

      $this.css({
        left:   offset.left + area.l,
        top:    offset.top + area.t,
        height: area.b - area.t,
        width:  area.r - area.l
      });

      area = $this = null;
    });
  },
  
  makeResizable: function () {
    var panel = this;
    
    function resizing(e, ui) {
      panel.imageResize = Math.floor(ui.size.width / 100) * 100; // The image_container has a border, which causes ui.size.width to increase by the border width.
      resizeHelptip.apply(this, [ ui.helper ].concat(e.type === 'resizestart' ? [ 'Drag to resize', e.pageY ] : panel.imageResize + 'px'));
    }
    
    function resizeHelptip(el, content, y) {
      if (typeof y === 'number') {
        el.data('y', y);
      } else {
        y = el.data('y');
      }
      
      el.html('<div class="bg"></div><div class="ui-tooltip"><div class="ui-tooltip-content"></div></div>').find('.ui-tooltip-content').html(content).parent().css('top', function () {
        return y - el.offset().top - $(this).outerHeight(true) / 2;
      });
      
      el = null;
    }
    
    this.elLk.container.resizable({
      handles: 'e',
      grid:    [ 100, 0 ],
      minWidth: this.minImageWidth,
      maxWidth: $(window).width() - this.el.offset().left,
      helper:   'image_resize_overlay',
      start:    resizing,
      resize:   resizing,
      stop:     function (e, ui) {
        if (ui.originalSize.width === ui.size.width) {
          $(this).css({ width: panel.imageResize, height: '' });
        } else {
          panel.resize(panel.imageResize);
        }
      }
    });
  },
  
  makeSortable: function () {
    var panel      = this;
    var wrapperTop = $('.boundaries_wrapper', this.el).position().top;
    var ulTop      = this.elLk.boundaries.position().top + wrapperTop - (Ensembl.browser.ie7 ? 3 : 0); // IE7 reports li.position().top as 3 pixels higher than other browsers, so offset that here.
    var lis        = []; // just a throwaway list to allocate areas to their respective tracks

    this.elLk.boundaries.children().each(function (i) {
      var li  = $(this);
      var t   = li.position().top + ulTop;
      var ref = []; // reference for array containing areas for a track that will be populated later

      li.data({ areas: ref, position: i, top: li.offset().top });
      lis.push({ top: Math.floor(t), bottom: Math.ceil(t + li.height()), areas: ref });

      li = null;
    });

    $.each(this.areas, function () {

      assignArea:
      for (var i = 0; i <= 10; i++) { // this is to overcome an apparent drawing code bug that areas sometimes are not completely enclosed inside a track's li
        for (var j = lis.length - 1; j >= 0; j--) {
          if (lis[j].top <= this.t + i && lis[j].bottom >= this.b - i) {
            lis[j].areas.push(this);
            break assignArea;
          }
        }
      }
    });

    this.elLk.boundaries.each(function () {
      $(this).data('updateURL', '/' + this.className.split(' ')[0] + '/Ajax/track_order');
    }).sortable({
      axis:   'y',
      handle: 'div.handle',
      revert: 200,
      helper: 'clone',
      placeholder: 'placeholder',
      start: function (e, ui) {
        panel.sortStart(e, ui);
      },
      stop: function (e, ui) {
        panel.sortStop(e, ui);
      },
      update: function (e, ui) {
        panel.sortUpdate(e, ui);
      }
    }).css('visibility', 'visible').find('div.handle').on({
      mousedown: function() {
        $(this.parentNode).stop().animate({opacity: 0.8}, 200);
      },
      mouseup: function() {
        $(this.parentNode).stop().animate({opacity: 1}, 200);
      }
    });
  },

  sortStart: function (e, ui) {

    // make the placeholder similar to the actual track but slightly faded so the saturated background colour beneath gives it a highlighted effect
    ui.placeholder.css({
      backgroundImage:     ui.item.css('backgroundImage'),
      backgroundPosition:  ui.item.css('backgroundPosition'),  // Firefox
      backgroundPositionY: ui.item.css('backgroundPositionY'), // IE (Chrome works with either)
      height:              ui.item.height(),
      opacity:             0.8
    }).html(ui.item.html()).addClass(ui.item.prop('className'));

    // add some transparency to the helper (already a clone of actual track) that moves with the mouse
    ui.helper.stop().css({opacity: 0.8}).addClass('helper');

    // css deals with the rest of the things
    $(document.body).addClass('track-reordering');

    this.dragging = true;
  },

  sortStop: function (e, ui) {
    ui.item.stop().animate({opacity: 1}, 200);
    $(document.body).removeClass('track-reordering');
    this.dragging = false;
  },

  sortUpdate: function(e, ui) {

    this.positionAreas();
    this.positionLayers();
    this.removeShare();
    Ensembl.EventManager.trigger('removeShare');

    var prev  = (ui.item.prev().prop('className') || '').replace(' ', '.');
    var track = ui.item.prop('className').replace(' ', '.');

    Ensembl.EventManager.triggerSpecific('changeTrackOrder', 'modal_config_' + this.id.toLowerCase(), track, prev);

    this.saveSortOrder(ui.item.parent().data('updateURL'), track, prev);
  },

  externalOrder: function(trackId, prevTrackId) {
    var track = this.elLk.boundaries.find('li.' + trackId);
    var prev  = prevTrackId ? this.elLk.boundaries.find('li.' + prevTrackId) : false;

    if (!track) {
      return;
    }

    if (prev && prev.length) {
      track.insertAfter(prev);
    } else {
      track.parent().prepend(track);
    }

    this.saveSortOrder(track.parent().data('updateURL'), trackId, prevTrackId || '');

    track = prev = null;
  },

  saveSortOrder: function(url, track, prev) {
    $.ajax({
      url: url,
      type: 'post',
      data: {
        image_config: this.imageConfig,
        track: track,
        prev: prev
      }
    });
  },

  positionAreas: function () {
    var tracks = this.elLk.boundaries.children();

    tracks.each(function (i) {
      var li = $(this);
      var top, move;

      if (i !== li.data('position')) {
        top  = li.offset().top;<commit_msg>Reposition areas and layers aftr externalOrder too<commit_after>/*
 * Copyright [1999-2014] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

Ensembl.Panel.ImageMap = Ensembl.Panel.Content.extend({
  constructor: function (id, params) {
    this.base(id, params);
    
    this.dragging         = false;
    this.clicking         = true;
    this.dragCoords       = {};
    this.dragRegion       = {};
    this.highlightRegions = {};
    this.areas            = [];
    this.draggables       = [];
    this.speciesCount     = 0;
    this.minImageWidth    = 500;
    
    function resetOffset() {
      delete this.imgOffset;
    }
    
    Ensembl.EventManager.register('highlightImage',     this, this.highlightImage);
    Ensembl.EventManager.register('mouseUp',            this, this.dragStop);
    Ensembl.EventManager.register('hashChange',         this, this.hashChange);
    Ensembl.EventManager.register('changeFavourite',    this, this.changeFavourite);
    Ensembl.EventManager.register('imageResize',        this, function () { if (this.xhr) { this.xhr.abort(); } this.getContent(); });
    Ensembl.EventManager.register('windowResize',       this, resetOffset);
    Ensembl.EventManager.register('ajaxLoaded',         this, resetOffset); // Adding content could cause scrollbars to appear, changing the offset, but this does not fire the window resize event
    Ensembl.EventManager.register('changeWidth',        this, function () { this.params.updateURL = Ensembl.updateURL({ image_width: false }, this.params.updateURL); Ensembl.EventManager.trigger('queuePageReload', this.id); });
    Ensembl.EventManager.register('highlightAllImages', this, function () { if (!this.align) { this.highlightAllImages(); } });
  },
  
  init: function () {
    var panel   = this;
    var species = {};
    
    this.base();
    
    this.imageConfig        = $('input.image_config', this.el).val();
    this.lastImage          = Ensembl.images.total > 1 && this.el.parents('.image_panel')[0] === Ensembl.images.last;
    this.hashChangeReload   = this.lastImage || $('.hash_change_reload', this.el).length;
    this.zMenus             = {};
    
    this.params.highlight   = (Ensembl.images.total === 1 || !this.lastImage);
    
    this.elLk.container     = $('.image_container',   this.el);
    this.elLk.drag          = $('.drag_select',       this.elLk.container);
    this.elLk.map           = $('map',                this.elLk.container);
    this.elLk.areas         = $('area',               this.elLk.map);
    this.elLk.exportMenu    = $('.iexport_menu',      this.elLk.container).appendTo('body').css('left', this.el.offset().left);
    this.elLk.resizeMenu    = $('.image_resize_menu', this.elLk.container).appendTo('body').css('left', this.el.offset().left);
    this.elLk.img           = $('img.imagemap',       this.elLk.container);
    this.elLk.hoverLabels   = $('.hover_label',       this.elLk.container);
    this.elLk.boundaries    = $('.boundaries',        this.elLk.container);
    this.elLk.toolbars      = $('.image_toolbar',     this.elLk.container);
    this.elLk.popupLinks    = $('a.popup',            this.elLk.toolbars);

    this.vertical = this.elLk.img.hasClass('vertical');
    this.multi    = this.elLk.areas.hasClass('multi');
    this.align    = this.elLk.areas.hasClass('align');
    
    this.makeImageMap();
    this.makeHoverLabels();
    this.initImageButtons()
    
    if (!this.vertical) {
      this.makeResizable();
    }
    
    species[this.id] = this.getSpecies();
    $.extend(this, Ensembl.Share);
    this.shareInit({ species: species, type: 'image', positionPopup: this.positionToolbarPopup });
    
    if (this.elLk.boundaries.length) {
      Ensembl.EventManager.register('changeTrackOrder', this, this.externalOrder);
      
      if (this.elLk.img[0].complete) {
        this.makeSortable();
      } else {
        this.elLk.img.on('load', function () { panel.makeSortable(); });
      }
    }
    
    if (typeof FileReader !== 'undefined') {
      this.dropFileUpload();
    }
    
    $('a',         this.elLk.toolbars).helptip({ track: false });
    $('a.iexport', this.elLk.toolbars).data('popup', this.elLk.exportMenu);
    $('a.resize',  this.elLk.toolbars).data('popup', this.elLk.resizeMenu);
    
    this.elLk.popupLinks.on('click', function () {
      var popup = $(this).data('popup');
      
      panel.elLk.popupLinks.map(function () { return ($(this).data('popup') || $()).filter(':visible')[0]; }).not(popup).hide();
      
      if (popup && !popup.hasClass('share_page')) {
        panel.positionToolbarPopup(popup, this).toggle();
      }
      
      popup = null;
      
      return false;
    });

    $('a.image_resize', this.elLk.resizeMenu).on('click', function () {
      if (!$(this).has('.current').length) {
        panel.resize(parseInt($(this).text(), 10) || Ensembl.imageWidth());
      }
      
      return false;
    });
  },

  initImageButtons: function() {
    var panel = this;

    this.el.find('._reset').on('click', function(e) {
      e.preventDefault();
      $.ajax({
        context: panel,
        url: this.href,
        type: 'post',
        success: function() {
          this.getContent();
        },
        data: {
          image_config: panel.imageConfig
        }
      });
    });
  },

  hashChange: function (r) {
    var reload = this.hashChangeReload;
    
    this.params.updateURL = Ensembl.urlFromHash(this.params.updateURL);
    
    if (Ensembl.images.total === 1) {
      this.highlightAllImages();
    } else if (!this.multi && this.highlightRegions[0]) {
      var range = this.highlightRegions[0][0].region.range;
      r = r.split(/\W/);
      
      if (parseInt(r[1], 10) < range.start || parseInt(r[2], 10) > range.end || range.chr !== r[0]) {
        reload = true;
      }
    }
    
    if (reload) {
      this.base();
    }
    
    if (this.align) {
      Ensembl.EventManager.trigger('highlightAllImages');
    }
  },
  
  getContent: function () {
    // If the panel contains an ajax loaded sub-panel, this function will be reached before ImageMap.init has been completed.
    // Make sure that this doesn't cause an error.
    if (this.imageConfig) {
      this.elLk.exportMenu.add(this.elLk.labelLayers).add(this.elLk.hoverLayers).add(this.elLk.resizeMenu).remove();
    
      for (var id in this.zMenus) {
        Ensembl.EventManager.trigger('destroyPanel', id);
      }
   
      this.removeShare();
    }
    
    this.base.apply(this, arguments);
    
    this.xhr.done(function (html) {
      if (!html) {
        delete Ensembl.images[this.imageNumber];
        Ensembl.EventManager.trigger('highlightAllImages');
      }
    });
  },
  
  makeImageMap: function () {
    var panel = this;
    
    var highlight = !!(window.location.pathname.match(/\/Location\//) && !this.vertical);
    var rect      = [ 'l', 't', 'r', 'b' ];
    var speciesNumber, c, r, start, end, scale;
    
    this.elLk.areas.each(function () {
      c = { a: this };
      
      if (this.shape && this.shape.toLowerCase() !== 'rect') {
        c.c = [];
        $.each(this.coords.split(/[ ,]/), function () { c.c.push(parseInt(this, 10)); });
      } else {
        $.each(this.coords.split(/[ ,]/), function (i) { c[rect[i]] = parseInt(this, 10); });
      }
      
      panel.areas.push(c);
      
      if (this.className.match(/drag/)) {
        // r = [ '#drag', image number, species number, species name, region, start, end, strand ]
        r        = c.a.href.split('|');
        start    = parseInt(r[5], 10);
        end      = parseInt(r[6], 10);
        scale    = (end - start + 1) / (this.vertical ? (c.b - c.t) : (c.r - c.l)); // bps per pixel on image
        
        c.range = { chr: r[4], start: start, end: end, scale: scale, vertical: this.vertical };
        
        panel.draggables.push(c);
        
        if (highlight === true) {
          r = this.href.split('|');
          speciesNumber = parseInt(r[1], 10) - 1;
          
          if (panel.multi || !speciesNumber) {
            if (!panel.highlightRegions[speciesNumber]) {
              panel.highlightRegions[speciesNumber] = [];
              panel.speciesCount++;
            }
            
            panel.highlightRegions[speciesNumber].push({ region: c });
            panel.imageNumber = parseInt(r[2], 10);
            
            Ensembl.images[panel.imageNumber] = Ensembl.images[panel.imageNumber] || {};
            Ensembl.images[panel.imageNumber][speciesNumber] = [ panel.imageNumber, speciesNumber, parseInt(r[5], 10), parseInt(r[6], 10) ];
          }
        }
      }
    });
    
    if (Ensembl.images.total) {
      this.highlightAllImages();
    }
    
    this.elLk.drag.on({
      mousedown: function (e) {

        if (!e.which || e.which === 1) { // Only draw the drag box for left clicks.
          panel.dragStart(e);
        }
        
        return false;
      },
      mousemove: function(e) {

        if (panel.dragging !== false) {
          return;
        }
        
        var area = panel.getArea(panel.getMapCoords(e));
        var tip;

        // change the cursor to pointer for clickable areas
        $(this).toggleClass('drag_select_pointer', !(!area || $(area.a).hasClass('label') || $(area.a).hasClass('drag') || $(area.a).hasClass('hover')));

        // Add helptips on navigation controls in multi species view
        if (area && area.a && $(area.a).hasClass('nav')) {
          if (tip !== area.a.alt) {
            tip = area.a.alt;
            
            if (!panel.elLk.navHelptip) {
              panel.elLk.navHelptip = $('<div class="ui-tooltip helptip-bottom"><div class="ui-tooltip-content"></div></div>');
            }
            
            panel.elLk.navHelptip.children().html(tip).end().appendTo('body').position({
              of: { pageX: panel.imgOffset.left + area.l + 10, pageY: panel.imgOffset.top + area.t - 48, preventDefault: true }, // fake an event
              my: 'center top'
            });
          }
        } else {
          if (panel.elLk.navHelptip) {
            panel.elLk.navHelptip.detach().css({ top: 0, left: 0 });
          }
        }
      },
      mouseleave: function(e) {
        if (e.relatedTarget) {

          if (panel.elLk.navHelptip) {
            panel.elLk.navHelptip.detach();
          }

        }
      },
      click: function (e, e2) {
        if (panel.clicking) {
          panel.makeZMenu(e2 || e, panel.getMapCoords(e2 || e));
        } else {
          panel.clicking = true;
        }
      }
    });
  },
  
  makeHoverLabels: function () {
    var panel = this;

    this.elLk.labelLayers = $();
    this.elLk.hoverLayers = $();

    $.each(this.areas, function() {

      if (!this.a) {
        return;
      }

      var $a = $(this.a);

      if ($a.hasClass('label')) {
        var hoverLabel = panel.elLk.hoverLabels.filter('.' + this.a.className.replace(/label /, ''));

        if (hoverLabel.length) {

          // add a div layer over the label, and append the hover menu to the layer. Hover menu toggling is controlled by CSS.
          panel.elLk.labelLayers = panel.elLk.labelLayers.add(
            $('<div class="label_layer">').append('<div class="label_layer_bg">').append(hoverLabel).appendTo(document.body).data({area: this})
          );
        }

        hoverLabel = null;

      } else if ($a.hasClass('hover')) {

        panel.elLk.hoverLayers = panel.elLk.hoverLayers.add(
          $('<div class="hover_layer">').appendTo(document.body).data({area: this}).on('click', function(e) {
            panel.clicking = true;
            panel.elLk.drag.triggerHandler('click', e);
          }
        ));
      }

      $a = null;
    });

    // apply css positions to the hover layers
    this.positionLayers();

    this.elLk.hoverLabels.each(function() {

      // position hover menus to the right of the layer and init the tab styled icons inside the hover menus
      $(this).css('left', function() { return $(this.parentNode).width(); }).find('._hl_icon').tabs($(this).find('._hl_tab'));

    // init config tab, fav icon and close icon
    }).find('a.config').on('click', function () {
      var config  = this.rel;
      var update  = this.href.split(';').reverse()[0].split('='); // update = [ trackId, renderer ]
      var fav     = '';
      var $this   = $(this);

      if ($this.hasClass('favourite')) {
        fav = $this.hasClass('selected') ? 'off' : 'on';
        Ensembl.EventManager.trigger('changeFavourite', update[0], fav === 'on');
      } else {
        $this.parents('.label_layer').addClass('hover_label_spinner');
      }

      $.ajax({
        url: this.href + fav,
        dataType: 'json',
        success: function (json) {
          if (json.updated) {
            Ensembl.EventManager.triggerSpecific('changeConfiguration', 'modal_config_' + config, update[0], update[1]);
            Ensembl.EventManager.trigger('reloadPage', panel.id);
          }
        }
      });
      
      $this = null;

      return false;
    }).end().find('input._copy_url').on('click focus blur', function(e) {
      $(this).val(this.defaultValue).select().parents('.label_layer').toggleClass('hover', e.type !== 'blur');
    });
  },

  positionLayers: function() {
    var offset = this.elLk.img.offset();
    var right  = this.draggables[0].l; // label ends where the drag region starts

    this.elLk.labelLayers.each(function() {
      var $this = $(this);
      var area  = $this.data('area');

      $this.css({
        left:   offset.left + area.l,
        top:    offset.top + area.t,
        height: area.b - area.t,
        width:  right - area.l
      });

      area = $this = null;
    });

    this.elLk.hoverLayers.each(function() {
      var $this = $(this);
      var area  = $this.data('area');

      $this.css({
        left:   offset.left + area.l,
        top:    offset.top + area.t,
        height: area.b - area.t,
        width:  area.r - area.l
      });

      area = $this = null;
    });
  },
  
  makeResizable: function () {
    var panel = this;
    
    function resizing(e, ui) {
      panel.imageResize = Math.floor(ui.size.width / 100) * 100; // The image_container has a border, which causes ui.size.width to increase by the border width.
      resizeHelptip.apply(this, [ ui.helper ].concat(e.type === 'resizestart' ? [ 'Drag to resize', e.pageY ] : panel.imageResize + 'px'));
    }
    
    function resizeHelptip(el, content, y) {
      if (typeof y === 'number') {
        el.data('y', y);
      } else {
        y = el.data('y');
      }
      
      el.html('<div class="bg"></div><div class="ui-tooltip"><div class="ui-tooltip-content"></div></div>').find('.ui-tooltip-content').html(content).parent().css('top', function () {
        return y - el.offset().top - $(this).outerHeight(true) / 2;
      });
      
      el = null;
    }
    
    this.elLk.container.resizable({
      handles: 'e',
      grid:    [ 100, 0 ],
      minWidth: this.minImageWidth,
      maxWidth: $(window).width() - this.el.offset().left,
      helper:   'image_resize_overlay',
      start:    resizing,
      resize:   resizing,
      stop:     function (e, ui) {
        if (ui.originalSize.width === ui.size.width) {
          $(this).css({ width: panel.imageResize, height: '' });
        } else {
          panel.resize(panel.imageResize);
        }
      }
    });
  },
  
  makeSortable: function () {
    var panel      = this;
    var wrapperTop = $('.boundaries_wrapper', this.el).position().top;
    var ulTop      = this.elLk.boundaries.position().top + wrapperTop - (Ensembl.browser.ie7 ? 3 : 0); // IE7 reports li.position().top as 3 pixels higher than other browsers, so offset that here.
    var lis        = []; // just a throwaway list to allocate areas to their respective tracks

    this.elLk.boundaries.children().each(function (i) {
      var li  = $(this);
      var t   = li.position().top + ulTop;
      var ref = []; // reference for array containing areas for a track that will be populated later

      li.data({ areas: ref, position: i, top: li.offset().top });
      lis.push({ top: Math.floor(t), bottom: Math.ceil(t + li.height()), areas: ref });

      li = null;
    });

    $.each(this.areas, function () {

      assignArea:
      for (var i = 0; i <= 10; i++) { // this is to overcome an apparent drawing code bug that areas sometimes are not completely enclosed inside a track's li
        for (var j = lis.length - 1; j >= 0; j--) {
          if (lis[j].top <= this.t + i && lis[j].bottom >= this.b - i) {
            lis[j].areas.push(this);
            break assignArea;
          }
        }
      }
    });

    this.elLk.boundaries.each(function () {
      $(this).data('updateURL', '/' + this.className.split(' ')[0] + '/Ajax/track_order');
    }).sortable({
      axis:   'y',
      handle: 'div.handle',
      revert: 200,
      helper: 'clone',
      placeholder: 'placeholder',
      start: function (e, ui) {
        panel.sortStart(e, ui);
      },
      stop: function (e, ui) {
        panel.sortStop(e, ui);
      },
      update: function (e, ui) {
        panel.sortUpdate(e, ui);
      }
    }).css('visibility', 'visible').find('div.handle').on({
      mousedown: function() {
        $(this.parentNode).stop().animate({opacity: 0.8}, 200);
      },
      mouseup: function() {
        $(this.parentNode).stop().animate({opacity: 1}, 200);
      }
    });
  },

  sortStart: function (e, ui) {

    // make the placeholder similar to the actual track but slightly faded so the saturated background colour beneath gives it a highlighted effect
    ui.placeholder.css({
      backgroundImage:     ui.item.css('backgroundImage'),
      backgroundPosition:  ui.item.css('backgroundPosition'),  // Firefox
      backgroundPositionY: ui.item.css('backgroundPositionY'), // IE (Chrome works with either)
      height:              ui.item.height(),
      opacity:             0.8
    }).html(ui.item.html()).addClass(ui.item.prop('className'));

    // add some transparency to the helper (already a clone of actual track) that moves with the mouse
    ui.helper.stop().css({opacity: 0.8}).addClass('helper');

    // css deals with the rest of the things
    $(document.body).addClass('track-reordering');

    this.dragging = true;
  },

  sortStop: function (e, ui) {
    ui.item.stop().animate({opacity: 1}, 200);
    $(document.body).removeClass('track-reordering');
    this.dragging = false;
  },

  sortUpdate: function(e, ui) {

    var prev  = (ui.item.prev().prop('className') || '').replace(' ', '.');
    var track = ui.item.prop('className').replace(' ', '.');

    Ensembl.EventManager.triggerSpecific('changeTrackOrder', 'modal_config_' + this.id.toLowerCase(), track, prev);

    this.saveSortOrder(ui.item.parent().data('updateURL'), track, prev);
  },

  externalOrder: function(trackId, prevTrackId) {
    var track = this.elLk.boundaries.find('li.' + trackId);
    var prev  = prevTrackId ? this.elLk.boundaries.find('li.' + prevTrackId) : false;

    if (!track) {
      return;
    }

    if (prev && prev.length) {
      track.insertAfter(prev);
    } else {
      track.parent().prepend(track);
    }

    this.saveSortOrder(track.parent().data('updateURL'), trackId, prevTrackId || '');

    track = prev = null;
  },

  saveSortOrder: function(url, track, prev) {
    this.positionAreas();
    this.positionLayers();
    this.removeShare();
    Ensembl.EventManager.trigger('removeShare');

    $.ajax({
      url: url,
      type: 'post',
      data: {
        image_config: this.imageConfig,
        track: track,
        prev: prev
      }
    });
  },

  positionAreas: function () {
    var tracks = this.elLk.boundaries.children();

    tracks.each(function (i) {
      var li = $(this);
      var top, move;

      if (i !== li.data('position')) {
        top  = li.offset().top;
{'fraction_non_alphanumeric': 0.16319452924655026, 'fraction_numerical': 0.0056661375015264375, 'mean_word_length': 2.249424648837394, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '646497', 'n_tokens_mistral': 13169, 'n_tokens_neox': 12749, 'n_words': 2653}