genoverse 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.eslintrc.js +197 -0
  2. package/.github/workflows/test.yml +24 -0
  3. package/LICENSE.TXT +24 -0
  4. package/README.md +11 -0
  5. package/css/controlPanel.css +200 -0
  6. package/css/fileDrop.css +22 -0
  7. package/css/font-awesome.css +3 -0
  8. package/css/fullscreen.css +19 -0
  9. package/css/genoverse.css +466 -0
  10. package/css/karyotype.css +85 -0
  11. package/css/resizer.css +36 -0
  12. package/css/tooltips.css +26 -0
  13. package/css/trackControls.css +111 -0
  14. package/expanded.html +120 -0
  15. package/fontawesome/css/fontawesome.min.css +5 -0
  16. package/fontawesome/css/regular.min.css +5 -0
  17. package/fontawesome/css/solid.min.css +5 -0
  18. package/fontawesome/webfonts/fa-brands-400.ttf +0 -0
  19. package/fontawesome/webfonts/fa-brands-400.woff +0 -0
  20. package/fontawesome/webfonts/fa-brands-400.woff2 +0 -0
  21. package/fontawesome/webfonts/fa-regular-400.ttf +0 -0
  22. package/fontawesome/webfonts/fa-regular-400.woff +0 -0
  23. package/fontawesome/webfonts/fa-regular-400.woff2 +0 -0
  24. package/fontawesome/webfonts/fa-solid-900.ttf +0 -0
  25. package/fontawesome/webfonts/fa-solid-900.woff +0 -0
  26. package/fontawesome/webfonts/fa-solid-900.woff2 +0 -0
  27. package/help.pdf +0 -0
  28. package/i/sort_handle.png +0 -0
  29. package/index.html +68 -0
  30. package/index.js +83 -0
  31. package/jest.config.js +4 -0
  32. package/js/Genoverse.js +1681 -0
  33. package/js/Track/Controller/Sequence.js +17 -0
  34. package/js/Track/Controller/Stranded.js +73 -0
  35. package/js/Track/Controller.js +620 -0
  36. package/js/Track/Model/File/BAM.js +44 -0
  37. package/js/Track/Model/File/BED.js +116 -0
  38. package/js/Track/Model/File/GFF.js +40 -0
  39. package/js/Track/Model/File/VCF.js +101 -0
  40. package/js/Track/Model/File/WIG.js +67 -0
  41. package/js/Track/Model/File.js +36 -0
  42. package/js/Track/Model/Gene/Ensembl.js +22 -0
  43. package/js/Track/Model/Gene.js +5 -0
  44. package/js/Track/Model/Sequence/Ensembl.js +4 -0
  45. package/js/Track/Model/Sequence/Fasta.js +60 -0
  46. package/js/Track/Model/Sequence.js +50 -0
  47. package/js/Track/Model/SequenceVariation.js +41 -0
  48. package/js/Track/Model/Stranded.js +28 -0
  49. package/js/Track/Model/Transcript/Ensembl.js +67 -0
  50. package/js/Track/Model/Transcript.js +5 -0
  51. package/js/Track/Model.js +303 -0
  52. package/js/Track/View/Gene/Ensembl.js +46 -0
  53. package/js/Track/View/Gene.js +6 -0
  54. package/js/Track/View/Sequence/Variation.js +115 -0
  55. package/js/Track/View/Sequence.js +63 -0
  56. package/js/Track/View/Transcript/Ensembl.js +12 -0
  57. package/js/Track/View/Transcript.js +28 -0
  58. package/js/Track/View.js +566 -0
  59. package/js/Track/library/Chromosome.js +145 -0
  60. package/js/Track/library/File/BAM.js +30 -0
  61. package/js/Track/library/File/BED.js +24 -0
  62. package/js/Track/library/File/BIGBED.js +47 -0
  63. package/js/Track/library/File/BIGWIG.js +52 -0
  64. package/js/Track/library/File/GFF.js +9 -0
  65. package/js/Track/library/File/VCF.js +71 -0
  66. package/js/Track/library/File/WIG.js +5 -0
  67. package/js/Track/library/File.js +10 -0
  68. package/js/Track/library/Gene.js +37 -0
  69. package/js/Track/library/Graph/Bar.js +235 -0
  70. package/js/Track/library/Graph/Line.js +296 -0
  71. package/js/Track/library/Graph.js +355 -0
  72. package/js/Track/library/HighlightRegion.js +292 -0
  73. package/js/Track/library/Legend.js +224 -0
  74. package/js/Track/library/Scalebar.js +227 -0
  75. package/js/Track/library/Scaleline.js +91 -0
  76. package/js/Track/library/Static.js +78 -0
  77. package/js/Track/library/dbSNP.js +142 -0
  78. package/js/Track.js +632 -0
  79. package/js/genomes/grch37.js +990 -0
  80. package/js/genomes/grch38.js +990 -0
  81. package/js/genoverse.min.js +2 -0
  82. package/js/genoverse.min.js.map +1 -0
  83. package/js/lib/BWReader.js +578 -0
  84. package/js/lib/Base.js +145 -0
  85. package/js/lib/VCFReader.js +286 -0
  86. package/js/lib/dalliance/js/bam.js +494 -0
  87. package/js/lib/dalliance/js/bin.js +185 -0
  88. package/js/lib/dalliance/js/das.js +749 -0
  89. package/js/lib/dalliance/js/utils.js +370 -0
  90. package/js/lib/dalliance-lib.js +3594 -0
  91. package/js/lib/dalliance-lib.min.js +68 -0
  92. package/js/lib/jDataView.js +2 -0
  93. package/js/lib/jParser.js +192 -0
  94. package/js/lib/jquery-ui.js +8 -0
  95. package/js/lib/jquery.js +2 -0
  96. package/js/lib/jquery.mousehold.js +53 -0
  97. package/js/lib/jquery.mousewheel.js +84 -0
  98. package/js/lib/jquery.tipsy.js +258 -0
  99. package/js/lib/rtree.js +1 -0
  100. package/js/plugins/controlPanel.js +395 -0
  101. package/js/plugins/fileDrop.js +62 -0
  102. package/js/plugins/focusRegion.js +12 -0
  103. package/js/plugins/fullscreen.js +77 -0
  104. package/js/plugins/karyotype.js +210 -0
  105. package/js/plugins/resizer.js +45 -0
  106. package/js/plugins/tooltips.js +94 -0
  107. package/js/plugins/trackControls.js +143 -0
  108. package/package.json +43 -0
  109. package/test/View/__snapshots__/render-bar-graph.test.js.snap +111 -0
  110. package/test/View/__snapshots__/render-blocks.test.js.snap +105 -0
  111. package/test/View/__snapshots__/render-chromosome.test.js.snap +5 -0
  112. package/test/View/__snapshots__/render-highlights.test.js.snap +73 -0
  113. package/test/View/__snapshots__/render-insert-variants.test.js.snap +9 -0
  114. package/test/View/__snapshots__/render-labels.test.js.snap +241 -0
  115. package/test/View/__snapshots__/render-legends.test.js.snap +13 -0
  116. package/test/View/__snapshots__/render-line-graph.test.js.snap +349 -0
  117. package/test/View/__snapshots__/render-scalebar.test.js.snap +49 -0
  118. package/test/View/__snapshots__/render-scaleline.test.js.snap +31 -0
  119. package/test/View/__snapshots__/render-sequence.test.js.snap +23 -0
  120. package/test/View/__snapshots__/render-stranded.test.js.snap +5 -0
  121. package/test/View/__snapshots__/render-transcripts.test.js.snap +193 -0
  122. package/test/View/render-bar-graph.test.js +87 -0
  123. package/test/View/render-blocks.test.js +171 -0
  124. package/test/View/render-chromosome.test.js +40 -0
  125. package/test/View/render-highlights.test.js +67 -0
  126. package/test/View/render-insert-variants.test.js +11 -0
  127. package/test/View/render-labels.test.js +266 -0
  128. package/test/View/render-legends.test.js +31 -0
  129. package/test/View/render-line-graph.test.js +169 -0
  130. package/test/View/render-scalebar.test.js +36 -0
  131. package/test/View/render-scaleline.test.js +28 -0
  132. package/test/View/render-sequence.test.js +49 -0
  133. package/test/View/render-stranded.test.js +10 -0
  134. package/test/View/render-transcripts.test.js +165 -0
  135. package/test/create-and-destroy.test.js +63 -0
  136. package/test/track-ordering.test.js +514 -0
  137. package/test/track_config/__snapshots__/config-settings.test.js.snap +23 -0
  138. package/test/track_config/config-settings.test.js +321 -0
  139. package/test/track_config/zoom-level-settings.test.js +98 -0
  140. package/test/utils.js +80 -0
  141. package/utils/createGenome.js +52 -0
  142. package/utils/devServer.js +36 -0
  143. package/utils/expandedTemplate.html +46 -0
  144. package/utils/git-hooks/post-commit +9 -0
  145. package/utils/git-hooks/pre-commit +7 -0
  146. package/utils/git-hooks/setup +6 -0
  147. package/utils/makeExpanded.js +19 -0
  148. package/webpack.config.js +39 -0
@@ -0,0 +1,1681 @@
1
+ var Genoverse = Base.extend({
2
+ // Defaults
3
+ baseURL : undefined, // If multiple instances of Genoverse exist on a page at once, specifying different baseURL values allows some/all to ignore external URL changes
4
+ urlParamTemplate : 'r=__CHR__:__START__-__END__', // Overwrite this for your URL style
5
+ width : 1000,
6
+ longestLabel : 30,
7
+ defaultLength : 5000,
8
+ defaultScrollDelta : 100,
9
+ tracks : [],
10
+ highlights : [],
11
+ plugins : [],
12
+ dragAction : 'scroll', // Options are: scroll, select, off
13
+ wheelAction : 'off', // Options are: zoom, off
14
+ isStatic : false, // If true, will stop drag, select and zoom actions occurring
15
+ saveable : false, // If true, track configuration and ordering will be saved in sessionStorage/localStorage
16
+ saveKey : '', // Default key for sessionStorage/localStorage configuration is 'genoverse'. saveKey will be appended to this if it is set
17
+ storageType : 'sessionStorage', // Set to localStorage for permanence
18
+ autoHideMessages : true, // Determines whether to collapse track messages by default
19
+ trackAutoHeight : false, // Determines whether to automatically resize tracks to show all their features (can be overridden by track.autoHeight)
20
+ hideEmptyTracks : true, // Determines whether to hide an automatically resized tracks if it has no features, or to show it empty (can be overridden by track.hideEmpty)
21
+ genome : undefined, // The genome used in the browser - can be an object or a string, which will be used to obtain a javascript file
22
+ useHash : undefined, // If true, window.location.hash is changed on navigation. If false, window.history.pushState is used. If undefined, pushState will be used if present in the browser
23
+
24
+ // Default coordinates for initial view, overwrite in your config
25
+ chr : 1,
26
+ start : 1,
27
+ end : 1000000,
28
+
29
+ constructor: function (config) {
30
+ var browser = this;
31
+
32
+ if (!this.supported()) {
33
+ return this.die('Your browser does not support this functionality');
34
+ }
35
+
36
+ config = config || {};
37
+
38
+ config.container = $(config.container); // Make sure container is a jquery object, jquery recognises itself automatically
39
+
40
+ if (!(config.container && config.container.length)) {
41
+ config.container = $('<div>').appendTo('body');
42
+ }
43
+
44
+ config.container.addClass('genoverse').data('genoverse', this);
45
+
46
+ $.extend(this, config);
47
+
48
+ this.eventNamespace = '.genoverse.' + (++Genoverse.id);
49
+ this.events = { browser: {}, tracks: {} };
50
+
51
+ $.when(Genoverse.ready, this.loadGenome(), this.loadPlugins()).always(function () {
52
+ Genoverse.wrapFunctions(browser);
53
+ browser.init();
54
+ });
55
+ },
56
+
57
+ loadGenome: function () {
58
+ if (typeof this.genome === 'string') {
59
+ var genomeName = this.genome.toLowerCase();
60
+
61
+ return $.ajax({
62
+ url : Genoverse.origin + 'js/genomes/' + genomeName + '.js',
63
+ dataType : 'script',
64
+ context : this,
65
+ success : function () {
66
+ this.genomeName = this.genome;
67
+ this.genome = Genoverse.Genomes[genomeName];
68
+
69
+ if (!this.genome) {
70
+ this.die('Unable to load genome ' + genomeName);
71
+ }
72
+ }
73
+ });
74
+ }
75
+ },
76
+
77
+ loadPlugins: function (plugins) {
78
+ var browser = this;
79
+ var loadPluginsTask = $.Deferred();
80
+ var i;
81
+
82
+ plugins = plugins || this.plugins;
83
+
84
+ this.loadedPlugins = this.loadedPlugins || {};
85
+
86
+ for (i in Genoverse.Plugins) {
87
+ this.loadedPlugins[i] = this.loadedPlugins[i] || 'script';
88
+ }
89
+
90
+ if (typeof plugins === 'string') {
91
+ plugins = [ plugins ];
92
+ }
93
+
94
+ plugins = plugins.map(function (plugin) {
95
+ return Array.isArray(plugin) ? plugin : [ plugin, {}];
96
+ });
97
+
98
+ var pluginsByName = plugins.reduce(
99
+ function (acc, plugin) {
100
+ acc[plugin[0]] = plugin;
101
+ return acc;
102
+ },
103
+ {}
104
+ );
105
+
106
+ function loadPlugin(arg) {
107
+ var plugin = arg[0];
108
+ var css = Genoverse.origin + 'css/' + plugin + '.css';
109
+ var js = Genoverse.origin + 'js/plugins/' + plugin + '.js';
110
+ var deferred = $.Deferred();
111
+
112
+ function getCSS() {
113
+ function done() {
114
+ browser.loadedPlugins[plugin] = browser.loadedPlugins[plugin] || 'script';
115
+ deferred.resolve(arg);
116
+ }
117
+
118
+ if (Genoverse.Plugins[plugin].noCSS || $('link[href="' + css + '"]').length) {
119
+ return done();
120
+ }
121
+
122
+ $('<link href="' + css + '" rel="stylesheet">').on('load', done).appendTo('body');
123
+ }
124
+
125
+ if (browser.loadedPlugins[plugin] || $('script[src="' + js + '"]').length) {
126
+ getCSS();
127
+ } else {
128
+ $.getScript(js, getCSS);
129
+ }
130
+
131
+ return deferred;
132
+ }
133
+
134
+ function initializePlugin(plugin, conf) {
135
+ if (typeof Genoverse.Plugins[plugin] !== 'function' || browser.loadedPlugins[plugin] === true) {
136
+ return [];
137
+ }
138
+
139
+ var requires = Genoverse.Plugins[plugin].requires;
140
+ var deferred = $.Deferred();
141
+
142
+ function init() {
143
+ if (browser.loadedPlugins[plugin] !== true) {
144
+ Genoverse.Plugins[plugin].call(browser, conf);
145
+ browser.container.addClass('gv-' + plugin.replace(/([A-Z])/g, '-$1').toLowerCase() + '-plugin');
146
+ browser.loadedPlugins[plugin] = true;
147
+ }
148
+
149
+ deferred.resolve();
150
+ }
151
+
152
+ if (requires) {
153
+ $.when(
154
+ browser.loadPlugins(
155
+ [].concat(requires).map(function (pluginName) {
156
+ return pluginsByName[pluginName] || pluginName;
157
+ })
158
+ )
159
+ ).done(init);
160
+ } else {
161
+ init();
162
+ }
163
+
164
+ return deferred;
165
+ }
166
+
167
+ // Load plugins css file
168
+ $.when.apply($, $.map(plugins, loadPlugin)).done(function () {
169
+ var pluginsLoaded = [];
170
+ var plugin;
171
+
172
+ for (i = 0; i < arguments.length; i++) {
173
+ plugin = arguments[i];
174
+
175
+ if (browser.loadedPlugins[plugin[0]] !== true) {
176
+ pluginsLoaded.push(initializePlugin(plugin[0], plugin[1]));
177
+ }
178
+ }
179
+
180
+ $.when.apply($, pluginsLoaded).always(loadPluginsTask.resolve);
181
+ });
182
+
183
+ return loadPluginsTask;
184
+ },
185
+
186
+ init: function () {
187
+ var width = this.width;
188
+
189
+ this.addDomElements(width);
190
+ this.addUserEventHandlers();
191
+
192
+ if (this.isStatic) {
193
+ this.dragAction = this.wheelAction = 'off';
194
+ this.urlParamTemplate = false;
195
+ }
196
+
197
+ this.tracksById = {};
198
+ this.prev = {};
199
+ this.legends = {};
200
+ this.initialLocation = { chr: this.chr, start: this.start, end: this.end };
201
+ this.saveKey = this.saveKey ? 'genoverse-' + this.saveKey : 'genoverse';
202
+ this.urlParamTemplate = this.urlParamTemplate || '';
203
+ this.useHash = typeof this.useHash === 'boolean' ? this.useHash : typeof window.history.pushState !== 'function';
204
+ this.textWidth = document.createElement('canvas').getContext('2d').measureText('W').width;
205
+ this.labelWidth = this.labelContainer.outerWidth(true) || 0;
206
+ this.width = Math.min(this.width - this.labelWidth, this.wrapper.width() || Infinity); // Recalculate the width to ignore the affect of borders
207
+ this.paramRegex = (
208
+ this.urlParamTemplate
209
+ ? new RegExp(
210
+ '([?&;])'
211
+ + this.urlParamTemplate
212
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
213
+ .replace(/(\b(\w+=)?__CHR__(.)?)/, '$2([\\w\\.]+)$3')
214
+ .replace(/(\b(\w+=)?__START__(.)?)/, '$2(\\d+)$3')
215
+ .replace(/(\b(\w+=)?__END__(.)?)/, '$2(\\d+)$3')
216
+ + '([;&])'
217
+ )
218
+ : ''
219
+ );
220
+
221
+ var coords = this.getCoords();
222
+
223
+ this.chr = coords.chr;
224
+
225
+ if (this.genome) {
226
+ this.chromosomeSize = this.genome[this.chr].size;
227
+ }
228
+
229
+ this.canChangeChr = !!this.genome;
230
+
231
+ if (this.saveable) {
232
+ this.loadConfig();
233
+ } else {
234
+ this.addTracks();
235
+ }
236
+
237
+ if (this.width > 0) {
238
+ this.setRange(coords.start, coords.end);
239
+ }
240
+
241
+ if (this.highlights.length) {
242
+ this.addHighlights(this.highlights);
243
+ }
244
+ },
245
+
246
+ loadConfig: function () {
247
+ var config;
248
+
249
+ this.defaultTracks = $.extend(true, [], this.tracks);
250
+
251
+ try {
252
+ config = window[this.storageType].getItem(this.saveKey);
253
+ } catch (e) {}
254
+
255
+ if (config) {
256
+ config = JSON.parse(config);
257
+ } else {
258
+ return this.addTracks();
259
+ }
260
+
261
+ var tracksByNamespace = Genoverse.getAllTrackTypes();
262
+ var tracks = [];
263
+ var tracksById = {};
264
+ var savedConfig = {};
265
+ var i, prop, track, trackId;
266
+
267
+ function setConfig($track, conf) {
268
+ for (prop in conf) {
269
+ if (prop === 'config') {
270
+ savedConfig[conf.id] = conf[prop];
271
+ } else {
272
+ if (prop === 'height') {
273
+ conf[prop] = parseInt(conf[prop], 10);
274
+
275
+ if (isNaN(conf[prop])) {
276
+ continue;
277
+ }
278
+ }
279
+
280
+ $track.prototype[prop] = conf[prop];
281
+ }
282
+ }
283
+ }
284
+
285
+ for (i = 0; i < this.tracks.length; i++) {
286
+ if (this.tracks[i].prototype.id) {
287
+ tracksById[this.tracks[i].prototype.id] = this.tracks[i];
288
+ }
289
+ }
290
+
291
+ for (i = 0; i < config.length; i++) {
292
+ track = tracksById[config[i].id];
293
+
294
+ if (track) {
295
+ setConfig(track, config[i]);
296
+ track._fromStorage = true;
297
+ } else if (tracksByNamespace[config[i].namespace]) {
298
+ track = tracksByNamespace[config[i].namespace];
299
+ trackId = track.prototype.id;
300
+
301
+ this.trackIds = this.trackIds || {};
302
+ this.trackIds[trackId] = this.trackIds[trackId] || 1;
303
+
304
+ if (tracksById[trackId]) {
305
+ track = tracksById[trackId];
306
+ }
307
+
308
+ setConfig(track, config[i]);
309
+ tracks.push(track);
310
+ }
311
+ }
312
+
313
+ for (i = 0; i < this.tracks.length; i++) {
314
+ if (this.tracks[i].prototype.id && !this.tracks[i]._fromStorage) {
315
+ continue;
316
+ }
317
+
318
+ tracks.push(this.tracks[i]);
319
+ }
320
+
321
+ this.tracks = tracks;
322
+ this.savedConfig = savedConfig;
323
+
324
+ this.addTracks();
325
+ },
326
+
327
+ saveConfig: function () {
328
+ if (this._constructing || !this.saveable) {
329
+ return;
330
+ }
331
+
332
+ var config = [];
333
+ var conf, j;
334
+
335
+ for (var i = 0; i < this.tracks.length; i++) {
336
+ if (this.tracks[i].id && !(this.tracks[i] instanceof Genoverse.Track.Legend) && !(this.tracks[i] instanceof Genoverse.Track.HighlightRegion)) {
337
+ // when saving height, initialHeight is the height of the track once margins have been added, while defaultHeight is the DEFINED height of the track.
338
+ // Subtracting the difference between them gives you back the correct height to input back into the track when loading configuration
339
+ conf = {
340
+ id : this.tracks[i].id,
341
+ namespace : this.tracks[i].namespace,
342
+ order : this.tracks[i].order,
343
+ autoHeight : this.tracks[i].autoHeight,
344
+ height : this.tracks[i].height - (this.tracks[i].initialHeight - this.tracks[i].defaultHeight)
345
+ };
346
+
347
+ if (this.tracks[i].config) {
348
+ for (j in this.tracks[i].config) {
349
+ conf.config = conf.config || {};
350
+ conf.config[j] = this.tracks[i].config[j];
351
+ }
352
+ }
353
+
354
+ config.push(conf);
355
+ }
356
+ }
357
+
358
+ // Safari in private browsing mode does not allow writes to storage, so wrap in a try/catch to stop errors occuring
359
+ try {
360
+ window[this.storageType].setItem(this.saveKey, JSON.stringify(config));
361
+ } catch (e) {}
362
+ },
363
+
364
+ resetConfig: function () {
365
+ // Non removable highlights should be re-added after reset
366
+ var unremovableHighlights = [];
367
+
368
+ if (this.tracksById.highlights) {
369
+ this.tracksById.highlights.removeHighlights();
370
+ unremovableHighlights = $.map(this.tracksById.highlights.prop('featuresById'), function (h) { return h; });
371
+ }
372
+
373
+ try {
374
+ window[this.storageType].removeItem(this.saveKey);
375
+ } catch (e) {}
376
+
377
+ this._constructing = true;
378
+ this.savedConfig = {};
379
+
380
+ this.removeTracks($.extend([], this.tracks)); // Shallow clone to ensure that removeTracks doesn't hit problems when splicing this.tracks
381
+ this.addTracks($.extend(true, [], this.defaultTracks));
382
+
383
+ if (unremovableHighlights.length) {
384
+ this.addHighlights(unremovableHighlights);
385
+ }
386
+
387
+ this._constructing = false;
388
+ },
389
+
390
+ addDomElements: function (width) {
391
+ this.menus = $();
392
+ this.labelContainer = $('<ul class="gv-label-container">').appendTo(this.container).sortable({
393
+ items : 'li:not(.gv-unsortable)',
394
+ handle : '.gv-handle',
395
+ axis : 'y',
396
+ helper : 'clone',
397
+ cursor : 'move',
398
+ update : $.proxy(this.updateTrackOrder, this),
399
+ start : function (e, ui) {
400
+ ui.placeholder.css({ height: ui.item.height(), visibility: 'visible' }).html(ui.item.html());
401
+ ui.helper.hide();
402
+ }
403
+ });
404
+
405
+ this.wrapper = $('<div class="gv-wrapper">').appendTo(this.container);
406
+ this.selector = $('<div class="gv-selector gv-crosshair">').appendTo(this.wrapper);
407
+
408
+ this.selectorControls = this.zoomInHighlight = this.zoomOutHighlight = $();
409
+
410
+ this.container.addClass('gv-canvas-container').width(width);
411
+
412
+ if (!this.isStatic) {
413
+ this.selectorControls = $(
414
+ '<div class="gv-selector-controls gv-panel">' +
415
+ ' <div class="gv-button-set">' +
416
+ ' <div class="gv-position">' +
417
+ ' <div class="gv-chr"></div>' +
418
+ ' <div class="gv-start-end">' +
419
+ ' <div class="gv-start"></div>' +
420
+ ' <div class="gv-end"></div>' +
421
+ ' </div>' +
422
+ ' </div>' +
423
+ ' </div>' +
424
+ ' <div class="gv-button-set">' +
425
+ ' <button class="gv-zoom-here">Zoom here</button>' +
426
+ ' </div>' +
427
+ ' <div class="gv-button-set">' +
428
+ ' <button class="gv-center">Center</button>' +
429
+ ' </div>' +
430
+ ' <div class="gv-button-set">' +
431
+ ' <button class="gv-highlight">Highlight</button>' +
432
+ ' </div>' +
433
+ ' <div class="gv-button-set">' +
434
+ ' <button class="gv-cancel">Cancel</button>' +
435
+ ' </div>' +
436
+ '</div>'
437
+ ).appendTo(this.selector);
438
+
439
+ this.zoomInHighlight = $(
440
+ '<div class="gv-canvas-zoom gv-i">' +
441
+ ' <div class="gv-t gv-l gv-h"></div>' +
442
+ ' <div class="gv-t gv-r gv-h"></div>' +
443
+ ' <div class="gv-t gv-l gv-v"></div>' +
444
+ ' <div class="gv-t gv-r gv-v"></div>' +
445
+ ' <div class="gv-b gv-l gv-h"></div>' +
446
+ ' <div class="gv-b gv-r gv-h"></div>' +
447
+ ' <div class="gv-b gv-l gv-v"></div>' +
448
+ ' <div class="gv-b gv-r gv-v"></div>' +
449
+ '</div>'
450
+ ).appendTo('body');
451
+
452
+ this.zoomOutHighlight = this.zoomInHighlight.clone().toggleClass('gv-i gv-o').appendTo('body');
453
+ }
454
+ },
455
+
456
+ addUserEventHandlers: function () {
457
+ var browser = this;
458
+ var eventNamespace = this.eventNamespace;
459
+ var documentEvents = {};
460
+ var events = {};
461
+
462
+ events['mousedown' + eventNamespace] = function (e) {
463
+ browser.hideMessages();
464
+
465
+ // Only scroll on left click, and do nothing if clicking on a button in selectorControls
466
+ if ((!e.which || e.which === 1) && !(this === browser.selector[0] && e.target !== this)) {
467
+ browser.mousedown(e);
468
+ }
469
+
470
+ return false;
471
+ };
472
+
473
+ events['mousewheel' + eventNamespace] = function (e, delta, deltaX, deltaY) {
474
+ if (browser.noWheelZoom) {
475
+ return true;
476
+ }
477
+
478
+ browser.hideMessages();
479
+
480
+ if (browser.wheelAction === 'zoom') {
481
+ return browser.mousewheelZoom(e, delta);
482
+ }
483
+
484
+ // Support horizontal wheel/2-finger scroll on trackpads
485
+ if (deltaY === 0 && deltaX !== 0) {
486
+ browser.startDragScroll(e);
487
+ browser.move(-deltaX * 10);
488
+ browser.stopDragScroll(false);
489
+ return false;
490
+ }
491
+ };
492
+
493
+ events['dblclick' + eventNamespace] = function (e) {
494
+ if (browser.isStatic) {
495
+ return true;
496
+ }
497
+
498
+ browser.hideMessages();
499
+ browser.mousewheelZoom(e, 1);
500
+ };
501
+
502
+ this.container.on(events, '.gv-image-container, .gv-selector');
503
+
504
+ this.selectorControls.on('click', function (e) {
505
+ var pos = browser.getSelectorPosition();
506
+
507
+ switch (e.target.className) {
508
+ case 'gv-zoom-here' : browser.setRange(pos.start, pos.end, true); break;
509
+ case 'gv-center' : browser.moveTo(browser.chr, pos.start, pos.end, true, true); browser.cancelSelect(); break;
510
+ case 'gv-highlight' : browser.addHighlight({ chr: browser.chr, start: pos.start, end: pos.end });
511
+ case 'gv-cancel' : browser.cancelSelect(); break;
512
+ default : break;
513
+ }
514
+ });
515
+
516
+ documentEvents['mouseup' + this.eventNamespace] = $.proxy(this.mouseup, this);
517
+ documentEvents['mousemove' + this.eventNamespace] = $.proxy(this.mousemove, this);
518
+ documentEvents['keydown' + this.eventNamespace] = $.proxy(this.keydown, this);
519
+ documentEvents['keyup' + this.eventNamespace] = $.proxy(this.keyup, this);
520
+ documentEvents['mousewheel' + this.eventNamespace] = function (e) {
521
+ if (browser.wheelAction === 'zoom') {
522
+ if (browser.wheelTimeout) {
523
+ clearTimeout(browser.wheelTimeout);
524
+ }
525
+
526
+ browser.noWheelZoom = browser.noWheelZoom || e.target !== browser.container[0];
527
+ browser.wheelTimeout = setTimeout(function () { browser.noWheelZoom = false; }, 300);
528
+ }
529
+ };
530
+
531
+ $(document).on(documentEvents);
532
+ $(window).on((this.useHash ? 'hashchange' : 'popstate') + this.eventNamespace, $.proxy(this.popState, this));
533
+ },
534
+
535
+ onTracks: function () {
536
+ var args = $.extend([], arguments);
537
+ var func = args.shift();
538
+ var mvc;
539
+
540
+ for (var i = 0; i < this.tracks.length; i++) {
541
+ if (this.tracks[i].disabled) {
542
+ continue;
543
+ }
544
+
545
+ mvc = this.tracks[i]._interface[func];
546
+
547
+ if (mvc) {
548
+ this.tracks[i][mvc][func].apply(this.tracks[i][mvc], args);
549
+ } else if (this.tracks[i][func]) {
550
+ this.tracks[i][func].apply(this.tracks[i], args);
551
+ }
552
+ }
553
+ },
554
+
555
+ reset: function () {
556
+ this.onTracks.apply(this, [ 'reset' ].concat([].slice.call(arguments)));
557
+ this.prev = {};
558
+ this.scale = 9e99; // arbitrary value so that setScale resets track scales as well
559
+ this.setRange(this.start, this.end);
560
+ },
561
+
562
+ setWidth: function (width) {
563
+ this.width = width;
564
+ this.width -= this.labelWidth;
565
+
566
+ if (this.controlPanel) {
567
+ this.width -= this.controlPanel.outerWidth(true);
568
+ }
569
+
570
+ if (this.superContainer) {
571
+ this.superContainer.width(width);
572
+ this.container.width(this.width);
573
+ } else {
574
+ this.container.width(width);
575
+ }
576
+
577
+ setTimeout(
578
+ (function () {
579
+ this.onTracks('setWidth', Math.min(this.width, this.container.width())); // If this.container has borders, this.container.width() could be less than this.width
580
+ this.reset('resizing');
581
+ }).bind(this),
582
+ 1
583
+ );
584
+ },
585
+
586
+ mousewheelZoom: function (e, delta) {
587
+ var browser = this;
588
+
589
+ clearTimeout(this.zoomDeltaTimeout);
590
+ clearTimeout(this.zoomTimeout);
591
+
592
+ this.zoomDeltaTimeout = setTimeout(function () {
593
+ if (delta > 0) {
594
+ browser.zoomInHighlight.css({ left: e.pageX - 20, top: e.pageY - 20, display: 'block' }).animate({
595
+ width: 80, height: 80, top: '-=20', left: '-=20'
596
+ }, {
597
+ complete: function () { $(this).css({ width: 40, height: 40, display: 'none' }); }
598
+ });
599
+ } else {
600
+ browser.zoomOutHighlight.css({ left: e.pageX - 40, top: e.pageY - 40, display: 'block' }).animate({
601
+ width: 40, height: 40, top: '+=20', left: '+=20'
602
+ }, {
603
+ complete: function () { $(this).css({ width: 80, height: 80, display: 'none' }); }
604
+ });
605
+ }
606
+ }, 100);
607
+
608
+ this.zoomTimeout = setTimeout(function () {
609
+ browser[delta > 0 ? 'zoomIn' : 'zoomOut'](e.pageX - browser.container.offset().left - browser.labelWidth);
610
+
611
+ if (browser.dragAction === 'select') {
612
+ browser.moveSelector(e);
613
+ }
614
+ }, 300);
615
+
616
+ return false;
617
+ },
618
+
619
+ startDragScroll: function (e) {
620
+ this.dragging = 'scroll';
621
+ this.scrolling = !e;
622
+ this.dragOffset = e ? e.pageX - this.left : 0;
623
+ this.dragStart = this.start;
624
+ this.scrollDelta = Math.max(this.scale, this.defaultScrollDelta);
625
+ },
626
+
627
+ stopDragScroll: function (update) {
628
+ this.dragging = false;
629
+ this.scrolling = false;
630
+
631
+ if (update !== false) {
632
+ if (this.start !== this.dragStart) {
633
+ this.updateURL();
634
+ }
635
+
636
+ this.checkTrackHeights();
637
+ }
638
+ },
639
+
640
+ startDragSelect: function (e) {
641
+ if (!e) {
642
+ return false;
643
+ }
644
+
645
+ var x = Math.max(0, e.pageX - this.wrapper.offset().left - 2);
646
+
647
+ this.dragging = 'select';
648
+ this.selectorStalled = false;
649
+ this.selectorStart = x;
650
+
651
+ this.selector.css({ left: x, width: 0 }).removeClass('gv-crosshair');
652
+ this.selectorControls.hide();
653
+ },
654
+
655
+ stopDragSelect: function (e) {
656
+ if (!e) {
657
+ return false;
658
+ }
659
+
660
+ this.dragging = false;
661
+ this.selectorStalled = true;
662
+
663
+ if (this.selector.outerWidth(true) < 2) {
664
+ return this.cancelSelect();
665
+ }
666
+
667
+ // Calculate the position, so that selectorControls appear near the mouse cursor
668
+ var top = Math.min(e.pageY - this.wrapper.offset().top, this.wrapper.outerHeight(true) - 1.2 * this.selectorControls.outerHeight(true));
669
+ var pos = this.getSelectorPosition();
670
+
671
+ this.selectorControls.find('.gv-chr').html(this.chr);
672
+ this.selectorControls.find('.gv-start').html(pos.start);
673
+ this.selectorControls.find('.gv-end').html(pos.end);
674
+
675
+ this.selectorControls.find('.gv-selector-location').html(this.chr + ':' + pos.start + '-' + pos.end).end().css({
676
+ top : top,
677
+ left : this.selector.outerWidth(true) / 2 - this.selectorControls.outerWidth(true) / 2
678
+ }).show();
679
+ },
680
+
681
+ cancelSelect: function (keepDragging) {
682
+ if (!keepDragging) {
683
+ this.dragging = false;
684
+ }
685
+
686
+ this.selectorStalled = false;
687
+
688
+ this.selector.addClass('gv-crosshair').width(0);
689
+ this.selectorControls.hide();
690
+
691
+ if (this.dragAction === 'scroll') {
692
+ this.selector.hide();
693
+ }
694
+ },
695
+
696
+ dragSelect: function (e) {
697
+ var x = e.pageX - this.wrapper.offset().left;
698
+
699
+ if (x > this.selectorStart) {
700
+ this.selector.css({
701
+ left : this.selectorStart,
702
+ width : Math.min(x - this.selectorStart, this.width - this.selectorStart - 1)
703
+ });
704
+ } else {
705
+ this.selector.css({
706
+ left : Math.max(x, 1),
707
+ width : Math.min(this.selectorStart - x, this.selectorStart - 1)
708
+ });
709
+ }
710
+ },
711
+
712
+ setDragAction: function (action, keepSelect) {
713
+ this.dragAction = action;
714
+
715
+ if (this.dragAction === 'select') {
716
+ this.selector.addClass('gv-crosshair').width(0).show();
717
+ } else if (keepSelect && !this.selector.hasClass('gv-crosshair')) {
718
+ this.selectorStalled = false;
719
+ } else {
720
+ this.cancelSelect();
721
+ this.selector.hide();
722
+ }
723
+ },
724
+
725
+ toggleSelect: function (on) {
726
+ if (on) {
727
+ this.prev.dragAction = 'scroll';
728
+ this.setDragAction('select');
729
+ } else {
730
+ this.setDragAction(this.prev.dragAction, true);
731
+ delete this.prev.dragAction;
732
+ }
733
+ },
734
+
735
+ setWheelAction: function (action) {
736
+ this.wheelAction = action;
737
+ },
738
+
739
+ keydown: function (e) {
740
+ if (e.which === 16 && !this.prev.dragAction && this.dragAction === 'scroll') { // shift key
741
+ this.toggleSelect(true);
742
+ } else if (e.which === 27) { // escape key
743
+ this.cancelSelect();
744
+ this.closeMenus();
745
+ }
746
+ },
747
+
748
+ keyup: function (e) {
749
+ if (e.which === 16 && this.prev.dragAction) { // shift key
750
+ this.toggleSelect();
751
+ }
752
+ },
753
+
754
+ mousedown: function (e) {
755
+ if (e.shiftKey) {
756
+ if (this.dragAction === 'scroll') {
757
+ this.toggleSelect(true);
758
+ }
759
+ } else if (this.prev.dragAction) {
760
+ this.toggleSelect();
761
+ }
762
+
763
+ switch (this.dragAction) {
764
+ case 'select' : this.startDragSelect(e); break;
765
+ case 'scroll' : this.startDragScroll(e); break;
766
+ default : break;
767
+ }
768
+ },
769
+
770
+ mouseup: function (e) {
771
+ switch (this.dragging) {
772
+ case 'select' : this.stopDragSelect(e); break;
773
+ case 'scroll' : this.stopDragScroll(); break;
774
+ default : break;
775
+ }
776
+ },
777
+
778
+ mousemove: function (e) {
779
+ if (this.dragging && !this.scrolling) {
780
+ switch (this.dragAction) {
781
+ case 'scroll' : this.move(e.pageX - this.dragOffset - this.left); break;
782
+ case 'select' : this.dragSelect(e); break;
783
+ default : break;
784
+ }
785
+ } else if (this.dragAction === 'select') {
786
+ this.moveSelector(e);
787
+ }
788
+ },
789
+
790
+ moveSelector: function (e) {
791
+ if (!this.selectorStalled) {
792
+ this.selector.css('left', e.pageX - this.wrapper.offset().left - 2);
793
+ }
794
+ },
795
+
796
+ move: function (delta) {
797
+ var scale = this.scale;
798
+ var start, end, left;
799
+
800
+ if (scale > 1) {
801
+ delta = Math.round(delta / scale) * scale; // Force stepping by base pair when in small regions
802
+ }
803
+
804
+ left = this.left + delta;
805
+
806
+ if (left <= this.minLeft) {
807
+ left = this.minLeft;
808
+ delta = this.minLeft - this.left;
809
+ } else if (left >= this.maxLeft) {
810
+ left = this.maxLeft;
811
+ delta = this.maxLeft - this.left;
812
+ }
813
+
814
+ start = Math.max(Math.round(this.start - delta / scale), 1);
815
+ end = start + this.length - 1;
816
+
817
+ if (end > this.chromosomeSize) {
818
+ end = this.chromosomeSize;
819
+ start = end - this.length + 1;
820
+ }
821
+
822
+ this.left = left;
823
+
824
+ if (start !== this.dragStart) {
825
+ this.closeMenus();
826
+ this.cancelSelect(true);
827
+ }
828
+
829
+ this.onTracks('move', delta);
830
+ this.setRange(start, end);
831
+ },
832
+
833
+ moveTo: function (chr, start, end, update, keepLength) {
834
+ if (typeof chr !== 'undefined' && String(chr) !== String(this.chr)) {
835
+ if (this.canChangeChr) {
836
+ if (this.genome && this.genome[chr]) {
837
+ this.chr = chr;
838
+ this.chromosomeSize = this.genome[chr].size;
839
+ this.start = this.end = this.scale = -1;
840
+ } else {
841
+ this.die('Chromosome cannot be found in genome');
842
+ }
843
+
844
+ this.onTracks('changeChr');
845
+ } else {
846
+ this.die('Chromosome changing is not allowed');
847
+ }
848
+ }
849
+
850
+ this.setRange(start, end, update, keepLength);
851
+
852
+ if (this.prev.scale === this.scale) {
853
+ this.left = Math.max(Math.min(this.left + Math.round((this.prev.start - this.start) * this.scale), this.maxLeft), this.minLeft);
854
+ this.onTracks('moveTo', this.chr, this.start, this.end, (this.prev.start - this.start) * this.scale);
855
+ }
856
+ },
857
+
858
+ setRange: function (start, end, update, keepLength) {
859
+ this.prev.start = this.start;
860
+ this.prev.end = this.end;
861
+ this.start = Math.min(Math.max(typeof start === 'number' ? Math.floor(start) : parseInt(start, 10), 1), this.chromosomeSize);
862
+ this.end = Math.max(Math.min(typeof end === 'number' ? Math.floor(end) : parseInt(end, 10), this.chromosomeSize), 1);
863
+
864
+ if (this.end < this.start) {
865
+ this.end = Math.min(this.start + this.defaultLength - 1, this.chromosomeSize);
866
+ }
867
+
868
+ if (keepLength && this.end - this.start + 1 !== this.length) {
869
+ if (this.end === this.chromosomeSize) {
870
+ this.start = this.end - this.length + 1;
871
+ } else {
872
+ var center = (this.start + this.end) / 2;
873
+ this.start = Math.max(Math.floor(center - this.length / 2), 1);
874
+ this.end = this.start + this.length - 1;
875
+
876
+ if (this.end > this.chromosomeSize) {
877
+ this.end = this.chromosomeSize;
878
+ this.start = this.end - this.length + 1;
879
+ }
880
+ }
881
+ } else {
882
+ this.length = this.end - this.start + 1;
883
+ }
884
+
885
+ this.setScale();
886
+
887
+ if (update === true && (this.prev.start !== this.start || this.prev.end !== this.end)) {
888
+ this.updateURL();
889
+ }
890
+ },
891
+
892
+ setScale: function () {
893
+ this.prev.scale = this.scale;
894
+ this.scale = this.width / this.length;
895
+ this.scaledStart = this.start * this.scale;
896
+
897
+ if (this.prev.scale !== this.scale) {
898
+ this.left = 0;
899
+ this.minLeft = Math.round((this.end - this.chromosomeSize) * this.scale);
900
+ this.maxLeft = Math.round((this.start - 1) * this.scale);
901
+ this.labelBuffer = Math.ceil(this.textWidth / this.scale) * this.longestLabel;
902
+
903
+ if (this.prev.scale) {
904
+ this.cancelSelect();
905
+ this.closeMenus();
906
+ }
907
+
908
+ this.onTracks('setScale');
909
+ this.onTracks('makeFirstImage');
910
+ }
911
+ },
912
+
913
+ checkTrackHeights: function () {
914
+ if (this.dragging) {
915
+ return;
916
+ }
917
+
918
+ this.onTracks('checkHeight');
919
+ },
920
+
921
+ resetTrackHeights: function () {
922
+ this.onTracks('resetHeight');
923
+ },
924
+
925
+ zoomIn: function (x) {
926
+ if (!x) {
927
+ x = this.width / 2;
928
+ }
929
+
930
+ var start = Math.round(this.start + x / (2 * this.scale));
931
+ var end = this.length === 2 ? start : Math.round(start + (this.length - 1) / 2);
932
+
933
+ this.setRange(start, end, true);
934
+ },
935
+
936
+ zoomOut: function (x) {
937
+ if (!x) {
938
+ x = this.width / 2;
939
+ }
940
+
941
+ var start = Math.round(this.start - x / this.scale);
942
+ var end = this.length === 1 ? start + 1 : Math.round(start + 2 * (this.length - 1));
943
+
944
+ this.setRange(start, end, true);
945
+ },
946
+
947
+ addTrack: function (track, after) {
948
+ return this.addTracks([ track ], after)[0];
949
+ },
950
+
951
+ addTracks: function (tracks, after) {
952
+ var defaults = {
953
+ browser : this,
954
+ width : Math.min(this.width, this.container.width()) // If this.container has borders, this.container.width() could be less than this.width
955
+ };
956
+
957
+ var push = !!tracks;
958
+ var order;
959
+
960
+ tracks = tracks || $.extend([], this.tracks);
961
+
962
+ if (push && !$.grep(this.tracks, function (t) { return typeof t === 'function'; }).length) {
963
+ var insertAfter = (after ? $.grep(this.tracks, function (t) { return t.order < after; }) : this.tracks).sort(function (a, b) { return b.order - a.order; })[0];
964
+
965
+ if (insertAfter) {
966
+ order = insertAfter.order + 0.1;
967
+ }
968
+ }
969
+
970
+ for (var i = 0; i < tracks.length; i++) {
971
+ tracks[i] = new tracks[i]($.extend(defaults, {
972
+ namespace : Genoverse.getTrackNamespace(tracks[i]),
973
+ order : typeof order === 'number' ? order : i,
974
+ config : this.savedConfig ? $.extend(true, {}, this.savedConfig[tracks[i].prototype.id]) : undefined
975
+ }));
976
+
977
+ if (tracks[i].id) {
978
+ this.tracksById[tracks[i].id] = tracks[i];
979
+ }
980
+
981
+ if (push) {
982
+ this.tracks.push(tracks[i]);
983
+ } else {
984
+ this.tracks[i] = tracks[i];
985
+ }
986
+ }
987
+
988
+ this.sortTracks();
989
+ this.saveConfig();
990
+
991
+ return tracks;
992
+ },
993
+
994
+ removeTrack: function (track) {
995
+ if (track) {
996
+ this.removeTracks((track.prop('childTracks') || []).concat(track));
997
+ }
998
+ },
999
+
1000
+ removeTracks: function (tracks) {
1001
+ var i = tracks.length;
1002
+ var track, j;
1003
+
1004
+ while (i--) {
1005
+ track = tracks[i];
1006
+ j = this.tracks.length;
1007
+
1008
+ while (j--) {
1009
+ if (track === this.tracks[j]) {
1010
+ this.tracks.splice(j, 1);
1011
+ break;
1012
+ }
1013
+ }
1014
+
1015
+ if (track.id) {
1016
+ delete this.tracksById[track.id];
1017
+ }
1018
+
1019
+ track.destructor(); // Destroy DOM elements and track itself
1020
+ }
1021
+
1022
+ this.saveConfig();
1023
+ },
1024
+
1025
+ sortTracks: function () {
1026
+ if ($.grep(this.tracks, function (t) { return typeof t !== 'object'; }).length) {
1027
+ return;
1028
+ }
1029
+
1030
+ var sorted = $.extend([], this.tracks).sort(function (a, b) { return a.order - b.order; });
1031
+ var labels = $();
1032
+ var containers = $();
1033
+ var container;
1034
+
1035
+ for (var i = 0; i < sorted.length; i++) {
1036
+ if (sorted[i].prop('parentTrack')) {
1037
+ continue;
1038
+ }
1039
+
1040
+ if (!sorted[i].prop('fixedOrder')) {
1041
+ sorted[i].prop('order', i);
1042
+ }
1043
+
1044
+ container = sorted[i].prop('superContainer') || sorted[i].prop('container');
1045
+
1046
+ if (sorted[i].prop('menus').length) {
1047
+ sorted[i].prop('top', container.position().top);
1048
+ }
1049
+
1050
+ labels.push(sorted[i].prop('label')[0]);
1051
+ containers.push(container[0]);
1052
+ }
1053
+
1054
+ this.labelContainer.append(labels);
1055
+ this.wrapper.append(containers);
1056
+
1057
+ // Correct the order
1058
+ this.tracks = sorted;
1059
+
1060
+ labels.map(function () { return $(this).data('track'); }).each(function () {
1061
+ if (this.prop('menus').length) {
1062
+ var diff = (this.prop('superContainer') || this.prop('container')).position().top - this.prop('top');
1063
+ this.prop('menus').css('top', function (j, top) { return parseInt(top, 10) + diff; });
1064
+ this.prop('top', null);
1065
+ }
1066
+ });
1067
+
1068
+ sorted = labels = containers = null;
1069
+ },
1070
+
1071
+ updateTrackOrder: function (e, ui) {
1072
+ var track = ui.item.data('track');
1073
+
1074
+ if (track.prop('unsortable') || track.prop('fixedOrder')) {
1075
+ return;
1076
+ }
1077
+
1078
+ var prev = ui.item.prev().data('track');
1079
+ var next = ui.item.next().data('track');
1080
+ var p = prev ? prev.prop('order') : 0;
1081
+ var n = next ? next.prop('order') : 0;
1082
+ var o = p || n;
1083
+ var order;
1084
+
1085
+ if (prev && next && Math.floor(n) === Math.floor(p)) {
1086
+ order = p + (n - p) / 2;
1087
+ } else {
1088
+ order = o + ((p ? 1 : -1) * Math.abs(Math.round(o) - o || 1)) / 2;
1089
+ }
1090
+
1091
+ track.prop('order', order);
1092
+
1093
+ this.sortTracks();
1094
+ this.saveConfig();
1095
+ },
1096
+
1097
+ updateURL: function () {
1098
+ if (this.urlParamTemplate) {
1099
+ if (this.useHash) {
1100
+ window.location.hash = this.getQueryString();
1101
+ } else {
1102
+ window.history.pushState({}, '', this.getQueryString());
1103
+ }
1104
+ }
1105
+ },
1106
+
1107
+ popState: function () {
1108
+ if (this.baseURL && !window.location.href.match(this.baseURL)) {
1109
+ return;
1110
+ }
1111
+
1112
+ var coords = this.getCoords();
1113
+ var start = parseInt(coords.start, 10);
1114
+ var end = parseInt(coords.end, 10);
1115
+
1116
+ if (
1117
+ (coords.chr && String(coords.chr) !== String(this.chr)) ||
1118
+ (coords.start && !(start === this.start && end === this.end))
1119
+ ) {
1120
+ // FIXME: a back action which changes scale or a zoom out will reset tracks, since scrollStart will not be the same as it was before
1121
+ this.moveTo(coords.chr, start, end);
1122
+ this.closeMenus();
1123
+ this.hideMessages();
1124
+ }
1125
+ },
1126
+
1127
+ getCoords: function () {
1128
+ var match = (decodeURIComponent(this.useHash ? window.location.hash.replace(/^#/, '?') || window.location.search : window.location.search) + '&').match(this.paramRegex);
1129
+ var coords = {};
1130
+ var i = 0;
1131
+
1132
+ if (!match) {
1133
+ return this.initialLocation;
1134
+ }
1135
+
1136
+ match = match.slice(2, -1);
1137
+
1138
+ $.each(this.urlParamTemplate.split('__'), function () {
1139
+ var tmp = this.match(/^(CHR|START|END)$/);
1140
+
1141
+ if (tmp) {
1142
+ coords[tmp[1].toLowerCase()] = tmp[1] === 'CHR' ? match[i++] : parseInt(match[i++], 10);
1143
+ }
1144
+ });
1145
+
1146
+ return coords.chr && coords.start && coords.end && (this.genome ? this.genome[coords.chr] : true) ? coords : this.initialLocation;
1147
+ },
1148
+
1149
+ getQueryString: function () {
1150
+ var location = this.urlParamTemplate
1151
+ .replace('__CHR__', this.chr)
1152
+ .replace('__START__', this.start)
1153
+ .replace('__END__', this.end);
1154
+
1155
+ var currentLocation = decodeURIComponent(this.useHash ? window.location.hash.replace(/^#/, '?') : window.location.search) + '&';
1156
+
1157
+ var newLocation = (
1158
+ currentLocation.match(this.paramRegex)
1159
+ ? currentLocation.replace(this.paramRegex, '$1' + location + '$5').slice(0, -1)
1160
+ : currentLocation + location
1161
+ );
1162
+
1163
+ if (this.useHash) {
1164
+ newLocation = newLocation.replace(/^[&?]/, '');
1165
+ } else if (newLocation.indexOf('?') !== 0) {
1166
+ newLocation = '?' + newLocation.replace(/^&/, '');
1167
+ }
1168
+
1169
+ return newLocation;
1170
+ },
1171
+
1172
+ getChromosomeSize: function (chr) {
1173
+ return chr && this.genome && this.genome[chr] ? this.genome[chr].size : this.chromosomeSize;
1174
+ },
1175
+
1176
+ supported: function () {
1177
+ var el = document.createElement('canvas');
1178
+ return !!(el.getContext && el.getContext('2d'));
1179
+ },
1180
+
1181
+ die: function (error, el) {
1182
+ if (el && el.length) {
1183
+ el.html(error);
1184
+ } else {
1185
+ throw error;
1186
+ }
1187
+
1188
+ this.failed = true;
1189
+ },
1190
+
1191
+ menuTemplate: $(
1192
+ '<div class="gv-menu">' +
1193
+ '<div class="gv-close gv-menu-button fas fa-times-circle"></div>' +
1194
+ '<div class="gv-menu-loading">Loading...</div>' +
1195
+ '<div class="gv-menu-error">An error has occurred</div>' +
1196
+ '<div class="gv-menu-content">' +
1197
+ '<div class="gv-title"></div>' +
1198
+ '<table class="gv-focus-highlight">' +
1199
+ '<tr>' +
1200
+ '<td><a class="gv-focus" href="#">Focus here</a></td>' +
1201
+ '<td><a class="gv-highlight" href="#">Highlight this feature</a></td>' +
1202
+ '</tr>' +
1203
+ '</table>' +
1204
+ '<table></table>' +
1205
+ '</div>' +
1206
+ '</div>'
1207
+ ).on('click', function (e) {
1208
+ if ($(e.target).hasClass('gv-close')) {
1209
+ $(this).fadeOut('fast', function () {
1210
+ var data = $(this).data();
1211
+
1212
+ if (data.track) {
1213
+ data.track.prop('menus', data.track.prop('menus').not(this));
1214
+ }
1215
+
1216
+ data.browser.menus = data.browser.menus.not(this);
1217
+ });
1218
+ }
1219
+ }),
1220
+
1221
+ makeMenu: function (features, event, track) {
1222
+ if (!features) {
1223
+ return false;
1224
+ }
1225
+
1226
+ if (!Array.isArray(features)) {
1227
+ features = [ features ];
1228
+ }
1229
+
1230
+ if (features.length === 0) {
1231
+ return false;
1232
+ }
1233
+
1234
+ if (features.length === 1) {
1235
+ return this.makeFeatureMenu(features[0], event, track);
1236
+ }
1237
+
1238
+ var browser = this;
1239
+ var menu = this.menuTemplate.clone(true).data({ browser: this });
1240
+ var contentEl = $('.gv-menu-content', menu).addClass('gv-menu-content-first');
1241
+ var table = $('table:not(.gv-focus-highlight)', contentEl);
1242
+
1243
+ $('.gv-focus-highlight, .gv-menu-loading', menu).remove();
1244
+ $('.gv-title', menu).html(features.length + ' features');
1245
+
1246
+ $.each(
1247
+ track ? track.model.sortFeatures(features) : features.sort(function (a, b) { return a.start - b.start; }),
1248
+ function (i, feature) {
1249
+ var location = feature.chr + ':' + feature.start + (feature.end === feature.start ? '' : '-' + feature.end);
1250
+ var title = feature.menuLabel || feature.name || (Array.isArray(feature.label) ? feature.label.join(' ') : feature.label) || (feature.id + '');
1251
+
1252
+ $('<a href="#">').html(title.match(location) ? title : (location + ' ' + title)).on('click', function (e) {
1253
+ browser.makeFeatureMenu(feature, e, track);
1254
+ return false;
1255
+ }).appendTo($('<td>').appendTo($('<tr>').appendTo(table)));
1256
+ }
1257
+ );
1258
+
1259
+ $('<div class="gv-menu-scroll-wrapper">').append(table).appendTo(contentEl);
1260
+
1261
+ menu.appendTo(this.superContainer || this.container).show();
1262
+
1263
+ if (event) {
1264
+ menu.css({ left: 0, top: 0 }).position({ of: event, my: 'left top', collision: 'flipfit' });
1265
+ }
1266
+
1267
+ this.menus = this.menus.add(menu);
1268
+
1269
+ if (track) {
1270
+ track.prop('menus', track.prop('menus').add(menu));
1271
+ }
1272
+
1273
+ return menu;
1274
+ },
1275
+
1276
+ makeFeatureMenu: function (feature, e, track) {
1277
+ var browser = this;
1278
+ var container = this.superContainer || this.container;
1279
+ var menu, content, loading, getMenu, isDeferred, i, j, el, chr, start, end, linkData, key, columns, colspan;
1280
+
1281
+ function focus() {
1282
+ var data = $(this).data();
1283
+ var length = data.end - data.start + 1;
1284
+ var context = Math.max(Math.round(length / 4), 25);
1285
+
1286
+ browser.moveTo(data.chr, data.start - context, data.end + context, true);
1287
+
1288
+ return false;
1289
+ }
1290
+
1291
+ function highlight() {
1292
+ browser.addHighlight($(this).data());
1293
+ return false;
1294
+ }
1295
+
1296
+ if (!feature.menuEl || feature.menuEl.data('hasErrored') === true) {
1297
+ menu = browser.menuTemplate.clone(true).data({ browser: browser, feature: feature });
1298
+ content = $('.gv-menu-content', menu).remove();
1299
+ loading = $('.gv-menu-loading', menu);
1300
+
1301
+ try {
1302
+ getMenu = track ? track.controller.populateMenu(feature) : feature;
1303
+ } catch (error) {
1304
+ getMenu = $.Deferred().reject(error);
1305
+ menu.data('hasErrored', true);
1306
+ }
1307
+
1308
+ isDeferred = typeof getMenu === 'object' && typeof getMenu.promise === 'function';
1309
+
1310
+ if (!isDeferred) {
1311
+ loading.hide();
1312
+ }
1313
+
1314
+ $.when(getMenu).done(function (properties) {
1315
+ var table;
1316
+
1317
+ if (!Array.isArray(properties)) {
1318
+ properties = [ properties ];
1319
+ }
1320
+
1321
+ for (i = 0; i < properties.length; i++) {
1322
+ table = '';
1323
+ el = content.clone().addClass(i ? '' : 'gv-menu-content-first').appendTo(menu);
1324
+ chr = typeof properties[i].chr !== 'undefined' ? properties[i].chr : feature.chr;
1325
+ start = parseInt(typeof properties[i].start !== 'undefined' ? properties[i].start : feature.start, 10);
1326
+ end = parseInt(typeof properties[i].end !== 'undefined' ? properties[i].end : feature.end, 10);
1327
+ columns = Math.max.apply(Math, $.map(properties[i], function (v) { return Array.isArray(v) ? v.length : 1; }));
1328
+
1329
+ $('.gv-title', el)[properties[i].title ? 'html' : 'remove'](properties[i].title);
1330
+
1331
+ if (track && start && end && !browser.isStatic) {
1332
+ linkData = { chr: chr, start: start, end: Math.max(end, start), label: feature.label || (properties[i].title || '').replace(/<[^>]+>/g, ''), color: feature.color };
1333
+
1334
+ $('.gv-focus', el).data(linkData).on('click', focus);
1335
+ $('.gv-highlight', el).data(linkData).on('click', highlight);
1336
+ } else {
1337
+ $('.gv-focus-highlight', el).remove();
1338
+ }
1339
+
1340
+ for (key in properties[i]) {
1341
+ if (/^start|end$/.test(key) && properties[i][key] === false) {
1342
+ continue;
1343
+ }
1344
+
1345
+ if (key !== 'title') {
1346
+ colspan = properties[i][key] === '' ? ' colspan="' + (columns + 1) + '"' : '';
1347
+ table += '<tr><td' + colspan + '>' + key + '</td>';
1348
+
1349
+ if (!colspan) {
1350
+ if (Array.isArray(properties[i][key])) {
1351
+ for (j = 0; j < properties[i][key].length; j++) {
1352
+ table += '<td>' + properties[i][key][j] + '</td>';
1353
+ }
1354
+ } else if (columns === 1) {
1355
+ table += '<td>' + properties[i][key] + '</td>';
1356
+ } else {
1357
+ table += '<td colspan="' + columns + '">' + properties[i][key] + '</td>';
1358
+ }
1359
+ }
1360
+
1361
+ table += '</tr>';
1362
+ }
1363
+ }
1364
+
1365
+ $('table:not(.gv-focus-highlight)', el)[table ? 'html' : 'remove'](table);
1366
+ }
1367
+
1368
+ if (isDeferred) {
1369
+ loading.hide();
1370
+ }
1371
+ }).fail(function (error) {
1372
+ loading.hide();
1373
+ menu.data('hasErrored', true);
1374
+ $('.gv-menu-error', menu).css('display', 'block');
1375
+ console.error(error); // eslint-disable-line no-console
1376
+ });
1377
+
1378
+ if (track) {
1379
+ menu.addClass(track.id).data('track', track);
1380
+ }
1381
+
1382
+ feature.menuEl = menu.appendTo(container);
1383
+ } else {
1384
+ feature.menuEl.appendTo(container); // Move the menu to the end of the container again, so that it will always be on top of other menus
1385
+ }
1386
+
1387
+ browser.menus = browser.menus.add(feature.menuEl);
1388
+
1389
+ if (track) {
1390
+ track.prop('menus', track.prop('menus').add(feature.menuEl));
1391
+ }
1392
+
1393
+ feature.menuEl.show(); // Must show before positioning, else position will be wrong
1394
+
1395
+ if (e) {
1396
+ feature.menuEl.css({ left: 0, top: 0 }).position({ of: e, my: 'left top', collision: 'flipfit' });
1397
+ }
1398
+
1399
+ return feature.menuEl;
1400
+ },
1401
+
1402
+ closeMenus: function (obj) {
1403
+ obj = obj || this;
1404
+
1405
+ obj.menus.filter(':visible').children('.gv-close').trigger('click');
1406
+ obj.menus = $();
1407
+ },
1408
+
1409
+ hideMessages: function () {
1410
+ if (this.autoHideMessages) {
1411
+ this.wrapper.find('.gv-message-container').addClass('gv-collapsed');
1412
+ }
1413
+ },
1414
+
1415
+ getSelectorPosition: function () {
1416
+ var left = this.selector.position().left;
1417
+ var width = this.selector.outerWidth(true);
1418
+ var start = Math.round(left / this.scale) + this.start;
1419
+ var end = Math.round((left + width) / this.scale) + this.start - 1;
1420
+ end = end <= start ? start : end;
1421
+
1422
+ return { start: start, end: end, left: left, width: width };
1423
+ },
1424
+
1425
+ addHighlight: function (highlight) {
1426
+ this.addHighlights([ highlight ]);
1427
+ },
1428
+
1429
+ addHighlights: function (highlights) {
1430
+ if (!this.tracksById.highlights) {
1431
+ this.addTrack(Genoverse.Track.HighlightRegion);
1432
+ }
1433
+
1434
+ this.tracksById.highlights.addHighlights(highlights);
1435
+ },
1436
+
1437
+ on: function (events, obj, fn, once) {
1438
+ var browser = this;
1439
+ var eventMap = {};
1440
+ var i, j, f, fnString, event;
1441
+
1442
+ function makeEventMap(types, handler) {
1443
+ types = types.split(' ');
1444
+
1445
+ for (j = 0; j < types.length; j++) {
1446
+ eventMap[types[j]] = (eventMap[types[j]] || []).concat(handler);
1447
+ }
1448
+ }
1449
+
1450
+ function makeFnString(func) {
1451
+ return func.toString();
1452
+ }
1453
+
1454
+ function compare(func) {
1455
+ f = func.toString();
1456
+
1457
+ for (j = 0; j < fnString.length; j++) {
1458
+ if (f === fnString[j]) {
1459
+ return true;
1460
+ }
1461
+ }
1462
+ }
1463
+
1464
+ if (typeof events === 'object') {
1465
+ for (i in events) {
1466
+ makeEventMap(i, events[i]);
1467
+ }
1468
+
1469
+ obj = obj || this;
1470
+ } else {
1471
+ if (typeof fn === 'undefined') {
1472
+ fn = obj;
1473
+ obj = this;
1474
+ }
1475
+
1476
+ makeEventMap(events, fn);
1477
+ }
1478
+
1479
+ var type = obj instanceof Genoverse.Track || obj === 'tracks' ? 'tracks' : 'browser';
1480
+
1481
+ for (i in eventMap) {
1482
+ event = i + (once ? '.once' : '');
1483
+
1484
+ browser.events[type][event] = browser.events[type][event] || [];
1485
+ fnString = $.map(eventMap[i], makeFnString);
1486
+
1487
+ if (!$.grep(browser.events[type][event], compare).length) {
1488
+ browser.events[type][event].push.apply(browser.events[type][event], eventMap[i]);
1489
+ }
1490
+ }
1491
+ },
1492
+
1493
+ once: function (events, obj, fn) {
1494
+ this.on(events, obj, fn, true);
1495
+ },
1496
+
1497
+ destroy: function () {
1498
+ this.onTracks('destructor');
1499
+ (this.superContainer || this.container).empty();
1500
+
1501
+ if (this.zoomInHighlight) {
1502
+ this.zoomInHighlight.add(this.zoomOutHighlight).remove();
1503
+ }
1504
+
1505
+ this.container.off(this.eventNamespace);
1506
+ $(window).add(document).off(this.eventNamespace);
1507
+
1508
+ for (var key in this) {
1509
+ delete this[key];
1510
+ }
1511
+ }
1512
+ }, $.extend({
1513
+ id : 0,
1514
+ ready : $.Deferred(),
1515
+ origin : (($('script[src]').filter(function () { return /\/(?:Genoverse|genoverse\.min.*)\.js$/.test(this.src); }).attr('src') || '').match(/(.*)js\/\w+/) || [])[1] || '',
1516
+ Genomes : {},
1517
+ Plugins : {},
1518
+
1519
+ wrapFunctions: function (obj) {
1520
+ for (var key in obj) {
1521
+ if (typeof obj[key] === 'function' && typeof obj[key].ancestor !== 'function' && !key.match(/^(base|extend|constructor|on|once|prop|loadPlugins|loadGenome)$/)) {
1522
+ Genoverse.functionWrap(key, obj);
1523
+ }
1524
+ }
1525
+ },
1526
+
1527
+ /**
1528
+ * functionWrap - wraps event handlers and adds debugging functionality
1529
+ */
1530
+ functionWrap: function (key, obj) {
1531
+ obj.functions = obj.functions || {};
1532
+
1533
+ if (obj.functions[key] || /^(before|after)/.test(key)) {
1534
+ return;
1535
+ }
1536
+
1537
+ var func = key.substring(0, 1).toUpperCase() + key.substring(1);
1538
+ var isBrowser = obj instanceof Genoverse;
1539
+ var mainObj = isBrowser || obj instanceof Genoverse.Track ? obj : obj.track;
1540
+ var events = isBrowser ? obj.events.browser : obj.browser.events.tracks;
1541
+ var debug;
1542
+
1543
+ if (mainObj.debug) {
1544
+ debug = [ isBrowser ? 'Genoverse' : mainObj.id || mainObj.name || 'Track' ];
1545
+
1546
+ if (!isBrowser && obj !== mainObj) {
1547
+ debug.push(obj instanceof Genoverse.Track.Controller ? 'Controller' : obj instanceof Genoverse.Track.Model ? 'Model' : 'View');
1548
+ }
1549
+
1550
+ debug = debug.concat(key).join('.');
1551
+ }
1552
+
1553
+ obj.functions[key] = obj[key];
1554
+
1555
+ obj[key] = function () {
1556
+ var args = [].slice.call(arguments);
1557
+ var currentConfig = (this._currentConfig || (this.track ? this.track._currentConfig : {}) || {}).func;
1558
+ var rtn;
1559
+
1560
+ // Debugging functionality
1561
+ // Enabled by "debug": true || 'time' || { functionName: true, ...} option
1562
+ if (mainObj.debug === true) { // if "debug": true, simply log function call
1563
+ console.log(debug); // eslint-disable-line no-console
1564
+ } else if (mainObj.debug === 'time' || (typeof mainObj.debug === 'object' && mainObj.debug[key])) { // if debug: 'time' || { functionName: true, ...}, log function time
1565
+ console.time('time: ' + debug); // eslint-disable-line no-console
1566
+ }
1567
+
1568
+ function trigger(when) {
1569
+ var once = events[when + func + '.once'] || [];
1570
+ var funcs = (events[when + func] || []).concat(once, typeof mainObj[when + func] === 'function' ? mainObj[when + func] : []);
1571
+
1572
+ if (once.length) {
1573
+ delete events[when + func + '.once'];
1574
+ }
1575
+
1576
+ for (var i = 0; i < funcs.length; i++) {
1577
+ funcs[i].apply(this, args);
1578
+ }
1579
+ }
1580
+
1581
+ trigger.call(this, 'before');
1582
+
1583
+ if (currentConfig && currentConfig[key]) {
1584
+ // override to add a value for this.base
1585
+ rtn = function () {
1586
+ this.base = this.functions[key] || function () {};
1587
+ return currentConfig[key].apply(this, arguments);
1588
+ }.apply(this, args);
1589
+ } else {
1590
+ rtn = this.functions[key].apply(this, args);
1591
+ }
1592
+
1593
+ trigger.call(this, 'after');
1594
+
1595
+ if (mainObj.debug === 'time' || (typeof mainObj.debug === 'object' && mainObj.debug[key])) {
1596
+ console.timeEnd('time: ' + debug); // eslint-disable-line no-console
1597
+ }
1598
+
1599
+ return rtn;
1600
+ };
1601
+ },
1602
+
1603
+ getAllTrackTypes: function (namespace, n) {
1604
+ namespace = namespace || Genoverse.Track;
1605
+
1606
+ if (n) {
1607
+ namespace = namespace[n];
1608
+ }
1609
+
1610
+ if (!namespace) {
1611
+ return [];
1612
+ }
1613
+
1614
+ var trackTypes = {};
1615
+
1616
+ $.each(namespace, function (type, func) {
1617
+ if (typeof func === 'function' && !Base[type] && !/^(Controller|Model|View)$/.test(type)) {
1618
+ $.each(Genoverse.getAllTrackTypes(namespace, type), function (subtype, fn) {
1619
+ if (typeof fn === 'function') {
1620
+ trackTypes[type + '.' + subtype] = fn;
1621
+ }
1622
+ });
1623
+
1624
+ trackTypes[type] = func;
1625
+ }
1626
+ });
1627
+
1628
+ return trackTypes;
1629
+ },
1630
+
1631
+ getTrackNamespace: function (track) {
1632
+ var trackTypes = Genoverse.getAllTrackTypes();
1633
+ var namespaces = $.map(trackTypes, function (constructor, name) { return track === constructor || track.prototype instanceof constructor ? name : null; }); // Find all namespaces which this track could be
1634
+ var j = namespaces.length;
1635
+ var i;
1636
+
1637
+ // Find the most specific namespace for this track - the one which isn't a parent of any other namespaces this track could be
1638
+ while (namespaces.length > 1) {
1639
+ for (i = 0; i < namespaces.length - 1; i++) {
1640
+ if (trackTypes[namespaces[i]].prototype instanceof trackTypes[namespaces[i + 1]]) {
1641
+ namespaces.splice(i + 1, 1);
1642
+ break;
1643
+ } else if (trackTypes[namespaces[i + 1]].prototype instanceof trackTypes[namespaces[i]]) {
1644
+ namespaces.splice(i, 1);
1645
+ break;
1646
+ }
1647
+ }
1648
+
1649
+ if (j-- < 0) {
1650
+ break; // Stop infinite loop if something went really wrong
1651
+ }
1652
+ }
1653
+
1654
+ return namespaces[0];
1655
+ }
1656
+ }, window.genoverseLoadOptions || {}));
1657
+
1658
+ $(function () {
1659
+ var cssReady = $.Deferred();
1660
+ var fontAwesomeReady = $.Deferred();
1661
+
1662
+ if (Genoverse.loadCSS === false || $('link[href^="' + Genoverse.origin + 'css/genoverse.css"]').length) {
1663
+ cssReady.resolve();
1664
+ } else {
1665
+ $('<link href="' + Genoverse.origin + 'css/genoverse.css" rel="stylesheet">').prependTo('body').on('load', cssReady.resolve);
1666
+ }
1667
+
1668
+ if (Genoverse.loadFontAwesome === false || $('link[href^="' + Genoverse.origin + 'css/font-awesome.css"]').length) {
1669
+ fontAwesomeReady.resolve();
1670
+ } else {
1671
+ $('<link href="' + Genoverse.origin + 'css/font-awesome.css" rel="stylesheet">').prependTo('body').on('load', fontAwesomeReady.resolve);
1672
+ }
1673
+
1674
+ $.when(cssReady, fontAwesomeReady).done(Genoverse.ready.resolve);
1675
+ });
1676
+
1677
+ window.Genoverse = Genoverse;
1678
+
1679
+ if (typeof module === 'object' && typeof module.exports === 'object') {
1680
+ module.exports = Genoverse;
1681
+ }