diff --git a/static/scripts/bundled/admin.bundled.js b/static/scripts/bundled/admin.bundled.js index a26be3c7d48f..6ae8da74ac85 100644 --- a/static/scripts/bundled/admin.bundled.js +++ b/static/scripts/bundled/admin.bundled.js @@ -1,2 +1,2 @@ -webpackJsonp([1],{264:function(e,t,a){"use strict";(function(e){function t(e){return e&&e.__esModule?e:{default:e}}var s=a(0),i=t(s),r=a(28),n=t(r),o=a(265),l=t(o),u=a(52),d=t(u),_=a(14),c=t(_),m=a(6),f=(t(m),a(10)),p=t(f),g=a(38),h=t(g),y=a(3),v=t(y),w=a(29),b=t(w),x=a(20),G=t(x),M=a(266),q=t(M),k=i.default;window.app=function(t,a){window.Galaxy=new n.default.GalaxyApp(t,a),Galaxy.debug("admin app");var s=h.default.extend({routes:{"(/)admin(/)users":"show_users","(/)admin(/)roles":"show_roles","(/)admin(/)groups":"show_groups","(/)admin(/)tool_versions":"show_tool_versions","(/)admin(/)quotas":"show_quotas","(/)admin(/)repositories":"show_repositories","(/)admin(/)forms":"show_forms","(/)admin(/)form(/)(:form_id)":"show_form","(/)admin/api_keys":"show_user_api_keys"},authenticate:function(e,t){return Galaxy.user&&Galaxy.user.id&&Galaxy.user.get("is_admin")},show_users:function(){this.page.display(new c.default({url_base:Galaxy.root+"admin/users_list",url_data:Galaxy.params,dict_format:!0}))},show_roles:function(){this.page.display(new c.default({url_base:Galaxy.root+"admin/roles_list",url_data:Galaxy.params,dict_format:!0}))},show_groups:function(){this.page.display(new c.default({url_base:Galaxy.root+"admin/groups_list",url_data:Galaxy.params,dict_format:!0}))},show_repositories:function(){this.page.display(new c.default({url_base:Galaxy.root+"admin_toolshed/browse_repositories",url_data:Galaxy.params,dict_format:!0}))},show_tool_versions:function(){this.page.display(new c.default({url_base:Galaxy.root+"admin/tool_versions_list",url_data:Galaxy.params,dict_format:!0}))},show_quotas:function(){this.page.display(new c.default({url_base:Galaxy.root+"admin/quotas_list",url_data:Galaxy.params,dict_format:!0}))},show_user_api_keys:function(){var e=document.createElement("div");this.page.display(e),new G.default(q.default).$mount(e)},show_forms:function(){this.page.display(new c.default({url_base:Galaxy.root+"forms/forms_list",url_data:Galaxy.params,dict_format:!0}))},show_form:function(e){var t="?id="+p.default.get("id"),a={reset_user_password:{title:"Reset passwords",url:"admin/reset_user_password"+t,icon:"fa-user",submit_title:"Save new password",redirect:"admin/users"},manage_roles_and_groups_for_user:{url:"admin/manage_roles_and_groups_for_user"+t,icon:"fa-users",redirect:"admin/users"},manage_users_and_groups_for_role:{url:"admin/manage_users_and_groups_for_role"+t,redirect:"admin/roles"},manage_users_and_roles_for_group:{url:"admin/manage_users_and_roles_for_group"+t,redirect:"admin/groups"},manage_users_and_groups_for_quota:{url:"admin/manage_users_and_groups_for_quota"+t,redirect:"admin/quotas"},create_role:{url:"admin/create_role",redirect:"admin/roles"},create_group:{url:"admin/create_group",redirect:"admin/groups"},create_quota:{url:"admin/create_quota",redirect:"admin/quotas"},rename_role:{url:"admin/rename_role"+t,redirect:"admin/roles"},rename_group:{url:"admin/rename_group"+t,redirect:"admin/groups"},rename_quota:{url:"admin/rename_quota"+t,redirect:"admin/quotas"},edit_quota:{url:"admin/edit_quota"+t,redirect:"admin/quotas"},set_quota_default:{url:"admin/set_quota_default"+t,redirect:"admin/quotas"},create_form:{url:"forms/create_form",redirect:"admin/forms"},edit_form:{url:"forms/edit_form"+t,redirect:"admin/forms"}};this.page.display(new d.default.View(a[e]))}});k(function(){e.extend(t.config,{active_view:"admin"}),v.default.setWindowTitle("Administration"),Galaxy.page=new b.default.View(e.extend(t,{Left:l.default,Router:s}))})}}).call(t,a(1))},265:function(e,t,a){"use strict";(function(e,s,i){Object.defineProperty(t,"__esModule",{value:!0});var r=a(4),n=function(e){return e&&e.__esModule?e:{default:e}}(r),o=e.View.extend({initialize:function(t,a){var s=this;this.page=t,this.root=a.root,this.config=a.config,this.settings=a.settings,this.message=a.message,this.status=a.status,this.model=new e.Model({title:(0,n.default)("Administration")}),this.categories=new e.Collection([{title:"Server",items:[{title:"Data types",url:"admin/view_datatypes_registry"},{title:"Data tables",url:"admin/view_tool_data_tables"},{title:"Display applications",url:"admin/display_applications"},{title:"Manage jobs",url:"admin/jobs"},{title:"Local data",url:"data_manager"}]},{title:"User Management",items:[{title:"Users",url:"admin/users",target:"__use_router__"},{title:"Quotas",url:"admin/quotas",target:"__use_router__",enabled:s.config.enable_quotas},{title:"Groups",url:"admin/groups",target:"__use_router__"},{title:"Roles",url:"admin/roles",target:"__use_router__"},{title:"Forms",url:"admin/forms",target:"__use_router__"},{title:"API keys",url:"admin/api_keys",target:"__use_router__"},{title:"Impersonate a user",url:"admin/impersonate",enabled:s.config.allow_user_impersonation}]},{title:"Tool Management",items:[{title:"Install new tools",url:"admin_toolshed/browse_tool_sheds",enabled:s.settings.is_tool_shed_installed},{title:"Install new tools (Beta)",url:"admin_toolshed/browse_toolsheds",enabled:s.settings.is_tool_shed_installed&&s.config.enable_beta_ts_api_install},{title:"Monitor installation",url:"admin_toolshed/monitor_repository_installation",enabled:s.settings.installing_repository_ids},{title:"Manage tools",url:"admin/repositories",enabled:s.settings.is_repo_installed,target:"__use_router__"},{title:"Manage metadata",url:"admin_toolshed/reset_metadata_on_selected_installed_repositories",enabled:s.settings.is_repo_installed},{title:"Manage whitelist",url:"admin/sanitize_whitelist"},{title:"Manage dependencies",url:"admin/manage_tool_dependencies"},{title:"View lineage",url:"admin/tool_versions",target:"__use_router__"},{title:"View migration stages",url:"admin/review_tool_migration_stages"},{title:"View error logs",url:"admin/tool_errors"}]}]),this.setElement(this._template())},render:function(){var e=this;this.$el.empty(),this.categories.each(function(t){var a=s(e._templateSection(t.attributes)),r=a.find(".ui-side-section-body");i.each(t.get("items"),function(t){if(void 0===t.enabled||t.enabled){var a=s("").attr({href:e.root+t.url}).text((0,n.default)(t.title));"__use_router__"==t.target?a.on("click",function(a){a.preventDefault(),e.page.router.push(t.url)}):a.attr("target","galaxy_main"),r.append(s("
").addClass("ui-side-section-body-title").append(a))}}),e.$el.append(a)}),this.page.$("#galaxy_main").prop("src",this.root+"admin/center?message="+this.message+"&status="+this.status)},_templateSection:function(e){return["'+this.grid_header(t)+' | ||
",n&&(c+='Advanced Search'),c+=" |
Close Advanced Search |
"+e.message+"",buttons:{Close:function(){Galaxy.modal.hide()}}})})),n.append(t("").text(" ")),n.append(t("").text("Try again").click(function(){i.init(!0)})))}}),this.update_icons(),a}},predraw_init:function(){var e=this;return t.getJSON(e.dataset.url(),{data_type:"data",stats:!0,chrom:e.view.chrom,low:0,high:e.view.max_high,hda_ldda:e.dataset.get("hda_ldda")},function(t){var i=t.data;if(i&&void 0!==i.min&&void 0!==i.max){var a=i.min,n=i.max;a=Math.floor(Math.min(0,Math.max(a,i.mean-2*i.sd))),n=Math.ceil(Math.max(0,Math.min(n,i.mean+2*i.sd))),e.config.set_default_value("min_value",a),e.config.set_default_value("max_value",n),e.config.set_value("min_value",a),e.config.set_value("max_value",n)}})},get_drawables:function(){return this}});var J=function(e,i,a){B.call(this,e,i,a);var n=this;if(G(n.container_div,n.drag_handle_class,".group",n),this.filters_manager=new b.default.FiltersManager(this,"filters"in a?a.filters:null),this.data_manager.set("filters_manager",this.filters_manager),this.filters_available=!1,this.tool=a.tool?new L(l.extend(a.tool,{track:this,tool_state:a.tool_state})):null,this.tile_cache=new h.default.Cache(10),this.left_offset=0,this.header_div&&(this.set_filters_manager(this.filters_manager),this.tool)){var o=new F({model:this.tool});o.render(),this.dynamic_tool_div=o.$el,this.header_div.after(this.dynamic_tool_div)}this.tiles_div=t("").addClass("tiles").appendTo(this.content_div),this.config.get_value("content_visible")||this.tiles_div.hide(),this.overlay_div=t("").addClass("overlay").appendTo(this.content_div),a.mode&&this.change_mode(a.mode)};O(J.prototype,R.prototype,B.prototype,{action_icons_def:B.prototype.action_icons_def.concat([{name:"show_more_rows_icon",title:"To minimize track height, not all feature rows are displayed. Click to display more rows.",css_class:"exclamation",on_click_fn:function(e){t(".tooltip").remove(),e.slotters[e.view.resolution_px_b].max_rows*=2,e.request_draw({clear_tile_cache:!0})},hide:!0}]),copy:function(t){var e=this.to_dict();O(e,{data_manager:this.data_manager});var i=new this.constructor(this.view,t,e);return i.change_mode(this.mode),i.enabled=this.enabled,i},set_filters_manager:function(t){this.filters_manager=t,this.header_div.after(this.filters_manager.parent_div)},to_dict:function(){return{track_type:this.get_type(),dataset:{id:this.dataset.id,hda_ldda:this.dataset.get("hda_ldda")},prefs:this.config.to_key_value_dict(),mode:this.mode,filters:this.filters_manager.to_dict(),tool_state:this.tool?this.tool.state_dict():{}}},set_min_max:function(){var e=this;return t.getJSON(e.dataset.url(),{data_type:"data",stats:!0,chrom:e.view.chrom,low:0,high:e.view.max_high,hda_ldda:e.dataset.get("hda_ldda")},function(t){var i=t.data;if(isNaN(parseFloat(e.config.get_value("min_value")))||isNaN(parseFloat(e.config.get_value("max_value")))){var a=i.min,n=i.max;a=Math.floor(Math.min(0,Math.max(a,i.mean-2*i.sd))),n=Math.ceil(Math.max(0,Math.min(n,i.mean+2*i.sd))),e.config.set_value("min_value",a),e.config.set_value("max_value",n)}})},change_mode:function(t){var e=this;return e.mode=t,e.config.set_value("mode",t),"Auto"===t&&this.data_manager.clear(),e.request_draw({clear_tile_cache:!0}),this.action_icons.mode_icon.attr("title","Set display mode (now: "+e.mode+")"),e},update_icons:function(){var t=this;t.action_icons.filters_icon.toggle(t.filters_available),t.action_icons.tools_icon.toggle(null!==t.tool),t.action_icons.param_space_viz_icon.toggle(null!==t.tool)},_gen_tile_cache_key:function(t,e){return t+"_"+e},request_draw:function(t){t&&t.clear_tile_cache&&this.tile_cache.clear(),this.view.request_redraw(t,this)},before_draw:function(){this.max_height_px=0},_draw:function(e){if(this.can_draw()){var i=e&&e.clear_after,a=this.view.low,n=this.view.high,o=this.view.container.width(),r=this.view.resolution_px_b,s=1/r;this.is_overview&&(a=this.view.max_low,n=this.view.max_high,r=o/(view.max_high-view.max_low),s=1/r),this.before_draw(),this.tiles_div.children().addClass("remove");for(var d,c,u=Math.floor(400*s),f=Math.floor(a/u),_=[],p=[];f*u
r&&(l=this.draw_element(t,this.mode,m,b,r,s,a,d,e),c.map_feature_data(m,b,l[0],l[1]),(v ",(0,y.default)(["Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ","workflows in order to have analyses done on each member of the entire group. This interface allows ","you to create a collection and re-order the final collection."].join(""))," ",(0,y.default)(['Once your collection is complete, enter a name and ','click "Create list".'].join("")),"D){var nt="+"==R.orientation?new u(D,L+3):new u(D-3,L),ot=_(tt,nt);if(ot){R.type="translation";for(var rt=ot.ranges(),st=0,lt=0;rt[0].min()>et[lt].max();)lt++;for(var it=0;it
i.field-3)return o[i.field-3]==t},[{offset:v,size:w}],e)}f+=c}return e([])}for(var b=null,_=0;_a._max&&(a._max=t._max):(i.push(a),a=t)}),i.push(a),this._ranges=i}function i(t,i){return t instanceof e||(t instanceof Array||(t=[t]),t=new e(t)),i&&t.insertRange(i),t}function a(i,a){for(var n=i.ranges(),o=a.ranges(),r=n.length,s=o.length,l=0,d=0,h=[];l").attr({id:"content_table",cellpadding:0});this.$el.append(t);var e=this.model.get_metadata("column_names"),i=n("").appendTo(t),a=n("
").appendTo(i);if(e)a.append(" "+e.join(" ")+" ");else for(var o=1;o<=this.model.get_metadata("columns");o++)a.append(""+o+" ");var r=this,s=this.model.get("first_data_chunk");s?this._renderChunk(s):n.when(r.model.get_next_chunk()).then(function(t){r._renderChunk(t)}),this.scroll_elt.scroll(function(){r.attempt_to_fetch()})},scrolled_to_bottom:function(){return!1},_renderCell:function(t,e,i){var a=n("").text(t),o=this.model.get_metadata("column_types");return void 0!==i?a.attr("colspan",i).addClass("stringalign"):o&&e "))})),this.row_count++,i},_renderChunk:function(t){var e=this.$el.find("table");a.each(t.ck_data.split("\n"),function(t,i){""!==t&&e.append(this._renderRow(t))},this)}}),p=_.extend({initialize:function(t){_.prototype.initialize.call(this,t);var e=a.find(this.$el.parents(),function(t){return"auto"===n(t).css("overflow")});e||(e=window),this.scroll_elt=n(e)},scrolled_to_bottom:function(){return this.$el.height()-this.scroll_elt.scrollTop()-this.scroll_elt.height()<=0}}),m=_.extend({initialize:function(t){_.prototype.initialize.call(this,t),this.scroll_elt=this.$el.css({position:"relative",overflow:"scroll",height:t.height||"500px"})},scrolled_to_bottom:function(){return this.$el.scrollTop()+this.$el.innerHeight()>=this.el.scrollHeight}}),g=t.View.extend({col:{chrom:null,start:null,end:null},url_viz:null,dataset_id:null,genome_build:null,file_ext:null,initialize:function(t){var e=parent.Galaxy;if(e&&e.modal&&(this.modal=e.modal),e&&e.frame&&(this.frame=e.frame),this.modal&&this.frame){var i=t.model,a=i.get("metadata");if(i.get("file_ext")){if(this.file_ext=i.get("file_ext"),"bed"==this.file_ext){if(!(a.get("chromCol")&&a.get("startCol")&&a.get("endCol")))return void console.log("TabularButtonTrackster : Bed-file metadata incomplete.");this.col.chrom=a.get("chromCol")-1,this.col.start=a.get("startCol")-1,this.col.end=a.get("endCol")-1}if("vcf"==this.file_ext){var n=function(t,e){for(var i=0;i ', this._templateOptions(options), \"
\"].join(\"\");\n },\n\n _templateOptions: function _templateOptions(options) {\n if (!options.length) {\n return \"\").addClass(\"tab-navigation nav nav-tabs\")).append($(\"\").addClass(\"tab-content\"));\n },\n\n /** Tab template */\n _template_tab: function _template_tab(options) {\n var $tmpl = $(\"\").addClass(\"tab-element\").attr(\"id\", \"tab-\" + options.id).append($(\"\").attr(\"id\", \"tab-title-link-\" + options.id));\n var $href = $tmpl.find(\"a\");\n options.icon && $href.append($(\"\").addClass(\"tab-icon fa\").addClass(options.icon));\n $href.append($(\"\").attr(\"id\", \"tab-title-text-\" + options.id).addClass(\"tab-title-text\").append(options.title));\n return $tmpl;\n }\n}); /**\n * Renders tabs e.g. used in the charts editor, behaves similar to repeat and section rendering\n */\nexports.default = { View: View };\n/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(2), __webpack_require__(0)))\n\n/***/ }),\n\n/***/ 26:\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _underscore = __webpack_require__(1);\n\nvar _ = _interopRequireWildcard(_underscore);\n\nvar _backbone = __webpack_require__(2);\n\nvar Backbone = _interopRequireWildcard(_backbone);\n\nvar _baseMvc = __webpack_require__(5);\n\nvar _baseMvc2 = _interopRequireDefault(_baseMvc);\n\nvar _localization = __webpack_require__(4);\n\nvar _localization2 = _interopRequireDefault(_localization);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }\n\nvar logNamespace = \"user\";\n//==============================================================================\n/** @class Model for a Galaxy user (including anonymous users).\n * @name User\n */\nvar User = Backbone.Model.extend(_baseMvc2.default.LoggableMixin).extend(\n/** @lends User.prototype */{\n _logNamespace: logNamespace,\n\n /** API location for this resource */\n urlRoot: function urlRoot() {\n return Galaxy.root + \"api/users\";\n },\n\n /** Model defaults\n * Note: don't check for anon-users with the username as the default is '(anonymous user)'\n * a safer method is if( !user.get( 'email' ) ) -> anon user\n */\n defaults: /** @lends User.prototype */{\n id: null,\n username: \"(\" + (0, _localization2.default)(\"anonymous user\") + \")\",\n email: \"\",\n total_disk_usage: 0,\n nice_total_disk_usage: \"\",\n quota_percent: null,\n is_admin: false\n },\n\n /** Set up and bind events\n * @param {Object} data Initial model data.\n */\n initialize: function initialize(data) {\n this.log(\"User.initialize:\", data);\n\n this.on(\"loaded\", function (model, resp) {\n this.log(this + \" has loaded:\", model, resp);\n });\n this.on(\"change\", function (model, data) {\n this.log(this + \" has changed:\", model, data.changes);\n });\n },\n\n isAnonymous: function isAnonymous() {\n return !this.get(\"email\");\n },\n\n isAdmin: function isAdmin() {\n return this.get(\"is_admin\");\n },\n\n /** Load a user with the API using an id.\n * If getting an anonymous user or no access to a user id, pass the User.CURRENT_ID_STR\n * (e.g. 'current') and the API will return the current transaction's user data.\n * @param {String} idOrCurrent encoded user id or the User.CURRENT_ID_STR\n * @param {Object} options hash to pass to Backbone.Model.fetch. Can contain success, error fns.\n * @fires loaded when the model has been loaded from the API, passing the newModel and AJAX response.\n */\n loadFromApi: function loadFromApi(idOrCurrent, options) {\n idOrCurrent = idOrCurrent || User.CURRENT_ID_STR;\n\n options = options || {};\n var model = this;\n var userFn = options.success;\n\n /** @ignore */\n options.success = function (newModel, response) {\n model.trigger(\"loaded\", newModel, response);\n if (userFn) {\n userFn(newModel, response);\n }\n };\n\n // requests for the current user must have a sep. constructed url (fetch don't work, ma)\n if (idOrCurrent === User.CURRENT_ID_STR) {\n options.url = this.urlRoot + \"/\" + User.CURRENT_ID_STR;\n }\n return Backbone.Model.prototype.fetch.call(this, options);\n },\n\n /** Clears all data from the sessionStorage.\n */\n clearSessionStorage: function clearSessionStorage() {\n for (var key in sessionStorage) {\n //TODO: store these under the user key so we don't have to do this\n // currently only history\n if (key.indexOf(\"history:\") === 0) {\n sessionStorage.removeItem(key);\n } else if (key === \"history-panel\") {\n sessionStorage.removeItem(key);\n }\n }\n },\n\n /** string representation */\n toString: function toString() {\n var userInfo = [this.get(\"username\")];\n if (this.get(\"id\")) {\n userInfo.unshift(this.get(\"id\"));\n userInfo.push(this.get(\"email\"));\n }\n return \"User(\" + userInfo.join(\":\") + \")\";\n }\n});\n\n// string to send to tell server to return this transaction's user (see api/users.py)\nUser.CURRENT_ID_STR = \"current\";\n\n// class method to load the current user via the api and return that model\nUser.getCurrentUserFromApi = function (options) {\n var currentUser = new User();\n currentUser.loadFromApi(User.CURRENT_ID_STR, options);\n return currentUser;\n};\n\n// (stub) collection for users (shouldn't be common unless admin UI)\nvar UserCollection = Backbone.Collection.extend(_baseMvc2.default.LoggableMixin).extend({\n model: User,\n urlRoot: function urlRoot() {\n return Galaxy.root + \"api/users\";\n }\n //logger : console,\n});\n\n//==============================================================================\nexports.default = {\n User: User\n};\n\n/***/ }),\n\n/***/ 265:\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n/* WEBPACK VAR INJECTION */(function(_) {\n\nvar _jquery = __webpack_require__(0);\n\nvar _jquery2 = _interopRequireDefault(_jquery);\n\nvar _galaxy = __webpack_require__(32);\n\nvar _galaxy2 = _interopRequireDefault(_galaxy);\n\nvar _adminPanel = __webpack_require__(266);\n\nvar _adminPanel2 = _interopRequireDefault(_adminPanel);\n\nvar _formWrapper = __webpack_require__(88);\n\nvar _formWrapper2 = _interopRequireDefault(_formWrapper);\n\nvar _gridView = __webpack_require__(11);\n\nvar _gridView2 = _interopRequireDefault(_gridView);\n\nvar _uiMisc = __webpack_require__(6);\n\nvar _uiMisc2 = _interopRequireDefault(_uiMisc);\n\nvar _queryStringParsing = __webpack_require__(14);\n\nvar _queryStringParsing2 = _interopRequireDefault(_queryStringParsing);\n\nvar _router = __webpack_require__(73);\n\nvar _router2 = _interopRequireDefault(_router);\n\nvar _utils = __webpack_require__(3);\n\nvar _utils2 = _interopRequireDefault(_utils);\n\nvar _page = __webpack_require__(34);\n\nvar _page2 = _interopRequireDefault(_page);\n\nvar _vue = __webpack_require__(64);\n\nvar _vue2 = _interopRequireDefault(_vue);\n\nvar _user_api_keys = __webpack_require__(267);\n\nvar _user_api_keys2 = _interopRequireDefault(_user_api_keys);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar $ = _jquery2.default;\n\n\nwindow.app = function app(options, bootstrapped) {\n window.Galaxy = new _galaxy2.default.GalaxyApp(options, bootstrapped);\n Galaxy.debug(\"admin app\");\n\n /** Routes */\n var AdminRouter = _router2.default.extend({\n routes: {\n \"(/)admin(/)users\": \"show_users\",\n \"(/)admin(/)roles\": \"show_roles\",\n \"(/)admin(/)groups\": \"show_groups\",\n \"(/)admin(/)tool_versions\": \"show_tool_versions\",\n \"(/)admin(/)quotas\": \"show_quotas\",\n \"(/)admin(/)repositories\": \"show_repositories\",\n \"(/)admin(/)forms\": \"show_forms\",\n \"(/)admin(/)form(/)(:form_id)\": \"show_form\",\n \"(/)admin/api_keys\": \"show_user_api_keys\"\n },\n\n authenticate: function authenticate(args, name) {\n return Galaxy.user && Galaxy.user.id && Galaxy.user.get(\"is_admin\");\n },\n\n show_users: function show_users() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"admin/users_list\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_roles: function show_roles() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"admin/roles_list\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_groups: function show_groups() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"admin/groups_list\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_repositories: function show_repositories() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"admin_toolshed/browse_repositories\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_tool_versions: function show_tool_versions() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"admin/tool_versions_list\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_quotas: function show_quotas() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"admin/quotas_list\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_user_api_keys: function show_user_api_keys() {\n var vuemount = document.createElement(\"div\");\n this.page.display(vuemount);\n new _vue2.default(_user_api_keys2.default).$mount(vuemount);\n },\n\n show_forms: function show_forms() {\n this.page.display(new _gridView2.default({\n url_base: Galaxy.root + \"forms/forms_list\",\n url_data: Galaxy.params,\n dict_format: true\n }));\n },\n\n show_form: function show_form(form_id) {\n var id = \"?id=\" + _queryStringParsing2.default.get(\"id\");\n var form_defs = {\n reset_user_password: {\n title: \"Reset passwords\",\n url: \"admin/reset_user_password\" + id,\n icon: \"fa-user\",\n submit_title: \"Save new password\",\n redirect: \"admin/users\"\n },\n manage_roles_and_groups_for_user: {\n url: \"admin/manage_roles_and_groups_for_user\" + id,\n icon: \"fa-users\",\n redirect: \"admin/users\"\n },\n manage_users_and_groups_for_role: {\n url: \"admin/manage_users_and_groups_for_role\" + id,\n redirect: \"admin/roles\"\n },\n manage_users_and_roles_for_group: {\n url: \"admin/manage_users_and_roles_for_group\" + id,\n redirect: \"admin/groups\"\n },\n manage_users_and_groups_for_quota: {\n url: \"admin/manage_users_and_groups_for_quota\" + id,\n redirect: \"admin/quotas\"\n },\n create_role: {\n url: \"admin/create_role\",\n redirect: \"admin/roles\"\n },\n create_group: {\n url: \"admin/create_group\",\n redirect: \"admin/groups\"\n },\n create_quota: {\n url: \"admin/create_quota\",\n redirect: \"admin/quotas\"\n },\n rename_role: {\n url: \"admin/rename_role\" + id,\n redirect: \"admin/roles\"\n },\n rename_group: {\n url: \"admin/rename_group\" + id,\n redirect: \"admin/groups\"\n },\n rename_quota: {\n url: \"admin/rename_quota\" + id,\n redirect: \"admin/quotas\"\n },\n edit_quota: {\n url: \"admin/edit_quota\" + id,\n redirect: \"admin/quotas\"\n },\n set_quota_default: {\n url: \"admin/set_quota_default\" + id,\n redirect: \"admin/quotas\"\n },\n create_form: {\n url: \"forms/create_form\",\n redirect: \"admin/forms\"\n },\n edit_form: {\n url: \"forms/edit_form\" + id,\n redirect: \"admin/forms\"\n }\n };\n this.page.display(new _formWrapper2.default.View(form_defs[form_id]));\n }\n });\n\n $(function () {\n _.extend(options.config, { active_view: \"admin\" });\n _utils2.default.setWindowTitle(\"Administration\");\n Galaxy.page = new _page2.default.View(_.extend(options, {\n Left: _adminPanel2.default,\n Router: AdminRouter\n }));\n });\n};\n/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(1)))\n\n/***/ }),\n\n/***/ 266:\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n/* WEBPACK VAR INJECTION */(function(Backbone, $, _) {\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _localization = __webpack_require__(4);\n\nvar _localization2 = _interopRequireDefault(_localization);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar AdminPanel = Backbone.View.extend({\n initialize: function initialize(page, options) {\n var self = this;\n this.page = page;\n this.root = options.root;\n this.config = options.config;\n this.settings = options.settings;\n this.message = options.message;\n this.status = options.status;\n this.model = new Backbone.Model({\n title: (0, _localization2.default)(\"Administration\")\n });\n this.categories = new Backbone.Collection([{\n title: \"Server\",\n items: [{\n title: \"Data types\",\n url: \"admin/view_datatypes_registry\"\n }, {\n title: \"Data tables\",\n url: \"admin/view_tool_data_tables\"\n }, {\n title: \"Display applications\",\n url: \"admin/display_applications\"\n }, {\n title: \"Manage jobs\",\n url: \"admin/jobs\"\n }, {\n title: \"Local data\",\n url: \"data_manager\"\n }]\n }, {\n title: \"User Management\",\n items: [{\n title: \"Users\",\n url: \"admin/users\",\n target: \"__use_router__\"\n }, {\n title: \"Quotas\",\n url: \"admin/quotas\",\n target: \"__use_router__\",\n enabled: self.config.enable_quotas\n }, {\n title: \"Groups\",\n url: \"admin/groups\",\n target: \"__use_router__\"\n }, {\n title: \"Roles\",\n url: \"admin/roles\",\n target: \"__use_router__\"\n }, {\n title: \"Forms\",\n url: \"admin/forms\",\n target: \"__use_router__\"\n }, {\n title: \"API keys\",\n url: \"admin/api_keys\",\n target: \"__use_router__\"\n }, {\n title: \"Impersonate a user\",\n url: \"admin/impersonate\",\n enabled: self.config.allow_user_impersonation\n }]\n }, {\n title: \"Tool Management\",\n items: [{\n title: \"Install new tools\",\n url: \"admin_toolshed/browse_tool_sheds\",\n enabled: self.settings.is_tool_shed_installed\n }, {\n title: \"Install new tools (Beta)\",\n url: \"admin_toolshed/browse_toolsheds\",\n enabled: self.settings.is_tool_shed_installed && self.config.enable_beta_ts_api_install\n }, {\n title: \"Monitor installation\",\n url: \"admin_toolshed/monitor_repository_installation\",\n enabled: self.settings.installing_repository_ids\n }, {\n title: \"Manage tools\",\n url: \"admin/repositories\",\n enabled: self.settings.is_repo_installed,\n target: \"__use_router__\"\n }, {\n title: \"Manage metadata\",\n url: \"admin_toolshed/reset_metadata_on_selected_installed_repositories\",\n enabled: self.settings.is_repo_installed\n }, {\n title: \"Manage whitelist\",\n url: \"admin/sanitize_whitelist\"\n }, {\n title: \"Manage dependencies\",\n url: \"admin/manage_tool_dependencies\"\n }, {\n title: \"View lineage\",\n url: \"admin/tool_versions\",\n target: \"__use_router__\"\n }, {\n title: \"View migration stages\",\n url: \"admin/review_tool_migration_stages\"\n }, {\n title: \"View error logs\",\n url: \"admin/tool_errors\"\n }]\n }]);\n this.setElement(this._template());\n },\n\n render: function render() {\n var self = this;\n this.$el.empty();\n this.categories.each(function (category) {\n var $section = $(self._templateSection(category.attributes));\n var $entries = $section.find(\".ui-side-section-body\");\n _.each(category.get(\"items\"), function (item) {\n if (item.enabled === undefined || item.enabled) {\n var $link = $(\"\").attr({ href: self.root + item.url }).text((0, _localization2.default)(item.title));\n if (item.target == \"__use_router__\") {\n $link.on(\"click\", function (e) {\n e.preventDefault();\n self.page.router.push(item.url);\n });\n } else {\n $link.attr(\"target\", \"galaxy_main\");\n }\n $entries.append($(\"\").addClass(\"ui-side-section-body-title\").append($link));\n }\n });\n self.$el.append($section);\n });\n this.page.$(\"#galaxy_main\").prop(\"src\", this.root + \"admin/center?message=\" + this.message + \"&status=\" + this.status);\n },\n\n _templateSection: function _templateSection(options) {\n return [\"
' +\n '' +\n \"\" +\n \"
' +\n '
' +\n '
${this.grid_table(\n options\n )}`;\n }\n\n // add info text\n if (options.info_text) {\n tmpl += `${this.grid_header(\n options\n )} ${options.title}
`;\n }\n if (options.global_actions) {\n tmpl += '';\n var show_popup = options.global_actions.length >= 3;\n if (show_popup) {\n tmpl +=\n '
\";\n }\n if (options.insert) {\n tmpl += options.insert;\n }\n\n // add grid filters\n tmpl += this.grid_filters(options);\n tmpl += \"\";\n\n // add checkbox\n if (options.show_item_checkboxes) {\n tmpl += \" \";\n\n // return template\n return tmpl;\n },\n\n // template\n body: function(options) {\n // initialize\n var tmpl = \"\";\n var num_rows_rendered = 0;\n var items_length = options.items.length;\n\n // empty grid?\n if (items_length == 0) {\n // No results.\n tmpl += '\";\n if (options.items.length > 0) {\n tmpl +=\n '' +\n '';\n }\n tmpl += \" \";\n }\n\n // create header elements\n for (var i in options.columns) {\n var column = options.columns[i];\n if (column.visible) {\n tmpl += ``;\n if (column.href) {\n tmpl += `${column.label}`;\n } else {\n tmpl += column.label;\n }\n tmpl += `${column.extra} `;\n }\n }\n\n // finalize\n tmpl += \" ';\n num_rows_rendered = 1;\n }\n\n // create rows\n for (var i in options.items) {\n // encode ids\n var item = options.items[i];\n var encoded_id = item.encode_id;\n var popupmenu_id = `grid-${i}-popup`;\n\n // Tag current\n tmpl += \"No Items \";\n\n // Item selection column\n if (options.show_item_checkboxes) {\n tmpl += ` \";\n num_rows_rendered++;\n }\n return tmpl;\n },\n\n // template\n footer: function(options) {\n // create template string\n var tmpl = \"\";\n\n // paging\n if (options.use_paging && options.num_pages > 1) {\n // get configuration\n var num_page_links = options.num_page_links;\n var cur_page_num = options.cur_page_num;\n var num_pages = options.num_pages;\n\n // First pass on min page.\n var page_link_range = num_page_links / 2;\n var min_page = cur_page_num - page_link_range;\n var min_offset = 0;\n if (min_page <= 0) {\n // Min page is too low.\n min_page = 1;\n min_offset = page_link_range - (cur_page_num - min_page);\n }\n\n // Set max page.\n var max_range = page_link_range + min_offset;\n var max_page = cur_page_num + max_range;\n var max_offset;\n if (max_page <= num_pages) {\n // Max page is fine.\n max_offset = 0;\n } else {\n // Max page is too high.\n max_page = num_pages;\n // +1 to account for the +1 in the loop below.\n max_offset = max_range - (max_page + 1 - cur_page_num);\n }\n\n // Second and final pass on min page to add any unused\n // offset from max to min.\n if (max_offset != 0) {\n min_page -= max_offset;\n if (min_page < 1) {\n min_page = 1;\n }\n }\n\n // template header\n tmpl += '`;\n }\n\n // Data columns\n for (var j in options.columns) {\n var column = options.columns[j];\n if (column.visible) {\n // Nowrap\n var nowrap = \"\";\n if (column.nowrap) {\n nowrap = 'style=\"white-space:nowrap;\"';\n }\n\n // get column settings\n var column_settings = item.column_config[column.label];\n\n // load attributes\n var link = column_settings.link;\n var value = column_settings.value;\n var target = column_settings.target;\n\n // unescape value\n if (jQuery.type(value) === \"string\") {\n value = value.replace(/\\/\\//g, \"/\");\n }\n\n // Attach popup menu?\n var id = \"\";\n var cls = \"\";\n if (column.attach_popup) {\n id = `grid-${i}-popup`;\n cls = \"menubutton\";\n if (link != \"\") {\n cls += \" split\";\n }\n cls += \" popup\";\n }\n\n // Check for row wrapping\n tmpl += ` `;\n\n // Link\n if (link) {\n if (options.operations.length != 0) {\n tmpl += ` \";\n }\n }\n tmpl += \"';\n if (options.show_item_checkboxes) {\n tmpl += \" \";\n }\n\n // Grid operations for multiple items.\n if (options.show_item_checkboxes) {\n // start template\n tmpl +=\n \"\";\n }\n tmpl += ' ' + '' + \"Page:\";\n\n if (min_page > 1) {\n tmpl +=\n '1 ...';\n }\n\n // create page urls\n for (var page_index = min_page; page_index < max_page + 1; page_index++) {\n if (page_index == options.cur_page_num) {\n tmpl += `${page_index}`;\n } else {\n tmpl += `${\n page_index\n }`;\n }\n }\n\n // show last page\n if (max_page < num_pages) {\n tmpl += `...${\n num_pages\n }`;\n }\n tmpl += \"\";\n\n // Show all link\n tmpl +=\n ' | Show All' +\n \" \" +\n \"\" +\n '' +\n \" \";\n }\n\n // count global operations\n var found_global = false;\n for (i in options.operations) {\n if (options.operations[i].global_operation) {\n found_global = true;\n break;\n }\n }\n\n // add global operations\n if (found_global) {\n tmpl += \"\" +\n ' ' +\n 'For selected items: ';\n\n // configure buttons for operations\n for (var i in options.operations) {\n var operation = options.operations[i];\n if (operation.allow_multiple) {\n tmpl += ` `;\n }\n }\n\n // finalize template\n tmpl += \" \" + \"\" + ' \";\n }\n\n // add legend\n if (options.legend) {\n tmpl += `';\n for (i in options.operations) {\n var operation = options.operations[i];\n if (operation.global_operation) {\n tmpl += `${operation.label}`;\n }\n }\n tmpl += \" \" + \" `;\n }\n\n // return\n return tmpl;\n },\n\n // template\n message: function(options) {\n var status = options.status;\n if ([\"success\", \"ok\"].indexOf(status) != -1) {\n status = \"done\";\n }\n return `${options.legend}
\" + \" \" + \" \" + \"`;\n\n // add standard filters\n for (var i in options.columns) {\n var column = options.columns[i];\n if (column.filterable == \"standard\") {\n tmpl += this.grid_column_filter(options, column);\n }\n }\n\n // finalize standard search\n tmpl += \"
\" + \"\" + \" \" + \"\";\n\n // show advanced search link in standard display\n if (show_advanced_search_link) {\n tmpl += 'Advanced Search';\n }\n\n // finalize standard search display\n tmpl += \" \" + \"
\" + \" `;\n\n // add advanced filters\n for (var i in options.columns) {\n var column = options.columns[i];\n if (column.filterable == \"advanced\") {\n tmpl += this.grid_column_filter(options, column);\n }\n }\n\n // finalize advanced search template\n tmpl += \"Close Advanced Search \";\n\n if (column.filterable == \"advanced\") {\n tmpl += ` \";\n\n // return template\n return tmpl;\n },\n\n // template for filter items\n filter_element: function(filter_key, filter_value) {\n filter_value = Utils.sanitize(filter_value);\n return `${filter_value}`;\n }\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/mvc/grid/grid-template.js","/**\n * Top-level trackster code, used for creating/loading visualizations and user interface elements.\n */\n\n// global variables\nvar ui = null;\nvar view = null;\nvar browser_router = null;\n\n// trackster viewer\nimport * as _ from \"libs/underscore\";\nimport tracks from \"viz/trackster/tracks\";\nimport visualization from \"viz/visualization\";\nimport mod_icon_btn from \"mvc/ui/icon-button\";\nimport query_string from \"utils/query-string-parsing\";\nimport GridView from \"mvc/grid/grid-view\";\nimport mod_utils from \"utils/utils\";\nimport \"libs/jquery/jquery.event.drag\";\nimport \"libs/jquery/jquery.event.hover\";\nimport \"libs/jquery/jquery.mousewheel\";\nimport \"libs/jquery/jquery-ui\";\nimport \"libs/jquery/select2\";\nimport \"libs/farbtastic\";\nimport \"libs/jquery/jquery.form\";\nimport \"libs/jquery/jquery.rating\";\nimport \"ui/editable-text\";\n/**\n * Base Object/Model for inhertiance.\n */\nvar Base = function() {\n if (this.initialize) {\n this.initialize.apply(this, arguments);\n }\n};\nBase.extend = Backbone.Model.extend;\n\n/**\n * User interface controls for trackster\n */\nvar TracksterUI = Base.extend({\n initialize: function(baseURL) {\n mod_utils.cssLoadFile(\"static/style/jquery.rating.css\");\n mod_utils.cssLoadFile(\"static/style/autocomplete_tagging.css\");\n mod_utils.cssLoadFile(\"static/style/jquery-ui/smoothness/jquery-ui.css\");\n mod_utils.cssLoadFile(\"static/style/library.css\");\n mod_utils.cssLoadFile(\"static/style/trackster.css\");\n this.baseURL = baseURL;\n },\n\n /**\n * Save visualization, returning a Deferred object for the remote call to save.\n */\n save_viz: function() {\n // show dialog\n Galaxy.modal.show({ title: \"Saving...\", body: \"progress\" });\n\n // Save bookmarks.\n var bookmarks = [];\n $(\".bookmark\").each(function() {\n bookmarks.push({\n position: $(this)\n .children(\".position\")\n .text(),\n annotation: $(this)\n .children(\".annotation\")\n .text()\n });\n });\n\n // FIXME: give unique IDs to Drawables and save overview as ID.\n var overview_track_name = view.overview_drawable ? view.overview_drawable.config.get_value(\"name\") : null;\n\n var viz_config = {\n view: view.to_dict(),\n viewport: {\n chrom: view.chrom,\n start: view.low,\n end: view.high,\n overview: overview_track_name\n },\n bookmarks: bookmarks\n };\n\n // Make call to save visualization.\n return $.ajax({\n url: `${Galaxy.root}visualization/save`,\n type: \"POST\",\n dataType: \"json\",\n data: {\n id: view.vis_id,\n title: view.config.get_value(\"name\"),\n dbkey: view.dbkey,\n type: \"trackster\",\n vis_json: JSON.stringify(viz_config)\n }\n })\n .success(vis_info => {\n Galaxy.modal.hide();\n view.vis_id = vis_info.vis_id;\n view.has_changes = false;\n\n // Needed to set URL when first saving a visualization.\n window.history.pushState({}, \"\", vis_info.url + window.location.hash);\n })\n .error(() => {\n // show dialog\n Galaxy.modal.show({\n title: \"Could Not Save\",\n body: \"Could not save visualization. Please try again later.\",\n buttons: {\n Cancel: function() {\n Galaxy.modal.hide();\n }\n }\n });\n });\n },\n\n /**\n * Create button menu\n */\n createButtonMenu: function() {\n var self = this;\n\n var menu = mod_icon_btn.create_icon_buttons_menu(\n [\n {\n icon_class: \"plus-button\",\n title: \"Add tracks\",\n on_click: function() {\n visualization.select_datasets({ dbkey: view.dbkey }, new_tracks => {\n _.each(new_tracks, track => {\n view.add_drawable(tracks.object_from_template(track, view, view));\n });\n });\n }\n },\n {\n icon_class: \"block--plus\",\n title: \"Add group\",\n on_click: function() {\n view.add_drawable(\n new tracks.DrawableGroup(view, view, {\n name: \"New Group\"\n })\n );\n }\n },\n {\n icon_class: \"bookmarks\",\n title: \"Bookmarks\",\n on_click: function() {\n // HACK -- use style to determine if panel is hidden and hide/show accordingly.\n force_right_panel($(\"div#right\").css(\"right\") == \"0px\" ? \"hide\" : \"show\");\n }\n },\n {\n icon_class: \"globe\",\n title: \"Circster\",\n on_click: function() {\n window.location = `${self.baseURL}visualization/circster?id=${view.vis_id}`;\n }\n },\n {\n icon_class: \"disk--arrow\",\n title: \"Save\",\n on_click: function() {\n self.save_viz();\n }\n },\n {\n icon_class: \"cross-circle\",\n title: \"Close\",\n on_click: function() {\n self.handle_unsaved_changes(view);\n }\n }\n ],\n {\n tooltip_config: { placement: \"bottom\" }\n }\n );\n\n this.buttonMenu = menu;\n return menu;\n },\n\n /**\n * Add bookmark.\n */\n add_bookmark: function(position, annotation, editable) {\n // Create HTML.\n var bookmarks_container = $(\"#right .unified-panel-body\");\n\n var new_bookmark = $(\"\")\n .addClass(\"bookmark\")\n .appendTo(bookmarks_container);\n\n var position_div = $(\"\")\n .addClass(\"position\")\n .appendTo(new_bookmark);\n\n var position_link = $(\"\")\n .text(position)\n .appendTo(position_div)\n .click(() => {\n view.go_to(position);\n return false;\n });\n\n var annotation_div = $(\"\")\n .text(annotation)\n .appendTo(new_bookmark);\n\n // If editable, enable bookmark deletion and annotation editing.\n if (editable) {\n var delete_icon_container = $(\"\")\n .addClass(\"delete-icon-container\")\n .prependTo(new_bookmark)\n .click(() => {\n // Remove bookmark.\n new_bookmark.slideUp(\"fast\");\n new_bookmark.remove();\n view.has_changes = true;\n return false;\n });\n\n var delete_icon = $(\"\")\n .addClass(\"icon-button delete\")\n .appendTo(delete_icon_container);\n\n annotation_div\n .make_text_editable({\n num_rows: 3,\n use_textarea: true,\n help_text: \"Edit bookmark note\"\n })\n .addClass(\"annotation\");\n }\n\n view.has_changes = true;\n return new_bookmark;\n },\n\n /**\n * Create a complete Trackster visualization. Returns view.\n */\n create_visualization: function(view_config, viewport_config, drawables_config, bookmarks_config, editable) {\n // Create view.\n var self = this;\n\n var view = new tracks.TracksterView(_.extend(view_config, { header: false }));\n\n view.editor = true;\n $.when(view.load_chroms_deferred).then(chrom_info => {\n // Viewport config.\n if (viewport_config) {\n var chrom = viewport_config.chrom;\n var start = viewport_config.start;\n var end = viewport_config.end;\n var overview_drawable_name = viewport_config.overview;\n\n if (chrom && start !== undefined && end) {\n view.change_chrom(chrom, start, end);\n } else {\n // No valid viewport, so use first chromosome.\n view.change_chrom(chrom_info[0].chrom);\n }\n } else {\n // No viewport, so use first chromosome.\n view.change_chrom(chrom_info[0].chrom);\n }\n\n // Add drawables to view.\n if (drawables_config) {\n // FIXME: can from_dict() be used to create view and add drawables?\n var drawable_config;\n\n var drawable_type;\n var drawable;\n for (var i = 0; i < drawables_config.length; i++) {\n view.add_drawable(tracks.object_from_template(drawables_config[i], view, view));\n }\n }\n\n // Set overview.\n var overview_drawable;\n for (var i = 0; i < view.drawables.length; i++) {\n if (view.drawables[i].config.get_value(\"name\") === overview_drawable_name) {\n view.set_overview(view.drawables[i]);\n break;\n }\n }\n\n // Load bookmarks.\n if (bookmarks_config) {\n var bookmark;\n for (var i = 0; i < bookmarks_config.length; i++) {\n bookmark = bookmarks_config[i];\n self.add_bookmark(bookmark[\"position\"], bookmark[\"annotation\"], editable);\n }\n }\n\n // View has no changes as of yet.\n view.has_changes = false;\n });\n\n // Final initialization.\n this.set_up_router({ view: view });\n\n return view;\n },\n\n /**\n * Set up location router to use hashes as track browser locations.\n */\n set_up_router: function(options) {\n new visualization.TrackBrowserRouter(options);\n Backbone.history.start();\n },\n\n /**\n * Set up keyboard navigation for a visualization.\n */\n init_keyboard_nav: function(view) {\n // Keyboard navigation. Scroll ~7% of height when scrolling up/down.\n $(document).keyup(e => {\n // Do not navigate if arrow keys used in input element.\n if ($(e.srcElement).is(\":input\")) {\n return;\n }\n\n // Key codes: left == 37, up == 38, right == 39, down == 40\n switch (e.which) {\n case 37:\n view.move_fraction(0.25);\n break;\n case 38:\n var change = Math.round(view.viewport_container.height() / 15.0);\n view.viewport_container.scrollTop(view.viewport_container.scrollTop() - 20);\n break;\n case 39:\n view.move_fraction(-0.25);\n break;\n case 40:\n var change = Math.round(view.viewport_container.height() / 15.0);\n view.viewport_container.scrollTop(view.viewport_container.scrollTop() + 20);\n break;\n }\n });\n },\n\n /**\n * Handle unsaved changes in visualization.\n */\n handle_unsaved_changes: function(view) {\n if (view.has_changes) {\n var self = this;\n Galaxy.modal.show({\n title: \"Close visualization\",\n body: \"There are unsaved changes to your visualization which will be lost if you do not save them.\",\n buttons: {\n Cancel: function() {\n Galaxy.modal.hide();\n },\n \"Leave without Saving\": function() {\n $(window).off(\"beforeunload\");\n window.location = `${Galaxy.root}visualization`;\n },\n Save: function() {\n $.when(self.save_viz()).then(() => {\n window.location = `${Galaxy.root}visualization`;\n });\n }\n }\n });\n } else {\n window.location = `${Galaxy.root}visualization`;\n }\n }\n});\n\nvar TracksterView = Backbone.View.extend({\n // initalize trackster\n initialize: function() {\n // load ui\n ui = new TracksterUI(Galaxy.root);\n\n // create button menu\n ui.createButtonMenu();\n\n // attach the button menu to the panel header and float it left\n ui.buttonMenu.$el.attr(\"style\", \"float: right\");\n\n // add to center panel\n $(\"#center .unified-panel-header-inner\").append(ui.buttonMenu.$el);\n\n // configure right panel\n $(\"#right .unified-panel-title\").append(\"Bookmarks\");\n $(\"#right .unified-panel-icons\").append(\n \"\"\n );\n\n // resize view when showing/hiding right panel (bookmarks for now).\n $(\"#right-border\").click(() => {\n view.resize_window();\n });\n\n // hide right panel\n force_right_panel(\"hide\");\n\n // check if id is available\n if (galaxy_config.app.id) {\n this.view_existing();\n } else if (query_string.get(\"dataset_id\")) {\n this.choose_existing_or_new();\n } else {\n this.view_new();\n }\n },\n\n choose_existing_or_new: function() {\n var self = this;\n var dbkey = query_string.get(\"dbkey\");\n var listTracksParams = {};\n\n var dataset_params = {\n dbkey: dbkey,\n dataset_id: query_string.get(\"dataset_id\"),\n hda_ldda: query_string.get(\"hda_ldda\"),\n gene_region: query_string.get(\"gene_region\")\n };\n\n if (dbkey) {\n listTracksParams[\"f-dbkey\"] = dbkey;\n }\n\n Galaxy.modal.show({\n title: \"View Data in a New or Saved Visualization?\",\n // either have text in here or have to remove body and the header/footer margins\n body: `${column_label}: `;\n }\n tmpl += '';\n if (column.is_text) {\n tmpl += ``;\n } else {\n // filter criteria\n tmpl += ``;\n\n // add category filters\n var seperator = false;\n for (var cf_label in options.categorical_filters[column_key]) {\n // get category filter\n var cf = options.categorical_filters[column_key][cf_label];\n\n // each filter will have only a single argument, so get that single argument\n var cf_key = \"\";\n var cf_arg = \"\";\n for (var key in cf) {\n cf_key = key;\n cf_arg = cf[key];\n }\n\n // add seperator\n if (seperator) {\n tmpl += \" | \";\n }\n seperator = true;\n\n // add category\n var filter = filters[column_key];\n if (filter && cf[column_key] && filter == cf_arg) {\n tmpl += `${cf_label}`;\n } else {\n tmpl += `${\n cf_label\n }`;\n }\n }\n tmpl += \"\";\n }\n tmpl += \" \" + \"You can add this dataset as:
`,\n buttons: {\n Cancel: function() {\n window.location = `${Galaxy.root}visualizations/list`;\n },\n \"View in saved visualization\": function() {\n self.view_in_saved(dataset_params);\n },\n \"View in new visualization\": function() {\n self.view_new();\n }\n }\n });\n },\n\n // view\n view_in_saved: function(dataset_params) {\n var tracks_grid = new GridView({\n url_base: `${Galaxy.root}visualization/list_tracks`,\n dict_format: true,\n embedded: true\n });\n Galaxy.modal.show({\n title: \"Add Data to Saved Visualization\",\n body: tracks_grid.$el,\n buttons: {\n Cancel: function() {\n window.location = `${Galaxy.root}visualizations/list`;\n },\n \"Add to visualization\": function() {\n $(parent.document)\n .find(\"input[name=id]:checked\")\n .each(function() {\n dataset_params.id = $(this).val();\n window.location = `${Galaxy.root}visualization/trackster?${$.param(dataset_params)}`;\n });\n }\n }\n });\n },\n\n // view\n view_existing: function() {\n // get config\n var viz_config = galaxy_config.app.viz_config;\n\n // view\n view = ui.create_visualization(\n {\n container: $(\"#center .unified-panel-body\"),\n name: viz_config.title,\n vis_id: viz_config.vis_id,\n dbkey: viz_config.dbkey\n },\n viz_config.viewport,\n viz_config.tracks,\n viz_config.bookmarks,\n true\n );\n\n // initialize editor\n this.init_editor();\n },\n\n // view\n view_new: function() {\n // reference this\n var self = this;\n\n // ajax\n $.ajax({\n url: `${Galaxy.root}api/genomes?chrom_info=True`,\n data: {},\n error: function() {\n alert(\"Couldn't create new browser.\");\n },\n success: function(response) {\n // show dialog\n Galaxy.modal.show({\n title: \"New Visualization\",\n body: self.template_view_new(response),\n buttons: {\n Cancel: function() {\n window.location = `${Galaxy.root}visualizations/list`;\n },\n Create: function() {\n self.create_browser($(\"#new-title\").val(), $(\"#new-dbkey\").val());\n Galaxy.modal.hide();\n }\n }\n });\n\n // select default\n var dbkeys_in_genomes = response.map(r => r[1]);\n if (galaxy_config.app.default_dbkey && _.contains(dbkeys_in_genomes, galaxy_config.app.default_dbkey)) {\n $(\"#new-dbkey\").val(galaxy_config.app.default_dbkey);\n }\n\n // change focus\n $(\"#new-title\").focus();\n $(\"select[name='dbkey']\").select2();\n\n // to support the large number of options for dbkey, enable scrolling in overlay.\n $(\"#overlay\").css(\"overflow\", \"auto\");\n }\n });\n },\n\n // new browser form\n template_view_new: function(response) {\n // start template\n var html =\n '`;\n\n // return\n return html;\n },\n\n // create\n create_browser: function(name, dbkey) {\n $(document).trigger(\"convert_to_values\");\n\n view = ui.create_visualization(\n {\n container: $(\"#center .unified-panel-body\"),\n name: name,\n dbkey: dbkey\n },\n galaxy_config.app.gene_region\n );\n\n // initialize editor\n this.init_editor();\n\n // modify view setting\n view.editor = true;\n },\n\n // initialization for editor-specific functions.\n init_editor: function() {\n // set title\n $(\"#center .unified-panel-title\").text(`${view.config.get_value(\"name\")} (${view.dbkey})`);\n\n // add dataset\n if (galaxy_config.app.add_dataset)\n $.ajax({\n url: `${Galaxy.root}api/datasets/${galaxy_config.app.add_dataset}`,\n data: { hda_ldda: \"hda\", data_type: \"track_config\" },\n dataType: \"json\",\n success: function(track_data) {\n view.add_drawable(tracks.object_from_template(track_data, view, view));\n }\n });\n\n // initialize icons\n $(\"#add-bookmark-button\").click(() => {\n // add new bookmark.\n var position = `${view.chrom}:${view.low}-${view.high}`;\n\n var annotation = \"Bookmark description\";\n return ui.add_bookmark(position, annotation, true);\n });\n\n // initialize keyboard\n ui.init_keyboard_nav(view);\n\n $(window).on(\"beforeunload\", () => {\n if (view.has_changes) {\n return \"There are unsaved changes to your visualization that will be lost if you leave this page.\";\n }\n });\n }\n});\n\nexport default {\n TracksterUI: TracksterUI,\n GalaxyApp: TracksterView\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/viz/trackster.js","import * as _ from \"libs/underscore\";\nimport visualization from \"viz/visualization\";\nimport viz_views from \"viz/viz_views\";\nimport util from \"viz/trackster/util\";\nimport slotting from \"viz/trackster/slotting\";\nimport painters from \"viz/trackster/painters\";\nimport filters_mod from \"viz/trackster/filters\";\nimport data from \"mvc/dataset/data\";\nimport tools_mod from \"mvc/tool/tools\";\nimport config_mod from \"utils/config\";\nimport bbi from \"viz/bbi-data-manager\";\nimport \"ui/editable-text\";\nvar extend = _.extend;\n\n// ---- Web UI specific utilities ----\n\n/**\n * Dictionary of HTML element-JavaScript object relationships.\n */\n// TODO: probably should separate moveable objects from containers.\nvar html_elt_js_obj_dict = {};\n\n/**\n * Designates an HTML as a container.\n */\nvar is_container = (element, obj) => {\n html_elt_js_obj_dict[element.attr(\"id\")] = obj;\n};\n\n/**\n * Make `element` moveable within parent and sibling elements by dragging `handle` (a selector).\n * Function manages JS objects, containers as well.\n *\n * @param element HTML element to make moveable\n * @param handle_class classname that denotes HTML element to be used as handle\n * @param container_selector selector used to identify possible containers for this element\n * @param element_js_obj JavaScript object associated with element; used\n */\nvar moveable = (element, handle_class, container_selector, element_js_obj) => {\n // HACK: set default value for container selector.\n container_selector = \".group\";\n\n // Register element with its object.\n html_elt_js_obj_dict[element.attr(\"id\")] = element_js_obj;\n\n // Need to provide selector for handle, not class.\n element\n .bind(\"drag\", { handle: `.${handle_class}`, relative: true }, function(e, d) {\n var element = $(this);\n var parent = $(this).parent();\n\n var // Only sorting amongst tracks and groups.\n children = parent.children(\".track,.group\");\n\n var this_obj = html_elt_js_obj_dict[$(this).attr(\"id\")];\n var child;\n var container;\n var top;\n var bottom;\n var i;\n\n //\n // Enable three types of dragging: (a) out of container; (b) into container;\n // (c) sibling movement, aka sorting. Handle in this order for simplicity.\n //\n\n // Handle dragging out of container.\n container = $(this).parents(container_selector);\n if (container.length !== 0) {\n top = container.position().top;\n bottom = top + container.outerHeight();\n var cur_container = html_elt_js_obj_dict[container.attr(\"id\")];\n if (d.offsetY < top) {\n // Moving above container.\n $(this).insertBefore(container);\n cur_container.remove_drawable(this_obj);\n cur_container.container.add_drawable_before(this_obj, cur_container);\n return;\n } else if (d.offsetY > bottom) {\n // Moving below container.\n $(this).insertAfter(container);\n cur_container.remove_drawable(this_obj);\n cur_container.container.add_drawable(this_obj);\n return;\n }\n }\n\n // Handle dragging into container. Child is appended to container's content_div.\n container = null;\n for (i = 0; i < children.length; i++) {\n child = $(children.get(i));\n top = child.position().top;\n bottom = top + child.outerHeight();\n // Dragging into container if child is a container and offset is inside container.\n if (child.is(container_selector) && this !== child.get(0) && d.offsetY >= top && d.offsetY <= bottom) {\n // Append/prepend based on where offsetY is closest to and return.\n if (d.offsetY - top < bottom - d.offsetY) {\n child.find(\".content-div\").prepend(this);\n } else {\n child.find(\".content-div\").append(this);\n }\n // Update containers. Object may not have container if it is being moved quickly.\n if (this_obj.container) {\n this_obj.container.remove_drawable(this_obj);\n }\n html_elt_js_obj_dict[child.attr(\"id\")].add_drawable(this_obj);\n return;\n }\n }\n\n // Handle sibling movement, aka sorting.\n\n // Determine new position\n for (i = 0; i < children.length; i++) {\n child = $(children.get(i));\n if (\n d.offsetY < child.position().top &&\n // Cannot move tracks above reference track or intro div.\n !(child.hasClass(\"reference-track\") || child.hasClass(\"intro\"))\n ) {\n break;\n }\n }\n\n // If not already in the right place, move. Need\n // to handle the end specially since we don't have\n // insert at index\n if (i === children.length) {\n if (this !== children.get(i - 1)) {\n parent.append(this);\n html_elt_js_obj_dict[parent.attr(\"id\")].move_drawable(this_obj, i);\n }\n } else if (this !== children.get(i)) {\n $(this).insertBefore(children.get(i));\n // Need to adjust insert position if moving down because move is changing\n // indices of all list items.\n html_elt_js_obj_dict[parent.attr(\"id\")].move_drawable(this_obj, d.deltaY > 0 ? i - 1 : i);\n }\n })\n .bind(\"dragstart\", function() {\n $(this).addClass(\"dragging\");\n })\n .bind(\"dragend\", function() {\n $(this).removeClass(\"dragging\");\n });\n};\n\n/**\n * Init constants & functions used throughout trackster.\n */\nvar // Padding at the top of tracks for error messages\nERROR_PADDING = 20;\n\nvar // Maximum number of rows un a slotted track\nMAX_FEATURE_DEPTH = 100;\n\nvar // Minimum width for window for squish to be used.\nMIN_SQUISH_VIEW_WIDTH = 12000;\n\nvar // Number of pixels per tile, not including left offset.\nTILE_SIZE = 400;\n\nvar DEFAULT_DATA_QUERY_WAIT = 5000;\n\nvar // Maximum number of chromosomes that are selectable at any one time.\nMAX_CHROMS_SELECTABLE = 100;\n\nvar DATA_ERROR = \"Cannot display dataset due to an error. \";\n\nvar DATA_NOCONVERTER = \"A converter for this dataset is not installed. Please check your datatypes_conf.xml file.\";\n\nvar DATA_NONE = \"No data for this chrom/contig.\";\n\nvar DATA_PENDING =\n \"Preparing data. This can take a while for a large dataset. \" +\n \"If the visualization is saved and closed, preparation will continue in the background.\";\n\nvar DATA_CANNOT_RUN_TOOL = \"Tool cannot be rerun: \";\nvar DATA_LOADING = \"Loading data...\";\nvar DATA_OK = \"Ready for display\";\nvar TILE_CACHE_SIZE = 10;\nvar DATA_CACHE_SIZE = 20;\n\nvar // Numerical/continuous data display modes.\nCONTINUOUS_DATA_MODES = [\"Histogram\", \"Line\", \"Filled\", \"Intensity\"];\n\n/**\n * Round a number to a given number of decimal places.\n */\nfunction round(num, places) {\n // Default rounding is to integer.\n if (!places) {\n places = 0;\n }\n\n var val = Math.pow(10, places);\n return Math.round(num * val) / val;\n}\n\n/**\n * Check if a server can do byte range requests.\n */\nfunction supportsByteRanges(url) {\n var promise = $.Deferred();\n $.ajax({\n type: \"HEAD\",\n url: url,\n beforeSend: function(xhr) {\n xhr.setRequestHeader(\"Range\", \"bytes=0-10\");\n },\n success: function(result, status, xhr) {\n promise.resolve(xhr.status === 206);\n }\n });\n\n return promise;\n}\n\n/**\n * Drawables hierarchy:\n *\n * Drawable\n * --> DrawableCollection\n * --> DrawableGroup\n * --> View\n * --> Track\n */\n\n/**\n * Base class for all drawable objects. Drawable objects are associated with a view and live in a\n * container. They have the following HTML elements and structure:\n * \"),\n key, value, row;\n for (key in feature_dict) {\n value = feature_dict[key];\n row = $(\"
\").appendTo(table);\n $(\" \").appendTo(row).text(key);\n $(\" \").attr(\"align\", \"left\").appendTo(row)\n .text(typeof(value) === 'number' ? round(value, 2) : value);\n }\n popup.append( $(\"
Dataset:${track.config.get_value(\n \"name\"\n )}
Region(s): `;\n\n var cancel_fn = () => {\n Galaxy.modal.hide();\n $(window).unbind(\"keypress.check_enter_esc\");\n };\n\n var ok_fn = () => {\n var regions_to_use = $('select[name=\"regions\"] option:selected').val(),\n regions,\n view_region = new visualization.GenomeRegion({\n chrom: view.chrom,\n start: view.low,\n end: view.high\n }),\n bookmarked_regions = _.map(\n $(\".bookmark\"),\n elt =>\n new visualization.GenomeRegion({\n from_str: $(elt)\n .children(\".position\")\n .text()\n })\n );\n\n // Get regions for visualization.\n if (regions_to_use === \"cur\") {\n // Use only current region.\n regions = [view_region];\n } else if (regions_to_use === \"bookmarks\") {\n // Use only bookmarks.\n regions = bookmarked_regions;\n } else {\n // Use both current region and bookmarks.\n regions = [view_region].concat(bookmarked_regions);\n }\n\n Galaxy.modal.hide();\n\n // Go to visualization.\n window.location.href = `${Galaxy.root}visualization/sweepster?${$.param({\n dataset_id: track.dataset.id,\n hda_ldda: track.dataset.get(\"hda_ldda\"),\n regions: JSON.stringify(new Backbone.Collection(regions).toJSON())\n })}`;\n };\n\n var check_enter_esc = e => {\n if ((e.keyCode || e.which) === 27) {\n // Escape key\n cancel_fn();\n } else if ((e.keyCode || e.which) === 13) {\n // Enter key\n ok_fn();\n }\n };\n\n // show dialog\n Galaxy.modal.show({\n title: \"Visualize tool parameter space and output from different parameter settings?\",\n body: html,\n buttons: { No: cancel_fn, Yes: ok_fn }\n });\n }\n },\n // Remove track.\n Drawable.prototype.action_icons_def[2]\n ],\n\n can_draw: function() {\n return this.dataset && Drawable.prototype.can_draw.call(this);\n },\n\n build_container_div: function() {\n return $(\"\")\n .addClass(\"track\")\n .attr(\"id\", `track_${this.id}`);\n },\n\n /**\n * Set track's dataset.\n */\n set_dataset: function(dataset) {\n this.dataset = dataset;\n this.data_manager.set(\"dataset\", dataset);\n },\n\n /**\n * Action to take during resize.\n */\n on_resize: function() {\n this.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Add resizing handle to drawable's container_div.\n */\n add_resize_handle: function() {\n var track = this;\n var in_handle = false;\n var in_drag = false;\n var drag_control = $(\"${result.message}
`,\n buttons: {\n Close: function() {\n Galaxy.modal.hide();\n }\n }\n });\n })\n );\n msg_elt.append($(\"\").text(\" \"));\n msg_elt.append(\n $(\"\")\n .text(\"Try again\")\n .click(() => {\n track.init(true);\n })\n );\n }\n } else if (result === \"no converter\") {\n track.container_div.addClass(\"error\");\n track.show_message(DATA_NOCONVERTER);\n } else if (\n result === \"no data\" ||\n (result.data !== undefined && (result.data === null || result.data.length === 0))\n ) {\n track.container_div.addClass(\"nodata\");\n track.show_message(DATA_NONE);\n } else if (result === \"pending\") {\n track.container_div.addClass(\"pending\");\n track.show_message(DATA_PENDING);\n //$(\"\").attr(\"src\", image_path + \"/yui/rel_interstitial_loading.gif\").appendTo(track.tiles_div);\n setTimeout(() => {\n track.init();\n }, track.data_query_wait);\n } else if (result === \"data\" || result.status === \"data\") {\n if (result.valid_chroms) {\n track.valid_chroms = result.valid_chroms;\n track.update_icons();\n }\n track.tiles_div.text(DATA_OK);\n if (track.view.chrom) {\n track.tiles_div.text(\"\");\n track.tiles_div.css(\"height\", `${track.visible_height_px}px`);\n track.enabled = true;\n // predraw_init may be asynchronous, wait for it and then draw\n $.when.apply($, track.predraw_init()).done(() => {\n init_deferred.resolve();\n track.container_div.removeClass(\"nodata error pending\");\n track.request_draw();\n });\n } else {\n init_deferred.resolve();\n }\n }\n });\n\n this.update_icons();\n return init_deferred;\n },\n\n /**\n * Additional initialization required before drawing track for the first time.\n */\n predraw_init: function() {\n var track = this;\n return $.getJSON(\n track.dataset.url(),\n {\n data_type: \"data\",\n stats: true,\n chrom: track.view.chrom,\n low: 0,\n high: track.view.max_high,\n hda_ldda: track.dataset.get(\"hda_ldda\")\n },\n result => {\n var data = result.data;\n\n // Tracks may not have stat data either because there is no data or data is not yet ready.\n if (data && data.min !== undefined && data.max !== undefined) {\n // Compute default minimum and maximum values\n var min_value = data.min;\n\n var max_value = data.max;\n // If mean and sd are present, use them to compute a ~95% window\n // but only if it would shrink the range on one side\n min_value = Math.floor(Math.min(0, Math.max(min_value, data.mean - 2 * data.sd)));\n max_value = Math.ceil(Math.max(0, Math.min(max_value, data.mean + 2 * data.sd)));\n // Update config, prefs\n track.config.set_default_value(\"min_value\", min_value);\n track.config.set_default_value(\"max_value\", max_value);\n track.config.set_value(\"min_value\", min_value);\n track.config.set_value(\"max_value\", max_value);\n }\n }\n );\n },\n\n /**\n * Returns all drawables in this drawable.\n */\n get_drawables: function() {\n return this;\n }\n});\n\nvar TiledTrack = function(view, container, obj_dict) {\n Track.call(this, view, container, obj_dict);\n\n var track = this;\n\n // Make track moveable.\n moveable(track.container_div, track.drag_handle_class, \".group\", track);\n\n // Attribute init.\n this.filters_manager = new filters_mod.FiltersManager(this, \"filters\" in obj_dict ? obj_dict.filters : null);\n // HACK: set filters manager for data manager.\n // FIXME: prolly need function to set filters and update data_manager reference.\n this.data_manager.set(\"filters_manager\", this.filters_manager);\n this.filters_available = false;\n this.tool = obj_dict.tool\n ? new TracksterTool(\n _.extend(obj_dict.tool, {\n track: this,\n tool_state: obj_dict.tool_state\n })\n )\n : null;\n this.tile_cache = new visualization.Cache(TILE_CACHE_SIZE);\n this.left_offset = 0;\n\n if (this.header_div) {\n //\n // Setup filters.\n //\n this.set_filters_manager(this.filters_manager);\n\n //\n // Create dynamic tool view.\n //\n if (this.tool) {\n var tool_view = new TracksterToolView({ model: this.tool });\n tool_view.render();\n this.dynamic_tool_div = tool_view.$el;\n this.header_div.after(this.dynamic_tool_div);\n }\n }\n\n // Add tiles_div, overlay_div to content_div.\n this.tiles_div = $(\"\")\n .addClass(\"tiles\")\n .appendTo(this.content_div);\n if (!this.config.get_value(\"content_visible\")) {\n this.tiles_div.hide();\n }\n this.overlay_div = $(\"\")\n .addClass(\"overlay\")\n .appendTo(this.content_div);\n\n if (obj_dict.mode) {\n this.change_mode(obj_dict.mode);\n }\n};\nextend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {\n action_icons_def: Track.prototype.action_icons_def.concat([\n // Show more rows when all features are not slotted.\n {\n name: \"show_more_rows_icon\",\n title: \"To minimize track height, not all feature rows are displayed. Click to display more rows.\",\n css_class: \"exclamation\",\n on_click_fn: function(track) {\n $(\".tooltip\").remove();\n track.slotters[track.view.resolution_px_b].max_rows *= 2;\n track.request_draw({ clear_tile_cache: true });\n },\n hide: true\n }\n ]),\n\n /**\n * Returns a copy of the track. The copy uses the same data manager so that the tracks can share data.\n */\n copy: function(container) {\n // Create copy.\n var obj_dict = this.to_dict();\n extend(obj_dict, {\n data_manager: this.data_manager\n });\n var new_track = new this.constructor(this.view, container, obj_dict);\n // Misc. init and return.\n new_track.change_mode(this.mode);\n new_track.enabled = this.enabled;\n return new_track;\n },\n\n /**\n * Set filters manager + HTML elements.\n */\n set_filters_manager: function(filters_manager) {\n this.filters_manager = filters_manager;\n this.header_div.after(this.filters_manager.parent_div);\n },\n\n /**\n * Returns representation of object in a dictionary for easy saving.\n * Use from_dict to recreate object.\n */\n to_dict: function() {\n return {\n track_type: this.get_type(),\n dataset: {\n id: this.dataset.id,\n hda_ldda: this.dataset.get(\"hda_ldda\")\n },\n prefs: this.config.to_key_value_dict(),\n mode: this.mode,\n filters: this.filters_manager.to_dict(),\n tool_state: this.tool ? this.tool.state_dict() : {}\n };\n },\n\n /**\n * Set track bounds for current chromosome.\n */\n set_min_max: function() {\n var track = this;\n\n return $.getJSON(\n track.dataset.url(),\n {\n data_type: \"data\",\n stats: true,\n chrom: track.view.chrom,\n low: 0,\n high: track.view.max_high,\n hda_ldda: track.dataset.get(\"hda_ldda\")\n },\n result => {\n var data = result.data;\n if (\n isNaN(parseFloat(track.config.get_value(\"min_value\"))) ||\n isNaN(parseFloat(track.config.get_value(\"max_value\")))\n ) {\n // Compute default minimum and maximum values\n var min_value = data.min;\n\n var max_value = data.max;\n // If mean and sd are present, use them to compute a ~95% window\n // but only if it would shrink the range on one side\n min_value = Math.floor(Math.min(0, Math.max(min_value, data.mean - 2 * data.sd)));\n max_value = Math.ceil(Math.max(0, Math.min(max_value, data.mean + 2 * data.sd)));\n // Update the prefs\n track.config.set_value(\"min_value\", min_value);\n track.config.set_value(\"max_value\", max_value);\n }\n }\n );\n },\n\n /**\n * Change track's mode.\n */\n change_mode: function(new_mode) {\n var track = this;\n // TODO: is it necessary to store the mode in two places (.mode and track_config)?\n track.mode = new_mode;\n track.config.set_value(\"mode\", new_mode);\n // FIXME: find a better way to get Auto data w/o clearing cache; using mode in the\n // data manager would work if Auto data were checked for compatibility when a specific\n // mode is chosen.\n if (new_mode === \"Auto\") {\n this.data_manager.clear();\n }\n track.request_draw({ clear_tile_cache: true });\n this.action_icons.mode_icon.attr(\"title\", `Set display mode (now: ${track.mode})`);\n return track;\n },\n\n /**\n * Update track's buttons.\n */\n update_icons: function() {\n var track = this;\n\n //\n // Show/hide filter icon.\n //\n track.action_icons.filters_icon.toggle(track.filters_available);\n\n //\n // Show/hide tool icons.\n //\n track.action_icons.tools_icon.toggle(track.tool !== null);\n track.action_icons.param_space_viz_icon.toggle(track.tool !== null);\n },\n\n /**\n * Generate a key for the tile cache.\n * TODO: create a TileCache object (like DataCache) and generate key internally.\n */\n _gen_tile_cache_key: function(w_scale, tile_region) {\n return `${w_scale}_${tile_region}`;\n },\n\n /**\n * Request that track be drawn.\n */\n request_draw: function(options) {\n if (options && options.clear_tile_cache) {\n this.tile_cache.clear();\n }\n this.view.request_redraw(options, this);\n },\n\n /**\n * Actions to be taken before drawing.\n */\n before_draw: function() {\n // Clear because this is set when drawing.\n this.max_height_px = 0;\n },\n\n /**\n * Draw track. Options include:\n * -force: force a redraw rather than use cached tiles (default: false)\n * -clear_after: clear old tiles after drawing new tiles (default: false)\n * -data_fetch: fetch data if necessary (default: true)\n *\n * NOTE: this function should never be called directly; use request_draw() so that drawing\n * management can be used.\n */\n _draw: function(options) {\n if (!this.can_draw()) {\n return;\n }\n\n var clear_after = options && options.clear_after;\n var low = this.view.low;\n var high = this.view.high;\n var range = high - low;\n var width = this.view.container.width();\n var w_scale = this.view.resolution_px_b;\n var resolution = 1 / w_scale;\n\n // For overview, adjust high, low, resolution, and w_scale.\n if (this.is_overview) {\n low = this.view.max_low;\n high = this.view.max_high;\n w_scale = width / (view.max_high - view.max_low);\n resolution = 1 / w_scale;\n }\n\n this.before_draw();\n\n //\n // Method for moving and/or removing tiles:\n // (a) mark all elements for removal using class 'remove'\n // (b) during tile drawing/placement, remove class for elements that are moved;\n // this occurs in show_tile()\n // (c) after drawing tiles, remove elements still marked for removal\n // (i.e. that still have class 'remove').\n //\n\n // Step (a) for (re)moving tiles.\n this.tiles_div.children().addClass(\"remove\");\n\n var // Tile width in bases.\n tile_width = Math.floor(TILE_SIZE * resolution);\n\n var // Index of first tile that overlaps visible region.\n tile_index = Math.floor(low / tile_width);\n\n var tile_region;\n var tile_promise;\n var tile_promises = [];\n var tiles = [];\n // Draw tiles.\n while (tile_index * tile_width < high) {\n // Get tile region.\n tile_region = new visualization.GenomeRegion({\n chrom: this.view.chrom,\n start: tile_index * tile_width,\n // Tile high cannot be larger than view.max_high, which the chromosome length.\n end: Math.min((tile_index + 1) * tile_width, this.view.max_high)\n });\n tile_promise = this.draw_helper(tile_region, w_scale, options);\n tile_promises.push(tile_promise);\n $.when(tile_promise).then(tile => {\n tiles.push(tile);\n });\n\n // Go to next tile.\n tile_index += 1;\n }\n\n // Step (c) for (re)moving tiles when clear_after is false.\n if (!clear_after) {\n this.tiles_div\n .children(\".remove\")\n .removeClass(\"remove\")\n .remove();\n }\n\n // When all tiles are drawn, call post-draw actions.\n var track = this;\n $.when.apply($, tile_promises).then(() => {\n // Step (c) for (re)moving tiles when clear_after is true:\n track.tiles_div.children(\".remove\").remove();\n\n // Only do postdraw actions for tiles; instances where tiles may not be drawn include:\n // (a) ReferenceTrack without sufficient resolution;\n // (b) data_fetch = false.\n tiles = _.filter(tiles, t => t !== null);\n if (tiles.length !== 0) {\n track.postdraw_actions(tiles, width, w_scale, clear_after);\n }\n });\n },\n\n /**\n * Add a maximum/minimum label to track.\n */\n _add_yaxis_label: function(type, on_change) {\n var track = this;\n var css_class = type === \"max\" ? \"top\" : \"bottom\";\n var text = type === \"max\" ? \"max\" : \"min\";\n var pref_name = type === \"max\" ? \"max_value\" : \"min_value\";\n var label = this.container_div.find(`.yaxislabel.${css_class}`);\n var value = round(track.config.get_value(pref_name), 1);\n\n // Default action for on_change is to redraw track.\n on_change =\n on_change ||\n (() => {\n track.request_draw({ clear_tile_cache: true });\n });\n\n if (label.length !== 0) {\n // Label already exists, so update value.\n label.text(value);\n } else {\n // Add label.\n label = $(\"\")\n .text(value)\n .make_text_editable({\n num_cols: 12,\n on_finish: function(new_val) {\n $(\".tooltip\").remove();\n track.config.set_value(pref_name, round(new_val, 1));\n on_change();\n },\n help_text: `Set ${text} value`\n })\n .addClass(`yaxislabel ${css_class}`)\n .css(\"color\", this.config.get_value(\"label_color\"));\n this.container_div.prepend(label);\n }\n },\n\n /**\n * Actions to be taken after draw has been completed. Draw is completed when all tiles have been\n * drawn/fetched and shown.\n */\n postdraw_actions: function(tiles, width, w_scale, clear_after) {\n var line_track_tiles = _.filter(tiles, tile => tile instanceof LineTrackTile);\n\n //\n // Take different actions depending on whether there are LineTrack/Coverage tiles.\n //\n\n if (line_track_tiles.length > 0) {\n // -- Drawing in Coverage mode. --\n\n // Clear because this is set when drawing.\n this.max_height_px = 0;\n var track = this;\n _.each(tiles, tile => {\n if (!(tile instanceof LineTrackTile)) {\n tile.html_elt.remove();\n track.draw_helper(tile.region, w_scale, {\n force: true,\n mode: \"Coverage\"\n });\n }\n });\n\n track._add_yaxis_label(\"max\");\n } else {\n // -- Drawing in non-Coverage mode. --\n\n // Remove Y-axis labels because there are no line track tiles.\n this.container_div.find(\".yaxislabel\").remove();\n\n //\n // If some tiles have icons, set padding of tiles without icons so features and rows align.\n //\n var icons_present = _.find(tiles, tile => tile.has_icons);\n\n if (icons_present) {\n _.each(tiles, tile => {\n if (!tile.has_icons) {\n // Need to align with other tile(s) that have icons.\n tile.html_elt.css(\"padding-top\", ERROR_PADDING);\n }\n });\n }\n }\n },\n\n /**\n * Returns appropriate display mode based on data.\n */\n get_mode: function(data) {\n return this.mode;\n },\n\n /**\n * Update track interface to show display mode being used.\n */\n update_auto_mode: function(display_mode) {\n // FIXME: needs to be implemented.\n },\n\n /**\n * Returns a list of drawables to draw. Defaults to current track.\n */\n _get_drawables: function() {\n return [this];\n },\n\n /**\n * Retrieves from cache, draws, or sets up drawing for a single tile. Returns either a Tile object or a\n * jQuery.Deferred object that is fulfilled when tile can be drawn again. Options include:\n * -force: force a redraw rather than use cached tiles (default: false)\n * -data_fetch: fetch data if necessary (default: true)\n */\n draw_helper: function(region, w_scale, options) {\n // Init options if necessary to avoid having to check if options defined.\n if (!options) {\n options = {};\n }\n\n var force = options.force;\n var mode = options.mode || this.mode;\n var resolution = 1 / w_scale;\n\n var // Useful vars.\n track = this;\n\n var drawables = this._get_drawables();\n var key = this._gen_tile_cache_key(w_scale, region);\n\n var is_tile = o => o && \"track\" in o;\n\n // Check tile cache, if found show existing tile in correct position\n var tile = force ? undefined : track.tile_cache.get_elt(key);\n if (tile) {\n if (is_tile(tile)) {\n track.show_tile(tile, w_scale);\n }\n return tile;\n }\n\n // If not fetching data, nothing more to do because data is needed to draw tile.\n if (options.data_fetch === false) {\n return null;\n }\n\n // Function that returns data/Deferreds needed to draw tile.\n var get_tile_data = () => {\n // HACK: if display mode (mode) is in continuous data modes, data mode must be coverage to get coverage data.\n var data_mode = _.find(CONTINUOUS_DATA_MODES, m => m === mode) ? \"Coverage\" : mode;\n\n // Map drawable object to data needed for drawing.\n var tile_data = _.map(drawables, (\n d // Get the track data/promise.\n ) => d.data_manager.get_data(region, data_mode, resolution, track.data_url_extra_params));\n\n // Get reference data/promise.\n if (view.reference_track) {\n tile_data.push(\n view.reference_track.data_manager.get_data(\n region,\n mode,\n resolution,\n view.reference_track.data_url_extra_params\n )\n );\n }\n\n return tile_data;\n };\n\n //\n // When data is available, draw tile.\n //\n var tile_drawn = $.Deferred();\n track.tile_cache.set_elt(key, tile_drawn);\n $.when.apply($, get_tile_data()).then(() => {\n var tile_data = get_tile_data();\n var tracks_data = tile_data;\n var seq_data;\n\n // Deferreds may show up here if trying to fetch a subset of data from a superset data chunk\n // that cannot be subsetted. This may occur if the superset has a message. If there is a\n // Deferred, try again from the top. NOTE: this condition could (should?) be handled by the\n // GenomeDataManager in visualization module.\n if (_.find(tile_data, d => util.is_deferred(d))) {\n track.tile_cache.set_elt(key, undefined);\n $.when(track.draw_helper(region, w_scale, options)).then(tile => {\n tile_drawn.resolve(tile);\n });\n return;\n }\n\n // If sequence data is available, subset to get only data in region.\n if (view.reference_track) {\n seq_data = view.reference_track.data_manager.subset_entry(tile_data.pop(), region);\n }\n\n // Get drawing modes, heights for all tracks.\n var drawing_modes = [];\n\n var drawing_heights = [];\n\n _.each(drawables, (d, i) => {\n var mode = d.mode;\n var data = tracks_data[i];\n if (mode === \"Auto\") {\n mode = d.get_mode(data);\n d.update_auto_mode(mode);\n }\n drawing_modes.push(mode);\n drawing_heights.push(d.get_canvas_height(data, mode, w_scale, width));\n });\n\n var canvas = track.view.canvas_manager.new_canvas();\n var tile_low = region.get(\"start\");\n var tile_high = region.get(\"end\");\n var all_data_index = 0;\n\n var width = Math.ceil((tile_high - tile_low) * w_scale) + track.left_offset;\n\n var height = _.max(drawing_heights);\n var tile;\n\n //\n // Draw all tracks on tile.\n //\n canvas.width = width;\n // Height is specified in options or is the height found above.\n canvas.height = options.height || height;\n var ctx = canvas.getContext(\"2d\");\n ctx.translate(track.left_offset, 0);\n if (drawables.length > 1) {\n ctx.globalAlpha = 0.5;\n ctx.globalCompositeOperation = \"source-over\";\n }\n _.each(drawables, (d, i) => {\n tile = d.draw_tile(tracks_data[i], ctx, drawing_modes[i], region, w_scale, seq_data);\n });\n\n // Don't cache, show if no tile.\n if (tile !== undefined) {\n track.tile_cache.set_elt(key, tile);\n track.show_tile(tile, w_scale);\n }\n\n tile_drawn.resolve(tile);\n });\n\n return tile_drawn;\n },\n\n /**\n * Returns canvas height needed to display data; return value is an integer that denotes the\n * number of pixels required.\n */\n get_canvas_height: function(result, mode, w_scale, canvas_width) {\n return this.visible_height_px;\n },\n\n /**\n * Draw line (bigwig) data onto tile.\n */\n _draw_line_track_tile: function(result, ctx, mode, region, w_scale) {\n // Set min/max if they are not already set.\n // FIXME: checking for different null/undefined/0 is messy; it would be nice to\n // standardize this.\n if ([undefined, null].indexOf(this.config.get_value(\"min_value\")) !== -1) {\n this.config.set_value(\"min_value\", 0);\n }\n if ([undefined, null, 0].indexOf(this.config.get_value(\"max_value\")) !== -1) {\n this.config.set_value(\"max_value\", _.max(_.map(result.data, d => d[1])) || 0);\n }\n\n var canvas = ctx.canvas;\n\n var painter = new painters.LinePainter(\n result.data,\n region.get(\"start\"),\n region.get(\"end\"),\n this.config.to_key_value_dict(),\n mode\n );\n\n painter.draw(ctx, canvas.width, canvas.height, w_scale);\n\n return new LineTrackTile(this, region, w_scale, canvas, result.data);\n },\n\n /**\n * Draw a track tile.\n * @param result result from server\n * @param ctx canvas context to draw on\n * @param mode mode to draw in\n * @param region region to draw on tile\n * @param w_scale pixels per base\n * @param ref_seq reference sequence data\n */\n draw_tile: function(result, ctx, mode, region, w_scale, ref_seq) {},\n\n /**\n * Show track tile and perform associated actions. Showing tile may actually move\n * an existing tile rather than reshowing it.\n */\n show_tile: function(tile, w_scale) {\n var track = this;\n var tile_element = tile.html_elt;\n\n // -- Show/move tile element. --\n\n tile.predisplay_actions();\n\n // Position tile element based on current viewport.\n var left = Math.round((tile.low - (this.is_overview ? this.view.max_low : this.view.low)) * w_scale);\n if (this.left_offset) {\n left -= this.left_offset;\n }\n tile_element.css(\"left\", left);\n\n if (tile_element.hasClass(\"remove\")) {\n // Step (b) for (re)moving tiles. See _draw() function for description of algorithm\n // for removing tiles.\n tile_element.removeClass(\"remove\");\n } else {\n // Showing new tile.\n this.tiles_div.append(tile_element);\n }\n\n // -- Update track, tile heights based on new tile. --\n\n tile_element.css(\"height\", \"auto\");\n\n // Update max height based on current tile's height.\n // BUG/HACK: tile_element.height() returns a height that is always 2 pixels too big, so\n // -2 to get the correct height.\n this.max_height_px = Math.max(this.max_height_px, tile_element.height() - 2);\n\n // Update height for all tiles based on max height.\n tile_element\n .parent()\n .children()\n .css(\"height\", `${this.max_height_px}px`);\n\n // Update track height based on max height and visible height.\n var track_height = this.max_height_px;\n if (this.visible_height_px !== 0) {\n track_height = Math.min(this.max_height_px, this.visible_height_px);\n }\n this.tiles_div.css(\"height\", `${track_height}px`);\n },\n\n /**\n * Utility function that creates a label string describing the region and parameters of a track's tool.\n */\n tool_region_and_parameters_str: function(region) {\n var track = this;\n var region_str = region !== undefined ? region.toString() : \"all\";\n var param_str = _.values(track.tool.get_inputs_dict()).join(\", \");\n return ` - region=[${region_str}], parameters=[${param_str}]`;\n },\n\n /**\n * Returns true if data is compatible with a given mode.\n */\n data_and_mode_compatible: function(data, mode) {\n // Only handle modes that user can set.\n if (mode === \"Auto\") {\n return true;\n } else if (mode === \"Coverage\") {\n // Histogram mode requires bigwig data.\n return data.dataset_type === \"bigwig\";\n } else if (data.dataset_type === \"bigwig\" || data.extra_info === \"no_detail\") {\n // All other modes--Dense, Squish, Pack--require data + details.\n return false;\n } else {\n return true;\n }\n },\n\n /**\n * Returns true if entry can be subsetted.\n */\n can_subset: function(entry) {\n // Do not subset entries with a message or data with no detail.\n if (entry.message || entry.extra_info === \"no_detail\") {\n return false;\n } else if (entry.dataset_type === \"bigwig\") {\n // Subset only if data is single-bp resolution.\n return entry.data[1][0] - entry.data[0][0] === 1;\n }\n\n return true;\n },\n\n /**\n * Set up track to receive tool data.\n */\n init_for_tool_data: function() {\n // Set up track to fetch raw data rather than converted data.\n this.data_manager.set(\"data_type\", \"raw_data\");\n this.data_query_wait = 1000;\n this.dataset_check_type = \"state\";\n\n // FIXME: this is optional and is disabled for now because it creates\n // additional converter jobs without a clear benefit because indexing\n // such a small dataset provides little benefit.\n //\n // Set up one-time, post-draw to clear tool execution settings.\n //\n /*\n this.normal_postdraw_actions = this.postdraw_actions;\n this.postdraw_actions = function(tiles, width, w_scale, clear_after) {\n var self = this;\n\n // Do normal postdraw init.\n self.normal_postdraw_actions(tiles, width, w_scale, clear_after);\n\n // Tool-execution specific post-draw init:\n\n // Reset dataset check, wait time.\n self.dataset_check_type = 'converted_datasets_state';\n self.data_query_wait = DEFAULT_DATA_QUERY_WAIT;\n\n // Reset data URL when dataset indexing has completed/when not pending.\n var ss_deferred = new util.ServerStateDeferred({\n url: self.dataset_state_url,\n url_params: {dataset_id : self.dataset.id, hda_ldda: self.dataset.get('hda_ldda')},\n interval: self.data_query_wait,\n // Set up deferred to check dataset state until it is not pending.\n success_fn: function(result) { return result !== \"pending\"; }\n });\n $.when(ss_deferred.go()).then(function() {\n // Dataset is indexed, so use converted data.\n self.data_manager.set('data_type', 'data');\n });\n\n // Reset post-draw actions function.\n self.postdraw_actions = self.normal_postdraw_actions;\n };\n */\n }\n});\n\nvar LabelTrack = function(view, container) {\n Track.call(this, view, container, {\n resize: false,\n header: false\n });\n this.container_div.addClass(\"label-track\");\n};\nextend(LabelTrack.prototype, Track.prototype, {\n init: function() {\n // Enable by default because there should always be data when drawing track.\n this.enabled = true;\n },\n\n /**\n * Additional initialization required before drawing track for the first time.\n */\n predraw_init: function() {},\n\n _draw: function(options) {\n var view = this.view;\n var range = view.high - view.low;\n\n var tickDistance = Math.floor(Math.pow(10, Math.floor(Math.log(range) / Math.log(10))));\n\n var position = Math.floor(view.low / tickDistance) * tickDistance;\n var width = this.view.container.width();\n var new_div = $(\"\").addClass(\"label-container\");\n while (position < view.high) {\n var screenPosition = Math.floor((position - view.low) / range * width);\n new_div.append(\n $(\"\")\n .addClass(\"pos-label\")\n .text(util.commatize(position))\n .css({\n left: screenPosition\n })\n );\n position += tickDistance;\n }\n this.content_div.children(\":first\").remove();\n this.content_div.append(new_div);\n }\n});\n\n// FIXME: Composite tracks have code for showing composite tracks with line tracks and\n// composite tracks with line + feature tracks. It's probably best if different classes\n// are created for each type of composite track.\n\n/**\n * A tiled track composed of multiple other tracks. Composite tracks only work with\n * bigwig data for now.\n */\nvar CompositeTrack = function(view, container, obj_dict) {\n TiledTrack.call(this, view, container, obj_dict);\n\n // Init drawables; each drawable is a copy so that config/preferences\n // are independent of each other. Also init left offset.\n this.drawables = [];\n if (\"drawables\" in obj_dict) {\n var drawable;\n for (var i = 0; i < obj_dict.drawables.length; i++) {\n drawable = obj_dict.drawables[i];\n this.drawables[i] = object_from_template(drawable, view, null);\n\n // Track's left offset is the max of all tracks.\n if (drawable.left_offset > this.left_offset) {\n this.left_offset = drawable.left_offset;\n }\n }\n this.enabled = true;\n }\n\n // Set all feature tracks to use Coverage mode.\n _.each(this.drawables, d => {\n if (d instanceof FeatureTrack || d instanceof ReadTrack) {\n d.change_mode(\"Coverage\");\n }\n });\n\n this.update_icons();\n\n // HACK: needed for saving object for now. Need to generalize get_type() to all Drawables and use\n // that for object type.\n this.obj_type = \"CompositeTrack\";\n};\n\nextend(CompositeTrack.prototype, TiledTrack.prototype, {\n display_modes: CONTINUOUS_DATA_MODES,\n\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n {\n key: \"min_value\",\n label: \"Min Value\",\n type: \"float\",\n default_value: undefined\n },\n {\n key: \"max_value\",\n label: \"Max Value\",\n type: \"float\",\n default_value: undefined\n },\n {\n key: \"mode\",\n type: \"string\",\n default_value: this.mode,\n hidden: true\n },\n {\n key: \"height\",\n type: \"int\",\n default_value: 30,\n hidden: true\n }\n ]);\n },\n\n action_icons_def: [\n // Create composite track from group's tracks.\n {\n name: \"composite_icon\",\n title: \"Show individual tracks\",\n css_class: \"layers-stack\",\n on_click_fn: function(track) {\n $(\".tooltip\").remove();\n track.show_group();\n }\n }\n ].concat(TiledTrack.prototype.action_icons_def),\n\n // HACK: CompositeTrack should inherit from DrawableCollection as well.\n /**\n * Returns representation of object in a dictionary for easy saving.\n * Use from_dict to recreate object.\n */\n to_dict: DrawableCollection.prototype.to_dict,\n\n add_drawable: DrawableCollection.prototype.add_drawable,\n\n unpack_drawables: DrawableCollection.prototype.unpack_drawables,\n\n config_onchange: function() {\n this.set_name(this.config.get_value(\"name\"));\n this.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Action to take during resize.\n */\n on_resize: function() {\n // Propogate visible height to other tracks.\n var visible_height = this.visible_height_px;\n _.each(this.drawables, d => {\n d.visible_height_px = visible_height;\n });\n Track.prototype.on_resize.call(this);\n },\n\n /**\n * Change mode for all tracks.\n */\n change_mode: function(new_mode) {\n TiledTrack.prototype.change_mode.call(this, new_mode);\n for (var i = 0; i < this.drawables.length; i++) {\n this.drawables[i].change_mode(new_mode);\n }\n },\n\n /**\n * Initialize component tracks and draw composite track when all components are initialized.\n */\n init: function() {\n // Init components.\n var init_deferreds = [];\n for (var i = 0; i < this.drawables.length; i++) {\n init_deferreds.push(this.drawables[i].init());\n }\n\n // Draw composite when all tracks available.\n var track = this;\n $.when.apply($, init_deferreds).then(() => {\n track.enabled = true;\n track.request_draw();\n });\n },\n\n update_icons: function() {\n // For now, hide filters and tool.\n this.action_icons.filters_icon.hide();\n this.action_icons.tools_icon.hide();\n this.action_icons.param_space_viz_icon.hide();\n },\n\n can_draw: Drawable.prototype.can_draw,\n\n _get_drawables: function() {\n return this.drawables;\n },\n\n /**\n * Replace this track with group that includes individual tracks.\n */\n show_group: function() {\n // Create group with individual tracks.\n var group = new DrawableGroup(this.view, this.container, {\n name: this.config.get_value(\"name\")\n });\n\n var track;\n for (var i = 0; i < this.drawables.length; i++) {\n track = this.drawables[i];\n track.update_icons();\n group.add_drawable(track);\n track.container = group;\n group.content_div.append(track.container_div);\n }\n\n // Replace track with group.\n var index = this.container.replace_drawable(this, group, true);\n group.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Actions taken before drawing.\n */\n before_draw: function() {\n // FIXME: this is needed only if there are feature tracks in the composite track.\n // TiledTrack.prototype.before_draw.call(this);\n\n //\n // Set min, max for tracks to be largest min, max.\n //\n\n // Get smallest min, biggest max.\n var min = _.min(_.map(this.drawables, d => d.config.get_value(\"min_value\")));\n\n var max = _.max(_.map(this.drawables, d => d.config.get_value(\"max_value\")));\n\n this.config.set_value(\"min_value\", min);\n this.config.set_value(\"max_value\", max);\n\n // Set all tracks to smallest min, biggest max.\n _.each(this.drawables, d => {\n d.config.set_value(\"min_value\", min);\n d.config.set_value(\"max_value\", max);\n });\n },\n\n /**\n * Update minimum, maximum for component tracks.\n */\n update_all_min_max: function() {\n var track = this;\n var min_value = this.config.get_value(\"min_value\");\n var max_value = this.config.get_value(\"max_value\");\n _.each(this.drawables, d => {\n d.config.set_value(\"min_value\", min_value);\n d.config.set_value(\"max_value\", max_value);\n });\n this.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Actions to be taken after draw has been completed. Draw is completed when all tiles have been\n * drawn/fetched and shown.\n */\n postdraw_actions: function(tiles, width, w_scale, clear_after) {\n // All tiles must be the same height in order to draw LineTracks, so redraw tiles as needed.\n var max_height = -1;\n\n var i;\n for (i = 0; i < tiles.length; i++) {\n var height = tiles[i].html_elt.find(\"canvas\").height();\n if (height > max_height) {\n max_height = height;\n }\n }\n\n for (i = 0; i < tiles.length; i++) {\n var tile = tiles[i];\n if (tile.html_elt.find(\"canvas\").height() !== max_height) {\n this.draw_helper(tile.region, w_scale, {\n force: true,\n height: max_height\n });\n tile.html_elt.remove();\n }\n }\n\n // Wrap function so that it can be called without object reference.\n var track = this;\n\n var t = () => {\n track.update_all_min_max();\n };\n\n // Add min, max labels.\n this._add_yaxis_label(\"min\", t);\n this._add_yaxis_label(\"max\", t);\n }\n});\n\n/**\n * Displays reference genome data.\n */\nvar ReferenceTrack = function(view) {\n TiledTrack.call(this, view, { content_div: view.top_labeltrack }, { resize: false, header: false });\n\n // Use offset to ensure that bases at tile edges are drawn.\n this.left_offset = view.canvas_manager.char_width_px;\n this.container_div.addClass(\"reference-track\");\n this.data_url = `${Galaxy.root}api/genomes/${this.view.dbkey}`;\n this.data_url_extra_params = { reference: true };\n this.data_manager = new visualization.GenomeReferenceDataManager({\n data_url: this.data_url,\n can_subset: this.can_subset\n });\n this.hide_contents();\n};\nextend(ReferenceTrack.prototype, Drawable.prototype, TiledTrack.prototype, {\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n {\n key: \"height\",\n type: \"int\",\n default_value: 13,\n hidden: true\n }\n ]);\n },\n\n init: function() {\n this.data_manager.clear();\n // Enable by default because there should always be data when drawing track.\n this.enabled = true;\n },\n\n /**\n * Additional initialization required before drawing track for the first time.\n */\n predraw_init: function() {},\n\n can_draw: Drawable.prototype.can_draw,\n\n /**\n * Draws and shows tile if reference data can be displayed; otherwise track is hidden.\n */\n draw_helper: function(region, w_scale, options) {\n var cur_visible = this.tiles_div.is(\":visible\");\n var new_visible;\n var tile = null;\n if (w_scale > this.view.canvas_manager.char_width_px) {\n this.tiles_div.show();\n new_visible = true;\n tile = TiledTrack.prototype.draw_helper.call(this, region, w_scale, options);\n } else {\n new_visible = false;\n this.tiles_div.hide();\n }\n\n // NOTE: viewport resizing conceptually belongs in postdraw_actions(), but currently\n // postdraw_actions is not called when reference track not shown due to no tiles. If\n // it is moved to postdraw_actions, resize must be called each time because cannot\n // easily detect showing/hiding.\n\n // If showing or hiding reference track, resize viewport.\n if (cur_visible !== new_visible) {\n this.view.resize_viewport();\n }\n\n return tile;\n },\n\n can_subset: function(entry) {\n return true;\n },\n\n /**\n * Draw ReferenceTrack tile.\n */\n draw_tile: function(data, ctx, mode, region, w_scale) {\n // Try to subset data.\n var subset = this.data_manager.subset_entry(data, region);\n\n var seq_data = subset.data;\n\n // Draw sequence data.\n var canvas = ctx.canvas;\n ctx.font = ctx.canvas.manager.default_font;\n ctx.textAlign = \"center\";\n for (var c = 0, str_len = seq_data.length; c < str_len; c++) {\n ctx.fillStyle = this.view.get_base_color(seq_data[c]);\n ctx.fillText(seq_data[c], Math.floor(c * w_scale), 10);\n }\n return new Tile(this, region, w_scale, canvas, subset);\n }\n});\n\n/**\n * Track displays continuous/numerical data. Track expects position data in 1-based format, i.e. wiggle format.\n */\nvar LineTrack = function(view, container, obj_dict) {\n this.mode = \"Histogram\";\n TiledTrack.call(this, view, container, obj_dict);\n // Need left offset for drawing overlap near tile boundaries.\n this.left_offset = 30;\n\n // If server has byte-range support, use BBI data manager to read directly from the BBI file.\n // FIXME: there should be a flag to wait for this check to complete before loading the track.\n var self = this;\n $.when(supportsByteRanges(`${Galaxy.root}datasets/${this.dataset.id}/display`)).then(supportsByteRanges => {\n if (supportsByteRanges) {\n self.data_manager = new bbi.BBIDataManager({\n dataset: self.dataset\n });\n }\n });\n};\n\nextend(LineTrack.prototype, Drawable.prototype, TiledTrack.prototype, {\n display_modes: CONTINUOUS_DATA_MODES,\n\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n { key: \"color\", label: \"Color\", type: \"color\" },\n {\n key: \"min_value\",\n label: \"Min Value\",\n type: \"float\",\n default_value: undefined\n },\n {\n key: \"max_value\",\n label: \"Max Value\",\n type: \"float\",\n default_value: undefined\n },\n {\n key: \"mode\",\n type: \"string\",\n default_value: this.mode,\n hidden: true\n },\n {\n key: \"height\",\n type: \"int\",\n default_value: 30,\n hidden: true\n }\n ]);\n },\n\n config_onchange: function() {\n this.set_name(this.config.get_value(\"name\"));\n this.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Actions to be taken before drawing.\n */\n // FIXME: can the default behavior be used; right now it breaks during resize.\n before_draw: function() {},\n\n /**\n * Draw track tile.\n */\n draw_tile: function(result, ctx, mode, region, w_scale) {\n return this._draw_line_track_tile(result, ctx, mode, region, w_scale);\n },\n\n /**\n * Subset data only if data is at single-base pair resolution.\n */\n can_subset: function(entry) {\n return entry.data[1][0] - entry.data[0][0] === 1;\n },\n\n /**\n * Add min, max labels.\n */\n postdraw_actions: function(tiles, width, w_scale, clear_after) {\n // Add min, max labels.\n this._add_yaxis_label(\"max\");\n this._add_yaxis_label(\"min\");\n }\n});\n\n/**\n * Diagonal heatmap for showing interactions data.\n */\nvar DiagonalHeatmapTrack = function(view, container, obj_dict) {\n this.mode = \"Heatmap\";\n TiledTrack.call(this, view, container, obj_dict);\n};\n\nextend(DiagonalHeatmapTrack.prototype, Drawable.prototype, TiledTrack.prototype, {\n display_modes: [\"Heatmap\"],\n\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n {\n key: \"pos_color\",\n label: \"Positive Color\",\n type: \"color\",\n default_value: \"#FF8C00\"\n },\n {\n key: \"neg_color\",\n label: \"Negative Color\",\n type: \"color\",\n default_value: \"#4169E1\"\n },\n {\n key: \"min_value\",\n label: \"Min Value\",\n type: \"int\",\n default_value: undefined\n },\n {\n key: \"max_value\",\n label: \"Max Value\",\n type: \"int\",\n default_value: undefined\n },\n {\n key: \"mode\",\n type: \"string\",\n default_value: this.mode,\n hidden: true\n },\n {\n key: \"height\",\n type: \"int\",\n default_value: 500,\n hidden: true\n }\n ]);\n },\n\n config_onchange: function() {\n this.set_name(this.config.get_value(\"name\"));\n this.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Additional initialization required before drawing track for the first time.\n */\n predraw_init: function() {\n var track = this;\n return $.getJSON(\n track.dataset.url(),\n {\n data_type: \"data\",\n stats: true,\n chrom: track.view.chrom,\n low: 0,\n high: track.view.max_high,\n hda_ldda: track.dataset.get(\"hda_ldda\")\n },\n result => {\n var data = result.data;\n }\n );\n },\n\n /**\n * Draw tile.\n */\n draw_tile: function(result, ctx, mode, region, w_scale) {\n // Paint onto canvas.\n var canvas = ctx.canvas;\n\n var painter = new painters.DiagonalHeatmapPainter(\n result.data,\n region.get(\"start\"),\n region.get(\"end\"),\n this.config.to_key_value_dict(),\n mode\n );\n\n painter.draw(ctx, canvas.width, canvas.height, w_scale);\n\n return new Tile(this, region, w_scale, canvas, result.data);\n }\n});\n\n/**\n * A track that displays features/regions. Track expects position data in BED format, i.e. 0-based, half-open.\n */\nvar FeatureTrack = function(view, container, obj_dict) {\n TiledTrack.call(this, view, container, obj_dict);\n this.container_div.addClass(\"feature-track\");\n this.summary_draw_height = 30;\n this.slotters = {};\n this.start_end_dct = {};\n this.left_offset = 200;\n\n // this.painter = painters.LinkedFeaturePainter;\n this.set_painter_from_config();\n};\nextend(FeatureTrack.prototype, Drawable.prototype, TiledTrack.prototype, {\n display_modes: [\"Auto\", \"Coverage\", \"Dense\", \"Squish\", \"Pack\"],\n\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n {\n key: \"block_color\",\n label: \"Block color\",\n type: \"color\"\n },\n {\n key: \"reverse_strand_color\",\n label: \"Antisense strand color\",\n type: \"color\"\n },\n {\n key: \"label_color\",\n label: \"Label color\",\n type: \"color\",\n default_value: \"black\"\n },\n {\n key: \"show_counts\",\n label: \"Show summary counts\",\n type: \"bool\",\n default_value: true,\n help: \"Show the number of items in each bin when drawing summary histogram\"\n },\n {\n key: \"min_value\",\n label: \"Histogram minimum\",\n type: \"float\",\n default_value: undefined,\n help: \"clear value to set automatically\"\n },\n {\n key: \"max_value\",\n label: \"Histogram maximum\",\n type: \"float\",\n default_value: undefined,\n help: \"clear value to set automatically\"\n },\n {\n key: \"connector_style\",\n label: \"Connector style\",\n type: \"select\",\n default_value: \"fishbones\",\n options: [\n {\n label: \"Line with arrows\",\n value: \"fishbone\"\n },\n { label: \"Arcs\", value: \"arcs\" }\n ]\n },\n {\n key: \"mode\",\n type: \"string\",\n default_value: this.mode,\n hidden: true\n },\n {\n key: \"height\",\n type: \"int\",\n default_value: 0,\n hidden: true\n }\n ]);\n },\n\n config_onchange: function() {\n this.set_name(this.config.get_value(\"name\"));\n this.set_painter_from_config();\n this.request_draw({ clear_tile_cache: true });\n },\n\n set_painter_from_config: function() {\n if (this.config.get_value(\"connector_style\") === \"arcs\") {\n this.painter = painters.ArcLinkedFeaturePainter;\n } else {\n this.painter = painters.LinkedFeaturePainter;\n }\n },\n\n /**\n * Actions to be taken after draw has been completed. Draw is completed when all tiles have been\n * drawn/fetched and shown.\n */\n postdraw_actions: function(tiles, width, w_scale, clear_after) {\n TiledTrack.prototype.postdraw_actions.call(this, tiles, width, w_scale, clear_after);\n\n var track = this;\n var i;\n\n var line_track_tiles = _.filter(tiles, t => t instanceof LineTrackTile);\n\n //\n // Finish drawing of features that span multiple tiles. Features that span multiple tiles\n // are labeled incomplete on the tile level because they cannot be completely drawn.\n //\n if (line_track_tiles.length === 0) {\n // Gather incomplete features together.\n var all_incomplete_features = {};\n _.each(_.pluck(tiles, \"incomplete_features\"), inc_features => {\n _.each(inc_features, feature => {\n all_incomplete_features[feature[0]] = feature;\n });\n });\n\n // Draw incomplete features on each tile.\n var self = this;\n _.each(tiles, tile => {\n // Remove features already drawn on tile originally.\n var tile_incomplete_features = _.omit(\n all_incomplete_features,\n _.map(tile.incomplete_features, f => f[0])\n );\n\n // Remove features already drawn on tile in past postdraw actions.\n tile_incomplete_features = _.omit(tile_incomplete_features, _.keys(tile.other_tiles_features_drawn));\n\n // Draw tile's incomplete features.\n if (_.size(tile_incomplete_features) !== 0) {\n // To draw incomplete features, create new canvas, copy original canvas/tile onto new\n // canvas, and then draw incomplete features on the new canvas.\n var features = {\n data: _.values(tile_incomplete_features)\n };\n\n var new_canvas = self.view.canvas_manager.new_canvas();\n var new_canvas_ctx = new_canvas.getContext(\"2d\");\n new_canvas.height = Math.max(\n tile.canvas.height,\n self.get_canvas_height(features, tile.mode, tile.w_scale, 100)\n );\n new_canvas.width = tile.canvas.width;\n new_canvas_ctx.drawImage(tile.canvas, 0, 0);\n new_canvas_ctx.translate(track.left_offset, 0);\n var new_tile = self.draw_tile(\n features,\n new_canvas_ctx,\n tile.mode,\n tile.region,\n tile.w_scale,\n tile.seq_data\n );\n $(tile.canvas).replaceWith($(new_tile.canvas));\n tile.canvas = new_canvas;\n _.extend(tile.other_tiles_features_drawn, all_incomplete_features);\n }\n });\n }\n\n // If mode is Coverage and tiles do not share max, redraw tiles as necessary using new max.\n /*\n This code isn't used right now because Coverage mode uses predefined max in preferences.\n if (track.mode === \"Coverage\") {\n // Get global max.\n var global_max = -1;\n for (i = 0; i < tiles.length; i++) {\n var cur_max = tiles[i].max_val;\n if (cur_max > global_max) {\n global_max = cur_max;\n }\n }\n\n for (i = 0; i < tiles.length; i++) {\n var tile = tiles[i];\n if (tile.max_val !== global_max) {\n tile.html_elt.remove();\n track.draw_helper(tile.index, w_scale, { more_tile_data: { force: true, max: global_max } } );\n }\n }\n }\n */\n\n //\n // Update filter attributes, UI.\n //\n\n // Update filtering UI.\n if (track.filters_manager) {\n var filters = track.filters_manager.filters;\n var f;\n for (f = 0; f < filters.length; f++) {\n filters[f].update_ui_elt();\n }\n\n // Determine if filters are available; this is based on the tiles' data.\n // Criteria for filter to be available: (a) it is applicable to tile data and (b) filter min != filter max.\n var filters_available = false;\n\n var example_feature;\n var filter;\n for (i = 0; i < tiles.length; i++) {\n if (tiles[i].data.length) {\n example_feature = tiles[i].data[0];\n for (f = 0; f < filters.length; f++) {\n filter = filters[f];\n if (filter.applies_to(example_feature) && filter.min !== filter.max) {\n filters_available = true;\n break;\n }\n }\n }\n }\n\n // If filter availability changed, hide filter div if necessary and update menu.\n if (track.filters_available !== filters_available) {\n track.filters_available = filters_available;\n if (!track.filters_available) {\n track.filters_manager.hide();\n }\n track.update_icons();\n }\n }\n\n //\n // If not all features slotted, show icon for showing more rows (slots).\n //\n if (tiles[0] instanceof FeatureTrackTile) {\n var all_slotted = true;\n for (i = 0; i < tiles.length; i++) {\n if (!tiles[i].all_slotted) {\n all_slotted = false;\n break;\n }\n }\n this.action_icons.show_more_rows_icon.toggle(!all_slotted);\n } else {\n this.action_icons.show_more_rows_icon.hide();\n }\n },\n\n /**\n * Update track interface to show display mode being used.\n */\n update_auto_mode: function(mode) {\n if (this.mode === \"Auto\") {\n if (mode === \"no_detail\") {\n mode = \"feature spans\";\n }\n this.action_icons.mode_icon.attr(\"title\", `Set display mode (now: Auto/${mode})`);\n }\n },\n\n /**\n * Place features in slots for drawing (i.e. pack features).\n * this.slotters[level] is created in this method. this.slotters[level]\n * is a Slotter object. Returns the number of slots used to pack features.\n */\n incremental_slots: function(level, features, mode) {\n // Get/create incremental slots for level. If display mode changed,\n // need to create new slots.\n\n var dummy_context = this.view.canvas_manager.dummy_context;\n\n var slotter = this.slotters[level];\n if (!slotter || slotter.mode !== mode) {\n slotter = new slotting.FeatureSlotter(level, mode, MAX_FEATURE_DEPTH, x => dummy_context.measureText(x));\n this.slotters[level] = slotter;\n }\n\n return slotter.slot_features(features);\n },\n\n /**\n * Returns appropriate display mode based on data.\n */\n get_mode: function(data) {\n var mode;\n // HACK: use no_detail mode track is in overview to prevent overview from being too large.\n if (data.extra_info === \"no_detail\" || this.is_overview) {\n mode = \"no_detail\";\n } else {\n // Choose b/t Squish and Pack.\n // Proxy measures for using Squish:\n // (a) error message re: limiting number of features shown;\n // (b) X number of features shown;\n // (c) size of view shown.\n // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles;\n // fix this so that tiles are redrawn as necessary to use the same mode.\n //if ( (result.message && result.message.match(/^Only the first [\\d]+/)) ||\n // (result.data && result.data.length > 2000) ||\n //var data = result.data;\n // if ( (data.length && data.length < 4) ||\n // (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) {\n if (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) {\n mode = \"Squish\";\n } else {\n mode = \"Pack\";\n }\n }\n return mode;\n },\n\n /**\n * Returns canvas height needed to display data; return value is an integer that denotes the\n * number of pixels required.\n */\n get_canvas_height: function(result, mode, w_scale, canvas_width) {\n if (mode === \"Coverage\" || result.dataset_type === \"bigwig\") {\n return this.summary_draw_height;\n } else {\n // All other modes require slotting.\n var rows_required = this.incremental_slots(w_scale, result.data, mode);\n // HACK: use dummy painter to get required height. Painter should be extended so that get_required_height\n // works as a static function.\n var dummy_painter = new this.painter(null, null, null, this.config.to_key_value_dict(), mode);\n return Math.max(this.min_height_px, dummy_painter.get_required_height(rows_required, canvas_width));\n }\n },\n\n /**\n * Draw FeatureTrack tile.\n * @param result result from server\n * @param cxt canvas context to draw on\n * @param mode mode to draw in\n * @param region region to draw on tile\n * @param w_scale pixels per base\n * @param ref_seq reference sequence data\n * @param cur_tile true if drawing is occurring on a currently visible tile.\n */\n draw_tile: function(result, ctx, mode, region, w_scale, ref_seq, cur_tile) {\n var track = this;\n var canvas = ctx.canvas;\n var tile_low = region.get(\"start\");\n var tile_high = region.get(\"end\");\n var left_offset = this.left_offset;\n\n // If data is line track data, draw line track tile.\n if (result.dataset_type === \"bigwig\") {\n return this._draw_line_track_tile(result, ctx, mode, region, w_scale);\n }\n\n // Handle row-by-row tracks\n\n // Preprocessing: filter features and determine whether all unfiltered features have been slotted.\n var filtered = [];\n\n var slots = this.slotters[w_scale].slots;\n var all_slotted = true;\n if (result.data) {\n var filters = this.filters_manager.filters;\n for (var i = 0, len = result.data.length; i < len; i++) {\n var feature = result.data[i];\n var hide_feature = false;\n var filter;\n for (var f = 0, flen = filters.length; f < flen; f++) {\n filter = filters[f];\n filter.update_attrs(feature);\n if (!filter.keep(feature)) {\n hide_feature = true;\n break;\n }\n }\n if (!hide_feature) {\n // Feature visible.\n filtered.push(feature);\n // Set flag if not slotted.\n if (!(feature[0] in slots)) {\n all_slotted = false;\n }\n }\n }\n }\n\n // Create painter.\n var filter_alpha_scaler = this.filters_manager.alpha_filter\n ? new FilterScaler(this.filters_manager.alpha_filter)\n : null;\n\n var filter_height_scaler = this.filters_manager.height_filter\n ? new FilterScaler(this.filters_manager.height_filter)\n : null;\n\n var painter = new this.painter(\n filtered,\n tile_low,\n tile_high,\n this.config.to_key_value_dict(),\n mode,\n filter_alpha_scaler,\n filter_height_scaler,\n // HACK: ref_seq only be defined for ReadTracks, and only the ReadPainter accepts that argument\n ref_seq,\n b => track.view.get_base_color(b)\n );\n\n var feature_mapper = null;\n\n ctx.fillStyle = this.config.get_value(\"block_color\");\n ctx.font = ctx.canvas.manager.default_font;\n ctx.textAlign = \"right\";\n\n if (result.data) {\n // Draw features.\n var draw_results = painter.draw(ctx, canvas.width, canvas.height, w_scale, slots);\n feature_mapper = draw_results.feature_mapper;\n incomplete_features = draw_results.incomplete_features;\n feature_mapper.translation = -left_offset;\n }\n\n // If not drawing on current tile, create new tile.\n if (!cur_tile) {\n return new FeatureTrackTile(\n track,\n region,\n w_scale,\n canvas,\n result.data,\n mode,\n result.message,\n all_slotted,\n feature_mapper,\n incomplete_features,\n ref_seq\n );\n }\n }\n});\n\n/**\n * Displays variant data.\n */\nvar VariantTrack = function(view, container, obj_dict) {\n TiledTrack.call(this, view, container, obj_dict);\n this.painter = painters.VariantPainter;\n this.summary_draw_height = 30;\n\n // Maximum resolution is ~45 pixels/base, so use this size left offset to ensure that full\n // variant is drawn when variant is at start of tile.\n this.left_offset = 30;\n};\n\nextend(VariantTrack.prototype, Drawable.prototype, TiledTrack.prototype, {\n display_modes: [\"Auto\", \"Coverage\", \"Dense\", \"Squish\", \"Pack\"],\n\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n {\n key: \"color\",\n label: \"Histogram color\",\n type: \"color\"\n },\n {\n key: \"show_sample_data\",\n label: \"Show sample data\",\n type: \"bool\",\n default_value: true\n },\n {\n key: \"show_labels\",\n label: \"Show summary and sample labels\",\n type: \"bool\",\n default_value: true\n },\n {\n key: \"summary_height\",\n label: \"Locus summary height\",\n type: \"float\",\n default_value: 20\n },\n {\n key: \"mode\",\n type: \"string\",\n default_value: this.mode,\n hidden: true\n },\n {\n key: \"height\",\n type: \"int\",\n default_value: 0,\n hidden: true\n }\n ]);\n },\n\n config_onchange: function() {\n this.set_name(this.config.get_value(\"name\"));\n this.request_draw({ clear_tile_cache: true });\n },\n\n /**\n * Draw tile.\n */\n draw_tile: function(result, ctx, mode, region, w_scale) {\n // Data could be coverage data or variant data.\n if (result.dataset_type === \"bigwig\") {\n return this._draw_line_track_tile(result, ctx, \"Histogram\", region, w_scale);\n } else {\n // result.dataset_type === 'variant'\n var view = this.view;\n\n var painter = new this.painter(\n result.data,\n region.get(\"start\"),\n region.get(\"end\"),\n this.config.to_key_value_dict(),\n mode,\n b => view.get_base_color(b)\n );\n\n painter.draw(ctx, ctx.canvas.width, ctx.canvas.height, w_scale);\n return new Tile(this, region, w_scale, ctx.canvas, result.data);\n }\n },\n\n /**\n * Returns canvas height needed to display data; return value is an integer that denotes the\n * number of pixels required.\n */\n get_canvas_height: function(result, mode, w_scale, canvas_width) {\n if (result.dataset_type === \"bigwig\") {\n return this.summary_draw_height;\n } else {\n // HACK: sample_names is not be defined when dataset definition is fetched before\n // dataset is complete (as is done when running tools). In that case, fall back on\n // # of samples in data. This can be fixed by re-requesting dataset definition\n // in init.\n var num_samples = this.dataset.get_metadata(\"sample_names\")\n ? this.dataset.get_metadata(\"sample_names\").length\n : 0;\n if (num_samples === 0 && result.data.length !== 0) {\n // Sample data is separated by commas, so this computes # of samples:\n num_samples = result.data[0][7].match(/,/g);\n if (num_samples === null) {\n num_samples = 1;\n } else {\n num_samples = num_samples.length + 1;\n }\n }\n\n var dummy_painter = new this.painter(null, null, null, this.config.to_key_value_dict(), mode);\n return dummy_painter.get_required_height(num_samples);\n }\n },\n\n /**\n * Additional initialization required before drawing track for the first time.\n */\n predraw_init: function() {\n var deferreds = [Track.prototype.predraw_init.call(this)];\n // FIXME: updating dataset metadata is only needed for visual analysis. Can\n // this be moved somewhere else?\n if (!this.dataset.get_metadata(\"sample_names\")) {\n deferreds.push(this.dataset.fetch());\n }\n return deferreds;\n },\n\n /**\n * Actions to be taken after draw has been completed. Draw is completed when all tiles have been\n * drawn/fetched and shown.\n */\n postdraw_actions: function(tiles, width, w_scale, clear_after) {\n TiledTrack.prototype.postdraw_actions.call(this, tiles, width, w_scale, clear_after);\n\n var line_track_tiles = _.filter(tiles, t => t instanceof LineTrackTile);\n\n // Add summary/sample labels if needed and not already included.\n var sample_names = this.dataset.get_metadata(\"sample_names\");\n if (\n line_track_tiles.length === 0 &&\n this.config.get_value(\"show_labels\") &&\n sample_names &&\n sample_names.length > 1\n ) {\n var font_size;\n\n // Add and/or style labels.\n if (this.container_div.find(\".yaxislabel.variant\").length === 0) {\n // Add summary and sample labels.\n\n // Add summary label to middle of summary area.\n font_size = this.config.get_value(\"summary_height\") / 2;\n this.tiles_div.prepend(\n $(\"\")\n .text(\"Summary\")\n .addClass(\"yaxislabel variant top\")\n .css({\n \"font-size\": `${font_size}px`,\n top: `${(this.config.get_value(\"summary_height\") - font_size) / 2}px`\n })\n );\n\n // Show sample labels.\n if (this.config.get_value(\"show_sample_data\")) {\n var samples_div_html = sample_names.join(\"
\");\n\n this.tiles_div.prepend(\n $(\"\")\n .html(samples_div_html)\n .addClass(\"yaxislabel variant top sample\")\n .css({\n top: this.config.get_value(\"summary_height\")\n })\n );\n }\n }\n\n // Style labels.\n\n // Match sample font size to mode.\n font_size = `${this.mode === \"Squish\" ? 5 : 10}px`;\n $(this.tiles_div)\n .find(\".sample\")\n .css({\n \"font-size\": font_size,\n \"line-height\": font_size\n });\n // Color labels to preference color.\n $(this.tiles_div)\n .find(\".yaxislabel\")\n .css(\"color\", this.config.get_value(\"label_color\"));\n } else {\n // Remove all labels.\n this.container_div.find(\".yaxislabel.variant\").remove();\n }\n }\n});\n\n/**\n * Track that displays mapped reads. Track expects position data in 1-based, closed format, i.e. SAM/BAM format.\n */\nvar ReadTrack = function(view, container, obj_dict) {\n FeatureTrack.call(this, view, container, obj_dict);\n this.painter = painters.ReadPainter;\n this.update_icons();\n};\n\nextend(ReadTrack.prototype, Drawable.prototype, TiledTrack.prototype, FeatureTrack.prototype, {\n build_config_params: function() {\n return _.union(Drawable.prototype.config_params, [\n {\n key: \"block_color\",\n label: \"Histogram color\",\n type: \"color\"\n },\n {\n key: \"detail_block_color\",\n label: \"Sense strand block color\",\n type: \"color\",\n default_value: \"#AAAAAA\"\n },\n {\n key: \"reverse_strand_color\",\n label: \"Antisense strand block color\",\n type: \"color\",\n default_value: \"#DDDDDD\"\n },\n {\n key: \"label_color\",\n label: \"Label color\",\n type: \"color\",\n default_value: \"black\"\n },\n {\n key: \"show_insertions\",\n label: \"Show insertions\",\n type: \"bool\",\n default_value: false\n },\n {\n key: \"show_differences\",\n label: \"Show differences only\",\n type: \"bool\",\n default_value: true\n },\n {\n key: \"show_counts\",\n label: \"Show summary counts\",\n type: \"bool\",\n default_value: true\n },\n {\n key: \"mode\",\n type: \"string\",\n default_value: this.mode,\n hidden: true\n },\n {\n key: \"min_value\",\n label: \"Histogram minimum\",\n type: \"float\",\n default_value: undefined,\n help: \"clear value to set automatically\"\n },\n {\n key: \"max_value\",\n label: \"Histogram maximum\",\n type: \"float\",\n default_value: undefined,\n help: \"clear value to set automatically\"\n },\n {\n key: \"height\",\n type: \"int\",\n default_value: 0,\n hidden: true\n }\n ]);\n },\n\n config_onchange: function() {\n this.set_name(this.config.get_value(\"name\"));\n this.request_draw({ clear_tile_cache: true });\n }\n});\n\n/**\n * Objects that can be added to a view.\n */\nvar addable_objects = {\n CompositeTrack: CompositeTrack,\n DrawableGroup: DrawableGroup,\n DiagonalHeatmapTrack: DiagonalHeatmapTrack,\n FeatureTrack: FeatureTrack,\n LineTrack: LineTrack,\n ReadTrack: ReadTrack,\n VariantTrack: VariantTrack,\n // For backward compatibility, map vcf track to variant.\n VcfTrack: VariantTrack\n};\n\n/**\n * Create new object from a template. A template can be either an object dictionary or an\n * object itself.\n */\nvar object_from_template = (template, view, container) => {\n if (\"copy\" in template) {\n // Template is an object.\n return template.copy(container);\n } else {\n // Template is a dictionary.\n var drawable_type = template.obj_type;\n // For backward compatibility:\n if (!drawable_type) {\n drawable_type = template.track_type;\n }\n return new addable_objects[drawable_type](view, container, template);\n }\n};\n\nexport default {\n TracksterView: TracksterView,\n DrawableGroup: DrawableGroup,\n LineTrack: LineTrack,\n FeatureTrack: FeatureTrack,\n DiagonalHeatmapTrack: DiagonalHeatmapTrack,\n ReadTrack: ReadTrack,\n VariantTrack: VariantTrack,\n CompositeTrack: CompositeTrack,\n object_from_template: object_from_template\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/viz/trackster/tracks.js","import * as _ from \"libs/underscore\";\n/**\n * View for track/group header.\n */\nvar TrackHeaderView = Backbone.View.extend({\n className: \"track-header\",\n\n initialize: function() {\n // Watch and update name changes.\n this.model.config.get(\"name\").on(\"change:value\", this.update_name, this);\n this.render();\n },\n\n render: function() {\n this.$el.append($(\"\").addClass(this.model.drag_handle_class));\n this.$el.append(\n $(\"\")\n .addClass(\"track-name\")\n .text(this.model.config.get_value(\"name\"))\n );\n\n // Icons container.\n this.action_icons = {};\n this.render_action_icons();\n\n // Suppress double clicks in header so that they do not impact viz under header.\n this.$el.dblclick(e => {\n e.stopPropagation();\n });\n\n // Needed for floating elts in header.\n this.$el.append($(\"\"));\n },\n\n update_name: function() {\n this.$el.find(\".track-name\").text(this.model.config.get_value(\"name\"));\n },\n\n render_action_icons: function() {\n var self = this;\n this.icons_div = $(\"\")\n .addClass(\"track-icons\")\n .hide()\n .appendTo(this.$el);\n _.each(this.model.action_icons_def, icon_dict => {\n self.add_action_icon(\n icon_dict.name,\n icon_dict.title,\n icon_dict.css_class,\n icon_dict.on_click_fn,\n icon_dict.prepend,\n icon_dict.hide\n );\n });\n\n // Set up behavior for modes popup.\n this.set_display_modes(this.model.display_modes);\n },\n\n /**\n * Add an action icon to this object. Appends icon unless prepend flag is specified.\n */\n add_action_icon: function(name, title, css_class, on_click_fn, prepend, hide) {\n var self = this;\n this.action_icons[name] = $(\"\")\n .attr(\"title\", title)\n .addClass(\"icon-button\")\n .addClass(css_class)\n .tooltip()\n .click(() => {\n on_click_fn(self.model);\n })\n .appendTo(this.icons_div);\n if (hide) {\n this.action_icons[name].hide();\n }\n },\n\n /**\n * Set track's modes and update mode icon popup.\n */\n set_display_modes: function(new_modes, init_mode) {\n if (!new_modes) {\n return;\n }\n\n // HACK: move this out of view and into track.\n\n // Set modes, init mode.\n this.model.display_modes = new_modes;\n this.model.mode = init_mode || this.model.config.get_value(\"mode\") || this.model.display_modes[0];\n\n this.action_icons.mode_icon.attr(\"title\", `Set display mode (now: ${this.mode})`);\n\n // Setup popup menu for changing modes.\n var self = this;\n\n var track = this.model;\n var mode_mapping = {};\n for (var i = 0, len = track.display_modes.length; i < len; i++) {\n var mode = track.display_modes[i];\n mode_mapping[mode] = (mode => () => {\n track.change_mode(mode);\n // HACK: the popup menu messes with the track's hover event, so manually show/hide\n // icons div for now.\n //self.icons_div.show();\n //track.container_div.mouseleave(function() { track.icons_div.hide(); } );\n })(mode);\n }\n\n make_popupmenu(this.action_icons.mode_icon, mode_mapping);\n }\n});\n\nexport default {\n TrackHeaderView: TrackHeaderView\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/viz/viz_views.js","import * as _ from \"libs/underscore\";\nvar extend = _.extend;\n\n// HACK: LABEL_SPACING is currently duplicated between here and painters\nvar LABEL_SPACING = 2;\n\nvar PACK_SPACING = 5;\n\n/**\n * Hold slotting information for a feature.\n */\nvar SlottedInfo = function(slot, feature) {\n this.slot = slot;\n this.feature = feature;\n};\n\n/**\n * FeatureSlotter determines slots in which to draw features for vertical\n * packing.\n *\n * This implementation is incremental, any feature assigned a slot will be\n * retained for slotting future features.\n */\nvar FeatureSlotter = function(w_scale, mode, max_rows, measureText) {\n this.slots = {};\n this.start_end_dct = {};\n this.w_scale = w_scale;\n this.mode = mode;\n this.include_label = mode === \"Pack\";\n this.max_rows = max_rows;\n this.measureText = measureText;\n};\n\n/**\n * Slot a set of features, `this.slots` will be updated with slots by id, and\n * the largest slot required for the passed set of features is returned\n */\nextend(FeatureSlotter.prototype, {\n /**\n * Get drawing coordinate for a feature.\n */\n _get_draw_coords: function(feature) {\n // Get initial draw coordinates using w_scale.\n var draw_start = Math.floor(feature[1] * this.w_scale);\n\n var draw_end = Math.ceil(feature[2] * this.w_scale);\n var f_name = feature[3];\n var text_align;\n\n // Update start, end drawing locations to include feature name.\n // Try to put the name on the left, if not, put on right.\n if (f_name !== undefined && this.include_label) {\n // Add gap for label spacing and extra pack space padding\n // TODO: Fix constants\n var text_len = this.measureText(f_name).width + (LABEL_SPACING + PACK_SPACING);\n if (draw_start - text_len >= 0) {\n draw_start -= text_len;\n text_align = \"left\";\n } else {\n draw_end += text_len;\n text_align = \"right\";\n }\n }\n\n /*\n if (slot_num < 0) {\n \n TODO: this is not yet working --\n console.log(feature_uid, \"looking for slot with text on the right\");\n // Slot not found. If text was on left, try on right and see\n // if slot can be found.\n // TODO: are there any checks we need to do to ensure that text\n // will fit on tile?\n if (text_align === \"left\") {\n draw_start -= text_len;\n draw_end -= text_len;\n text_align = \"right\";\n slot_num = find_slot(draw_start, draw_end);\n }\n if (slot_num >= 0) {\n console.log(feature_uid, \"found slot with text on the right\");\n }\n\n }\n */\n\n return [draw_start, draw_end];\n },\n\n /**\n * Find the first slot such that current feature doesn't overlap any other features in that slot.\n * Returns -1 if no slot was found.\n */\n _find_slot: function(draw_coords) {\n // TODO: Use a data structure for faster searching of available slots.\n var draw_start = draw_coords[0];\n\n var draw_end = draw_coords[1];\n for (var slot_num = 0; slot_num <= this.max_rows; slot_num++) {\n var has_overlap = false;\n var slot = this.start_end_dct[slot_num];\n if (slot !== undefined) {\n // Iterate through features already in slot to see if current feature will fit.\n for (var k = 0, k_len = slot.length; k < k_len; k++) {\n var s_e = slot[k];\n if (draw_end > s_e[0] && draw_start < s_e[1]) {\n // There is overlap\n has_overlap = true;\n break;\n }\n }\n }\n if (!has_overlap) {\n return slot_num;\n }\n }\n return -1;\n },\n\n /**\n * Slot features.\n */\n slot_features: function(features) {\n var start_end_dct = this.start_end_dct;\n var undone = [];\n var highest_slot = 0;\n var feature;\n var feature_uid;\n\n // Loop through features to (a) find those that are not yet slotted and (b) update\n // those that are slotted if new information is availabe. For (a), features already\n // slotted (based on slotting from other tiles) will retain their current slot.\n for (var i = 0, len = features.length; i < len; i++) {\n feature = features[i];\n feature_uid = feature[0];\n var slotted_info = this.slots[feature_uid];\n\n // Separate and handle slotted vs. unslotted features.\n if (slotted_info) {\n // Feature is slotted; if feature now has larger start/end coordinates,\n // update drawing coordinates.\n if (feature[1] < slotted_info.feature[1] || slotted_info.feature[2] < feature[2]) {\n // Feature has changed (e.g. a single read now has its pair), so recalculate its\n // drawing coordinates.\n var old_draw_coords = this._get_draw_coords(slotted_info.feature);\n\n var new_draw_coords = this._get_draw_coords(feature);\n var slotted_coords = this.start_end_dct[slotted_info.slot];\n for (var k = 0; k < slotted_coords.length; k++) {\n var dc = slotted_coords[k];\n if (dc[0] === old_draw_coords[0] && dc[1] === old_draw_coords[1]) {\n // Replace old drawing coordinates with new ones.\n slotted_coords[k] = new_draw_coords;\n }\n }\n }\n highest_slot = Math.max(highest_slot, this.slots[feature_uid].slot);\n } else {\n undone.push(i);\n }\n }\n\n // Slot unslotted features.\n\n // Do slotting.\n for (var i = 0, len = undone.length; i < len; i++) {\n feature = features[undone[i]];\n feature_uid = feature[0];\n var draw_coords = this._get_draw_coords(feature);\n\n // Find slot.\n var slot_num = this._find_slot(draw_coords);\n\n // Do slotting.\n if (slot_num >= 0) {\n // Add current feature to slot.\n if (start_end_dct[slot_num] === undefined) {\n start_end_dct[slot_num] = [];\n }\n start_end_dct[slot_num].push(draw_coords);\n this.slots[feature_uid] = new SlottedInfo(slot_num, feature);\n highest_slot = Math.max(highest_slot, slot_num);\n }\n }\n\n // Debugging: view slots data.\n /*\n for (var i = 0; i < MAX_FEATURE_DEPTH; i++) {\n var slot = start_end_dct[i];\n if (slot !== undefined) {\n console.log(i, \"*************\");\n for (var k = 0, k_len = slot.length; k < k_len; k++) {\n console.log(\"\\t\", slot[k][0], slot[k][1]);\n }\n }\n }\n */\n return highest_slot + 1;\n }\n});\n\nexport default {\n FeatureSlotter: FeatureSlotter\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/viz/trackster/slotting.js","import * as _ from \"libs/underscore\";\n\n/**\n * Compute the type of overlap between two regions. They are assumed to be on the same chrom/contig.\n * The overlap is computed relative to the second region; hence, OVERLAP_START indicates that the first\n * region overlaps the start (but not the end) of the second region.\n * NOTE: Coordinates are assumed to be in BED format: half open (start is closed, end is open).\n */\nvar BEFORE = 1001;\n\nvar CONTAINS = 1002;\nvar OVERLAP_START = 1003;\nvar OVERLAP_END = 1004;\nvar CONTAINED_BY = 1005;\nvar AFTER = 1006;\nvar compute_overlap = (first_region, second_region) => {\n var first_start = first_region[0];\n var first_end = first_region[1];\n var second_start = second_region[0];\n var second_end = second_region[1];\n var overlap;\n if (first_start < second_start) {\n if (first_end <= second_start) {\n overlap = BEFORE;\n } else if (first_end <= second_end) {\n overlap = OVERLAP_START;\n } else {\n // first_end > second_end\n overlap = CONTAINS;\n }\n } else {\n // first_start >= second_start\n if (first_start > second_end) {\n overlap = AFTER;\n } else if (first_end <= second_end) {\n overlap = CONTAINED_BY;\n } else {\n overlap = OVERLAP_END;\n }\n }\n\n return overlap;\n};\n\n/**\n * Returns true if regions overlap.\n */\nvar is_overlap = (first_region, second_region) => {\n var overlap = compute_overlap(first_region, second_region);\n return overlap !== BEFORE && overlap !== AFTER;\n};\n\n/**\n * Draw a dashed line on a canvas using filled rectangles. This function is based on:\n * http://vetruvet.blogspot.com/2010/10/drawing-dashed-lines-on-html5-canvas.html\n * However, that approach uses lines, which don't seem to render as well, so use\n * rectangles instead.\n */\nvar dashedLine = (ctx, x1, y1, x2, y2, dashLen) => {\n if (dashLen === undefined) {\n dashLen = 4;\n }\n var dX = x2 - x1;\n var dY = y2 - y1;\n var dashes = Math.floor(Math.sqrt(dX * dX + dY * dY) / dashLen);\n var dashX = dX / dashes;\n var dashY = dY / dashes;\n var q;\n\n for (q = 0; q < dashes; q++, x1 += dashX, y1 += dashY) {\n if (q % 2 !== 0) {\n continue;\n }\n ctx.fillRect(x1, y1, dashLen, 1);\n }\n};\n\n/**\n * Draw an isosceles triangle that points down.\n */\nvar drawDownwardEquilateralTriangle = function(ctx, down_vertex_x, down_vertex_y, side_len) {\n // Compute other two points of triangle.\n var x1 = down_vertex_x - side_len / 2;\n\n var x2 = down_vertex_x + side_len / 2;\n var y = down_vertex_y - Math.sqrt(side_len * 3 / 2);\n\n // Draw and fill.\n ctx.beginPath();\n ctx.moveTo(x1, y);\n ctx.lineTo(x2, y);\n ctx.lineTo(down_vertex_x, down_vertex_y);\n ctx.lineTo(x1, y);\n\n ctx.strokeStyle = this.fillStyle;\n ctx.fill();\n ctx.stroke();\n ctx.closePath();\n};\n\n/**\n * Base class for all scalers. Scalers produce values that are used to change (scale) drawing attributes.\n */\nvar Scaler = function(default_val) {\n this.default_val = default_val ? default_val : 1;\n};\n\n/**\n * Produce a scaling value.\n */\nScaler.prototype.gen_val = function(input) {\n return this.default_val;\n};\n\n/**\n * Results from painter.draw()\n */\nvar DrawResults = function(options) {\n this.incomplete_features = options.incomplete_features;\n this.feature_mapper = options.feature_mapper;\n};\n\n/**\n * Base class for painters\n *\n * -- Mode and prefs are both optional\n */\nvar Painter = function(data, view_start, view_end, prefs, mode) {\n // Data and data properties\n this.data = data;\n // View\n this.view_start = view_start;\n this.view_end = view_end;\n // Drawing prefs\n this.prefs = _.extend({}, this.default_prefs, prefs);\n this.mode = mode;\n};\n\nPainter.prototype.default_prefs = {};\n\n/**\n * Draw on the context using a rectangle of width x height using scale w_scale.\n */\nPainter.prototype.draw = (ctx, width, height, w_scale) => {};\n\n/**\n * Get starting drawing position, which is offset a half-base left of coordinate.\n */\nPainter.prototype.get_start_draw_pos = function(chrom_pos, w_scale) {\n return this._chrom_pos_to_draw_pos(chrom_pos, w_scale, -0.5);\n};\n\n/**\n * Get end drawing position, which is offset a half-base right of coordinate.\n */\nPainter.prototype.get_end_draw_pos = function(chrom_pos, w_scale) {\n return this._chrom_pos_to_draw_pos(chrom_pos, w_scale, 0.5);\n};\n\n/**\n * Get drawing position.\n */\nPainter.prototype.get_draw_pos = function(chrom_pos, w_scale) {\n return this._chrom_pos_to_draw_pos(chrom_pos, w_scale, 0);\n};\n\n/**\n * Convert chromosome position to drawing position.\n */\nPainter.prototype._chrom_pos_to_draw_pos = function(chrom_pos, w_scale, offset) {\n return Math.floor(w_scale * (Math.max(0, chrom_pos - this.view_start) + offset));\n};\n\nvar LinePainter = function(data, view_start, view_end, prefs, mode) {\n Painter.call(this, data, view_start, view_end, prefs, mode);\n};\n\nLinePainter.prototype.default_prefs = {\n min_value: undefined,\n max_value: undefined,\n mode: \"Histogram\",\n color: \"#000\",\n overflow_color: \"#F66\"\n};\n\nLinePainter.prototype.draw = function(ctx, width, height, w_scale) {\n var in_path = false;\n var min_value = this.prefs.min_value;\n var max_value = this.prefs.max_value;\n var vertical_range = max_value - min_value;\n var height_px = height;\n var view_start = this.view_start;\n var mode = this.mode;\n var data = this.data;\n\n ctx.save();\n\n // Pixel position of 0 on the y axis\n var y_zero = Math.round(height + min_value / vertical_range * height);\n\n // Horizontal line to denote x-axis\n if (mode !== \"Intensity\") {\n ctx.fillStyle = \"#aaa\";\n ctx.fillRect(0, y_zero, width, 1);\n }\n\n ctx.beginPath();\n var x_scaled;\n var y;\n var delta_x_pxs;\n if (data.length > 1) {\n delta_x_pxs = _.map(data.slice(0, -1), (d, i) => Math.ceil((data[i + 1][0] - data[i][0]) * w_scale));\n } else {\n delta_x_pxs = [10];\n }\n\n // Painter color can be in either block_color (FeatureTrack) or color pref (LineTrack).\n var painter_color = this.prefs.block_color || this.prefs.color;\n\n var // Extract RGB from preference color.\n pref_color = parseInt(painter_color.slice(1), 16);\n\n var pref_r = (pref_color & 0xff0000) >> 16;\n var pref_g = (pref_color & 0x00ff00) >> 8;\n var pref_b = pref_color & 0x0000ff;\n var top_overflow = false;\n var bot_overflow = false;\n\n // Paint track.\n var delta_x_px;\n for (var i = 0, len = data.length; i < len; i++) {\n // Reset attributes for next point.\n ctx.fillStyle = ctx.strokeStyle = painter_color;\n top_overflow = bot_overflow = false;\n delta_x_px = delta_x_pxs[i];\n\n x_scaled = Math.floor((data[i][0] - view_start - 0.5) * w_scale);\n y = data[i][1];\n\n // Process Y (scaler) value.\n if (y === null) {\n if (in_path && mode === \"Filled\") {\n ctx.lineTo(x_scaled, height_px);\n }\n in_path = false;\n continue;\n }\n\n // Bound Y value by min, max.\n if (y < min_value) {\n bot_overflow = true;\n y = min_value;\n } else if (y > max_value) {\n top_overflow = true;\n y = max_value;\n }\n\n // Draw point.\n if (mode === \"Histogram\") {\n // y becomes the bar height in pixels, which is the negated for canvas coords\n y = Math.round(y / vertical_range * height_px);\n ctx.fillRect(x_scaled, y_zero, delta_x_px, -y);\n } else if (mode === \"Intensity\") {\n var saturation = (y - min_value) / vertical_range;\n\n var // Range is [pref_color, 255] where saturation = 0 --> 255 and saturation = 1 --> pref color\n new_r = Math.round(pref_r + (255 - pref_r) * (1 - saturation));\n\n var new_g = Math.round(pref_g + (255 - pref_g) * (1 - saturation));\n var new_b = Math.round(pref_b + (255 - pref_b) * (1 - saturation));\n ctx.fillStyle = `rgb(${new_r},${new_g},${new_b})`;\n ctx.fillRect(x_scaled, 0, delta_x_px, height_px);\n } else {\n // mode is Coverage/Line or Filled.\n\n // Scale Y value.\n y = Math.round(height_px - (y - min_value) / vertical_range * height_px);\n if (in_path) {\n ctx.lineTo(x_scaled, y);\n } else {\n in_path = true;\n if (mode === \"Filled\") {\n ctx.moveTo(x_scaled, height_px);\n ctx.lineTo(x_scaled, y);\n } else {\n ctx.moveTo(x_scaled, y);\n // Use this approach (note: same as for filled) to draw line from 0 to\n // first data point.\n //ctx.moveTo(x_scaled, height_px);\n //ctx.lineTo(x_scaled, y);\n }\n }\n }\n\n // Draw lines at boundaries if overflowing min or max\n ctx.fillStyle = this.prefs.overflow_color;\n if (top_overflow || bot_overflow) {\n var overflow_x;\n if (mode === \"Histogram\" || mode === \"Intensity\") {\n overflow_x = delta_x_px;\n } else {\n // Line and Filled, which are points\n x_scaled -= 2; // Move it over to the left so it's centered on the point\n overflow_x = 4;\n }\n if (top_overflow) {\n ctx.fillRect(x_scaled, 0, overflow_x, 3);\n }\n if (bot_overflow) {\n ctx.fillRect(x_scaled, height_px - 3, overflow_x, 3);\n }\n }\n ctx.fillStyle = painter_color;\n }\n if (mode === \"Filled\") {\n if (in_path) {\n ctx.lineTo(x_scaled, y_zero);\n ctx.lineTo(0, y_zero);\n }\n ctx.fill();\n } else {\n ctx.stroke();\n }\n\n ctx.restore();\n};\n\n/**\n * Mapper that contains information about feature locations and data.\n */\nvar FeaturePositionMapper = function(slot_height) {\n this.feature_positions = {};\n this.slot_height = slot_height;\n this.translation = 0;\n this.y_translation = 0;\n};\n\n/**\n * Map feature data to a position defined by
NOTE: currently only uses the console.debug log function\n * (as opposed to debug, error, warn, etc.)\n * @name LoggableMixin\n *\n * @example\n * // Add to your models/views at the definition using chaining:\n * var MyModel = Backbone.Model.extend( LoggableMixin ).extend({ // ... });\n *\n * // or - more explicitly AFTER the definition:\n * var MyModel = Backbone.Model.extend({\n * logger : console\n * // ...\n * this.log( '$#%& it! - broken already...' );\n * })\n * _.extend( MyModel.prototype, LoggableMixin )\n *\n */\nvar LoggableMixin = /** @lends LoggableMixin# */ {\n // replace null with console (if available) to see all logs for a particular view/model\n /** The logging object whose log function will be used to output\n * messages. Null will supress all logging. Commonly set to console.\n */\n logger: null,\n /** @type {String} a namespace for filtering/focusing log output */\n _logNamespace: \".\"\n};\naddLogging(LoggableMixin);\n\n//==============================================================================\n/** Backbone model that syncs to the browser's sessionStorage API.\n * This all largely happens behind the scenes and no special calls are required.\n */\nvar SessionStorageModel = Backbone.Model.extend({\n initialize: function(initialAttrs) {\n // check for sessionStorage and error if no id is provided\n this._checkEnabledSessionStorage();\n if (!initialAttrs.id) {\n throw new Error(\"SessionStorageModel requires an id in the initial attributes\");\n }\n this.id = initialAttrs.id;\n\n // load existing from storage (if any), clear any attrs set by bbone before init is called,\n // layer initial over existing and defaults, and save\n var existing = !this.isNew() ? this._read(this) : {};\n this.clear({ silent: true });\n this.save(_.extend({}, this.defaults, existing, initialAttrs), {\n silent: true\n });\n\n // save on any change to it immediately\n this.on(\"change\", function() {\n this.save();\n });\n },\n\n _checkEnabledSessionStorage: function() {\n try {\n return window.sessionStorage.length >= 0;\n } catch (err) {\n alert(\"Please enable cookies in your browser for this Galaxy site\");\n return false;\n }\n },\n\n /** override of bbone sync to save to sessionStorage rather than REST\n * bbone options (success, errror, etc.) should still apply\n */\n sync: function(method, model, options) {\n if (!options.silent) {\n model.trigger(\"request\", model, {}, options);\n }\n var returned = {};\n switch (method) {\n case \"create\":\n returned = this._create(model);\n break;\n case \"read\":\n returned = this._read(model);\n break;\n case \"update\":\n returned = this._update(model);\n break;\n case \"delete\":\n returned = this._delete(model);\n break;\n }\n if (returned !== undefined || returned !== null) {\n if (options.success) {\n options.success();\n }\n } else {\n if (options.error) {\n options.error();\n }\n }\n return returned;\n },\n\n /** set storage to the stringified item */\n _create: function(model) {\n try {\n var json = model.toJSON();\n var set = sessionStorage.setItem(model.id, JSON.stringify(json));\n return set === null ? set : json;\n // DOMException is thrown in Safari if in private browsing mode and sessionStorage is attempted:\n // http://stackoverflow.com/questions/14555347\n // TODO: this could probably use a more general soln - like detecting priv. mode + safari => non-ajaxing Model\n } catch (err) {\n if (!(err instanceof DOMException && navigator.userAgent.indexOf(\"Safari\") > -1)) {\n throw err;\n }\n }\n return null;\n },\n\n /** read and parse json from storage */\n _read: function(model) {\n return JSON.parse(sessionStorage.getItem(model.id));\n },\n\n /** set storage to the item (alias to create) */\n _update: function(model) {\n return model._create(model);\n },\n\n /** remove the item from storage */\n _delete: function(model) {\n return sessionStorage.removeItem(model.id);\n },\n\n /** T/F whether sessionStorage contains the model's id (data is present) */\n isNew: function() {\n return !sessionStorage.hasOwnProperty(this.id);\n },\n\n _log: function() {\n return JSON.stringify(this.toJSON(), null, \" \");\n },\n toString: function() {\n return `SessionStorageModel(${this.id})`;\n }\n});\n(() => {\n SessionStorageModel.prototype = _.omit(SessionStorageModel.prototype, \"url\", \"urlRoot\");\n})();\n\n//==============================================================================\n/** Function that allows mixing of hashs into bbone MVC while showing the mixins first\n * (before the more local class overrides/hash).\n * Basically, a simple reversal of param order on _.defaults() - to show mixins in top of definition.\n * @example:\n * var NewModel = Something.extend( mixin( MyMixinA, MyMixinB, { ... myVars : ... }) );\n *\n * NOTE: this does not combine any hashes (like events, etc.) and you're expected to handle that\n */\nfunction mixin(mixinHash1, /* mixinHash2, etc: ... variadic */ propsHash) {\n var args = Array.prototype.slice.call(arguments, 0);\n var lastArg = args.pop();\n args.unshift(lastArg);\n return _.defaults.apply(_, args);\n}\n\n//==============================================================================\n/** A mixin for models that allow T/F/Matching to their attributes - useful when\n * searching or filtering collections of models.\n * @example:\n * see hda-model for searchAttribute and searchAliases definition examples.\n * see history-contents.matches for how collections are filtered\n * and see readonly-history-view.searchHdas for how user input is connected to the filtering\n */\nvar SearchableModelMixin = {\n /** what attributes of an HDA will be used in a text search */\n searchAttributes: [\n // override\n ],\n\n /** our attr keys don't often match the labels we display to the user - so, when using\n * attribute specifiers ('name=\"bler\"') in a term, allow passing in aliases for the\n * following attr keys.\n */\n searchAliases: {\n // override\n },\n\n /** search the attribute with key attrKey for the string searchFor; T/F if found */\n searchAttribute: function(attrKey, searchFor) {\n var attrVal = this.get(attrKey);\n //this.debug( 'searchAttribute', attrKey, attrVal, searchFor );\n // bail if empty searchFor or unsearchable values\n if (!searchFor || (attrVal === undefined || attrVal === null)) {\n return false;\n }\n // pass to sep. fn for deep search of array attributes\n if (_.isArray(attrVal)) {\n return this._searchArrayAttribute(attrVal, searchFor);\n }\n return (\n attrVal\n .toString()\n .toLowerCase()\n .indexOf(searchFor.toLowerCase()) !== -1\n );\n },\n\n /** deep(er) search for array attributes; T/F if found */\n _searchArrayAttribute: function(array, searchFor) {\n //this.debug( '_searchArrayAttribute', array, searchFor );\n searchFor = searchFor.toLowerCase();\n //precondition: searchFor has already been validated as non-empty string\n //precondition: assumes only 1 level array\n //TODO: could possibly break up searchFor more (CSV...)\n return _.any(\n array,\n elem =>\n elem\n .toString()\n .toLowerCase()\n .indexOf(searchFor.toLowerCase()) !== -1\n );\n },\n\n /** search all searchAttributes for the string searchFor,\n * returning a list of keys of attributes that contain searchFor\n */\n search: function(searchFor) {\n var model = this;\n return _.filter(this.searchAttributes, key => model.searchAttribute(key, searchFor));\n },\n\n /** alias of search, but returns a boolean; accepts attribute specifiers where\n * the attributes searched can be narrowed to a single attribute using\n * the form: matches( 'genome_build=hg19' )\n * (the attribute keys allowed can also be aliases to the true attribute key;\n * see searchAliases above)\n * @param {String} term plain text or ATTR_SPECIFIER sep. key=val pair\n * @returns {Boolean} was term found in (any) attribute(s)\n */\n matches: function(term) {\n var ATTR_SPECIFIER = \"=\";\n var split = term.split(ATTR_SPECIFIER);\n // attribute is specified - search only that\n if (split.length >= 2) {\n var attrKey = split[0];\n attrKey = this.searchAliases[attrKey] || attrKey;\n return this.searchAttribute(attrKey, split[1]);\n }\n // no attribute is specified - search all attributes in searchAttributes\n return !!this.search(term).length;\n },\n\n /** an implicit AND search for all terms; IOW, a model must match all terms given\n * where terms is a whitespace separated value string.\n * e.g. given terms of: 'blah bler database=hg19'\n * an HDA would have to have attributes containing blah AND bler AND a genome_build == hg19\n * To include whitespace in terms: wrap the term in double quotations (name=\"blah bler\").\n */\n matchesAll: function(terms) {\n var model = this;\n // break the terms up by whitespace and filter out the empty strings\n terms = terms.match(/(\".*\"|\\w*=\".*\"|\\S*)/g).filter(s => !!s);\n return _.all(terms, term => {\n term = term.replace(/\"/g, \"\");\n return model.matches(term);\n });\n }\n};\n\n//==============================================================================\n/** A view that renders hidden and shows when some activator is clicked.\n * options:\n * showFn: the effect used to show/hide the View (defaults to jq.toggle)\n * $elementShown: some jqObject (defaults to this.$el) to be shown/hidden\n * onShowFirstTime: fn called the first time the view is shown\n * onshow: fn called every time the view is shown\n * onhide: fn called every time the view is hidden\n * events:\n * hiddenUntilActivated:shown (the view is passed as an arg)\n * hiddenUntilActivated:hidden (the view is passed as an arg)\n * instance vars:\n * view.hidden {boolean} is the view in the hidden state\n */\nvar HiddenUntilActivatedViewMixin = /** @lends hiddenUntilActivatedMixin# */ {\n //TODO: since this is a mixin, consider moving toggle, hidden into HUAVOptions\n\n /** call this in your initialize to set up the mixin\n * @param {jQuery} $activator the 'button' that's clicked to show/hide the view\n * @param {Object} hash with mixin options\n */\n hiddenUntilActivated: function($activator, options) {\n // call this in your view's initialize fn\n options = options || {};\n //TODO: flesh out options - show them all here\n this.HUAVOptions = {\n $elementShown: this.$el,\n showFn: jQuery.prototype.toggle,\n showSpeed: \"fast\"\n };\n _.extend(this.HUAVOptions, options || {});\n /** has this been shown already (and onshowFirstTime called)? */\n this.HUAVOptions.hasBeenShown = this.HUAVOptions.$elementShown.is(\":visible\");\n this.hidden = this.isHidden();\n\n if ($activator) {\n var mixin = this;\n $activator.on(\"click\", ev => {\n mixin.toggle(mixin.HUAVOptions.showSpeed);\n });\n }\n },\n\n //TODO:?? remove? use .hidden?\n /** returns T/F if the view is hidden */\n isHidden: function() {\n return this.HUAVOptions.$elementShown.is(\":hidden\");\n },\n\n /** toggle the hidden state, show/hide $elementShown, call onshow/hide, trigger events */\n toggle: function() {\n //TODO: more specific name - toggle is too general\n // can be called manually as well with normal toggle arguments\n //TODO: better as a callback (when the show/hide is actually done)\n // show\n if (this.hidden) {\n // fire the optional fns on the first/each showing - good for render()\n if (!this.HUAVOptions.hasBeenShown) {\n if (_.isFunction(this.HUAVOptions.onshowFirstTime)) {\n this.HUAVOptions.hasBeenShown = true;\n this.HUAVOptions.onshowFirstTime.call(this);\n }\n }\n if (_.isFunction(this.HUAVOptions.onshow)) {\n this.HUAVOptions.onshow.call(this);\n this.trigger(\"hiddenUntilActivated:shown\", this);\n }\n this.hidden = false;\n\n // hide\n } else {\n if (_.isFunction(this.HUAVOptions.onhide)) {\n this.HUAVOptions.onhide.call(this);\n this.trigger(\"hiddenUntilActivated:hidden\", this);\n }\n this.hidden = true;\n }\n return this.HUAVOptions.showFn.apply(this.HUAVOptions.$elementShown, arguments);\n }\n};\n\n//==============================================================================\n/** Mixin for views that can be dragged and dropped\n * Allows for the drag behavior to be turned on/off, setting/removing jQuery event\n * handlers each time.\n * dataTransfer data is set to the JSON string of the view's model.toJSON\n * Override '$dragHandle' to define the draggable DOM sub-element.\n */\nvar DraggableViewMixin = {\n /** set up instance vars to track whether this view is currently draggable */\n initialize: function(attributes) {\n /** is the body of this hda view expanded/not? */\n this.draggable = attributes.draggable || false;\n },\n\n /** what part of the view's DOM triggers the dragging */\n $dragHandle: function() {\n //TODO: make abstract/general - move this to listItem\n // override to the element you want to be your view's handle\n return this.$(\".title-bar\");\n },\n\n /** toggle whether this view is draggable */\n toggleDraggable: function() {\n if (this.draggable) {\n this.draggableOff();\n } else {\n this.draggableOn();\n }\n },\n\n /** allow the view to be dragged, set up event handlers */\n draggableOn: function() {\n this.draggable = true;\n this.dragStartHandler = _.bind(this._dragStartHandler, this);\n this.dragEndHandler = _.bind(this._dragEndHandler, this);\n\n var handle = this.$dragHandle()\n .attr(\"draggable\", true)\n .get(0);\n handle.addEventListener(\"dragstart\", this.dragStartHandler, false);\n handle.addEventListener(\"dragend\", this.dragEndHandler, false);\n },\n\n /** turn of view dragging and remove event listeners */\n draggableOff: function() {\n this.draggable = false;\n var handle = this.$dragHandle()\n .attr(\"draggable\", false)\n .get(0);\n handle.removeEventListener(\"dragstart\", this.dragStartHandler, false);\n handle.removeEventListener(\"dragend\", this.dragEndHandler, false);\n },\n\n /** sets the dataTransfer data to the model's toJSON\n * @fires draggable:dragstart (bbone event) which is passed the event and this view\n */\n _dragStartHandler: function(event) {\n event.dataTransfer.effectAllowed = \"move\";\n //ASSUMES: this.model\n //TODO: all except IE: should be 'application/json', IE: must be 'text'\n event.dataTransfer.setData(\"text\", JSON.stringify(this.model.toJSON()));\n this.trigger(\"draggable:dragstart\", event, this);\n return false;\n },\n\n /** handle the dragend\n * @fires draggable:dragend (bbone event) which is passed the event and this view\n */\n _dragEndHandler: function(event) {\n this.trigger(\"draggable:dragend\", event, this);\n return false;\n }\n};\n\n//==============================================================================\n/** Mixin that allows a view to be selected (gen. from a list).\n * Selection controls ($selector) may be hidden/shown/toggled.\n * The bbone event 'selectable' is fired when the controls are shown/hidden (passed T/F).\n * Default rendering is a font-awesome checkbox.\n * Default selector is '.selector' within the view's $el.\n * The bbone events 'selected' and 'de-selected' are fired when the $selector is clicked.\n * Both events are passed the view and the (jQuery) event.\n */\nvar SelectableViewMixin = {\n /** Set up instance state vars for whether the selector is shown and whether the view has been selected */\n initialize: function(attributes) {\n /** is the view currently in selection mode? */\n this.selectable = attributes.selectable || false;\n /** is the view currently selected? */\n this.selected = attributes.selected || false;\n },\n\n /** $el sub-element where the selector is rendered and what can be clicked to select. */\n $selector: function() {\n return this.$(\".selector\");\n },\n\n /** How the selector is rendered - defaults to font-awesome checkbox */\n _renderSelected: function() {\n // override\n this.$selector()\n .find(\"span\")\n .toggleClass(\"fa-check-square-o\", this.selected)\n .toggleClass(\"fa-square-o\", !this.selected);\n },\n\n /** Toggle whether the selector is shown */\n toggleSelector: function() {\n //TODO: use this.selectable\n if (!this.$selector().is(\":visible\")) {\n this.showSelector();\n } else {\n this.hideSelector();\n }\n },\n\n /** Display the selector control.\n * @param {Number} a jQuery fx speed\n * @fires: selectable which is passed true (IOW, the selector is shown) and the view\n */\n showSelector: function(speed) {\n speed = speed !== undefined ? speed : this.fxSpeed;\n // make sure selected state is represented properly\n this.selectable = true;\n this.trigger(\"selectable\", true, this);\n this._renderSelected();\n if (speed) {\n this.$selector().show(speed);\n } else {\n this.$selector().show();\n }\n },\n\n /** remove the selector control\n * @param {Number} a jQuery fx speed\n * @fires: selectable which is passed false (IOW, the selector is not shown) and the view\n */\n hideSelector: function(speed) {\n speed = speed !== undefined ? speed : this.fxSpeed;\n // reverse the process from showSelect\n this.selectable = false;\n this.trigger(\"selectable\", false, this);\n if (speed) {\n this.$selector().hide(speed);\n } else {\n this.$selector().hide();\n }\n },\n\n /** Toggle whether the view is selected */\n toggleSelect: function(event) {\n if (this.selected) {\n this.deselect(event);\n } else {\n this.select(event);\n }\n },\n\n /** Select this view and re-render the selector control to show it\n * @param {Event} a jQuery event that caused the selection\n * @fires: selected which is passed the view and the DOM event that triggered it (optionally)\n */\n select: function(event) {\n // switch icon, set selected, and trigger event\n if (!this.selected) {\n this.trigger(\"selected\", this, event);\n this.selected = true;\n this._renderSelected();\n }\n return false;\n },\n\n /** De-select this view and re-render the selector control to show it\n * @param {Event} a jQuery event that caused the selection\n * @fires: de-selected which is passed the view and the DOM event that triggered it (optionally)\n */\n deselect: function(event) {\n // switch icon, set selected, and trigger event\n if (this.selected) {\n this.trigger(\"de-selected\", this, event);\n this.selected = false;\n this._renderSelected();\n }\n return false;\n }\n};\n\n//==============================================================================\n/** Return an underscore template fn from an array of strings.\n * @param {String[]} template the template strings to compile into the underscore template fn\n * @param {String} jsonNamespace an optional namespace for the json data passed in (defaults to 'model')\n * @returns {Function} the (wrapped) underscore template fn\n * The function accepts:\n *\n * The template strings can access:\n * the json/model hash using model (\"<%- model.myAttr %>) using the jsonNamespace above\n * _l: the localizer function\n * view (if passed): ostensibly, the view using the template (handy for view instance vars)\n * Because they're namespaced, undefined attributes will not throw an error.\n *\n * @example:\n * templateBler : BASE_MVC.wrapTemplate([\n * '\").attr({\n id: \"content_table\",\n cellpadding: 0\n });\n this.$el.append(data_table);\n var column_names = this.model.get_metadata(\"column_names\");\n var header_container = $(\"\").appendTo(data_table);\n var header_row = $(\"
\").appendTo(header_container);\n if (column_names) {\n header_row.append(` ${column_names.join(\" \")} `);\n } else {\n for (var j = 1; j <= this.model.get_metadata(\"columns\"); j++) {\n header_row.append(`${j} `);\n }\n }\n\n // Render first chunk.\n var self = this;\n\n var first_chunk = this.model.get(\"first_data_chunk\");\n if (first_chunk) {\n // First chunk is bootstrapped, so render now.\n this._renderChunk(first_chunk);\n } else {\n // No bootstrapping, so get first chunk and then render.\n $.when(self.model.get_next_chunk()).then(result => {\n self._renderChunk(result);\n });\n }\n\n // -- Show new chunks during scrolling. --\n\n // Set up chunk loading when scrolling using the scrolling element.\n this.scroll_elt.scroll(() => {\n self.attempt_to_fetch();\n });\n },\n\n /**\n * Returns true if user has scrolled to the bottom of the view.\n */\n scrolled_to_bottom: function() {\n return false;\n },\n\n // -- Helper functions. --\n\n _renderCell: function(cell_contents, index, colspan) {\n var $cell = $(\"\").text(cell_contents);\n var column_types = this.model.get_metadata(\"column_types\");\n if (colspan !== undefined) {\n $cell.attr(\"colspan\", colspan).addClass(\"stringalign\");\n } else if (column_types) {\n if (index < column_types.length) {\n if (column_types[index] === \"str\" || column_types[index] === \"list\") {\n /* Left align all str columns, right align the rest */\n $cell.addClass(\"stringalign\");\n }\n }\n }\n return $cell;\n },\n\n _renderRow: function(line) {\n // Check length of cells to ensure this is a complete row.\n var cells = line.split(\"\\t\");\n\n var row = $(\" \");\n var num_columns = this.model.get_metadata(\"columns\");\n\n if (this.row_count % 2 !== 0) {\n row.addClass(\"dark_row\");\n }\n\n if (cells.length === num_columns) {\n _.each(\n cells,\n function(cell_contents, index) {\n row.append(this._renderCell(cell_contents, index));\n },\n this\n );\n } else if (cells.length > num_columns) {\n // SAM file or like format with optional metadata included.\n _.each(\n cells.slice(0, num_columns - 1),\n function(cell_contents, index) {\n row.append(this._renderCell(cell_contents, index));\n },\n this\n );\n row.append(this._renderCell(cells.slice(num_columns - 1).join(\"\\t\"), num_columns - 1));\n } else if (cells.length === 1) {\n // Comment line, just return the one cell.\n row.append(this._renderCell(line, 0, num_columns));\n } else {\n // cells.length is greater than one, but less than num_columns. Render cells and pad tds.\n // Possibly a SAM file or like format with optional metadata missing.\n // Could also be a tabular file with a line with missing columns.\n _.each(\n cells,\n function(cell_contents, index) {\n row.append(this._renderCell(cell_contents, index));\n },\n this\n );\n _.each(_.range(num_columns - cells.length), () => {\n row.append($(\" ")),this.$cover=this.$(".upload-settings-cover"),this.$table=this.$(".upload-settings-table > tbody"),this.listenTo(this.model,"change",this.render,this),this.model.trigger("change")},render:function(){var e=this;this.$table.empty(),s.each(this.options.parameters,function(t){var i=n("").addClass("upload-"+t.id+" upload-icon-button fa").addClass(e.model.get(t.id)&&e.options.class_check||e.options.class_uncheck).on("click",function(){e.model.get("enabled")&&e.model.set(t.id,!e.model.get(t.id))});e.$table.append(n("\"));\n });\n }\n\n this.row_count++;\n return row;\n },\n\n _renderChunk: function(chunk) {\n var data_table = this.$el.find(\"table\");\n _.each(\n chunk.ck_data.split(\"\\n\"),\n function(line, index) {\n if (line !== \"\") {\n data_table.append(this._renderRow(line));\n }\n },\n this\n );\n }\n});\n\n/**\n * Tabular view that is placed at the top level of page. Scrolling occurs\n * view top-level elements outside of view.\n */\nvar TopLevelTabularDatasetChunkedView = TabularDatasetChunkedView.extend({\n initialize: function(options) {\n TabularDatasetChunkedView.prototype.initialize.call(this, options);\n\n // Scrolling happens in top-level elements.\n var scroll_elt = _.find(this.$el.parents(), p => $(p).css(\"overflow\") === \"auto\");\n\n // If no scrolling element found, use window.\n if (!scroll_elt) {\n scroll_elt = window;\n }\n\n // Wrap scrolling element for easy access.\n this.scroll_elt = $(scroll_elt);\n },\n\n /**\n * Returns true if user has scrolled to the bottom of the view.\n */\n scrolled_to_bottom: function() {\n return this.$el.height() - this.scroll_elt.scrollTop() - this.scroll_elt.height() <= 0;\n }\n});\n\n/**\n * Tabular view tnat is embedded in a page. Scrolling occurs in view's el.\n */\nvar EmbeddedTabularDatasetChunkedView = TabularDatasetChunkedView.extend({\n initialize: function(options) {\n TabularDatasetChunkedView.prototype.initialize.call(this, options);\n\n // Because view is embedded, set up div to do scrolling.\n this.scroll_elt = this.$el.css({\n position: \"relative\",\n overflow: \"scroll\",\n height: options.height || \"500px\"\n });\n },\n\n /**\n * Returns true if user has scrolled to the bottom of the view.\n */\n scrolled_to_bottom: function() {\n return this.$el.scrollTop() + this.$el.innerHeight() >= this.el.scrollHeight;\n }\n});\n\n/** Button for trackster visualization */\nvar TabularButtonTracksterView = Backbone.View.extend({\n // gene region columns\n col: {\n chrom: null,\n start: null,\n end: null\n },\n\n // url for trackster\n url_viz: null,\n\n // dataset id\n dataset_id: null,\n\n // database key\n genome_build: null,\n\n // data type\n file_ext: null,\n\n // backbone initialize\n initialize: function(options) {\n // check if environment is available\n var Galaxy = parent.Galaxy;\n\n // link galaxy modal or create one\n if (Galaxy && Galaxy.modal) {\n this.modal = Galaxy.modal;\n }\n\n // link galaxy frames\n if (Galaxy && Galaxy.frame) {\n this.frame = Galaxy.frame;\n }\n\n // check\n if (!this.modal || !this.frame) {\n return;\n }\n\n // model/metadata\n var model = options.model;\n var metadata = model.get(\"metadata\");\n\n // check for datatype\n if (!model.get(\"file_ext\")) {\n return;\n }\n\n // get data type\n this.file_ext = model.get(\"file_ext\");\n\n // check for bed-file format\n if (this.file_ext == \"bed\") {\n // verify that metadata exists\n if (metadata.get(\"chromCol\") && metadata.get(\"startCol\") && metadata.get(\"endCol\")) {\n // read in columns\n this.col.chrom = metadata.get(\"chromCol\") - 1;\n this.col.start = metadata.get(\"startCol\") - 1;\n this.col.end = metadata.get(\"endCol\") - 1;\n } else {\n console.log(\"TabularButtonTrackster : Bed-file metadata incomplete.\");\n return;\n }\n }\n\n // check for vcf-file format\n if (this.file_ext == \"vcf\") {\n // search array\n function search(str, array) {\n for (var j = 0; j < array.length; j++) if (array[j].match(str)) return j;\n return -1;\n }\n\n // load\n this.col.chrom = search(\"Chrom\", metadata.get(\"column_names\"));\n this.col.start = search(\"Pos\", metadata.get(\"column_names\"));\n this.col.end = null;\n\n // verify that metadata exists\n if (this.col.chrom == -1 || this.col.start == -1) {\n console.log(\"TabularButtonTrackster : VCF-file metadata incomplete.\");\n return;\n }\n }\n\n // check\n if (this.col.chrom === undefined) {\n return;\n }\n\n // get dataset id\n if (model.id) {\n this.dataset_id = model.id;\n } else {\n console.log(\"TabularButtonTrackster : Dataset identification is missing.\");\n return;\n }\n\n // get url\n if (model.get(\"url_viz\")) {\n this.url_viz = model.get(\"url_viz\");\n } else {\n console.log(\"TabularButtonTrackster : Url for visualization controller is missing.\");\n return;\n }\n\n // get genome_build / database key\n if (model.get(\"genome_build\")) {\n this.genome_build = model.get(\"genome_build\");\n }\n\n // create the icon\n var btn_viz = new mod_icon_btn.IconButtonView({\n model: new mod_icon_btn.IconButton({\n title: \"Visualize\",\n icon_class: \"chart_curve\",\n id: \"btn_viz\"\n })\n });\n\n // set element\n this.setElement(options.$el);\n\n // add to element\n this.$el.append(btn_viz.render().$el);\n\n // hide the button\n this.hide();\n },\n\n /** Add event handlers */\n events: {\n \"mouseover tr\": \"show\",\n mouseleave: \"hide\"\n },\n\n // show button\n show: function(e) {\n var self = this;\n\n // is numeric\n function is_numeric(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n\n // check\n if (this.col.chrom === null) return;\n\n // get selected data line\n var row = $(e.target).parent();\n\n // verify that location has been found\n var chrom = row\n .children()\n .eq(this.col.chrom)\n .html();\n var start = row\n .children()\n .eq(this.col.start)\n .html();\n\n // end is optional\n var end = this.col.end\n ? row\n .children()\n .eq(this.col.end)\n .html()\n : start;\n\n // double check location\n if (!chrom.match(\"^#\") && chrom !== \"\" && is_numeric(start)) {\n // get target gene region\n var btn_viz_pars = {\n dataset_id: this.dataset_id,\n gene_region: `${chrom}:${start}-${end}`\n };\n\n // get button position\n var offset = row.offset();\n var left = offset.left - 10;\n var top = offset.top - $(window).scrollTop() + 3;\n\n // update css\n $(\"#btn_viz\").css({\n position: \"fixed\",\n top: `${top}px`,\n left: `${left}px`\n });\n $(\"#btn_viz\").off(\"click\");\n $(\"#btn_viz\").click(() => {\n self.frame.add({\n title: \"Trackster\",\n url: `${self.url_viz}/trackster?${$.param(btn_viz_pars)}`\n });\n });\n\n // show the button\n $(\"#btn_viz\").show();\n } else {\n // hide the button\n $(\"#btn_viz\").hide();\n }\n },\n\n /** hide button */\n hide: function() {\n this.$(\"#btn_viz\").hide();\n }\n});\n\n// -- Utility functions. --\n\n/**\n * Create a model, attach it to a view, render view, and attach it to a parent element.\n */\nvar createModelAndView = (model, view, model_config, parent_elt) => {\n // Create model, view.\n var a_view = new view({\n model: new model(model_config)\n });\n\n // Render view and add to parent element.\n a_view.render();\n if (parent_elt) {\n parent_elt.append(a_view.$el);\n }\n\n return a_view;\n};\n\n/**\n * Create a tabular dataset chunked view (and requisite tabular dataset model)\n * and appends to parent_elt.\n */\nvar createTabularDatasetChunkedView = options => {\n // If no model, create and set model from dataset config.\n if (!options.model) {\n options.model = new TabularDataset(options.dataset_config);\n }\n\n var parent_elt = options.parent_elt;\n var embedded = options.embedded;\n\n // Clean up options so that only needed options are passed to view.\n delete options.embedded;\n delete options.parent_elt;\n delete options.dataset_config;\n\n // Create and set up view.\n var view = embedded\n ? new EmbeddedTabularDatasetChunkedView(options)\n : new TopLevelTabularDatasetChunkedView(options);\n view.render();\n\n if (parent_elt) {\n parent_elt.append(view.$el);\n // If we're sticking this in another element, once it's appended check\n // to make sure we've filled enough space.\n // Without this, the scroll elements don't work.\n view.expand_to_container();\n }\n\n return view;\n};\n\nexport default {\n Dataset: Dataset,\n TabularDataset: TabularDataset,\n DatasetCollection: DatasetCollection,\n TabularDatasetChunkedView: TabularDatasetChunkedView,\n createTabularDatasetChunkedView: createTabularDatasetChunkedView\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/mvc/dataset/data.js","/** Generic form view */\nimport Form from \"mvc/form/form-view\";\nimport Ui from \"mvc/ui/ui-misc\";\nvar View = Backbone.View.extend({\n initialize: function(options) {\n this.model = new Backbone.Model(options);\n this.url = this.model.get(\"url\");\n this.redirect = this.model.get(\"redirect\");\n this.setElement(\"\");\n this.render();\n },\n\n render: function() {\n var self = this;\n $.ajax({\n url: Galaxy.root + this.url,\n type: \"GET\"\n })\n .done(response => {\n var options = $.extend({}, self.model.attributes, response);\n var form = new Form({\n title: options.title,\n message: options.message,\n status: options.status || \"warning\",\n icon: options.icon,\n inputs: options.inputs,\n buttons: {\n submit: new Ui.Button({\n tooltip: options.submit_tooltip,\n title: options.submit_title || \"Save\",\n icon: options.submit_icon || \"fa-save\",\n cls: \"btn btn-primary ui-clear-float\",\n onclick: function() {\n self._submit(form);\n }\n })\n }\n });\n self.$el.empty().append(form.$el);\n })\n .fail(response => {\n self.$el.empty().append(\n new Ui.Message({\n message: `Failed to load resource ${self.url}.`,\n status: \"danger\",\n persistent: true\n }).$el\n );\n });\n },\n\n _submit: function(form) {\n var self = this;\n $.ajax({\n url: Galaxy.root + self.url,\n data: JSON.stringify(form.data.create()),\n type: \"PUT\",\n contentType: \"application/json\"\n })\n .done(response => {\n var success_message = {\n message: response.message,\n status: \"success\",\n persistent: false\n };\n if (self.redirect) {\n window.location = `${Galaxy.root + self.redirect}?${$.param(success_message)}`;\n } else {\n form.data.matchModel(response, (input, input_id) => {\n form.field_list[input_id].value(input.value);\n });\n self._showMessage(form, success_message);\n }\n })\n .fail(response => {\n self._showMessage(form, {\n message: response.responseJSON.err_msg,\n status: \"danger\",\n persistent: false\n });\n });\n },\n\n _showMessage: function(form, options) {\n var $panel = form.$el\n .parents()\n .filter(function() {\n return [\"auto\", \"scroll\"].indexOf($(this).css(\"overflow\")) != -1;\n })\n .first();\n $panel.animate({ scrollTop: 0 }, 500);\n form.message.update(options);\n }\n});\n\nexport default {\n View: View\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/mvc/form/form-wrapper.js","import * as _ from \"libs/underscore\";\nimport data_mod from \"mvc/dataset/data\";\nimport util_mod from \"viz/trackster/util\";\nimport config_mod from \"utils/config\";\nimport GridView from \"mvc/grid/grid-view\";\nimport Tabs from \"mvc/ui/ui-tabs\";\nimport Ui from \"mvc/ui/ui-misc\";\n/**\n * Mixin for returning custom JSON representation from toJSON. Class attribute to_json_keys defines a set of attributes\n * to include in the representation; to_json_mappers defines mappers for returned objects.\n */\nvar CustomToJSON = {\n /**\n * Returns JSON representation of object using to_json_keys and to_json_mappers.\n */\n toJSON: function() {\n var self = this;\n var json = {};\n _.each(self.constructor.to_json_keys, k => {\n var val = self.get(k);\n if (k in self.constructor.to_json_mappers) {\n val = self.constructor.to_json_mappers[k](val, self);\n }\n json[k] = val;\n });\n return json;\n }\n};\n\n/**\n * Model, view, and controller objects for Galaxy visualization framework.\n *\n * Models have no references to views, instead using events to indicate state\n * changes; this is advantageous because multiple views can use the same object\n * and models can be used without views.\n */\n\n/**\n * Use a popup grid to select datasets from histories or libraries. After datasets are selected,\n * track definitions are obtained from the server and the success_fn is called with the list of\n * definitions for selected datasets.\n */\nvar select_datasets = (filters, success_fn) => {\n // history dataset selection tab\n var history_grid = new GridView({\n url_base: `${Galaxy.root}visualization/list_history_datasets`,\n filters: filters,\n dict_format: true,\n embedded: true\n });\n\n // library dataset selection tab\n var library_grid = new GridView({\n url_base: `${Galaxy.root}visualization/list_library_datasets`,\n dict_format: true,\n embedded: true\n });\n\n // build tabs\n var tabs = new Tabs.View();\n tabs.add({\n id: \"histories\",\n title: \"Histories\",\n $el: $(\"\").append(history_grid.$el)\n });\n tabs.add({\n id: \"libraries\",\n title: \"Libraries\",\n $el: $(\"\").append(library_grid.$el)\n });\n\n // modal\n Galaxy.modal.show({\n title: \"Select datasets for new tracks\",\n body: tabs.$el,\n closing_events: true,\n buttons: {\n Cancel: function() {\n Galaxy.modal.hide();\n },\n Add: function() {\n var requests = [];\n tabs.$(\"input.grid-row-select-checkbox[name=id]:checked\").each(function() {\n window.console.log($(this).val());\n requests[requests.length] = $.ajax({\n url: `${Galaxy.root}api/datasets/${$(this).val()}`,\n dataType: \"json\",\n data: {\n data_type: \"track_config\",\n hda_ldda: tabs.current() == \"histories\" ? \"hda\" : \"ldda\"\n }\n });\n });\n // To preserve order, wait until there are definitions for all tracks and then add\n // them sequentially.\n $.when.apply($, requests).then(function() {\n // jQuery always returns an Array for arguments, so need to look at first element\n // to determine whether multiple requests were made and consequently how to\n // map arguments to track definitions.\n var track_defs = arguments[0] instanceof Array ? $.map(arguments, arg => arg[0]) : [arguments[0]];\n success_fn(track_defs);\n });\n Galaxy.modal.hide();\n }\n }\n });\n};\n\n// --------- Models ---------\n\n/**\n * Canvas manager is used to create canvases for browsers as well as providing a pattern cache\n */\nvar CanvasManager = function(default_font) {\n this.default_font = default_font !== undefined ? default_font : \"9px Monaco, Lucida Console, monospace\";\n\n this.dummy_canvas = this.new_canvas();\n this.dummy_context = this.dummy_canvas.getContext(\"2d\");\n this.dummy_context.font = this.default_font;\n\n this.char_width_px = this.dummy_context.measureText(\"A\").width;\n\n this.patterns = {};\n\n // FIXME: move somewhere to make this more general\n this.load_pattern(\"right_strand\", \"/visualization/strand_right.png\");\n this.load_pattern(\"left_strand\", \"/visualization/strand_left.png\");\n this.load_pattern(\"right_strand_inv\", \"/visualization/strand_right_inv.png\");\n this.load_pattern(\"left_strand_inv\", \"/visualization/strand_left_inv.png\");\n};\n\n_.extend(CanvasManager.prototype, {\n load_pattern: function(key, path) {\n var patterns = this.patterns;\n var dummy_context = this.dummy_context;\n var image = new Image();\n image.src = `${Galaxy.root}static/images${path}`;\n image.onload = () => {\n patterns[key] = dummy_context.createPattern(image, \"repeat\");\n };\n },\n get_pattern: function(key) {\n return this.patterns[key];\n },\n new_canvas: function() {\n var canvas = $(\"\")[0];\n // Keep a reference back to the manager\n canvas.manager = this;\n return canvas;\n }\n});\n\n/**\n * Generic cache that handles key/value pairs. Keys can be any object that can be\n * converted to a String and compared.\n */\nvar Cache = Backbone.Model.extend({\n defaults: {\n num_elements: 20,\n // Objects in cache; indexes into cache are strings of keys.\n obj_cache: null,\n // key_ary contains keys for objects in cache.\n key_ary: null\n },\n\n initialize: function(options) {\n this.clear();\n },\n\n /**\n * Get an element from the cache using its key.\n */\n get_elt: function(key) {\n var obj_cache = this.attributes.obj_cache;\n var key_ary = this.attributes.key_ary;\n var key_str = key.toString();\n\n var index = _.indexOf(key_ary, k => k.toString() === key_str);\n\n // Update cache.\n if (index !== -1) {\n // Object is in cache, so update it.\n if (obj_cache[key_str].stale) {\n // Object is stale: remove key and object.\n key_ary.splice(index, 1);\n delete obj_cache[key_str];\n } else {\n // Move key to back because it is most recently used.\n this.move_key_to_end(key, index);\n }\n }\n\n return obj_cache[key_str];\n },\n\n /**\n * Put an element into the cache.\n */\n set_elt: function(key, value) {\n var obj_cache = this.attributes.obj_cache;\n var key_ary = this.attributes.key_ary;\n var key_str = key.toString();\n var num_elements = this.attributes.num_elements;\n\n // Update keys, objects.\n if (!obj_cache[key_str]) {\n // Add object to cache.\n\n if (key_ary.length >= num_elements) {\n // Cache full, so remove first element.\n var deleted_key = key_ary.shift();\n delete obj_cache[deleted_key.toString()];\n }\n\n // Add key.\n key_ary.push(key);\n }\n\n // Add object.\n obj_cache[key_str] = value;\n return value;\n },\n\n /**\n * Move key to end of cache. Keys are removed from the front, so moving a key to the end\n * delays the key's removal.\n */\n move_key_to_end: function(key, index) {\n this.attributes.key_ary.splice(index, 1);\n this.attributes.key_ary.push(key);\n },\n\n /**\n * Clear all elements from the cache.\n */\n clear: function() {\n this.attributes.obj_cache = {};\n this.attributes.key_ary = [];\n },\n\n /** Returns the number of elements in the cache. */\n size: function() {\n return this.attributes.key_ary.length;\n },\n\n /** Returns key most recently added to cache. */\n most_recently_added: function() {\n return this.size() === 0\n ? null\n : // Most recent key is at the end of key array.\n this.attributes.key_ary[this.attributes.key_ary.length - 1];\n }\n});\n\n/**\n * Data manager for genomic data. Data is connected to and queryable by genomic regions.\n */\nvar GenomeDataManager = Cache.extend({\n defaults: _.extend({}, Cache.prototype.defaults, {\n dataset: null,\n genome: null,\n init_data: null,\n min_region_size: 200,\n filters_manager: null,\n data_type: \"data\",\n data_mode_compatible: function(entry, mode) {\n return true;\n },\n can_subset: function(entry) {\n return false;\n }\n }),\n\n /**\n * Initialization.\n */\n initialize: function(options) {\n Cache.prototype.initialize.call(this);\n\n // Set initial entries in data manager.\n var initial_entries = this.get(\"init_data\");\n if (initial_entries) {\n this.add_data(initial_entries);\n }\n },\n\n /**\n * Add data entries to manager; each entry should be a dict with attributes region (key), data, and data_type.\n * If necessary, manager size is increased to hold all data.\n */\n add_data: function(entries) {\n // Increase size to accomodate all entries.\n if (this.get(\"num_elements\") < entries.length) {\n this.set(\"num_elements\", entries.length);\n }\n\n // Put data into manager.\n var self = this;\n _.each(entries, entry => {\n self.set_data(entry.region, entry);\n });\n },\n\n /**\n * Returns deferred that resolves to true when dataset is ready (or false if dataset\n * cannot be used).\n */\n data_is_ready: function() {\n var dataset = this.get(\"dataset\");\n var ready_deferred = $.Deferred();\n\n var // If requesting raw data, query dataset state; if requesting (converted) data,\n // need to query converted datasets state.\n query_type =\n this.get(\"data_type\") === \"raw_data\"\n ? \"state\"\n : this.get(\"data_type\") === \"data\" ? \"converted_datasets_state\" : \"error\";\n\n var ss_deferred = new util_mod.ServerStateDeferred({\n ajax_settings: {\n url: this.get(\"dataset\").url(),\n data: {\n hda_ldda: dataset.get(\"hda_ldda\"),\n data_type: query_type\n },\n dataType: \"json\"\n },\n interval: 5000,\n success_fn: function(response) {\n return response !== \"pending\";\n }\n });\n\n $.when(ss_deferred.go()).then(response => {\n ready_deferred.resolve(response === \"ok\" || response === \"data\");\n });\n return ready_deferred;\n },\n\n /**\n * Perform a feature search from server; returns Deferred object that resolves when data is available.\n */\n search_features: function(query) {\n var dataset = this.get(\"dataset\");\n\n var params = {\n query: query,\n hda_ldda: dataset.get(\"hda_ldda\"),\n data_type: \"features\"\n };\n\n return $.getJSON(dataset.url(), params);\n },\n\n /**\n * Load data from server and manages data entries. Adds a Deferred to manager\n * for region; when data becomes available, replaces Deferred with data.\n * Returns the Deferred that resolves when data is available.\n */\n load_data: function(region, mode, resolution, extra_params) {\n // Setup data request params.\n var dataset = this.get(\"dataset\");\n\n var params = {\n data_type: this.get(\"data_type\"),\n chrom: region.get(\"chrom\"),\n low: region.get(\"start\"),\n high: region.get(\"end\"),\n mode: mode,\n resolution: resolution,\n hda_ldda: dataset.get(\"hda_ldda\")\n };\n\n $.extend(params, extra_params);\n\n // Add track filters to params.\n var filters_manager = this.get(\"filters_manager\");\n if (filters_manager) {\n var filter_names = [];\n var filters = filters_manager.filters;\n for (var i = 0; i < filters.length; i++) {\n filter_names.push(filters[i].name);\n }\n params.filter_cols = JSON.stringify(filter_names);\n }\n\n // Do request.\n var manager = this;\n\n var entry = $.getJSON(dataset.url(), params, result => {\n // Add region to the result.\n result.region = region;\n manager.set_data(region, result);\n });\n\n this.set_data(region, entry);\n return entry;\n },\n\n /**\n * Get data from dataset.\n */\n get_data: function(region, mode, resolution, extra_params) {\n // Look for entry and return if it's a deferred or if data available is compatible with mode.\n var entry = this.get_elt(region);\n if (entry && (util_mod.is_deferred(entry) || this.get(\"data_mode_compatible\")(entry, mode))) {\n return entry;\n }\n\n //\n // Look in cache for data that can be used.\n // TODO: this logic could be improved if the visualization knew whether\n // the data was \"index\" or \"data.\"\n //\n var key_ary = this.get(\"key_ary\");\n\n var obj_cache = this.get(\"obj_cache\");\n var entry_region;\n var is_subregion;\n for (var i = 0; i < key_ary.length; i++) {\n entry_region = key_ary[i];\n\n if (entry_region.contains(region)) {\n is_subregion = true;\n\n // This entry has data in the requested range. Return if data\n // is compatible and can be subsetted.\n entry = obj_cache[entry_region.toString()];\n if (\n util_mod.is_deferred(entry) ||\n (this.get(\"data_mode_compatible\")(entry, mode) && this.get(\"can_subset\")(entry))\n ) {\n this.move_key_to_end(entry_region, i);\n\n // If there's data, subset it.\n if (!util_mod.is_deferred(entry)) {\n var subset_entry = this.subset_entry(entry, region);\n this.set_data(region, subset_entry);\n entry = subset_entry;\n }\n\n return entry;\n }\n }\n }\n\n // FIXME: There _may_ be instances where region is a subregion of another entry but cannot be\n // subsetted. For these cases, do not increase length because region will never be found (and\n // an infinite loop will occur.)\n // If needed, extend region to make it minimum size.\n if (!is_subregion && region.length() < this.attributes.min_region_size) {\n // IDEA: alternative heuristic is to find adjacent cache entry to region and use that to extend.\n // This would prevent bad extensions when zooming in/out while still preserving the behavior\n // below.\n\n // Use copy of region to avoid changing actual region.\n region = region.copy();\n\n // Use heuristic to extend region: extend relative to last data request.\n var last_request = this.most_recently_added();\n if (!last_request || region.get(\"start\") > last_request.get(\"start\")) {\n // This request is after the last request, so extend right.\n region.set(\"end\", region.get(\"start\") + this.attributes.min_region_size);\n } else {\n // This request is after the last request, so extend left.\n region.set(\"start\", region.get(\"end\") - this.attributes.min_region_size);\n }\n\n // Trim region to avoid invalid coordinates.\n region.set(\"genome\", this.attributes.genome);\n region.trim();\n }\n\n return this.load_data(region, mode, resolution, extra_params);\n },\n\n /**\n * Alias for set_elt for readbility.\n */\n set_data: function(region, entry) {\n this.set_elt(region, entry);\n },\n\n /** \"Deep\" data request; used as a parameter for DataManager.get_more_data() */\n DEEP_DATA_REQ: \"deep\",\n\n /** \"Broad\" data request; used as a parameter for DataManager.get_more_data() */\n BROAD_DATA_REQ: \"breadth\",\n\n /**\n * Gets more data for a region using either a depth-first or a breadth-first approach.\n */\n get_more_data: function(region, mode, resolution, extra_params, req_type) {\n var cur_data = this._mark_stale(region);\n if (!(cur_data && this.get(\"data_mode_compatible\")(cur_data, mode))) {\n console.log(\"ERROR: problem with getting more data: current data is not compatible\");\n return;\n }\n\n //\n // Set parameters based on request type.\n //\n var query_low = region.get(\"start\");\n if (req_type === this.DEEP_DATA_REQ) {\n // Use same interval but set start_val to skip data that's already in cur_data.\n $.extend(extra_params, {\n start_val: cur_data.data.length + 1\n });\n } else if (req_type === this.BROAD_DATA_REQ) {\n // To get past an area of extreme feature depth, set query low to be after either\n // (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.\n query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;\n }\n var query_region = region.copy().set(\"start\", query_low);\n\n //\n // Get additional data, append to current data, and set new data. Use a custom deferred object\n // to signal when new data is available.\n //\n var data_manager = this;\n\n var new_data_request = this.load_data(query_region, mode, resolution, extra_params);\n\n var new_data_available = $.Deferred();\n // load_data sets cache to new_data_request, but use custom deferred object so that signal and data\n // is all data, not just new data.\n this.set_data(region, new_data_available);\n $.when(new_data_request).then(result => {\n // Update data and message.\n if (result.data) {\n result.data = cur_data.data.concat(result.data);\n if (result.max_low) {\n result.max_low = cur_data.max_low;\n }\n if (result.message) {\n // HACK: replace number in message with current data length. Works but is ugly.\n result.message = result.message.replace(/[0-9]+/, result.data.length);\n }\n }\n data_manager.set_data(region, result);\n new_data_available.resolve(result);\n });\n return new_data_available;\n },\n\n /**\n * Returns true if more detailed data can be obtained for entry.\n */\n can_get_more_detailed_data: function(region) {\n var cur_data = this.get_elt(region);\n\n // Can only get more detailed data for bigwig data that has less than 8000 data points.\n // Summary tree returns *way* too much data, and 8000 data points ~ 500KB.\n return cur_data.dataset_type === \"bigwig\" && cur_data.data.length < 8000;\n },\n\n /**\n * Returns more detailed data for an entry.\n */\n get_more_detailed_data: function(region, mode, resolution, detail_multiplier, extra_params) {\n // Mark current entry as stale.\n var cur_data = this._mark_stale(region);\n if (!cur_data) {\n console.log(\"ERROR getting more detailed data: no current data\");\n return;\n }\n\n if (!extra_params) {\n extra_params = {};\n }\n\n // Use additional parameters to get more detailed data.\n if (cur_data.dataset_type === \"bigwig\") {\n // FIXME: constant should go somewhere.\n extra_params.num_samples = 1000 * detail_multiplier;\n }\n\n return this.load_data(region, mode, resolution, extra_params);\n },\n\n /**\n * Marks cache data as stale.\n */\n _mark_stale: function(region) {\n var entry = this.get_elt(region);\n if (!entry) {\n console.log(\"ERROR: no data to mark as stale: \", this.get(\"dataset\"), region.toString());\n }\n entry.stale = true;\n return entry;\n },\n\n /**\n * Returns an array of data with each entry representing one chromosome/contig\n * of data or, if data is not available, returns a Deferred that resolves to the\n * data when it becomes available.\n */\n get_genome_wide_data: function(genome) {\n // -- Get all data. --\n\n var self = this;\n\n var all_data_available = true;\n\n var // Map chromosome info into genome data.\n gw_data = _.map(genome.get(\"chroms_info\").chrom_info, chrom_info => {\n var chrom_data = self.get_elt(\n new GenomeRegion({\n chrom: chrom_info.chrom,\n start: 0,\n end: chrom_info.len\n })\n );\n\n // Set flag if data is not available.\n if (!chrom_data) {\n all_data_available = false;\n }\n\n return chrom_data;\n });\n\n // -- If all data is available, return it. --\n if (all_data_available) {\n return gw_data;\n }\n\n // -- All data is not available, so load from server. --\n\n var deferred = $.Deferred();\n $.getJSON(this.get(\"dataset\").url(), { data_type: \"genome_data\" }, genome_wide_data => {\n self.add_data(genome_wide_data.data);\n deferred.resolve(genome_wide_data.data);\n });\n\n return deferred;\n },\n\n /**\n * Returns entry with only data in the subregion.\n */\n subset_entry: function(entry, subregion) {\n // Dictionary from entry type to function for subsetting data.\n var subset_fns = {\n bigwig: function(data, subregion) {\n return _.filter(\n data,\n data_point => data_point[0] >= subregion.get(\"start\") && data_point[0] <= subregion.get(\"end\")\n );\n },\n refseq: function(data, subregion) {\n var seq_start = subregion.get(\"start\") - entry.region.get(\"start\");\n return entry.data.slice(seq_start, seq_start + subregion.length());\n }\n };\n\n // Subset entry if there is a function for subsetting and regions are not the same.\n var subregion_data = entry.data;\n if (!entry.region.same(subregion) && entry.dataset_type in subset_fns) {\n subregion_data = subset_fns[entry.dataset_type](entry.data, subregion);\n }\n\n // Return entry with subregion's data.\n return {\n region: subregion,\n data: subregion_data,\n dataset_type: entry.dataset_type\n };\n }\n});\n\nvar GenomeReferenceDataManager = GenomeDataManager.extend({\n initialize: function(options) {\n // Use generic object in place of dataset and set urlRoot to fetch data.\n var dataset_placeholder = new Backbone.Model();\n dataset_placeholder.urlRoot = options.data_url;\n this.set(\"dataset\", dataset_placeholder);\n },\n\n load_data: function(region, mode, resolution, extra_params) {\n // Fetch data if region is not too large.\n return region.length() <= 100000\n ? GenomeDataManager.prototype.load_data.call(this, region, mode, resolution, extra_params)\n : { data: null, region: region };\n }\n});\n\n/**\n * A genome build.\n */\nvar Genome = Backbone.Model.extend({\n defaults: {\n name: null,\n key: null,\n chroms_info: null\n },\n\n initialize: function(options) {\n this.id = options.dbkey;\n },\n\n /**\n * Shorthand for getting to chromosome information.\n */\n get_chroms_info: function() {\n return this.attributes.chroms_info.chrom_info;\n },\n\n /**\n * Returns a GenomeRegion object denoting a complete chromosome.\n */\n get_chrom_region: function(chr_name) {\n // FIXME: use findWhere in underscore 1.4\n var chrom_info = _.find(this.get_chroms_info(), chrom_info => chrom_info.chrom === chr_name);\n return new GenomeRegion({\n chrom: chrom_info.chrom,\n end: chrom_info.len\n });\n },\n\n /** Returns the length of a chromosome. */\n get_chrom_len: function(chr_name) {\n // FIXME: use findWhere in underscore 1.4\n return _.find(this.get_chroms_info(), chrom_info => chrom_info.chrom === chr_name).len;\n }\n});\n\n/**\n * A genomic region.\n */\nvar GenomeRegion = Backbone.Model.extend(\n {\n defaults: {\n chrom: null,\n start: 0,\n end: 0,\n str_val: null,\n genome: null\n },\n\n /**\n * Returns true if this region is the same as a given region.\n * It does not test the genome right now.\n */\n same: function(region) {\n return (\n this.attributes.chrom === region.get(\"chrom\") &&\n this.attributes.start === region.get(\"start\") &&\n this.attributes.end === region.get(\"end\")\n );\n },\n\n /**\n * If from_str specified, use it to initialize attributes.\n */\n initialize: function(options) {\n if (options.from_str) {\n var pieces = options.from_str.split(\":\");\n var chrom = pieces[0];\n var start_end = pieces[1].split(\"-\");\n this.set({\n chrom: chrom,\n start: parseInt(start_end[0], 10),\n end: parseInt(start_end[1], 10)\n });\n }\n\n // Keep a copy of region's string value for fast lookup.\n this.attributes.str_val = `${this.get(\"chrom\")}:${this.get(\"start\")}-${this.get(\"end\")}`;\n\n // Set str_val on attribute change.\n this.on(\n \"change\",\n function() {\n this.attributes.str_val = `${this.get(\"chrom\")}:${this.get(\"start\")}-${this.get(\"end\")}`;\n },\n this\n );\n },\n\n copy: function() {\n return new GenomeRegion({\n chrom: this.get(\"chrom\"),\n start: this.get(\"start\"),\n end: this.get(\"end\")\n });\n },\n\n length: function() {\n return this.get(\"end\") - this.get(\"start\");\n },\n\n /** Returns region in canonical form chrom:start-end */\n toString: function() {\n return this.attributes.str_val;\n },\n\n toJSON: function() {\n return {\n chrom: this.get(\"chrom\"),\n start: this.get(\"start\"),\n end: this.get(\"end\")\n };\n },\n\n /**\n * Compute the type of overlap between this region and another region. The overlap is computed relative to the given/second region;\n * hence, OVERLAP_START indicates that the first region overlaps the start (but not the end) of the second region.\n */\n compute_overlap: function(a_region) {\n var first_chrom = this.get(\"chrom\");\n var second_chrom = a_region.get(\"chrom\");\n var first_start = this.get(\"start\");\n var second_start = a_region.get(\"start\");\n var first_end = this.get(\"end\");\n var second_end = a_region.get(\"end\");\n var overlap;\n\n // Compare chroms.\n if (first_chrom && second_chrom && first_chrom !== second_chrom) {\n return GenomeRegion.overlap_results.DIF_CHROMS;\n }\n\n // Compare regions.\n if (first_start < second_start) {\n if (first_end < second_start) {\n overlap = GenomeRegion.overlap_results.BEFORE;\n } else if (first_end < second_end) {\n overlap = GenomeRegion.overlap_results.OVERLAP_START;\n } else {\n // first_end >= second_end\n overlap = GenomeRegion.overlap_results.CONTAINS;\n }\n } else if (first_start > second_start) {\n if (first_start > second_end) {\n overlap = GenomeRegion.overlap_results.AFTER;\n } else if (first_end <= second_end) {\n overlap = GenomeRegion.overlap_results.CONTAINED_BY;\n } else {\n overlap = GenomeRegion.overlap_results.OVERLAP_END;\n }\n } else {\n // first_start === second_start\n overlap =\n first_end >= second_end\n ? GenomeRegion.overlap_results.CONTAINS\n : GenomeRegion.overlap_results.CONTAINED_BY;\n }\n\n return overlap;\n },\n\n /**\n * Trim a region to match genome's constraints.\n */\n trim: function(genome) {\n // Assume that all chromosome/contigs start at 0.\n if (this.attributes.start < 0) {\n this.attributes.start = 0;\n }\n\n // Only try to trim the end if genome is set.\n if (this.attributes.genome) {\n var chrom_len = this.attributes.genome.get_chrom_len(this.attributes.chrom);\n if (this.attributes.end > chrom_len) {\n this.attributes.end = chrom_len - 1;\n }\n }\n\n return this;\n },\n\n /**\n * Returns true if this region contains a given region.\n */\n contains: function(a_region) {\n return this.compute_overlap(a_region) === GenomeRegion.overlap_results.CONTAINS;\n },\n\n /**\n * Returns true if regions overlap.\n */\n overlaps: function(a_region) {\n return (\n _.intersection(\n [this.compute_overlap(a_region)],\n [\n GenomeRegion.overlap_results.DIF_CHROMS,\n GenomeRegion.overlap_results.BEFORE,\n GenomeRegion.overlap_results.AFTER\n ]\n ).length === 0\n );\n }\n },\n {\n overlap_results: {\n DIF_CHROMS: 1000,\n BEFORE: 1001,\n CONTAINS: 1002,\n OVERLAP_START: 1003,\n OVERLAP_END: 1004,\n CONTAINED_BY: 1005,\n AFTER: 1006\n }\n }\n);\n\nvar GenomeRegionCollection = Backbone.Collection.extend({\n model: GenomeRegion\n});\n\n/**\n * A genome browser bookmark.\n */\nvar BrowserBookmark = Backbone.Model.extend({\n defaults: {\n region: null,\n note: \"\"\n },\n\n initialize: function(options) {\n this.set(\"region\", new GenomeRegion(options.region));\n }\n});\n\n/**\n * Bookmarks collection.\n */\nvar BrowserBookmarkCollection = Backbone.Collection.extend({\n model: BrowserBookmark\n});\n\n/**\n * A track of data in a genome visualization.\n */\n// TODO: rename to Track and merge with Trackster's Track object.\nvar BackboneTrack = Backbone.Model.extend(CustomToJSON).extend(\n {\n defaults: {\n mode: \"Auto\"\n },\n\n initialize: function(options) {\n this.set(\"dataset\", new data_mod.Dataset(options.dataset));\n\n // -- Set up config settings. --\n var models = [\n {\n key: \"name\",\n default_value: this.get(\"dataset\").get(\"name\")\n },\n { key: \"color\" },\n {\n key: \"min_value\",\n label: \"Min Value\",\n type: \"float\",\n default_value: 0\n },\n {\n key: \"max_value\",\n label: \"Max Value\",\n type: \"float\",\n default_value: 1\n }\n ];\n\n this.set(\"config\", config_mod.ConfigSettingCollection.from_models_and_saved_values(models, options.prefs));\n\n // -- Set up data manager. --\n var preloaded_data = this.get(\"preloaded_data\");\n if (preloaded_data) {\n preloaded_data = preloaded_data.data;\n } else {\n preloaded_data = [];\n }\n this.set(\n \"data_manager\",\n new GenomeDataManager({\n dataset: this.get(\"dataset\"),\n init_data: preloaded_data\n })\n );\n }\n },\n {\n // This definition matches that produced by to_dict() methods in tracks.js\n to_json_keys: [\"track_type\", \"dataset\", \"prefs\", \"mode\", \"filters\", \"tool_state\"],\n to_json_mappers: {\n prefs: function(p, self) {\n if (_.size(p) === 0) {\n p = {\n name: self\n .get(\"config\")\n .get(\"name\")\n .get(\"value\"),\n color: self\n .get(\"config\")\n .get(\"color\")\n .get(\"value\")\n };\n }\n return p;\n },\n dataset: function(d) {\n return {\n id: d.id,\n hda_ldda: d.get(\"hda_ldda\")\n };\n }\n }\n }\n);\n\nvar BackboneTrackCollection = Backbone.Collection.extend({\n model: BackboneTrack\n});\n\n/**\n * A visualization.\n */\nvar Visualization = Backbone.Model.extend({\n defaults: {\n title: \"\",\n type: \"\"\n },\n\n urlRoot: `${Galaxy.root}api/visualizations`,\n\n /**\n * POSTs visualization's JSON to its URL using the parameter 'vis_json'\n * Note: This is necessary because (a) Galaxy requires keyword args and\n * (b) Galaxy does not handle PUT now.\n */\n save: function() {\n return $.ajax({\n url: this.url(),\n type: \"POST\",\n dataType: \"json\",\n data: {\n vis_json: JSON.stringify(this)\n }\n });\n }\n});\n\n/**\n * A visualization of genome data.\n */\nvar GenomeVisualization = Visualization.extend(CustomToJSON).extend(\n {\n defaults: _.extend({}, Visualization.prototype.defaults, {\n dbkey: \"\",\n drawables: null,\n bookmarks: null,\n viewport: null\n }),\n\n initialize: function(options) {\n // Replace drawables with tracks.\n this.set(\"drawables\", new BackboneTrackCollection(options.tracks));\n\n var models = [];\n this.set(\"config\", config_mod.ConfigSettingCollection.from_models_and_saved_values(models, options.prefs));\n\n // Clear track and data definitions to avoid storing large objects.\n this.unset(\"tracks\");\n this.get(\"drawables\").each(d => {\n d.unset(\"preloaded_data\");\n });\n },\n\n /**\n * Add a track or array of tracks to the visualization.\n */\n add_tracks: function(tracks) {\n this.get(\"drawables\").add(tracks);\n }\n },\n {\n // This definition matches that produced by to_dict() methods in tracks.js\n to_json_keys: [\"view\", \"viewport\", \"bookmarks\"],\n\n to_json_mappers: {\n view: function(dummy, self) {\n return {\n obj_type: \"View\",\n prefs: {\n name: self.get(\"title\"),\n content_visible: true\n },\n drawables: self.get(\"drawables\")\n };\n }\n }\n }\n);\n\n/**\n * -- Routers --\n */\n\n/**\n * Router for track browser.\n */\nvar TrackBrowserRouter = Backbone.Router.extend({\n initialize: function(options) {\n this.view = options.view;\n\n // Can't put regular expression in routes dictionary.\n // NOTE: parentheses are used to denote parameters returned to callback.\n this.route(/([\\w]+)$/, \"change_location\");\n this.route(/([\\w\\+]+\\:[\\d,]+-[\\d,]+)$/, \"change_location\");\n\n // Handle navigate events from view.\n var self = this;\n self.view.on(\"navigate\", new_loc => {\n self.navigate(new_loc);\n });\n },\n\n change_location: function(new_loc) {\n this.view.go_to(new_loc);\n }\n});\n\nexport default {\n BackboneTrack: BackboneTrack,\n BrowserBookmark: BrowserBookmark,\n BrowserBookmarkCollection: BrowserBookmarkCollection,\n Cache: Cache,\n CanvasManager: CanvasManager,\n Genome: Genome,\n GenomeDataManager: GenomeDataManager,\n GenomeRegion: GenomeRegion,\n GenomeRegionCollection: GenomeRegionCollection,\n GenomeVisualization: GenomeVisualization,\n GenomeReferenceDataManager: GenomeReferenceDataManager,\n TrackBrowserRouter: TrackBrowserRouter,\n Visualization: Visualization,\n select_datasets: select_datasets\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/viz/visualization.js"],"sourceRoot":""}
\ No newline at end of file
diff --git a/static/scripts/bundled/analysis.bundled.js b/static/scripts/bundled/analysis.bundled.js
index 98fb4f4c30e4..341f117efac8 100644
--- a/static/scripts/bundled/analysis.bundled.js
+++ b/static/scripts/bundled/analysis.bundled.js
@@ -1,4 +1,4 @@
-webpackJsonp([0],[,,,,,,,,,,,,function(e,t,i){"use strict";(function(e,n,s){Object.defineProperty(t,"__esModule",{value:!0});var a=i(3),o=function(e){return e&&e.__esModule?e:{default:e}}(a),l=e.View.extend({optionsDefault:{with_close:!0,title:null,placement:"top",container:"body",body:null},initialize:function(e){this.setElement(this._template()),this.uid=o.default.uid(),this.options=n.defaults(e||{},this.optionsDefault),this.options.container.parent().append(this.el),this.$title=this.$(".popover-title-label"),this.$close=this.$(".popover-close"),this.$body=this.$(".popover-content"),this.options.body&&this.append(this.options.body);var t=this;s("body").on("mousedown."+this.uid,function(e){t.visible&&!s(t.options.container).is(e.target)&&!s(t.el).is(e.target)&&0===s(t.el).has(e.target).length&&t.hide()})},render:function(){this.$title.html(this.options.title),this.$el.removeClass().addClass("ui-popover popover fade in").addClass(this.options.placement),this.$el.css(this._get_placement(this.options.placement));var e=this;this.options.with_close?this.$close.on("click",function(){e.hide()}).show():this.$close.off().hide()},title:function(e){void 0!==e&&(this.options.title=e,this.$title.html(e))},show:function(){this.render(),this.$el.show(),this.visible=!0},hide:function(){this.$el.hide(),this.visible=!1},append:function(e){this.$body.append(e)},empty:function(){this.$body.empty()},remove:function(){s("body").off("mousedown."+this.uid),this.$el.remove()},_get_placement:function(e){var t,i,n=this._get_width(this.$el),s=this.$el.height(),a=this.options.container,o=this._get_width(a),l=this._get_height(a),r=a.position();if(t=i=0,-1!=["top","bottom"].indexOf(e))switch(i=r.left-n+(o+n)/2,e){case"top":t=r.top-s-5;break;case"bottom":t=r.top+l+5}else switch(t=r.top-s+(l+s)/2,e){case"right":i=r.left+o}return{top:t,left:i}},_get_width:function(e){return e.width()+parseInt(e.css("padding-left"))+parseInt(e.css("margin-left"))+parseInt(e.css("padding-right"))+parseInt(e.css("margin-right"))},_get_height:function(e){return e.height()+parseInt(e.css("padding-top"))+parseInt(e.css("padding-bottom"))},_template:function(e){return' '),t.join(" ")},tagsToCSV:function(){var e=this,t=this.model.get("tags");return!n.isArray(t)||n.isEmpty(t)?"":t.map(function(t){return n.escape(e._nameToHash(t))}).sort().join(",")},$input:function(){return this.$el.find("input.tags-input")},_getTagsUsed:function(){var e=this;return n.map(Galaxy.user.get("tags_used"),e._nameToHash)},_setUpBehaviors:function(){var e=this;this.$input().on("change",function(t){t.val=n.map(t.val,e._hashToName),e.model.save({tags:t.val}),t.added&&e._addNewTagToTagsUsed(""+t.added.text)})},_addNewTagToTagsUsed:function(e){var t=Galaxy.user.get("tags_used");n.contains(t,e)||(t.push(e),t.sort(),Galaxy.user.set("tags_used",t))},remove:function(){this.$input.off(),this.stopListening(this.model),e.View.prototype.remove.call(this)},toString:function(){return["TagsEditor(",""+this.model,")"].join("")}});t.default={TagsEditor:d}}).call(t,i(2),i(1))},,,,,,,,,,,,function(e,t,i){"use strict";(function(e){function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var s=i(0),a=n(s),o=i(10),l=n(o),r=i(6),d=n(r),c=a.default,u=e.Router.extend({initialize:function(e,t){this.page=e,this.options=t},push:function(e,t){t=t||{},t.__identifer=Math.random().toString(36).substr(2),c.isEmptyObject(t)||(e+=-1==e.indexOf("?")?"?":"&",e+=c.param(t,!0)),Galaxy.params=t,this.navigate(e,{trigger:!0})},execute:function(e,t,i){Galaxy.debug("router execute:",e,t,i);var n=l.default.parse(t.pop());t.push(n),e&&(this.authenticate(t,i)?e.apply(this,t):this.access_denied())},authenticate:function(e,t){return!0},access_denied:function(){this.page.display(new d.default.Message({status:"danger",message:"You must be logged in with proper credentials to make this request.",persistent:!0}))}});t.default=u}).call(t,i(2))},function(e,t,i){"use strict";(function(e,n,s){Object.defineProperty(t,"__esModule",{value:!0});var a=i(3);!function(e){e&&e.__esModule}(a);t.default=e.View.extend({options:{class_check:"fa-check-square-o",class_uncheck:"fa-square-o",parameters:[{id:"space_to_tab",title:"Convert spaces to tabs"},{id:"to_posix_lines",title:"Use POSIX standard"}]},initialize:function(e){this.model=e.model,this.setElement(n("").addClass("upload-settings")),this.$el.append(n("").addClass("upload-settings-cover")),this.$el.append(n("
").addClass("upload-settings-table ui-table-striped").append("
").append(n(" ").append(i)).append(n(" ").append(t.title)))}),this.$cover[this.model.get("enabled")&&"hide"||"show"]()}})}).call(t,i(2),i(0),i(1))},function(e,t,i){"use strict";(function(e,n,s){Object.defineProperty(t,"__esModule",{value:!0});var a=i(3),o=function(e){return e&&e.__esModule?e:{default:e}}(a);t.default=e.View.extend({initialize:function(t){this.model=new e.Model({cls:"upload-ftp",class_add:"upload-icon-button fa fa-square-o",class_remove:"upload-icon-button fa fa-check-square-o",class_partial:"upload-icon-button fa fa-minus-square-o",help_enabled:!0,help_text:"This Galaxy server allows you to upload files via FTP. To upload some files, log in to the FTP server at "+t.ftp_upload_site+" using your Galaxy credentials.",collection:null,onchange:function(){},onadd:function(){},onremove:function(){}}).set(t),this.collection=this.model.get("collection"),this.setElement(this._template()),this.$content=this.$(".upload-ftp-content"),this.$wait=this.$(".upload-ftp-wait"),this.$help=this.$(".upload-ftp-help"),this.$number=this.$(".upload-ftp-number"),this.$disk=this.$(".upload-ftp-disk"),this.$body=this.$(".upload-ftp-body"),this.$warning=this.$(".upload-ftp-warning"),this.$select=this.$(".upload-ftp-select-all"),this.render()},render:function(){var e=this;this.$wait.show(),this.$content.hide(),this.$warning.hide(),this.$help.hide(),n.ajax({url:Galaxy.root+"api/remote_files",method:"GET",success:function(t){e.model.set("ftp_files",t),e._index(),e._renderTable()},error:function(){e._renderTable()}})},_renderTable:function(){var e=this,t=this.model.get("ftp_files");if(this.rows=[],t&&t.length>0){this.$body.empty();var i=0;s.each(t,function(t){e.rows.push(e._renderRow(t)),i+=t.size}),this.$number.html(t.length+" files"),this.$disk.html(o.default.bytesToString(i,!0)),this.collection&&(this.$("._has_collection").show(),this.$select.addClass(this.model.get("class_add")).off().on("click",function(){e._all()}),this._refresh()),this.$content.show()}else this.$warning.show();this.model.get("help_enabled")&&this.$help.show(),this.$wait.hide()},_renderRow:function(e){var t=this,i=this.model.attributes,s=n(this._templateRow(e)),a=s.find(".icon");if(this.$body.append(s),this.collection){var o=this.ftp_index[e.path];a.addClass(void 0===o?i.class_add:i.class_remove),s.on("click",function(){t._switch(a,e),t._refresh()})}else s.on("click",function(){i.onchange(e)});return a},_index:function(){var e=this;this.ftp_index={},this.collection&&this.collection.each(function(t){"ftp"==t.get("file_mode")&&(e.ftp_index[t.get("file_path")]=t.id)})},_all:function(){var e=this.model.attributes,t=this.model.get("ftp_files"),i=this.$select.hasClass(e.class_add);for(var n in t){var s=t[n],a=this.ftp_index[s.path];(void 0===a&&i||void 0!==a&&!i)&&this._switch(this.rows[n],s)}this._refresh()},_switch:function(e,t){e.removeClass();var i=this.model.attributes,n=this.ftp_index[t.path];if(void 0===n){var s=i.onadd(t);e.addClass(i.class_remove),this.ftp_index[t.path]=s}else i.onremove(n),e.addClass(i.class_add),this.ftp_index[t.path]=void 0},_refresh:function(){var e=s.reduce(this.ftp_index,function(e,t){return void 0!==t&&e++,e},0);this.$select.removeClass(),0==e?this.$select.addClass(this.model.get("class_add")):this.$select.addClass(e==this.rows.length?this.model.get("class_remove"):this.model.get("class_partial"))},_templateRow:function(e){return' "},_template:function(){return''+s.escape(e.path)+' '+o.default.bytesToString(e.size)+' '+e.ctime+" Name Size Created "+JSON.stringify(n.responseJSON)+"
":i+=": "+s}t._showAlert(i,"alert-danger")},events:{"click .more-help":"_clickMoreHelp","click .less-help":"_clickLessHelp","click .main-help":"_toggleHelp","click .header .alert button":"_hideAlert","click .reset":"reset","click .clear-selected":"clearSelectedElements","click .collection-elements":"clearSelectedElements","dragover .collection-elements":"_dragoverElements","drop .collection-elements":"_dropElements","collection-element.dragstart .collection-elements":"_elementDragstart","collection-element.dragend .collection-elements":"_elementDragend","change .collection-name":"_changeName","keydown .collection-name":"_nameCheckForEnter","change .hide-originals":"_changeHideOriginals","click .cancel-create":"_cancelCreate","click .create-collection":"_clickCreate"},reset:function(){this._instanceSetUp(),this._elementsSetUp(),this.render()},clearSelectedElements:function(e){this.$(".collection-elements .collection-element").removeClass("selected"),this.$(".collection-elements-controls > .clear-selected").hide()},_dragoverElements:function(e){e.preventDefault();var t=this.$list();this._checkForAutoscroll(t,e.originalEvent.clientY);var i=this._getNearestElement(e.originalEvent.clientY);this.$(".element-drop-placeholder").remove();var n=s('');i.length?i.before(n):t.append(n)},_checkForAutoscroll:function(e,t){var i=e.offset(),n=e.scrollTop(),s=t-i.top,a=i.top+e.outerHeight()-t;s>=0&&s","
","<% _.each( problems, function( problem ){ %>","
"].join("")),noElementsLeft:n.template(['<% _.each( problems, function( problem ){ %>","
","<% } else if( _.size( elements ) < 1 ){ %>",(0,y.default)("No datasets were selected"),".","<% } %>","
",(0,y.default)("At least one element is needed for the collection"),". ",(0,y.default)("You may need to "),'',(0,y.default)("cancel")," ",(0,y.default)("and reselect new elements"),".","","','
","<%= dataset.peek %>
',"<% } %>","<% } %>","").attr({id:"content_table",cellpadding:0});this.$el.append(e);var t=this.model.get_metadata("column_names"),i=a("").appendTo(e),n=a("
").appendTo(i);if(t)n.append(" "+t.join(" ")+" ");else for(var s=1;s<=this.model.get_metadata("columns");s++)n.append(""+s+" ");var o=this,r=this.model.get("first_data_chunk");r?this._renderChunk(r):a.when(o.model.get_next_chunk()).then(function(e){o._renderChunk(e)}),this.scroll_elt.scroll(function(){o.attempt_to_fetch()})},scrolled_to_bottom:function(){return!1},_renderCell:function(e,t,i){var n=a("").text(e),s=this.model.get_metadata("column_types");return void 0!==i?n.attr("colspan",i).addClass("stringalign"):s&&t "))})),this.row_count++,i},_renderChunk:function(e){var t=this.$el.find("table");n.each(e.ck_data.split("\n"),function(e,i){""!==e&&t.append(this._renderRow(e))},this)}}),_=p.extend({initialize:function(e){p.prototype.initialize.call(this,e);var t=n.find(this.$el.parents(),function(e){return"auto"===a(e).css("overflow")});t||(t=window),this.scroll_elt=a(t)},scrolled_to_bottom:function(){return this.$el.height()-this.scroll_elt.scrollTop()-this.scroll_elt.height()<=0}}),m=p.extend({initialize:function(e){p.prototype.initialize.call(this,e),this.scroll_elt=this.$el.css({position:"relative",overflow:"scroll",height:e.height||"500px"})},scrolled_to_bottom:function(){return this.$el.scrollTop()+this.$el.innerHeight()>=this.el.scrollHeight}}),g=e.View.extend({col:{chrom:null,start:null,end:null},url_viz:null,dataset_id:null,genome_build:null,file_ext:null,initialize:function(e){var t=parent.Galaxy;if(t&&t.modal&&(this.modal=t.modal),t&&t.frame&&(this.frame=t.frame),this.modal&&this.frame){var i=e.model,n=i.get("metadata");if(i.get("file_ext")){if(this.file_ext=i.get("file_ext"),"bed"==this.file_ext){if(!(n.get("chromCol")&&n.get("startCol")&&n.get("endCol")))return void console.log("TabularButtonTrackster : Bed-file metadata incomplete.");this.col.chrom=n.get("chromCol")-1,this.col.start=n.get("startCol")-1,this.col.end=n.get("endCol")-1}if("vcf"==this.file_ext){var a=function(e,t){for(var i=0;i ',this._templateOptions(t),"
"].join("")},_templateOptions:function(e){return e.length?n.map(e,function(e){return e.divider?'':e.header?['").addClass("tab-navigation nav nav-tabs")).append(n("").addClass("tab-content"))},_template_tab:function(e){var t=n("").addClass("tab-element").attr("id","tab-"+e.id).append(n("").attr("id","tab-title-link-"+e.id)),i=t.find("a");return e.icon&&i.append(n("").addClass("tab-icon fa").addClass(e.icon)),i.append(n("").attr("id","tab-title-text-"+e.id).addClass("tab-title-text").append(e.title)),t}}));t.default={View:s}}).call(t,i(2),i(0))},function(e,t,i){"use strict";(function(e,n,a){Object.defineProperty(t,"__esModule",{value:!0});var s=i(3),o=function(e){return e&&e.__esModule?e:{default:e}}(s),r=e.View.extend({optionsDefault:{with_close:!0,title:null,placement:"top",container:"body",body:null},initialize:function(e){this.setElement(this._template()),this.uid=o.default.uid(),this.options=n.defaults(e||{},this.optionsDefault),this.options.container.parent().append(this.el),this.$title=this.$(".popover-title-label"),this.$close=this.$(".popover-close"),this.$body=this.$(".popover-content"),this.options.body&&this.append(this.options.body);var t=this;a("body").on("mousedown."+this.uid,function(e){t.visible&&!a(t.options.container).is(e.target)&&!a(t.el).is(e.target)&&0===a(t.el).has(e.target).length&&t.hide()})},render:function(){this.$title.html(this.options.title),this.$el.removeClass().addClass("ui-popover popover fade in").addClass(this.options.placement),this.$el.css(this._get_placement(this.options.placement));var e=this;this.options.with_close?this.$close.on("click",function(){e.hide()}).show():this.$close.off().hide()},title:function(e){void 0!==e&&(this.options.title=e,this.$title.html(e))},show:function(){this.render(),this.$el.show(),this.visible=!0},hide:function(){this.$el.hide(),this.visible=!1},append:function(e){this.$body.append(e)},empty:function(){this.$body.empty()},remove:function(){a("body").off("mousedown."+this.uid),this.$el.remove()},_get_placement:function(e){var t,i,n=this._get_width(this.$el),a=this.$el.height(),s=this.options.container,o=this._get_width(s),r=this._get_height(s),l=s.position();if(t=i=0,-1!=["top","bottom"].indexOf(e))switch(i=l.left-n+(o+n)/2,e){case"top":t=l.top-a-5;break;case"bottom":t=l.top+r+5}else switch(t=l.top-a+(r+a)/2,e){case"right":i=l.left+o}return{top:t,left:i}},_get_width:function(e){return e.width()+parseInt(e.css("padding-left"))+parseInt(e.css("margin-left"))+parseInt(e.css("padding-right"))+parseInt(e.css("margin-right"))},_get_height:function(e){return e.height()+parseInt(e.css("padding-top"))+parseInt(e.css("padding-bottom"))},_template:function(e){return'
'+this.grid_table(e),e.info_text&&(t+=''+this.grid_header(e)+' "+e.title+"
"),e.global_actions){t+='';var i=e.global_actions.length>=3;i&&(t+='
"}return e.insert&&(t+=e.insert),t+=this.grid_filters(e),t+="";e.show_item_checkboxes&&(t+=" "},body:function(t){var i="",n=0;0==t.items.length&&(i+='",e.items.length>0&&(t+=''),t+=" ");for(var i in e.columns){var n=e.columns[i];n.visible&&(t+='',n.href?t+=''+n.label+"":t+=n.label,t+=''+n.extra+" ")}return t+=" ',n=1);for(var a in t.items){var s=t.items[a],o=s.encode_id;i+="No Items ",t.show_item_checkboxes&&(i+=' ",n++}return i},footer:function(e){var t="";if(e.use_paging&&e.num_pages>1){var i=e.num_page_links,n=e.cur_page_num,a=e.num_pages,s=i/2,o=n-s,r=0;o<=0&&(o=1,r=s-(n-o));var l,d=s+r,c=n+d;c<=a?l=0:(c=a,l=d-(c+1-n)),0!=l&&(o-=l)<1&&(o=1),t+='');for(var r in t.columns){var l=t.columns[r];if(l.visible){var d="";l.nowrap&&(d='style="white-space:nowrap;"');var c=s.column_config[l.label],u=c.link,h=c.value,f=c.target;"string"===e.type(h)&&(h=h.replace(/\/\//g,"/"));var p="",_="";l.attach_popup&&(p="grid-"+a+"-popup",_="menubutton",""!=u&&(_+=" split"),_+=" popup"),i+=" ",u?(0!=t.operations.length&&(i+=' "}}i+="',e.show_item_checkboxes&&(t+=" '}if(e.show_item_checkboxes){t+='"),t+=' Page:',o>1&&(t+='1 ...');for(var u=o;u "}var p=!1;for(h in e.operations)if(e.operations[h].global_operation){p=!0;break}if(p){t+='For selected items: ';for(var h in e.operations){var f=e.operations[h];f.allow_multiple&&(t+=' ')}t+=" "}return e.legend&&(t+='';for(h in e.operations){var f=e.operations[h];f.global_operation&&(t+=''+f.label+"")}t+=" "),t},message:function(e){var t=e.status;return-1!=["success","ok"].indexOf(t)&&(t="done"),''+e.legend+" ';for(var s in e.columns){var o=e.columns[s];"standard"==o.filterable&&(u+=this.grid_column_filter(e,o))}u+="
",a&&(u+='Advanced Search'),u+=" ';for(var s in e.columns){var o=e.columns[s];"advanced"==o.filterable&&(u+=this.grid_column_filter(e,o))}return u+="Close Advanced Search ";if("advanced"==i.filterable&&(o+=' "},filter_element:function(e,t){return''+(t=s.default.sanitize(t))+''}}}).call(t,i(0),i(1))},function(e,t,i){"use strict";(function(e,n){function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var s=i(1),o=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t.default=e,t}(s),r=i(42),l=a(r),d=i(9),c=a(d),u=i(21),h=a(u),f=i(14),p=a(f),_=i(11),m=a(_),g=i(3),v=a(g);i(57),i(56),i(58),i(62),i(61),i(63),i(59),i(60),i(16);var w=null,y=null,b=function(){this.initialize&&this.initialize.apply(this,arguments)};b.extend=e.Model.extend;var x=b.extend({initialize:function(e){v.default.cssLoadFile("static/style/jquery.rating.css"),v.default.cssLoadFile("static/style/autocomplete_tagging.css"),v.default.cssLoadFile("static/style/jquery-ui/smoothness/jquery-ui.css"),v.default.cssLoadFile("static/style/library.css"),v.default.cssLoadFile("static/style/trackster.css"),this.baseURL=e},save_viz:function(){Galaxy.modal.show({title:"Saving...",body:"progress"});var e=[];n(".bookmark").each(function(){e.push({position:n(this).children(".position").text(),annotation:n(this).children(".annotation").text()})});var t=y.overview_drawable?y.overview_drawable.config.get_value("name"):null,i={view:y.to_dict(),viewport:{chrom:y.chrom,start:y.low,end:y.high,overview:t},bookmarks:e};return n.ajax({url:Galaxy.root+"visualization/save",type:"POST",dataType:"json",data:{id:y.vis_id,title:y.config.get_value("name"),dbkey:y.dbkey,type:"trackster",vis_json:JSON.stringify(i)}}).success(function(e){Galaxy.modal.hide(),y.vis_id=e.vis_id,y.has_changes=!1,window.history.pushState({},"",e.url+window.location.hash)}).error(function(){Galaxy.modal.show({title:"Could Not Save",body:"Could not save visualization. Please try again later.",buttons:{Cancel:function(){Galaxy.modal.hide()}}})})},createButtonMenu:function(){var e=this,t=h.default.create_icon_buttons_menu([{icon_class:"plus-button",title:"Add tracks",on_click:function(){c.default.select_datasets({dbkey:y.dbkey},function(e){o.each(e,function(e){y.add_drawable(l.default.object_from_template(e,y,y))})})}},{icon_class:"block--plus",title:"Add group",on_click:function(){y.add_drawable(new l.default.DrawableGroup(y,y,{name:"New Group"}))}},{icon_class:"bookmarks",title:"Bookmarks",on_click:function(){force_right_panel("0px"==n("div#right").css("right")?"hide":"show")}},{icon_class:"globe",title:"Circster",on_click:function(){window.location=e.baseURL+"visualization/circster?id="+y.vis_id}},{icon_class:"disk--arrow",title:"Save",on_click:function(){e.save_viz()}},{icon_class:"cross-circle",title:"Close",on_click:function(){e.handle_unsaved_changes(y)}}],{tooltip_config:{placement:"bottom"}});return this.buttonMenu=t,t},add_bookmark:function(e,t,i){var a=n("#right .unified-panel-body"),s=n("").addClass("bookmark").appendTo(a),o=n("").addClass("position").appendTo(s),r=(n("").text(e).appendTo(o).click(function(){return y.go_to(e),!1}),n("").text(t).appendTo(s));if(i){var l=n("").addClass("delete-icon-container").prependTo(s).click(function(){return s.slideUp("fast"),s.remove(),y.has_changes=!0,!1});n("").addClass("icon-button delete").appendTo(l);r.make_text_editable({num_rows:3,use_textarea:!0,help_text:"Edit bookmark note"}).addClass("annotation")}return y.has_changes=!0,s},create_visualization:function(e,t,i,a,s){var r=this,d=new l.default.TracksterView(o.extend(e,{header:!1}));return d.editor=!0,n.when(d.load_chroms_deferred).then(function(e){if(t){var n=t.chrom,o=t.start,c=t.end,u=t.overview;n&&void 0!==o&&c?d.change_chrom(n,o,c):d.change_chrom(e[0].chrom)}else d.change_chrom(e[0].chrom);if(i)for(var h=0;h'+a+": "),o+='',i.is_text){o+=''}else{o+='';var m=!1;for(var g in t.categorical_filters[s]){var v=t.categorical_filters[s][g],w="",y="";for(var b in v)w=b,y=v[b];m&&(o+=" | "),m=!0;var h=n[s];h&&v[s]&&h==y?o+=''+g+"":o+=''+g+""}o+=""}return o+=" You can add this dataset as:
",buttons:{Cancel:function(){window.location=Galaxy.root+"visualizations/list"},"View in saved visualization":function(){e.view_in_saved(n)},"View in new visualization":function(){e.view_new()}}})},view_in_saved:function(e){var t=new m.default({url_base:Galaxy.root+"visualization/list_tracks",dict_format:!0,embedded:!0});Galaxy.modal.show({title:"Add Data to Saved Visualization",body:t.$el,buttons:{Cancel:function(){window.location=Galaxy.root+"visualizations/list"},"Add to visualization":function(){n(parent.document).find("input[name=id]:checked").each(function(){e.id=n(this).val(),window.location=Galaxy.root+"visualization/trackster?"+n.param(e)})}}})},view_existing:function(){var e=galaxy_config.app.viz_config;y=w.create_visualization({container:n("#center .unified-panel-body"),name:e.title,vis_id:e.vis_id,dbkey:e.dbkey},e.viewport,e.tracks,e.bookmarks,!0),this.init_editor()},view_new:function(){var e=this;n.ajax({url:Galaxy.root+"api/genomes?chrom_info=True",data:{},error:function(){alert("Couldn't create new browser.")},success:function(t){Galaxy.modal.show({title:"New Visualization",body:e.template_view_new(t),buttons:{Cancel:function(){window.location=Galaxy.root+"visualizations/list"},Create:function(){e.create_browser(n("#new-title").val(),n("#new-dbkey").val()),Galaxy.modal.hide()}}});var i=t.map(function(e){return e[1]});galaxy_config.app.default_dbkey&&o.contains(i,galaxy_config.app.default_dbkey)&&n("#new-dbkey").val(galaxy_config.app.default_dbkey),n("#new-title").focus(),n("select[name='dbkey']").select2(),n("#overlay").css("overflow","auto")}})},template_view_new:function(e){for(var t=''},create_browser:function(e,t){n(document).trigger("convert_to_values"),y=w.create_visualization({container:n("#center .unified-panel-body"),name:e,dbkey:t},galaxy_config.app.gene_region),this.init_editor(),y.editor=!0},init_editor:function(){n("#center .unified-panel-title").text(y.config.get_value("name")+" ("+y.dbkey+")"),galaxy_config.app.add_dataset&&n.ajax({url:Galaxy.root+"api/datasets/"+galaxy_config.app.add_dataset,data:{hda_ldda:"hda",data_type:"track_config"},dataType:"json",success:function(e){y.add_drawable(l.default.object_from_template(e,y,y))}}),n("#add-bookmark-button").click(function(){var e=y.chrom+":"+y.low+"-"+y.high;return w.add_bookmark(e,"Bookmark description",!0)}),w.init_keyboard_nav(y),n(window).on("beforeunload",function(){if(y.has_changes)return"There are unsaved changes to your visualization that will be lost if you leave this page."})}});t.default={TracksterUI:x,GalaxyApp:k}}).call(t,i(2),i(0))},function(e,t,i){"use strict";(function(e,n){function a(e){return e&&e.__esModule?e:{default:e}}function s(e,t){t||(t=0);var i=Math.pow(10,t);return Math.round(e*i)/i}function o(t){var i=e.Deferred();return e.ajax({type:"HEAD",url:t,beforeSend:function(e){e.setRequestHeader("Range","bytes=0-10")},success:function(e,t,n){i.resolve(206===n.status)}}),i}Object.defineProperty(t,"__esModule",{value:!0});var r=i(1),l=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t.default=e,t}(r),d=i(9),c=a(d),u=i(43),h=a(u),f=i(10),p=a(f),_=i(44),m=a(_),g=i(45),v=a(g),w=i(46),y=a(w),b=i(8),x=a(b),k=i(27),C=a(k),S=i(13),$=a(S),T=i(49),M=a(T);i(16);var D=l.extend,P={},A=function(e,t){P[e.attr("id")]=t},E=function(t,i,n,a){n=".group",P[t.attr("id")]=a,t.bind("drag",{handle:"."+i,relative:!0},function(t,i){var a,s,o,r,l,d=(e(this),e(this).parent()),c=d.children(".track,.group"),u=P[e(this).attr("id")];if(s=e(this).parents(n),0!==s.length){o=s.position().top,r=o+s.outerHeight();var h=P[s.attr("id")];if(i.offsetY
Dataset:"+t.config.get_value("name")+'
Region(s): ',a=function(){Galaxy.modal.hide(),e(window).unbind("keypress.check_enter_esc")},s=function(){var i,a=e('select[name="regions"] option:selected').val(),s=new c.default.GenomeRegion({chrom:view.chrom,start:view.low,end:view.high}),o=l.map(e(".bookmark"),function(t){return new c.default.GenomeRegion({from_str:e(t).children(".position").text()})});i="cur"===a?[s]:"bookmarks"===a?o:[s].concat(o),Galaxy.modal.hide(),window.location.href=Galaxy.root+"visualization/sweepster?"+e.param({dataset_id:t.dataset.id,hda_ldda:t.dataset.get("hda_ldda"),regions:JSON.stringify(new n.Collection(i).toJSON())})};Galaxy.modal.show({title:"Visualize tool parameter space and output from different parameter settings?",body:i,buttons:{No:a,Yes:s}})}},L.prototype.action_icons_def[2]],can_draw:function(){return this.dataset&&L.prototype.can_draw.call(this)},build_container_div:function(){return e("").addClass("track").attr("id","track_"+this.id)},set_dataset:function(e){this.dataset=e,this.data_manager.set("dataset",e)},on_resize:function(){this.request_draw({clear_tile_cache:!0})},add_resize_handle:function(){var t=this,i=!1,n=!1,a=e(""+t.message+"
",buttons:{Close:function(){Galaxy.modal.hide()}}})})),a.append(e("").text(" ")),a.append(e("").text("Try again").click(function(){i.init(!0)})))}}),this.update_icons(),n}},predraw_init:function(){var t=this;return e.getJSON(t.dataset.url(),{data_type:"data",stats:!0,chrom:t.view.chrom,low:0,high:t.view.max_high,hda_ldda:t.dataset.get("hda_ldda")},function(e){var i=e.data;if(i&&void 0!==i.min&&void 0!==i.max){var n=i.min,a=i.max;n=Math.floor(Math.min(0,Math.max(n,i.mean-2*i.sd))),a=Math.ceil(Math.max(0,Math.min(a,i.mean+2*i.sd))),t.config.set_default_value("min_value",n),t.config.set_default_value("max_value",a),t.config.set_value("min_value",n),t.config.set_value("max_value",a)}})},get_drawables:function(){return this}});var J=function(t,i,n){q.call(this,t,i,n);var a=this;if(E(a.container_div,a.drag_handle_class,".group",a),this.filters_manager=new y.default.FiltersManager(this,"filters"in n?n.filters:null),this.data_manager.set("filters_manager",this.filters_manager),this.filters_available=!1,this.tool=n.tool?new G(l.extend(n.tool,{track:this,tool_state:n.tool_state})):null,this.tile_cache=new c.default.Cache(10),this.left_offset=0,this.header_div&&(this.set_filters_manager(this.filters_manager),this.tool)){var s=new H({model:this.tool});s.render(),this.dynamic_tool_div=s.$el,this.header_div.after(this.dynamic_tool_div)}this.tiles_div=e("").addClass("tiles").appendTo(this.content_div),this.config.get_value("content_visible")||this.tiles_div.hide(),this.overlay_div=e("").addClass("overlay").appendTo(this.content_div),n.mode&&this.change_mode(n.mode)};D(J.prototype,L.prototype,q.prototype,{action_icons_def:q.prototype.action_icons_def.concat([{name:"show_more_rows_icon",title:"To minimize track height, not all feature rows are displayed. Click to display more rows.",css_class:"exclamation",on_click_fn:function(t){e(".tooltip").remove(),t.slotters[t.view.resolution_px_b].max_rows*=2,t.request_draw({clear_tile_cache:!0})},hide:!0}]),copy:function(e){var t=this.to_dict();D(t,{data_manager:this.data_manager});var i=new this.constructor(this.view,e,t);return i.change_mode(this.mode),i.enabled=this.enabled,i},set_filters_manager:function(e){this.filters_manager=e,this.header_div.after(this.filters_manager.parent_div)},to_dict:function(){return{track_type:this.get_type(),dataset:{id:this.dataset.id,hda_ldda:this.dataset.get("hda_ldda")},prefs:this.config.to_key_value_dict(),mode:this.mode,filters:this.filters_manager.to_dict(),tool_state:this.tool?this.tool.state_dict():{}}},set_min_max:function(){var t=this;return e.getJSON(t.dataset.url(),{data_type:"data",stats:!0,chrom:t.view.chrom,low:0,high:t.view.max_high,hda_ldda:t.dataset.get("hda_ldda")},function(e){var i=e.data;if(isNaN(parseFloat(t.config.get_value("min_value")))||isNaN(parseFloat(t.config.get_value("max_value")))){var n=i.min,a=i.max;n=Math.floor(Math.min(0,Math.max(n,i.mean-2*i.sd))),a=Math.ceil(Math.max(0,Math.min(a,i.mean+2*i.sd))),t.config.set_value("min_value",n),t.config.set_value("max_value",a)}})},change_mode:function(e){var t=this;return t.mode=e,t.config.set_value("mode",e),"Auto"===e&&this.data_manager.clear(),t.request_draw({clear_tile_cache:!0}),this.action_icons.mode_icon.attr("title","Set display mode (now: "+t.mode+")"),t},update_icons:function(){var e=this;e.action_icons.filters_icon.toggle(e.filters_available),e.action_icons.tools_icon.toggle(null!==e.tool),e.action_icons.param_space_viz_icon.toggle(null!==e.tool)},_gen_tile_cache_key:function(e,t){return e+"_"+t},request_draw:function(e){e&&e.clear_tile_cache&&this.tile_cache.clear(),this.view.request_redraw(e,this)},before_draw:function(){this.max_height_px=0},_draw:function(t){if(this.can_draw()){var i=t&&t.clear_after,n=this.view.low,a=this.view.high,s=this.view.container.width(),o=this.view.resolution_px_b,r=1/o;this.is_overview&&(n=this.view.max_low,a=this.view.max_high,o=s/(view.max_high-view.max_low),r=1/o),this.before_draw(),this.tiles_div.children().addClass("remove");for(var d,u,h=Math.floor(400*r),f=Math.floor(n/h),p=[],_=[];f*h").text(d).make_text_editable({num_cols:12,on_finish:function(t){e(".tooltip").remove(),n.config.set_value(r,s(t,1)),i()},help_text:"Set "+o+" value"}).addClass("yaxislabel "+a).css("color",this.config.get_value("label_color")),this.container_div.prepend(l))},postdraw_actions:function(e,t,i,n){if(l.filter(e,function(e){return e instanceof U}).length>0){this.max_height_px=0;var a=this;l.each(e,function(e){e instanceof U||(e.html_elt.remove(),a.draw_helper(e.region,i,{force:!0,mode:"Coverage"}))}),a._add_yaxis_label("max")}else this.container_div.find(".yaxislabel").remove(),l.find(e,function(e){return e.has_icons})&&l.each(e,function(e){e.has_icons||e.html_elt.css("padding-top",20)})},get_mode:function(e){return this.mode},update_auto_mode:function(e){},_get_drawables:function(){return[this]},draw_helper:function(t,i,n){n||(n={});var a=n.force,s=n.mode||this.mode,o=1/i,r=this,d=this._get_drawables(),c=this._gen_tile_cache_key(i,t),u=a?void 0:r.tile_cache.get_elt(c);if(u)return function(e){return e&&"track"in e}(u)&&r.show_tile(u,i),u;if(!1===n.data_fetch)return null;var h=function(){var e=l.find(I,function(e){return e===s})?"Coverage":s,i=l.map(d,function(i){return i.data_manager.get_data(t,e,o,r.data_url_extra_params)});return view.reference_track&&i.push(view.reference_track.data_manager.get_data(t,s,o,view.reference_track.data_url_extra_params)),i},f=e.Deferred();return r.tile_cache.set_elt(c,f),e.when.apply(e,h()).then(function(){var a,s=h(),o=s;if(l.find(s,function(e){return p.default.is_deferred(e)}))return r.tile_cache.set_elt(c,void 0),void e.when(r.draw_helper(t,i,n)).then(function(e){f.resolve(e)});view.reference_track&&(a=view.reference_track.data_manager.subset_entry(s.pop(),t));var u=[],_=[];l.each(d,function(e,t){var n=e.mode,a=o[t];"Auto"===n&&(n=e.get_mode(a),e.update_auto_mode(n)),u.push(n),_.push(e.get_canvas_height(a,n,i,y))});var m,g=r.view.canvas_manager.new_canvas(),v=t.get("start"),w=t.get("end"),y=Math.ceil((w-v)*i)+r.left_offset,b=l.max(_);g.width=y,g.height=n.height||b;var x=g.getContext("2d");x.translate(r.left_offset,0),d.length>1&&(x.globalAlpha=.5,x.globalCompositeOperation="source-over"),l.each(d,function(e,n){m=e.draw_tile(o[n],x,u[n],t,i,a)}),void 0!==m&&(r.tile_cache.set_elt(c,m),r.show_tile(m,i)),f.resolve(m)}),f},get_canvas_height:function(e,t,i,n){return this.visible_height_px},_draw_line_track_tile:function(e,t,i,n,a){-1!==[void 0,null].indexOf(this.config.get_value("min_value"))&&this.config.set_value("min_value",0),-1!==[void 0,null,0].indexOf(this.config.get_value("max_value"))&&this.config.set_value("max_value",l.max(l.map(e.data,function(e){return e[1]}))||0);var s=t.canvas;return new v.default.LinePainter(e.data,n.get("start"),n.get("end"),this.config.to_key_value_dict(),i).draw(t,s.width,s.height,a),new U(this,n,a,s,e.data)},draw_tile:function(e,t,i,n,a,s){},show_tile:function(e,t){var i=e.html_elt;e.predisplay_actions();var n=Math.round((e.low-(this.is_overview?this.view.max_low:this.view.low))*t);this.left_offset&&(n-=this.left_offset),i.css("left",n),i.hasClass("remove")?i.removeClass("remove"):this.tiles_div.append(i),i.css("height","auto"),this.max_height_px=Math.max(this.max_height_px,i.height()-2),i.parent().children().css("height",this.max_height_px+"px");var a=this.max_height_px;0!==this.visible_height_px&&(a=Math.min(this.max_height_px,this.visible_height_px)),this.tiles_div.css("height",a+"px")},tool_region_and_parameters_str:function(e){var t=this;return" - region=["+(void 0!==e?e.toString():"all")+"], parameters=["+l.values(t.tool.get_inputs_dict()).join(", ")+"]"},data_and_mode_compatible:function(e,t){return"Auto"===t||("Coverage"===t?"bigwig"===e.dataset_type:"bigwig"!==e.dataset_type&&"no_detail"!==e.extra_info)},can_subset:function(e){return!e.message&&"no_detail"!==e.extra_info&&("bigwig"!==e.dataset_type||e.data[1][0]-e.data[0][0]==1)},init_for_tool_data:function(){this.data_manager.set("data_type","raw_data"),this.data_query_wait=1e3,this.dataset_check_type="state"}});var W=function(e,t){q.call(this,e,t,{resize:!1,header:!1}),this.container_div.addClass("label-track")};D(W.prototype,q.prototype,{init:function(){this.enabled=!0},predraw_init:function(){},_draw:function(t){for(var i=this.view,n=i.high-i.low,a=Math.floor(Math.pow(10,Math.floor(Math.log(n)/Math.log(10)))),s=Math.floor(i.low/a)*a,o=this.view.container.width(),r=e("").addClass("label-container");s
");this.tiles_div.prepend(e("").html(d).addClass("yaxislabel variant top sample").css({top:this.config.get_value("summary_height")}))}r=("Squish"===this.mode?5:10)+"px",e(this.tiles_div).find(".sample").css({"font-size":r,"line-height":r}),e(this.tiles_div).find(".yaxislabel").css("color",this.config.get_value("label_color"))}else this.container_div.find(".yaxislabel.variant").remove()}});var te=function(e,t,i){Z.call(this,e,t,i),this.painter=v.default.ReadPainter,this.update_icons()};D(te.prototype,L.prototype,J.prototype,Z.prototype,{build_config_params:function(){return l.union(L.prototype.config_params,[{key:"block_color",label:"Histogram color",type:"color"},{key:"detail_block_color",label:"Sense strand block color",type:"color",default_value:"#AAAAAA"},{key:"reverse_strand_color",label:"Antisense strand block color",type:"color",default_value:"#DDDDDD"},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_insertions",label:"Show insertions",type:"bool",default_value:!1},{key:"show_differences",label:"Show differences only",type:"bool",default_value:!0},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:!0},{key:"mode",type:"string",default_value:this.mode,hidden:!0},{key:"min_value",label:"Histogram minimum",type:"float",default_value:void 0,help:"clear value to set automatically"},{key:"max_value",label:"Histogram maximum",type:"float",default_value:void 0,help:"clear value to set automatically"},{key:"height",type:"int",default_value:0,hidden:!0}])},config_onchange:function(){this.set_name(this.config.get_value("name")),this.request_draw({clear_tile_cache:!0})}});var ie={CompositeTrack:Y,DrawableGroup:N,DiagonalHeatmapTrack:K,FeatureTrack:Z,LineTrack:Q,ReadTrack:te,VariantTrack:ee,VcfTrack:ee},ne=function(e,t,i){if("copy"in e)return e.copy(i);var n=e.obj_type;return n||(n=e.track_type),new ie[n](t,i,e)};t.default={TracksterView:j,DrawableGroup:N,LineTrack:Q,FeatureTrack:Z,DiagonalHeatmapTrack:K,ReadTrack:te,VariantTrack:ee,CompositeTrack:Y,object_from_template:ne}}).call(t,i(0),i(2))},function(e,t,i){"use strict";(function(e,n){Object.defineProperty(t,"__esModule",{value:!0});var a=i(1),s=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t.default=e,t}(a),o=e.View.extend({className:"track-header",initialize:function(){this.model.config.get("name").on("change:value",this.update_name,this),this.render()},render:function(){this.$el.append(n("").addClass(this.model.drag_handle_class)),this.$el.append(n("").addClass("track-name").text(this.model.config.get_value("name"))),this.action_icons={},this.render_action_icons(),this.$el.dblclick(function(e){e.stopPropagation()}),this.$el.append(n(""))},update_name:function(){this.$el.find(".track-name").text(this.model.config.get_value("name"))},render_action_icons:function(){var e=this;this.icons_div=n("").addClass("track-icons").hide().appendTo(this.$el),s.each(this.model.action_icons_def,function(t){e.add_action_icon(t.name,t.title,t.css_class,t.on_click_fn,t.prepend,t.hide)}),this.set_display_modes(this.model.display_modes)},add_action_icon:function(e,t,i,a,s,o){var r=this;this.action_icons[e]=n("").attr("title",t).addClass("icon-button").addClass(i).tooltip().click(function(){a(r.model)}).appendTo(this.icons_div),o&&this.action_icons[e].hide()},set_display_modes:function(e,t){if(e){this.model.display_modes=e,this.model.mode=t||this.model.config.get_value("mode")||this.model.display_modes[0],this.action_icons.mode_icon.attr("title","Set display mode (now: "+this.mode+")");for(var i=this.model,n={},a=0,s=i.display_modes.length;a=0?(t-=a,"left"):(i+=a,"right")}return[t,i]},_find_slot:function(e){for(var t=e[0],i=e[1],n=0;n<=this.max_rows;n++){var a=!1,s=this.start_end_dct[n];if(void 0!==s)for(var o=0,r=s.length;o