genoverse 3.2.0 → 4.0.1

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