igv 3.2.6 → 3.3.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.
package/dist/igv.js CHANGED
@@ -12880,6 +12880,20 @@
12880
12880
 
12881
12881
  async loadAll() {
12882
12882
 
12883
+
12884
+ const pushChromosome = (current, order) => {
12885
+ const length = current.length || (current.offset + current.seq.length);
12886
+ if (!chrNameSet.has(current.chr)) {
12887
+ this.sequences.set(current.chr, []);
12888
+ this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12889
+ chrNameSet.add(current.chr);
12890
+ } else {
12891
+ const c = this.chromosomes.get(current.chr);
12892
+ c.bpLength = Math.max(c.bpLength, length);
12893
+ }
12894
+ this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
12895
+ };
12896
+
12883
12897
  let data;
12884
12898
  if (isDataURL(this.fastaURL)) {
12885
12899
  let bytes = decodeDataURI$1(this.fastaURL);
@@ -12893,70 +12907,62 @@
12893
12907
 
12894
12908
  const chrNameSet = new Set();
12895
12909
  const lines = splitLines$2(data);
12896
- const len = lines.length;
12897
- let lineNo = 0;
12898
12910
  let order = 0;
12899
- let nextLine;
12900
12911
  let current = {};
12901
- while (lineNo < len) {
12902
- nextLine = lines[lineNo++].trim();
12912
+ for (let nextLine of lines) {
12903
12913
  if (nextLine.startsWith("#") || nextLine.length === 0) ; else if (nextLine.startsWith(">")) {
12904
12914
  // Start the next sequence
12905
- if (current && current.seq) {
12906
- pushChromosome.call(this, current, order++);
12915
+ if (current.seq && current.seq.length > 0) {
12916
+ pushChromosome(current, order++);
12907
12917
  }
12918
+ current.seq = "";
12908
12919
 
12909
12920
  const parts = nextLine.substr(1).split(/\s+/);
12910
12921
 
12911
- // Check for samtools style locus string. This is not perfect, and could fail on weird sequence names
12912
- const nameParts = parts[0].split(':');
12913
- current.chr = nameParts[0];
12914
- current.seq = "";
12915
- current.offset = 0;
12916
- if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12917
- const locusParts = nameParts[1].split('-');
12918
- if (locusParts.length === 2 &&
12919
- /^[0-9]+$/.test(locusParts[0]) &&
12920
- /^[0-9]+$/.test(locusParts[1])) ;
12921
- const from = Number.parseInt(locusParts[0]);
12922
- const to = Number.parseInt(locusParts[1]);
12923
- if (to > from) { // TODO this should be an error
12922
+
12923
+ // Check for @len= token, which is a non-standard extension supporting igv-reports.
12924
+ if (nextLine.includes("@len=")) {
12925
+ const nameParts = parts[0].split(':');
12926
+ current.chr = nameParts[0];
12927
+ if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12928
+
12929
+ const locusParts = nameParts[1].split('-');
12930
+ if (locusParts.length === 2 &&
12931
+ /^[0-9]+$/.test(locusParts[0]) &&
12932
+ /^[0-9]+$/.test(locusParts[1])) ;
12933
+ const from = Number.parseInt(locusParts[0]);
12934
+ Number.parseInt(locusParts[1]);
12924
12935
  current.offset = from - 1;
12925
- }
12926
12936
 
12927
- // Check for chromosome length token
12928
- if (parts.length > 1 && parts[1].startsWith("@len=")) {
12929
- try {
12930
- current.length = parseInt(parts[1].trim().substring(5));
12931
- } catch (e) {
12937
+ // Check for chromosome length token
12938
+ if (parts.length > 1 && parts[1].startsWith("@len=")) {
12939
+ try {
12940
+ current.length = parseInt(parts[1].trim().substring(5));
12941
+ } catch (e) {
12942
+ current.length = undefined;
12943
+ console.error(`Error parsing sequence length for ${nextLine}`);
12944
+ }
12945
+ } else {
12932
12946
  current.length = undefined;
12933
- console.error(`Error parsing sequence length for ${nextLine}`);
12934
12947
  }
12935
- } else {
12936
- current.length = undefined;
12937
12948
  }
12949
+ } else {
12950
+ // No special tokens, a standard FASTA header
12951
+ current.chr = parts[0];
12952
+ current.offset = 0;
12938
12953
  }
12954
+
12939
12955
  } else {
12956
+ // Not a header or comment, so it must be sequence data
12940
12957
  current.seq += nextLine;
12941
12958
  }
12942
- // add last seq
12943
- if (current && current.seq) {
12944
- pushChromosome.call(this, current, order);
12945
- }
12946
12959
  }
12947
12960
 
12948
- function pushChromosome(current, order) {
12949
- const length = current.length || (current.offset + current.seq.length);
12950
- if (!chrNameSet.has(current.chr)) {
12951
- this.sequences.set(current.chr, []);
12952
- this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12953
- chrNameSet.add(current.chr);
12954
- } else {
12955
- const c = this.chromosomes.get(current.chr);
12956
- c.bpLength = Math.max(c.bpLength, length);
12957
- }
12958
- this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
12961
+ // Handle the last sequence
12962
+ if (current.seq && current.seq.length > 0) {
12963
+ pushChromosome(current, order);
12959
12964
  }
12965
+
12960
12966
  }
12961
12967
 
12962
12968
  /**
@@ -13721,17 +13727,22 @@
13721
13727
 
13722
13728
  static magic = 2026540177
13723
13729
  littleEndian = true
13730
+ type = 'BPTree' // Either BPTree or BPChromTree
13724
13731
  nodeCache = new Map()
13725
13732
 
13726
- static async loadBpTree(path, config, startOffset) {
13727
- const bpTree = new BPTree(path, config, startOffset);
13733
+ static async loadBpTree(path, config, startOffset, type, loader) {
13734
+ const bpTree = new BPTree(path, config, startOffset, type, loader);
13728
13735
  return bpTree.init()
13729
13736
  }
13730
13737
 
13731
- constructor(path, config, startOffset) {
13738
+ constructor(path, config, startOffset, type, loader) {
13732
13739
  this.path = path;
13733
13740
  this.config = config;
13734
13741
  this.startOffset = startOffset;
13742
+ if(type) {
13743
+ this.type = type;
13744
+ }
13745
+ this.loader = loader || igvxhr;
13735
13746
  }
13736
13747
 
13737
13748
  async init() {
@@ -13757,69 +13768,22 @@
13757
13768
  return this
13758
13769
  }
13759
13770
 
13760
- async search(term) {
13761
-
13771
+ getItemCount() {
13762
13772
  if(!this.header) {
13763
- await this.init();
13773
+ throw Error("Header not initialized")
13764
13774
  }
13775
+ return this.header.itemCount
13776
+ }
13765
13777
 
13766
- const {keySize, valSize} = this.header;
13778
+ async search(term) {
13767
13779
 
13768
- if (!(valSize === 16 || valSize === 8)) {
13769
- throw Error(`Unexpected valSize ${valSize}`)
13780
+ if(!this.header) {
13781
+ await this.init();
13770
13782
  }
13771
13783
 
13772
- const readTreeNode = async (offset) => {
13773
-
13774
- if (this.nodeCache.has(offset)) {
13775
- return this.nodeCache.get(offset)
13776
- } else {
13777
-
13778
- let binaryParser = await this.#getParserFor(offset, 4);
13779
- const type = binaryParser.getByte();
13780
- binaryParser.getByte();
13781
- const count = binaryParser.getUShort();
13782
- const items = [];
13783
-
13784
- if (type === 1) {
13785
- // Leaf node
13786
- const size = count * (keySize + valSize);
13787
- binaryParser = await this.#getParserFor(offset + 4, size);
13788
- for (let i = 0; i < count; i++) {
13789
- const key = binaryParser.getFixedLengthString(keySize);
13790
- const offset = binaryParser.getLong();
13791
-
13792
- let value;
13793
- if (valSize === 16) {
13794
- const length = binaryParser.getInt();
13795
- binaryParser.getInt();
13796
- value = {offset, length};
13797
- } else {
13798
- value = {offset};
13799
- }
13800
- items.push({key, value});
13801
- }
13802
- } else {
13803
- // Non leaf node
13804
- const size = count * (keySize + 8);
13805
- binaryParser = await this.#getParserFor(offset + 4, size);
13806
-
13807
- for (let i = 0; i < count; i++) {
13808
- const key = binaryParser.getFixedLengthString(keySize);
13809
- const offset = binaryParser.getLong();
13810
- items.push({key, offset});
13811
- }
13812
- }
13813
-
13814
- const node = {type, count, items};
13815
- this.nodeCache.set(offset, node);
13816
- return node
13817
- }
13818
- };
13819
-
13820
13784
  const walkTreeNode = async (offset) => {
13821
13785
 
13822
- const node = await readTreeNode(offset);
13786
+ const node = await this.readTreeNode(offset);
13823
13787
 
13824
13788
  if (node.type === 1) {
13825
13789
  // Leaf node
@@ -13850,9 +13814,66 @@
13850
13814
  return walkTreeNode(this.header.nodeOffset)
13851
13815
  }
13852
13816
 
13817
+ async readTreeNode (offset) {
13818
+
13819
+ if (this.nodeCache.has(offset)) {
13820
+ return this.nodeCache.get(offset)
13821
+ } else {
13822
+ let binaryParser = await this.#getParserFor(offset, 4);
13823
+ const type = binaryParser.getByte();
13824
+ binaryParser.getByte();
13825
+ const count = binaryParser.getUShort();
13826
+ const items = [];
13827
+
13828
+ const {keySize, valSize} = this.header;
13829
+
13830
+ if (type === 1) {
13831
+ // Leaf node
13832
+ const size = count * (keySize + valSize);
13833
+ binaryParser = await this.#getParserFor(offset + 4, size);
13834
+ for (let i = 0; i < count; i++) {
13835
+ const key = binaryParser.getFixedLengthString(keySize);
13836
+ let value;
13837
+ if(this.type === 'BPChromTree') {
13838
+ const id = binaryParser.getInt();
13839
+ const size = binaryParser.getInt();
13840
+ value = {id, size};
13841
+ } else {
13842
+ const offset = binaryParser.getLong();
13843
+ if (valSize === 16) {
13844
+ const length = binaryParser.getLong();
13845
+ value = {offset, length};
13846
+ } else {
13847
+ value = {offset};
13848
+ }
13849
+ }
13850
+ items.push({key, value});
13851
+ }
13852
+ } else {
13853
+ // Non leaf node
13854
+ const size = count * (keySize + 8);
13855
+ binaryParser = await this.#getParserFor(offset + 4, size);
13856
+
13857
+ for (let i = 0; i < count; i++) {
13858
+ const key = binaryParser.getFixedLengthString(keySize);
13859
+ const offset = binaryParser.getLong();
13860
+ items.push({key, offset});
13861
+ }
13862
+ }
13863
+
13864
+ const node = {type, count, items};
13865
+ this.nodeCache.set(offset, node);
13866
+ return node
13867
+ }
13868
+ }
13869
+
13853
13870
  async #getParserFor(start, size) {
13854
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13855
- return new BinaryParser$1(new DataView(data), this.littleEndian)
13871
+ try {
13872
+ const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13873
+ return new BinaryParser$1(new DataView(data), this.littleEndian)
13874
+ } catch (e) {
13875
+ console.error(e);
13876
+ }
13856
13877
  }
13857
13878
 
13858
13879
  }
@@ -13878,6 +13899,7 @@
13878
13899
 
13879
13900
  littleEndian
13880
13901
  metaIndex = new Map()
13902
+ chromosomeNames
13881
13903
 
13882
13904
  constructor(config) {
13883
13905
  this.url = config.twoBitURL || config.fastaURL;
@@ -13970,9 +13992,16 @@
13970
13992
  return sequenceBases
13971
13993
  }
13972
13994
 
13995
+ /**
13996
+ * Read the internal index of the 2bit file. This is a list of sequence names and their offsets in the file.
13997
+ *
13998
+ * @returns {Promise<Map<any, any>>}
13999
+ * @private
14000
+ */
13973
14001
  async _readIndex() {
13974
14002
 
13975
14003
  const index = new Map();
14004
+ this.chromosomeNames = [];
13976
14005
 
13977
14006
  const loadRange = {start: 0, size: 64};
13978
14007
  let arrayBuffer = await igvxhr.loadArrayBuffer(this.url, {range: loadRange});
@@ -14025,6 +14054,8 @@
14025
14054
  index.set(name, offset);
14026
14055
 
14027
14056
  estNameLength = Math.floor(estNameLength * (i / (i + 1)) + name.length / (i + 1));
14057
+
14058
+ this.chromosomeNames.push(name);
14028
14059
  }
14029
14060
  return index
14030
14061
  }
@@ -22170,84 +22201,6 @@
22170
22201
  return Math.ceil(Math.log(Math.max(0, (chrSize / (bpPerPixel * 700)))) / log2)
22171
22202
  }
22172
22203
 
22173
- /**
22174
- * A ChromTree parses a UCSC bigbed/bigwig "chromosomeTree" section of the header to produce 2 maps,
22175
- * (1) ID -> chromosome names, and its
22176
- * (2) chromsome name -> ID
22177
- *
22178
- * Both maps are needed by IGV
22179
- *
22180
- * The chromosome tree is a B+ index, but is located continguously in memory in the header section of the file. This
22181
- * makes it feasible to parse the whole tree with data from a single fetch. In the end the tree is discarded
22182
- * leaving only the mapps.
22183
- */
22184
- class ChromTree {
22185
-
22186
- constructor(header, nameToID, valueToKey, sumLengths) {
22187
- this.header = header;
22188
- this.nameToId = nameToID;
22189
- this.idToName = valueToKey;
22190
- this.sumLengths = sumLengths;
22191
- }
22192
-
22193
- static parseTree(binaryParser, startOffset, genome = false) {
22194
- {
22195
- const magic = binaryParser.getInt();
22196
- const blockSize = binaryParser.getInt();
22197
- const keySize = binaryParser.getInt();
22198
- const valSize = binaryParser.getInt();
22199
- const itemCount = binaryParser.getLong();
22200
- const reserved = binaryParser.getLong();
22201
-
22202
- const header = {magic, blockSize, keySize, valSize, itemCount, reserved};
22203
- const nameToId = new Map();
22204
- const idToName = [];
22205
- let sumLengths = 0;
22206
- const readTreeNode = (offset) => {
22207
- if (offset >= 0) binaryParser.position = offset;
22208
- const type = binaryParser.getByte();
22209
- binaryParser.getByte();
22210
- const count = binaryParser.getUShort();
22211
-
22212
- if (type === 1) {
22213
- // Leaf node
22214
- for (let i = 0; i < count; i++) {
22215
- let key = binaryParser.getFixedLengthString(keySize);
22216
- let value;
22217
- if (valSize === 8) {
22218
- value = binaryParser.getInt();
22219
- const chromSize = binaryParser.getInt();
22220
- sumLengths += chromSize;
22221
- if (genome) key = genome.getChromosomeName(key); // Translate to canonical chr name
22222
- nameToId.set(key, value);
22223
- idToName[value] = key;
22224
-
22225
- } else {
22226
- throw Error(`Unexpected "valSize" value in chromosome tree. Expected 8, actual value is ${valSize}`)
22227
- }
22228
- }
22229
- } else {
22230
- // non-leaf
22231
- for (let i = 0; i < count; i++) {
22232
- binaryParser.getFixedLengthString(keySize);
22233
- const childOffset = binaryParser.getLong();
22234
- const bufferOffset = childOffset - startOffset;
22235
- const currOffset = binaryParser.position;
22236
- readTreeNode(bufferOffset);
22237
- binaryParser.position = currOffset;
22238
- }
22239
- }
22240
- };
22241
-
22242
- // Recursively walk tree to populate dictionary
22243
- readTreeNode( -1);
22244
-
22245
- return new ChromTree(header, nameToId, idToName, sumLengths)
22246
- }
22247
- }
22248
-
22249
- }
22250
-
22251
22204
  const RPTREE_HEADER_SIZE = 48;
22252
22205
  const RPTREE_NODE_LEAF_ITEM_SIZE = 32; // leaf item size
22253
22206
  const RPTREE_NODE_CHILD_ITEM_SIZE = 24; // child item size
@@ -22258,11 +22211,12 @@
22258
22211
  littleEndian = true
22259
22212
  nodeCache = new Map()
22260
22213
 
22261
- constructor(path, config, startOffset) {
22214
+ constructor(path, config, startOffset, loader) {
22262
22215
 
22263
22216
  this.path = path;
22264
22217
  this.config = config;
22265
22218
  this.startOffset = startOffset;
22219
+ this.loader = loader || igvxhr;
22266
22220
  }
22267
22221
 
22268
22222
 
@@ -22306,7 +22260,7 @@
22306
22260
  }
22307
22261
 
22308
22262
  async #getParserFor(start, size) {
22309
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
22263
+ const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
22310
22264
  return new BinaryParser$1(new DataView(data), this.littleEndian)
22311
22265
  }
22312
22266
 
@@ -22663,6 +22617,165 @@
22663
22617
  }
22664
22618
  }
22665
22619
 
22620
+ class ChromTree {
22621
+
22622
+ nameToId = new Map()
22623
+ idToName = new Map()
22624
+
22625
+ constructor(path, config, startOffset, loader) {
22626
+ this.path = path;
22627
+ this.config = config;
22628
+ this.startOffset = startOffset;
22629
+
22630
+ this.bpTree = new BPTree(path, config, startOffset, 'BPChromTree', loader);
22631
+ }
22632
+
22633
+ async init() {
22634
+ return this.bpTree.init()
22635
+ }
22636
+
22637
+ getItemCount() {
22638
+ return this.bpTree.getItemCount()
22639
+ }
22640
+
22641
+ /**
22642
+ * Return the chromosome ID for the given name. This is the internal chromosome ID for the parent BB file only.
22643
+ * @param {string} chr - The chromosome name.
22644
+ * @returns {number|null} - The chromosome ID or null if not found.
22645
+ */
22646
+ async getIdForName(chr) {
22647
+ if (this.nameToId.has(chr)) {
22648
+ return this.nameToId.get(chr)
22649
+ } else {
22650
+ try {
22651
+ const result = await this.bpTree.search(chr);
22652
+ if (result) {
22653
+ const id = result.id;
22654
+ this.nameToId.set(chr, id);
22655
+ return id
22656
+ } else {
22657
+ return
22658
+ }
22659
+ } catch (error) {
22660
+ throw new Error(error)
22661
+ }
22662
+ }
22663
+ }
22664
+
22665
+ /**
22666
+ * Return the chromosome name for the given ID. This is a potentially expensive operation as it involves
22667
+ * walking the tree until the leaf item for the given name is found. Currently it is used in only 2
22668
+ * situations:
22669
+ * (1) decoding features from a bigbed search-by-name query
22670
+ * (2) decoding bigwig data from the whole genome view
22671
+ * @param {number} id
22672
+ * @return {string|null}
22673
+ */
22674
+ async getNameForId(id) {
22675
+ if (this.idToName.has(id)) {
22676
+ return this.idToName.get(id)
22677
+ } else {
22678
+ const name = await this.searchForName(id);
22679
+ if (name !== null) {
22680
+ this.idToName.set(id, name);
22681
+ return name
22682
+ }
22683
+ }
22684
+ return null
22685
+ }
22686
+
22687
+ /**
22688
+ * Perform a reverse search by traversing the tree starting at the given offset. This is potentially expensive
22689
+ * as it traverses the tree to find the name corresponding to the given ID. It shoud not be used for large trees.
22690
+ *
22691
+ * @param {number} id - The ID to search for.
22692
+ * @returns {string|null} - The name corresponding to the ID, or null if not found.
22693
+ */
22694
+ async searchForName(id) {
22695
+
22696
+ const reverseSearch = async (offset, id) => {
22697
+
22698
+ const node = await this.bpTree.readTreeNode(offset);
22699
+
22700
+ let found = null;
22701
+
22702
+ if (node.type === 1) {
22703
+ // Leaf node
22704
+ for (const item of node.items) {
22705
+ const key = item.key;
22706
+ const itemId = item.value.id;
22707
+ if (itemId === id) {
22708
+ found = key;
22709
+ }
22710
+ // Cache the name and ID for future lookups
22711
+ this.nameToId.set(key, itemId);
22712
+ this.idToName.set(id, itemId);
22713
+ }
22714
+ return found
22715
+ } else {
22716
+ // Non-leaf node
22717
+ for (const item of node.items) {
22718
+ found = await reverseSearch.call(this, item.offset, id);
22719
+ if (found !== null) {
22720
+ break
22721
+ }
22722
+ }
22723
+ }
22724
+ return found
22725
+ };
22726
+
22727
+ try {
22728
+ return reverseSearch.call(this, this.startOffset + 32, id)
22729
+ } catch (error) {
22730
+ throw new Error(error)
22731
+ }
22732
+ }
22733
+
22734
+ /**
22735
+ * Return an estimated length of the genome, which might be the actual length if the number of contigs is small.
22736
+ * This is only used for calculating a default feature visibility window.
22737
+ *
22738
+ * @return {number}
22739
+ */
22740
+ async estimateGenomeSize() {
22741
+ try {
22742
+ const runningTotal = {total: 0, count: 0};
22743
+ await this.accumulateSize(this.startOffset + 32, runningTotal, 10000);
22744
+ const itemCount = this.getItemCount();
22745
+ return (itemCount / runningTotal.count) * runningTotal.total
22746
+
22747
+ } catch (error) {
22748
+ console.error("Error estimating genome size", error);
22749
+ return -1
22750
+ }
22751
+ }
22752
+
22753
+ async accumulateSize(offset, runningTotal, maxCount) {
22754
+
22755
+ const node = await this.bpTree.readTreeNode(offset);
22756
+
22757
+ if (node.type === 1) {
22758
+ // Leaf node
22759
+ for (const item of node.items) {
22760
+ const value = item.value;
22761
+ runningTotal.total += value.size;
22762
+ runningTotal.count += 1;
22763
+ }
22764
+ } else {
22765
+ // Non-leaf node. Items are visited in random order to avoid biasing the estimate
22766
+ const shuffledItems = node.items.slice().sort(() => Math.random() - 0.5);
22767
+ for (const item of shuffledItems) {
22768
+ await this.accumulateSize(item.offset, runningTotal, maxCount);
22769
+ if (runningTotal.count > maxCount) {
22770
+ break
22771
+ }
22772
+ }
22773
+ }
22774
+ return runningTotal
22775
+ }
22776
+
22777
+ }
22778
+
22666
22779
  /*
22667
22780
  * The MIT License (MIT)
22668
22781
  *
@@ -22726,14 +22839,40 @@
22726
22839
  async preload() {
22727
22840
  const data = await igvxhr.loadArrayBuffer(this.path);
22728
22841
  this.loader = new DataBuffer(data);
22842
+ for (let rpTree of this.rpTreeCache.values()) {
22843
+ rpTree.loader = this.loader;
22844
+ }
22845
+ if (this._searchTrees) {
22846
+ for (let bpTree of this._searchTrees) {
22847
+ bpTree.loader = this.loader;
22848
+ }
22849
+ }
22729
22850
  }
22730
22851
 
22731
- async readWGFeatures(bpPerPixel, windowFunction) {
22852
+ async readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction) {
22853
+
22732
22854
  await this.loadHeader();
22733
- const chrIdx1 = 0;
22734
- const chrIdx2 = this.chromTree.idToName.length - 1;
22735
- const chr1 = this.chromTree.idToName[chrIdx1];
22736
- const chr2 = this.chromTree.idToName[chrIdx2];
22855
+ // Convert the logic to JavaScript
22856
+ let minID = Number.MAX_SAFE_INTEGER;
22857
+ let maxID = -1;
22858
+ let chr1;
22859
+ let chr2;
22860
+
22861
+ for (const chr of wgChromosomeNames) {
22862
+ const id = await this.getIdForChr(chr);
22863
+ if (id === null || id === undefined) {
22864
+ continue
22865
+ }
22866
+ if (id < minID) {
22867
+ minID = id;
22868
+ chr1 = chr;
22869
+ }
22870
+ if (id > maxID) {
22871
+ maxID = id;
22872
+ chr2 = chr;
22873
+ }
22874
+ }
22875
+
22737
22876
  return this.readFeatures(chr1, 0, chr2, Number.MAX_VALUE, bpPerPixel, windowFunction)
22738
22877
  }
22739
22878
 
@@ -22744,8 +22883,8 @@
22744
22883
 
22745
22884
  await this.loadHeader();
22746
22885
 
22747
- let chrIdx1 = await this.#getIdForChr(chr1);
22748
- let chrIdx2 = await this.#getIdForChr(chr2);
22886
+ const chrIdx1 = await this.getIdForChr(chr1);
22887
+ const chrIdx2 = await this.getIdForChr(chr2);
22749
22888
 
22750
22889
  if (chrIdx1 === undefined || chrIdx2 === undefined) {
22751
22890
  return []
@@ -22804,7 +22943,7 @@
22804
22943
  } else {
22805
22944
  plain = uint8Array;
22806
22945
  }
22807
- decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, this.chromTree.idToName, windowFunction, this.littleEndian);
22946
+ await decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, windowFunction);
22808
22947
  }
22809
22948
 
22810
22949
  features.sort(function (a, b) {
@@ -22821,29 +22960,30 @@
22821
22960
  * @param chr
22822
22961
  * @returns {Promise<*>}
22823
22962
  */
22824
- async #getIdForChr(chr) {
22963
+ async getIdForChr(chr) {
22825
22964
 
22826
22965
  if (this.chrAliasTable.has(chr)) {
22827
22966
  chr = this.chrAliasTable.get(chr);
22828
- if (chr === undefined) {
22967
+ if (!chr) {
22829
22968
  return undefined
22830
22969
  }
22831
22970
  }
22832
22971
 
22833
- let chrIdx = this.chromTree.nameToId.get(chr);
22972
+ let chrIdx = await this.chromTree.getIdForName(chr);
22834
22973
 
22835
22974
  // Try alias
22836
22975
  if (chrIdx === undefined && this.genome) {
22837
22976
  const aliasRecord = await this.genome.getAliasRecord(chr);
22838
22977
  let alias;
22839
22978
  if (aliasRecord) {
22840
- const aliases = Object.keys(aliasRecord)
22841
- .filter(k => k !== "start" && k !== "end")
22842
- .map(k => aliasRecord[k])
22843
- .filter(a => this.chromTree.nameToId.has(a));
22844
- if (aliases.length > 0) {
22845
- alias = aliases[0];
22846
- chrIdx = this.chromTree.nameToId.get(aliases[0]);
22979
+ for (let k of Object.keys(aliasRecord)) {
22980
+ if (k === "start" || k === "end") continue
22981
+ alias = aliasRecord[k];
22982
+ if (alias === chr) continue // Already tried this
22983
+ chrIdx = await this.chromTree.getIdForName(alias);
22984
+ if (chrIdx !== undefined) {
22985
+ break
22986
+ }
22847
22987
  }
22848
22988
  }
22849
22989
  this.chrAliasTable.set(chr, alias); // alias may be undefined => no alias exists. Setting prevents repeated attempts
@@ -22930,7 +23070,8 @@
22930
23070
  this.header.extraIndexOffsets.length > 0) {
22931
23071
  this._searchTrees = [];
22932
23072
  for (let offset of this.header.extraIndexOffsets) {
22933
- const bpTree = await BPTree.loadBpTree(this.path, this.config, offset);
23073
+ const type = undefined;
23074
+ const bpTree = await BPTree.loadBpTree(this.path, this.config, offset, type, this.loader);
22934
23075
  this._searchTrees.push(bpTree);
22935
23076
  }
22936
23077
  }
@@ -23047,15 +23188,13 @@
23047
23188
  this.totalSummary = new BWTotalSummary(extHeaderParser);
23048
23189
  }
23049
23190
 
23050
- // Chrom data index. The start is known, size is not, but we can estimate it
23051
- const bufferSize = Math.min(200000, Math.max(10000, header.fullDataOffset - header.chromTreeOffset));
23052
- this.chromTree = await this.#readChromTree(header.chromTreeOffset, bufferSize);
23053
- this.chrNames = new Set(this.chromTree.idToName);
23191
+ this.chromTree = new ChromTree(this.path, this.config, header.chromTreeOffset, this.loader);
23192
+ await this.chromTree.init();
23054
23193
 
23055
23194
  // Estimate feature density from dataCount (bigbed only)
23056
- if("bigbed" === this.type) {
23195
+ if ("bigbed" === this.type) {
23057
23196
  const dataCount = await this.#readDataCount(header.fullDataOffset);
23058
- this.featureDensity = dataCount / this.chromTree.sumLengths;
23197
+ this.featureDensity = dataCount / await this.chromTree.estimateGenomeSize();
23059
23198
  }
23060
23199
 
23061
23200
  this.header = header;
@@ -23079,38 +23218,6 @@
23079
23218
  return binaryParser.getInt()
23080
23219
  }
23081
23220
 
23082
- /**
23083
- * Used when the chromTreeOffset is > fullDataOffset, that is when the chrom tree is not in the initial chunk
23084
- * read for parsing the header. We know the start position, but not the total size of the chrom tree
23085
- *
23086
- * @returns {Promise<void>}
23087
- */
23088
- async #readChromTree(chromTreeOffset, bufferSize) {
23089
-
23090
- let size = bufferSize;
23091
- const load = async () => {
23092
- const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
23093
- range: {
23094
- start: chromTreeOffset,
23095
- size: size
23096
- }
23097
- }));
23098
- const binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
23099
- return ChromTree.parseTree(binaryParser, chromTreeOffset, this.genome)
23100
- };
23101
-
23102
- let error;
23103
- while (size < 1000000) {
23104
- try {
23105
- const chromTree = await load();
23106
- return chromTree
23107
- } catch (e) {
23108
- error = e;
23109
- size *= 2;
23110
- }
23111
- }
23112
- throw (error)
23113
- }
23114
23221
 
23115
23222
  async loadExtendedHeader(offset) {
23116
23223
 
@@ -23166,7 +23273,7 @@
23166
23273
  if (rpTree) {
23167
23274
  return rpTree
23168
23275
  } else {
23169
- rpTree = new RPTree(this.path, this.config, offset);
23276
+ rpTree = new RPTree(this.path, this.config, offset, this.loader);
23170
23277
  await rpTree.init();
23171
23278
  this.rpTreeCache.set(offset, rpTree);
23172
23279
  return rpTree
@@ -23206,9 +23313,7 @@
23206
23313
  const plain = (this.header.uncompressBuffSize > 0) ? inflate_1$3(uint8Array) : uint8Array;
23207
23314
  const decodeFunction = getBedDataDecoder.call(this);
23208
23315
  const features = [];
23209
- decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
23210
- features, this.chromTree.idToName);
23211
-
23316
+ await decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, features);
23212
23317
  return features
23213
23318
 
23214
23319
  }
@@ -23276,7 +23381,7 @@
23276
23381
  }
23277
23382
 
23278
23383
 
23279
- function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23384
+ async function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23280
23385
 
23281
23386
  const binaryParser = new BinaryParser$1(data, littleEndian);
23282
23387
  const chromId = binaryParser.getInt();
@@ -23317,7 +23422,7 @@
23317
23422
  else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break
23318
23423
 
23319
23424
  if (Number.isFinite(value)) {
23320
- const chr = chrDict[chromId];
23425
+ const chr = await this.chromTree.getNameForId(chromId);
23321
23426
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23322
23427
  }
23323
23428
  }
@@ -23328,12 +23433,13 @@
23328
23433
 
23329
23434
  const minSize = 3 * 4 + 1; // Minimum # of bytes required for a bed record
23330
23435
  const decoder = getDecoder(this.header.definedFieldCount, this.header.fieldCount, this.autoSql, this.format);
23331
- return function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23332
- const binaryParser = new BinaryParser$1(data, littleEndian);
23436
+ return async function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray) {
23437
+
23438
+ const binaryParser = new BinaryParser$1(data, this.littleEndian);
23333
23439
  while (binaryParser.remLength() >= minSize) {
23334
23440
 
23335
23441
  const chromId = binaryParser.getInt();
23336
- const chr = chrDict[chromId];
23442
+ const chr = await this.chromTree.getNameForId(chromId);
23337
23443
  const chromStart = binaryParser.getInt();
23338
23444
  const chromEnd = binaryParser.getInt();
23339
23445
  const rest = binaryParser.getString();
@@ -23350,8 +23456,7 @@
23350
23456
  }
23351
23457
  }
23352
23458
 
23353
-
23354
- function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23459
+ async function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23355
23460
 
23356
23461
  const binaryParser = new BinaryParser$1(data, littleEndian);
23357
23462
  const minSize = 8 * 4; // Minimum # of bytes required for a zoom record
@@ -23383,7 +23488,7 @@
23383
23488
 
23384
23489
 
23385
23490
  if (Number.isFinite(value)) {
23386
- const chr = chrDict[chromId];
23491
+ const chr = await this.chromTree.getNameForId(chromId);
23387
23492
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23388
23493
 
23389
23494
 
@@ -23468,7 +23573,8 @@
23468
23573
 
23469
23574
  let features;
23470
23575
  if ("all" === chr.toLowerCase()) {
23471
- features = isBigWig ? await this.getWGValues(windowFunction, bpPerPixel) : [];
23576
+ const wgChromosomeNames = this.genome.wgChromosomeNames;
23577
+ features = isBigWig && wgChromosomeNames? await this.getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) : [];
23472
23578
  } else {
23473
23579
  features = await this.reader.readFeatures(chr, start, chr, end, bpPerPixel, windowFunction);
23474
23580
  }
@@ -23492,15 +23598,14 @@
23492
23598
 
23493
23599
  }
23494
23600
 
23495
- async getWGValues(windowFunction, bpPerPixel) {
23601
+ async getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) {
23496
23602
 
23497
23603
  const genome = this.genome;
23498
23604
  const cached = this.#wgValues[windowFunction];
23499
23605
  if (cached && cached.bpPerPixel > 0.8 * bpPerPixel && cached.bpPerPixel < 1.2 * bpPerPixel) {
23500
23606
  return cached.values
23501
23607
  } else {
23502
-
23503
- const features = await this.reader.readWGFeatures(bpPerPixel, windowFunction);
23608
+ const features = await this.reader.readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction);
23504
23609
  let wgValues = [];
23505
23610
  for (let f of features) {
23506
23611
  const chr = f.chr;
@@ -28295,7 +28400,7 @@
28295
28400
  features
28296
28401
  };
28297
28402
 
28298
- const track = await browser.loadTrack(trackConfig);
28403
+ const track = (await browser.loadTrackList([trackConfig]))[0];
28299
28404
  track.openTableView();
28300
28405
 
28301
28406
  } catch (e) {
@@ -30347,109 +30452,578 @@
30347
30452
  }
30348
30453
  }
30349
30454
 
30350
- /*
30351
- https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30352
- https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30353
- https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30354
- https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30355
- */
30455
+ const parentOverrideProperties = new Set(["visibility", "priority", "group"]);
30356
30456
 
30457
+ const nonInheritableProperties = new Set([
30458
+ "track", "type", "shortLabel", "longLabel", "bigDataUrl",
30459
+ "parent", "superTrack", "priority", "view", "compositeContainer", "compositeTrack"
30460
+ ]);
30357
30461
 
30358
- class Hub {
30359
30462
 
30360
- static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30361
- static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30362
- "cpgIslandExtUnmasked", "windowMasker"])
30463
+ class Stanza {
30464
+
30465
+ properties = new Map()
30466
+
30467
+ constructor(type, name) {
30468
+ this.type = type;
30469
+ this.name = name;
30470
+ }
30471
+
30472
+ setProperty(key, value) {
30473
+ this.properties.set(key, value);
30474
+ }
30475
+
30476
+ getProperty(key) {
30477
+ if (this.properties.has("noInherit")) {
30478
+ return this.properties.get(key)
30479
+ } else if (this.parent && parentOverrideProperties.has(key) && this.parent.hasProperty(key)) {
30480
+ return this.parent.getProperty(key)
30481
+ } else if (this.properties.has(key)) {
30482
+ return this.properties.get(key)
30483
+ } else if (this.parent && !nonInheritableProperties.has(key)) {
30484
+ return this.parent.getProperty(key)
30485
+ } else {
30486
+ return undefined
30487
+ }
30488
+ }
30489
+
30490
+ hasProperty(key) {
30491
+ return this.getProperty(key) !== null && this.getProperty(key) !== undefined
30492
+ }
30363
30493
 
30364
- static async loadHub(url) {
30494
+ hasOwnProperty(key) {
30495
+ return this.properties.has(key)
30496
+ }
30365
30497
 
30366
- const idx = url.lastIndexOf("/");
30367
- const baseURL = url.substring(0, idx + 1);
30368
- const stanzas = await loadStanzas(url);
30369
- let groups;
30370
- if ("genome" === stanzas[1].type) {
30371
- const genome = stanzas[1];
30372
- if (genome.hasProperty("groups")) {
30373
- const groupsTxtURL = baseURL + genome.getProperty("groups");
30374
- groups = await loadStanzas(groupsTxtURL);
30498
+ getOwnProperty(key) {
30499
+ return this.properties.get(key)
30500
+ }
30501
+
30502
+ removeProperty(key) {
30503
+ this.properties.delete(key);
30504
+ }
30505
+
30506
+ get format() {
30507
+ const type = this.getProperty("type");
30508
+ if (type) {
30509
+ // Trim extra bed qualifiers (e.g. bigBed + 4)
30510
+ return firstWord$1(type)
30511
+ }
30512
+ return undefined // unknown type
30513
+ }
30514
+
30515
+ /**
30516
+ * IGV display mode
30517
+ */
30518
+ get displayMode() {
30519
+ let viz = this.getProperty("visibility");
30520
+ if (!viz) {
30521
+ return "COLLAPSED"
30522
+ } else {
30523
+ viz = viz.toLowerCase();
30524
+ switch (viz) {
30525
+ case "dense":
30526
+ return "COLLAPSED"
30527
+ case "pack":
30528
+ return "EXPANDED"
30529
+ case "squish":
30530
+ return "SQUISHED"
30531
+ default:
30532
+ return "COLLAPSED"
30375
30533
  }
30534
+ }
30535
+ }
30536
+ }
30537
+
30538
+
30539
+ function firstWord$1(str) {
30540
+ const idx = str.indexOf(' ');
30541
+ return idx > 0 ? str.substring(0, idx) : str
30542
+ }
30376
30543
 
30377
- // If the genome has a chromSizes file, and it is not too large, set the chromSizesURL property. This will
30378
- // enable whole genome view and the chromosome pulldown
30379
- if (genome.hasProperty("chromSizes")) {
30380
- const chromSizesURL = baseURL + genome.getProperty("chromSizes");
30381
- const l = await getContentLength(chromSizesURL);
30382
- if (l !== null && Number.parseInt(l) < 1000000) {
30383
- genome.setProperty("chromSizesURL", chromSizesURL);
30544
+ class TrackConfigContainer {
30545
+ constructor(name, label, priority, defaultOpen) {
30546
+ this.name = name;
30547
+ this.priority = priority;
30548
+ this.label = label;
30549
+ this.defaultOpen = defaultOpen;
30550
+ this.tracks = [];
30551
+ this.children = [];
30552
+ }
30553
+
30554
+ isEmpty() {
30555
+ return this.tracks.length === 0 &&
30556
+ (!this.children || this.children.length === 0 || this.children.every(child => child.isEmpty()));
30557
+ }
30558
+
30559
+ map(callback) {
30560
+ this.tracks.forEach(callback);
30561
+ this.children.forEach(child => child.map(callback));
30562
+ }
30563
+
30564
+ findTracks(filter) {
30565
+ const found = [];
30566
+ this._find(found, filter);
30567
+ return found;
30568
+ }
30569
+
30570
+ _find(found, filter) {
30571
+ this.tracks.forEach(track => {
30572
+ if (filter(track)) {
30573
+ found.push(track);
30384
30574
  }
30575
+ });
30576
+ this.children.forEach(child => child._find(found, filter));
30577
+ }
30578
+
30579
+ countTracks() {
30580
+ return this.tracks.length + this.children.reduce((count, child) => count + child.countTracks(), 0);
30581
+ }
30582
+
30583
+ countSelectedTracks() {
30584
+ const selectedCount = this.tracks.filter(track => track.visible).length;
30585
+ return selectedCount + this.children.reduce((count, child) => count + child.countSelectedTracks(), 0);
30586
+ }
30587
+
30588
+ trim() {
30589
+ this.children = this.children.filter(child => !child.isEmpty());
30590
+ this.children.forEach(child => child.trim());
30591
+ }
30592
+
30593
+ setTrackVisibility(loadedTrackPaths) {
30594
+ this.tracks.forEach(track => {
30595
+ track.visible = loadedTrackPaths.has(track.url);
30596
+ });
30597
+ this.children.forEach(child => child.setTrackVisibility(loadedTrackPaths));
30598
+ }
30599
+ }
30600
+
30601
+ const supportedTypes = new Set([
30602
+ "bigbed", "bigwig", "biggenepred", "vcftabix", "refgene",
30603
+ "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf", "vcf", "vcfphasedtrio",
30604
+ "bigdbsnp", "rmask", "genepred", "wig", "bedgraph", "interact", "broadpeak", "narrowpeak", "gappedpeak",
30605
+ "gistic", "seg", "mut, bigrmsk"
30606
+ ]);
30607
+
30608
+ const filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30609
+ "cpgIslandExtUnmasked", "windowMasker"]);
30610
+
30611
+ class TrackDbHub {
30612
+
30613
+ constructor(trackStanzas, groupStanzas) {
30614
+ this.groupStanzas = groupStanzas;
30615
+ this.trackStanzas = trackStanzas;
30616
+ }
30617
+
30618
+ findCytobandURL() {
30619
+ for (const t of this.trackStanzas) {
30620
+ if (t.name === "cytoBandIdeo" && t.hasProperty("bigDataUrl")) {
30621
+ return t.getProperty("bigDataUrl")
30385
30622
  }
30386
30623
  }
30624
+ }
30387
30625
 
30388
- // TODO -- categorize extra "user" supplied and other tracks in some distinctive way before including them
30389
- // load includes. Nested includes are not supported
30390
- // for (let s of stanzas.slice()) {
30391
- // if ("include" === s.type) {
30392
- // const includeStanzas = await loadStanzas(baseURL + s.getProperty("include"))
30393
- // for (s of includeStanzas) {
30394
- // s.setProperty("visibility", "hide")
30395
- // stanzas.push(s)
30396
- // }
30397
- // }
30398
- // }
30626
+ getSupportedTrackCount() {
30627
+ let count = 0;
30628
+ for (const t of this.trackStanzas) {
30629
+ if (!filterTracks.has(t.name) &&
30630
+ t.hasProperty("bigDataUrl") &&
30631
+ t.format &&
30632
+ supportedTypes.has(t.format.toLowerCase())) {
30633
+ count++;
30634
+ }
30635
+ }
30636
+ return count
30637
+ }
30638
+
30639
+ getGroupedTrackConfigurations() {
30640
+
30641
+ if (!this.groupTrackConfigs) {
30642
+ this.groupTrackConfigs = [];
30643
+ const trackContainers = new Map();
30399
30644
 
30400
- return new Hub(url, stanzas, groups)
30645
+ // create a container for tracks with no parent
30646
+ const nullContainer = new TrackConfigContainer('', '', 0, true);
30647
+ this.groupTrackConfigs.push(nullContainer);
30648
+
30649
+ const hasGroups = this.groupStanzas && this.groupStanzas.length > 0;
30650
+ if (hasGroups) {
30651
+ for (const groupStanza of this.groupStanzas) {
30652
+ const name = groupStanza.getProperty("name");
30653
+ const defaultOpen = groupStanza.getProperty("defaultIsClosed") === "0";
30654
+ const priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
30655
+ const container = new TrackConfigContainer(name, groupStanza.getProperty("label"), priority, defaultOpen);
30656
+ trackContainers.set(name, container);
30657
+ this.groupTrackConfigs.push(container);
30658
+ }
30659
+ }
30660
+
30661
+ for (let s of this.trackStanzas) {
30662
+
30663
+ const isContainer = (s.hasOwnProperty("superTrack") && !s.hasOwnProperty("bigDataUrl")) ||
30664
+ s.hasOwnProperty("compositeTrack") || s.hasOwnProperty("view") ||
30665
+ (s.hasOwnProperty("container") && s.getOwnProperty("container").equals("multiWig"));
30666
+
30667
+ // Find parent, if any. "group" containers can be implicit, all other types should be explicitly
30668
+ // defined before their children
30669
+ let parent;
30670
+
30671
+ if (s.hasOwnProperty("parent")) {
30672
+ parent = trackContainers.get(s.getOwnProperty("parent"));
30673
+ }
30674
+
30675
+ if (!parent && hasGroups && s.hasProperty("group")) {
30676
+ const groupName = s.getProperty("group");
30677
+ if (trackContainers.has(groupName)) {
30678
+ parent = trackContainers.get(groupName);
30679
+ } else {
30680
+ const container = new TrackConfigContainer(groupName, groupName, 1000, true);
30681
+ trackContainers.set(groupName, container);
30682
+ this.groupTrackConfigs.push(container);
30683
+ parent = container;
30684
+ }
30685
+ }
30686
+
30687
+ if (isContainer) {
30688
+
30689
+ const name = s.getProperty("track");
30690
+ const priority = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
30691
+ const defaultOpen = s.getProperty("defaultIsClosed") === "0";
30692
+ const longLabel = s.getOwnProperty("longLabel");
30693
+ const label = longLabel && longLabel.length < 50 ? longLabel : s.getOwnProperty("shortLabel");
30694
+ const container = new TrackConfigContainer(name, label, priority, defaultOpen);
30695
+
30696
+ if (trackContainers.has(name)) {
30697
+ throw new Error(`Duplicate track container: ${name}`)
30698
+ }
30699
+ trackContainers.set(name, container);
30700
+
30701
+ if (parent) {
30702
+ parent.children.push(container);
30703
+ } else {
30704
+ // No parent or a superTrack => promote to top level
30705
+ this.groupTrackConfigs.push(container);
30706
+ }
30707
+ } else if (!filterTracks.has(s.name) &&
30708
+ s.hasProperty("bigDataUrl") &&
30709
+ s.format &&
30710
+ supportedTypes.has(s.format.toLowerCase())) {
30711
+
30712
+ const trackConfig = this.#getTrackConfig(s);
30713
+ if (parent) {
30714
+ parent.tracks.push(trackConfig);
30715
+ } else {
30716
+ nullContainer.tracks.push(trackConfig);
30717
+ }
30718
+ }
30719
+ }
30720
+
30721
+ }
30722
+
30723
+ // Filter empty groups and sort
30724
+ this.groupTrackConfigs.forEach(c => c.trim());
30725
+ this.groupTrackConfigs = this.groupTrackConfigs.filter(t => !t.isEmpty());
30726
+
30727
+ this.groupTrackConfigs.sort((a, b) => a.priority - b.priority);
30728
+ return this.groupTrackConfigs
30401
30729
  }
30402
30730
 
30403
- constructor(url, stanzas, groupStanzas) {
30731
+ /**
30732
+ * Return an array of igv track config objects that satisfy the filter
30733
+ */
30734
+ #getTracksConfigs(filter) {
30735
+ return this.trackStanzas.filter(t => {
30736
+ return supportedTypes.has(t.format) && t.hasProperty("bigDataUrl") && (!filter || filter(t))
30737
+ })
30738
+ .map(t => this.#getTrackConfig(t))
30739
+ }
30404
30740
 
30405
- this.url = url;
30406
30741
 
30407
- const idx = url.lastIndexOf("/");
30408
- this.baseURL = url.substring(0, idx + 1);
30742
+ /** example
30743
+ * track gc5Base
30744
+ * shortLabel GC Percent
30745
+ * longLabel GC Percent in 5-Base Windows
30746
+ * group map
30747
+ * visibility full
30748
+ * autoScale Off
30749
+ * maxHeightPixels 128:36:16
30750
+ * graphTypeDefault Bar
30751
+ * gridDefault OFF
30752
+ * windowingFunction Mean
30753
+ * color 0,0,0
30754
+ * altColor 128,128,128
30755
+ * viewLimits 30:70
30756
+ * type bigWig 0 100
30757
+ * bigDataUrl bbi/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base.bw
30758
+ * html html/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base
30759
+ * @param t
30760
+ */
30761
+ #getTrackConfig(t) {
30409
30762
 
30410
- // The first stanza must be type = hub
30411
- if ("hub" === stanzas[0].type) {
30412
- this.hubStanza = stanzas[0];
30413
- } else {
30414
- throw Error("Unexpected hub.txt file -- does the first line start with 'hub'?")
30763
+ const format = t.format;
30764
+
30765
+ const config = {
30766
+ "id": t.getProperty("track"),
30767
+ "name": t.getProperty("shortLabel"),
30768
+ "format": format,
30769
+ "url": t.getProperty("bigDataUrl"),
30770
+ "displayMode": t.displayMode,
30771
+ };
30772
+
30773
+ if ("vcfTabix" === format) {
30774
+ config.indexURL = config.url + ".tbi";
30775
+ }
30776
+
30777
+ if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30778
+ if (config.description) config.description += "<br/>";
30779
+ config.description =
30780
+ `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
30781
+ } else if (t.hasProperty("longLabel")) {
30782
+ config.description = t.getProperty("longLabel");
30783
+ }
30784
+
30785
+ if (t.hasProperty("autoScale")) {
30786
+ config.autoscale = t.getProperty("autoScale").toLowerCase() === "on";
30787
+ }
30788
+ if (t.hasProperty("maxHeightPixels")) {
30789
+ const tokens = t.getProperty("maxHeightPixels").split(":");
30790
+ config.maxHeight = Number.parseInt(tokens[0]);
30791
+ config.height = Number.parseInt(tokens[1]);
30792
+ config.minHeight = Number.parseInt(tokens[2]);
30793
+ }
30794
+ // TODO -- graphTypeDefault
30795
+ // TODO -- windowingFunction
30796
+ if (t.hasProperty("color")) {
30797
+ const c = t.getProperty("color");
30798
+ config.color = c.indexOf(",") > 0 ? `rgb(${c})` : c;
30799
+ }
30800
+ if (t.hasProperty("altColor")) {
30801
+ const c = t.getProperty("altColor");
30802
+ config.altColor = c.indexOf(",") > 0 ? `rgb(${c})` : c;
30803
+ }
30804
+ if (t.hasProperty("viewLimits")) {
30805
+ const tokens = t.getProperty("viewLimits").split(":");
30806
+ let min, max;
30807
+ if (tokens.length > 1) {
30808
+ min = Number.parseInt(tokens[0]);
30809
+ max = Number.parseInt(tokens[1]);
30810
+ }
30811
+ if (Number.isNaN(max) || Number.isNaN(min)) {
30812
+ console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
30813
+ } else {
30814
+ config.min = min;
30815
+ config.max = max;
30816
+ }
30817
+
30818
+ }
30819
+ if (t.hasProperty("itemRgb")) ;
30820
+ if ("hide" === t.getProperty("visibility")) {
30821
+ // TODO -- this not supported yet
30822
+ config.visible = false;
30415
30823
  }
30416
- if ("on" !== this.hubStanza.getProperty("useOneFile")) {
30417
- throw Error("Only 'useOneFile' hubs are currently supported")
30824
+ if (t.hasProperty("url")) {
30825
+ config.infoURL = t.getProperty("url");
30418
30826
  }
30419
- if (stanzas.length < 2) {
30420
- throw Error("Expected at least 2 stanzas, hub and genome")
30827
+ if (t.hasProperty("searchIndex")) {
30828
+ config.searchIndex = t.getProperty("searchIndex");
30829
+ }
30830
+ if (t.hasProperty("searchTrix")) {
30831
+ config.trixURL = t.getProperty("searchTrix");
30832
+ }
30833
+ if (t.hasProperty("html")) {
30834
+ config.html = t.getProperty("html");
30421
30835
  }
30422
30836
 
30423
- // The second stanza should be a genome
30424
- if ("genome" === stanzas[1].type) {
30425
- this.genomeStanza = stanzas[1];
30426
- } else {
30427
- throw Error(`Unexpected hub file -- expected "genome" stanza but found "${stanzas[1].type}"`)
30837
+ if (t.hasProperty("group")) {
30838
+ config._group = t.getProperty("group");
30839
+ if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
30840
+ const nextPriority = this.groupPriorityMap.get(config._group) + 1;
30841
+ config.order = nextPriority;
30842
+ this.groupPriorityMap.set(config._group, nextPriority);
30843
+ }
30844
+ }
30845
+
30846
+ if (t.hasProperty("metadata")) {
30847
+ config.attributes = parseMetadata(t.getProperty("metadata"));
30848
+ }
30849
+
30850
+ if (t.hasProperty("maxWindowToDraw")) {
30851
+ let maxWindowToDraw = parseInt(t.getProperty("maxWindowToDraw"), 10);
30852
+ if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
30853
+ maxWindowToDraw = Number.MAX_SAFE_INTEGER;
30854
+ }
30855
+ config.visibilityWindow = maxWindowToDraw;
30428
30856
  }
30429
30857
 
30430
- // Remaining stanzas should be tracks
30431
- this.trackStanzas = [];
30432
- for (let i = 2; i < stanzas.length; i++) {
30433
- if ("track" === stanzas[i].type) {
30434
- this.trackStanzas.push(stanzas[i]);
30858
+ // IGV does not support "maxWindowCoverage" in the same way as UCSC. Use to limit visibility window
30859
+ if (t.hasProperty("maxWindowCoverage")) {
30860
+ let maxWindowToDraw = parseInt(t.getProperty("maxWindowCoverage"), 10);
30861
+ if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
30862
+ maxWindowToDraw = Number.MAX_SAFE_INTEGER;
30435
30863
  }
30864
+ config.visibilityWindow = maxWindowToDraw;
30436
30865
  }
30437
30866
 
30438
- if (groupStanzas) {
30439
- this.groupStanzas = groupStanzas;
30440
- this.groupPriorityMap = new Map();
30441
- for (let g of groupStanzas) {
30442
- if (g.hasProperty("priority")) {
30443
- this.groupPriorityMap.set(g.getProperty("name"), Number.parseInt(g.getProperty("priority")) * 10);
30867
+ return config
30868
+ }
30869
+ }
30870
+
30871
+ function htmlText(html) {
30872
+ // Assumes a pattern like <span style="color:#C58DAA">Digestive</span>
30873
+ const idx1 = html.indexOf('>');
30874
+ const idx2 = html.indexOf('<', idx1);
30875
+ if (idx1 > 0 && idx2 > idx1) {
30876
+ return html.substring(idx1 + 1, idx2)
30877
+ } else {
30878
+ return html
30879
+ }
30880
+ }
30881
+
30882
+ /**
30883
+ * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4).
30884
+ * Ignore levels > 3
30885
+ *
30886
+ * @param {string} priorityString Priority as a string (e.g. 3.4)
30887
+ * @return {number} A priority as an integer
30888
+ */
30889
+ function getPriority(priorityString) {
30890
+ try {
30891
+ const tokens = priorityString.trim().split(".");
30892
+ let p = parseInt(tokens[0], 10) * 100;
30893
+ if (tokens.length > 1) {
30894
+ p += parseInt(tokens[1], 10) * 10;
30895
+ }
30896
+ if (tokens.length > 2) {
30897
+ p += parseInt(tokens[2], 10);
30898
+ }
30899
+ return p
30900
+ } catch (e) {
30901
+ console.error(`Error parsing priority string: ${priorityString}`, e);
30902
+ return Number.MAX_SAFE_INTEGER
30903
+ }
30904
+ }
30905
+
30906
+ function parseMetadata(metadata) {
30907
+ const attrs = new Map();
30908
+ let lastMetdataLengh = -1;
30909
+ while (metadata && metadata.length > 0) {
30910
+ try {
30911
+ if (metadata.length === lastMetdataLengh) {
30912
+ break
30913
+ }
30914
+ lastMetdataLengh = metadata.length;
30915
+ let idx = metadata.indexOf("=");
30916
+ if (idx === -1 || idx === metadata.length - 1) {
30917
+ break
30918
+ }
30919
+ let idx2;
30920
+ const key = capitalize(stripQuotes$2(metadata.substring(0, idx)));
30921
+ let value;
30922
+
30923
+ if (metadata.charAt(idx + 1) === '"') {
30924
+ idx++;
30925
+ idx2 = metadata.indexOf('" ', idx + 1);
30926
+ value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1);
30927
+ idx2++;
30928
+ } else {
30929
+ idx2 = metadata.indexOf(" ", idx + 1);
30930
+ if (idx2 === -1) {
30931
+ idx2 = metadata.length;
30444
30932
  }
30933
+ value = metadata.substring(idx + 1, idx2);
30934
+ }
30935
+ value = stripQuotes$2(value);
30936
+ if (value.endsWith('"')) {
30937
+ value = value.substring(0, value.length - 1);
30938
+ }
30939
+ if (value.startsWith("<") && value.endsWith(">")) {
30940
+ value = htmlText(value);
30445
30941
  }
30942
+ attrs.set(key, value);
30943
+ if (idx2 === metadata.length) {
30944
+ break
30945
+ }
30946
+ metadata = idx2 > 0 ? metadata.substring(idx2 + 1).trim() : "";
30947
+ } catch (e) {
30948
+ // We don't want to fail parsing the hub due to a failure parsing metadata. Also, we don't want to
30949
+ // overwhelm the log. Metadata is of marginal importance in IGV.
30446
30950
  }
30447
30951
  }
30952
+ return attrs
30953
+ }
30954
+
30955
+ /*
30956
+ https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30957
+ https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30958
+ https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30959
+ https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30960
+ */
30961
+
30962
+ const idMappings = new Map([
30963
+ ["hg38", "GCF_000001405.40"],
30964
+ ["mm39", "GCF_000001635.27"],
30965
+ ["mm10", "GCF_000001635.26"],
30966
+ ["bosTau9", "GCF_002263795.1"],
30967
+ ["canFam4", "GCF_011100685.1"],
30968
+ ["canFam6", "GCF_000002285.5"],
30969
+ ["ce11", "GCF_000002985.6"],
30970
+ ["dm6", "GCF_000001215.4"],
30971
+ ["galGal6", "GCF_000002315.6"],
30972
+ ["gorGor6", "GCF_008122165.1"],
30973
+ ["macFas5", "GCA_000364345.1"],
30974
+ ["panTro6", "GCA_002880755.3"],
30975
+ ["rn6", "GCF_000001895.5"],
30976
+ ["rn7", "GCF_015227675.2"],
30977
+ ["sacCer3", "GCF_000146045.2"],
30978
+ ["sacCer2", "GCF_000146045.2"],
30979
+ ["susScr11", "GCF_000003025.6"],
30980
+ ["taeGut1", "GCF_000002275.3"],
30981
+ ["tetNig2", "GCF_000002275.3"],
30982
+ ["xenTro10", "GCF_000002035.6"],
30983
+ ["xenTro9", "GCF_000002035.6"],
30984
+ ["tair10", "GCF_000001735.4"],
30985
+ ]);
30448
30986
 
30449
- getDefaultPosition() {
30450
- return this.genomeStanza.getProperty("defaultPos")
30987
+ class Hub {
30988
+
30989
+ static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30990
+ static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30991
+ "cpgIslandExtUnmasked", "windowMasker"])
30992
+
30993
+ constructor(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas) {
30994
+
30995
+ this.url = url;
30996
+ this.hubStanza = hubStanza;
30997
+ this.genomeStanzas = genomeStanzas;
30998
+ this.trackStanzas = trackStanzas;
30999
+ this.groupStanzas = groupStanzas;
31000
+ this.trackHubMap = new Map();
31001
+
31002
+ // trackStanzas will not be null if this is a "onefile" hub
31003
+ if (trackStanzas) {
31004
+ const genomeId = genomeStanzas[0].getProperty("genome"); // Assumption here this is a single genome hub
31005
+ this.trackHubMap.set(genomeId, new TrackDbHub(trackStanzas, groupStanzas));
31006
+ }
30451
31007
  }
30452
31008
 
31009
+
31010
+ getName() {
31011
+ return this.hubStanza.getProperty("hub")
31012
+ }
31013
+
31014
+ getShortLabel() {
31015
+ return this.hubStanza.getProperty("shortLabel")
31016
+ }
31017
+
31018
+ getLongLabel() {
31019
+ return this.hubStanza.getProperty("longLabel")
31020
+ }
31021
+
31022
+ getDescriptionUrl() {
31023
+ return this.hubStanza.getProperty("descriptionUrl")
31024
+ }
31025
+
31026
+
30453
31027
  /* Example genome stanza
30454
31028
  genome GCF_000186305.1
30455
31029
  taxId 176946
@@ -30468,133 +31042,126 @@
30468
31042
  isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30469
31043
  */
30470
31044
 
30471
- getGenomeConfig(options = {}) {
30472
- // TODO -- add blat? htmlPath?
31045
+ getGenomeConfig(genomeId) {
31046
+
31047
+ const genomeStanza = genomeId ? this.genomeStanzas.find(s => s.getProperty("genome") === genomeId) : this.genomeStanzas[0];
31048
+ if (!genomeStanza) {
31049
+ throw new Error(`Genome not found in hub: ${genomeId}`)
31050
+ }
31051
+ return this.#getGenomeConfig(genomeStanza)
31052
+ }
31053
+
31054
+ #getGenomeConfig(genomeStanza) {
30473
31055
 
30474
- const id = this.genomeStanza.getProperty("genome");
31056
+ const id = genomeStanza.getProperty("genome");
30475
31057
  const gsName =
30476
31058
  this.hubStanza.getProperty("shortLabel") ||
30477
- this.genomeStanza.getProperty("scientificName") ||
30478
- this.genomeStanza.getProperty("organism") ||
30479
- this.genomeStanza.getProperty("description");
31059
+ genomeStanza.getProperty("scientificName") ||
31060
+ genomeStanza.getProperty("organism") ||
31061
+ genomeStanza.getProperty("description");
30480
31062
  const name = gsName + (gsName ? ` (${id})` : ` ${id}`);
30481
31063
 
30482
31064
  const config = {
30483
- hubURL: this.url,
31065
+
30484
31066
  id: id,
30485
31067
  name: name,
30486
- twoBitURL: this.baseURL + this.genomeStanza.getProperty("twoBitPath"),
31068
+ twoBitURL: genomeStanza.getProperty("twoBitPath"),
30487
31069
  nameSet: "ucsc",
31070
+ hubs: [this.url]
30488
31071
  };
30489
31072
 
30490
- if (this.genomeStanza.hasProperty("chromSizesURL")) {
30491
- config.chromSizesURL = this.genomeStanza.getProperty("chromSizesURL");
31073
+ if (genomeStanza.hasProperty("chromSizes")) {
31074
+ config.chromSizesURL = genomeStanza.getProperty("chromSizes");
30492
31075
  } else {
30493
31076
  config.wholeGenomeView = false;
30494
31077
  config.showChromosomeWidget = false;
30495
31078
  }
30496
31079
 
30497
- if (this.genomeStanza.hasProperty("defaultPos")) {
30498
- const hubLocus = this.genomeStanza.getProperty("defaultPos");
31080
+ if (genomeStanza.hasProperty("defaultPos")) {
31081
+ const hubLocus = genomeStanza.getProperty("defaultPos");
30499
31082
  // Strip out coordinates => whole chromosome view
30500
- if (hubLocus) {
30501
- const idx = hubLocus.lastIndexOf(":");
30502
- config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus;
30503
- }
31083
+ // if (hubLocus) {
31084
+ // const idx = hubLocus.lastIndexOf(":")
31085
+ // config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus
31086
+ // }
31087
+ config.locus = hubLocus;
30504
31088
  }
30505
31089
 
30506
- if (this.genomeStanza.hasProperty("blat")) {
30507
- config.blat = this.baseURL + this.genomeStanza.getProperty("blat");
30508
- }
30509
- if (this.genomeStanza.hasProperty("chromAliasBb")) {
30510
- config.chromAliasBbURL = this.baseURL + this.genomeStanza.getProperty("chromAliasBb");
31090
+ if (genomeStanza.hasProperty("blat")) {
31091
+ config.blat = genomeStanza.getProperty("blat");
30511
31092
  }
30512
- if (this.genomeStanza.hasProperty("chromAlias")) {
30513
- config.aliasURL = this.baseURL + this.genomeStanza.getProperty("chromAlias");
31093
+ if (genomeStanza.hasProperty("chromAliasBb")) {
31094
+ config.chromAliasBbURL = genomeStanza.getProperty("chromAliasBb");
30514
31095
  }
30515
- if (this.genomeStanza.hasProperty("twoBitBptURL")) {
30516
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptURL");
31096
+ if (genomeStanza.hasProperty("chromAlias")) {
31097
+ config.aliasURL = genomeStanza.getProperty("chromAlias");
30517
31098
  }
30518
-
30519
- if (this.genomeStanza.hasProperty("twoBitBptUrl")) {
30520
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl");
31099
+ if (genomeStanza.hasProperty("twoBitBptURL")) {
31100
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptURL");
30521
31101
  }
30522
31102
 
30523
- // chromSizes can take a very long time to load, and is not useful with the default WGV = off
30524
- if (options.includeChromSizes && this.genomeStanza.hasProperty("chromSizes")) {
30525
- config.chromSizesURL = this.baseURL + this.genomeStanza.getProperty("chromSizes");
31103
+ if (genomeStanza.hasProperty("twoBitBptUrl")) {
31104
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptUrl");
30526
31105
  }
30527
31106
 
30528
31107
  if (this.hubStanza.hasProperty("longLabel")) {
30529
31108
  config.description = this.hubStanza.getProperty("longLabel").replace("/", "\n");
30530
31109
  } else {
30531
31110
  config.description = config.id;
30532
- if (this.genomeStanza.hasProperty("description")) {
30533
- config.description += `\n${this.genomeStanza.getProperty("description")}`;
31111
+ if (genomeStanza.hasProperty("description")) {
31112
+ config.description += `\n${genomeStanza.getProperty("description")}`;
30534
31113
  }
30535
- if (this.genomeStanza.hasProperty("organism")) {
30536
- config.description += `\n${this.genomeStanza.getProperty("organism")}`;
31114
+ if (genomeStanza.hasProperty("organism")) {
31115
+ config.description += `\n${genomeStanza.getProperty("organism")}`;
30537
31116
  }
30538
- if (this.genomeStanza.hasProperty("scientificName")) {
30539
- config.description += `\n${this.genomeStanza.getProperty("scientificName")}`;
31117
+ if (genomeStanza.hasProperty("scientificName")) {
31118
+ config.description += `\n${genomeStanza.getProperty("scientificName")}`;
30540
31119
  }
30541
31120
 
30542
- if (this.genomeStanza.hasProperty("htmlPath")) {
30543
- config.infoURL = this.baseURL + this.genomeStanza.getProperty("htmlPath");
31121
+ if (genomeStanza.hasProperty("htmlPath")) {
31122
+ config.infoURL = genomeStanza.getProperty("htmlPath");
30544
31123
  }
30545
31124
  }
30546
31125
 
30547
- // Search for cytoband
30548
- /*
30549
- track cytoBandIdeo
30550
- shortLabel Chromosome Band (Ideogram)
30551
- longLabel Ideogram for Orientation
30552
- group map
30553
- visibility dense
30554
- type bigBed 4 +
30555
- bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb
30556
- */
30557
- const cytoStanza = this.trackStanzas.filter(t => "cytoBandIdeo" === t.name && t.hasProperty("bigDataUrl"));
30558
- if (cytoStanza.length > 0) {
30559
- config.cytobandBbURL = this.baseURL + cytoStanza[0].getProperty("bigDataUrl");
30560
- }
30561
-
30562
31126
  // Tracks.
30563
31127
  const filter = (t) => !Hub.filterTracks.has(t.name) && "hide" !== t.getProperty("visibility");
30564
31128
  config.tracks = this.#getTracksConfigs(filter);
30565
31129
 
30566
-
30567
31130
  return config
30568
31131
  }
30569
31132
 
30570
- getGroupedTrackConfigurations() {
30571
-
30572
- // Organize track configs by group
30573
- const trackConfigMap = new Map();
30574
- for (let c of this.#getTracksConfigs()) {
30575
- if (c.name === "cytoBandIdeo") continue
30576
- const groupName = c.group || "other";
30577
- if (trackConfigMap.has(groupName)) {
30578
- trackConfigMap.get(groupName).push(c);
30579
- } else {
30580
- trackConfigMap.set(groupName, [c]);
30581
- }
31133
+ async getGroupedTrackConfigurations(genomeId) {
31134
+ let trackHub = await this.#getTrackDbHub(genomeId);
31135
+ if (!trackHub && idMappings.has(genomeId)) {
31136
+ trackHub = await this.#getTrackDbHub(idMappings.get(genomeId));
30582
31137
  }
31138
+ if (!trackHub) {
31139
+ console.log(`Warning: no trackDB found for genomeId ${genomeId}.`);
31140
+ }
31141
+ return trackHub ? trackHub.getGroupedTrackConfigurations() : []
31142
+ }
30583
31143
 
30584
- // Build group structure
30585
- const groupStanazMap = this.groupStanzas ?
30586
- new Map(this.groupStanzas.map(groupStanza => [groupStanza.getProperty("name"), groupStanza])) :
30587
- new Map();
30588
-
30589
- return Array.from(trackConfigMap.keys()).map(groupName => {
30590
- return {
30591
- label: groupStanazMap.has(groupName) ? groupStanazMap.get(groupName).getProperty("label") : groupName,
30592
- tracks: trackConfigMap.get(groupName)
31144
+ async #getTrackDbHub(genomeId) {
31145
+ let trackHub = this.trackHubMap.get(genomeId);
31146
+ if (!trackHub) {
31147
+ for (let stanza of this.genomeStanzas) {
31148
+ if (genomeId === stanza.getProperty("genome")) {
31149
+ try {
31150
+ const trackDbURL = stanza.getProperty("trackDb");
31151
+ const trackStanzas = await loadStanzas(trackDbURL);
31152
+ trackHub = new TrackDbHub(trackStanzas, this.groupStanzas);
31153
+ this.trackHubMap.set(genomeId, trackHub);
31154
+ } catch (error) {
31155
+ console.error(`Error loading trackDb file: ${stanza.getProperty("trackDb")}`, error);
31156
+ }
31157
+ break
31158
+ }
30593
31159
  }
30594
- })
30595
-
31160
+ }
31161
+ return trackHub
30596
31162
  }
30597
31163
 
31164
+
30598
31165
  /**
30599
31166
  * Return an array of igv track config objects that satisfy the filter
30600
31167
  */
@@ -30632,7 +31199,7 @@
30632
31199
  "id": t.getProperty("track"),
30633
31200
  "name": t.getProperty("shortLabel"),
30634
31201
  "format": format,
30635
- "url": this.baseURL + t.getProperty("bigDataUrl"),
31202
+ "url": t.getProperty("bigDataUrl"),
30636
31203
  "displayMode": t.displayMode,
30637
31204
  };
30638
31205
 
@@ -30643,7 +31210,7 @@
30643
31210
  if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30644
31211
  if (config.description) config.description += "<br/>";
30645
31212
  config.description =
30646
- `<a target="_blank" href="${this.baseURL + t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
31213
+ `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
30647
31214
  } else if (t.hasProperty("longLabel")) {
30648
31215
  config.description = t.getProperty("longLabel");
30649
31216
  }
@@ -30675,7 +31242,7 @@
30675
31242
  max = Number.parseInt(tokens[1]);
30676
31243
  }
30677
31244
  if (Number.isNaN(max) || Number.isNaN(min)) {
30678
- console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
31245
+ console.warn(`Unexpected viewLimits value in track line: ${t.getProperty("viewLimits")}`);
30679
31246
  } else {
30680
31247
  config.min = min;
30681
31248
  config.max = max;
@@ -30694,15 +31261,15 @@
30694
31261
  config.searchIndex = t.getProperty("searchIndex");
30695
31262
  }
30696
31263
  if (t.hasProperty("searchTrix")) {
30697
- config.trixURL = this.baseURL + t.getProperty("searchTrix");
31264
+ config.trixURL = t.getProperty("searchTrix");
30698
31265
  }
30699
31266
 
30700
31267
  if (t.hasProperty("group")) {
30701
- config.group = t.getProperty("group");
30702
- if (this.groupPriorityMap && this.groupPriorityMap.has(config.group)) {
30703
- const nextPriority = this.groupPriorityMap.get(config.group) + 1;
31268
+ config._group = t.getProperty("group");
31269
+ if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
31270
+ const nextPriority = this.groupPriorityMap.get(config._group) + 1;
30704
31271
  config.order = nextPriority;
30705
- this.groupPriorityMap.set(config.group, nextPriority);
31272
+ this.groupPriorityMap.set(config._group, nextPriority);
30706
31273
  }
30707
31274
  }
30708
31275
 
@@ -30711,96 +31278,92 @@
30711
31278
 
30712
31279
  }
30713
31280
 
30714
- function firstWord(str) {
30715
- const idx = str.indexOf(' ');
30716
- return idx > 0 ? str.substring(0, idx) : str
30717
- }
31281
+ /*
31282
+ https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
31283
+ https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
31284
+ https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
31285
+ https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
31286
+ */
30718
31287
 
30719
- class Stanza {
31288
+ const urlProperties = new Set(["descriptionUrl", "desriptionUrl",
31289
+ "twoBitPath", "blat", "chromAliasBb", "twoBitBptURL", "twoBitBptUrl", "htmlPath", "bigDataUrl",
31290
+ "genomesFile", "trackDb", "groups", "include", "html", "searchTrix", "groups",
31291
+ "chromSizes"]);
30720
31292
 
30721
- properties = new Map()
30722
31293
 
30723
- constructor(type, name) {
30724
- this.type = type;
30725
- this.name = name;
30726
- }
31294
+ const hubCache = new Map();
30727
31295
 
30728
- setProperty(key, value) {
30729
- this.properties.set(key, value);
31296
+ async function loadHub(url) {
31297
+ if (hubCache.has(url)) {
31298
+ return hubCache.get(url)
30730
31299
  }
30731
31300
 
30732
- getProperty(key) {
30733
- if (this.properties.has(key)) {
30734
- return this.properties.get(key)
30735
- } else if (this.parent) {
30736
- return this.parent.getProperty(key)
30737
- } else {
30738
- return undefined
30739
- }
31301
+ const stanzas = await loadStanzas(url);
31302
+ if (stanzas.length < 1) {
31303
+ throw new Error("Empty hub file")
30740
31304
  }
30741
31305
 
30742
- hasProperty(key) {
30743
- if (this.properties.has(key)) {
30744
- return true
30745
- } else if (this.parent) {
30746
- return this.parent.hasProperty(key)
30747
- } else {
30748
- return false
30749
- }
31306
+ const hubStanza = stanzas[0];
31307
+ if (hubStanza.type !== "hub") {
31308
+ throw new Error("First stanza must be a hub stanza")
30750
31309
  }
30751
31310
 
30752
- get format() {
30753
- const type = this.getProperty("type");
30754
- if (type) {
30755
- // Trim extra bed qualifiers (e.g. bigBed + 4)
30756
- return firstWord(type)
31311
+ let genomeStanzas;
31312
+ let trackStanzas;
31313
+ if (hubStanza.getProperty("useOneFile") === "on") {
31314
+ // This is a "onefile" hub, all stanzas are in the same file
31315
+ if (stanzas[1].type !== "genome") {
31316
+ throw new Error("Unexpected hub file -- expected 'genome' stanza but found " + stanzas[1].type)
30757
31317
  }
30758
- return undefined // unknown type
30759
- }
31318
+ const genomeStanza = stanzas[1];
31319
+ genomeStanzas = [genomeStanza];
31320
+ trackStanzas = stanzas.slice(2);
30760
31321
 
30761
- /**
30762
- * IGV display mode
30763
- */
30764
- get displayMode() {
30765
- let viz = this.getProperty("visibility");
30766
- if (!viz) {
30767
- return "COLLAPSED"
30768
- } else {
30769
- viz = viz.toLowerCase();
30770
- switch (viz) {
30771
- case "dense":
30772
- return "COLLAPSED"
30773
- case "pack":
30774
- return "EXPANDED"
30775
- case "squish":
30776
- return "SQUISHED"
30777
- default:
30778
- return "COLLAPSED"
31322
+ // If this is an assembly check chromSizes. This file can be very large, and not needed if whole genome view
31323
+ // is not enabled. Remove it if > 100 kb
31324
+ if (genomeStanza.hasOwnProperty("chromSizes")) {
31325
+ const chromSizes = genomeStanza.getProperty("chromSizes");
31326
+ try {
31327
+ const contentLength = await igvxhr.getContentLength(chromSizes);
31328
+ if (contentLength > 100000) {
31329
+ genomeStanza.removeProperty("chromSizes");
31330
+ }
31331
+ } catch (e) {
31332
+ console.error(`Error getting content length for chromSizes ${chromSizes}`, e);
30779
31333
  }
30780
- }
30781
- }
30782
- }
30783
31334
 
31335
+ }
30784
31336
 
30785
- /**
30786
- * Return the content length of the resource. If the content length cannot be determined return null;
30787
- * @param url
30788
- * @returns {Promise<number|string>}
30789
- */
30790
- async function getContentLength(url) {
30791
- try {
30792
- const response = await fetch(url, {method: 'HEAD'});
30793
- const headers = response.headers;
30794
- if (headers.has("content-length")) {
30795
- return headers.get("content-length")
30796
- } else {
30797
- return null
31337
+ } else {
31338
+ if (!hubStanza.hasProperty("genomesFile")) {
31339
+ throw new Error("hub.txt must specify 'genomesFile'")
30798
31340
  }
30799
- } catch (e) {
30800
- return null
31341
+ genomeStanzas = await loadStanzas(hubStanza.getProperty("genomesFile"));
30801
31342
  }
31343
+
31344
+ // Load group files for all genomes, if any.
31345
+ const uniqGroupURLs = new Set();
31346
+ genomeStanzas.forEach(s => {
31347
+ const groupURL = s.getProperty("groups");
31348
+ if (groupURL) uniqGroupURLs.add(groupURL);
31349
+
31350
+ });
31351
+ const groupStanzas = [];
31352
+ const groupPromises = Array.from(uniqGroupURLs).map(async url => {
31353
+ const stanza = await loadStanzas(url);
31354
+ return stanza
31355
+ });
31356
+ const groupResults = await Promise.all(groupPromises);
31357
+ groupResults.forEach(stanza => groupStanzas.push(...stanza));
31358
+
31359
+ const hub = new Hub(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas);
31360
+
31361
+ hubCache.set(url, hub);
31362
+
31363
+ return hub
30802
31364
  }
30803
31365
 
31366
+
30804
31367
  /**
30805
31368
  * Parse a UCSC file
30806
31369
  * @param url
@@ -30808,38 +31371,92 @@
30808
31371
  */
30809
31372
  async function loadStanzas(url) {
30810
31373
 
30811
- const response = await fetch(url);
30812
- const data = await response.text();
31374
+ const idx = url.lastIndexOf("/");
31375
+ const baseURL = url.substring(0, idx + 1);
31376
+ const host = getHost(url);
31377
+
31378
+ //const response = await fetch(url)
31379
+ const data = await igvxhr.loadString(url, {}); //await response.text()
30813
31380
  const lines = data.split(/\n|\r\n|\r/g);
30814
31381
 
30815
31382
  const nodes = [];
30816
31383
  let currentNode;
30817
31384
  let startNewNode = true;
30818
- for (let line of lines) {
30819
- const indent = indentLevel(line);
30820
- const i = line.indexOf(' ', indent);
30821
- if (i < 0) {
31385
+ for (let i = 0; i < lines.length; i++) {
31386
+
31387
+ let line = lines[i].trim();
31388
+
31389
+ if (line.length == 0) {
30822
31390
  // Break - start a new node
30823
31391
  startNewNode = true;
30824
31392
  } else {
30825
- const key = line.substring(indent, i).trim();
30826
- if (key.startsWith("#")) continue
30827
- const value = line.substring(i + 1).trim();
31393
+ if (line.startsWith("#")) {
31394
+ continue
31395
+ }
31396
+
31397
+ while (line.endsWith('\\')) {
31398
+ i++;
31399
+ if (i >= lines.length) {
31400
+ break
31401
+ }
31402
+ line = line.substring(0, line.length - 1) + lines[i].trim();
31403
+ }
31404
+
31405
+ if (line.startsWith("include")) {
31406
+ const relativeURL = line.substring(8).trim();
31407
+ const includeURL = getDataURL(relativeURL, host, baseURL);
31408
+ const includeStanzas = await loadStanzas(includeURL);
31409
+ for (let s of includeStanzas) {
31410
+ nodes.push(s);
31411
+ }
31412
+ }
31413
+
31414
+
31415
+ const i = line.indexOf(' ');
31416
+ const key = line.substring(0, i).trim();
31417
+ let value = line.substring(i + 1).trim();
31418
+
31419
+ if (key === "type") {
31420
+ // The "type" property contains format and sometimes other information. For example, data range
31421
+ // on a bigwig "type bigWig 0 .5"
31422
+ const tokens = value.split(/\s+/);
31423
+ value = tokens[0];
31424
+ if (value === "bigWig" && tokens.length === 3) {
31425
+ // This is a bigWig with a range
31426
+ const min = tokens[1];
31427
+ const max = tokens[2];
31428
+ if (currentNode) {
31429
+ currentNode.setProperty("min", min);
31430
+ currentNode.setProperty("max", max);
31431
+ }
31432
+ }
31433
+
31434
+ } else if (!["shortLabel", "longLabel", "metadata", "label"].includes(key)) {
31435
+ const tokens = value.split(/\s+/);
31436
+ value = tokens[0];
31437
+ }
31438
+
31439
+ if (urlProperties.has(key) || value.endsWith("URL") || value.endsWith("Url")) {
31440
+ value = getDataURL(value, host, baseURL);
31441
+ }
31442
+
30828
31443
  if (startNewNode) {
30829
- // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks,
30830
- // so track stanzas are flattened
30831
- const newNode = new Stanza(key, value);
30832
- nodes.push(newNode);
30833
- currentNode = newNode;
31444
+ currentNode = new Stanza(key, value);
31445
+ nodes.push(currentNode);
30834
31446
  startNewNode = false;
30835
31447
  }
31448
+
30836
31449
  currentNode.setProperty(key, value);
30837
31450
  }
30838
31451
  }
30839
-
30840
31452
  return resolveParents(nodes)
30841
31453
  }
30842
31454
 
31455
+ function firstWord(str) {
31456
+ const idx = str.indexOf(' ');
31457
+ return idx > 0 ? str.substring(0, idx) : str
31458
+ }
31459
+
30843
31460
  function resolveParents(nodes) {
30844
31461
  const nodeMap = new Map();
30845
31462
  for (let n of nodes) {
@@ -30854,13 +31471,30 @@
30854
31471
  return nodes
30855
31472
  }
30856
31473
 
30857
- function indentLevel(str) {
30858
- let level = 0;
30859
- for (level = 0; level < str.length; level++) {
30860
- const c = str.charAt(level);
30861
- if (c !== ' ' && c !== '\t') break
31474
+ function getDataURL(url, host, baseURL) {
31475
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("gs://") || url.startsWith("s3://")) {
31476
+ return url
31477
+ } else if (url.startsWith("/")) {
31478
+ return host + url
31479
+ } else {
31480
+ return baseURL + url
30862
31481
  }
30863
- return level
31482
+ }
31483
+
31484
+ function getHost(url) {
31485
+ let host;
31486
+ if (url.startsWith("https://") || url.startsWith("http://")) {
31487
+ try {
31488
+ const tmp = new URL(url);
31489
+ host = `${tmp.protocol}//${tmp.host}`;
31490
+ } catch (e) {
31491
+ console.error("Error parsing base URL host", e);
31492
+ throw e
31493
+ }
31494
+ } else {
31495
+ host = '';
31496
+ }
31497
+ return host
30864
31498
  }
30865
31499
 
30866
31500
  const DEFAULT_GENOMES_URL = "https://igv.org/genomes/genomes.json";
@@ -30944,8 +31578,8 @@
30944
31578
  if ((genomeID.startsWith("GCA_") || genomeID.startsWith("GCF_")) && genomeID.length >= 13) {
30945
31579
  try {
30946
31580
  const hubURL = convertToHubURL(genomeID);
30947
- const hub = await Hub.loadHub(hubURL);
30948
- reference = hub.getGenomeConfig();
31581
+ const hub = await loadHub(hubURL);
31582
+ reference = hub.getGenomeConfig(genomeID);
30949
31583
  } catch (e) {
30950
31584
  console.error(e);
30951
31585
  }
@@ -32907,27 +33541,18 @@
32907
33541
  return [ r, g, b ]
32908
33542
  });
32909
33543
 
32910
- const colorForNA = appleCrayonRGB('magnesium');
32911
- const sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors'];
32912
-
32913
33544
  class SampleInfo {
32914
33545
 
32915
33546
  static emptySpaceReplacement = '|'
32916
-
32917
- sampleDictionary = {}
32918
- attributeNames = []
32919
- sampleMappingDictionary = {}
32920
- colorDictionary = {}
32921
- attributeRangeLUT = {}
33547
+ static colorForNA = appleCrayonRGB('magnesium')
33548
+ static sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors']
32922
33549
 
32923
33550
  constructor(browser) {
32924
-
32925
33551
  const found = browser.tracks.some(t => typeof t.getSamples === 'function');
32926
33552
  if (found.length > 0) {
32927
33553
  browser.sampleInfoControl.setButtonVisibility(true);
32928
33554
  }
32929
33555
  this.initialize();
32930
-
32931
33556
  }
32932
33557
 
32933
33558
  initialize() {
@@ -32954,47 +33579,60 @@
32954
33579
 
32955
33580
  getAttributes(sampleName) {
32956
33581
 
32957
- const key = 0 === Object.keys(this.sampleMappingDictionary) ? sampleName : (this.sampleMappingDictionary[sampleName] || sampleName);
33582
+ const key = this.sampleMappingDictionary[sampleName] || sampleName;
32958
33583
  return this.sampleDictionary[key]
32959
33584
  }
32960
33585
 
32961
- async loadSampleInfoFile(path) {
32962
- try {
32963
- const string = await igvxhr.loadString(path);
32964
- this.#processSampleInfoFileAsString(string);
32965
- this.sampleInfoFiles.push(path);
32966
- } catch (e) {
32967
- console.error(e.message);
33586
+ async loadSampleInfo(config) {
33587
+
33588
+ if (config.url) {
33589
+ await this.loadSampleInfoFile(config.url);
33590
+ } else {
33591
+
33592
+ const samples = { ...config };
33593
+ for (const [key, record] of Object.entries(samples)) {
33594
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33595
+ }
33596
+
33597
+ const [ value ] = Object.values(samples);
33598
+ const attributes = Object.keys(value);
33599
+
33600
+ this.loadSampleInfoHelper(attributes, samples);
33601
+
32968
33602
  }
32969
- }
32970
33603
 
32971
- #processSampleInfoFileAsString(string) {
33604
+ this.initialized = true;
33605
+ }
32972
33606
 
32973
- const sectionDictionary = createSectionDictionary(string);
33607
+ loadSampleInfoHelper(attributes, samples){
32974
33608
 
32975
- for (const [header, value] of Object.entries(sectionDictionary)) {
32976
- switch (header) {
32977
- case '#sampleTable':
32978
- this.#accumulateSampleTableDictionary(value);
32979
- break
32980
- case '#sampleMapping':
32981
- this.#accumulateSampleMappingDictionary(value);
32982
- break
32983
- case '#colors':
32984
- this.#accumulateColorScheme(value);
32985
- break
33609
+ // Establish the range of values for each attribute
33610
+ const lut = createAttributeRangeLUT(attributes, samples);
33611
+ accumulateDictionary(this.attributeRangeLUT, lut);
32986
33612
 
33613
+ // Ensure unique attribute names list
33614
+ const currentAttributeNameSet = new Set(this.attributeNames);
33615
+ for (const name of attributes) {
33616
+ if (!currentAttributeNameSet.has(name)) {
33617
+ this.attributeNames.push(name);
32987
33618
  }
32988
33619
  }
32989
33620
 
32990
- this.initialized = true;
33621
+ accumulateDictionary(this.sampleDictionary, samples);
32991
33622
 
32992
33623
  }
32993
33624
 
33625
+ async loadSampleInfoFile(path) {
33626
+ try {
33627
+ const string = await igvxhr.loadString(path);
33628
+ this.#processSampleInfoFileAsString(string);
33629
+ this.sampleInfoFiles.push(path);
33630
+ } catch (e) {
33631
+ console.error(e.message);
33632
+ }
33633
+ }
33634
+
32994
33635
  getAttributeColor(attribute, value) {
32995
- // if (value === 'NA') {
32996
- // console.log(`${ attribute } : ${ value }`)
32997
- // }
32998
33636
 
32999
33637
  let color;
33000
33638
 
@@ -33012,7 +33650,7 @@
33012
33650
 
33013
33651
  } else if (typeof value === "string") {
33014
33652
 
33015
- color = 'NA' === value ? colorForNA : stringToRGBString(value);
33653
+ color = 'NA' === value ? SampleInfo.colorForNA : SampleInfo.stringToRGBString(value);
33016
33654
 
33017
33655
  } else {
33018
33656
 
@@ -33080,6 +33718,27 @@
33080
33718
  return json
33081
33719
  }
33082
33720
 
33721
+ #processSampleInfoFileAsString(string) {
33722
+
33723
+ const sectionDictionary = createSectionDictionary(string);
33724
+
33725
+ for (const [header, value] of Object.entries(sectionDictionary)) {
33726
+ switch (header) {
33727
+ case '#sampleTable':
33728
+ this.#accumulateSampleTableDictionary(value);
33729
+ break
33730
+ case '#sampleMapping':
33731
+ this.#accumulateSampleMappingDictionary(value);
33732
+ break
33733
+ case '#colors':
33734
+ this.#accumulateColorScheme(value);
33735
+ break
33736
+
33737
+ }
33738
+ }
33739
+
33740
+ }
33741
+
33083
33742
  #accumulateSampleTableDictionary(lines) {
33084
33743
 
33085
33744
  // shift array with first item that is 'sample' or 'Linking_id'. Remaining items are attribute names
@@ -33119,22 +33778,11 @@
33119
33778
  } // for (lines)
33120
33779
 
33121
33780
  for (const [key, record] of Object.entries(samples)) {
33122
- samples[key] = toNumericalRepresentation(record);
33781
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33123
33782
  }
33124
33783
 
33125
- // Establish the range of values for each attribute
33126
- const lut = createAttributeRangeLUT(attributes, samples);
33127
- accumulateDictionary(this.attributeRangeLUT, lut);
33128
-
33129
- // Ensure unique attribute names list
33130
- const currentAttributeNameSet = new Set(this.attributeNames);
33131
- for (const name of attributes) {
33132
- if (!currentAttributeNameSet.has(name)) {
33133
- this.attributeNames.push(name);
33134
- }
33135
- }
33784
+ this.loadSampleInfoHelper(attributes, samples);
33136
33785
 
33137
- accumulateDictionary(this.sampleDictionary, samples);
33138
33786
  }
33139
33787
 
33140
33788
  #accumulateSampleMappingDictionary(lines) {
@@ -33235,7 +33883,7 @@
33235
33883
  this.colorDictionary[attribute] = attributeValue => {
33236
33884
 
33237
33885
  if ('NA' === attributeValue) {
33238
- return colorForNA
33886
+ return SampleInfo.colorForNA
33239
33887
  } else {
33240
33888
  const [min, max] = this.attributeRangeLUT[attribute];
33241
33889
  const interpolant = (attributeValue - min) / (max - min);
@@ -33255,8 +33903,34 @@
33255
33903
 
33256
33904
  }
33257
33905
 
33258
- }
33906
+ static toNumericalRepresentation(obj) {
33907
+ const result = Object.assign({}, obj);
33259
33908
 
33909
+ for (const [key, value] of Object.entries(result)) {
33910
+ if (typeof value === 'string' && !isNaN(value)) {
33911
+ result[key] = Number(value);
33912
+ }
33913
+ }
33914
+
33915
+ return result
33916
+ }
33917
+
33918
+ static stringToRGBString(str) {
33919
+ let hash = 0;
33920
+ for (let i = 0; i < str.length; i++) {
33921
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
33922
+ }
33923
+
33924
+ let color = [];
33925
+ for (let i = 0; i < 3; i++) {
33926
+ const value = (hash >> (i * 8)) & 0xff;
33927
+ color.push(value);
33928
+ }
33929
+
33930
+ return `rgb(${color.join(', ')})`
33931
+ }
33932
+
33933
+ }
33260
33934
 
33261
33935
  function createSectionDictionary(string) {
33262
33936
 
@@ -33267,14 +33941,14 @@
33267
33941
  let currentHeader;
33268
33942
 
33269
33943
  // If the first line does not start with a section header an initial #sampleTable is implied
33270
- if (!sampleInfoFileHeaders.includes(lines[0])) {
33944
+ if (!SampleInfo.sampleInfoFileHeaders.includes(lines[0])) {
33271
33945
  currentHeader = '#sampleTable';
33272
33946
  dictionary[currentHeader] = [];
33273
33947
  }
33274
33948
 
33275
33949
  for (const line of lines) {
33276
33950
 
33277
- if (sampleInfoFileHeaders.includes(line)) {
33951
+ if (SampleInfo.sampleInfoFileHeaders.includes(line)) {
33278
33952
  currentHeader = line;
33279
33953
  dictionary[currentHeader] = [];
33280
33954
  } else if (currentHeader && false === line.startsWith('#')) {
@@ -33293,7 +33967,6 @@
33293
33967
  }
33294
33968
  }
33295
33969
 
33296
-
33297
33970
  function createAttributeRangeLUT(names, dictionary) {
33298
33971
 
33299
33972
  const lut = {};
@@ -33339,35 +34012,6 @@
33339
34012
  return lut
33340
34013
  }
33341
34014
 
33342
- function toNumericalRepresentation(obj) {
33343
-
33344
- const result = Object.assign({}, obj);
33345
-
33346
- for (const [key, value] of Object.entries(result)) {
33347
- if (typeof value === 'string' && !isNaN(value)) {
33348
- result[key] = Number(value);
33349
- }
33350
- }
33351
-
33352
- return result
33353
- }
33354
-
33355
- function stringToRGBString(str) {
33356
-
33357
- let hash = 0;
33358
- for (let i = 0; i < str.length; i++) {
33359
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
33360
- }
33361
-
33362
- let color = [];
33363
- for (let i = 0; i < 3; i++) {
33364
- const value = (hash >> (i * 8)) & 0xff;
33365
- color.push(value);
33366
- }
33367
-
33368
- return `rgb(${color.join(', ')})`
33369
- }
33370
-
33371
34015
  const sampleInfoTileXShim = 8;
33372
34016
  const sampleInfoTileWidth = 16;
33373
34017
 
@@ -33976,7 +34620,11 @@
33976
34620
 
33977
34621
  checkCanvas() {
33978
34622
 
33979
- const width = this.browser.sampleNameViewportWidth || 0;
34623
+ let width = 0;
34624
+ if (true === this.browser.showSampleNames) {
34625
+ width = undefined === this.browser.sampleNameViewportWidth ? 0 : this.browser.sampleNameViewportWidth;
34626
+ }
34627
+
33980
34628
  this.ctx.canvas.width = width * window.devicePixelRatio;
33981
34629
  this.ctx.canvas.style.width = `${width}px`;
33982
34630
 
@@ -35088,7 +35736,7 @@
35088
35736
  let element = document.createElement('div');
35089
35737
  element.textContent = 'Separate tracks';
35090
35738
 
35091
- function click(e) {
35739
+ async function click(e) {
35092
35740
 
35093
35741
  // Capture state which will be nulled when track is removed
35094
35742
  const groupAutoscale = this.autoscale;
@@ -35104,9 +35752,10 @@
35104
35752
  track.autoscaleGroup = name;
35105
35753
  }
35106
35754
  track.isMergedTrack = false;
35107
- browser.addTrack(track.config, track);
35755
+ browser.addTrack(track);
35108
35756
  }
35109
- browser.updateViews();
35757
+ await browser.updateViews();
35758
+ browser.reorderTracks();
35110
35759
  }
35111
35760
 
35112
35761
  return {element, click}
@@ -35201,7 +35850,7 @@
35201
35850
  }
35202
35851
  }
35203
35852
 
35204
- function trackOverlayClickHandler(e) {
35853
+ async function trackOverlayClickHandler(e) {
35205
35854
 
35206
35855
  if (true === isOverlayTrackCriteriaMet(this.browser)) {
35207
35856
 
@@ -35238,9 +35887,9 @@
35238
35887
  track.trackView.dispose();
35239
35888
  }
35240
35889
 
35241
- this.browser.addTrack(config, mergedTrack);
35242
- mergedTrack.trackView.updateViews();
35243
-
35890
+ this.browser.addTrack(mergedTrack);
35891
+ await mergedTrack.trackView.updateViews();
35892
+ this.browser.reorderTracks();
35244
35893
  }
35245
35894
 
35246
35895
  }
@@ -36847,14 +37496,45 @@
36847
37496
 
36848
37497
 
36849
37498
  present(options, e) {
36850
-
36851
37499
  this.label.textContent = options.label;
36852
37500
  this._input.value = options.value;
36853
37501
  this.callback = options.callback || options.click;
36854
37502
 
36855
- const { top} = e.currentTarget.parentElement.getBoundingClientRect();
36856
- this.container.style.top = `${ top }px`;
36857
- this.container.style.display = 'flex';
37503
+ this.container.style.display = '';
37504
+
37505
+ // Get click coordinates
37506
+ const clickX = e.clientX;
37507
+ const clickY = e.clientY;
37508
+
37509
+ // Get dialog dimensions
37510
+ const dialogWidth = this.container.offsetWidth;
37511
+ const dialogHeight = this.container.offsetHeight;
37512
+
37513
+ // Calculate available space
37514
+ const windowWidth = window.innerWidth;
37515
+ const windowHeight = window.innerHeight;
37516
+
37517
+ // Calculate position to keep dialog on screen
37518
+ let left = clickX;
37519
+ let top = clickY;
37520
+
37521
+ // Adjust horizontal position if dialog would go off screen
37522
+ if (left + dialogWidth > windowWidth) {
37523
+ left = windowWidth - dialogWidth - 10; // 10px padding from edge
37524
+ }
37525
+
37526
+ // Adjust vertical position if dialog would go off screen
37527
+ if (top + dialogHeight > windowHeight) {
37528
+ top = windowHeight - dialogHeight - 10; // 10px padding from edge
37529
+ }
37530
+
37531
+ // Ensure minimum distance from edges
37532
+ left = Math.max(10, left);
37533
+ top = Math.max(10, top);
37534
+
37535
+ // Apply positions
37536
+ this.container.style.left = `${left}px`;
37537
+ this.container.style.top = `${top}px`;
36858
37538
  }
36859
37539
  }
36860
37540
 
@@ -50502,7 +51182,7 @@
50502
51182
 
50503
51183
  let url = this.url.slice(); // slice => copy
50504
51184
  if (this.config.oauthToken) {
50505
- const token = resolveToken(this.config.oauthToken);
51185
+ const token = await resolveToken(this.config.oauthToken);
50506
51186
  headers['Authorization'] = `Bearer ${token}`;
50507
51187
  }
50508
51188
 
@@ -55267,7 +55947,7 @@
55267
55947
  async function openH5File(options) {
55268
55948
 
55269
55949
  // Some clients (notably igv-webapp) pass a File reference in the url field. Fix this
55270
- if(options.url && isBlobLike(options.url)) {
55950
+ if (options.url && isBlobLike(options.url)) {
55271
55951
  options.file = options.url;
55272
55952
  options.url = undefined;
55273
55953
  }
@@ -55297,26 +55977,30 @@
55297
55977
 
55298
55978
  async function readExternalIndex(options) {
55299
55979
 
55300
- let indexReader;
55301
- if(options.indexReader) {
55302
- indexReader = options.indexReader;
55303
- }
55304
- else if(options.index) {
55980
+ if (options.index) {
55305
55981
  return options.index
55306
- } else if (options.indexURL) {
55307
- indexReader = new RemoteFile({url: options.indexURL});
55308
- } else if (options.indexPath) {
55309
- indexReader = new NodeLocalFile({path: options.indexPath});
55310
- } else if (options.indexFile) {
55311
- indexReader = new BlobFile({file: options.indexFile});
55312
- }
55313
- if (indexReader) {
55982
+ } else {
55983
+ let indexReader;
55984
+ if (options.indexReader) {
55985
+ indexReader = options.indexReader;
55986
+ } else {
55987
+ let indexOptions;
55988
+ if (options.indexURL) {
55989
+ indexOptions = Object.assign({url: options.indexURL}, options);
55990
+ } else if (options.indexPath) {
55991
+ indexOptions = Object.assign({path: options.indexPath}, options);
55992
+ } else if (options.indexFile) {
55993
+ indexOptions = Object.assign({file: options.indexFile}, options);
55994
+ } else {
55995
+ return undefined
55996
+ }
55997
+ indexReader = getReaderFor(indexOptions);
55998
+ }
55314
55999
  const indexFileContents = await indexReader.read();
55315
56000
  const indexFileJson = new TextDecoder().decode(indexFileContents);
55316
56001
  return JSON.parse(indexFileJson)
55317
- } else {
55318
- return undefined
55319
56002
  }
56003
+
55320
56004
  }
55321
56005
 
55322
56006
 
@@ -55394,9 +56078,9 @@
55394
56078
  * @param {string} h5_file - path for the pytor file
55395
56079
  * @param {integer} bin_size - bin size
55396
56080
  */
55397
- constructor(h5_file, bin_size=100000){
56081
+ constructor(config, bin_size=100000){
55398
56082
 
55399
- this.h5_file = h5_file;
56083
+ this.config = config;
55400
56084
  this.bin_size = bin_size;
55401
56085
  this.h5_obj = undefined;
55402
56086
  this.pytorKeys = [];
@@ -55406,11 +56090,8 @@
55406
56090
  async fetch(){
55407
56091
 
55408
56092
  if(!this.h5_obj) {
55409
- this.h5_obj = await openH5File({
55410
- url: this.h5_file,
55411
- fetchSize: 1000000,
55412
- maxSize: 200000000
55413
- });
56093
+ const options = Object.assign(this.config, {fetchSize: 1000000, maxSize: 200000000});
56094
+ this.h5_obj = await openH5File(options);
55414
56095
  }
55415
56096
  return this.h5_obj
55416
56097
  }
@@ -64446,7 +65127,7 @@ ${indent}columns: ${matrix.columns}
64446
65127
  this.set_available_callers();
64447
65128
 
64448
65129
  } else {
64449
- this.cnvpytor_obj = new HDF5Reader(this.config.url, this.bin_size);
65130
+ this.cnvpytor_obj = new HDF5Reader(this.config, this.bin_size);
64450
65131
  // get chrom list that currently user viewing
64451
65132
  let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
64452
65133
 
@@ -69434,6 +70115,23 @@ ${indent}columns: ${matrix.columns}
69434
70115
  return this.genome.getChromosome(this.chr)
69435
70116
  }
69436
70117
 
70118
+ /**
70119
+ * Update reference frame based on new viewport width
70120
+ * @param {number} viewportWidth - The calculated viewport width
70121
+ */
70122
+ updateForViewportWidth(viewportWidth) {
70123
+ const {chr} = this;
70124
+ const {bpLength} = this.getChromosome();
70125
+ const viewportWidthBP = this.toBP(viewportWidth);
70126
+
70127
+ // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
70128
+ if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
70129
+ this.bpPerPixel = bpLength / viewportWidth;
70130
+ } else {
70131
+ this.end = this.start + this.toBP(viewportWidth);
70132
+ }
70133
+ }
70134
+
69437
70135
  getMultiLocusLabelBPLengthOnly(pixels) {
69438
70136
  const margin = '&nbsp';
69439
70137
  const ss = Math.floor(this.start) + 1;
@@ -69519,7 +70217,7 @@ ${indent}columns: ${matrix.columns}
69519
70217
  })
69520
70218
  }
69521
70219
 
69522
- const _version = "3.2.6";
70220
+ const _version = "3.3.0";
69523
70221
  function version() {
69524
70222
  return _version
69525
70223
  }
@@ -69563,10 +70261,24 @@ ${indent}columns: ${matrix.columns}
69563
70261
  this.select.setAttribute('name', 'chromosome-select-widget');
69564
70262
  this.container.appendChild(this.select);
69565
70263
 
69566
- this.select.addEventListener('change', () => {
70264
+ this.select.addEventListener('change', async () => {
69567
70265
  this.select.blur();
69568
70266
  if (this.select.value !== '' && maximumSequenceCountExceeded !== this.select.value) {
69569
- browser.search(this.select.value);
70267
+
70268
+ if (this.select.value.trim().toLowerCase() === "all" || this.select.value === "*") {
70269
+ if (browser.genome.wholeGenomeView) {
70270
+ const wgChr = browser.genome.getChromosome("all");
70271
+ return {chr: "all", start: 0, end: wgChr.bpLength}
70272
+ }
70273
+ } else {
70274
+ const chromosome = await browser.genome.loadChromosome(this.select.value);
70275
+ const locusObject = {chr: chromosome.name};
70276
+ if (locusObject.start === undefined && locusObject.end === undefined) {
70277
+ locusObject.start = 0;
70278
+ locusObject.end = chromosome.bpLength;
70279
+ }
70280
+ browser.updateLoci([locusObject]);
70281
+ }
69570
70282
  }
69571
70283
  });
69572
70284
 
@@ -69591,7 +70303,9 @@ ${indent}columns: ${matrix.columns}
69591
70303
  this.genome = genome;
69592
70304
 
69593
70305
  // Start with explicit chromosome name list
69594
- const list = genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) || [];
70306
+ const list = (genome.wgChromosomeNames) ?
70307
+ genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) :
70308
+ [];
69595
70309
 
69596
70310
  if (this.showAllChromosomes && genome.chromosomeNames.length > 1) {
69597
70311
  const exclude = new Set(list);
@@ -70817,7 +71531,7 @@ ${indent}columns: ${matrix.columns}
70817
71531
  });
70818
71532
 
70819
71533
  this.searchInput.addEventListener('change', () => {
70820
- browser.doSearch(this.searchInput.value);
71534
+ this.doSearch(this.searchInput.value);
70821
71535
  });
70822
71536
 
70823
71537
  const searchIconContainer = document.createElement('div');
@@ -70828,7 +71542,7 @@ ${indent}columns: ${matrix.columns}
70828
71542
  searchIconContainer.appendChild(searchIcon);
70829
71543
 
70830
71544
  searchIconContainer.addEventListener('click', () => {
70831
- browser.doSearch(this.searchInput.value);
71545
+ this.doSearch(this.searchInput.value);
70832
71546
  });
70833
71547
 
70834
71548
  this.windowSizePanel = new WindowSizePanel(locusSizeGroup, browser);
@@ -70976,6 +71690,22 @@ ${indent}columns: ${matrix.columns}
70976
71690
  this.navigation.style.display = 'flex';
70977
71691
  }
70978
71692
 
71693
+ /**
71694
+
71695
+ * Search for the locus string -- this function is called from the navbar search box, and is not part of the API.
71696
+ * Wraps ```search``` and presents an error dialog if false.
71697
+ *
71698
+ * @param locus
71699
+ * @param init
71700
+ * @returns {Promise<void>}
71701
+ */
71702
+ async doSearch(locus) {
71703
+ const success = await this.browser.search(locus);
71704
+ if (!success) {
71705
+ this.browser.alert.present(new Error(`Unrecognized locus: <b> ${locus} </b>`));
71706
+ }
71707
+ }
71708
+
70979
71709
  }
70980
71710
 
70981
71711
  function logo() {
@@ -72227,6 +72957,9 @@ ${indent}columns: ${matrix.columns}
72227
72957
  aliasRecord["_cap"] = cap;
72228
72958
  }
72229
72959
 
72960
+ const chrPrefixAlias = aliasRecord.chr.startsWith("chr") ? aliasRecord.chr.substring(3) : "chr" + aliasRecord.chr;
72961
+ aliasRecord["_chrprefix_"] = chrPrefixAlias;
72962
+
72230
72963
  }
72231
72964
 
72232
72965
  }
@@ -72251,7 +72984,7 @@ ${indent}columns: ${matrix.columns}
72251
72984
  }
72252
72985
 
72253
72986
  async preload(chrNames) {
72254
- await this.reader.preload();
72987
+ await this.reader.preload();
72255
72988
  for(let nm of chrNames) {
72256
72989
  await this.search(nm);
72257
72990
  }
@@ -72303,12 +73036,6 @@ ${indent}columns: ${matrix.columns}
72303
73036
  }
72304
73037
  return this.aliasRecordCache.get(alias)
72305
73038
  }
72306
-
72307
- async getChromosomeNames() {
72308
- await this.reader.loadHeader();
72309
- return Array.from(this.reader.chrNames)
72310
- }
72311
-
72312
73039
  }
72313
73040
 
72314
73041
  /**
@@ -72333,10 +73060,9 @@ ${indent}columns: ${matrix.columns}
72333
73060
  this.genome = genome;
72334
73061
  }
72335
73062
 
72336
- async preload() {
72337
- return this.loadAliases();
73063
+ async preload(chrNames) {
73064
+ // A no-op, this is a text file, no need to preload
72338
73065
  }
72339
-
72340
73066
  /**
72341
73067
  * Return the canonical chromosome name for the alias. If none found return the alias
72342
73068
  *
@@ -72502,7 +73228,7 @@ ${indent}columns: ${matrix.columns}
72502
73228
  for (let line of lines) {
72503
73229
 
72504
73230
  const tokens = line.split("\t");
72505
- const chrName = tokens[0]; //genome.getChromosomeName(tokens[0]) // Note allowance for alias name, not sure why this is needed here
73231
+ const chrName = tokens[0];
72506
73232
  if (!lastChr) lastChr = chrName;
72507
73233
 
72508
73234
  if (chrName !== lastChr) {
@@ -72520,37 +73246,20 @@ ${indent}columns: ${matrix.columns}
72520
73246
  bands.push(new Cytoband(start, end, name, stain));
72521
73247
  }
72522
73248
  }
72523
-
72524
- }
72525
-
72526
- /**
72527
- * Infer genome chromosome names from cytoband data. This should only be used as a last resort
72528
- */
72529
- async getChromosomeNames() {
72530
- if(this.cytobands.size === 0) {
72531
- await this.#loadCytobands();
73249
+ if(bands.length > 0) {
73250
+ this.cytobands.set(lastChr, bands);
72532
73251
  }
72533
- return Array.from(this.cytobands.keys())
72534
- }
72535
73252
 
72536
- /**
72537
- * Infer chromosome objects from cytoband data. This should only be used as last resort.
72538
- */
72539
- async getChromosomes() {
72540
- if(this.cytobands.size === 0) {
72541
- await this.#loadCytobands();
72542
- }
72543
-
72544
- const chromosomes = [];
72545
- let order = 0;
72546
- for(let [chrName, cytoList] of this.cytobands.entries()) {
72547
- chromosomes.push(new Chromosome(chrName, order++, cytoList[cytoList.length - 1].end));
72548
- }
72549
- return chromosomes
72550
73253
  }
72551
73254
 
72552
73255
  }
72553
73256
 
73257
+ const ucsdIDMap = new Map([
73258
+ ["1kg_ref", "hg18"],
73259
+ ["1kg_v37", "hg19"],
73260
+ ["b37", "hg19"]
73261
+ ]);
73262
+
72554
73263
  /**
72555
73264
  * The Genome class represents an assembly and consists of the following elements
72556
73265
  * sequence - Object representing the DNA sequence
@@ -72575,8 +73284,10 @@ ${indent}columns: ${matrix.columns}
72575
73284
  this.config = config;
72576
73285
  this.browser = browser;
72577
73286
  this.id = config.id || generateGenomeID(config);
73287
+ this.ucscID = config.ucscID || ucsdIDMap.get(this.id) || this.id;
73288
+ this.blatDB = config.blatDB || this.ucscID;
72578
73289
  this.name = config.name;
72579
- this.nameSet = config.nameSet;
73290
+ this.nameSet = config.nameSet || 'ucsc';
72580
73291
  }
72581
73292
 
72582
73293
 
@@ -72584,49 +73295,45 @@ ${indent}columns: ${matrix.columns}
72584
73295
 
72585
73296
  const config = this.config;
72586
73297
 
73298
+ // Load sequence
72587
73299
  this.sequence = await loadSequence(config, this.browser);
72588
73300
 
72589
- if (config.chromSizesURL) {
72590
- // a chromSizes file is neccessary for 2bit sequences for whole-genome view
73301
+
73302
+ // Load cytobands. This is optional but required to support the ideogram. Only needed for whole genome view
73303
+ if(false !== config.showIdeogram && false !== config.wholeGenomeView) {
73304
+ if (config.cytobandURL) {
73305
+ this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
73306
+ } else if (config.cytobandBbURL) {
73307
+ this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
73308
+ }
73309
+ }
73310
+
73311
+ // Search for chromosomes, that is an array of chromosome objects containing name and length. This is
73312
+ // optional but required to support whole genome view.
73313
+ if (this.sequence.chromosomes) {
73314
+ this.chromosomes = this.sequence.chromosomes;
73315
+ } else if (config.chromSizesURL) {
72591
73316
  this.chromosomes = await loadChromSizes(config.chromSizesURL);
72592
73317
  } else {
72593
- // if the sequence defines chromosomes use them (fasta does, 2bit does not)
72594
- this.chromosomes = this.sequence.chromosomes || new Map();
73318
+ this.chromosomes = new Map(); // Cache, chromosome are added as they are loaded
72595
73319
  }
72596
73320
 
72597
- if (this.chromosomes.size > 0) {
73321
+ // Search for chromosome names. This is optional but required to support the chromosome pulldown
73322
+ if (this.sequence.chromosomeNames) {
73323
+ this.chromosomeNames = this.sequence.chromosomeNames; // Twobit files can supply chromosome names unless they use an external index
73324
+ } else if (this.chromosomes.size > 0) {
72598
73325
  this.chromosomeNames = Array.from(this.chromosomes.keys());
72599
73326
  }
72600
73327
 
73328
+ // Chromosome alias
72601
73329
  if (config.chromAliasBbURL) {
72602
73330
  this.chromAlias = new ChromAliasBB(config.chromAliasBbURL, Object.assign({}, config), this);
72603
- if (!this.chromosomeNames) {
72604
- this.chromosomeNames = await this.chromAlias.getChromosomeNames();
72605
- }
72606
73331
  } else if (config.aliasURL) {
72607
73332
  this.chromAlias = new ChromAliasFile(config.aliasURL, Object.assign({}, config), this);
72608
73333
  } else if (this.chromosomeNames) {
72609
73334
  this.chromAlias = new ChromAliasDefaults(this.id, this.chromosomeNames);
72610
73335
  }
72611
73336
 
72612
- if (config.cytobandBbURL) {
72613
- this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
72614
- } else if (config.cytobandURL) {
72615
- this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
72616
- }
72617
-
72618
- // Last resort for chromosome information -- retrieve it from the cytoband source if supported
72619
- if (!this.chromosomeNames && typeof this.cytobandSource.getChromosomeNames === 'function') {
72620
- this.chromosomeNames = await this.cytobandSource.getChromosomeNames();
72621
- }
72622
- if (this.chromosomes.size === 0 && typeof this.cytobandSource.getChromosomes === 'function') {
72623
- const c = await this.cytobandSource.getChromosomes();
72624
- for (let chromosome of c) {
72625
- this.chromosomes.set(c.name, c);
72626
- }
72627
- }
72628
-
72629
-
72630
73337
  if (false !== config.wholeGenomeView && this.chromosomes.size > 0) {
72631
73338
  // Set chromosome order for WG view and chromosome pulldown. If chromosome order is not specified sort
72632
73339
  if (config.chromosomeOrder) {
@@ -72678,9 +73385,9 @@ ${indent}columns: ${matrix.columns}
72678
73385
  getHomeChromosomeName() {
72679
73386
  if (this.showWholeGenomeView() && this.chromosomes.has("all")) {
72680
73387
  return "all"
72681
- } else {
73388
+ } else if (this.chromosomeNames) {
72682
73389
  return this.chromosomeNames[0]
72683
- }
73390
+ } else ;
72684
73391
  }
72685
73392
 
72686
73393
  getChromosomeName(chr) {
@@ -72731,18 +73438,18 @@ ${indent}columns: ${matrix.columns}
72731
73438
  if (!aliasRecord && chr !== chr.toLowerCase()) {
72732
73439
  aliasRecord = await this.chromAlias.search(chr.toLowerCase());
72733
73440
  }
72734
- if(aliasRecord) {
73441
+ if (aliasRecord) {
72735
73442
  // Add some aliases for case insensitivy
72736
73443
  const upper = aliasRecord.chr.toUpperCase();
72737
73444
  const lower = aliasRecord.chr.toLowerCase();
72738
73445
  const cap = aliasRecord.chr.charAt(0).toUpperCase() + aliasRecord.chr.slice(1);
72739
- if(aliasRecord.chr !== upper) {
73446
+ if (aliasRecord.chr !== upper) {
72740
73447
  aliasRecord["_uppercase"] = upper;
72741
73448
  }
72742
- if(aliasRecord.chr !== lower) {
73449
+ if (aliasRecord.chr !== lower) {
72743
73450
  aliasRecord["_lowercase"] = lower;
72744
73451
  }
72745
- if(aliasRecord.chr !== cap) {
73452
+ if (aliasRecord.chr !== cap) {
72746
73453
  aliasRecord["_cap"] = cap;
72747
73454
  }
72748
73455
  }
@@ -72874,6 +73581,10 @@ ${indent}columns: ${matrix.columns}
72874
73581
  return undefined
72875
73582
  }
72876
73583
  }
73584
+
73585
+ getHubURLs() {
73586
+ return this.config.hubs
73587
+ }
72877
73588
  }
72878
73589
 
72879
73590
  /**
@@ -72916,7 +73627,7 @@ ${indent}columns: ${matrix.columns}
72916
73627
  }
72917
73628
  }
72918
73629
 
72919
- var igvCss = '.igv-ui-dropdown {\n cursor: default;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2048;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n background-color: white;\n}\n.igv-ui-dropdown > div {\n overflow-y: auto;\n overflow-x: hidden;\n background-color: white;\n}\n.igv-ui-dropdown > div > div {\n padding: 4px;\n width: 100%;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: 1px;\n background-color: white;\n}\n.igv-ui-dropdown > div > div:last-child {\n border-bottom-color: transparent;\n border-bottom-width: 0;\n}\n.igv-ui-dropdown > div > div:hover {\n cursor: pointer;\n background-color: rgba(0, 0, 0, 0.04);\n}\n\n.igv-ui-popover {\n cursor: default;\n position: absolute;\n z-index: 2048;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n background-color: white;\n}\n.igv-ui-popover > div:first-child {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-width: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-popover > div:first-child > div:first-child {\n margin-left: 4px;\n}\n.igv-ui-popover > div:first-child > div:last-child {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-popover > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-popover > div:last-child {\n user-select: text;\n overflow-y: auto;\n overflow-x: hidden;\n max-height: 400px;\n max-width: 800px;\n background-color: white;\n border-bottom-width: 0;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-ui-popover > div:last-child > div {\n margin-left: 4px;\n margin-right: 4px;\n min-width: 220px;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.igv-ui-popover > div:last-child > div > span {\n font-weight: bolder;\n}\n.igv-ui-popover > div:last-child hr {\n width: 100%;\n}\n\n.igv-ui-alert-dialog-container {\n position: fixed;\n top: 20%;\n left: 50%;\n transform: translateX(-50%);\n z-index: 2048;\n box-sizing: content-box;\n width: 400px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n outline: none;\n font-family: \"Open Sans\", sans-serif;\n font-size: 15px;\n font-weight: 400;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:first-child {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-alert-dialog-container > div:first-child div:first-child {\n padding-left: 8px;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body {\n -webkit-user-select: text;\n -moz-user-select: text;\n -ms-user-select: text;\n user-select: text;\n color: #373737;\n width: 100%;\n height: calc(100% - 24px - 64px);\n overflow-y: scroll;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body .igv-ui-alert-dialog-body-copy {\n margin: 16px;\n width: auto;\n height: auto;\n overflow-wrap: break-word;\n word-break: break-word;\n background-color: white;\n border: unset;\n}\n.igv-ui-alert-dialog-container > div:last-child {\n width: 100%;\n margin-bottom: 10px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:last-child div {\n margin: unset;\n width: 40px;\n height: 30px;\n line-height: 30px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n border-color: #2B81AF;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-alert-dialog-container > div:last-child div:hover {\n cursor: pointer;\n border-color: #25597f;\n background-color: #25597f;\n}\n\n.igv-ui-generic-dialog-container {\n box-sizing: content-box;\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n width: 300px;\n height: fit-content;\n padding-bottom: 16px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input > div {\n width: fit-content;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input[type=range] {\n width: 70%;\n -webkit-appearance: none;\n background: linear-gradient(90deg, white, black);\n outline: none;\n margin: 0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input output {\n display: block;\n height: 100%;\n width: 20%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n padding-top: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div {\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n\n.igv-ui-generic-container {\n position: fixed;\n cursor: pointer;\n background-color: white;\n z-index: 2048;\n box-sizing: content-box;\n width: 300px;\n height: 200px;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-ui-generic-container > div:nth-child(1) > div {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-ui-colorpicker-container {\n background-color: #eee;\n height: fit-content;\n flex-direction: column;\n flex-wrap: nowrap;\n}\n.igv-ui-colorpicker-container > div:nth-child(2) {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-colorpicker-container > div:nth-child(3) {\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n padding: 0.5rem;\n font-family: \"Open Sans\", sans-serif;\n font-size: 1rem;\n font-weight: 400;\n width: 100%;\n border-top-style: solid;\n border-top-width: thin;\n border-top-color: #373737;\n}\n.igv-ui-colorpicker-container > div:nth-child(4) {\n width: 100%;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n\n.igv-ui-color-swatch {\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n margin: 2px;\n width: 32px;\n height: 32px;\n border-style: solid;\n border-width: 0;\n border-color: white;\n border-radius: 3px;\n}\n\n.igv-ui-color-swatch-shim {\n cursor: pointer;\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n width: 32px;\n height: 32px;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: white;\n border-radius: 4px;\n}\n\n.igv-ui-dialog {\n z-index: 2048;\n position: fixed;\n width: fit-content;\n height: fit-content;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n background-color: white;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n}\n.igv-ui-dialog .igv-ui-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-dialog .igv-ui-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-dialog .igv-ui-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-dialog .igv-ui-dialog-one-liner {\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin: 8px;\n overflow-wrap: break-word;\n background-color: white;\n font-weight: bold;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel {\n width: 100%;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div {\n margin: 16px;\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child {\n background-color: #5ea4e0;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child {\n background-color: #c4c4c4;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-ui-panel, .igv-ui-panel-row, .igv-ui-panel-column {\n z-index: 2048;\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-panel-column {\n display: flex;\n flex-direction: column;\n}\n\n.igv-ui-panel-row {\n display: flex;\n flex-direction: row;\n}\n\n.igv-ui-textbox {\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-table {\n background-color: white;\n}\n\n.igv-ui-table thead {\n position: sticky;\n top: 0;\n}\n\n.igv-ui-table th {\n text-align: left;\n}\n\n.igv-ui-table td {\n padding-right: 20px;\n}\n\n.igv-ui-table tr:hover {\n background-color: lightblue;\n}\n\n.igv-ui-center-fixed {\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n.igv-navbar {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n box-sizing: border-box;\n width: 100%;\n color: #444;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n line-height: 32px;\n padding-left: 8px;\n padding-right: 8px;\n margin-top: 2px;\n margin-bottom: 6px;\n height: 32px;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: #f3f3f3;\n}\n.igv-navbar .igv-navbar-left-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 32px;\n line-height: 32px;\n}\n.igv-navbar .igv-navbar-left-container .igv-logo {\n width: 34px;\n height: 32px;\n margin-right: 8px;\n}\n.igv-navbar .igv-navbar-left-container .igv-current-genome {\n height: 32px;\n margin-left: 4px;\n margin-right: 4px;\n user-select: none;\n line-height: 32px;\n vertical-align: middle;\n text-align: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n height: 100%;\n width: 125px;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container select {\n display: block;\n cursor: pointer;\n width: 100px;\n height: 75%;\n outline: none;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n margin-left: 8px;\n height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 240px;\n height: 22px;\n line-height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container input.igv-search-input {\n cursor: text;\n width: 85%;\n height: 22px;\n line-height: 22px;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n text-align: left;\n padding-left: 8px;\n margin-right: 8px;\n outline: none;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: white;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container .igv-search-icon-container {\n cursor: pointer;\n height: 16px;\n width: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-windowsize-panel-container {\n margin-left: 4px;\n user-select: none;\n}\n.igv-navbar .igv-navbar-right-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container {\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container-hidden {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget {\n color: #737373;\n font-size: 18px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:nth-child(even) {\n display: block;\n height: fit-content;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget input {\n display: block;\n width: 125px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 {\n color: #737373;\n font-size: 18px;\n height: 32px;\n line-height: 32px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:nth-child(even) {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 input {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-hidden {\n display: none;\n}\n\n.igv-navbar-button {\n display: block;\n box-sizing: unset;\n padding-left: 6px;\n padding-right: 6px;\n height: 18px;\n text-transform: capitalize;\n user-select: none;\n line-height: 18px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 11px;\n font-weight: 200;\n color: #737373;\n background-color: #f3f3f3;\n border-color: #737373;\n border-style: solid;\n border-width: thin;\n border-radius: 6px;\n}\n\n.igv-navbar-button:hover {\n cursor: pointer;\n}\n\n.igv-navbar-button-clicked {\n color: white;\n background-color: #737373;\n}\n\n.igv-navbar-icon-button {\n cursor: pointer;\n position: relative;\n width: 24px;\n height: 24px;\n margin-left: 4px;\n margin-right: 4px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:first-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: -18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:last-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.igv-navbar-text-button {\n cursor: pointer;\n position: relative;\n margin-left: 2px;\n margin-right: 2px;\n border: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar-text-button > div:nth-child(2) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 0;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-text-button > div:nth-child(3) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 42px;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n#igv-text-button-label {\n text-anchor: middle;\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-inactive rect {\n stroke: #737373;\n fill: white;\n}\n.igv-navbar-text-button-svg-inactive text {\n fill: #737373;\n}\n.igv-navbar-text-button-svg-inactive tspan {\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-hover rect {\n stroke: #737373;\n fill: #737373;\n}\n.igv-navbar-text-button-svg-hover text {\n fill: white;\n}\n.igv-navbar-text-button-svg-hover tspan {\n dominant-baseline: middle;\n}\n\n#igv-save-svg-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-svg-group text {\n fill: #737373;\n}\n\n#igv-save-svg-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-svg-group:hover text {\n fill: white;\n}\n\n#igv-save-png-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-png-group text {\n fill: #737373;\n}\n\n#igv-save-png-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-png-group:hover text {\n fill: white;\n}\n\n.igv-zoom-in-notice-container {\n z-index: 256;\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translate(-50%, 0%);\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n background-color: white;\n}\n.igv-zoom-in-notice-container > div {\n padding-left: 4px;\n padding-right: 4px;\n padding-top: 2px;\n padding-bottom: 2px;\n width: 100%;\n height: 100%;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: #3f3f3f;\n}\n\n.igv-zoom-in-notice {\n position: absolute;\n top: 10px;\n left: 50%;\n}\n.igv-zoom-in-notice div {\n position: relative;\n left: -50%;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #3f3f3f;\n background-color: rgba(255, 255, 255, 0.51);\n z-index: 64;\n}\n\n.igv-container-spinner {\n position: absolute;\n top: 90%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 1024;\n width: 24px;\n height: 24px;\n pointer-events: none;\n color: #737373;\n}\n\n.igv-multi-locus-close-button {\n position: absolute;\n top: 2px;\n right: 0;\n padding-left: 2px;\n padding-right: 2px;\n width: 12px;\n height: 12px;\n color: #666666;\n background-color: white;\n z-index: 1000;\n}\n.igv-multi-locus-close-button > svg {\n vertical-align: top;\n}\n\n.igv-multi-locus-close-button:hover {\n cursor: pointer;\n color: #434343;\n}\n\n.igv-multi-locus-ruler-label {\n z-index: 64;\n position: absolute;\n top: 2px;\n left: 0;\n width: 100%;\n height: 12px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-multi-locus-ruler-label > div {\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n color: rgb(16, 16, 16);\n background-color: white;\n}\n.igv-multi-locus-ruler-label > div {\n cursor: pointer;\n}\n\n.igv-multi-locus-ruler-label-square-dot {\n z-index: 64;\n position: absolute;\n left: 50%;\n top: 5%;\n transform: translate(-50%, 0%);\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-multi-locus-ruler-label-square-dot > div:first-child {\n width: 14px;\n height: 14px;\n}\n.igv-multi-locus-ruler-label-square-dot > div:last-child {\n margin-left: 16px;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: rgb(16, 16, 16);\n}\n\n.igv-ruler-sweeper {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 26px;\n bottom: 0;\n left: 0;\n width: 0;\n z-index: 99999;\n background-color: rgba(68, 134, 247, 0.25);\n}\n\n.igv-ruler-tooltip {\n pointer-events: none;\n z-index: 128;\n position: absolute;\n top: 0;\n left: 0;\n width: 1px;\n height: 32px;\n background-color: transparent;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ruler-tooltip > div {\n pointer-events: none;\n width: 128px;\n height: auto;\n padding: 1px;\n color: #373737;\n font-size: 10px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: #373737;\n}\n\n.igv-track-label {\n position: absolute;\n left: 8px;\n top: 8px;\n width: auto;\n height: auto;\n max-width: 50%;\n padding-left: 4px;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n text-align: center;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-color: #444;\n border-radius: 2px;\n border-style: solid;\n border-width: thin;\n background-color: white;\n z-index: 128;\n cursor: pointer;\n}\n\n.igv-track-label:hover,\n.igv-track-label:focus,\n.igv-track-label:active {\n background-color: #e8e8e8;\n}\n\n.igv-track-label-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-top: 4px;\n}\n\n.igv-center-line {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n z-index: 8;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-left-style: dashed;\n border-left-width: thin;\n border-right-style: dashed;\n border-right-width: thin;\n}\n\n.igv-center-line-wide {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(127, 127, 127, 0.51);\n}\n\n.igv-center-line-thin {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(0, 0, 0, 0);\n}\n\n.igv-cursor-guide-horizontal {\n display: none;\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n left: 0;\n right: 0;\n top: 50%;\n height: 1px;\n z-index: 32;\n margin-left: 50px;\n margin-right: 54px;\n border-top-style: dotted;\n border-top-width: thin;\n border-top-color: rgba(127, 127, 127, 0.76);\n}\n\n.igv-cursor-guide-vertical {\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 1px;\n z-index: 32;\n border-left-style: dotted;\n border-left-width: thin;\n border-left-color: rgba(127, 127, 127, 0.76);\n display: none;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-generic-dialog-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n width: 300px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n z-index: 2048;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-generic-dialog-container .igv-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input div {\n width: 30%;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div {\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-generic-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-generic-container > div:nth-child(1) i {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-menu-popup {\n position: absolute;\n top: 0;\n left: 0;\n width: max-content;\n max-width: 400px;\n z-index: 512;\n cursor: pointer;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background: white;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-end;\n text-align: left;\n}\n.igv-menu-popup > div:not(:first-child) {\n width: 100%;\n}\n.igv-menu-popup > div:not(:first-child) > div {\n background: white;\n}\n.igv-menu-popup > div:not(:first-child) > div.context-menu {\n padding-left: 4px;\n padding-right: 4px;\n}\n.igv-menu-popup > div:not(:first-child) > div:last-child {\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-menu-popup > div:not(:first-child) > div:hover {\n background: #efefef;\n}\n\n.igv-menu-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-bottom: 1px;\n padding-top: 1px;\n}\n\n.igv-menu-popup-header {\n position: relative;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-menu-popup-header div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-menu-popup-header div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-menu-popup-check-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 20px;\n margin-right: 4px;\n background-color: transparent;\n}\n.igv-menu-popup-check-container div {\n padding-top: 2px;\n padding-left: 8px;\n}\n.igv-menu-popup-check-container div:first-child {\n position: relative;\n width: 12px;\n height: 12px;\n}\n.igv-menu-popup-check-container div:first-child svg {\n position: absolute;\n width: 12px;\n height: 12px;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-loading-spinner-container {\n z-index: 1024;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 32px;\n height: 32px;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-loading-spinner-container > div {\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n border: 4px solid rgba(128, 128, 128, 0.5);\n border-top-color: rgb(255, 255, 255);\n animation: spin 1s ease-in-out infinite;\n -webkit-animation: spin 1s ease-in-out infinite;\n}\n\n@keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n@-webkit-keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n.igv-roi-menu {\n position: absolute;\n z-index: 512;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background-color: white;\n width: 192px;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-menu > div:first-child {\n height: 24px;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-roi-menu > div:first-child > div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-roi-menu > div:first-child > div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-menu > div:last-child {\n background-color: white;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: 0;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n text-align: start;\n vertical-align: middle;\n}\n.igv-roi-menu > div:last-child > div {\n height: 24px;\n padding-left: 4px;\n border-bottom-style: solid;\n border-bottom-width: thin;\n border-bottom-color: #7f7f7f;\n}\n.igv-roi-menu > div:last-child > div:not(:first-child):hover {\n cursor: pointer;\n background-color: rgba(127, 127, 127, 0.1);\n}\n.igv-roi-menu > div:last-child div:first-child {\n font-style: italic;\n text-align: center;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.igv-roi-menu > div:last-child > div:last-child {\n border-bottom-width: 0;\n border-bottom-color: transparent;\n}\n\n.igv-roi-placeholder {\n font-style: normal;\n color: rgba(75, 75, 75, 0.6);\n}\n\n.igv-roi-table {\n position: absolute;\n z-index: 1024;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n resize: both;\n overflow: hidden;\n width: min-content;\n max-width: 1600px;\n border-color: #7f7f7f;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n font-weight: 400;\n background-color: white;\n cursor: default;\n}\n.igv-roi-table > div {\n height: 24px;\n font-size: 14px;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n}\n.igv-roi-table > div:first-child {\n border-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-top-width: 0;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-roi-table > div:first-child > div:first-child {\n text-align: center;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n margin-left: 4px;\n margin-right: 4px;\n width: calc(100% - 4px - 12px);\n}\n.igv-roi-table > div:first-child > div:last-child {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7f7f7f;\n}\n.igv-roi-table > div:first-child > div:last-child > svg {\n display: block;\n}\n.igv-roi-table > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-table > .igv-roi-table-description {\n padding: 4px;\n margin-left: 4px;\n word-break: break-all;\n overflow-y: auto;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-goto-explainer {\n margin-top: 5px;\n margin-left: 4px;\n color: #7F7F7F;\n font-style: italic;\n height: 24px;\n border-top: solid lightgray;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-column-titles {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n padding-right: 16px;\n background-color: white;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: #7f7f7f;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n overflow: auto;\n height: 360px;\n flex: 1 1 auto;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: transparent;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row-hover {\n background-color: rgba(0, 0, 0, 0.04);\n}\n.igv-roi-table > div:last-child {\n min-height: 32px;\n height: 32px;\n line-height: 32px;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: transparent;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-width: 0;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n\n.igv-roi-table-row-selected {\n background-color: rgba(0, 0, 0, 0.125);\n}\n\n.igv-roi-table-button {\n cursor: pointer;\n height: 20px;\n user-select: none;\n line-height: 20px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 13px;\n font-weight: 400;\n color: black;\n padding-left: 6px;\n padding-right: 6px;\n background-color: rgb(239, 239, 239);\n border-color: black;\n border-style: solid;\n border-width: thin;\n border-radius: 3px;\n}\n\n.igv-roi-table-button:hover {\n font-weight: 400;\n background-color: rgba(0, 0, 0, 0.13);\n}\n\n.igv-roi-region {\n z-index: 64;\n position: absolute;\n top: 0;\n bottom: 0;\n pointer-events: none;\n overflow: visible;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-region > div {\n position: relative;\n width: 100%;\n height: 8px;\n pointer-events: auto;\n}\n\n.igv-roi-menu-row {\n height: 24px;\n padding-left: 8px;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n background-color: white;\n}\n\n.igv-roi-menu-row-edit-description {\n width: -webkit-fill-available;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n background-color: white;\n padding-left: 4px;\n padding-right: 4px;\n padding-bottom: 4px;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-menu-row-edit-description > label {\n margin-left: 2px;\n margin-bottom: 0;\n display: block;\n width: -webkit-fill-available;\n}\n.igv-roi-menu-row-edit-description > input {\n display: block;\n margin-left: 2px;\n margin-right: 2px;\n margin-bottom: 1px;\n width: -webkit-fill-available;\n}\n\n.picker_wrapper.no_alpha .picker_alpha {\n display: none;\n}\n\n.picker_wrapper.no_editor .picker_editor {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.picker_wrapper.no_cancel .picker_cancel {\n display: none;\n}\n\n.layout_default.picker_wrapper {\n display: flex;\n flex-flow: row wrap;\n justify-content: space-between;\n align-items: stretch;\n font-size: 10px;\n width: 25em;\n padding: 0.5em;\n}\n\n.layout_default.picker_wrapper input, .layout_default.picker_wrapper button {\n font-size: 1rem;\n}\n\n.layout_default.picker_wrapper > * {\n margin: 0.5em;\n}\n\n.layout_default.picker_wrapper::before {\n content: \"\";\n display: block;\n width: 100%;\n height: 0;\n order: 1;\n}\n\n.layout_default .picker_slider, .layout_default .picker_selector {\n padding: 1em;\n}\n\n.layout_default .picker_hue {\n width: 100%;\n}\n\n.layout_default .picker_sl {\n flex: 1 1 auto;\n}\n\n.layout_default .picker_sl::before {\n content: \"\";\n display: block;\n padding-bottom: 100%;\n}\n\n.layout_default .picker_editor {\n order: 1;\n width: 6.5rem;\n}\n\n.layout_default .picker_editor input {\n width: 100%;\n height: 100%;\n}\n\n.layout_default .picker_sample {\n order: 1;\n flex: 1 1 auto;\n}\n\n.layout_default .picker_done, .layout_default .picker_cancel {\n order: 1;\n}\n\n.picker_wrapper {\n box-sizing: border-box;\n background: #f2f2f2;\n box-shadow: 0 0 0 1px silver;\n cursor: default;\n font-family: sans-serif;\n color: #444;\n pointer-events: auto;\n}\n\n.picker_wrapper:focus {\n outline: none;\n}\n\n.picker_wrapper button, .picker_wrapper input {\n box-sizing: border-box;\n border: none;\n box-shadow: 0 0 0 1px silver;\n outline: none;\n}\n\n.picker_wrapper button:focus, .picker_wrapper button:active, .picker_wrapper input:focus, .picker_wrapper input:active {\n box-shadow: 0 0 2px 1px #1e90ff;\n}\n\n.picker_wrapper button {\n padding: 0.4em 0.6em;\n cursor: pointer;\n background-color: #f5f5f5;\n background-image: linear-gradient(0deg, gainsboro, transparent);\n}\n\n.picker_wrapper button:active {\n background-image: linear-gradient(0deg, transparent, gainsboro);\n}\n\n.picker_wrapper button:hover {\n background-color: #fff;\n}\n\n.picker_selector {\n position: absolute;\n z-index: 1;\n display: block;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n border: 2px solid #fff;\n border-radius: 100%;\n box-shadow: 0 0 3px 1px #67b9ff;\n background: currentColor;\n cursor: pointer;\n}\n\n.picker_slider .picker_selector {\n border-radius: 2px;\n}\n\n.picker_hue {\n position: relative;\n background-image: linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_sl {\n position: relative;\n box-shadow: 0 0 0 1px silver;\n background-image: linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%), linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%), linear-gradient(90deg, #808080, rgba(128, 128, 128, 0));\n}\n\n.picker_alpha, .picker_sample {\n position: relative;\n background: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em, linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_alpha .picker_selector, .picker_sample .picker_selector {\n background: none;\n}\n\n.picker_editor input {\n font-family: monospace;\n padding: 0.2em 0.4em;\n}\n\n.picker_sample::before {\n content: \"\";\n position: absolute;\n display: block;\n width: 100%;\n height: 100%;\n background: currentColor;\n}\n\n.picker_arrow {\n position: absolute;\n z-index: -1;\n}\n\n.picker_wrapper.popup {\n position: absolute;\n z-index: 2;\n margin: 1.5em;\n}\n\n.picker_wrapper.popup, .picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n background: #f2f2f2;\n box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.4);\n}\n\n.picker_wrapper.popup .picker_arrow {\n width: 3em;\n height: 3em;\n margin: 0;\n}\n\n.picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n z-index: -99;\n}\n\n.picker_wrapper.popup .picker_arrow::before {\n width: 100%;\n height: 100%;\n -webkit-transform: skew(45deg);\n transform: skew(45deg);\n -webkit-transform-origin: 0 100%;\n transform-origin: 0 100%;\n}\n\n.picker_wrapper.popup .picker_arrow::after {\n width: 150%;\n height: 150%;\n box-shadow: none;\n}\n\n.popup.popup_top {\n bottom: 100%;\n left: 0;\n}\n\n.popup.popup_top .picker_arrow {\n bottom: 0;\n left: 0;\n -webkit-transform: rotate(-90deg);\n transform: rotate(-90deg);\n}\n\n.popup.popup_bottom {\n top: 100%;\n left: 0;\n}\n\n.popup.popup_bottom .picker_arrow {\n top: 0;\n left: 0;\n -webkit-transform: rotate(90deg) scale(1, -1);\n transform: rotate(90deg) scale(1, -1);\n}\n\n.popup.popup_left {\n top: 0;\n right: 100%;\n}\n\n.popup.popup_left .picker_arrow {\n top: 0;\n right: 0;\n -webkit-transform: scale(-1, 1);\n transform: scale(-1, 1);\n}\n\n.popup.popup_right {\n top: 0;\n left: 100%;\n}\n\n.popup.popup_right .picker_arrow {\n top: 0;\n left: 0;\n}\n\n.igv-container {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n padding-top: 4px;\n user-select: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n min-height: 160px;\n}\n\n.igv-viewport {\n position: relative;\n margin-top: 5px;\n line-height: 1;\n overflow-x: hidden;\n overflow-y: hidden;\n}\n\n.igv-viewport-content {\n position: relative;\n width: 100%;\n}\n.igv-viewport-content > canvas {\n position: relative;\n display: block;\n}\n\n.igv-column-container {\n position: relative;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n width: 100%;\n}\n\n.igv-column-shim {\n width: 1px;\n margin-left: 2px;\n margin-right: 2px;\n background-color: #545453;\n}\n\n.igv-axis-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 50px;\n}\n.igv-axis-column > div {\n position: relative;\n margin-top: 5px;\n width: 100%;\n}\n.igv-axis-column > div > div {\n z-index: 512;\n position: absolute;\n top: 8px;\n left: 8px;\n width: fit-content;\n height: fit-content;\n background-color: transparent;\n display: grid;\n align-items: start;\n justify-items: center;\n}\n.igv-axis-column > div > div > input {\n display: block;\n margin: unset;\n cursor: pointer;\n}\n\n.igv-column {\n position: relative;\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-info-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-name-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-scrollbar-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 14px;\n}\n.igv-scrollbar-column > div {\n position: relative;\n margin-top: 5px;\n width: 14px;\n}\n.igv-scrollbar-column > div > div {\n cursor: pointer;\n position: absolute;\n top: 0;\n left: 2px;\n width: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: #c4c4c4;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-scrollbar-column > div > div:hover {\n background-color: #c4c4c4;\n}\n\n.igv-track-drag-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 12px;\n background-color: white;\n}\n.igv-track-drag-column > .igv-track-drag-handle {\n z-index: 512;\n position: relative;\n cursor: pointer;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.igv-track-drag-column .igv-track-drag-handle-color {\n background-color: #c4c4c4;\n}\n.igv-track-drag-column .igv-track-drag-handle-hover-color {\n background-color: #787878;\n}\n.igv-track-drag-column .igv-track-drag-handle-selected-color {\n background-color: #0963fa;\n}\n.igv-track-drag-column > .igv-track-drag-shim {\n position: relative;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n}\n\n.igv-gear-menu-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 28px;\n}\n.igv-gear-menu-column > div {\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n margin-top: 5px;\n width: 100%;\n background: white;\n}\n.igv-gear-menu-column > div > div {\n position: relative;\n margin-top: 4px;\n width: 16px;\n height: 16px;\n color: #7F7F7F;\n}\n.igv-gear-menu-column > div > div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-vertical-center {\n margin: 0 !important;\n top: 50% !important;\n -ms-transform: translateY(-50%) !important;\n transform: translateY(-50%) !important;\n}\n\n/*# sourceMappingURL=igv.css.map */\n';
73630
+ var igvCss = '.igv-ui-dropdown {\n cursor: default;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2048;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n background-color: white;\n}\n.igv-ui-dropdown > div {\n overflow-y: auto;\n overflow-x: hidden;\n background-color: white;\n}\n.igv-ui-dropdown > div > div {\n padding: 4px;\n width: 100%;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: 1px;\n background-color: white;\n}\n.igv-ui-dropdown > div > div:last-child {\n border-bottom-color: transparent;\n border-bottom-width: 0;\n}\n.igv-ui-dropdown > div > div:hover {\n cursor: pointer;\n background-color: rgba(0, 0, 0, 0.04);\n}\n\n.igv-ui-popover {\n cursor: default;\n position: absolute;\n z-index: 2048;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n background-color: white;\n}\n.igv-ui-popover > div:first-child {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-width: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-popover > div:first-child > div:first-child {\n margin-left: 4px;\n}\n.igv-ui-popover > div:first-child > div:last-child {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-popover > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-popover > div:last-child {\n user-select: text;\n overflow-y: auto;\n overflow-x: hidden;\n max-height: 400px;\n max-width: 800px;\n background-color: white;\n border-bottom-width: 0;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-ui-popover > div:last-child > div {\n margin-left: 4px;\n margin-right: 4px;\n min-width: 220px;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.igv-ui-popover > div:last-child > div > span {\n font-weight: bolder;\n}\n.igv-ui-popover > div:last-child hr {\n width: 100%;\n}\n\n.igv-ui-alert-dialog-container {\n position: fixed;\n top: 20%;\n left: 50%;\n transform: translateX(-50%);\n z-index: 2048;\n box-sizing: content-box;\n width: 400px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n outline: none;\n font-family: \"Open Sans\", sans-serif;\n font-size: 15px;\n font-weight: 400;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:first-child {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-alert-dialog-container > div:first-child div:first-child {\n padding-left: 8px;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body {\n -webkit-user-select: text;\n -moz-user-select: text;\n -ms-user-select: text;\n user-select: text;\n color: #373737;\n width: 100%;\n height: calc(100% - 24px - 64px);\n overflow-y: scroll;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body .igv-ui-alert-dialog-body-copy {\n margin: 16px;\n width: auto;\n height: auto;\n overflow-wrap: break-word;\n word-break: break-word;\n background-color: white;\n border: unset;\n}\n.igv-ui-alert-dialog-container > div:last-child {\n width: 100%;\n margin-bottom: 10px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:last-child div {\n margin: unset;\n width: 40px;\n height: 30px;\n line-height: 30px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n border-color: #2B81AF;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-alert-dialog-container > div:last-child div:hover {\n cursor: pointer;\n border-color: #25597f;\n background-color: #25597f;\n}\n\n.igv-ui-generic-dialog-container {\n box-sizing: content-box;\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n width: 300px;\n height: fit-content;\n padding-bottom: 16px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input > div {\n width: fit-content;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input[type=range] {\n width: 70%;\n -webkit-appearance: none;\n background: linear-gradient(90deg, white, black);\n outline: none;\n margin: 0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input output {\n display: block;\n height: 100%;\n width: 20%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n padding-top: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div {\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n\n.igv-ui-generic-container {\n position: fixed;\n cursor: pointer;\n background-color: white;\n z-index: 2048;\n box-sizing: content-box;\n width: 300px;\n height: 200px;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-ui-generic-container > div:nth-child(1) > div {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-ui-colorpicker-container {\n background-color: #eee;\n height: fit-content;\n flex-direction: column;\n flex-wrap: nowrap;\n}\n.igv-ui-colorpicker-container > div:nth-child(2) {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-colorpicker-container > div:nth-child(3) {\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n padding: 0.5rem;\n font-family: \"Open Sans\", sans-serif;\n font-size: 1rem;\n font-weight: 400;\n width: 100%;\n border-top-style: solid;\n border-top-width: thin;\n border-top-color: #373737;\n}\n.igv-ui-colorpicker-container > div:nth-child(4) {\n width: 100%;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n\n.igv-ui-color-swatch {\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n margin: 2px;\n width: 32px;\n height: 32px;\n border-style: solid;\n border-width: 0;\n border-color: white;\n border-radius: 3px;\n}\n\n.igv-ui-color-swatch-shim {\n cursor: pointer;\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n width: 32px;\n height: 32px;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: white;\n border-radius: 4px;\n}\n\n.igv-ui-dialog {\n z-index: 2048;\n position: fixed;\n width: fit-content;\n height: fit-content;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n background-color: white;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n}\n.igv-ui-dialog .igv-ui-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-dialog .igv-ui-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-dialog .igv-ui-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-dialog .igv-ui-dialog-one-liner {\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin: 8px;\n overflow-wrap: break-word;\n background-color: white;\n font-weight: bold;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel {\n width: 100%;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div {\n margin: 16px;\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child {\n background-color: #5ea4e0;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child {\n background-color: #c4c4c4;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-ui-panel, .igv-ui-panel-row, .igv-ui-panel-column {\n z-index: 2048;\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-panel-column {\n display: flex;\n flex-direction: column;\n}\n\n.igv-ui-panel-row {\n display: flex;\n flex-direction: row;\n}\n\n.igv-ui-textbox {\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-table {\n background-color: white;\n}\n\n.igv-ui-table thead {\n position: sticky;\n top: 0;\n}\n\n.igv-ui-table th {\n text-align: left;\n}\n\n.igv-ui-table td {\n padding-right: 20px;\n}\n\n.igv-ui-table tr:hover {\n background-color: lightblue;\n}\n\n.igv-ui-center-fixed {\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n.igv-navbar {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n box-sizing: border-box;\n width: 100%;\n color: #444;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n line-height: 32px;\n padding-left: 8px;\n padding-right: 8px;\n margin-top: 2px;\n margin-bottom: 6px;\n height: 32px;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: #f3f3f3;\n}\n.igv-navbar .igv-navbar-left-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 32px;\n line-height: 32px;\n}\n.igv-navbar .igv-navbar-left-container .igv-logo {\n width: 34px;\n height: 32px;\n margin-right: 8px;\n}\n.igv-navbar .igv-navbar-left-container .igv-current-genome {\n height: 32px;\n margin-left: 4px;\n margin-right: 4px;\n user-select: none;\n line-height: 32px;\n vertical-align: middle;\n text-align: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n height: 100%;\n width: 125px;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container select {\n display: block;\n cursor: pointer;\n width: 100px;\n height: 75%;\n outline: none;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n margin-left: 8px;\n height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 240px;\n height: 22px;\n line-height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container input.igv-search-input {\n cursor: text;\n width: 85%;\n height: 22px;\n line-height: 22px;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n text-align: left;\n padding-left: 8px;\n margin-right: 8px;\n outline: none;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: white;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container .igv-search-icon-container {\n cursor: pointer;\n height: 16px;\n width: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-windowsize-panel-container {\n margin-left: 4px;\n user-select: none;\n}\n.igv-navbar .igv-navbar-right-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container {\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container-hidden {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget {\n color: #737373;\n font-size: 18px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:nth-child(even) {\n display: block;\n height: fit-content;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget input {\n display: block;\n width: 125px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 {\n color: #737373;\n font-size: 18px;\n height: 32px;\n line-height: 32px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:nth-child(even) {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 input {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-hidden {\n display: none;\n}\n\n.igv-navbar-button {\n display: block;\n box-sizing: unset;\n padding-left: 6px;\n padding-right: 6px;\n height: 18px;\n text-transform: capitalize;\n user-select: none;\n line-height: 18px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 11px;\n font-weight: 200;\n color: #737373;\n background-color: #f3f3f3;\n border-color: #737373;\n border-style: solid;\n border-width: thin;\n border-radius: 6px;\n}\n\n.igv-navbar-button:hover {\n cursor: pointer;\n}\n\n.igv-navbar-button-clicked {\n color: white;\n background-color: #737373;\n}\n\n.igv-navbar-icon-button {\n cursor: pointer;\n position: relative;\n width: 24px;\n height: 24px;\n margin-left: 4px;\n margin-right: 4px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:first-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: -18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:last-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.igv-navbar-text-button {\n cursor: pointer;\n position: relative;\n margin-left: 2px;\n margin-right: 2px;\n border: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar-text-button > div:nth-child(2) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 0;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-text-button > div:nth-child(3) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 42px;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n#igv-text-button-label {\n text-anchor: middle;\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-inactive rect {\n stroke: #737373;\n fill: white;\n}\n.igv-navbar-text-button-svg-inactive text {\n fill: #737373;\n}\n.igv-navbar-text-button-svg-inactive tspan {\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-hover rect {\n stroke: #737373;\n fill: #737373;\n}\n.igv-navbar-text-button-svg-hover text {\n fill: white;\n}\n.igv-navbar-text-button-svg-hover tspan {\n dominant-baseline: middle;\n}\n\n#igv-save-svg-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-svg-group text {\n fill: #737373;\n}\n\n#igv-save-svg-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-svg-group:hover text {\n fill: white;\n}\n\n#igv-save-png-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-png-group text {\n fill: #737373;\n}\n\n#igv-save-png-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-png-group:hover text {\n fill: white;\n}\n\n.igv-zoom-in-notice-container {\n z-index: 256;\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translate(-50%, 0%);\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n background-color: white;\n}\n.igv-zoom-in-notice-container > div {\n padding-left: 4px;\n padding-right: 4px;\n padding-top: 2px;\n padding-bottom: 2px;\n width: 100%;\n height: 100%;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: #3f3f3f;\n}\n\n.igv-zoom-in-notice {\n position: absolute;\n top: 10px;\n left: 50%;\n}\n.igv-zoom-in-notice div {\n position: relative;\n left: -50%;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #3f3f3f;\n background-color: rgba(255, 255, 255, 0.51);\n z-index: 64;\n}\n\n.igv-container-spinner {\n position: absolute;\n top: 90%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 1024;\n width: 24px;\n height: 24px;\n pointer-events: none;\n color: #737373;\n}\n\n.igv-multi-locus-close-button {\n position: absolute;\n top: 2px;\n right: 0;\n padding-left: 2px;\n padding-right: 2px;\n width: 12px;\n height: 12px;\n color: #666666;\n background-color: white;\n z-index: 1000;\n}\n.igv-multi-locus-close-button > svg {\n vertical-align: top;\n}\n\n.igv-multi-locus-close-button:hover {\n cursor: pointer;\n color: #434343;\n}\n\n.igv-multi-locus-ruler-label {\n z-index: 64;\n position: absolute;\n top: 2px;\n left: 0;\n width: 100%;\n height: 12px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-multi-locus-ruler-label > div {\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n color: rgb(16, 16, 16);\n background-color: white;\n}\n.igv-multi-locus-ruler-label > div {\n cursor: pointer;\n}\n\n.igv-multi-locus-ruler-label-square-dot {\n z-index: 64;\n position: absolute;\n left: 50%;\n top: 5%;\n transform: translate(-50%, 0%);\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-multi-locus-ruler-label-square-dot > div:first-child {\n width: 14px;\n height: 14px;\n}\n.igv-multi-locus-ruler-label-square-dot > div:last-child {\n margin-left: 16px;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: rgb(16, 16, 16);\n}\n\n.igv-ruler-sweeper {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 26px;\n bottom: 0;\n left: 0;\n width: 0;\n z-index: 99999;\n background-color: rgba(68, 134, 247, 0.25);\n}\n\n.igv-ruler-tooltip {\n pointer-events: none;\n z-index: 128;\n position: absolute;\n top: 0;\n left: 0;\n width: 1px;\n height: 32px;\n background-color: transparent;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ruler-tooltip > div {\n pointer-events: none;\n width: 128px;\n height: auto;\n padding: 1px;\n color: #373737;\n font-size: 10px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: #373737;\n}\n\n.igv-track-label {\n position: absolute;\n left: 8px;\n top: 8px;\n width: auto;\n height: auto;\n max-width: 50%;\n padding-left: 4px;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n text-align: center;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-color: #444;\n border-radius: 2px;\n border-style: solid;\n border-width: thin;\n background-color: white;\n z-index: 128;\n cursor: pointer;\n}\n\n.igv-track-label:hover,\n.igv-track-label:focus,\n.igv-track-label:active {\n background-color: #e8e8e8;\n}\n\n.igv-track-label-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-top: 4px;\n}\n\n.igv-center-line {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n z-index: 8;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-left-style: dashed;\n border-left-width: thin;\n border-right-style: dashed;\n border-right-width: thin;\n}\n\n.igv-center-line-wide {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(127, 127, 127, 0.51);\n}\n\n.igv-center-line-thin {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(0, 0, 0, 0);\n}\n\n.igv-cursor-guide-horizontal {\n display: none;\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n left: 0;\n right: 0;\n top: 50%;\n height: 1px;\n z-index: 32;\n margin-left: 50px;\n margin-right: 54px;\n border-top-style: dotted;\n border-top-width: thin;\n border-top-color: rgba(127, 127, 127, 0.76);\n}\n\n.igv-cursor-guide-vertical {\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 1px;\n z-index: 32;\n border-left-style: dotted;\n border-left-width: thin;\n border-left-color: rgba(127, 127, 127, 0.76);\n display: none;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-generic-dialog-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n width: 300px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n z-index: 2048;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-generic-dialog-container .igv-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input div {\n width: 30%;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div {\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-generic-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-generic-container > div:nth-child(1) i {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-menu-popup {\n position: absolute;\n top: 0;\n left: 0;\n width: max-content;\n max-width: 400px;\n z-index: 2048;\n cursor: pointer;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background: white;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-end;\n text-align: left;\n}\n.igv-menu-popup > div:not(:first-child) {\n width: 100%;\n}\n.igv-menu-popup > div:not(:first-child) > div {\n background: white;\n}\n.igv-menu-popup > div:not(:first-child) > div.context-menu {\n padding-left: 4px;\n padding-right: 4px;\n}\n.igv-menu-popup > div:not(:first-child) > div:last-child {\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-menu-popup > div:not(:first-child) > div:hover {\n background: #efefef;\n}\n\n.igv-menu-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-bottom: 1px;\n padding-top: 1px;\n}\n\n.igv-menu-popup-header {\n position: relative;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-menu-popup-header div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-menu-popup-header div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-menu-popup-check-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 20px;\n margin-right: 4px;\n background-color: transparent;\n}\n.igv-menu-popup-check-container div {\n padding-top: 2px;\n padding-left: 8px;\n}\n.igv-menu-popup-check-container div:first-child {\n position: relative;\n width: 12px;\n height: 12px;\n}\n.igv-menu-popup-check-container div:first-child svg {\n position: absolute;\n width: 12px;\n height: 12px;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-loading-spinner-container {\n z-index: 1024;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 32px;\n height: 32px;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-loading-spinner-container > div {\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n border: 4px solid rgba(128, 128, 128, 0.5);\n border-top-color: rgb(255, 255, 255);\n animation: spin 1s ease-in-out infinite;\n -webkit-animation: spin 1s ease-in-out infinite;\n}\n\n@keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n@-webkit-keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n.igv-roi-menu {\n position: absolute;\n z-index: 512;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background-color: white;\n width: 192px;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-menu > div:first-child {\n height: 24px;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-roi-menu > div:first-child > div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-roi-menu > div:first-child > div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-menu > div:last-child {\n background-color: white;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: 0;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n text-align: start;\n vertical-align: middle;\n}\n.igv-roi-menu > div:last-child > div {\n height: 24px;\n padding-left: 4px;\n border-bottom-style: solid;\n border-bottom-width: thin;\n border-bottom-color: #7f7f7f;\n}\n.igv-roi-menu > div:last-child > div:not(:first-child):hover {\n cursor: pointer;\n background-color: rgba(127, 127, 127, 0.1);\n}\n.igv-roi-menu > div:last-child div:first-child {\n font-style: italic;\n text-align: center;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.igv-roi-menu > div:last-child > div:last-child {\n border-bottom-width: 0;\n border-bottom-color: transparent;\n}\n\n.igv-roi-placeholder {\n font-style: normal;\n color: rgba(75, 75, 75, 0.6);\n}\n\n.igv-roi-table {\n position: absolute;\n z-index: 1024;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n resize: both;\n overflow: hidden;\n width: min-content;\n max-width: 1600px;\n border-color: #7f7f7f;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n font-weight: 400;\n background-color: white;\n cursor: default;\n}\n.igv-roi-table > div {\n height: 24px;\n font-size: 14px;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n}\n.igv-roi-table > div:first-child {\n border-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-top-width: 0;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-roi-table > div:first-child > div:first-child {\n text-align: center;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n margin-left: 4px;\n margin-right: 4px;\n width: calc(100% - 4px - 12px);\n}\n.igv-roi-table > div:first-child > div:last-child {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7f7f7f;\n}\n.igv-roi-table > div:first-child > div:last-child > svg {\n display: block;\n}\n.igv-roi-table > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-table > .igv-roi-table-description {\n padding: 4px;\n margin-left: 4px;\n word-break: break-all;\n overflow-y: auto;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-goto-explainer {\n margin-top: 5px;\n margin-left: 4px;\n color: #7F7F7F;\n font-style: italic;\n height: 24px;\n border-top: solid lightgray;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-column-titles {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n padding-right: 16px;\n background-color: white;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: #7f7f7f;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n overflow: auto;\n height: 360px;\n flex: 1 1 auto;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: transparent;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row-hover {\n background-color: rgba(0, 0, 0, 0.04);\n}\n.igv-roi-table > div:last-child {\n min-height: 32px;\n height: 32px;\n line-height: 32px;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: transparent;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-width: 0;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n\n.igv-roi-table-row-selected {\n background-color: rgba(0, 0, 0, 0.125);\n}\n\n.igv-roi-table-button {\n cursor: pointer;\n height: 20px;\n user-select: none;\n line-height: 20px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 13px;\n font-weight: 400;\n color: black;\n padding-left: 6px;\n padding-right: 6px;\n background-color: rgb(239, 239, 239);\n border-color: black;\n border-style: solid;\n border-width: thin;\n border-radius: 3px;\n}\n\n.igv-roi-table-button:hover {\n font-weight: 400;\n background-color: rgba(0, 0, 0, 0.13);\n}\n\n.igv-roi-region {\n z-index: 64;\n position: absolute;\n top: 0;\n bottom: 0;\n pointer-events: none;\n overflow: visible;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-region > div {\n position: relative;\n width: 100%;\n height: 8px;\n pointer-events: auto;\n}\n\n.igv-roi-menu-row {\n height: 24px;\n padding-left: 8px;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n background-color: white;\n}\n\n.igv-roi-menu-row-edit-description {\n width: -webkit-fill-available;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n background-color: white;\n padding-left: 4px;\n padding-right: 4px;\n padding-bottom: 4px;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-menu-row-edit-description > label {\n margin-left: 2px;\n margin-bottom: 0;\n display: block;\n width: -webkit-fill-available;\n}\n.igv-roi-menu-row-edit-description > input {\n display: block;\n margin-left: 2px;\n margin-right: 2px;\n margin-bottom: 1px;\n width: -webkit-fill-available;\n}\n\n.picker_wrapper.no_alpha .picker_alpha {\n display: none;\n}\n\n.picker_wrapper.no_editor .picker_editor {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.picker_wrapper.no_cancel .picker_cancel {\n display: none;\n}\n\n.layout_default.picker_wrapper {\n display: flex;\n flex-flow: row wrap;\n justify-content: space-between;\n align-items: stretch;\n font-size: 10px;\n width: 25em;\n padding: 0.5em;\n}\n\n.layout_default.picker_wrapper input, .layout_default.picker_wrapper button {\n font-size: 1rem;\n}\n\n.layout_default.picker_wrapper > * {\n margin: 0.5em;\n}\n\n.layout_default.picker_wrapper::before {\n content: \"\";\n display: block;\n width: 100%;\n height: 0;\n order: 1;\n}\n\n.layout_default .picker_slider, .layout_default .picker_selector {\n padding: 1em;\n}\n\n.layout_default .picker_hue {\n width: 100%;\n}\n\n.layout_default .picker_sl {\n flex: 1 1 auto;\n}\n\n.layout_default .picker_sl::before {\n content: \"\";\n display: block;\n padding-bottom: 100%;\n}\n\n.layout_default .picker_editor {\n order: 1;\n width: 6.5rem;\n}\n\n.layout_default .picker_editor input {\n width: 100%;\n height: 100%;\n}\n\n.layout_default .picker_sample {\n order: 1;\n flex: 1 1 auto;\n}\n\n.layout_default .picker_done, .layout_default .picker_cancel {\n order: 1;\n}\n\n.picker_wrapper {\n box-sizing: border-box;\n background: #f2f2f2;\n box-shadow: 0 0 0 1px silver;\n cursor: default;\n font-family: sans-serif;\n color: #444;\n pointer-events: auto;\n}\n\n.picker_wrapper:focus {\n outline: none;\n}\n\n.picker_wrapper button, .picker_wrapper input {\n box-sizing: border-box;\n border: none;\n box-shadow: 0 0 0 1px silver;\n outline: none;\n}\n\n.picker_wrapper button:focus, .picker_wrapper button:active, .picker_wrapper input:focus, .picker_wrapper input:active {\n box-shadow: 0 0 2px 1px #1e90ff;\n}\n\n.picker_wrapper button {\n padding: 0.4em 0.6em;\n cursor: pointer;\n background-color: #f5f5f5;\n background-image: linear-gradient(0deg, gainsboro, transparent);\n}\n\n.picker_wrapper button:active {\n background-image: linear-gradient(0deg, transparent, gainsboro);\n}\n\n.picker_wrapper button:hover {\n background-color: #fff;\n}\n\n.picker_selector {\n position: absolute;\n z-index: 1;\n display: block;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n border: 2px solid #fff;\n border-radius: 100%;\n box-shadow: 0 0 3px 1px #67b9ff;\n background: currentColor;\n cursor: pointer;\n}\n\n.picker_slider .picker_selector {\n border-radius: 2px;\n}\n\n.picker_hue {\n position: relative;\n background-image: linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_sl {\n position: relative;\n box-shadow: 0 0 0 1px silver;\n background-image: linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%), linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%), linear-gradient(90deg, #808080, rgba(128, 128, 128, 0));\n}\n\n.picker_alpha, .picker_sample {\n position: relative;\n background: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em, linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_alpha .picker_selector, .picker_sample .picker_selector {\n background: none;\n}\n\n.picker_editor input {\n font-family: monospace;\n padding: 0.2em 0.4em;\n}\n\n.picker_sample::before {\n content: \"\";\n position: absolute;\n display: block;\n width: 100%;\n height: 100%;\n background: currentColor;\n}\n\n.picker_arrow {\n position: absolute;\n z-index: -1;\n}\n\n.picker_wrapper.popup {\n position: absolute;\n z-index: 2;\n margin: 1.5em;\n}\n\n.picker_wrapper.popup, .picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n background: #f2f2f2;\n box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.4);\n}\n\n.picker_wrapper.popup .picker_arrow {\n width: 3em;\n height: 3em;\n margin: 0;\n}\n\n.picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n z-index: -99;\n}\n\n.picker_wrapper.popup .picker_arrow::before {\n width: 100%;\n height: 100%;\n -webkit-transform: skew(45deg);\n transform: skew(45deg);\n -webkit-transform-origin: 0 100%;\n transform-origin: 0 100%;\n}\n\n.picker_wrapper.popup .picker_arrow::after {\n width: 150%;\n height: 150%;\n box-shadow: none;\n}\n\n.popup.popup_top {\n bottom: 100%;\n left: 0;\n}\n\n.popup.popup_top .picker_arrow {\n bottom: 0;\n left: 0;\n -webkit-transform: rotate(-90deg);\n transform: rotate(-90deg);\n}\n\n.popup.popup_bottom {\n top: 100%;\n left: 0;\n}\n\n.popup.popup_bottom .picker_arrow {\n top: 0;\n left: 0;\n -webkit-transform: rotate(90deg) scale(1, -1);\n transform: rotate(90deg) scale(1, -1);\n}\n\n.popup.popup_left {\n top: 0;\n right: 100%;\n}\n\n.popup.popup_left .picker_arrow {\n top: 0;\n right: 0;\n -webkit-transform: scale(-1, 1);\n transform: scale(-1, 1);\n}\n\n.popup.popup_right {\n top: 0;\n left: 100%;\n}\n\n.popup.popup_right .picker_arrow {\n top: 0;\n left: 0;\n}\n\n.igv-container {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n padding-top: 4px;\n user-select: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n min-height: 160px;\n}\n\n.igv-viewport {\n position: relative;\n margin-top: 5px;\n line-height: 1;\n overflow-x: hidden;\n overflow-y: hidden;\n}\n\n.igv-viewport-content {\n position: relative;\n width: 100%;\n}\n.igv-viewport-content > canvas {\n position: relative;\n display: block;\n}\n\n.igv-column-container {\n position: relative;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n width: 100%;\n}\n\n.igv-column-shim {\n width: 1px;\n margin-left: 2px;\n margin-right: 2px;\n background-color: #545453;\n}\n\n.igv-axis-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 50px;\n}\n.igv-axis-column > div {\n position: relative;\n margin-top: 5px;\n width: 100%;\n}\n.igv-axis-column > div > div {\n z-index: 512;\n position: absolute;\n top: 8px;\n left: 8px;\n width: fit-content;\n height: fit-content;\n background-color: transparent;\n display: grid;\n align-items: start;\n justify-items: center;\n}\n.igv-axis-column > div > div > input {\n display: block;\n margin: unset;\n cursor: pointer;\n}\n\n.igv-column {\n position: relative;\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-info-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-name-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-scrollbar-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 14px;\n}\n.igv-scrollbar-column > div {\n position: relative;\n margin-top: 5px;\n width: 14px;\n}\n.igv-scrollbar-column > div > div {\n cursor: pointer;\n position: absolute;\n top: 0;\n left: 2px;\n width: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: #c4c4c4;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-scrollbar-column > div > div:hover {\n background-color: #c4c4c4;\n}\n\n.igv-track-drag-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 12px;\n background-color: white;\n}\n.igv-track-drag-column > .igv-track-drag-handle {\n z-index: 512;\n position: relative;\n cursor: pointer;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.igv-track-drag-column .igv-track-drag-handle-color {\n background-color: #c4c4c4;\n}\n.igv-track-drag-column .igv-track-drag-handle-hover-color {\n background-color: #787878;\n}\n.igv-track-drag-column .igv-track-drag-handle-selected-color {\n background-color: #0963fa;\n}\n.igv-track-drag-column > .igv-track-drag-shim {\n position: relative;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n}\n\n.igv-gear-menu-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 28px;\n}\n.igv-gear-menu-column > div {\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n margin-top: 5px;\n width: 100%;\n background: white;\n}\n.igv-gear-menu-column > div > div {\n position: relative;\n margin-top: 4px;\n width: 16px;\n height: 16px;\n color: #7F7F7F;\n}\n.igv-gear-menu-column > div > div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-vertical-center {\n margin: 0 !important;\n top: 50% !important;\n -ms-transform: translateY(-50%) !important;\n transform: translateY(-50%) !important;\n}\n\n/*# sourceMappingURL=igv.css.map */\n';
72920
73631
 
72921
73632
  /**
72922
73633
  * Manages XQTL selections.
@@ -73450,7 +74161,7 @@ ${indent}columns: ${matrix.columns}
73450
74161
  this.dataRangeDialog = new DataRangeDialog(this, this.root);
73451
74162
  this.dataRangeDialog.container.id = `igv-data-range-dialog-${guid$2()}`;
73452
74163
 
73453
- this.genericColorPicker = new GenericColorPicker({ parent: this.root, width: 180 });
74164
+ this.genericColorPicker = new GenericColorPicker({parent: this.root, width: 180});
73454
74165
  this.genericColorPicker.container.id = `igv-track-color-picker-${guid$2()}`;
73455
74166
 
73456
74167
  this.sliderDialog = new SliderDialog(this.root);
@@ -73460,10 +74171,10 @@ ${indent}columns: ${matrix.columns}
73460
74171
 
73461
74172
  getSampleNameViewportWidth() {
73462
74173
 
73463
- if (undefined === this.sampleNameViewportWidth) {
74174
+ if (false === this.showSampleNames || undefined === this.sampleNameViewportWidth) {
73464
74175
  return 0
73465
74176
  } else {
73466
- return false === this.showSampleNames ? 0 : this.sampleNameViewportWidth
74177
+ return this.sampleNameViewportWidth
73467
74178
  }
73468
74179
 
73469
74180
  }
@@ -73625,7 +74336,8 @@ ${indent}columns: ${matrix.columns}
73625
74336
  } else {
73626
74337
  session = options;
73627
74338
  }
73628
- return this.loadSessionObject(session)
74339
+
74340
+ await this.loadSessionObject(session);
73629
74341
  }
73630
74342
 
73631
74343
  /**
@@ -73654,8 +74366,7 @@ ${indent}columns: ${matrix.columns}
73654
74366
  config = new XMLSession(string, knownGenomes);
73655
74367
 
73656
74368
  } else if (filename.endsWith("hub.txt")) {
73657
-
73658
- const hub = await Hub.loadHub(urlOrFile, options);
74369
+ const hub = await loadHub(urlOrFile);
73659
74370
  const genomeConfig = hub.getGenomeConfig();
73660
74371
  config = {
73661
74372
  reference: genomeConfig
@@ -73746,7 +74457,7 @@ ${indent}columns: ${matrix.columns}
73746
74457
  }
73747
74458
 
73748
74459
  // ROIs
73749
- if(session.showROIOverlays !== undefined) {
74460
+ if (session.showROIOverlays !== undefined) {
73750
74461
  this.roiManager.showOverlays = session.showROIOverlays;
73751
74462
  }
73752
74463
  this.roiManager.clearROIs();
@@ -73760,12 +74471,16 @@ ${indent}columns: ${matrix.columns}
73760
74471
  // Sample info
73761
74472
  const localSampleInfoFiles = [];
73762
74473
  if (session.sampleinfo) {
73763
- for (const config of session.sampleinfo) {
73764
-
73765
- if (config.file) {
73766
- localSampleInfoFiles.push(config.file);
74474
+ for (const sampleInfoConfig of session.sampleinfo) {
74475
+ // The "file" property is recorded in the session when a local file is referenced. It can't be used
74476
+ // on reloading, its only purpose is to present an alert to the user. This could also be used
74477
+ // to prompt the user to load the file manually, but we don't currently do that.
74478
+ if (sampleInfoConfig.file) {
74479
+ localSampleInfoFiles.push(sampleInfoConfig.file);
73767
74480
  } else {
73768
- this.loadSampleInfo(config);
74481
+ // this.loadSampleInfo(sampleInfoConfig)
74482
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74483
+
73769
74484
  }
73770
74485
 
73771
74486
  }
@@ -73806,23 +74521,14 @@ ${indent}columns: ${matrix.columns}
73806
74521
  }
73807
74522
  }
73808
74523
 
73809
- await this.loadTrackList(nonLocalTrackConfigurations);
73810
-
73811
- // The ruler and ideogram tracks are not explicitly loaded, but needs updated nonetheless.
73812
- for (let rtv of this.trackViews.filter((tv) => tv.track.type === 'ruler' || tv.track.type === 'ideogram')) {
73813
- await rtv.updateViews();
74524
+ // Load a hidden track -- used to populate searchable database without creating a track
74525
+ const configHidden = nonLocalTrackConfigurations.filter(config => true === config.hidden);
74526
+ for (const config of configHidden) {
74527
+ const featureSource = FeatureSource(config, this.genome);
74528
+ await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
73814
74529
  }
73815
74530
 
73816
- // If any tracks are selected show the selection buttons
73817
- if (this.trackViews.some(tv => tv.track.selected)) {
73818
- this.navbar.setEnableTrackSelection(true);
73819
- }
73820
-
73821
- this.updateUIWithReferenceFrameList();
73822
-
73823
- this.updateLocusSearchWidget();
73824
-
73825
- return trackConfigurations
74531
+ await this.loadTrackList(nonLocalTrackConfigurations);
73826
74532
 
73827
74533
  }
73828
74534
 
@@ -73883,13 +74589,8 @@ ${indent}columns: ${matrix.columns}
73883
74589
  }
73884
74590
 
73885
74591
  if (genomeChange) {
73886
- let trackConfigurations;
73887
- if (genomeConfig.hubURL) {
73888
- // TODO -- refactor this so "hub" is not loaded twice
73889
- const hub = await Hub.loadHub(genomeConfig.hubURL);
73890
- trackConfigurations = hub.getGroupedTrackConfigurations();
73891
- }
73892
- this.fireEvent('genomechange', [{genome, trackConfigurations}]);
74592
+
74593
+ this.fireEvent('genomechange', [{genome}]);
73893
74594
 
73894
74595
  if (this.circularView) {
73895
74596
  this.circularView.setAssembly({
@@ -73928,7 +74629,7 @@ ${indent}columns: ${matrix.columns}
73928
74629
  let genomeConfig;
73929
74630
  const isHubGenome = idOrConfig.hubURL || (idOrConfig.url && isString$2(idOrConfig.url) && idOrConfig.url.endsWith("/hub.txt"));
73930
74631
  if (isHubGenome) {
73931
- const hub = await Hub.loadHub(idOrConfig.hubURL || idOrConfig.url, idOrConfig);
74632
+ const hub = await loadHub(idOrConfig.hubURL || idOrConfig.url);
73932
74633
  genomeConfig = hub.getGenomeConfig();
73933
74634
  } else if (isString$2(idOrConfig) || !(idOrConfig.url || idOrConfig.fastaURL || idOrConfig.twoBitURL || idOrConfig.gbkURL)) {
73934
74635
  // Either an ID, a json string, or an object missing required properties.
@@ -73958,22 +74659,9 @@ ${indent}columns: ${matrix.columns}
73958
74659
 
73959
74660
  await this.loadTrackList(tracks);
73960
74661
 
73961
- await this.updateViews();
73962
-
73963
74662
  return this.genome
73964
74663
  }
73965
74664
 
73966
- /**
73967
- * Load a UCSC single-file genome assembly hub.
73968
- * @param options
73969
- * @returns {Promise<void>}
73970
- */
73971
- async loadTrackHub(options) {
73972
- const hub = await Hub.loadHub(options.url, options);
73973
- const genomeConfig = setDefaults(hub.getGenomeConfig());
73974
- return this.loadGenome(genomeConfig)
73975
- }
73976
-
73977
74665
  /**
73978
74666
  * Called after a session load, search, pan (horizontal drag), or resize
73979
74667
  *
@@ -74070,18 +74758,23 @@ ${indent}columns: ${matrix.columns}
74070
74758
  }
74071
74759
 
74072
74760
  const promises = [];
74073
- for (let config of configList) {
74074
- promises.push(this._loadTrack(config));
74761
+ for (const config of configList) {
74762
+ promises.push(this.#loadTrackHelper(config));
74075
74763
  }
74076
74764
 
74077
74765
  const loadedTracks = await Promise.all(promises);
74078
74766
 
74079
- const groupAutoscaleViews = this.trackViews.filter(function (trackView) {
74080
- return trackView.track.autoscaleGroup
74081
- });
74082
- if (groupAutoscaleViews.length > 0) {
74083
- this.updateViews();
74767
+ // If any tracks are selected show the selection buttons
74768
+ if (this.trackViews.some(({track}) => track.selected)) {
74769
+ this.navbar.setEnableTrackSelection(true);
74084
74770
  }
74771
+
74772
+ this.reorderTracks();
74773
+
74774
+ await resize.call(this);
74775
+
74776
+ this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74777
+
74085
74778
  return loadedTracks
74086
74779
  }
74087
74780
 
@@ -74095,54 +74788,23 @@ ${indent}columns: ${matrix.columns}
74095
74788
  */
74096
74789
  async loadTrack(config) {
74097
74790
 
74098
- // Default configuration sync option to true. This is the expected behavior for public API calls
74099
- config.sync = (config.sync !== false);
74100
-
74101
- const newTrack = this._loadTrack(config);
74102
-
74103
- if (newTrack && config.autoscaleGroup) {
74104
- // Await newTrack load and update all views
74105
- await newTrack;
74791
+ const loadedTracks = await this.loadTrackList([config]);
74792
+ if (config.autoscaleGroup) {
74106
74793
  this.updateViews();
74107
74794
  }
74108
-
74109
- return newTrack
74795
+ return loadedTracks[0]
74110
74796
  }
74111
74797
 
74112
- /**
74113
- * Return a promise to load a track. Private function used by loadTrack() and loadTrackList()
74114
- *
74115
- * @param config
74116
- * @returns {*}
74117
- */
74118
-
74119
- async _loadTrack(config) {
74798
+ async #loadTrackHelper(config) {
74120
74799
 
74121
74800
  // config might be json
74122
74801
  if (isString$2(config)) {
74123
74802
  config = JSON.parse(config);
74124
74803
  }
74125
74804
 
74805
+ let track;
74126
74806
  try {
74127
-
74128
- // Load a hidden track -- used to populate searchable database without creating a track
74129
- if (config.hidden) {
74130
- const featureSource = FeatureSource(config, this.genome);
74131
- await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
74132
- return
74133
- }
74134
-
74135
- const newTrack = await this.createTrack(config);
74136
-
74137
- if ('sampleinfo' === config.type) {
74138
- this.layoutChange();
74139
- return
74140
- } else if (undefined === newTrack) {
74141
- return
74142
- }
74143
-
74144
- return this.addTrack(config, newTrack)
74145
-
74807
+ track = await this.createTrack(config);
74146
74808
  } catch (error) {
74147
74809
 
74148
74810
  let msg = error.message || error.error || error.toString();
@@ -74159,62 +74821,53 @@ ${indent}columns: ${matrix.columns}
74159
74821
  }
74160
74822
 
74161
74823
  msg = `${msg} : ${isFile(config.url) ? config.url.name : config.url}`;
74162
- // msg += (": " + FileUtils.isFile(config.url) ? config.url.name : config.url)
74163
74824
  const err = new Error(msg);
74164
74825
  console.error(err);
74165
74826
  throw err
74166
- // this.alert.present(new Error(msg), undefined)
74167
74827
  }
74168
- }
74169
74828
 
74170
- async addTrack(config, newTrack) {
74829
+ if (track) {
74830
+ return await this.addTrack(track)
74831
+ } else {
74832
+ return undefined
74833
+ }
74834
+
74835
+ }
74171
74836
 
74837
+ async addTrack(track) {
74172
74838
 
74173
74839
  // Set order field of track here, otherwise track order might get shuffled during asynchronous load
74174
- if (undefined === newTrack.order) {
74175
- newTrack.order = this.trackViews.length;
74840
+ if (undefined === track.order) {
74841
+ track.order = this.trackViews.length;
74176
74842
  }
74177
74843
 
74178
- const trackView = new TrackView(this, this.columnContainer, newTrack);
74844
+ const trackView = new TrackView(this, this.columnContainer, track);
74179
74845
  this.trackViews.push(trackView);
74180
74846
  toggleTrackLabels(this.trackViews, this.doShowTrackLabels);
74181
74847
 
74182
- if (typeof newTrack.postInit === 'function') {
74848
+ if (typeof track.postInit === 'function') {
74183
74849
  try {
74184
74850
  trackView.startSpinner();
74185
- await newTrack.postInit();
74851
+ await track.postInit();
74186
74852
  } finally {
74187
74853
  trackView.stopSpinner();
74188
74854
  }
74189
74855
  }
74190
74856
 
74191
- if (!newTrack.autoscaleGroup) {
74192
- // Group autoscale will get updated later (as a group)
74193
- if (config.sync) {
74194
- await trackView.updateViews();
74195
- } else {
74196
- trackView.updateViews();
74197
- }
74198
- }
74199
-
74200
- if (typeof newTrack.hasSamples === 'function' && newTrack.hasSamples()) {
74857
+ if (typeof track.hasSamples === 'function' && track.hasSamples()) {
74201
74858
 
74202
74859
  if (this.sampleInfo.hasAttributes()) {
74203
74860
  this.sampleInfoControl.setButtonVisibility(true);
74204
74861
  }
74205
74862
 
74206
74863
  if (this.config.showSampleNameButton !== false) {
74207
- this.sampleNameControl.show(); // If not explicitly set
74864
+ this.sampleNameControl.show();
74208
74865
  }
74209
74866
  }
74210
74867
 
74211
- // repositioned here to solve layout issue.
74212
- this.reorderTracks();
74213
- this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74868
+ track.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74214
74869
 
74215
- newTrack.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74216
-
74217
- return newTrack
74870
+ return track
74218
74871
 
74219
74872
  }
74220
74873
 
@@ -74359,7 +75012,6 @@ ${indent}columns: ${matrix.columns}
74359
75012
  }
74360
75013
  }
74361
75014
 
74362
-
74363
75015
  reorderTracks() {
74364
75016
 
74365
75017
  this.trackViews.sort(function (a, b) {
@@ -74596,19 +75248,19 @@ ${indent}columns: ${matrix.columns}
74596
75248
 
74597
75249
  this.updateLocusSearchWidget();
74598
75250
 
74599
- for (let frame of this.referenceFrameList) {
74600
- if (frame.bpPerPixel <= bppSequenceThreshold) {
74601
- await this.genome.getSequence(frame.chr, frame.start, frame.start + 1);
75251
+ for (const {bpPerPixel, chr, start} of this.referenceFrameList) {
75252
+ if (bpPerPixel <= bppSequenceThreshold) {
75253
+ await this.genome.getSequence(chr, start, start + 1);
74602
75254
  }
74603
75255
  }
74604
75256
 
74605
- for (let centerGuide of this.centerLineList) {
75257
+ for (const centerGuide of this.centerLineList) {
74606
75258
  centerGuide.repaint();
74607
75259
  }
74608
75260
 
74609
75261
  // Don't autoscale while dragging.
74610
75262
  if (this.dragObject) {
74611
- for (let trackView of trackViews) {
75263
+ for (const trackView of trackViews) {
74612
75264
  await trackView.updateViews();
74613
75265
  }
74614
75266
  } else {
@@ -74632,8 +75284,8 @@ ${indent}columns: ${matrix.columns}
74632
75284
  // Calculate group autoscale dataRange
74633
75285
  if (Object.entries(groupAutoscaleTrackViews).length > 0) {
74634
75286
  for (const [group, trackViews] of Object.entries(groupAutoscaleTrackViews)) {
74635
- const featureArray = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
74636
- const dataRange = doAutoscale(featureArray.flat());
75287
+ const inViewFeatures = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
75288
+ const dataRange = doAutoscale(inViewFeatures.flat());
74637
75289
  for (const trackView of trackViews) {
74638
75290
  trackView.track.dataRange = Object.assign({}, dataRange);
74639
75291
  trackView.track.autoscale = false;
@@ -74642,7 +75294,7 @@ ${indent}columns: ${matrix.columns}
74642
75294
  }
74643
75295
  }
74644
75296
 
74645
- await Promise.all(otherTrackViews.map(tv => tv.updateViews()));
75297
+ await Promise.all(otherTrackViews.map(trackView => trackView.updateViews()));
74646
75298
  }
74647
75299
 
74648
75300
  }
@@ -74665,10 +75317,10 @@ ${indent}columns: ${matrix.columns}
74665
75317
  referenceFrame.end = referenceFrame.start + referenceFrame.bpPerPixel * width;
74666
75318
  }
74667
75319
 
74668
- const chrName = referenceFrameList.length === 1 ? this.referenceFrameList[0].chr : '';
74669
-
74670
75320
  const loc = this.referenceFrameList.map(rf => rf.getLocusString()).join(' ');
74671
75321
 
75322
+ const chrName = referenceFrameList.length === 1 ? this.genome.getChromosomeDisplayName(this.referenceFrameList[0].chr) : '';
75323
+
74672
75324
  this.navbar.updateLocus(loc, chrName);
74673
75325
 
74674
75326
  this.fireEvent('locuschange', [this.referenceFrameList]);
@@ -74685,6 +75337,46 @@ ${indent}columns: ${matrix.columns}
74685
75337
  return Math.floor(width / columnCount)
74686
75338
  }
74687
75339
 
75340
+ /**
75341
+ * Update reference frames based on new viewport width
75342
+ * @param {number} viewportWidth - The calculated viewport width
75343
+ */
75344
+ updateReferenceFrames(viewportWidth) {
75345
+
75346
+ for (const referenceFrame of this.referenceFrameList) {
75347
+ referenceFrame.updateForViewportWidth(viewportWidth);
75348
+ }
75349
+ }
75350
+
75351
+ /**
75352
+ * Update DOM viewport elements with new width
75353
+ * @param {number} viewportWidth - The calculated viewport width
75354
+ */
75355
+ updateViewportElements(viewportWidth) {
75356
+
75357
+ for (let i = 0; i < this.referenceFrameList.length; i++) {
75358
+
75359
+ for (const {viewports} of this.trackViews) {
75360
+ viewports[i].setWidth(viewportWidth);
75361
+ }
75362
+
75363
+ for (const {sampleInfoViewport} of this.trackViews) {
75364
+ sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
75365
+ sampleInfoViewport.repaint();
75366
+ }
75367
+
75368
+ }
75369
+ }
75370
+
75371
+ /**
75372
+ * Synchronize UI state after viewport updates
75373
+ * @returns {Promise<void>}
75374
+ */
75375
+ async syncUIState() {
75376
+ this.updateUIWithReferenceFrameList();
75377
+ await this.updateViews(true);
75378
+ }
75379
+
74688
75380
  minimumBases() {
74689
75381
  return this.config.minimumBases
74690
75382
  }
@@ -74870,29 +75562,12 @@ ${indent}columns: ${matrix.columns}
74870
75562
  }
74871
75563
 
74872
75564
  /**
74873
- * @deprecated This is a deprecated method with no known usages. To be removed in a future release.
75565
+ * @deprecated This is a deprecated method with no known usages.
74874
75566
  */
74875
75567
  async goto(chr, start, end) {
74876
75568
  await this.search(chr + ":" + start + "-" + end);
74877
75569
  }
74878
75570
 
74879
- /**
74880
-
74881
- * Search for the locus string -- this function is called from various igv.js GUI elements, and is not part of the
74882
- * API. Wraps ```search``` and presents an error dialog if false.
74883
- *
74884
- * @param string
74885
- * @param init
74886
- * @returns {Promise<void>}
74887
- */
74888
- async doSearch(string, init) {
74889
- const success = await this.search(string, init);
74890
- if (!success) {
74891
- this.alert.present(new Error(`Unrecognized locus: <b> ${string} </b>`));
74892
- }
74893
- return success
74894
- }
74895
-
74896
75571
 
74897
75572
  /**
74898
75573
  * Search for the locus string
@@ -74905,6 +75580,10 @@ ${indent}columns: ${matrix.columns}
74905
75580
  async search(stringOrArray, init) {
74906
75581
 
74907
75582
  const loci = await search(this, stringOrArray);
75583
+ return this.updateLoci(loci, init)
75584
+ }
75585
+
75586
+ async updateLoci(loci, init) {
74908
75587
 
74909
75588
  if (loci && loci.length > 0) {
74910
75589
 
@@ -74941,9 +75620,9 @@ ${indent}columns: ${matrix.columns}
74941
75620
  }
74942
75621
  }
74943
75622
 
74944
- async loadSampleInfo(config) {
75623
+ async loadSampleInfo(sampleInfoConfig) {
74945
75624
 
74946
- await this.sampleInfo.loadSampleInfoFile(config.url);
75625
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74947
75626
 
74948
75627
  for (const {sampleInfoViewport} of this.trackViews) {
74949
75628
  sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
@@ -75081,9 +75760,9 @@ ${indent}columns: ${matrix.columns}
75081
75760
  json["locus"] = locus.length === 1 ? locus[0] : locus;
75082
75761
 
75083
75762
  const roiSets = this.roiManager.toJSON();
75084
- if(roiSets) {
75763
+ if (roiSets) {
75085
75764
  json["roi"] = roiSets;
75086
- if(!this.roiManager.showOverlays){
75765
+ if (!this.roiManager.showOverlays) {
75087
75766
  json["showROIOverlays"] = false; // true is the default
75088
75767
  }
75089
75768
  }
@@ -75426,8 +76105,6 @@ ${indent}columns: ${matrix.columns}
75426
76105
  }
75427
76106
  }
75428
76107
 
75429
-
75430
-
75431
76108
  // Navbar delegates
75432
76109
  get sampleInfoControl() {
75433
76110
  return this.navbar.sampleInfoControl
@@ -75449,6 +76126,9 @@ ${indent}columns: ${matrix.columns}
75449
76126
  return this.navbar.sampleNameControl
75450
76127
  }
75451
76128
 
76129
+ async blat(sequence) {
76130
+ return createBlatTrack({sequence, browser: this, name: 'Blat', title: 'Blat'})
76131
+ }
75452
76132
  }
75453
76133
 
75454
76134
  function getFileExtension(input) {
@@ -75474,7 +76154,7 @@ ${indent}columns: ${matrix.columns}
75474
76154
  }
75475
76155
 
75476
76156
  /**
75477
- * Function called win window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
76157
+ * Called when window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
75478
76158
  * than class method because it needs to be copied and bound to specific instances of browser to support listener
75479
76159
  * removal
75480
76160
  *
@@ -75482,40 +76162,14 @@ ${indent}columns: ${matrix.columns}
75482
76162
  */
75483
76163
  async function resize() {
75484
76164
 
75485
- if (!this.referenceFrameList) return
75486
-
75487
- const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
75488
-
75489
- for (let referenceFrame of this.referenceFrameList) {
75490
-
75491
- const index = this.referenceFrameList.indexOf(referenceFrame);
75492
-
75493
- const {chr, genome} = referenceFrame;
75494
-
75495
- const {bpLength} = genome.getChromosome(referenceFrame.chr);
75496
-
75497
- const viewportWidthBP = referenceFrame.toBP(viewportWidth);
75498
-
75499
- // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
75500
- if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
75501
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) bpp. viewport ${ StringUtils.numberFormatter(viewportWidthBP) } > ${ StringUtils.numberFormatter(bpLength) }.`)
75502
- referenceFrame.bpPerPixel = bpLength / viewportWidth;
75503
- } else {
75504
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) end.`)
75505
- referenceFrame.end = referenceFrame.start + referenceFrame.toBP(viewportWidth);
75506
- }
75507
-
75508
- for (let {viewports} of this.trackViews) {
75509
- viewports[index].setWidth(viewportWidth);
75510
- }
75511
-
76165
+ if (undefined === this.referenceFrameList || 0 === this.referenceFrameList.length) {
76166
+ return
75512
76167
  }
75513
76168
 
75514
- this.updateUIWithReferenceFrameList();
75515
-
75516
- //TODO -- update view only if needed. Reducing size never needed. Increasing size maybe
75517
-
75518
- await this.updateViews(true);
76169
+ const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
76170
+ this.updateReferenceFrames(viewportWidth);
76171
+ this.updateViewportElements(viewportWidth);
76172
+ await this.syncUIState();
75519
76173
  }
75520
76174
 
75521
76175
 
@@ -75984,7 +76638,8 @@ ${indent}columns: ${matrix.columns}
75984
76638
  registerTrackClass,
75985
76639
  registerTrackCreatorFunction,
75986
76640
  registerFileFormats,
75987
- loadSessionFile: Browser.loadSessionFile
76641
+ loadSessionFile: Browser.loadSessionFile,
76642
+ loadHub
75988
76643
  };
75989
76644
 
75990
76645
  return index;