@apollo-annotation/jbrowse-plugin-apollo 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.esm.js +2072 -1496
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2069 -1493
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +2256 -1533
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +13 -11
  12. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +7 -10
  13. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +3 -0
  14. package/src/FeatureDetailsWidget/Attributes.tsx +27 -27
  15. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +65 -0
  16. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +6 -1
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +25 -2
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +0 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +8 -1
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +88 -40
  21. package/src/LinearApolloDisplay/glyphs/Glyph.ts +8 -1
  22. package/src/LinearApolloDisplay/stateModel/base.ts +28 -2
  23. package/src/LinearApolloDisplay/stateModel/layouts.ts +65 -11
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +25 -6
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +1 -2
  26. package/src/OntologyManager/OntologyStore/index.ts +6 -2
  27. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +41 -13
  28. package/src/OntologyManager/index.ts +35 -0
  29. package/src/SixFrameFeatureDisplay/stateModel.ts +11 -2
  30. package/src/TabularEditor/HybridGrid/Feature.tsx +1 -2
  31. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +0 -1
  32. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +8 -1
  33. package/src/components/AddRefSeqAliases.tsx +7 -8
  34. package/src/components/CopyFeature.tsx +1 -1
  35. package/src/components/CreateApolloAnnotation.tsx +304 -0
  36. package/src/components/DownloadGFF3.tsx +5 -1
  37. package/src/components/FilterFeatures.tsx +120 -0
  38. package/src/components/ModifyFeatureAttribute.tsx +27 -27
  39. package/src/components/OntologyTermMultiSelect.tsx +5 -5
  40. package/src/extensions/annotationFromJBrowseFeature.test.ts +119 -0
  41. package/src/extensions/annotationFromJBrowseFeature.ts +171 -0
  42. package/src/extensions/annotationFromPileup.ts +1 -1
  43. package/src/extensions/index.ts +1 -0
  44. package/src/index.ts +8 -2
  45. package/src/session/ClientDataStore.ts +29 -0
  46. package/src/session/session.ts +2 -5
  47. package/src/LinearApolloDisplay/stateModel/getGlyph.ts +0 -40
@@ -4702,10 +4702,10 @@
4702
4702
  annotationFeatureToGFF3$1.annotationFeatureToGFF3 = annotationFeatureToGFF3;
4703
4703
  var util_1$4 = require$$1__default$1["default"];
4704
4704
  function annotationFeatureToGFF3(feature, parentId, refSeqNames) {
4705
- var _feature$attributes$g, _feature$attributes, _feature$attributes2;
4706
- var attributes = JSON.parse(JSON.stringify(feature.attributes));
4705
+ var _feature$attributes, _feature$attributes$g, _feature$attributes2, _feature$attributes3;
4706
+ var attributes = JSON.parse(JSON.stringify((_feature$attributes = feature.attributes) !== null && _feature$attributes !== void 0 ? _feature$attributes : {}));
4707
4707
  var ontologyTerms = [];
4708
- var source = (_feature$attributes$g = (_feature$attributes = feature.attributes) === null || _feature$attributes === void 0 || (_feature$attributes = _feature$attributes.gff_source) === null || _feature$attributes === void 0 ? void 0 : _feature$attributes[0]) !== null && _feature$attributes$g !== void 0 ? _feature$attributes$g : null;
4708
+ var source = (_feature$attributes$g = (_feature$attributes2 = feature.attributes) === null || _feature$attributes2 === void 0 || (_feature$attributes2 = _feature$attributes2.gff_source) === null || _feature$attributes2 === void 0 ? void 0 : _feature$attributes2[0]) !== null && _feature$attributes$g !== void 0 ? _feature$attributes$g : null;
4709
4709
  delete attributes.gff_source;
4710
4710
  if (parentId) {
4711
4711
  attributes.Parent = [parentId];
@@ -4763,7 +4763,7 @@
4763
4763
  if (ontologyTerms.length > 0) {
4764
4764
  attributes.Ontology_term = ontologyTerms;
4765
4765
  }
4766
- var gff_score = (_feature$attributes2 = feature.attributes) === null || _feature$attributes2 === void 0 ? void 0 : _feature$attributes2.gff_score;
4766
+ var gff_score = (_feature$attributes3 = feature.attributes) === null || _feature$attributes3 === void 0 ? void 0 : _feature$attributes3.gff_score;
4767
4767
  var score = null;
4768
4768
  if (gff_score && gff_score.length > 0) {
4769
4769
  if (gff_score[0]) {
@@ -5069,7 +5069,7 @@
5069
5069
  min = _getFeatureMinMax2[0],
5070
5070
  max = _getFeatureMinMax2[1];
5071
5071
  var convertedChildren = convertChildren(gff3Feature, refSeq, featureIds);
5072
- var convertedAttributes = convertFeatureAttributes(gff3Feature);
5072
+ var convertedAttributes = convertFeatureAttributes$1(gff3Feature);
5073
5073
  var feature = {
5074
5074
  _id: new bson_objectid_1$1["default"]().toHexString(),
5075
5075
  refSeq: refSeq !== null && refSeq !== void 0 ? refSeq : refName,
@@ -5117,7 +5117,7 @@
5117
5117
  var max = Math.max.apply(Math, _toConsumableArray(maxes));
5118
5118
  return [min - 1, max];
5119
5119
  }
5120
- function convertFeatureAttributes(gff3Feature) {
5120
+ function convertFeatureAttributes$1(gff3Feature) {
5121
5121
  var convertedAttributes = {};
5122
5122
  var scores = gff3Feature.map(function (f) {
5123
5123
  return f.score;
@@ -5171,6 +5171,9 @@
5171
5171
  var newKey = (0, gffReservedKeys_1.isGFFReservedAttribute)(key) ? gffReservedKeys_1.gffToInternal[key] : key;
5172
5172
  var existingVal = convertedAttributes[newKey];
5173
5173
  if (existingVal) {
5174
+ // if (JSON.stringify(existingVal) === JSON.stringify(val)) {
5175
+ // continue
5176
+ // }
5174
5177
  var valSet = new Set([].concat(_toConsumableArray(existingVal), _toConsumableArray(val)));
5175
5178
  convertedAttributes[newKey] = _toConsumableArray(valSet);
5176
5179
  } else {
@@ -5204,6 +5207,8 @@
5204
5207
  firstFeature = _locationsWithChildre[0];
5205
5208
  var childFeatures = firstFeature.child_features;
5206
5209
  var cdsFeatures = [];
5210
+ var exonFeatures = [];
5211
+ var utrFeatures = [];
5207
5212
  var _iterator2 = _createForOfIteratorHelper(childFeatures),
5208
5213
  _step2;
5209
5214
  try {
@@ -5211,6 +5216,12 @@
5211
5216
  var childFeature = _step2.value;
5212
5217
  var _childFeature = _slicedToArray(childFeature, 1),
5213
5218
  firstChildFeatureLocation = _childFeature[0];
5219
+ if (firstChildFeatureLocation.type === 'exon') {
5220
+ exonFeatures.push(childFeature);
5221
+ }
5222
+ if (firstChildFeatureLocation.type === 'three_prime_UTR' || firstChildFeatureLocation.type === 'five_prime_UTR') {
5223
+ utrFeatures.push(childFeature);
5224
+ }
5214
5225
  if (firstChildFeatureLocation.type === 'three_prime_UTR' || firstChildFeatureLocation.type === 'five_prime_UTR' || firstChildFeatureLocation.type === 'intron' || firstChildFeatureLocation.type === 'start_codon' || firstChildFeatureLocation.type === 'stop_codon') {
5215
5226
  continue;
5216
5227
  }
@@ -5226,24 +5237,217 @@
5226
5237
  } finally {
5227
5238
  _iterator2.f();
5228
5239
  }
5229
- var processedCDS = cdsFeatures.length > 0 ? processCDS(cdsFeatures, refSeq, featureIds) : [];
5230
- var _iterator3 = _createForOfIteratorHelper(processedCDS),
5231
- _step3;
5232
- try {
5233
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
5234
- var cds = _step3.value;
5235
- convertedChildren[cds._id] = cds;
5240
+ if (cdsFeatures.length > 0) {
5241
+ var processedCDS = processCDS(cdsFeatures, refSeq, featureIds);
5242
+ var _iterator3 = _createForOfIteratorHelper(processedCDS),
5243
+ _step3;
5244
+ try {
5245
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
5246
+ var cds = _step3.value;
5247
+ convertedChildren[cds._id] = cds;
5248
+ }
5249
+ } catch (err) {
5250
+ _iterator3.e(err);
5251
+ } finally {
5252
+ _iterator3.f();
5253
+ }
5254
+ var missingExons = inferMissingExons(cdsFeatures, exonFeatures, utrFeatures, processedCDS[0].refSeq);
5255
+ var _iterator4 = _createForOfIteratorHelper(missingExons),
5256
+ _step4;
5257
+ try {
5258
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
5259
+ var exon = _step4.value;
5260
+ convertedChildren[exon._id] = exon;
5261
+ }
5262
+ } catch (err) {
5263
+ _iterator4.e(err);
5264
+ } finally {
5265
+ _iterator4.f();
5236
5266
  }
5237
- } catch (err) {
5238
- _iterator3.e(err);
5239
- } finally {
5240
- _iterator3.f();
5241
5267
  }
5242
5268
  if (Object.keys(convertedChildren).length > 0) {
5243
5269
  return convertedChildren;
5244
5270
  }
5245
5271
  return;
5246
5272
  }
5273
+ function inferMissingExons(cdsFeatures, existingExons, utrFeatures, refSeq) {
5274
+ // Convert utrFeatures from GFF3Feature to AnnotationFeatureSnapshot
5275
+ var utrExons = [];
5276
+ var _iterator5 = _createForOfIteratorHelper(utrFeatures),
5277
+ _step5;
5278
+ try {
5279
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
5280
+ var utrs = _step5.value;
5281
+ var _iterator7 = _createForOfIteratorHelper(utrs),
5282
+ _step7;
5283
+ try {
5284
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
5285
+ var utr = _step7.value;
5286
+ if (!utr.start || !utr.end) {
5287
+ throw new Error("UTR has undefined start and/or end\n: ".concat(JSON.stringify(utr, null, 2)));
5288
+ }
5289
+ var strand = undefined;
5290
+ if (utr.strand === '+') {
5291
+ strand = 1;
5292
+ } else if (utr.strand === '-') {
5293
+ strand = -1;
5294
+ }
5295
+ utrExons.push({
5296
+ _id: new bson_objectid_1$1["default"]().toHexString(),
5297
+ refSeq: refSeq,
5298
+ type: 'exon',
5299
+ min: utr.start - 1,
5300
+ max: utr.end,
5301
+ strand: strand
5302
+ });
5303
+ }
5304
+ } catch (err) {
5305
+ _iterator7.e(err);
5306
+ } finally {
5307
+ _iterator7.f();
5308
+ }
5309
+ }
5310
+ } catch (err) {
5311
+ _iterator5.e(err);
5312
+ } finally {
5313
+ _iterator5.f();
5314
+ }
5315
+ utrExons.sort(function (a, b) {
5316
+ return a.min - b.min;
5317
+ });
5318
+ var missingExons = [];
5319
+ var _iterator6 = _createForOfIteratorHelper(cdsFeatures),
5320
+ _step6;
5321
+ try {
5322
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
5323
+ var protein = _step6.value;
5324
+ protein.sort(function (a, b) {
5325
+ if (!a.start || !b.start) {
5326
+ throw new Error('CDS has undefined start');
5327
+ }
5328
+ return a.start - b.start;
5329
+ });
5330
+ for (var cdsIdx = 0; cdsIdx < protein.length; cdsIdx++) {
5331
+ var cds = protein[cdsIdx];
5332
+ // For CDS check if there is an exon containing it. If not, create an exon with same coords as the CDS.
5333
+ var exonFound = false;
5334
+ var _iterator8 = _createForOfIteratorHelper(existingExons),
5335
+ _step8;
5336
+ try {
5337
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
5338
+ var x = _step8.value;
5339
+ if (x.length != 1) {
5340
+ throw new Error('Unexpected number of exons');
5341
+ }
5342
+ var _x = _slicedToArray(x, 1),
5343
+ exon = _x[0];
5344
+ if (exon.start && exon.end && cds.start && cds.end && exon.start <= cds.start && exon.end >= cds.end) {
5345
+ exonFound = true;
5346
+ break;
5347
+ }
5348
+ }
5349
+ } catch (err) {
5350
+ _iterator8.e(err);
5351
+ } finally {
5352
+ _iterator8.f();
5353
+ }
5354
+ if (!exonFound) {
5355
+ if (!cds.start || !cds.end) {
5356
+ throw new Error("CDS has undefined start and/or end: ".concat(JSON.stringify(cds, null, 2)));
5357
+ }
5358
+ var _strand = undefined;
5359
+ if (cds.strand === '+') {
5360
+ _strand = 1;
5361
+ } else if (cds.strand === '-') {
5362
+ _strand = -1;
5363
+ }
5364
+ var newExon = {
5365
+ _id: new bson_objectid_1$1["default"]().toHexString(),
5366
+ refSeq: refSeq,
5367
+ type: 'exon',
5368
+ min: cds.start - 1,
5369
+ max: cds.end,
5370
+ strand: _strand
5371
+ };
5372
+ if (cdsIdx === 0) {
5373
+ // If this CDS is the leftmost (or the only CDS in this protein), check if we need to add UTRs before it
5374
+ var _iterator9 = _createForOfIteratorHelper(utrExons),
5375
+ _step9;
5376
+ try {
5377
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
5378
+ var _utr = _step9.value;
5379
+ if (_utr.max > newExon.min) {
5380
+ break;
5381
+ }
5382
+ if (_utr.max === newExon.min) {
5383
+ // UTR ends where exon begins: Extend the exon to include this UTR
5384
+ newExon.min = _utr.min;
5385
+ } else {
5386
+ missingExons.push(_utr);
5387
+ }
5388
+ }
5389
+ } catch (err) {
5390
+ _iterator9.e(err);
5391
+ } finally {
5392
+ _iterator9.f();
5393
+ }
5394
+ }
5395
+ if (cdsIdx === protein.length - 1) {
5396
+ // If this CDS is the rightmost (or the only CDS in this protein), check if we need to add UTRs after it
5397
+ var _iterator10 = _createForOfIteratorHelper(utrExons),
5398
+ _step10;
5399
+ try {
5400
+ for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
5401
+ var _utr2 = _step10.value;
5402
+ if (_utr2.min < newExon.max) {
5403
+ continue;
5404
+ }
5405
+ if (_utr2.min === newExon.max) {
5406
+ // UTR begins where exon end: Extend the exon to include this UTR
5407
+ newExon.max = _utr2.max;
5408
+ } else {
5409
+ missingExons.push(_utr2);
5410
+ }
5411
+ }
5412
+ } catch (err) {
5413
+ _iterator10.e(err);
5414
+ } finally {
5415
+ _iterator10.f();
5416
+ }
5417
+ }
5418
+ missingExons.push(newExon);
5419
+ }
5420
+ }
5421
+ }
5422
+ } catch (err) {
5423
+ _iterator6.e(err);
5424
+ } finally {
5425
+ _iterator6.f();
5426
+ }
5427
+ var mergedExons = mergeAnnotationFeatures(missingExons);
5428
+ return mergedExons;
5429
+ }
5430
+ function mergeAnnotationFeatures(features) {
5431
+ if (features.length === 0) {
5432
+ return [];
5433
+ }
5434
+ features.sort(function (a, b) {
5435
+ return a.min - b.min;
5436
+ });
5437
+ var res = [];
5438
+ res.push(features[0]);
5439
+ for (var i = 1; i < features.length; i++) {
5440
+ var last = res.at(-1);
5441
+ var curr = features[i];
5442
+ // If current interval overlaps with the last merged interval, merge them
5443
+ if (last && curr.min <= last.max) {
5444
+ last.max = Math.max(last.max, curr.max);
5445
+ } else {
5446
+ res.push(curr);
5447
+ }
5448
+ }
5449
+ return res;
5450
+ }
5247
5451
  /**
5248
5452
  * If a GFF3 file has CDS features that either (1) don't have an ID or (2) have
5249
5453
  * different IDs for each CDS, we have to do a bit of guessing about how they
@@ -5295,11 +5499,11 @@
5295
5499
  return cds[0];
5296
5500
  });
5297
5501
  var groupedLocations = [];
5298
- var _iterator4 = _createForOfIteratorHelper(cdsLocations),
5299
- _step4;
5502
+ var _iterator11 = _createForOfIteratorHelper(cdsLocations),
5503
+ _step11;
5300
5504
  try {
5301
5505
  var _loop = function _loop() {
5302
- var location = _step4.value;
5506
+ var location = _step11.value;
5303
5507
  var lastGroup = groupedLocations.at(-1);
5304
5508
  if (!lastGroup) {
5305
5509
  groupedLocations.push([location]);
@@ -5315,13 +5519,13 @@
5315
5519
  lastGroup.push(location);
5316
5520
  }
5317
5521
  };
5318
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
5522
+ for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
5319
5523
  if (_loop()) continue;
5320
5524
  }
5321
5525
  } catch (err) {
5322
- _iterator4.e(err);
5526
+ _iterator11.e(err);
5323
5527
  } finally {
5324
- _iterator4.f();
5528
+ _iterator11.f();
5325
5529
  }
5326
5530
  return groupedLocations.map(function (group) {
5327
5531
  return gff3ToAnnotationFeature(group, refSeq, featureIds);
@@ -20864,7 +21068,7 @@
20864
21068
  }), 'Add');
20865
21069
  default_1$a = Add["default"] = _default$c;
20866
21070
 
20867
- var version = "0.3.1";
21071
+ var version = "0.3.2";
20868
21072
 
20869
21073
  const ApolloConfigSchema = configuration.ConfigurationSchema('ApolloInternetAccount', {
20870
21074
  baseURL: {
@@ -39013,17 +39217,6 @@
39013
39217
  }
39014
39218
  return _createClass(AbortError);
39015
39219
  }( /*#__PURE__*/_wrapNativeSuper(Error));
39016
- /**
39017
- * properly check if the given AbortSignal is aborted.
39018
- * per the standard, if the signal reads as aborted,
39019
- * this function throws either a DOMException AbortError, or a regular error
39020
- * with a `code` attribute set to `ERR_ABORTED`.
39021
- *
39022
- * for convenience, passing `undefined` is a no-op
39023
- *
39024
- * @param signal -
39025
- * @returns nothing
39026
- */
39027
39220
  function checkAbortSignal(signal) {
39028
39221
  if (!signal) {
39029
39222
  return;
@@ -39040,11 +39233,6 @@
39040
39233
  return setTimeout(resolve, ms);
39041
39234
  });
39042
39235
  }
39043
- /**
39044
- * Skips to the next tick, then runs `checkAbortSignal`.
39045
- * Await this to inside an otherwise synchronous loop to
39046
- * provide a place to break when an abort signal is received.
39047
- */
39048
39236
  function abortBreakPoint(_x) {
39049
39237
  return _abortBreakPoint.apply(this, arguments);
39050
39238
  }
@@ -39075,26 +39263,12 @@
39075
39263
  }
39076
39264
  function observeAbortSignal(signal) {
39077
39265
  if (!signal) {
39078
- return rxjs_1.Observable.create();
39266
+ return new rxjs_1.Observable();
39079
39267
  }
39080
39268
  return (0, rxjs_1.fromEvent)(signal, 'abort');
39081
39269
  }
39082
- /**
39083
- * check if the given exception was caused by an operation being intentionally aborted
39084
- * @param exception -
39085
- */
39086
39270
  function isAbortException(exception) {
39087
- return exception instanceof Error && (
39088
- // DOMException
39089
- exception.name === 'AbortError' ||
39090
- // standard-ish non-DOM abort exception
39091
- exception.code === 'ERR_ABORTED' ||
39092
- // message contains aborted for bubbling through RPC
39093
- // things we have seen that we want to catch here
39094
- // Error: aborted
39095
- // AbortError: aborted
39096
- // AbortError: The user aborted a request.
39097
- !!/\b(aborted|aborterror)\b/i.test(exception.message));
39271
+ return exception instanceof Error && (exception.name === 'AbortError' || exception.code === 'ERR_ABORTED' || !!/\b(aborted|aborterror)\b/i.test(exception.message));
39098
39272
  }
39099
39273
 
39100
39274
  var jsonpath$1 = {exports: {}};
@@ -46029,6 +46203,8 @@
46029
46203
  /** load a OBO Graph JSON file into a database */
46030
46204
  async function loadOboGraphJson(db) {
46031
46205
  const startTime = Date.now();
46206
+ let percentProgress = 1;
46207
+ this.options.update?.('Parsing JSON', percentProgress);
46032
46208
  // TODO: using file streaming along with an event-based json parser
46033
46209
  // instead of JSON.parse and .readFile could probably make this faster
46034
46210
  // and less memory intensive
@@ -46039,6 +46215,8 @@
46039
46215
  catch {
46040
46216
  throw new Error('Error in loading ontology');
46041
46217
  }
46218
+ percentProgress += 5;
46219
+ this.options.update?.('Parsing JSON complete', percentProgress);
46042
46220
  const parseTime = Date.now();
46043
46221
  const [graph, ...additionalGraphs] = oboGraph.graphs ?? [];
46044
46222
  if (!graph) {
@@ -46057,31 +46235,51 @@
46057
46235
  const fullTextIndexPaths = getTextIndexFields
46058
46236
  .call(this)
46059
46237
  .map((def) => def.jsonPath);
46060
- for (const node of graph.nodes ?? []) {
46061
- if (isOntologyDBNode(node)) {
46062
- await nodeStore.add({
46063
- ...node,
46064
- fullTextWords: serializeWords(getWords(node, fullTextIndexPaths, this.prefixes)),
46065
- });
46238
+ if (graph.nodes) {
46239
+ let lastProgress = Math.round(percentProgress);
46240
+ for (const [, node] of graph.nodes.entries()) {
46241
+ percentProgress += 64 * (1 / graph.nodes.length);
46242
+ if (Math.round(percentProgress) != lastProgress &&
46243
+ percentProgress < 100) {
46244
+ this.options.update?.('Processing nodes', percentProgress);
46245
+ lastProgress = Math.round(percentProgress);
46246
+ }
46247
+ if (isOntologyDBNode(node)) {
46248
+ await nodeStore.add({
46249
+ ...node,
46250
+ fullTextWords: serializeWords(getWords(node, fullTextIndexPaths, this.prefixes)),
46251
+ });
46252
+ }
46066
46253
  }
46067
46254
  }
46068
46255
  // load edges
46069
46256
  const edgeStore = tx.objectStore('edges');
46070
- for (const edge of graph.edges ?? []) {
46071
- if (isOntologyDBEdge(edge)) {
46072
- await edgeStore.add(edge);
46257
+ if (graph.edges) {
46258
+ let lastProgress = Math.round(percentProgress);
46259
+ for (const [, edge] of graph.edges.entries()) {
46260
+ percentProgress += 30 * (1 / graph.edges.length);
46261
+ if (Math.round(percentProgress) != lastProgress &&
46262
+ percentProgress < 100) {
46263
+ this.options.update?.('Processing edges', percentProgress);
46264
+ lastProgress = Math.round(percentProgress);
46265
+ }
46266
+ if (isOntologyDBEdge(edge)) {
46267
+ await edgeStore.add(edge);
46268
+ }
46073
46269
  }
46074
46270
  }
46075
46271
  await tx.done;
46076
46272
  // record some metadata about this ontology and load operation
46077
46273
  const tx2 = db.transaction('meta', 'readwrite');
46274
+ // eslint-disable-next-line @typescript-eslint/unbound-method
46275
+ const { update, ...otherOptions } = this.options;
46078
46276
  await tx2.objectStore('meta').add({
46079
46277
  ontologyRecord: {
46080
46278
  name: this.ontologyName,
46081
46279
  version: this.ontologyVersion,
46082
46280
  sourceLocation: this.sourceLocation,
46083
46281
  },
46084
- storeOptions: this.options,
46282
+ storeOptions: otherOptions,
46085
46283
  graphMeta: graph.meta,
46086
46284
  timestamp: String(new Date()),
46087
46285
  schemaVersion,
@@ -46146,8 +46344,8 @@
46146
46344
  this.ontologyName = name;
46147
46345
  this.ontologyVersion = version;
46148
46346
  this.sourceLocation = source;
46149
- this.db = this.prepareDatabase();
46150
46347
  this.options = options ?? {};
46348
+ this.db = this.prepareDatabase();
46151
46349
  }
46152
46350
  /**
46153
46351
  * check that the configuration of this ontology appears valid. Does not
@@ -46192,9 +46390,12 @@
46192
46390
  return db;
46193
46391
  }
46194
46392
  try {
46195
- const { sourceLocation, sourceType } = this;
46393
+ const { options, sourceLocation, sourceType } = this;
46196
46394
  if (sourceType === 'obo-graph-json') {
46395
+ options.update?.('', 0);
46396
+ // add more updates inside `loadOboGraphJson`
46197
46397
  await this.loadOboGraphJson(db);
46398
+ options.update?.('', 100);
46198
46399
  }
46199
46400
  else {
46200
46401
  throw new Error(`ontology source file ${JSON.stringify(sourceLocation)} has type ${sourceType}, which is not yet supported`);
@@ -46414,6 +46615,7 @@
46414
46615
  version: 'unversioned',
46415
46616
  source: require$$1$3.types.union(mst.LocalPathLocation, mst.UriLocation, mst.BlobLocation),
46416
46617
  options: require$$1$3.types.frozen(),
46618
+ equivalentTypes: require$$1$3.types.map(require$$1$3.types.array(require$$1$3.types.string)),
46417
46619
  })
46418
46620
  .volatile((_self) => ({
46419
46621
  dataStore: undefined,
@@ -46431,6 +46633,37 @@
46431
46633
  this.initDataStore();
46432
46634
  }));
46433
46635
  },
46636
+ setEquivalentTypes(type, equivalentTypes) {
46637
+ self.equivalentTypes.set(type, equivalentTypes);
46638
+ },
46639
+ }))
46640
+ .actions((self) => ({
46641
+ loadEquivalentTypes: require$$1$3.flow(function* loadEquivalentTypes(type) {
46642
+ if (!self.dataStore) {
46643
+ return;
46644
+ }
46645
+ const terms = (yield self.dataStore.getTermsWithLabelOrSynonym(type));
46646
+ const equivalents = terms
46647
+ .map((term) => term.lbl)
46648
+ .filter((term) => term != undefined);
46649
+ self.setEquivalentTypes(type, equivalents);
46650
+ }),
46651
+ }))
46652
+ .views((self) => ({
46653
+ isTypeOf(queryType, typeOf) {
46654
+ if (queryType === typeOf) {
46655
+ return true;
46656
+ }
46657
+ if (!self.dataStore) {
46658
+ return false;
46659
+ }
46660
+ const equivalents = self.equivalentTypes.get(typeOf);
46661
+ if (!equivalents) {
46662
+ void self.loadEquivalentTypes(typeOf);
46663
+ return false;
46664
+ }
46665
+ return equivalents.includes(queryType);
46666
+ },
46434
46667
  }));
46435
46668
  const OntologyManagerType = require$$1$3.types
46436
46669
  .model('OntologyManager', {
@@ -46884,7 +47117,7 @@
46884
47117
  }
46885
47118
  const newRefNames = [...Object.entries(refNameAliases)]
46886
47119
  .filter(([id, refName]) => id !== refName)
46887
- .map(([id, refName]) => ({ _id: id, name: refName ?? '' }));
47120
+ .map(([id, refName]) => ({ _id: id, name: refName }));
46888
47121
  setRefNames(newRefNames);
46889
47122
  setSelectedRefSeqId(newRefNames[0]?._id || '');
46890
47123
  }
@@ -47249,7 +47482,11 @@
47249
47482
  }
47250
47483
  const { exportID } = (await response.json());
47251
47484
  const exportURL = new URL('export', internetAccount.baseURL);
47252
- const exportSearchParams = new URLSearchParams({ exportID });
47485
+ const params = {
47486
+ exportID,
47487
+ includeFASTA: 'true',
47488
+ };
47489
+ const exportSearchParams = new URLSearchParams(params);
47253
47490
  exportURL.search = exportSearchParams.toString();
47254
47491
  const exportUri = exportURL.toString();
47255
47492
  window.open(exportUri, '_blank');
@@ -48473,8 +48710,8 @@
48473
48710
  // .map((m) => m.score)
48474
48711
  // .join(', ')
48475
48712
  return (React__namespace.createElement("li", { ...other },
48476
- React__namespace.createElement(material.Grid, { container: true },
48477
- React__namespace.createElement(material.Grid, { item: true },
48713
+ React__namespace.createElement(material.Grid2, { container: true },
48714
+ React__namespace.createElement(material.Grid2, null,
48478
48715
  React__namespace.createElement(material.Typography, { component: "span" }, ontologyManager.applyPrefixes(option.term.id)),
48479
48716
  ' ',
48480
48717
  React__namespace.createElement(HighlightedText, { str: option.term.lbl ?? '(no label)', search: inputValue }),
@@ -48675,43 +48912,43 @@
48675
48912
  return (React__default["default"].createElement(Dialog, { open: true, title: "Feature attributes", handleClose: handleClose, maxWidth: false, "data-testid": "modify-feature-attribute" },
48676
48913
  React__default["default"].createElement("form", { onSubmit: onSubmit },
48677
48914
  React__default["default"].createElement(material.DialogContent, null,
48678
- React__default["default"].createElement(material.Grid, { container: true, direction: "column", spacing: 1 },
48915
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column", spacing: 1 },
48679
48916
  Object.entries(attributes).map(([key, value]) => {
48680
48917
  const EditorComponent = reservedKeys$1.get(key) ?? CustomAttributeValueEditor$1;
48681
- return (React__default["default"].createElement(material.Grid, { container: true, item: true, spacing: 3, alignItems: "center", key: key },
48682
- React__default["default"].createElement(material.Grid, { item: true, xs: "auto" },
48918
+ return (React__default["default"].createElement(material.Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
48919
+ React__default["default"].createElement(material.Grid2, null,
48683
48920
  React__default["default"].createElement(material.Paper, { variant: "outlined", className: classes.attributeName },
48684
48921
  React__default["default"].createElement(material.Typography, null, key))),
48685
- React__default["default"].createElement(material.Grid, { item: true, flexGrow: 1 },
48922
+ React__default["default"].createElement(material.Grid2, { flexGrow: 1 },
48686
48923
  React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: makeOnChange(key) })),
48687
- React__default["default"].createElement(material.Grid, { item: true, xs: 1 },
48924
+ React__default["default"].createElement(material.Grid2, null,
48688
48925
  React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => {
48689
48926
  deleteAttribute(key);
48690
48927
  } },
48691
48928
  React__default["default"].createElement(default_1$7, { fontSize: "medium", key: key })))));
48692
48929
  }),
48693
- React__default["default"].createElement(material.Grid, { item: true },
48930
+ React__default["default"].createElement(material.Grid2, null,
48694
48931
  React__default["default"].createElement(material.Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
48695
48932
  setShowAddNewForm(true);
48696
48933
  } }, "Add new")),
48697
- showAddNewForm ? (React__default["default"].createElement(material.Grid, { item: true },
48934
+ showAddNewForm ? (React__default["default"].createElement(material.Grid2, null,
48698
48935
  React__default["default"].createElement(material.Paper, { elevation: 8, className: classes.newAttributePaper },
48699
- React__default["default"].createElement(material.Grid, { container: true, direction: "column" },
48700
- React__default["default"].createElement(material.Grid, { item: true },
48936
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column" },
48937
+ React__default["default"].createElement(material.Grid2, null,
48701
48938
  React__default["default"].createElement(material.FormControl, null,
48702
48939
  React__default["default"].createElement(material.FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
48703
48940
  React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
48704
- React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid, { container: true, spacing: 1, alignItems: "center" },
48705
- React__default["default"].createElement(material.Grid, { item: true },
48941
+ React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid2, { container: true, spacing: 1, alignItems: "center" },
48942
+ React__default["default"].createElement(material.Grid2, null,
48706
48943
  React__default["default"].createElement(material.Typography, null, "Custom")),
48707
- React__default["default"].createElement(material.Grid, { item: true },
48944
+ React__default["default"].createElement(material.Grid2, null,
48708
48945
  React__default["default"].createElement(material.TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys$1.has(newAttributeKey)
48709
48946
  ? ''
48710
48947
  : newAttributeKey, disabled: reservedKeys$1.has(newAttributeKey), onChange: (event) => {
48711
48948
  setNewAttributeKey(event.target.value);
48712
48949
  } }))) }),
48713
48950
  [...reservedKeys$1.keys()].map((key) => (React__default["default"].createElement(material.FormControlLabel, { key: key, value: key, control: React__default["default"].createElement(material.Radio, null), label: key })))))),
48714
- React__default["default"].createElement(material.Grid, { item: true },
48951
+ React__default["default"].createElement(material.Grid2, null,
48715
48952
  React__default["default"].createElement(material.DialogActions, null,
48716
48953
  React__default["default"].createElement(material.Button, { key: "addButton", color: "primary", variant: "contained", style: { margin: 2 }, onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
48717
48954
  React__default["default"].createElement(material.Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
@@ -49099,13 +49336,12 @@
49099
49336
  };
49100
49337
  return (React__default["default"].createElement(Dialog, { open: true, title: "Add reference sequence aliases", handleClose: handleClose, maxWidth: 'sm', "data-testid": "add-refseq-alias", fullWidth: true },
49101
49338
  React__default["default"].createElement(material.DialogContent, { style: { display: 'flex', flexDirection: 'column' } },
49102
- React__default["default"].createElement(material.Grid, { container: true, spacing: 2 },
49103
- React__default["default"].createElement(material.Grid, { item: true, xs: 4 },
49339
+ React__default["default"].createElement(material.Grid2, { container: true, spacing: 2 },
49340
+ React__default["default"].createElement(material.Grid2, null,
49104
49341
  React__default["default"].createElement(material.FormControl, { disabled: enableSubmit && !errorMessage, fullWidth: true },
49105
49342
  React__default["default"].createElement(material.InputLabel, { id: "demo-simple-select-label" }, "Assembly"),
49106
49343
  React__default["default"].createElement(material.Select, { labelId: "demo-simple-select-label", id: "demo-simple-select", label: "Assembly", value: selectedAssembly?.name ?? '', onChange: handleChangeAssembly }, assemblies.map((option) => (React__default["default"].createElement(material.MenuItem, { key: option.name, value: option.name }, option.displayName ?? option.name)))))),
49107
- React__default["default"].createElement(material.Grid, { item: true, xs: 1 }),
49108
- React__default["default"].createElement(material.Grid, { item: true, xs: 7 },
49344
+ React__default["default"].createElement(material.Grid2, null,
49109
49345
  React__default["default"].createElement(material.InputLabel, null, "Load RefName alias"),
49110
49346
  React__default["default"].createElement("input", { type: "file", onChange: handleChangeFileHandler, ref: fileRef, disabled: (enableSubmit && !errorMessage) || !selectedAssembly }))),
49111
49347
  selectedAssembly && refNameAliasMap.size > 0 ? (React__default["default"].createElement("div", { style: { height: 200, width: '100%', marginTop: 20 } },
@@ -49796,16 +50032,7 @@
49796
50032
  function isSimpleFeatureSerialized(args) {
49797
50033
  return 'uniqueId' in args && _typeof(args.data) !== 'object';
49798
50034
  }
49799
- /**
49800
- * Simple implementation of a feature object.
49801
- */
49802
50035
  var SimpleFeature = /*#__PURE__*/function () {
49803
- /**
49804
- * @param args - SimpleFeature args
49805
- *
49806
- * Note: args.data.subfeatures can be an array of these same args,
49807
- * which will be inflated to more instances of this class.
49808
- */
49809
50036
  function SimpleFeature(args) {
49810
50037
  var _this = this;
49811
50038
  _classCallCheck(this, SimpleFeature);
@@ -49813,14 +50040,9 @@
49813
50040
  if (isSimpleFeatureSerialized(args)) {
49814
50041
  this.data = args;
49815
50042
  } else {
49816
- this.data = args.data || {};
49817
- // load handle from args.parent (not args.data.parent)
49818
- // this reason is because if args is an object, it likely isn't properly loaded with
49819
- // parent as a Feature reference (probably a raw parent ID or something instead)
50043
+ this.data = args.data;
49820
50044
  this.parentHandle = args.parent;
49821
50045
  }
49822
- // the feature id comes from
49823
- // args.id, args.data.uniqueId, or args.uniqueId due to this initialization
49824
50046
  var id = isSimpleFeatureSerialized(args) ? args.uniqueId : args.id;
49825
50047
  if (id === undefined || id === null) {
49826
50048
  throw new Error('SimpleFeature requires a unique `id` or `data.uniqueId` attribute');
@@ -49830,9 +50052,7 @@
49830
50052
  throw new Error("invalid feature data, end less than start. end: ".concat(this.data.end, " start: ").concat(this.data.start));
49831
50053
  }
49832
50054
  if (this.data.subfeatures) {
49833
- this.subfeatures = (_a = this.data.subfeatures) === null || _a === void 0 ? void 0 : _a.map(
49834
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49835
- function (f, i) {
50055
+ this.subfeatures = (_a = this.data.subfeatures) === null || _a === void 0 ? void 0 : _a.map(function (f, i) {
49836
50056
  return typeof f.get !== 'function' ? new SimpleFeature({
49837
50057
  id: f.uniqueId || "".concat(id, "-").concat(i),
49838
50058
  data: _objectSpread2({
@@ -49843,52 +50063,31 @@
49843
50063
  });
49844
50064
  }
49845
50065
  }
49846
- /**
49847
- * Get a piece of data about the feature. All features must have
49848
- * 'start' and 'end', but everything else is optional.
49849
- */
49850
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49851
50066
  _createClass(SimpleFeature, [{
49852
50067
  key: "get",
49853
50068
  value: function get(name) {
49854
50069
  return name === 'subfeatures' ? this.subfeatures : name === 'parent' ? this.parent() : this.data[name];
49855
50070
  }
49856
- /**
49857
- * Set an item of data.
49858
- */
49859
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49860
50071
  }, {
49861
50072
  key: "set",
49862
50073
  value: function set(name, val) {
49863
50074
  this.data[name] = val;
49864
50075
  }
49865
- /**
49866
- * Get an array listing which data keys are present in this feature.
49867
- */
49868
50076
  }, {
49869
50077
  key: "tags",
49870
50078
  value: function tags() {
49871
50079
  return Object.keys(this.data);
49872
50080
  }
49873
- /**
49874
- * Get the unique ID of this feature.
49875
- */
49876
50081
  }, {
49877
50082
  key: "id",
49878
50083
  value: function id() {
49879
50084
  return this.uniqueId;
49880
50085
  }
49881
- /**
49882
- * Get this feature's parent feature, or undefined if none.
49883
- */
49884
50086
  }, {
49885
50087
  key: "parent",
49886
50088
  value: function parent() {
49887
50089
  return this.parentHandle;
49888
50090
  }
49889
- /**
49890
- * Get an array of child features, or undefined if none.
49891
- */
49892
50091
  }, {
49893
50092
  key: "children",
49894
50093
  value: function children() {
@@ -49932,11 +50131,11 @@
49932
50131
  const isInWebWorker$1 = typeof sessionStorage === 'undefined';
49933
50132
  class ApolloSequenceAdapter extends BaseAdapter.BaseSequenceAdapter {
49934
50133
  regions;
49935
- async getRefNames(opts) {
49936
- const regions = await this.getRegions(opts);
50134
+ async getRefNames() {
50135
+ const regions = await this.getRegions();
49937
50136
  return regions.map((regions) => regions.refName);
49938
50137
  }
49939
- async getRegions(opts) {
50138
+ async getRegions() {
49940
50139
  if (this.regions) {
49941
50140
  return this.regions;
49942
50141
  }
@@ -49968,7 +50167,7 @@
49968
50167
  removeEventListener('message', messageListener);
49969
50168
  resolve(data.regions);
49970
50169
  };
49971
- addEventListener('message', messageListener, opts);
50170
+ addEventListener('message', messageListener);
49972
50171
  // @ts-expect-error waiting for types to be published
49973
50172
  globalThis.rpcServer.emit('apollo', {
49974
50173
  apollo: true,
@@ -49985,7 +50184,7 @@
49985
50184
  * @param param -
49986
50185
  * @returns Observable of Feature objects in the region
49987
50186
  */
49988
- getFeatures(region, opts) {
50187
+ getFeatures(region) {
49989
50188
  const { end, refName, start } = region;
49990
50189
  const assemblyId = configuration.readConfObject(this.config, 'assemblyId');
49991
50190
  const regionWithAssemblyName = { ...region, assemblyName: assemblyId };
@@ -50022,7 +50221,7 @@
50022
50221
  removeEventListener('message', messageListener);
50023
50222
  resolve(data.sequence);
50024
50223
  };
50025
- addEventListener('message', messageListener, opts);
50224
+ addEventListener('message', messageListener);
50026
50225
  // @ts-expect-error waiting for types to be published
50027
50226
  globalThis.rpcServer.emit('apollo', {
50028
50227
  apollo: true,
@@ -50819,7 +51018,7 @@
50819
51018
  .filter(([id, refName]) => id !== refName)
50820
51019
  .map(([id, refName]) => ({
50821
51020
  _id: id,
50822
- name: refName ?? '',
51021
+ name: refName,
50823
51022
  }));
50824
51023
  const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
50825
51024
  if (!refSeqId) {
@@ -50955,6 +51154,285 @@
50955
51154
  return pluggableElement;
50956
51155
  }
50957
51156
 
51157
+ /* eslint-disable react-hooks/exhaustive-deps */
51158
+ const isGeneOrTranscript = (annotationFeature, apolloSessionModel) => {
51159
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
51160
+ if (!featureTypeOntology) {
51161
+ throw new Error('featureTypeOntology is undefined');
51162
+ }
51163
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'gene') ||
51164
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
51165
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
51166
+ };
51167
+ const isTranscript = (annotationFeature, apolloSessionModel) => {
51168
+ const { featureTypeOntology } = apolloSessionModel.apolloDataStore.ontologyManager;
51169
+ if (!featureTypeOntology) {
51170
+ throw new Error('featureTypeOntology is undefined');
51171
+ }
51172
+ return (featureTypeOntology.isTypeOf(annotationFeature.type, 'mRNA') ||
51173
+ featureTypeOntology.isTypeOf(annotationFeature.type, 'transcript'));
51174
+ };
51175
+ function CreateApolloAnnotation({ annotationFeature, assembly, handleClose, refSeqId, session, }) {
51176
+ const apolloSessionModel = session;
51177
+ const childIds = React.useMemo(() => Object.keys(annotationFeature.children ?? {}), [annotationFeature]);
51178
+ const features = React.useMemo(() => {
51179
+ for (const [, asm] of apolloSessionModel.apolloDataStore.assemblies) {
51180
+ if (asm._id === assembly.name) {
51181
+ for (const [, refSeq] of asm.refSeqs) {
51182
+ if (refSeq._id === refSeqId) {
51183
+ return refSeq.features;
51184
+ }
51185
+ }
51186
+ }
51187
+ }
51188
+ return [];
51189
+ }, []);
51190
+ const [parentFeatureChecked, setParentFeatureChecked] = React.useState(true);
51191
+ const [checkedChildrens, setCheckedChildrens] = React.useState(childIds);
51192
+ const [errorMessage, setErrorMessage] = React.useState('');
51193
+ const [destinationFeatures, setDestinationFeatures] = React.useState([]);
51194
+ const [selectedDestinationFeature, setSelectedDestinationFeature] = React.useState();
51195
+ const getFeatures = (min, max) => {
51196
+ const filteredFeatures = [];
51197
+ for (const [, f] of features) {
51198
+ const featureSnapshot = require$$1$3.getSnapshot(f);
51199
+ if (min >= featureSnapshot.min && max <= featureSnapshot.max) {
51200
+ filteredFeatures.push(featureSnapshot);
51201
+ }
51202
+ }
51203
+ return filteredFeatures;
51204
+ };
51205
+ React.useEffect(() => {
51206
+ setErrorMessage('');
51207
+ if (checkedChildrens.length === 0) {
51208
+ setParentFeatureChecked(false);
51209
+ return;
51210
+ }
51211
+ if (annotationFeature.children) {
51212
+ const checkedAnnotationFeatureChildren = Object.values(annotationFeature.children)
51213
+ .filter((child) => isTranscript(child, apolloSessionModel))
51214
+ .filter((child) => checkedChildrens.includes(child._id));
51215
+ const mins = checkedAnnotationFeatureChildren.map((f) => f.min);
51216
+ const maxes = checkedAnnotationFeatureChildren.map((f) => f.max);
51217
+ const min = Math.min(...mins);
51218
+ const max = Math.max(...maxes);
51219
+ const filteredFeatures = getFeatures(min, max);
51220
+ setDestinationFeatures(filteredFeatures);
51221
+ if (filteredFeatures.length === 0 &&
51222
+ checkedChildrens.length > 0 &&
51223
+ !parentFeatureChecked) {
51224
+ setErrorMessage('No destination features found');
51225
+ }
51226
+ }
51227
+ }, [checkedChildrens]);
51228
+ const handleParentFeatureCheck = (event) => {
51229
+ const isChecked = event.target.checked;
51230
+ setParentFeatureChecked(isChecked);
51231
+ setCheckedChildrens(isChecked ? childIds : []);
51232
+ };
51233
+ const handleChildFeatureCheck = (event, child) => {
51234
+ setCheckedChildrens((prevChecked) => event.target.checked
51235
+ ? [...prevChecked, child._id]
51236
+ : prevChecked.filter((childId) => childId !== child._id));
51237
+ };
51238
+ const handleDestinationFeatureChange = (e) => {
51239
+ const selectedFeature = destinationFeatures.find((f) => f._id === e.target.value);
51240
+ setSelectedDestinationFeature(selectedFeature);
51241
+ };
51242
+ const handleCreateApolloAnnotation = async () => {
51243
+ if (parentFeatureChecked) {
51244
+ const change = new dist$2.AddFeatureChange({
51245
+ changedIds: [annotationFeature._id],
51246
+ typeName: 'AddFeatureChange',
51247
+ assembly: assembly.name,
51248
+ addedFeature: annotationFeature,
51249
+ });
51250
+ await apolloSessionModel.apolloDataStore.changeManager.submit(change);
51251
+ session.notify('Annotation added successfully', 'success');
51252
+ handleClose();
51253
+ }
51254
+ else {
51255
+ if (!annotationFeature.children) {
51256
+ return;
51257
+ }
51258
+ if (!selectedDestinationFeature) {
51259
+ return;
51260
+ }
51261
+ for (const childId of checkedChildrens) {
51262
+ const child = annotationFeature.children[childId];
51263
+ const change = new dist$2.AddFeatureChange({
51264
+ parentFeatureId: selectedDestinationFeature._id,
51265
+ changedIds: [selectedDestinationFeature._id],
51266
+ typeName: 'AddFeatureChange',
51267
+ assembly: assembly.name,
51268
+ addedFeature: child,
51269
+ });
51270
+ await apolloSessionModel.apolloDataStore.changeManager.submit(change);
51271
+ session.notify('Annotation added successfully', 'success');
51272
+ handleClose();
51273
+ }
51274
+ }
51275
+ };
51276
+ return (React__default["default"].createElement(Dialog, { open: true, title: "Create Apollo Annotation", handleClose: handleClose, fullWidth: true, maxWidth: "sm" },
51277
+ React__default["default"].createElement(material.DialogTitle, { fontSize: 15 }, "Select the feature to be copied to apollo track"),
51278
+ React__default["default"].createElement(material.DialogContent, null,
51279
+ React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
51280
+ isGeneOrTranscript(annotationFeature, apolloSessionModel) && (React__default["default"].createElement(material.FormControlLabel, { control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: parentFeatureChecked, onChange: handleParentFeatureCheck }), label: `${annotationFeature.type}:${annotationFeature.min}..${annotationFeature.max}` })),
51281
+ annotationFeature.children && (React__default["default"].createElement(material.Box, { sx: { display: 'flex', flexDirection: 'column', ml: 3 } }, Object.values(annotationFeature.children)
51282
+ .filter((child) => isTranscript(child, apolloSessionModel))
51283
+ .map((child) => (React__default["default"].createElement(material.FormControlLabel, { key: child._id, control: React__default["default"].createElement(material.Checkbox, { size: "small", checked: checkedChildrens.includes(child._id), onChange: (e) => {
51284
+ handleChildFeatureCheck(e, child);
51285
+ } }), label: `${child.type}:${child.min}..${child.max}` })))))),
51286
+ !parentFeatureChecked &&
51287
+ checkedChildrens.length > 0 &&
51288
+ destinationFeatures.length > 0 && (React__default["default"].createElement(material.Box, { sx: { ml: 3 } },
51289
+ React__default["default"].createElement(material.Typography, { variant: "caption", fontSize: 12 }, "Select the destination feature to copy the selected features"),
51290
+ React__default["default"].createElement(material.Box, { sx: { mt: 1 } },
51291
+ React__default["default"].createElement(material.Select, { labelId: "label", style: { width: '100%' }, value: selectedDestinationFeature?._id ?? '', onChange: handleDestinationFeatureChange }, destinationFeatures.map((f) => (React__default["default"].createElement(material.MenuItem, { key: f._id, value: f._id }, `${f.type}:${f.min}..${f.max}`)))))))),
51292
+ React__default["default"].createElement(material.DialogActions, null,
51293
+ React__default["default"].createElement(material.Button, { variant: "contained", type: "submit", disabled: checkedChildrens.length === 0 ||
51294
+ (!parentFeatureChecked &&
51295
+ checkedChildrens.length > 0 &&
51296
+ !selectedDestinationFeature), onClick: handleCreateApolloAnnotation }, "Create"),
51297
+ React__default["default"].createElement(material.Button, { variant: "outlined", type: "submit", onClick: handleClose }, "Cancel")),
51298
+ errorMessage ? (React__default["default"].createElement(material.DialogContent, null,
51299
+ React__default["default"].createElement(material.DialogContentText, { color: "error" }, errorMessage))) : null));
51300
+ }
51301
+
51302
+ function simpleFeatureToGFF3Feature(feature, refSeqId) {
51303
+ const xfeature = JSON.parse(JSON.stringify(feature));
51304
+ const children = xfeature.subfeatures;
51305
+ const gff3Feature = [
51306
+ {
51307
+ start: xfeature.start + 1,
51308
+ end: xfeature.end,
51309
+ seq_id: refSeqId,
51310
+ source: xfeature.source ?? null,
51311
+ type: xfeature.type ?? null,
51312
+ score: xfeature.score ?? null,
51313
+ strand: xfeature.strand ? (xfeature.strand === 1 ? '+' : '-') : null,
51314
+ phase: xfeature.phase !== null || xfeature.phase !== undefined
51315
+ ? xfeature.phase
51316
+ : null,
51317
+ attributes: convertFeatureAttributes(xfeature),
51318
+ derived_features: [],
51319
+ child_features: children
51320
+ ? children.map((x) => simpleFeatureToGFF3Feature(x, refSeqId))
51321
+ : [],
51322
+ },
51323
+ ];
51324
+ return gff3Feature;
51325
+ }
51326
+ function jbrowseFeatureToAnnotationFeature(feature, refSeqId) {
51327
+ return dist$2.gff3ToAnnotationFeature(simpleFeatureToGFF3Feature(feature, refSeqId));
51328
+ }
51329
+ function convertFeatureAttributes(feature) {
51330
+ const attributes = {};
51331
+ const defaultFields = new Set([
51332
+ 'start',
51333
+ 'end',
51334
+ 'type',
51335
+ 'strand',
51336
+ 'refName',
51337
+ 'subfeatures',
51338
+ 'derived_features',
51339
+ 'phase',
51340
+ 'source',
51341
+ 'score',
51342
+ ]);
51343
+ for (const [key, value] of Object.entries(feature)) {
51344
+ if (defaultFields.has(key)) {
51345
+ continue;
51346
+ }
51347
+ attributes[key] = Array.isArray(value) ? value.map(String) : [String(value)];
51348
+ }
51349
+ return attributes;
51350
+ }
51351
+ function annotationFromJBrowseFeature(pluggableElement) {
51352
+ if (pluggableElement.name !== 'LinearBasicDisplay') {
51353
+ return pluggableElement;
51354
+ }
51355
+ const { stateModel } = pluggableElement;
51356
+ const newStateModel = stateModel
51357
+ .views((self) => ({
51358
+ getFirstRegion() {
51359
+ const lgv = require$$1$2.getContainingView(self);
51360
+ return lgv.dynamicBlocks.contentBlocks[0];
51361
+ },
51362
+ getAssembly() {
51363
+ const firstRegion = self.getFirstRegion();
51364
+ const session = require$$1$2.getSession(self);
51365
+ const { assemblyManager } = session;
51366
+ const { assemblyName } = firstRegion;
51367
+ const assembly = assemblyManager.get(assemblyName);
51368
+ if (!assembly) {
51369
+ throw new Error(`Could not find assembly named ${assemblyName}`);
51370
+ }
51371
+ return assembly;
51372
+ },
51373
+ getRefSeqId(assembly) {
51374
+ const firstRegion = self.getFirstRegion();
51375
+ const { refName } = firstRegion;
51376
+ const { refNameAliases } = assembly;
51377
+ if (!refNameAliases) {
51378
+ throw new Error(`Could not find aliases for ${assembly.name}`);
51379
+ }
51380
+ const newRefNames = [...Object.entries(refNameAliases)]
51381
+ .filter(([id, refName]) => id !== refName)
51382
+ .map(([id, refName]) => ({
51383
+ _id: id,
51384
+ name: refName,
51385
+ }));
51386
+ const refSeqId = newRefNames.find((item) => item.name === refName)?._id;
51387
+ if (!refSeqId) {
51388
+ throw new Error(`Could not find refSeqId named ${refName}`);
51389
+ }
51390
+ return refSeqId;
51391
+ },
51392
+ getAnnotationFeature(assembly) {
51393
+ const refSeqId = self.getRefSeqId(assembly);
51394
+ const sfeature = self.contextMenuFeature.data;
51395
+ return jbrowseFeatureToAnnotationFeature(sfeature, refSeqId);
51396
+ },
51397
+ }))
51398
+ .views((self) => {
51399
+ const superContextMenuItems = self.contextMenuItems;
51400
+ const session = require$$1$2.getSession(self);
51401
+ const assembly = self.getAssembly();
51402
+ return {
51403
+ contextMenuItems() {
51404
+ const feature = self.contextMenuFeature;
51405
+ if (!feature) {
51406
+ return superContextMenuItems();
51407
+ }
51408
+ return [
51409
+ ...superContextMenuItems(),
51410
+ {
51411
+ label: 'Create Apollo annotation',
51412
+ icon: default_1$a,
51413
+ onClick: () => {
51414
+ session.queueDialog((doneCallback) => [
51415
+ CreateApolloAnnotation,
51416
+ {
51417
+ session,
51418
+ handleClose: () => {
51419
+ doneCallback();
51420
+ },
51421
+ annotationFeature: self.getAnnotationFeature(assembly),
51422
+ assembly,
51423
+ refSeqId: self.getRefSeqId(assembly),
51424
+ },
51425
+ ]);
51426
+ },
51427
+ },
51428
+ ];
51429
+ },
51430
+ };
51431
+ });
51432
+ pluggableElement.stateModel = newStateModel;
51433
+ return pluggableElement;
51434
+ }
51435
+
50958
51436
  /* eslint-disable @typescript-eslint/unbound-method */
50959
51437
  const StringTextField = mobxReact.observer(function StringTextField({ onChangeCommitted, value: initialValue, ...props }) {
50960
51438
  const [value, setValue] = React.useState(String(initialValue));
@@ -51161,44 +51639,44 @@
51161
51639
  }
51162
51640
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
51163
51641
  React__default["default"].createElement(material.Typography, { variant: "h5" }, "Attributes"),
51164
- React__default["default"].createElement(material.Grid, { container: true, direction: "column", spacing: 1 },
51642
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column", spacing: 1 },
51165
51643
  Object.entries(attributes).map(([key, value]) => {
51166
51644
  if (key === '') {
51167
51645
  return null;
51168
51646
  }
51169
51647
  const EditorComponent = reservedKeys.get(key) ?? CustomAttributeValueEditor;
51170
- return (React__default["default"].createElement(material.Grid, { container: true, item: true, spacing: 3, alignItems: "center", key: key },
51171
- React__default["default"].createElement(material.Grid, { item: true, xs: "auto" },
51648
+ return (React__default["default"].createElement(material.Grid2, { container: true, spacing: 3, alignItems: "center", key: key },
51649
+ React__default["default"].createElement(material.Grid2, null,
51172
51650
  React__default["default"].createElement(material.Paper, { variant: "outlined", className: classes.attributeName },
51173
51651
  React__default["default"].createElement(material.Typography, null, key))),
51174
- React__default["default"].createElement(material.Grid, { item: true, flexGrow: 1 },
51652
+ React__default["default"].createElement(material.Grid2, { flexGrow: 1 },
51175
51653
  React__default["default"].createElement(EditorComponent, { session: session, value: value, onChange: (newValue) => onChangeCommitted(key, newValue) })),
51176
- React__default["default"].createElement(material.Grid, { item: true, xs: 1 },
51654
+ React__default["default"].createElement(material.Grid2, null,
51177
51655
  React__default["default"].createElement(material.IconButton, { "aria-label": "delete", size: "medium", disabled: !editable, onClick: () => onChangeCommitted(key) },
51178
51656
  React__default["default"].createElement(default_1$7, { fontSize: "medium", key: key })))));
51179
51657
  }),
51180
- React__default["default"].createElement(material.Grid, { item: true },
51658
+ React__default["default"].createElement(material.Grid2, null,
51181
51659
  React__default["default"].createElement(material.Button, { color: "primary", variant: "contained", disabled: showAddNewForm || !editable, onClick: () => {
51182
51660
  setShowAddNewForm(true);
51183
51661
  } }, "Add new")),
51184
- showAddNewForm ? (React__default["default"].createElement(material.Grid, { item: true },
51662
+ showAddNewForm ? (React__default["default"].createElement(material.Grid2, null,
51185
51663
  React__default["default"].createElement(material.Paper, { elevation: 8, className: classes.newAttributePaper },
51186
- React__default["default"].createElement(material.Grid, { container: true, direction: "column" },
51187
- React__default["default"].createElement(material.Grid, { item: true },
51664
+ React__default["default"].createElement(material.Grid2, { container: true, direction: "column" },
51665
+ React__default["default"].createElement(material.Grid2, null,
51188
51666
  React__default["default"].createElement(material.FormControl, null,
51189
51667
  React__default["default"].createElement(material.FormLabel, { id: "attribute-radio-button-group" }, "Select attribute type"),
51190
51668
  React__default["default"].createElement(material.RadioGroup, { "aria-labelledby": "demo-radio-buttons-group-label", defaultValue: "custom", name: "radio-buttons-group", onChange: handleRadioButtonChange },
51191
- React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid, { container: true, spacing: 1, alignItems: "center" },
51192
- React__default["default"].createElement(material.Grid, { item: true },
51669
+ React__default["default"].createElement(material.FormControlLabel, { value: "custom", control: React__default["default"].createElement(material.Radio, null), disableTypography: true, label: React__default["default"].createElement(material.Grid2, { container: true, spacing: 1, alignItems: "center" },
51670
+ React__default["default"].createElement(material.Grid2, null,
51193
51671
  React__default["default"].createElement(material.Typography, null, "Custom")),
51194
- React__default["default"].createElement(material.Grid, { item: true },
51672
+ React__default["default"].createElement(material.Grid2, null,
51195
51673
  React__default["default"].createElement(material.TextField, { label: "Custom attribute key", variant: "outlined", value: reservedKeys.has(newAttributeKey)
51196
51674
  ? ''
51197
51675
  : newAttributeKey, disabled: reservedKeys.has(newAttributeKey), onChange: (event) => {
51198
51676
  setNewAttributeKey(event.target.value);
51199
51677
  } }))) }),
51200
51678
  [...reservedKeys.keys()].map((key) => (React__default["default"].createElement(material.FormControlLabel, { key: key, value: key, control: React__default["default"].createElement(material.Radio, null), label: key })))))),
51201
- React__default["default"].createElement(material.Grid, { item: true },
51679
+ React__default["default"].createElement(material.Grid2, null,
51202
51680
  React__default["default"].createElement(material.DialogActions, null,
51203
51681
  React__default["default"].createElement(material.Button, { key: "addButton", color: "primary", variant: "contained", onClick: handleAddNewAttributeChange, disabled: !newAttributeKey }, "Add"),
51204
51682
  React__default["default"].createElement(material.Button, { key: "cancelAddButton", variant: "outlined", type: "submit", onClick: () => {
@@ -51380,6 +51858,47 @@
51380
51858
  React__default["default"].createElement("div", null, showSequence && (React__default["default"].createElement("textarea", { readOnly: true, rows: 20, className: classes.sequence, value: sequence })))));
51381
51859
  });
51382
51860
 
51861
+ const FeatureDetailsNavigation = mobxReact.observer(function FeatureDetailsNavigation(props) {
51862
+ const { feature, model } = props;
51863
+ const { children, parent } = feature;
51864
+ const childFeatures = [];
51865
+ if (children) {
51866
+ for (const [, child] of children) {
51867
+ childFeatures.push(child);
51868
+ }
51869
+ }
51870
+ if (!(parent ?? childFeatures.length > 0)) {
51871
+ return null;
51872
+ }
51873
+ return (React__default["default"].createElement("div", null,
51874
+ React__default["default"].createElement(material.Typography, { variant: "h5" }, "Go to related feature"),
51875
+ parent && (React__default["default"].createElement("div", null,
51876
+ React__default["default"].createElement(material.Typography, { variant: "h6" }, "Parent:"),
51877
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: () => {
51878
+ model.setFeature(parent);
51879
+ } },
51880
+ parent.type,
51881
+ " (",
51882
+ parent.min,
51883
+ "..",
51884
+ parent.max,
51885
+ ")"))),
51886
+ childFeatures.length > 0 && (React__default["default"].createElement("div", null,
51887
+ React__default["default"].createElement(material.Typography, { variant: "h6" },
51888
+ childFeatures.length === 1 ? 'Child' : 'Children',
51889
+ ":"),
51890
+ childFeatures.map((child) => (React__default["default"].createElement("div", { key: child._id, style: { marginBottom: 5 } },
51891
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: () => {
51892
+ model.setFeature(child);
51893
+ } },
51894
+ child.type,
51895
+ " (",
51896
+ child.min,
51897
+ "..",
51898
+ child.max,
51899
+ ")"))))))));
51900
+ });
51901
+
51383
51902
  const useStyles$8 = mui.makeStyles()((theme) => ({
51384
51903
  root: {
51385
51904
  padding: theme.spacing(2),
@@ -51410,7 +51929,9 @@
51410
51929
  React__default["default"].createElement("hr", null),
51411
51930
  React__default["default"].createElement(Attributes, { feature: feature, session: session, assembly: currentAssembly._id, editable: true }),
51412
51931
  React__default["default"].createElement("hr", null),
51413
- React__default["default"].createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName })));
51932
+ React__default["default"].createElement(Sequence, { feature: feature, session: session, assembly: currentAssembly._id, refName: refName }),
51933
+ React__default["default"].createElement("hr", null),
51934
+ React__default["default"].createElement(FeatureDetailsNavigation, { model: model, feature: feature })));
51414
51935
  });
51415
51936
 
51416
51937
  var dist = {};
@@ -51551,18 +52072,27 @@
51551
52072
  return false;
51552
52073
  },
51553
52074
  get transcriptParts() {
51554
- if (self.type !== 'mRNA') {
51555
- throw new Error('Only features of type "mRNA" or equivalent can calculate CDS locations');
52075
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
52076
+ var session = (0, util_1.getSession)(self);
52077
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
52078
+ var apolloDataStore = session.apolloDataStore;
52079
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
52080
+ var featureTypeOntology = apolloDataStore.ontologyManager.featureTypeOntology;
52081
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
52082
+ if (!featureTypeOntology.isTypeOf(self.type, 'transcript')) {
52083
+ throw new Error('Only features of type "transcript" or equivalent can calculate CDS locations');
51556
52084
  }
51557
52085
  var children = self.children;
51558
52086
  if (!children) {
51559
- throw new Error('no CDS or exons in mRNA');
52087
+ throw new Error('no CDS or exons in transcript');
51560
52088
  }
51561
- var cdsChildren = _toConsumableArray(children.values()).filter(function (child) {
51562
- return child.type === 'CDS';
52089
+ var cdsChildren = _toConsumableArray(children.values()).filter(
52090
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
52091
+ function (child) {
52092
+ return featureTypeOntology.isTypeOf(child.type, 'CDS');
51563
52093
  });
51564
52094
  if (cdsChildren.length === 0) {
51565
- throw new Error('no CDS in mRNA');
52095
+ throw new Error('no CDS in transcript');
51566
52096
  }
51567
52097
  var transcriptParts = [];
51568
52098
  var _iterator4 = _createForOfIteratorHelper(cdsChildren),
@@ -51581,7 +52111,8 @@
51581
52111
  for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
51582
52112
  var _step5$value = _slicedToArray(_step5.value, 2),
51583
52113
  _child = _step5$value[1];
51584
- if (_child.type === 'exon') {
52114
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
52115
+ if (featureTypeOntology.isTypeOf(_child.type, 'exon')) {
51585
52116
  exonLocations.push({
51586
52117
  min: _child.min,
51587
52118
  max: _child.max
@@ -52162,7 +52693,14 @@
52162
52693
  if (!refData) {
52163
52694
  return null;
52164
52695
  }
52165
- const { strand, transcriptParts } = feature;
52696
+ let strand, transcriptParts;
52697
+ try {
52698
+ ;
52699
+ ({ strand, transcriptParts } = feature);
52700
+ }
52701
+ catch {
52702
+ return null;
52703
+ }
52166
52704
  const [firstLocation] = transcriptParts;
52167
52705
  const locationData = firstLocation
52168
52706
  .map((loc, idx) => {
@@ -52303,6 +52841,28 @@
52303
52841
  segments.push({ type: 'CDS', sequenceLines, locs });
52304
52842
  return segments;
52305
52843
  }
52844
+ case 'protein': {
52845
+ let wholeSequence = '';
52846
+ const [firstLocation] = cdsLocations;
52847
+ const locs = [];
52848
+ for (const loc of firstLocation) {
52849
+ let sequence = getSequence(loc.min, loc.max);
52850
+ if (strand === -1) {
52851
+ sequence = require$$1$2.revcom(sequence);
52852
+ }
52853
+ wholeSequence += sequence;
52854
+ locs.push({ min: loc.min, max: loc.max });
52855
+ }
52856
+ let protein = '';
52857
+ for (let i = 0; i < wholeSequence.length; i += 3) {
52858
+ const codonSeq = wholeSequence.slice(i, i + 3).toUpperCase();
52859
+ protein +=
52860
+ require$$1$2.defaultCodonTable[codonSeq] || '&';
52861
+ }
52862
+ const sequenceLines = dist$2.splitStringIntoChunks(protein, SEQUENCE_WRAP_LENGTH);
52863
+ segments.push({ type: 'protein', sequenceLines, locs });
52864
+ return segments;
52865
+ }
52306
52866
  }
52307
52867
  }
52308
52868
  function getSegmentColor(type) {
@@ -52391,7 +52951,8 @@
52391
52951
  React__default["default"].createElement(material.Select, { defaultValue: "CDS", value: selectedOption, onChange: handleChangeSeqOption },
52392
52952
  React__default["default"].createElement(material.MenuItem, { value: "CDS" }, "CDS"),
52393
52953
  React__default["default"].createElement(material.MenuItem, { value: "cDNA" }, "cDNA"),
52394
- React__default["default"].createElement(material.MenuItem, { value: "genomic" }, "Genomic")),
52954
+ React__default["default"].createElement(material.MenuItem, { value: "genomic" }, "Genomic"),
52955
+ React__default["default"].createElement(material.MenuItem, { value: "protein" }, "Protein")),
52395
52956
  React__default["default"].createElement(material.Paper, { style: {
52396
52957
  fontFamily: 'monospace',
52397
52958
  padding: theme.spacing(),
@@ -52629,7 +53190,12 @@
52629
53190
  ]);
52630
53191
  },
52631
53192
  });
52632
- if (feature.type === 'mRNA' && require$$1$2.isSessionModelWithWidgets(session)) {
53193
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
53194
+ if (!featureTypeOntology) {
53195
+ throw new Error('featureTypeOntology is undefined');
53196
+ }
53197
+ if (featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
53198
+ require$$1$2.isSessionModelWithWidgets(session)) {
52633
53199
  menuItems.push({
52634
53200
  label: 'Edit transcript details',
52635
53201
  onClick: () => {
@@ -52701,6 +53267,381 @@
52701
53267
  } })));
52702
53268
  });
52703
53269
 
53270
+ /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
53271
+ const useStyles$4 = mui.makeStyles()((theme) => ({
53272
+ typeContent: {
53273
+ display: 'inline-block',
53274
+ width: '174px',
53275
+ height: '100%',
53276
+ cursor: 'text',
53277
+ },
53278
+ feature: {
53279
+ td: {
53280
+ position: 'relative',
53281
+ verticalAlign: 'top',
53282
+ paddingLeft: '0.5em',
53283
+ },
53284
+ },
53285
+ arrow: {
53286
+ display: 'inline-block',
53287
+ width: '1.6em',
53288
+ textAlign: 'center',
53289
+ cursor: 'pointer',
53290
+ },
53291
+ arrowExpanded: {
53292
+ transform: 'rotate(90deg)',
53293
+ },
53294
+ hoveredFeature: {
53295
+ backgroundColor: theme.palette.action.hover,
53296
+ },
53297
+ typeInputElement: {
53298
+ border: 'none',
53299
+ background: 'none',
53300
+ },
53301
+ typeErrorMessage: {
53302
+ color: 'red',
53303
+ },
53304
+ }));
53305
+ function makeContextMenuItems(display, feature) {
53306
+ const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
53307
+ return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
53308
+ }
53309
+ function getTopLevelFeature(feature) {
53310
+ let cur = feature;
53311
+ while (cur.parent) {
53312
+ cur = cur.parent;
53313
+ }
53314
+ return cur;
53315
+ }
53316
+ const Feature = mobxReact.observer(function Feature({ depth, feature, isHovered, isSelected, model: displayState, selectedFeatureClass, setContextMenu, }) {
53317
+ const { classes } = useStyles$4();
53318
+ const { apolloHover, changeManager, selectedFeature, session, tabularEditor: tabularEditorState, } = displayState;
53319
+ const { featureCollapsed, filterText } = tabularEditorState;
53320
+ const { _id, children, max, min, strand, type } = feature;
53321
+ const expanded = !featureCollapsed.get(_id);
53322
+ const toggleExpanded = (e) => {
53323
+ e.stopPropagation();
53324
+ tabularEditorState.setFeatureCollapsed(_id, expanded);
53325
+ };
53326
+ // pop up a snackbar in the session notifying user of an error
53327
+ const notifyError = (e) => {
53328
+ session.notify(e.message, 'error');
53329
+ };
53330
+ return (React__default["default"].createElement(React__default["default"].Fragment, null,
53331
+ React__default["default"].createElement("tr", { onMouseEnter: (_e) => {
53332
+ displayState.setApolloHover({
53333
+ feature,
53334
+ topLevelFeature: getTopLevelFeature(feature),
53335
+ glyph: displayState.getGlyph(getTopLevelFeature(feature)),
53336
+ });
53337
+ }, className: classes.feature +
53338
+ (isSelected
53339
+ ? ` ${selectedFeatureClass}`
53340
+ : isHovered
53341
+ ? ` ${classes.hoveredFeature}`
53342
+ : ''), onClick: (e) => {
53343
+ e.stopPropagation();
53344
+ displayState.setSelectedFeature(feature);
53345
+ }, onContextMenu: (e) => {
53346
+ e.preventDefault();
53347
+ setContextMenu({
53348
+ position: { left: e.clientX + 2, top: e.clientY - 6 },
53349
+ items: makeContextMenuItems(displayState, feature),
53350
+ });
53351
+ return false;
53352
+ } },
53353
+ React__default["default"].createElement("td", { style: {
53354
+ whiteSpace: 'nowrap',
53355
+ borderLeft: `${depth * 2}em solid transparent`,
53356
+ } },
53357
+ children?.size ? (
53358
+ // TODO: a11y
53359
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
53360
+ React__default["default"].createElement("div", { onClick: toggleExpanded, className: classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '') }, "\u276F")) : null,
53361
+ React__default["default"].createElement("div", { className: classes.typeContent },
53362
+ React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: 170 }, value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTypeTerms.bind(null, feature), renderInput: (params) => {
53363
+ return (React__default["default"].createElement("div", { ref: params.InputProps.ref },
53364
+ React__default["default"].createElement("input", { type: "text", ...params.inputProps, className: classes.typeInputElement, style: { width: 170 } }),
53365
+ params.error ? (React__default["default"].createElement("div", { className: classes.typeErrorMessage }, params.errorMessage ?? 'unknown error')) : null));
53366
+ }, onChange: (oldValue, newValue) => {
53367
+ if (newValue) {
53368
+ handleFeatureTypeChange(changeManager, feature, oldValue, newValue).catch(notifyError);
53369
+ }
53370
+ } }))),
53371
+ React__default["default"].createElement("td", null,
53372
+ React__default["default"].createElement(NumberCell, { initialValue: min + 1, notifyError: notifyError, onChangeCommitted: (newStart) => handleFeatureStartChange(changeManager, feature, min, newStart - 1) })),
53373
+ React__default["default"].createElement("td", null,
53374
+ React__default["default"].createElement(NumberCell, { initialValue: max, notifyError: notifyError, onChangeCommitted: (newEnd) => handleFeatureEndChange(changeManager, feature, max, newEnd) })),
53375
+ React__default["default"].createElement("td", null, strand === 1 ? '+' : strand === -1 ? '-' : undefined),
53376
+ React__default["default"].createElement("td", null,
53377
+ React__default["default"].createElement(FeatureAttributes, { filterText: filterText, feature: feature }))),
53378
+ expanded && children
53379
+ ? [...children.entries()]
53380
+ .filter((entry) => {
53381
+ if (!filterText) {
53382
+ return true;
53383
+ }
53384
+ const [, childFeature] = entry;
53385
+ // search feature and its subfeatures for the text
53386
+ const text = JSON.stringify(childFeature);
53387
+ return text.includes(filterText);
53388
+ })
53389
+ .map(([featureId, childFeature]) => {
53390
+ const childHovered = apolloHover?.feature._id === childFeature._id;
53391
+ const childSelected = selectedFeature?._id === childFeature._id;
53392
+ return (React__default["default"].createElement(Feature, { isHovered: childHovered, isSelected: childSelected, selectedFeatureClass: selectedFeatureClass, key: featureId, depth: (depth || 0) + 1, feature: childFeature, model: displayState, setContextMenu: setContextMenu }));
53393
+ })
53394
+ : null));
53395
+ });
53396
+ async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
53397
+ const { parent: parentFeature } = feature;
53398
+ if (parentFeature) {
53399
+ // if this is a child of an existing feature, restrict the autocomplete choices to valid
53400
+ // parts of that feature
53401
+ const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(parentFeature.type, { includeSubclasses: false });
53402
+ // eslint-disable-next-line unicorn/no-array-callback-reference
53403
+ const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass);
53404
+ if (parentTypeClassTerms.length > 0) {
53405
+ const subpartTerms = await ontologyStore.getClassesThat('part_of', parentTypeClassTerms);
53406
+ return subpartTerms;
53407
+ }
53408
+ }
53409
+ return;
53410
+ }
53411
+
53412
+ const useStyles$3 = mui.makeStyles()((theme) => ({
53413
+ scrollableTable: {
53414
+ width: '100%',
53415
+ height: '100%',
53416
+ th: {
53417
+ position: 'sticky',
53418
+ top: 0,
53419
+ zIndex: 2,
53420
+ textAlign: 'left',
53421
+ background: theme.palette.background.paper,
53422
+ paddingTop: '3.2em',
53423
+ },
53424
+ td: { whiteSpace: 'normal' },
53425
+ },
53426
+ selectedFeature: {
53427
+ backgroundColor: theme.palette.action.selected,
53428
+ },
53429
+ }));
53430
+ const HybridGrid = mobxReact.observer(function HybridGrid({ model, }) {
53431
+ const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model;
53432
+ const theme = material.useTheme();
53433
+ const { classes } = useStyles$3();
53434
+ const scrollContainerRef = React.useRef(null);
53435
+ const [contextMenu, setContextMenu] = React.useState(null);
53436
+ const { filterText } = tabularEditor;
53437
+ // scrolls to selected feature if one is selected and it's not already visible
53438
+ React.useEffect(() => {
53439
+ const scrollContainer = scrollContainerRef.current;
53440
+ if (scrollContainer && selectedFeature) {
53441
+ const selectedRow = scrollContainer.querySelector(`.${classes.selectedFeature}`);
53442
+ if (selectedRow) {
53443
+ const currScroll = scrollContainer.scrollTop;
53444
+ const newScrollTop = selectedRow.offsetTop - 25;
53445
+ const isVisible = newScrollTop > currScroll &&
53446
+ newScrollTop < currScroll + scrollContainer.offsetHeight;
53447
+ if (!isVisible) {
53448
+ scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' });
53449
+ }
53450
+ }
53451
+ }
53452
+ }, [selectedFeature, seenFeatures, classes.selectedFeature]);
53453
+ return (React__default["default"].createElement("div", { ref: scrollContainerRef, style: { width: '100%', overflowY: 'auto', height: '100%' } },
53454
+ React__default["default"].createElement("table", { className: classes.scrollableTable },
53455
+ React__default["default"].createElement("thead", null,
53456
+ React__default["default"].createElement("tr", null,
53457
+ React__default["default"].createElement("th", null, "Type"),
53458
+ React__default["default"].createElement("th", null, "Start"),
53459
+ React__default["default"].createElement("th", null, "End"),
53460
+ React__default["default"].createElement("th", null, "Strand"),
53461
+ React__default["default"].createElement("th", null, "Attributes"))),
53462
+ React__default["default"].createElement("tbody", null, [...seenFeatures.entries()]
53463
+ .filter((entry) => {
53464
+ if (!filterText) {
53465
+ return true;
53466
+ }
53467
+ const [, feature] = entry;
53468
+ // search feature and its subfeatures for the text
53469
+ const text = JSON.stringify(feature);
53470
+ return text.includes(filterText);
53471
+ })
53472
+ .sort((a, b) => {
53473
+ return a[1].min - b[1].min;
53474
+ })
53475
+ .map(([featureId, feature]) => {
53476
+ const isSelected = selectedFeature?._id === featureId;
53477
+ const isHovered = apolloHover?.feature._id === featureId;
53478
+ return (React__default["default"].createElement(Feature, { key: featureId, isSelected: isSelected, isHovered: isHovered, selectedFeatureClass: classes.selectedFeature, feature: feature, model: model, depth: 0, setContextMenu: setContextMenu }));
53479
+ }))),
53480
+ React__default["default"].createElement(ui.Menu, { open: Boolean(contextMenu), onMenuItemClick: (_, callback) => {
53481
+ callback();
53482
+ setContextMenu(null);
53483
+ }, onClose: () => {
53484
+ setContextMenu(null);
53485
+ }, TransitionProps: {
53486
+ onExit: () => {
53487
+ setContextMenu(null);
53488
+ },
53489
+ }, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenu?.items ?? [], anchorReference: "anchorPosition", anchorPosition: contextMenu?.position })));
53490
+ });
53491
+
53492
+ var Clear = {};
53493
+
53494
+ var _interopRequireDefault$5 = interopRequireDefault.exports;
53495
+ Object.defineProperty(Clear, "__esModule", {
53496
+ value: true
53497
+ });
53498
+ var default_1$5 = Clear["default"] = void 0;
53499
+ var _createSvgIcon$5 = /*#__PURE__*/_interopRequireDefault$5(createSvgIcon);
53500
+ var _jsxRuntime$5 = require$$2__default["default"];
53501
+ var _default$5 = /*#__PURE__*/(0, _createSvgIcon$5["default"])( /*#__PURE__*/(0, _jsxRuntime$5.jsx)("path", {
53502
+ d: "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
53503
+ }), 'Clear');
53504
+ default_1$5 = Clear["default"] = _default$5;
53505
+
53506
+ var UnfoldLess = {};
53507
+
53508
+ var _interopRequireDefault$4 = interopRequireDefault.exports;
53509
+ Object.defineProperty(UnfoldLess, "__esModule", {
53510
+ value: true
53511
+ });
53512
+ var default_1$4 = UnfoldLess["default"] = void 0;
53513
+ var _createSvgIcon$4 = /*#__PURE__*/_interopRequireDefault$4(createSvgIcon);
53514
+ var _jsxRuntime$4 = require$$2__default["default"];
53515
+ var _default$4 = /*#__PURE__*/(0, _createSvgIcon$4["default"])( /*#__PURE__*/(0, _jsxRuntime$4.jsx)("path", {
53516
+ d: "M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"
53517
+ }), 'UnfoldLess');
53518
+ default_1$4 = UnfoldLess["default"] = _default$4;
53519
+
53520
+ /* eslint-disable @typescript-eslint/unbound-method */
53521
+ const useStyles$2 = mui.makeStyles()({
53522
+ toolbar: {
53523
+ width: '100%',
53524
+ display: 'flex',
53525
+ paddingRight: '2em',
53526
+ flexDirection: 'row',
53527
+ justifyContent: 'space-between',
53528
+ position: 'absolute',
53529
+ zIndex: 4,
53530
+ },
53531
+ filterText: {},
53532
+ });
53533
+ const ToolBar = mobxReact.observer(function ToolBar({ model: displayState, }) {
53534
+ const model = displayState.tabularEditor;
53535
+ const { classes } = useStyles$2();
53536
+ return (React__default["default"].createElement("div", { className: classes.toolbar },
53537
+ React__default["default"].createElement(material.Tooltip, { title: "Collapse all" },
53538
+ React__default["default"].createElement(material.IconButton, { "aria-label": "collapse", sx: { marginTop: 0 }, onClick: model.collapseAllFeatures },
53539
+ React__default["default"].createElement(default_1$4, null))),
53540
+ React__default["default"].createElement(material.TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
53541
+ model.setFilterText(event.target.value);
53542
+ }, InputProps: {
53543
+ endAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "end" },
53544
+ React__default["default"].createElement(material.IconButton, { onClick: () => {
53545
+ model.clearFilterText();
53546
+ } },
53547
+ React__default["default"].createElement(default_1$5, null)))),
53548
+ } })));
53549
+ });
53550
+
53551
+ function stopPropagation(e) {
53552
+ e.stopPropagation();
53553
+ }
53554
+ const TabularEditorPane = mobxReact.observer(function TabularEditorPane({ model: displayState, }) {
53555
+ const model = displayState.tabularEditor;
53556
+ if (!model.isShown) {
53557
+ return null;
53558
+ }
53559
+ return (
53560
+ // TODO: a11y
53561
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
53562
+ React__default["default"].createElement("div", { onMouseDown: stopPropagation, onClick: stopPropagation, style: { width: '100%', height: '100%', position: 'relative' } },
53563
+ React__default["default"].createElement(ToolBar, { model: displayState }),
53564
+ React__default["default"].createElement(HybridGrid, { model: displayState })));
53565
+ });
53566
+
53567
+ const TabularEditorStateModelType = require$$1$3.types
53568
+ .model('TabularEditor', {
53569
+ isShown: true,
53570
+ featureCollapsed: require$$1$3.types.map(require$$1$3.types.boolean),
53571
+ filterText: '',
53572
+ })
53573
+ .actions((self) => ({
53574
+ setFeatureCollapsed(id, state) {
53575
+ self.featureCollapsed.set(id, state);
53576
+ },
53577
+ setFilterText(text) {
53578
+ self.filterText = text;
53579
+ },
53580
+ clearFilterText() {
53581
+ self.filterText = '';
53582
+ },
53583
+ collapseAllFeatures() {
53584
+ // iterate over all seen features and set them to collapsed
53585
+ const display = require$$1$3.getParent(self);
53586
+ for (const [featureId] of display.seenFeatures.entries()) {
53587
+ self.featureCollapsed.set(featureId, true);
53588
+ }
53589
+ },
53590
+ togglePane() {
53591
+ self.isShown = !self.isShown;
53592
+ },
53593
+ hidePane() {
53594
+ self.isShown = false;
53595
+ },
53596
+ showPane() {
53597
+ self.isShown = true;
53598
+ },
53599
+ // onPatch(patch: any) {
53600
+ // console.log(patch)
53601
+ // },
53602
+ }));
53603
+
53604
+ const FilterFeatures = mobxReact.observer(function FilterFeatures({ featureTypes, handleClose, onUpdate, session, }) {
53605
+ const [type, setType] = React.useState('');
53606
+ const [selectedFeatureTypes, setSelectedFeatureTypes] = React.useState(featureTypes);
53607
+ const handleChange = (value) => {
53608
+ setType(value);
53609
+ };
53610
+ const handleAddFeatureType = () => {
53611
+ if (type) {
53612
+ if (selectedFeatureTypes.includes(type)) {
53613
+ return;
53614
+ }
53615
+ onUpdate([...selectedFeatureTypes, type]);
53616
+ setSelectedFeatureTypes([...selectedFeatureTypes, type]);
53617
+ }
53618
+ };
53619
+ const handleFeatureTypeDelete = (value) => {
53620
+ const newTypes = selectedFeatureTypes.filter((type) => type !== value);
53621
+ onUpdate(newTypes);
53622
+ setSelectedFeatureTypes(newTypes);
53623
+ };
53624
+ return (React__default["default"].createElement(Dialog, { open: true, maxWidth: false, "data-testid": "filter-features-dialog", title: "Filter features by type", handleClose: handleClose },
53625
+ React__default["default"].createElement(material.DialogContent, null,
53626
+ React__default["default"].createElement(material.DialogContentText, null, "Select the feature types you want to display in the apollo track"),
53627
+ React__default["default"].createElement(material.Grid2, { container: true, spacing: 2 },
53628
+ React__default["default"].createElement(material.Grid2, null,
53629
+ React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: '100%' }, value: type, filterTerms: isOntologyClass, renderInput: (params) => (React__default["default"].createElement(material.TextField, { ...params, label: "Feature type", variant: "outlined", fullWidth: true })), onChange: (oldValue, newValue) => {
53630
+ if (newValue) {
53631
+ handleChange(newValue);
53632
+ }
53633
+ } })),
53634
+ React__default["default"].createElement(material.Grid2, null,
53635
+ React__default["default"].createElement(material.Button, { variant: "contained", onClick: handleAddFeatureType, disabled: !type, style: { marginTop: 9 }, size: "medium" }, "Add"))),
53636
+ selectedFeatureTypes.length > 0 && (React__default["default"].createElement("div", null,
53637
+ React__default["default"].createElement("hr", null),
53638
+ React__default["default"].createElement("div", { style: { width: 300 } },
53639
+ React__default["default"].createElement(material.DialogContentText, null, "Selected feature types:"),
53640
+ React__default["default"].createElement(material.Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 0.5 } }, selectedFeatureTypes.map((value) => (React__default["default"].createElement(material.Chip, { key: value, label: value, onDelete: () => {
53641
+ handleFeatureTypeDelete(value);
53642
+ } }))))))))));
53643
+ });
53644
+
52704
53645
  const minDisplayHeight = 20;
52705
53646
  function baseModelFactory(_pluginManager, configSchema) {
52706
53647
  return pluggableElementTypes.BaseDisplay.named('BaseLinearApolloDisplay')
@@ -52710,6 +53651,7 @@
52710
53651
  graphical: true,
52711
53652
  table: false,
52712
53653
  heightPreConfig: require$$1$3.types.maybe(require$$1$3.types.refinement('displayHeight', require$$1$3.types.number, (n) => n >= minDisplayHeight)),
53654
+ filteredFeatureTypes: require$$1$3.types.array(require$$1$3.types.string),
52713
53655
  })
52714
53656
  .views((self) => {
52715
53657
  const { configuration, renderProps: superRenderProps } = self;
@@ -52824,9 +53766,12 @@
52824
53766
  self.graphical = true;
52825
53767
  self.table = true;
52826
53768
  },
53769
+ updateFilteredFeatureTypes(types) {
53770
+ self.filteredFeatureTypes = require$$1$3.cast(types);
53771
+ },
52827
53772
  }))
52828
53773
  .views((self) => {
52829
- const { trackMenuItems: superTrackMenuItems } = self;
53774
+ const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self;
52830
53775
  return {
52831
53776
  trackMenuItems() {
52832
53777
  const { graphical, table } = self;
@@ -52862,6 +53807,25 @@
52862
53807
  },
52863
53808
  ],
52864
53809
  },
53810
+ {
53811
+ label: 'Filter features by type',
53812
+ onClick: () => {
53813
+ const session = self.session;
53814
+ self.session.queueDialog((doneCallback) => [
53815
+ FilterFeatures,
53816
+ {
53817
+ session,
53818
+ handleClose: () => {
53819
+ doneCallback();
53820
+ },
53821
+ featureTypes: require$$1$3.getSnapshot(filteredFeatureTypes),
53822
+ onUpdate: (types) => {
53823
+ self.updateFilteredFeatureTypes(types);
53824
+ },
53825
+ },
53826
+ ]);
53827
+ },
53828
+ },
52865
53829
  ];
52866
53830
  },
52867
53831
  };
@@ -52884,779 +53848,6 @@
52884
53848
  }));
52885
53849
  }
52886
53850
 
52887
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
52888
- function layoutsModelFactory(pluginManager, configSchema) {
52889
- const BaseLinearApolloDisplay = baseModelFactory(pluginManager, configSchema);
52890
- return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
52891
- .props({
52892
- featuresMinMaxLimit: 500_000,
52893
- })
52894
- .volatile(() => ({
52895
- seenFeatures: mobx.observable.map(),
52896
- }))
52897
- .views((self) => ({
52898
- get featuresMinMax() {
52899
- const { assemblyManager } = self.session;
52900
- return self.lgv.displayedRegions.map((region) => {
52901
- const assembly = assemblyManager.get(region.assemblyName);
52902
- let min;
52903
- let max;
52904
- const { end, refName, start } = region;
52905
- for (const [, feature] of self.seenFeatures) {
52906
- if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
52907
- !require$$1$2.doesIntersect2(start, end, feature.min, feature.max) ||
52908
- feature.length > self.featuresMinMaxLimit) {
52909
- continue;
52910
- }
52911
- if (min === undefined) {
52912
- ({ min } = feature);
52913
- }
52914
- if (max === undefined) {
52915
- ({ max } = feature);
52916
- }
52917
- if (feature.minWithChildren < min) {
52918
- ({ min } = feature);
52919
- }
52920
- if (feature.maxWithChildren > max) {
52921
- ({ max } = feature);
52922
- }
52923
- }
52924
- if (min !== undefined && max !== undefined) {
52925
- return [min, max];
52926
- }
52927
- return;
52928
- });
52929
- },
52930
- }))
52931
- .actions((self) => ({
52932
- addSeenFeature(feature) {
52933
- self.seenFeatures.set(feature._id, feature);
52934
- },
52935
- deleteSeenFeature(featureId) {
52936
- self.seenFeatures.delete(featureId);
52937
- },
52938
- }))
52939
- .views((self) => ({
52940
- get featureLayouts() {
52941
- const { assemblyManager } = self.session;
52942
- return self.lgv.displayedRegions.map((region, idx) => {
52943
- const assembly = assemblyManager.get(region.assemblyName);
52944
- const featureLayout = new Map();
52945
- const minMax = self.featuresMinMax[idx];
52946
- if (!minMax) {
52947
- return featureLayout;
52948
- }
52949
- const [min, max] = minMax;
52950
- const rows = [];
52951
- const { end, refName, start } = region;
52952
- for (const [id, feature] of self.seenFeatures.entries()) {
52953
- if (!require$$1$3.isAlive(feature)) {
52954
- self.deleteSeenFeature(id);
52955
- continue;
52956
- }
52957
- if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
52958
- !require$$1$2.doesIntersect2(start, end, feature.min, feature.max)) {
52959
- continue;
52960
- }
52961
- const rowCount = getGlyph(feature).getRowCount(feature, self.lgv.bpPerPx);
52962
- let startingRow = 0;
52963
- let placed = false;
52964
- while (!placed) {
52965
- let rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
52966
- if (rowsForFeature.length < rowCount) {
52967
- for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
52968
- const newRowNumber = rows.length;
52969
- rows[newRowNumber] = Array.from({ length: max - min });
52970
- featureLayout.set(newRowNumber, []);
52971
- }
52972
- rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
52973
- }
52974
- if (rowsForFeature
52975
- .map((rowForFeature) => {
52976
- // zero-length features are allowed in the spec
52977
- const featureMax = feature.max - feature.min === 0
52978
- ? feature.min + 1
52979
- : feature.max;
52980
- let start = feature.min - min, end = featureMax - min;
52981
- if (feature.min - min < 0) {
52982
- start = 0;
52983
- end = featureMax - feature.min;
52984
- }
52985
- return rowForFeature.slice(start, end).some(Boolean);
52986
- })
52987
- .some(Boolean)) {
52988
- startingRow += 1;
52989
- continue;
52990
- }
52991
- for (let rowNum = startingRow; rowNum < startingRow + rowCount; rowNum++) {
52992
- const row = rows[rowNum];
52993
- let start = feature.min - min, end = feature.max - min;
52994
- if (feature.min - min < 0) {
52995
- start = 0;
52996
- end = feature.max - feature.min;
52997
- }
52998
- row.fill(true, start, end);
52999
- const layoutRow = featureLayout.get(rowNum);
53000
- layoutRow?.push([rowNum - startingRow, feature]);
53001
- }
53002
- placed = true;
53003
- }
53004
- }
53005
- return featureLayout;
53006
- });
53007
- },
53008
- getFeatureLayoutPosition(feature) {
53009
- const { featureLayouts } = this;
53010
- for (const [idx, layout] of featureLayouts.entries()) {
53011
- for (const [layoutRowNum, layoutRow] of layout) {
53012
- for (const [featureRowNum, layoutFeature] of layoutRow) {
53013
- if (featureRowNum !== 0) {
53014
- // Same top-level feature in all feature rows, so only need to
53015
- // check the first one
53016
- continue;
53017
- }
53018
- if (feature._id === layoutFeature._id) {
53019
- return {
53020
- layoutIndex: idx,
53021
- layoutRow: layoutRowNum,
53022
- featureRow: featureRowNum,
53023
- };
53024
- }
53025
- if (layoutFeature.hasDescendant(feature._id)) {
53026
- const row = getGlyph(layoutFeature).getRowForFeature(layoutFeature, feature);
53027
- if (row !== undefined) {
53028
- return {
53029
- layoutIndex: idx,
53030
- layoutRow: layoutRowNum,
53031
- featureRow: row,
53032
- };
53033
- }
53034
- }
53035
- }
53036
- }
53037
- }
53038
- return;
53039
- },
53040
- }))
53041
- .views((self) => ({
53042
- get highestRow() {
53043
- return Math.max(0, ...self.featureLayouts.map((layout) => Math.max(...layout.keys())));
53044
- },
53045
- }))
53046
- .actions((self) => ({
53047
- afterAttach() {
53048
- require$$1$3.addDisposer(self, mobx.autorun(() => {
53049
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
53050
- return;
53051
- }
53052
- for (const region of self.regions) {
53053
- const assembly = self.session.apolloDataStore.assemblies.get(region.assemblyName);
53054
- const ref = assembly?.getByRefName(region.refName);
53055
- const features = ref?.features;
53056
- if (!features) {
53057
- continue;
53058
- }
53059
- for (const [, feature] of features) {
53060
- if (require$$1$2.doesIntersect2(region.start, region.end, feature.min, feature.max) &&
53061
- !self.seenFeatures.has(feature._id)) {
53062
- self.addSeenFeature(feature);
53063
- }
53064
- }
53065
- }
53066
- }, { name: 'LinearApolloDisplaySetSeenFeatures', delay: 1000 }));
53067
- },
53068
- }));
53069
- }
53070
-
53071
- function renderingModelIntermediateFactory(pluginManager, configSchema) {
53072
- const LinearApolloDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
53073
- return LinearApolloDisplayLayouts.named('LinearApolloDisplayRendering')
53074
- .props({
53075
- sequenceRowHeight: 15,
53076
- apolloRowHeight: 20,
53077
- detailsMinHeight: 200,
53078
- detailsHeight: 200,
53079
- lastRowTooltipBufferHeight: 40,
53080
- isShown: true,
53081
- })
53082
- .volatile(() => ({
53083
- canvas: null,
53084
- overlayCanvas: null,
53085
- collaboratorCanvas: null,
53086
- seqTrackCanvas: null,
53087
- seqTrackOverlayCanvas: null,
53088
- theme: undefined,
53089
- }))
53090
- .views((self) => ({
53091
- get featuresHeight() {
53092
- return ((self.highestRow + 1) * self.apolloRowHeight +
53093
- self.lastRowTooltipBufferHeight);
53094
- },
53095
- }))
53096
- .actions((self) => ({
53097
- toggleShown() {
53098
- self.isShown = !self.isShown;
53099
- },
53100
- setDetailsHeight(newHeight) {
53101
- self.detailsHeight = self.isShown
53102
- ? Math.max(Math.min(newHeight, self.height - 100), Math.min(self.height, self.detailsMinHeight))
53103
- : newHeight;
53104
- },
53105
- setCanvas(canvas) {
53106
- self.canvas = canvas;
53107
- },
53108
- setOverlayCanvas(canvas) {
53109
- self.overlayCanvas = canvas;
53110
- },
53111
- setCollaboratorCanvas(canvas) {
53112
- self.collaboratorCanvas = canvas;
53113
- },
53114
- setSeqTrackCanvas(canvas) {
53115
- self.seqTrackCanvas = canvas;
53116
- },
53117
- setSeqTrackOverlayCanvas(canvas) {
53118
- self.seqTrackOverlayCanvas = canvas;
53119
- },
53120
- setTheme(theme) {
53121
- self.theme = theme;
53122
- },
53123
- afterAttach() {
53124
- require$$1$3.addDisposer(self, mobx.autorun(() => {
53125
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
53126
- return;
53127
- }
53128
- const ctx = self.collaboratorCanvas?.getContext('2d');
53129
- if (!ctx) {
53130
- return;
53131
- }
53132
- ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
53133
- for (const collaborator of self.session.collaborators) {
53134
- const { locations } = collaborator;
53135
- if (locations.length === 0) {
53136
- continue;
53137
- }
53138
- let idx = 0;
53139
- for (const displayedRegion of self.lgv.displayedRegions) {
53140
- for (const location of locations) {
53141
- if (location.refSeq !== displayedRegion.refName) {
53142
- continue;
53143
- }
53144
- const { end, refSeq, start } = location;
53145
- const locationStartPxInfo = self.lgv.bpToPx({
53146
- refName: refSeq,
53147
- coord: start,
53148
- regionNumber: idx,
53149
- });
53150
- if (!locationStartPxInfo) {
53151
- continue;
53152
- }
53153
- const locationStartPx = locationStartPxInfo.offsetPx - self.lgv.offsetPx;
53154
- const locationWidthPx = (end - start) / self.lgv.bpPerPx;
53155
- ctx.fillStyle = 'rgba(0,255,0,.2)';
53156
- ctx.fillRect(locationStartPx, 1, locationWidthPx, 100);
53157
- ctx.fillStyle = 'black';
53158
- ctx.fillText(collaborator.name, locationStartPx + 1, 11, locationWidthPx - 2);
53159
- }
53160
- idx++;
53161
- }
53162
- }
53163
- }, { name: 'LinearApolloDisplayRenderCollaborators' }));
53164
- },
53165
- }));
53166
- }
53167
- function colorCode(letter, theme) {
53168
- return (theme?.palette.bases[letter.toUpperCase()].main.toString() ?? 'lightgray');
53169
- }
53170
- function codonColorCode(letter) {
53171
- const colorMap = {
53172
- M: '#33ee33',
53173
- '*': '#f44336',
53174
- };
53175
- return colorMap[letter.toUpperCase()];
53176
- }
53177
- function reverseCodonSeq(seq) {
53178
- return [...seq]
53179
- .map((c) => require$$1$2.revcom(c))
53180
- .reverse()
53181
- .join('');
53182
- }
53183
- function drawLetter(seqTrackctx, startPx, widthPx, letter, textY) {
53184
- const fontSize = Math.min(widthPx, 10);
53185
- seqTrackctx.fillStyle = '#000';
53186
- seqTrackctx.font = `${fontSize}px`;
53187
- const textWidth = seqTrackctx.measureText(letter).width;
53188
- const textX = startPx + (widthPx - textWidth) / 2;
53189
- seqTrackctx.fillText(letter, textX, textY + 10);
53190
- }
53191
- function drawTranslation(seqTrackctx, bpPerPx, trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight, seq, i, reverse) {
53192
- let codonSeq = seq.slice(i, i + 3).toUpperCase();
53193
- if (reverse) {
53194
- codonSeq = reverseCodonSeq(codonSeq);
53195
- }
53196
- const codonLetter = require$$1$2.defaultCodonTable[codonSeq];
53197
- if (!codonLetter) {
53198
- return;
53199
- }
53200
- const fillColor = codonColorCode(codonLetter);
53201
- if (fillColor) {
53202
- seqTrackctx.fillStyle = fillColor;
53203
- seqTrackctx.fillRect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
53204
- }
53205
- if (bpPerPx <= 0.1) {
53206
- seqTrackctx.rect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
53207
- seqTrackctx.stroke();
53208
- drawLetter(seqTrackctx, trnslStartPx, trnslWidthPx, codonLetter, trnslY);
53209
- }
53210
- }
53211
- function sequenceRenderingModelFactory(pluginManager, configSchema) {
53212
- const LinearApolloDisplayRendering = renderingModelIntermediateFactory(pluginManager, configSchema);
53213
- return LinearApolloDisplayRendering.actions((self) => ({
53214
- afterAttach() {
53215
- require$$1$3.addDisposer(self, mobx.autorun(async () => {
53216
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
53217
- return;
53218
- }
53219
- if (self.lgv.bpPerPx > 3) {
53220
- return;
53221
- }
53222
- const seqTrackctx = self.seqTrackCanvas?.getContext('2d');
53223
- if (!seqTrackctx) {
53224
- return;
53225
- }
53226
- seqTrackctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
53227
- const frames = self.lgv.bpPerPx <= 1
53228
- ? [3, 2, 1, 0, 0, -1, -2, -3]
53229
- : [3, 2, 1, -1, -2, -3];
53230
- let height = 0;
53231
- for (const frame of frames) {
53232
- const frameColor = self.theme?.palette.framesCDS.at(frame)?.main;
53233
- if (frameColor) {
53234
- seqTrackctx.fillStyle = frameColor;
53235
- seqTrackctx.fillRect(0, height, self.lgv.dynamicBlocks.totalWidthPx, self.sequenceRowHeight);
53236
- }
53237
- height += self.sequenceRowHeight;
53238
- }
53239
- for (const [idx, region] of self.regions.entries()) {
53240
- const driver = self.session.apolloDataStore.getBackendDriver(region.assemblyName);
53241
- if (!driver) {
53242
- throw new Error('Failed to get the backend driver');
53243
- }
53244
- const { seq } = await driver.getSequence(region);
53245
- if (!seq) {
53246
- return;
53247
- }
53248
- for (const [i, letter] of [...seq].entries()) {
53249
- const trnslXOffset = (self.lgv.bpToPx({
53250
- refName: region.refName,
53251
- coord: region.start + i,
53252
- regionNumber: idx,
53253
- })?.offsetPx ?? 0) - self.lgv.offsetPx;
53254
- const trnslWidthPx = 3 / self.lgv.bpPerPx;
53255
- const trnslStartPx = self.lgv.displayedRegions[idx].reversed
53256
- ? trnslXOffset - trnslWidthPx
53257
- : trnslXOffset;
53258
- // Draw translation forward
53259
- for (let j = 2; j >= 0; j--) {
53260
- if ((region.start + i) % 3 === j) {
53261
- drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (2 - j), trnslWidthPx, self.sequenceRowHeight, seq, i, false);
53262
- }
53263
- }
53264
- if (self.lgv.bpPerPx <= 1) {
53265
- const xOffset = (self.lgv.bpToPx({
53266
- refName: region.refName,
53267
- coord: region.start + i,
53268
- regionNumber: idx,
53269
- })?.offsetPx ?? 0) - self.lgv.offsetPx;
53270
- const widthPx = 1 / self.lgv.bpPerPx;
53271
- const startPx = self.lgv.displayedRegions[idx].reversed
53272
- ? xOffset - widthPx
53273
- : xOffset;
53274
- // Draw forward
53275
- seqTrackctx.beginPath();
53276
- seqTrackctx.fillStyle = colorCode(letter, self.theme);
53277
- seqTrackctx.rect(startPx, self.sequenceRowHeight * 3, widthPx, self.sequenceRowHeight);
53278
- seqTrackctx.fill();
53279
- if (self.lgv.bpPerPx <= 0.1) {
53280
- seqTrackctx.stroke();
53281
- drawLetter(seqTrackctx, startPx, widthPx, letter, self.sequenceRowHeight * 3);
53282
- }
53283
- // Draw reverse
53284
- const revLetter = require$$1$2.revcom(letter);
53285
- seqTrackctx.beginPath();
53286
- seqTrackctx.fillStyle = colorCode(revLetter, self.theme);
53287
- seqTrackctx.rect(startPx, self.sequenceRowHeight * 4, widthPx, self.sequenceRowHeight);
53288
- seqTrackctx.fill();
53289
- if (self.lgv.bpPerPx <= 0.1) {
53290
- seqTrackctx.stroke();
53291
- drawLetter(seqTrackctx, startPx, widthPx, revLetter, self.sequenceRowHeight * 4);
53292
- }
53293
- }
53294
- // Draw translation reverse
53295
- for (let k = 0; k <= 2; k++) {
53296
- const rowOffset = self.lgv.bpPerPx <= 1 ? 5 : 3;
53297
- if ((region.start + i) % 3 === k) {
53298
- drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (rowOffset + k), trnslWidthPx, self.sequenceRowHeight, seq, i, true);
53299
- }
53300
- }
53301
- }
53302
- }
53303
- }, { name: 'LinearApolloDisplayRenderSequence' }));
53304
- },
53305
- }));
53306
- }
53307
- function renderingModelFactory(pluginManager, configSchema) {
53308
- const LinearApolloDisplayRendering = sequenceRenderingModelFactory(pluginManager, configSchema);
53309
- return LinearApolloDisplayRendering.actions((self) => ({
53310
- afterAttach() {
53311
- require$$1$3.addDisposer(self, mobx.autorun(() => {
53312
- const { canvas, featureLayouts, featuresHeight, lgv } = self;
53313
- if (!lgv.initialized || self.regionCannotBeRendered()) {
53314
- return;
53315
- }
53316
- const { displayedRegions, dynamicBlocks } = lgv;
53317
- const ctx = canvas?.getContext('2d');
53318
- if (!ctx) {
53319
- return;
53320
- }
53321
- ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight);
53322
- for (const [idx, featureLayout] of featureLayouts.entries()) {
53323
- const displayedRegion = displayedRegions[idx];
53324
- for (const [row, featureLayoutRow] of featureLayout.entries()) {
53325
- for (const [featureRow, feature] of featureLayoutRow) {
53326
- if (featureRow > 0) {
53327
- continue;
53328
- }
53329
- if (!require$$1$2.doesIntersect2(displayedRegion.start, displayedRegion.end, feature.min, feature.max)) {
53330
- continue;
53331
- }
53332
- getGlyph(feature).draw(ctx, feature, row, self, idx);
53333
- }
53334
- }
53335
- }
53336
- }, { name: 'LinearApolloDisplayRenderFeatures' }));
53337
- },
53338
- }));
53339
- }
53340
-
53341
- function isMousePositionWithFeatureAndGlyph(mousePosition) {
53342
- return 'featureAndGlyphUnderMouse' in mousePosition;
53343
- }
53344
- function getMousePosition(event, lgv) {
53345
- const canvas = event.currentTarget;
53346
- const { clientX, clientY } = event;
53347
- const { left, top } = canvas.getBoundingClientRect();
53348
- const x = clientX - left;
53349
- const y = clientY - top;
53350
- const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x);
53351
- return { x, y, refName, bp, regionNumber };
53352
- }
53353
- function getTranslationRow(frame, bpPerPx) {
53354
- const offset = bpPerPx <= 1 ? 2 : 0;
53355
- switch (frame) {
53356
- case 3: {
53357
- return 0;
53358
- }
53359
- case 2: {
53360
- return 1;
53361
- }
53362
- case 1: {
53363
- return 2;
53364
- }
53365
- case -1: {
53366
- return 3 + offset;
53367
- }
53368
- case -2: {
53369
- return 4 + offset;
53370
- }
53371
- case -3: {
53372
- return 5 + offset;
53373
- }
53374
- }
53375
- }
53376
- function getSeqRow(strand, bpPerPx) {
53377
- if (bpPerPx > 1 || strand === undefined) {
53378
- return;
53379
- }
53380
- return strand === 1 ? 3 : 4;
53381
- }
53382
- function highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx) {
53383
- if (row !== undefined) {
53384
- seqTrackOverlayctx.fillStyle =
53385
- theme?.palette.action.focus ?? 'rgba(0,0,0,0.04)';
53386
- seqTrackOverlayctx.fillRect(startPx, sequenceRowHeight * row, widthPx, sequenceRowHeight);
53387
- }
53388
- }
53389
- function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
53390
- const LinearApolloDisplayRendering = renderingModelFactory(pluginManager, configSchema);
53391
- return LinearApolloDisplayRendering.named('LinearApolloDisplayMouseEvents')
53392
- .volatile(() => ({
53393
- apolloDragging: null,
53394
- cursor: undefined,
53395
- apolloHover: undefined,
53396
- }))
53397
- .views((self) => ({
53398
- getMousePosition(event) {
53399
- const mousePosition = getMousePosition(event, self.lgv);
53400
- const { bp, regionNumber, y } = mousePosition;
53401
- const row = Math.floor(y / self.apolloRowHeight);
53402
- const featureLayout = self.featureLayouts[regionNumber];
53403
- const layoutRow = featureLayout.get(row);
53404
- if (!layoutRow) {
53405
- return mousePosition;
53406
- }
53407
- const foundFeature = layoutRow.find((f) => bp >= f[1].min && bp <= f[1].max);
53408
- if (!foundFeature) {
53409
- return mousePosition;
53410
- }
53411
- const [featureRow, topLevelFeature] = foundFeature;
53412
- const glyph = getGlyph(topLevelFeature);
53413
- const feature = glyph.getFeatureFromLayout(topLevelFeature, bp, featureRow);
53414
- if (!feature) {
53415
- return mousePosition;
53416
- }
53417
- return {
53418
- ...mousePosition,
53419
- featureAndGlyphUnderMouse: { feature, topLevelFeature, glyph },
53420
- };
53421
- },
53422
- }))
53423
- .actions((self) => ({
53424
- continueDrag(mousePosition, event) {
53425
- if (!self.apolloDragging) {
53426
- throw new Error('continueDrag() called with no current drag in progress');
53427
- }
53428
- event.stopPropagation();
53429
- self.apolloDragging = { ...self.apolloDragging, current: mousePosition };
53430
- },
53431
- setDragging(dragInfo) {
53432
- self.apolloDragging = dragInfo ?? null;
53433
- },
53434
- }))
53435
- .actions((self) => ({
53436
- setApolloHover(n) {
53437
- self.apolloHover = n;
53438
- },
53439
- setCursor(cursor) {
53440
- if (self.cursor !== cursor) {
53441
- self.cursor = cursor;
53442
- }
53443
- },
53444
- }))
53445
- .actions(() => ({
53446
- // onClick(event: CanvasMouseEvent) {
53447
- onClick() {
53448
- // TODO: set the selected feature
53449
- },
53450
- }));
53451
- }
53452
- function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
53453
- const LinearApolloDisplayRendering = mouseEventsModelIntermediateFactory(pluginManager, configSchema);
53454
- return LinearApolloDisplayRendering.actions((self) => ({
53455
- afterAttach() {
53456
- require$$1$3.addDisposer(self, mobx.autorun(() => {
53457
- // This type is wrong in @jbrowse/core
53458
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
53459
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
53460
- return;
53461
- }
53462
- const seqTrackOverlayctx = self.seqTrackOverlayCanvas?.getContext('2d');
53463
- if (!seqTrackOverlayctx) {
53464
- return;
53465
- }
53466
- seqTrackOverlayctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
53467
- const { apolloHover, lgv, regions, sequenceRowHeight, theme } = self;
53468
- if (!apolloHover) {
53469
- return;
53470
- }
53471
- const { feature } = apolloHover;
53472
- for (const [idx, region] of regions.entries()) {
53473
- if (feature.type === 'CDS') {
53474
- const parentFeature = feature.parent;
53475
- if (!parentFeature) {
53476
- continue;
53477
- }
53478
- const cdsLocs = parentFeature.cdsLocations.find((loc) => feature.min === loc.at(0)?.min &&
53479
- feature.max === loc.at(-1)?.max);
53480
- if (!cdsLocs) {
53481
- continue;
53482
- }
53483
- for (const dl of cdsLocs) {
53484
- const frame = require$$1$2.getFrame(dl.min, dl.max, feature.strand ?? 1, dl.phase);
53485
- const row = getTranslationRow(frame, lgv.bpPerPx);
53486
- const offset = (lgv.bpToPx({
53487
- refName: region.refName,
53488
- coord: dl.min,
53489
- regionNumber: idx,
53490
- })?.offsetPx ?? 0) - lgv.offsetPx;
53491
- const widthPx = (dl.max - dl.min) / lgv.bpPerPx;
53492
- const startPx = lgv.displayedRegions[idx].reversed
53493
- ? offset - widthPx
53494
- : offset;
53495
- highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
53496
- }
53497
- }
53498
- else {
53499
- const row = getSeqRow(feature.strand, lgv.bpPerPx);
53500
- const offset = (lgv.bpToPx({
53501
- refName: region.refName,
53502
- coord: feature.min,
53503
- regionNumber: idx,
53504
- })?.offsetPx ?? 0) - lgv.offsetPx;
53505
- const widthPx = feature.length / lgv.bpPerPx;
53506
- const startPx = lgv.displayedRegions[idx].reversed
53507
- ? offset - widthPx
53508
- : offset;
53509
- highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
53510
- }
53511
- }
53512
- }, { name: 'LinearApolloDisplayRenderSeqHighlight' }));
53513
- },
53514
- }));
53515
- }
53516
- function mouseEventsModelFactory(pluginManager, configSchema) {
53517
- const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
53518
- return LinearApolloDisplayMouseEvents.views((self) => ({
53519
- contextMenuItems(contextCoord) {
53520
- const { apolloHover } = self;
53521
- if (!(apolloHover && contextCoord)) {
53522
- return [];
53523
- }
53524
- const { topLevelFeature } = apolloHover;
53525
- const glyph = getGlyph(topLevelFeature);
53526
- return glyph.getContextMenuItems(self);
53527
- },
53528
- }))
53529
- .actions((self) => ({
53530
- // explicitly pass in a feature in case it's not the same as the one in
53531
- // mousePosition (e.g. if features are drawn overlapping).
53532
- startDrag(mousePosition, feature, edge) {
53533
- self.apolloDragging = {
53534
- start: mousePosition,
53535
- current: mousePosition,
53536
- feature,
53537
- edge,
53538
- };
53539
- },
53540
- endDrag() {
53541
- if (!self.apolloDragging) {
53542
- throw new Error('endDrag() called with no current drag in progress');
53543
- }
53544
- const { current, edge, feature, start } = self.apolloDragging;
53545
- // don't do anything if it was only dragged a tiny bit
53546
- if (Math.abs(current.x - start.x) <= 4) {
53547
- self.setDragging();
53548
- self.setCursor();
53549
- return;
53550
- }
53551
- const { displayedRegions } = self.lgv;
53552
- const region = displayedRegions[start.regionNumber];
53553
- const assembly = self.getAssemblyId(region.assemblyName);
53554
- let change;
53555
- if (edge === 'max') {
53556
- const featureId = feature._id;
53557
- const oldEnd = feature.max;
53558
- const newEnd = current.bp;
53559
- change = new dist$2.LocationEndChange({
53560
- typeName: 'LocationEndChange',
53561
- changedIds: [featureId],
53562
- featureId,
53563
- oldEnd,
53564
- newEnd,
53565
- assembly,
53566
- });
53567
- }
53568
- else {
53569
- const featureId = feature._id;
53570
- const oldStart = feature.min;
53571
- const newStart = current.bp;
53572
- change = new dist$2.LocationStartChange({
53573
- typeName: 'LocationStartChange',
53574
- changedIds: [featureId],
53575
- featureId,
53576
- oldStart,
53577
- newStart,
53578
- assembly,
53579
- });
53580
- }
53581
- void self.changeManager.submit(change);
53582
- self.setDragging();
53583
- self.setCursor();
53584
- },
53585
- }))
53586
- .actions((self) => ({
53587
- onMouseDown(event) {
53588
- const mousePosition = self.getMousePosition(event);
53589
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
53590
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseDown(self, mousePosition, event);
53591
- }
53592
- },
53593
- onMouseMove(event) {
53594
- const mousePosition = self.getMousePosition(event);
53595
- if (self.apolloDragging) {
53596
- self.setCursor('col-resize');
53597
- self.continueDrag(mousePosition, event);
53598
- return;
53599
- }
53600
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
53601
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseMove(self, mousePosition, event);
53602
- }
53603
- else {
53604
- self.setApolloHover();
53605
- self.setCursor();
53606
- }
53607
- },
53608
- onMouseLeave(event) {
53609
- self.setDragging();
53610
- self.setApolloHover();
53611
- const mousePosition = self.getMousePosition(event);
53612
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
53613
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseLeave(self, mousePosition, event);
53614
- }
53615
- },
53616
- onMouseUp(event) {
53617
- const mousePosition = self.getMousePosition(event);
53618
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
53619
- mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
53620
- }
53621
- if (self.apolloDragging) {
53622
- self.endDrag();
53623
- }
53624
- },
53625
- }))
53626
- .actions((self) => ({
53627
- afterAttach() {
53628
- require$$1$3.addDisposer(self, mobx.autorun(() => {
53629
- // This type is wrong in @jbrowse/core
53630
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
53631
- if (!self.lgv.initialized || self.regionCannotBeRendered()) {
53632
- return;
53633
- }
53634
- const ctx = self.overlayCanvas?.getContext('2d');
53635
- if (!ctx) {
53636
- return;
53637
- }
53638
- ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
53639
- const { apolloDragging, apolloHover } = self;
53640
- if (!apolloHover) {
53641
- return;
53642
- }
53643
- const { glyph } = apolloHover;
53644
- // draw mouseover hovers
53645
- glyph.drawHover(self, ctx);
53646
- // draw tooltip on hover
53647
- glyph.drawTooltip(self, ctx);
53648
- // dragging previews
53649
- if (apolloDragging) {
53650
- // NOTE: the glyph where the drag started is responsible for drawing the preview.
53651
- // it can call methods in other glyphs to help with this though.
53652
- const glyph = getGlyph(apolloDragging.feature.topLevelFeature);
53653
- glyph.drawDragPreview(self, ctx);
53654
- }
53655
- }, { name: 'LinearApolloDisplayRenderMouseoverAndDrag' }));
53656
- },
53657
- }));
53658
- }
53659
-
53660
53851
  function drawBoxOutline(ctx, x, y, width, height, color) {
53661
53852
  drawBox(ctx, x, y, width, height, color);
53662
53853
  if (width <= 2) {
@@ -53921,7 +54112,12 @@
53921
54112
  session.showWidget(apolloFeatureWidget);
53922
54113
  },
53923
54114
  });
53924
- if (sourceFeature.type === 'mRNA' && require$$1$2.isSessionModelWithWidgets(session)) {
54115
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
54116
+ if (!featureTypeOntology) {
54117
+ throw new Error('featureTypeOntology is undefined');
54118
+ }
54119
+ if (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') &&
54120
+ require$$1$2.isSessionModelWithWidgets(session)) {
53925
54121
  menuItems.push({
53926
54122
  label: 'Edit transcript details',
53927
54123
  onClick: () => {
@@ -54079,6 +54275,11 @@
54079
54275
  return;
54080
54276
  }
54081
54277
  const { apolloSelectedFeature } = session;
54278
+ const { apolloDataStore } = session;
54279
+ const { featureTypeOntology } = apolloDataStore.ontologyManager;
54280
+ if (!featureTypeOntology) {
54281
+ throw new Error('featureTypeOntology is undefined');
54282
+ }
54082
54283
  // Draw background for gene
54083
54284
  const topLevelFeatureMinX = (lgv.bpToPx({
54084
54285
  refName,
@@ -54090,22 +54291,23 @@
54090
54291
  ? topLevelFeatureMinX - topLevelFeatureWidthPx
54091
54292
  : topLevelFeatureMinX;
54092
54293
  const topLevelFeatureTop = row * rowHeight;
54093
- const topLevelFeatureHeight = getRowCount$1(feature) * rowHeight;
54294
+ const topLevelFeatureHeight = getRowCount$1(feature, featureTypeOntology) * rowHeight;
54094
54295
  ctx.fillStyle = material.alpha(theme?.palette.background.paper ?? '#ffffff', 0.6);
54095
54296
  ctx.fillRect(topLevelFeatureStartPx, topLevelFeatureTop, topLevelFeatureWidthPx, topLevelFeatureHeight);
54096
- // Draw lines on different rows for each mRNA
54297
+ // Draw lines on different rows for each transcript
54097
54298
  let currentRow = 0;
54098
- for (const [, mrna] of children) {
54099
- if (mrna.type !== 'mRNA') {
54299
+ for (const [, transcript] of children) {
54300
+ const isTranscript = featureTypeOntology.isTypeOf(transcript.type, 'transcript');
54301
+ if (!isTranscript) {
54100
54302
  currentRow += 1;
54101
54303
  continue;
54102
54304
  }
54103
- const { children: childrenOfmRNA, min } = mrna;
54104
- if (!childrenOfmRNA) {
54305
+ const { children: childrenOfTranscript, min } = transcript;
54306
+ if (!childrenOfTranscript) {
54105
54307
  continue;
54106
54308
  }
54107
- for (const [, cds] of childrenOfmRNA) {
54108
- if (cds.type !== 'CDS') {
54309
+ for (const [, cds] of childrenOfTranscript) {
54310
+ if (!featureTypeOntology.isTypeOf(cds.type, 'CDS')) {
54109
54311
  continue;
54110
54312
  }
54111
54313
  const minX = (lgv.bpToPx({
@@ -54113,7 +54315,7 @@
54113
54315
  coord: min,
54114
54316
  regionNumber: displayedRegionIndex,
54115
54317
  })?.offsetPx ?? 0) - offsetPx;
54116
- const widthPx = mrna.length / bpPerPx;
54318
+ const widthPx = transcript.length / bpPerPx;
54117
54319
  const startPx = reversed ? minX - widthPx : minX;
54118
54320
  const height = Math.round((currentRow + 1 / 2) * rowHeight) + row * rowHeight;
54119
54321
  ctx.strokeStyle = theme?.palette.text.primary ?? 'black';
@@ -54126,21 +54328,21 @@
54126
54328
  }
54127
54329
  const forwardFill = theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight;
54128
54330
  const backwardFill = theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight;
54129
- // Draw exon and CDS for each mRNA
54331
+ // Draw exon and CDS for each transcript
54130
54332
  currentRow = 0;
54131
54333
  for (const [, child] of children) {
54132
- if (child.type !== 'mRNA') {
54334
+ if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
54133
54335
  boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex);
54134
54336
  currentRow += 1;
54135
54337
  continue;
54136
54338
  }
54137
54339
  for (const cdsRow of child.cdsLocations) {
54138
- const { _id, children: childrenOfmRNA } = child;
54139
- if (!childrenOfmRNA) {
54340
+ const { _id, children: childrenOfTranscript } = child;
54341
+ if (!childrenOfTranscript) {
54140
54342
  continue;
54141
54343
  }
54142
- for (const [, exon] of childrenOfmRNA) {
54143
- if (exon.type !== 'exon') {
54344
+ for (const [, exon] of childrenOfTranscript) {
54345
+ if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
54144
54346
  continue;
54145
54347
  }
54146
54348
  const minX = (lgv.bpToPx({
@@ -54236,7 +54438,8 @@
54236
54438
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight);
54237
54439
  }
54238
54440
  function drawHover$1(stateModel, ctx) {
54239
- const { apolloHover, apolloRowHeight, lgv, theme } = stateModel;
54441
+ const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel;
54442
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
54240
54443
  if (!apolloHover) {
54241
54444
  return;
54242
54445
  }
@@ -54259,10 +54462,13 @@
54259
54462
  const top = row * apolloRowHeight;
54260
54463
  const widthPx = length / bpPerPx;
54261
54464
  ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)';
54262
- ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature));
54465
+ if (!featureTypeOntology) {
54466
+ throw new Error('featureTypeOntology is undefined');
54467
+ }
54468
+ ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount$1(feature, featureTypeOntology));
54263
54469
  }
54264
- function getFeatureFromLayout$1(feature, bp, row) {
54265
- const featureInThisRow = featuresForRow$1(feature)[row] || [];
54470
+ function getFeatureFromLayout$1(feature, bp, row, featureTypeOntology) {
54471
+ const featureInThisRow = featuresForRow$1(feature, featureTypeOntology)[row] || [];
54266
54472
  for (const f of featureInThisRow) {
54267
54473
  let featureObj;
54268
54474
  if (bp >= f.min && bp <= f.max && f.parent) {
@@ -54271,9 +54477,9 @@
54271
54477
  if (!featureObj) {
54272
54478
  continue;
54273
54479
  }
54274
- if (featureObj.type === 'CDS' &&
54480
+ if (featureTypeOntology.isTypeOf(featureObj.type, 'CDS') &&
54275
54481
  featureObj.parent &&
54276
- featureObj.parent.type === 'mRNA') {
54482
+ featureTypeOntology.isTypeOf(featureObj.parent.type, 'transcript')) {
54277
54483
  const { cdsLocations } = featureObj.parent;
54278
54484
  for (const cdsLoc of cdsLocations) {
54279
54485
  for (const loc of cdsLoc) {
@@ -54282,7 +54488,7 @@
54282
54488
  }
54283
54489
  }
54284
54490
  }
54285
- // If mouse position is in the intron region, return the mRNA
54491
+ // If mouse position is in the intron region, return the transcript
54286
54492
  return featureObj.parent;
54287
54493
  }
54288
54494
  // If mouse position is in a feature that is not a CDS, return the feature
@@ -54290,33 +54496,36 @@
54290
54496
  }
54291
54497
  return feature;
54292
54498
  }
54293
- function getRowCount$1(feature, _bpPerPx) {
54499
+ function getRowCount$1(feature, featureTypeOntology, _bpPerPx) {
54294
54500
  const { children, type } = feature;
54295
54501
  if (!children) {
54296
54502
  return 1;
54297
54503
  }
54504
+ const isTranscript = featureTypeOntology.isTypeOf(type, 'transcript');
54298
54505
  let rowCount = 0;
54299
- if (type === 'mRNA') {
54506
+ if (isTranscript) {
54300
54507
  for (const [, child] of children) {
54301
- if (child.type === 'CDS') {
54508
+ const isCds = featureTypeOntology.isTypeOf(child.type, 'CDS');
54509
+ if (isCds) {
54302
54510
  rowCount += 1;
54303
54511
  }
54304
54512
  }
54305
54513
  return rowCount;
54306
54514
  }
54307
54515
  for (const [, child] of children) {
54308
- rowCount += getRowCount$1(child);
54516
+ rowCount += getRowCount$1(child, featureTypeOntology);
54309
54517
  }
54310
54518
  return rowCount;
54311
54519
  }
54312
54520
  /**
54313
54521
  * A list of all the subfeatures for each row for a given feature, as well as
54314
54522
  * the feature itself.
54315
- * If the row contains an mRNA, the order is CDS -\> exon -\> mRNA -\> gene
54316
- * If the row does not contain an mRNA, the order is subfeature -\> gene
54523
+ * If the row contains a transcript, the order is CDS -\> exon -\> transcript -\> gene
54524
+ * If the row does not contain an transcript, the order is subfeature -\> gene
54317
54525
  */
54318
- function featuresForRow$1(feature) {
54319
- if (feature.type !== 'gene') {
54526
+ function featuresForRow$1(feature, featureTypeOntology) {
54527
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
54528
+ if (!isGene) {
54320
54529
  throw new Error('Top level feature for GeneGlyph must have type "gene"');
54321
54530
  }
54322
54531
  const { children } = feature;
@@ -54325,7 +54534,7 @@
54325
54534
  }
54326
54535
  const features = [];
54327
54536
  for (const [, child] of children) {
54328
- if (child.type !== 'mRNA') {
54537
+ if (!featureTypeOntology.isTypeOf(child.type, 'transcript')) {
54329
54538
  features.push([child, feature]);
54330
54539
  continue;
54331
54540
  }
@@ -54335,10 +54544,10 @@
54335
54544
  const cdss = [];
54336
54545
  const exons = [];
54337
54546
  for (const [, grandchild] of child.children) {
54338
- if (grandchild.type === 'CDS') {
54547
+ if (featureTypeOntology.isTypeOf(grandchild.type, 'CDS')) {
54339
54548
  cdss.push(grandchild);
54340
54549
  }
54341
- else if (grandchild.type === 'exon') {
54550
+ else if (featureTypeOntology.isTypeOf(grandchild.type, 'exon')) {
54342
54551
  exons.push(grandchild);
54343
54552
  }
54344
54553
  }
@@ -54348,599 +54557,1082 @@
54348
54557
  }
54349
54558
  return features;
54350
54559
  }
54351
- function getRowForFeature$1(feature, childFeature) {
54352
- const rows = featuresForRow$1(feature);
54353
- for (const [idx, row] of rows.entries()) {
54354
- if (row.some((feature) => feature._id === childFeature._id)) {
54355
- return idx;
54356
- }
54357
- }
54358
- return;
54560
+ function getRowForFeature$1(feature, childFeature, featureTypeOntology) {
54561
+ const rows = featuresForRow$1(feature, featureTypeOntology);
54562
+ for (const [idx, row] of rows.entries()) {
54563
+ if (row.some((feature) => feature._id === childFeature._id)) {
54564
+ return idx;
54565
+ }
54566
+ }
54567
+ return;
54568
+ }
54569
+ function onMouseDown$1(stateModel, currentMousePosition, event) {
54570
+ const { featureAndGlyphUnderMouse } = currentMousePosition;
54571
+ // swallow the mouseDown if we are on the edge of the feature so that we
54572
+ // don't start dragging the view if we try to drag the feature edge
54573
+ const { feature } = featureAndGlyphUnderMouse;
54574
+ const draggableFeature = getDraggableFeatureInfo(currentMousePosition, feature, stateModel);
54575
+ if (draggableFeature) {
54576
+ event.stopPropagation();
54577
+ stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge);
54578
+ }
54579
+ }
54580
+ function onMouseMove$1(stateModel, mousePosition) {
54581
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
54582
+ const { featureAndGlyphUnderMouse } = mousePosition;
54583
+ stateModel.setApolloHover(featureAndGlyphUnderMouse);
54584
+ const { feature } = featureAndGlyphUnderMouse;
54585
+ const draggableFeature = getDraggableFeatureInfo(mousePosition, feature, stateModel);
54586
+ if (draggableFeature) {
54587
+ stateModel.setCursor('col-resize');
54588
+ return;
54589
+ }
54590
+ }
54591
+ stateModel.setCursor();
54592
+ }
54593
+ function onMouseUp$1(stateModel, mousePosition) {
54594
+ if (stateModel.apolloDragging) {
54595
+ return;
54596
+ }
54597
+ const { featureAndGlyphUnderMouse } = mousePosition;
54598
+ if (featureAndGlyphUnderMouse?.feature) {
54599
+ stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature);
54600
+ }
54601
+ }
54602
+ function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
54603
+ const { session } = stateModel;
54604
+ const { apolloDataStore } = session;
54605
+ const { featureTypeOntology } = apolloDataStore.ontologyManager;
54606
+ if (!featureTypeOntology) {
54607
+ throw new Error('featureTypeOntology is undefined');
54608
+ }
54609
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
54610
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript');
54611
+ const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS');
54612
+ if (isGene || isTranscript) {
54613
+ return;
54614
+ }
54615
+ const { bp, refName, regionNumber, x } = mousePosition;
54616
+ const { lgv } = stateModel;
54617
+ const { offsetPx } = lgv;
54618
+ const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber });
54619
+ const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber });
54620
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
54621
+ return;
54622
+ }
54623
+ const minPx = minPxInfo.offsetPx - offsetPx;
54624
+ const maxPx = maxPxInfo.offsetPx - offsetPx;
54625
+ if (Math.abs(maxPx - minPx) < 8) {
54626
+ return;
54627
+ }
54628
+ if (Math.abs(minPx - x) < 4) {
54629
+ return { feature, edge: 'min' };
54630
+ }
54631
+ if (Math.abs(maxPx - x) < 4) {
54632
+ return { feature, edge: 'max' };
54633
+ }
54634
+ if (isCds) {
54635
+ const transcript = feature.parent;
54636
+ if (!transcript?.children) {
54637
+ return;
54638
+ }
54639
+ const exonChildren = [];
54640
+ for (const child of transcript.children.values()) {
54641
+ const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon');
54642
+ if (childIsExon) {
54643
+ exonChildren.push(child);
54644
+ }
54645
+ }
54646
+ const overlappingExon = exonChildren.find((child) => {
54647
+ const [start, end] = require$$1$2.intersection2(bp, bp + 1, child.min, child.max);
54648
+ return start !== undefined && end !== undefined;
54649
+ });
54650
+ if (!overlappingExon) {
54651
+ return;
54652
+ }
54653
+ const minPxInfo = lgv.bpToPx({
54654
+ refName,
54655
+ coord: overlappingExon.min,
54656
+ regionNumber,
54657
+ });
54658
+ const maxPxInfo = lgv.bpToPx({
54659
+ refName,
54660
+ coord: overlappingExon.max,
54661
+ regionNumber,
54662
+ });
54663
+ if (minPxInfo === undefined || maxPxInfo === undefined) {
54664
+ return;
54665
+ }
54666
+ const minPx = minPxInfo.offsetPx - offsetPx;
54667
+ const maxPx = maxPxInfo.offsetPx - offsetPx;
54668
+ if (Math.abs(maxPx - minPx) < 8) {
54669
+ return;
54670
+ }
54671
+ if (Math.abs(minPx - x) < 4) {
54672
+ return { feature: overlappingExon, edge: 'min' };
54673
+ }
54674
+ if (Math.abs(maxPx - x) < 4) {
54675
+ return { feature: overlappingExon, edge: 'max' };
54676
+ }
54677
+ }
54678
+ return;
54679
+ }
54680
+ // False positive here, none of these functions use "this"
54681
+ /* eslint-disable @typescript-eslint/unbound-method */
54682
+ const { drawTooltip: drawTooltip$1, getContextMenuItems: getContextMenuItems$1, onMouseLeave: onMouseLeave$1 } = boxGlyph;
54683
+ /* eslint-enable @typescript-eslint/unbound-method */
54684
+ const geneGlyph = {
54685
+ draw: draw$1,
54686
+ drawDragPreview: drawDragPreview$1,
54687
+ drawHover: drawHover$1,
54688
+ drawTooltip: drawTooltip$1,
54689
+ getContextMenuItems: getContextMenuItems$1,
54690
+ getFeatureFromLayout: getFeatureFromLayout$1,
54691
+ getRowCount: getRowCount$1,
54692
+ getRowForFeature: getRowForFeature$1,
54693
+ onMouseDown: onMouseDown$1,
54694
+ onMouseLeave: onMouseLeave$1,
54695
+ onMouseMove: onMouseMove$1,
54696
+ onMouseUp: onMouseUp$1,
54697
+ };
54698
+
54699
+ function featuresForRow(feature) {
54700
+ const features = [[feature]];
54701
+ if (feature.children) {
54702
+ for (const [, child] of feature.children) {
54703
+ features.push(...featuresForRow(child));
54704
+ }
54705
+ }
54706
+ return features;
54707
+ }
54708
+ function getRowCount(feature) {
54709
+ return featuresForRow(feature).length;
54710
+ }
54711
+ function draw(ctx, feature, row, stateModel, displayedRegionIndex) {
54712
+ for (let i = 0; i < getRowCount(feature); i++) {
54713
+ drawRow(ctx, feature, row + i, row, stateModel, displayedRegionIndex);
54714
+ }
54715
+ }
54716
+ function drawRow(ctx, topLevelFeature, row, topRow, stateModel, displayedRegionIndex) {
54717
+ const features = featuresForRow(topLevelFeature)[row - topRow];
54718
+ for (const feature of features) {
54719
+ drawFeature(ctx, feature, row, stateModel, displayedRegionIndex);
54720
+ }
54721
+ }
54722
+ function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
54723
+ const { apolloRowHeight: heightPx, lgv, session } = stateModel;
54724
+ const { bpPerPx, displayedRegions, offsetPx } = lgv;
54725
+ const displayedRegion = displayedRegions[displayedRegionIndex];
54726
+ const minX = (lgv.bpToPx({
54727
+ refName: displayedRegion.refName,
54728
+ coord: feature.min,
54729
+ regionNumber: displayedRegionIndex,
54730
+ })?.offsetPx ?? 0) - offsetPx;
54731
+ const { reversed } = displayedRegion;
54732
+ const { apolloSelectedFeature } = session;
54733
+ const widthPx = feature.length / bpPerPx;
54734
+ const startPx = reversed ? minX - widthPx : minX;
54735
+ const top = row * heightPx;
54736
+ const rowCount = getRowCount(feature);
54737
+ const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
54738
+ const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
54739
+ if (rowCount > 1) {
54740
+ // draw background that encapsulates all child features
54741
+ const featureHeight = rowCount * heightPx;
54742
+ drawBox(ctx, startPx, top, widthPx, featureHeight, groupingColor);
54743
+ }
54744
+ boxGlyph.draw(ctx, feature, row, stateModel, displayedRegionIndex);
54745
+ }
54746
+ function drawHover(stateModel, ctx) {
54747
+ const { apolloHover, apolloRowHeight, lgv } = stateModel;
54748
+ if (!apolloHover) {
54749
+ return;
54750
+ }
54751
+ const { feature } = apolloHover;
54752
+ const position = stateModel.getFeatureLayoutPosition(feature);
54753
+ if (!position) {
54754
+ return;
54755
+ }
54756
+ const { featureRow, layoutIndex, layoutRow } = position;
54757
+ const { bpPerPx, displayedRegions, offsetPx } = lgv;
54758
+ const displayedRegion = displayedRegions[layoutIndex];
54759
+ const { refName, reversed } = displayedRegion;
54760
+ const { length, max, min } = feature;
54761
+ const startPx = (lgv.bpToPx({
54762
+ refName,
54763
+ coord: reversed ? max : min,
54764
+ regionNumber: layoutIndex,
54765
+ })?.offsetPx ?? 0) - offsetPx;
54766
+ const top = (layoutRow + featureRow) * apolloRowHeight;
54767
+ const widthPx = length / bpPerPx;
54768
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
54769
+ ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature));
54770
+ }
54771
+ function getFeatureFromLayout(feature, bp, row) {
54772
+ const layoutRow = featuresForRow(feature)[row];
54773
+ return layoutRow.find((f) => bp >= f.min && bp <= f.max);
54774
+ }
54775
+ function getRowForFeature(feature, childFeature) {
54776
+ const rows = featuresForRow(feature);
54777
+ for (const [idx, row] of rows.entries()) {
54778
+ if (row.some((feature) => feature._id === childFeature._id)) {
54779
+ return idx;
54780
+ }
54781
+ }
54782
+ return;
54783
+ }
54784
+ // False positive here, none of these functions use "this"
54785
+ /* eslint-disable @typescript-eslint/unbound-method */
54786
+ const { drawDragPreview, drawTooltip, getContextMenuItems, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, } = boxGlyph;
54787
+ /* eslint-enable @typescript-eslint/unbound-method */
54788
+ const genericChildGlyph = {
54789
+ draw,
54790
+ drawDragPreview,
54791
+ drawHover,
54792
+ drawTooltip,
54793
+ getContextMenuItems,
54794
+ getFeatureFromLayout,
54795
+ getRowCount,
54796
+ getRowForFeature,
54797
+ onMouseDown,
54798
+ onMouseLeave,
54799
+ onMouseMove,
54800
+ onMouseUp,
54801
+ };
54802
+
54803
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
54804
+ function layoutsModelFactory(pluginManager, configSchema) {
54805
+ const BaseLinearApolloDisplay = baseModelFactory(pluginManager, configSchema);
54806
+ return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
54807
+ .props({
54808
+ featuresMinMaxLimit: 500_000,
54809
+ })
54810
+ .volatile(() => ({
54811
+ seenFeatures: mobx.observable.map(),
54812
+ }))
54813
+ .views((self) => ({
54814
+ get featuresMinMax() {
54815
+ const { assemblyManager } = self.session;
54816
+ return self.lgv.displayedRegions.map((region) => {
54817
+ const assembly = assemblyManager.get(region.assemblyName);
54818
+ let min;
54819
+ let max;
54820
+ const { end, refName, start } = region;
54821
+ for (const [, feature] of self.seenFeatures) {
54822
+ if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
54823
+ !require$$1$2.doesIntersect2(start, end, feature.min, feature.max) ||
54824
+ feature.length > self.featuresMinMaxLimit ||
54825
+ (self.filteredFeatureTypes.length > 0 &&
54826
+ !self.filteredFeatureTypes.includes(feature.type))) {
54827
+ continue;
54828
+ }
54829
+ if (min === undefined) {
54830
+ ({ min } = feature);
54831
+ }
54832
+ if (max === undefined) {
54833
+ ({ max } = feature);
54834
+ }
54835
+ if (feature.minWithChildren < min) {
54836
+ ({ min } = feature);
54837
+ }
54838
+ if (feature.maxWithChildren > max) {
54839
+ ({ max } = feature);
54840
+ }
54841
+ }
54842
+ if (min !== undefined && max !== undefined) {
54843
+ return [min, max];
54844
+ }
54845
+ return;
54846
+ });
54847
+ },
54848
+ getGlyph(feature) {
54849
+ if (this.looksLikeGene(feature)) {
54850
+ return geneGlyph;
54851
+ }
54852
+ if (feature.children?.size) {
54853
+ return genericChildGlyph;
54854
+ }
54855
+ return boxGlyph;
54856
+ },
54857
+ looksLikeGene(feature) {
54858
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
54859
+ if (!featureTypeOntology) {
54860
+ return false;
54861
+ }
54862
+ const { children } = feature;
54863
+ if (!children?.size) {
54864
+ return false;
54865
+ }
54866
+ const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene');
54867
+ if (!isGene) {
54868
+ return false;
54869
+ }
54870
+ for (const [, child] of children) {
54871
+ if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
54872
+ const { children: grandChildren } = child;
54873
+ if (!grandChildren?.size) {
54874
+ return false;
54875
+ }
54876
+ const hasCDS = [...grandChildren.values()].some((grandchild) => featureTypeOntology.isTypeOf(grandchild.type, 'CDS'));
54877
+ const hasExon = [...grandChildren.values()].some((grandchild) => featureTypeOntology.isTypeOf(grandchild.type, 'exon'));
54878
+ if (hasCDS && hasExon) {
54879
+ return true;
54880
+ }
54881
+ }
54882
+ }
54883
+ return false;
54884
+ },
54885
+ }))
54886
+ .actions((self) => ({
54887
+ addSeenFeature(feature) {
54888
+ self.seenFeatures.set(feature._id, feature);
54889
+ },
54890
+ deleteSeenFeature(featureId) {
54891
+ self.seenFeatures.delete(featureId);
54892
+ },
54893
+ }))
54894
+ .views((self) => ({
54895
+ get featureLayouts() {
54896
+ const { assemblyManager } = self.session;
54897
+ return self.lgv.displayedRegions.map((region, idx) => {
54898
+ const assembly = assemblyManager.get(region.assemblyName);
54899
+ const featureLayout = new Map();
54900
+ const minMax = self.featuresMinMax[idx];
54901
+ if (!minMax) {
54902
+ return featureLayout;
54903
+ }
54904
+ const [min, max] = minMax;
54905
+ const rows = [];
54906
+ const { end, refName, start } = region;
54907
+ for (const [id, feature] of self.seenFeatures.entries()) {
54908
+ if (!require$$1$3.isAlive(feature)) {
54909
+ self.deleteSeenFeature(id);
54910
+ continue;
54911
+ }
54912
+ if (refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
54913
+ !require$$1$2.doesIntersect2(start, end, feature.min, feature.max) ||
54914
+ (self.filteredFeatureTypes.length > 0 &&
54915
+ !self.filteredFeatureTypes.includes(feature.type))) {
54916
+ continue;
54917
+ }
54918
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
54919
+ if (!featureTypeOntology) {
54920
+ throw new Error('featureTypeOntology is undefined');
54921
+ }
54922
+ const rowCount = self
54923
+ .getGlyph(feature)
54924
+ .getRowCount(feature, featureTypeOntology, self.lgv.bpPerPx);
54925
+ let startingRow = 0;
54926
+ let placed = false;
54927
+ while (!placed) {
54928
+ let rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
54929
+ if (rowsForFeature.length < rowCount) {
54930
+ for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
54931
+ const newRowNumber = rows.length;
54932
+ rows[newRowNumber] = Array.from({ length: max - min });
54933
+ featureLayout.set(newRowNumber, []);
54934
+ }
54935
+ rowsForFeature = rows.slice(startingRow, startingRow + rowCount);
54936
+ }
54937
+ if (rowsForFeature
54938
+ .map((rowForFeature) => {
54939
+ // zero-length features are allowed in the spec
54940
+ const featureMax = feature.max - feature.min === 0
54941
+ ? feature.min + 1
54942
+ : feature.max;
54943
+ let start = feature.min - min, end = featureMax - min;
54944
+ if (feature.min - min < 0) {
54945
+ start = 0;
54946
+ end = featureMax - feature.min;
54947
+ }
54948
+ return rowForFeature.slice(start, end).some(Boolean);
54949
+ })
54950
+ .some(Boolean)) {
54951
+ startingRow += 1;
54952
+ continue;
54953
+ }
54954
+ for (let rowNum = startingRow; rowNum < startingRow + rowCount; rowNum++) {
54955
+ const row = rows[rowNum];
54956
+ let start = feature.min - min, end = feature.max - min;
54957
+ if (feature.min - min < 0) {
54958
+ start = 0;
54959
+ end = feature.max - feature.min;
54960
+ }
54961
+ row.fill(true, start, end);
54962
+ const layoutRow = featureLayout.get(rowNum);
54963
+ layoutRow?.push([rowNum - startingRow, feature]);
54964
+ }
54965
+ placed = true;
54966
+ }
54967
+ }
54968
+ return featureLayout;
54969
+ });
54970
+ },
54971
+ getFeatureLayoutPosition(feature) {
54972
+ const { featureLayouts } = this;
54973
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
54974
+ for (const [idx, layout] of featureLayouts.entries()) {
54975
+ for (const [layoutRowNum, layoutRow] of layout) {
54976
+ for (const [featureRowNum, layoutFeature] of layoutRow) {
54977
+ if (featureRowNum !== 0) {
54978
+ // Same top-level feature in all feature rows, so only need to
54979
+ // check the first one
54980
+ continue;
54981
+ }
54982
+ if (feature._id === layoutFeature._id) {
54983
+ return {
54984
+ layoutIndex: idx,
54985
+ layoutRow: layoutRowNum,
54986
+ featureRow: featureRowNum,
54987
+ };
54988
+ }
54989
+ if (layoutFeature.hasDescendant(feature._id)) {
54990
+ if (!featureTypeOntology) {
54991
+ throw new Error('featureTypeOntology is undefined');
54992
+ }
54993
+ const row = self
54994
+ .getGlyph(layoutFeature)
54995
+ .getRowForFeature(layoutFeature, feature, featureTypeOntology);
54996
+ if (row !== undefined) {
54997
+ return {
54998
+ layoutIndex: idx,
54999
+ layoutRow: layoutRowNum,
55000
+ featureRow: row,
55001
+ };
55002
+ }
55003
+ }
55004
+ }
55005
+ }
55006
+ }
55007
+ return;
55008
+ },
55009
+ }))
55010
+ .views((self) => ({
55011
+ get highestRow() {
55012
+ return Math.max(0, ...self.featureLayouts.map((layout) => Math.max(...layout.keys())));
55013
+ },
55014
+ }))
55015
+ .actions((self) => ({
55016
+ afterAttach() {
55017
+ require$$1$3.addDisposer(self, mobx.autorun(() => {
55018
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
55019
+ return;
55020
+ }
55021
+ for (const region of self.regions) {
55022
+ const assembly = self.session.apolloDataStore.assemblies.get(region.assemblyName);
55023
+ const ref = assembly?.getByRefName(region.refName);
55024
+ const features = ref?.features;
55025
+ if (!features) {
55026
+ continue;
55027
+ }
55028
+ for (const [, feature] of features) {
55029
+ if (require$$1$2.doesIntersect2(region.start, region.end, feature.min, feature.max) &&
55030
+ !self.seenFeatures.has(feature._id)) {
55031
+ self.addSeenFeature(feature);
55032
+ }
55033
+ }
55034
+ }
55035
+ }, { name: 'LinearApolloDisplaySetSeenFeatures', delay: 1000 }));
55036
+ },
55037
+ }));
55038
+ }
55039
+
55040
+ function renderingModelIntermediateFactory(pluginManager, configSchema) {
55041
+ const LinearApolloDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
55042
+ return LinearApolloDisplayLayouts.named('LinearApolloDisplayRendering')
55043
+ .props({
55044
+ sequenceRowHeight: 15,
55045
+ apolloRowHeight: 20,
55046
+ detailsMinHeight: 200,
55047
+ detailsHeight: 200,
55048
+ lastRowTooltipBufferHeight: 40,
55049
+ isShown: true,
55050
+ })
55051
+ .volatile(() => ({
55052
+ canvas: null,
55053
+ overlayCanvas: null,
55054
+ collaboratorCanvas: null,
55055
+ seqTrackCanvas: null,
55056
+ seqTrackOverlayCanvas: null,
55057
+ theme: undefined,
55058
+ }))
55059
+ .views((self) => ({
55060
+ get featuresHeight() {
55061
+ return ((self.highestRow + 1) * self.apolloRowHeight +
55062
+ self.lastRowTooltipBufferHeight);
55063
+ },
55064
+ }))
55065
+ .actions((self) => ({
55066
+ toggleShown() {
55067
+ self.isShown = !self.isShown;
55068
+ },
55069
+ setDetailsHeight(newHeight) {
55070
+ self.detailsHeight = self.isShown
55071
+ ? Math.max(Math.min(newHeight, self.height - 100), Math.min(self.height, self.detailsMinHeight))
55072
+ : newHeight;
55073
+ },
55074
+ setCanvas(canvas) {
55075
+ self.canvas = canvas;
55076
+ },
55077
+ setOverlayCanvas(canvas) {
55078
+ self.overlayCanvas = canvas;
55079
+ },
55080
+ setCollaboratorCanvas(canvas) {
55081
+ self.collaboratorCanvas = canvas;
55082
+ },
55083
+ setSeqTrackCanvas(canvas) {
55084
+ self.seqTrackCanvas = canvas;
55085
+ },
55086
+ setSeqTrackOverlayCanvas(canvas) {
55087
+ self.seqTrackOverlayCanvas = canvas;
55088
+ },
55089
+ setTheme(theme) {
55090
+ self.theme = theme;
55091
+ },
55092
+ afterAttach() {
55093
+ require$$1$3.addDisposer(self, mobx.autorun(() => {
55094
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
55095
+ return;
55096
+ }
55097
+ const ctx = self.collaboratorCanvas?.getContext('2d');
55098
+ if (!ctx) {
55099
+ return;
55100
+ }
55101
+ ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
55102
+ for (const collaborator of self.session.collaborators) {
55103
+ const { locations } = collaborator;
55104
+ if (locations.length === 0) {
55105
+ continue;
55106
+ }
55107
+ let idx = 0;
55108
+ for (const displayedRegion of self.lgv.displayedRegions) {
55109
+ for (const location of locations) {
55110
+ if (location.refSeq !== displayedRegion.refName) {
55111
+ continue;
55112
+ }
55113
+ const { end, refSeq, start } = location;
55114
+ const locationStartPxInfo = self.lgv.bpToPx({
55115
+ refName: refSeq,
55116
+ coord: start,
55117
+ regionNumber: idx,
55118
+ });
55119
+ if (!locationStartPxInfo) {
55120
+ continue;
55121
+ }
55122
+ const locationStartPx = locationStartPxInfo.offsetPx - self.lgv.offsetPx;
55123
+ const locationWidthPx = (end - start) / self.lgv.bpPerPx;
55124
+ ctx.fillStyle = 'rgba(0,255,0,.2)';
55125
+ ctx.fillRect(locationStartPx, 1, locationWidthPx, 100);
55126
+ ctx.fillStyle = 'black';
55127
+ ctx.fillText(collaborator.name, locationStartPx + 1, 11, locationWidthPx - 2);
55128
+ }
55129
+ idx++;
55130
+ }
55131
+ }
55132
+ }, { name: 'LinearApolloDisplayRenderCollaborators' }));
55133
+ },
55134
+ }));
55135
+ }
55136
+ function colorCode(letter, theme) {
55137
+ return (theme?.palette.bases[letter.toUpperCase()].main.toString() ?? 'lightgray');
54359
55138
  }
54360
- function onMouseDown$1(stateModel, currentMousePosition, event) {
54361
- const { featureAndGlyphUnderMouse } = currentMousePosition;
54362
- // swallow the mouseDown if we are on the edge of the feature so that we
54363
- // don't start dragging the view if we try to drag the feature edge
54364
- const { feature } = featureAndGlyphUnderMouse;
54365
- const draggableFeature = getDraggableFeatureInfo(currentMousePosition, feature, stateModel);
54366
- if (draggableFeature) {
54367
- event.stopPropagation();
54368
- stateModel.startDrag(currentMousePosition, draggableFeature.feature, draggableFeature.edge);
54369
- }
55139
+ function codonColorCode(letter) {
55140
+ const colorMap = {
55141
+ M: '#33ee33',
55142
+ '*': '#f44336',
55143
+ };
55144
+ return colorMap[letter.toUpperCase()];
54370
55145
  }
54371
- function onMouseMove$1(stateModel, mousePosition) {
54372
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
54373
- const { featureAndGlyphUnderMouse } = mousePosition;
54374
- stateModel.setApolloHover(featureAndGlyphUnderMouse);
54375
- const { feature } = featureAndGlyphUnderMouse;
54376
- const draggableFeature = getDraggableFeatureInfo(mousePosition, feature, stateModel);
54377
- if (draggableFeature) {
54378
- stateModel.setCursor('col-resize');
54379
- return;
54380
- }
54381
- }
54382
- stateModel.setCursor();
55146
+ function reverseCodonSeq(seq) {
55147
+ return [...seq]
55148
+ .map((c) => require$$1$2.revcom(c))
55149
+ .reverse()
55150
+ .join('');
54383
55151
  }
54384
- function onMouseUp$1(stateModel, mousePosition) {
54385
- if (stateModel.apolloDragging) {
54386
- return;
54387
- }
54388
- const { featureAndGlyphUnderMouse } = mousePosition;
54389
- if (featureAndGlyphUnderMouse?.feature) {
54390
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature);
54391
- }
55152
+ function drawLetter(seqTrackctx, startPx, widthPx, letter, textY) {
55153
+ const fontSize = Math.min(widthPx, 10);
55154
+ seqTrackctx.fillStyle = '#000';
55155
+ seqTrackctx.font = `${fontSize}px`;
55156
+ const textWidth = seqTrackctx.measureText(letter).width;
55157
+ const textX = startPx + (widthPx - textWidth) / 2;
55158
+ seqTrackctx.fillText(letter, textX, textY + 10);
54392
55159
  }
54393
- function getDraggableFeatureInfo(mousePosition, feature, stateModel) {
54394
- if (feature.type === 'gene' || feature.type === 'mRNA') {
54395
- return;
54396
- }
54397
- const { bp, refName, regionNumber, x } = mousePosition;
54398
- const { lgv } = stateModel;
54399
- const { offsetPx } = lgv;
54400
- const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber });
54401
- const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber });
54402
- if (minPxInfo === undefined || maxPxInfo === undefined) {
54403
- return;
55160
+ function drawTranslation(seqTrackctx, bpPerPx, trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight, seq, i, reverse) {
55161
+ let codonSeq = seq.slice(i, i + 3).toUpperCase();
55162
+ if (reverse) {
55163
+ codonSeq = reverseCodonSeq(codonSeq);
54404
55164
  }
54405
- const minPx = minPxInfo.offsetPx - offsetPx;
54406
- const maxPx = maxPxInfo.offsetPx - offsetPx;
54407
- if (Math.abs(maxPx - minPx) < 8) {
55165
+ const codonLetter = require$$1$2.defaultCodonTable[codonSeq];
55166
+ if (!codonLetter) {
54408
55167
  return;
54409
55168
  }
54410
- if (Math.abs(minPx - x) < 4) {
54411
- return { feature, edge: 'min' };
55169
+ const fillColor = codonColorCode(codonLetter);
55170
+ if (fillColor) {
55171
+ seqTrackctx.fillStyle = fillColor;
55172
+ seqTrackctx.fillRect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
54412
55173
  }
54413
- if (Math.abs(maxPx - x) < 4) {
54414
- return { feature, edge: 'max' };
55174
+ if (bpPerPx <= 0.1) {
55175
+ seqTrackctx.rect(trnslStartPx, trnslY, trnslWidthPx, sequenceRowHeight);
55176
+ seqTrackctx.stroke();
55177
+ drawLetter(seqTrackctx, trnslStartPx, trnslWidthPx, codonLetter, trnslY);
54415
55178
  }
54416
- if (feature.type === 'CDS') {
54417
- const mRNA = feature.parent;
54418
- if (!mRNA?.children) {
54419
- return;
54420
- }
54421
- const exonChildren = [...mRNA.children.values()].filter((child) => child.type === 'exon');
54422
- const overlappingExon = exonChildren.find((child) => {
54423
- const [start, end] = require$$1$2.intersection2(bp, bp + 1, child.min, child.max);
54424
- return start !== undefined && end !== undefined;
54425
- });
54426
- if (!overlappingExon) {
54427
- return;
55179
+ }
55180
+ function sequenceRenderingModelFactory(pluginManager, configSchema) {
55181
+ const LinearApolloDisplayRendering = renderingModelIntermediateFactory(pluginManager, configSchema);
55182
+ return LinearApolloDisplayRendering.actions((self) => ({
55183
+ afterAttach() {
55184
+ require$$1$3.addDisposer(self, mobx.autorun(async () => {
55185
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
55186
+ return;
55187
+ }
55188
+ if (self.lgv.bpPerPx > 3) {
55189
+ return;
55190
+ }
55191
+ const seqTrackctx = self.seqTrackCanvas?.getContext('2d');
55192
+ if (!seqTrackctx) {
55193
+ return;
55194
+ }
55195
+ seqTrackctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
55196
+ const frames = self.lgv.bpPerPx <= 1
55197
+ ? [3, 2, 1, 0, 0, -1, -2, -3]
55198
+ : [3, 2, 1, -1, -2, -3];
55199
+ let height = 0;
55200
+ for (const frame of frames) {
55201
+ const frameColor = self.theme?.palette.framesCDS.at(frame)?.main;
55202
+ if (frameColor) {
55203
+ seqTrackctx.fillStyle = frameColor;
55204
+ seqTrackctx.fillRect(0, height, self.lgv.dynamicBlocks.totalWidthPx, self.sequenceRowHeight);
55205
+ }
55206
+ height += self.sequenceRowHeight;
55207
+ }
55208
+ for (const [idx, region] of self.regions.entries()) {
55209
+ const driver = self.session.apolloDataStore.getBackendDriver(region.assemblyName);
55210
+ if (!driver) {
55211
+ throw new Error('Failed to get the backend driver');
55212
+ }
55213
+ const { seq } = await driver.getSequence(region);
55214
+ if (!seq) {
55215
+ return;
55216
+ }
55217
+ for (const [i, letter] of [...seq].entries()) {
55218
+ const trnslXOffset = (self.lgv.bpToPx({
55219
+ refName: region.refName,
55220
+ coord: region.start + i,
55221
+ regionNumber: idx,
55222
+ })?.offsetPx ?? 0) - self.lgv.offsetPx;
55223
+ const trnslWidthPx = 3 / self.lgv.bpPerPx;
55224
+ const trnslStartPx = self.lgv.displayedRegions[idx].reversed
55225
+ ? trnslXOffset - trnslWidthPx
55226
+ : trnslXOffset;
55227
+ // Draw translation forward
55228
+ for (let j = 2; j >= 0; j--) {
55229
+ if ((region.start + i) % 3 === j) {
55230
+ drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (2 - j), trnslWidthPx, self.sequenceRowHeight, seq, i, false);
55231
+ }
55232
+ }
55233
+ if (self.lgv.bpPerPx <= 1) {
55234
+ const xOffset = (self.lgv.bpToPx({
55235
+ refName: region.refName,
55236
+ coord: region.start + i,
55237
+ regionNumber: idx,
55238
+ })?.offsetPx ?? 0) - self.lgv.offsetPx;
55239
+ const widthPx = 1 / self.lgv.bpPerPx;
55240
+ const startPx = self.lgv.displayedRegions[idx].reversed
55241
+ ? xOffset - widthPx
55242
+ : xOffset;
55243
+ // Draw forward
55244
+ seqTrackctx.beginPath();
55245
+ seqTrackctx.fillStyle = colorCode(letter, self.theme);
55246
+ seqTrackctx.rect(startPx, self.sequenceRowHeight * 3, widthPx, self.sequenceRowHeight);
55247
+ seqTrackctx.fill();
55248
+ if (self.lgv.bpPerPx <= 0.1) {
55249
+ seqTrackctx.stroke();
55250
+ drawLetter(seqTrackctx, startPx, widthPx, letter, self.sequenceRowHeight * 3);
55251
+ }
55252
+ // Draw reverse
55253
+ const revLetter = require$$1$2.revcom(letter);
55254
+ seqTrackctx.beginPath();
55255
+ seqTrackctx.fillStyle = colorCode(revLetter, self.theme);
55256
+ seqTrackctx.rect(startPx, self.sequenceRowHeight * 4, widthPx, self.sequenceRowHeight);
55257
+ seqTrackctx.fill();
55258
+ if (self.lgv.bpPerPx <= 0.1) {
55259
+ seqTrackctx.stroke();
55260
+ drawLetter(seqTrackctx, startPx, widthPx, revLetter, self.sequenceRowHeight * 4);
55261
+ }
55262
+ }
55263
+ // Draw translation reverse
55264
+ for (let k = 0; k <= 2; k++) {
55265
+ const rowOffset = self.lgv.bpPerPx <= 1 ? 5 : 3;
55266
+ if ((region.start + i) % 3 === k) {
55267
+ drawTranslation(seqTrackctx, self.lgv.bpPerPx, trnslStartPx, self.sequenceRowHeight * (rowOffset + k), trnslWidthPx, self.sequenceRowHeight, seq, i, true);
55268
+ }
55269
+ }
55270
+ }
55271
+ }
55272
+ }, { name: 'LinearApolloDisplayRenderSequence' }));
55273
+ },
55274
+ }));
55275
+ }
55276
+ function renderingModelFactory(pluginManager, configSchema) {
55277
+ const LinearApolloDisplayRendering = sequenceRenderingModelFactory(pluginManager, configSchema);
55278
+ return LinearApolloDisplayRendering.actions((self) => ({
55279
+ afterAttach() {
55280
+ require$$1$3.addDisposer(self, mobx.autorun(() => {
55281
+ const { canvas, featureLayouts, featuresHeight, lgv } = self;
55282
+ if (!lgv.initialized || self.regionCannotBeRendered()) {
55283
+ return;
55284
+ }
55285
+ const { displayedRegions, dynamicBlocks } = lgv;
55286
+ const ctx = canvas?.getContext('2d');
55287
+ if (!ctx) {
55288
+ return;
55289
+ }
55290
+ ctx.clearRect(0, 0, dynamicBlocks.totalWidthPx, featuresHeight);
55291
+ for (const [idx, featureLayout] of featureLayouts.entries()) {
55292
+ const displayedRegion = displayedRegions[idx];
55293
+ for (const [row, featureLayoutRow] of featureLayout.entries()) {
55294
+ for (const [featureRow, feature] of featureLayoutRow) {
55295
+ if (featureRow > 0) {
55296
+ continue;
55297
+ }
55298
+ if (!require$$1$2.doesIntersect2(displayedRegion.start, displayedRegion.end, feature.min, feature.max)) {
55299
+ continue;
55300
+ }
55301
+ self.getGlyph(feature).draw(ctx, feature, row, self, idx);
55302
+ }
55303
+ }
55304
+ }
55305
+ }, { name: 'LinearApolloDisplayRenderFeatures' }));
55306
+ },
55307
+ }));
55308
+ }
55309
+
55310
+ function isMousePositionWithFeatureAndGlyph(mousePosition) {
55311
+ return 'featureAndGlyphUnderMouse' in mousePosition;
55312
+ }
55313
+ function getMousePosition(event, lgv) {
55314
+ const canvas = event.currentTarget;
55315
+ const { clientX, clientY } = event;
55316
+ const { left, top } = canvas.getBoundingClientRect();
55317
+ const x = clientX - left;
55318
+ const y = clientY - top;
55319
+ const { coord: bp, index: regionNumber, refName } = lgv.pxToBp(x);
55320
+ return { x, y, refName, bp, regionNumber };
55321
+ }
55322
+ function getTranslationRow(frame, bpPerPx) {
55323
+ const offset = bpPerPx <= 1 ? 2 : 0;
55324
+ switch (frame) {
55325
+ case 3: {
55326
+ return 0;
54428
55327
  }
54429
- const minPxInfo = lgv.bpToPx({
54430
- refName,
54431
- coord: overlappingExon.min,
54432
- regionNumber,
54433
- });
54434
- const maxPxInfo = lgv.bpToPx({
54435
- refName,
54436
- coord: overlappingExon.max,
54437
- regionNumber,
54438
- });
54439
- if (minPxInfo === undefined || maxPxInfo === undefined) {
54440
- return;
55328
+ case 2: {
55329
+ return 1;
54441
55330
  }
54442
- const minPx = minPxInfo.offsetPx - offsetPx;
54443
- const maxPx = maxPxInfo.offsetPx - offsetPx;
54444
- if (Math.abs(maxPx - minPx) < 8) {
54445
- return;
55331
+ case 1: {
55332
+ return 2;
54446
55333
  }
54447
- if (Math.abs(minPx - x) < 4) {
54448
- return { feature: overlappingExon, edge: 'min' };
55334
+ case -1: {
55335
+ return 3 + offset;
54449
55336
  }
54450
- if (Math.abs(maxPx - x) < 4) {
54451
- return { feature: overlappingExon, edge: 'max' };
55337
+ case -2: {
55338
+ return 4 + offset;
54452
55339
  }
54453
- }
54454
- return;
54455
- }
54456
- // False positive here, none of these functions use "this"
54457
- /* eslint-disable @typescript-eslint/unbound-method */
54458
- const { drawTooltip: drawTooltip$1, getContextMenuItems: getContextMenuItems$1, onMouseLeave: onMouseLeave$1 } = boxGlyph;
54459
- /* eslint-enable @typescript-eslint/unbound-method */
54460
- const geneGlyph = {
54461
- draw: draw$1,
54462
- drawDragPreview: drawDragPreview$1,
54463
- drawHover: drawHover$1,
54464
- drawTooltip: drawTooltip$1,
54465
- getContextMenuItems: getContextMenuItems$1,
54466
- getFeatureFromLayout: getFeatureFromLayout$1,
54467
- getRowCount: getRowCount$1,
54468
- getRowForFeature: getRowForFeature$1,
54469
- onMouseDown: onMouseDown$1,
54470
- onMouseLeave: onMouseLeave$1,
54471
- onMouseMove: onMouseMove$1,
54472
- onMouseUp: onMouseUp$1,
54473
- };
54474
-
54475
- function featuresForRow(feature) {
54476
- const features = [[feature]];
54477
- if (feature.children) {
54478
- for (const [, child] of feature.children) {
54479
- features.push(...featuresForRow(child));
55340
+ case -3: {
55341
+ return 5 + offset;
54480
55342
  }
54481
55343
  }
54482
- return features;
54483
- }
54484
- function getRowCount(feature) {
54485
- return featuresForRow(feature).length;
54486
- }
54487
- function draw(ctx, feature, row, stateModel, displayedRegionIndex) {
54488
- for (let i = 0; i < getRowCount(feature); i++) {
54489
- drawRow(ctx, feature, row + i, row, stateModel, displayedRegionIndex);
54490
- }
54491
- }
54492
- function drawRow(ctx, topLevelFeature, row, topRow, stateModel, displayedRegionIndex) {
54493
- const features = featuresForRow(topLevelFeature)[row - topRow];
54494
- for (const feature of features) {
54495
- drawFeature(ctx, feature, row, stateModel, displayedRegionIndex);
54496
- }
54497
- }
54498
- function drawFeature(ctx, feature, row, stateModel, displayedRegionIndex) {
54499
- const { apolloRowHeight: heightPx, lgv, session } = stateModel;
54500
- const { bpPerPx, displayedRegions, offsetPx } = lgv;
54501
- const displayedRegion = displayedRegions[displayedRegionIndex];
54502
- const minX = (lgv.bpToPx({
54503
- refName: displayedRegion.refName,
54504
- coord: feature.min,
54505
- regionNumber: displayedRegionIndex,
54506
- })?.offsetPx ?? 0) - offsetPx;
54507
- const { reversed } = displayedRegion;
54508
- const { apolloSelectedFeature } = session;
54509
- const widthPx = feature.length / bpPerPx;
54510
- const startPx = reversed ? minX - widthPx : minX;
54511
- const top = row * heightPx;
54512
- const rowCount = getRowCount(feature);
54513
- const isSelected = isSelectedFeature(feature, apolloSelectedFeature);
54514
- const groupingColor = isSelected ? 'rgba(130,0,0,0.45)' : 'rgba(255,0,0,0.25)';
54515
- if (rowCount > 1) {
54516
- // draw background that encapsulates all child features
54517
- const featureHeight = rowCount * heightPx;
54518
- drawBox(ctx, startPx, top, widthPx, featureHeight, groupingColor);
54519
- }
54520
- boxGlyph.draw(ctx, feature, row, stateModel, displayedRegionIndex);
54521
55344
  }
54522
- function drawHover(stateModel, ctx) {
54523
- const { apolloHover, apolloRowHeight, lgv } = stateModel;
54524
- if (!apolloHover) {
54525
- return;
54526
- }
54527
- const { feature } = apolloHover;
54528
- const position = stateModel.getFeatureLayoutPosition(feature);
54529
- if (!position) {
55345
+ function getSeqRow(strand, bpPerPx) {
55346
+ if (bpPerPx > 1 || strand === undefined) {
54530
55347
  return;
54531
55348
  }
54532
- const { featureRow, layoutIndex, layoutRow } = position;
54533
- const { bpPerPx, displayedRegions, offsetPx } = lgv;
54534
- const displayedRegion = displayedRegions[layoutIndex];
54535
- const { refName, reversed } = displayedRegion;
54536
- const { length, max, min } = feature;
54537
- const startPx = (lgv.bpToPx({
54538
- refName,
54539
- coord: reversed ? max : min,
54540
- regionNumber: layoutIndex,
54541
- })?.offsetPx ?? 0) - offsetPx;
54542
- const top = (layoutRow + featureRow) * apolloRowHeight;
54543
- const widthPx = length / bpPerPx;
54544
- ctx.fillStyle = 'rgba(0,0,0,0.2)';
54545
- ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature));
54546
- }
54547
- function getFeatureFromLayout(feature, bp, row) {
54548
- const layoutRow = featuresForRow(feature)[row];
54549
- return layoutRow.find((f) => bp >= f.min && bp <= f.max);
55349
+ return strand === 1 ? 3 : 4;
54550
55350
  }
54551
- function getRowForFeature(feature, childFeature) {
54552
- const rows = featuresForRow(feature);
54553
- for (const [idx, row] of rows.entries()) {
54554
- if (row.some((feature) => feature._id === childFeature._id)) {
54555
- return idx;
54556
- }
55351
+ function highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx) {
55352
+ if (row !== undefined) {
55353
+ seqTrackOverlayctx.fillStyle =
55354
+ theme?.palette.action.focus ?? 'rgba(0,0,0,0.04)';
55355
+ seqTrackOverlayctx.fillRect(startPx, sequenceRowHeight * row, widthPx, sequenceRowHeight);
54557
55356
  }
54558
- return;
54559
55357
  }
54560
- // False positive here, none of these functions use "this"
54561
- /* eslint-disable @typescript-eslint/unbound-method */
54562
- const { drawDragPreview, drawTooltip, getContextMenuItems, onMouseDown, onMouseLeave, onMouseMove, onMouseUp, } = boxGlyph;
54563
- /* eslint-enable @typescript-eslint/unbound-method */
54564
- const genericChildGlyph = {
54565
- draw,
54566
- drawDragPreview,
54567
- drawHover,
54568
- drawTooltip,
54569
- getContextMenuItems,
54570
- getFeatureFromLayout,
54571
- getRowCount,
54572
- getRowForFeature,
54573
- onMouseDown,
54574
- onMouseLeave,
54575
- onMouseMove,
54576
- onMouseUp,
54577
- };
54578
-
54579
- /** get the appropriate glyph for the given top-level feature */
54580
- function getGlyph(feature) {
54581
- if (looksLikeGene(feature)) {
54582
- return geneGlyph;
54583
- }
54584
- if (feature.children?.size) {
54585
- return genericChildGlyph;
54586
- }
54587
- return boxGlyph;
55358
+ function mouseEventsModelIntermediateFactory(pluginManager, configSchema) {
55359
+ const LinearApolloDisplayRendering = renderingModelFactory(pluginManager, configSchema);
55360
+ return LinearApolloDisplayRendering.named('LinearApolloDisplayMouseEvents')
55361
+ .volatile(() => ({
55362
+ apolloDragging: null,
55363
+ cursor: undefined,
55364
+ apolloHover: undefined,
55365
+ }))
55366
+ .views((self) => ({
55367
+ getMousePosition(event) {
55368
+ const mousePosition = getMousePosition(event, self.lgv);
55369
+ const { bp, regionNumber, y } = mousePosition;
55370
+ const row = Math.floor(y / self.apolloRowHeight);
55371
+ const featureLayout = self.featureLayouts[regionNumber];
55372
+ const layoutRow = featureLayout.get(row);
55373
+ if (!layoutRow) {
55374
+ return mousePosition;
55375
+ }
55376
+ const foundFeature = layoutRow.find((f) => bp >= f[1].min && bp <= f[1].max);
55377
+ if (!foundFeature) {
55378
+ return mousePosition;
55379
+ }
55380
+ const [featureRow, topLevelFeature] = foundFeature;
55381
+ const glyph = self.getGlyph(topLevelFeature);
55382
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
55383
+ if (!featureTypeOntology) {
55384
+ throw new Error('featureTypeOntology is undefined');
55385
+ }
55386
+ const feature = glyph.getFeatureFromLayout(topLevelFeature, bp, featureRow, featureTypeOntology);
55387
+ if (!feature) {
55388
+ return mousePosition;
55389
+ }
55390
+ return {
55391
+ ...mousePosition,
55392
+ featureAndGlyphUnderMouse: { feature, topLevelFeature, glyph },
55393
+ };
55394
+ },
55395
+ }))
55396
+ .actions((self) => ({
55397
+ continueDrag(mousePosition, event) {
55398
+ if (!self.apolloDragging) {
55399
+ throw new Error('continueDrag() called with no current drag in progress');
55400
+ }
55401
+ event.stopPropagation();
55402
+ self.apolloDragging = { ...self.apolloDragging, current: mousePosition };
55403
+ },
55404
+ setDragging(dragInfo) {
55405
+ self.apolloDragging = dragInfo ?? null;
55406
+ },
55407
+ }))
55408
+ .actions((self) => ({
55409
+ setApolloHover(n) {
55410
+ self.apolloHover = n;
55411
+ },
55412
+ setCursor(cursor) {
55413
+ if (self.cursor !== cursor) {
55414
+ self.cursor = cursor;
55415
+ }
55416
+ },
55417
+ }))
55418
+ .actions(() => ({
55419
+ // onClick(event: CanvasMouseEvent) {
55420
+ onClick() {
55421
+ // TODO: set the selected feature
55422
+ },
55423
+ }));
55424
+ }
55425
+ function mouseEventsSeqHightlightModelFactory(pluginManager, configSchema) {
55426
+ const LinearApolloDisplayRendering = mouseEventsModelIntermediateFactory(pluginManager, configSchema);
55427
+ return LinearApolloDisplayRendering.actions((self) => ({
55428
+ afterAttach() {
55429
+ require$$1$3.addDisposer(self, mobx.autorun(() => {
55430
+ // This type is wrong in @jbrowse/core
55431
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
55432
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
55433
+ return;
55434
+ }
55435
+ const seqTrackOverlayctx = self.seqTrackOverlayCanvas?.getContext('2d');
55436
+ if (!seqTrackOverlayctx) {
55437
+ return;
55438
+ }
55439
+ seqTrackOverlayctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.lgv.bpPerPx <= 1 ? 125 : 95);
55440
+ const { apolloHover, lgv, regions, sequenceRowHeight, session, theme, } = self;
55441
+ if (!apolloHover) {
55442
+ return;
55443
+ }
55444
+ const { feature } = apolloHover;
55445
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager;
55446
+ if (!featureTypeOntology) {
55447
+ throw new Error('featureTypeOntology is undefined');
55448
+ }
55449
+ for (const [idx, region] of regions.entries()) {
55450
+ if (featureTypeOntology.isTypeOf(feature.type, 'CDS')) {
55451
+ const parentFeature = feature.parent;
55452
+ if (!parentFeature) {
55453
+ continue;
55454
+ }
55455
+ const cdsLocs = parentFeature.cdsLocations.find((loc) => feature.min === loc.at(0)?.min &&
55456
+ feature.max === loc.at(-1)?.max);
55457
+ if (!cdsLocs) {
55458
+ continue;
55459
+ }
55460
+ for (const dl of cdsLocs) {
55461
+ const frame = require$$1$2.getFrame(dl.min, dl.max, feature.strand ?? 1, dl.phase);
55462
+ const row = getTranslationRow(frame, lgv.bpPerPx);
55463
+ const offset = (lgv.bpToPx({
55464
+ refName: region.refName,
55465
+ coord: dl.min,
55466
+ regionNumber: idx,
55467
+ })?.offsetPx ?? 0) - lgv.offsetPx;
55468
+ const widthPx = (dl.max - dl.min) / lgv.bpPerPx;
55469
+ const startPx = lgv.displayedRegions[idx].reversed
55470
+ ? offset - widthPx
55471
+ : offset;
55472
+ highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
55473
+ }
55474
+ }
55475
+ else {
55476
+ const row = getSeqRow(feature.strand, lgv.bpPerPx);
55477
+ const offset = (lgv.bpToPx({
55478
+ refName: region.refName,
55479
+ coord: feature.min,
55480
+ regionNumber: idx,
55481
+ })?.offsetPx ?? 0) - lgv.offsetPx;
55482
+ const widthPx = feature.length / lgv.bpPerPx;
55483
+ const startPx = lgv.displayedRegions[idx].reversed
55484
+ ? offset - widthPx
55485
+ : offset;
55486
+ highlightSeq(seqTrackOverlayctx, theme, startPx, sequenceRowHeight, row, widthPx);
55487
+ }
55488
+ }
55489
+ }, { name: 'LinearApolloDisplayRenderSeqHighlight' }));
55490
+ },
55491
+ }));
54588
55492
  }
54589
- function looksLikeGene(feature) {
54590
- const { children } = feature;
54591
- if (!children?.size) {
54592
- return false;
54593
- }
54594
- for (const [, child] of children) {
54595
- if (child.type === 'mRNA') {
54596
- const { children: grandChildren } = child;
54597
- if (!grandChildren?.size) {
54598
- return false;
54599
- }
54600
- const hasCDS = [...grandChildren.values()].some((grandchild) => grandchild.type === 'CDS');
54601
- const hasExon = [...grandChildren.values()].some((grandchild) => grandchild.type === 'exon');
54602
- if (hasCDS && hasExon) {
54603
- return true;
55493
+ function mouseEventsModelFactory(pluginManager, configSchema) {
55494
+ const LinearApolloDisplayMouseEvents = mouseEventsSeqHightlightModelFactory(pluginManager, configSchema);
55495
+ return LinearApolloDisplayMouseEvents.views((self) => ({
55496
+ contextMenuItems(contextCoord) {
55497
+ const { apolloHover } = self;
55498
+ if (!(apolloHover && contextCoord)) {
55499
+ return [];
54604
55500
  }
54605
- }
54606
- }
54607
- return false;
54608
- }
54609
-
54610
- /* eslint-disable @typescript-eslint/use-unknown-in-catch-callback-variable */
54611
- const useStyles$4 = mui.makeStyles()((theme) => ({
54612
- typeContent: {
54613
- display: 'inline-block',
54614
- width: '174px',
54615
- height: '100%',
54616
- cursor: 'text',
54617
- },
54618
- feature: {
54619
- td: {
54620
- position: 'relative',
54621
- verticalAlign: 'top',
54622
- paddingLeft: '0.5em',
55501
+ const { topLevelFeature } = apolloHover;
55502
+ const glyph = self.getGlyph(topLevelFeature);
55503
+ return glyph.getContextMenuItems(self);
54623
55504
  },
54624
- },
54625
- arrow: {
54626
- display: 'inline-block',
54627
- width: '1.6em',
54628
- textAlign: 'center',
54629
- cursor: 'pointer',
54630
- },
54631
- arrowExpanded: {
54632
- transform: 'rotate(90deg)',
54633
- },
54634
- hoveredFeature: {
54635
- backgroundColor: theme.palette.action.hover,
54636
- },
54637
- typeInputElement: {
54638
- border: 'none',
54639
- background: 'none',
54640
- },
54641
- typeErrorMessage: {
54642
- color: 'red',
54643
- },
54644
- }));
54645
- function makeContextMenuItems(display, feature) {
54646
- const { changeManager, getAssemblyId, regions, selectedFeature, session, setSelectedFeature, } = display;
54647
- return featureContextMenuItems(feature, regions[0], getAssemblyId, selectedFeature, setSelectedFeature, session, changeManager);
54648
- }
54649
- function getTopLevelFeature(feature) {
54650
- let cur = feature;
54651
- while (cur.parent) {
54652
- cur = cur.parent;
54653
- }
54654
- return cur;
54655
- }
54656
- const Feature = mobxReact.observer(function Feature({ depth, feature, isHovered, isSelected, model: displayState, selectedFeatureClass, setContextMenu, }) {
54657
- const { classes } = useStyles$4();
54658
- const { apolloHover, changeManager, selectedFeature, session, tabularEditor: tabularEditorState, } = displayState;
54659
- const { featureCollapsed, filterText } = tabularEditorState;
54660
- const { _id, children, max, min, strand, type } = feature;
54661
- const expanded = !featureCollapsed.get(_id);
54662
- const toggleExpanded = (e) => {
54663
- e.stopPropagation();
54664
- tabularEditorState.setFeatureCollapsed(_id, expanded);
54665
- };
54666
- // pop up a snackbar in the session notifying user of an error
54667
- const notifyError = (e) => {
54668
- session.notify(e.message, 'error');
54669
- };
54670
- return (React__default["default"].createElement(React__default["default"].Fragment, null,
54671
- React__default["default"].createElement("tr", { onMouseEnter: (_e) => {
54672
- displayState.setApolloHover({
54673
- feature,
54674
- topLevelFeature: getTopLevelFeature(feature),
54675
- glyph: getGlyph(getTopLevelFeature(feature)),
55505
+ }))
55506
+ .actions((self) => ({
55507
+ // explicitly pass in a feature in case it's not the same as the one in
55508
+ // mousePosition (e.g. if features are drawn overlapping).
55509
+ startDrag(mousePosition, feature, edge) {
55510
+ self.apolloDragging = {
55511
+ start: mousePosition,
55512
+ current: mousePosition,
55513
+ feature,
55514
+ edge,
55515
+ };
55516
+ },
55517
+ endDrag() {
55518
+ if (!self.apolloDragging) {
55519
+ throw new Error('endDrag() called with no current drag in progress');
55520
+ }
55521
+ const { current, edge, feature, start } = self.apolloDragging;
55522
+ // don't do anything if it was only dragged a tiny bit
55523
+ if (Math.abs(current.x - start.x) <= 4) {
55524
+ self.setDragging();
55525
+ self.setCursor();
55526
+ return;
55527
+ }
55528
+ const { displayedRegions } = self.lgv;
55529
+ const region = displayedRegions[start.regionNumber];
55530
+ const assembly = self.getAssemblyId(region.assemblyName);
55531
+ let change;
55532
+ if (edge === 'max') {
55533
+ const featureId = feature._id;
55534
+ const oldEnd = feature.max;
55535
+ const newEnd = current.bp;
55536
+ change = new dist$2.LocationEndChange({
55537
+ typeName: 'LocationEndChange',
55538
+ changedIds: [featureId],
55539
+ featureId,
55540
+ oldEnd,
55541
+ newEnd,
55542
+ assembly,
54676
55543
  });
54677
- }, className: classes.feature +
54678
- (isSelected
54679
- ? ` ${selectedFeatureClass}`
54680
- : isHovered
54681
- ? ` ${classes.hoveredFeature}`
54682
- : ''), onClick: (e) => {
54683
- e.stopPropagation();
54684
- displayState.setSelectedFeature(feature);
54685
- }, onContextMenu: (e) => {
54686
- e.preventDefault();
54687
- setContextMenu({
54688
- position: { left: e.clientX + 2, top: e.clientY - 6 },
54689
- items: makeContextMenuItems(displayState, feature),
55544
+ }
55545
+ else {
55546
+ const featureId = feature._id;
55547
+ const oldStart = feature.min;
55548
+ const newStart = current.bp;
55549
+ change = new dist$2.LocationStartChange({
55550
+ typeName: 'LocationStartChange',
55551
+ changedIds: [featureId],
55552
+ featureId,
55553
+ oldStart,
55554
+ newStart,
55555
+ assembly,
54690
55556
  });
54691
- return false;
54692
- } },
54693
- React__default["default"].createElement("td", { style: {
54694
- whiteSpace: 'nowrap',
54695
- borderLeft: `${depth * 2}em solid transparent`,
54696
- } },
54697
- children?.size ? (
54698
- // TODO: a11y
54699
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
54700
- React__default["default"].createElement("div", { onClick: toggleExpanded, className: classes.arrow + (expanded ? ` ${classes.arrowExpanded}` : '') }, "\u276F")) : null,
54701
- React__default["default"].createElement("div", { className: classes.typeContent },
54702
- React__default["default"].createElement(OntologyTermAutocomplete, { session: session, ontologyName: "Sequence Ontology", style: { width: 170 }, value: type, filterTerms: isOntologyClass, fetchValidTerms: fetchValidTypeTerms.bind(null, feature), renderInput: (params) => {
54703
- return (React__default["default"].createElement("div", { ref: params.InputProps.ref },
54704
- React__default["default"].createElement("input", { type: "text", ...params.inputProps, className: classes.typeInputElement, style: { width: 170 } }),
54705
- params.error ? (React__default["default"].createElement("div", { className: classes.typeErrorMessage }, params.errorMessage ?? 'unknown error')) : null));
54706
- }, onChange: (oldValue, newValue) => {
54707
- if (newValue) {
54708
- handleFeatureTypeChange(changeManager, feature, oldValue, newValue).catch(notifyError);
54709
- }
54710
- } }))),
54711
- React__default["default"].createElement("td", null,
54712
- React__default["default"].createElement(NumberCell, { initialValue: min + 1, notifyError: notifyError, onChangeCommitted: (newStart) => handleFeatureStartChange(changeManager, feature, min, newStart - 1) })),
54713
- React__default["default"].createElement("td", null,
54714
- React__default["default"].createElement(NumberCell, { initialValue: max, notifyError: notifyError, onChangeCommitted: (newEnd) => handleFeatureEndChange(changeManager, feature, max, newEnd) })),
54715
- React__default["default"].createElement("td", null, strand === 1 ? '+' : strand === -1 ? '-' : undefined),
54716
- React__default["default"].createElement("td", null,
54717
- React__default["default"].createElement(FeatureAttributes, { filterText: filterText, feature: feature }))),
54718
- expanded && children
54719
- ? [...children.entries()]
54720
- .filter((entry) => {
54721
- if (!filterText) {
54722
- return true;
54723
- }
54724
- const [, childFeature] = entry;
54725
- // search feature and its subfeatures for the text
54726
- const text = JSON.stringify(childFeature);
54727
- return text.includes(filterText);
54728
- })
54729
- .map(([featureId, childFeature]) => {
54730
- const childHovered = apolloHover?.feature._id === childFeature._id;
54731
- const childSelected = selectedFeature?._id === childFeature._id;
54732
- return (React__default["default"].createElement(Feature, { isHovered: childHovered, isSelected: childSelected, selectedFeatureClass: selectedFeatureClass, key: featureId, depth: (depth || 0) + 1, feature: childFeature, model: displayState, setContextMenu: setContextMenu }));
54733
- })
54734
- : null));
54735
- });
54736
- async function fetchValidTypeTerms(feature, ontologyStore, _signal) {
54737
- const { parent: parentFeature } = feature;
54738
- if (parentFeature) {
54739
- // if this is a child of an existing feature, restrict the autocomplete choices to valid
54740
- // parts of that feature
54741
- const parentTypeTerms = await ontologyStore.getTermsWithLabelOrSynonym(parentFeature.type, { includeSubclasses: false });
54742
- // eslint-disable-next-line unicorn/no-array-callback-reference
54743
- const parentTypeClassTerms = parentTypeTerms.filter(isOntologyClass);
54744
- if (parentTypeClassTerms.length > 0) {
54745
- const subpartTerms = await ontologyStore.getClassesThat('part_of', parentTypeClassTerms);
54746
- return subpartTerms;
54747
- }
54748
- }
54749
- return;
54750
- }
54751
-
54752
- /* eslint-disable @typescript-eslint/no-unsafe-call */
54753
- const useStyles$3 = mui.makeStyles()((theme) => ({
54754
- scrollableTable: {
54755
- width: '100%',
54756
- height: '100%',
54757
- th: {
54758
- position: 'sticky',
54759
- top: 0,
54760
- zIndex: 2,
54761
- textAlign: 'left',
54762
- background: theme.palette.background.paper,
54763
- paddingTop: '3.2em',
55557
+ }
55558
+ void self.changeManager.submit(change);
55559
+ self.setDragging();
55560
+ self.setCursor();
54764
55561
  },
54765
- td: { whiteSpace: 'normal' },
54766
- },
54767
- selectedFeature: {
54768
- backgroundColor: theme.palette.action.selected,
54769
- },
54770
- }));
54771
- const HybridGrid = mobxReact.observer(function HybridGrid({ model, }) {
54772
- const { apolloHover, seenFeatures, selectedFeature, tabularEditor } = model;
54773
- const theme = material.useTheme();
54774
- const { classes } = useStyles$3();
54775
- const scrollContainerRef = React.useRef(null);
54776
- const [contextMenu, setContextMenu] = React.useState(null);
54777
- const { filterText } = tabularEditor;
54778
- // scrolls to selected feature if one is selected and it's not already visible
54779
- React.useEffect(() => {
54780
- const scrollContainer = scrollContainerRef.current;
54781
- if (scrollContainer && selectedFeature) {
54782
- const selectedRow = scrollContainer.querySelector(`.${classes.selectedFeature}`);
54783
- if (selectedRow) {
54784
- const currScroll = scrollContainer.scrollTop;
54785
- const newScrollTop = selectedRow.offsetTop - 25;
54786
- const isVisible = newScrollTop > currScroll &&
54787
- newScrollTop < currScroll + scrollContainer.offsetHeight;
54788
- if (!isVisible) {
54789
- scrollContainer.scroll({ top: newScrollTop - 40, behavior: 'smooth' });
54790
- }
55562
+ }))
55563
+ .actions((self) => ({
55564
+ onMouseDown(event) {
55565
+ const mousePosition = self.getMousePosition(event);
55566
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
55567
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseDown(self, mousePosition, event);
54791
55568
  }
54792
- }
54793
- }, [selectedFeature, seenFeatures, classes.selectedFeature]);
54794
- return (React__default["default"].createElement("div", { ref: scrollContainerRef, style: { width: '100%', overflowY: 'auto', height: '100%' } },
54795
- React__default["default"].createElement("table", { className: classes.scrollableTable },
54796
- React__default["default"].createElement("thead", null,
54797
- React__default["default"].createElement("tr", null,
54798
- React__default["default"].createElement("th", null, "Type"),
54799
- React__default["default"].createElement("th", null, "Start"),
54800
- React__default["default"].createElement("th", null, "End"),
54801
- React__default["default"].createElement("th", null, "Strand"),
54802
- React__default["default"].createElement("th", null, "Attributes"))),
54803
- React__default["default"].createElement("tbody", null, [...seenFeatures.entries()]
54804
- .filter((entry) => {
54805
- if (!filterText) {
54806
- return true;
55569
+ },
55570
+ onMouseMove(event) {
55571
+ const mousePosition = self.getMousePosition(event);
55572
+ if (self.apolloDragging) {
55573
+ self.setCursor('col-resize');
55574
+ self.continueDrag(mousePosition, event);
55575
+ return;
55576
+ }
55577
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
55578
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseMove(self, mousePosition, event);
55579
+ }
55580
+ else {
55581
+ self.setApolloHover();
55582
+ self.setCursor();
55583
+ }
55584
+ },
55585
+ onMouseLeave(event) {
55586
+ self.setDragging();
55587
+ self.setApolloHover();
55588
+ const mousePosition = self.getMousePosition(event);
55589
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
55590
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseLeave(self, mousePosition, event);
55591
+ }
55592
+ },
55593
+ onMouseUp(event) {
55594
+ const mousePosition = self.getMousePosition(event);
55595
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
55596
+ mousePosition.featureAndGlyphUnderMouse.glyph.onMouseUp(self, mousePosition, event);
55597
+ }
55598
+ if (self.apolloDragging) {
55599
+ self.endDrag();
55600
+ }
55601
+ },
55602
+ }))
55603
+ .actions((self) => ({
55604
+ afterAttach() {
55605
+ require$$1$3.addDisposer(self, mobx.autorun(() => {
55606
+ // This type is wrong in @jbrowse/core
55607
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
55608
+ if (!self.lgv.initialized || self.regionCannotBeRendered()) {
55609
+ return;
54807
55610
  }
54808
- const [, feature] = entry;
54809
- // search feature and its subfeatures for the text
54810
- const text = JSON.stringify(feature);
54811
- return text.includes(filterText);
54812
- })
54813
- .sort((a, b) => {
54814
- return a[1].min - b[1].min;
54815
- })
54816
- .map(([featureId, feature]) => {
54817
- const isSelected = selectedFeature?._id === featureId;
54818
- const isHovered = apolloHover?.feature._id === featureId;
54819
- return (React__default["default"].createElement(Feature, { key: featureId, isSelected: isSelected, isHovered: isHovered, selectedFeatureClass: classes.selectedFeature, feature: feature, model: model, depth: 0, setContextMenu: setContextMenu }));
54820
- }))),
54821
- React__default["default"].createElement(ui.Menu, { open: Boolean(contextMenu), onMenuItemClick: (_, callback) => {
54822
- callback();
54823
- setContextMenu(null);
54824
- }, onClose: () => {
54825
- setContextMenu(null);
54826
- }, TransitionProps: {
54827
- onExit: () => {
54828
- setContextMenu(null);
54829
- },
54830
- }, style: { zIndex: theme.zIndex.tooltip }, menuItems: contextMenu?.items ?? [], anchorReference: "anchorPosition", anchorPosition: contextMenu?.position })));
54831
- });
54832
-
54833
- var Clear = {};
54834
-
54835
- var _interopRequireDefault$5 = interopRequireDefault.exports;
54836
- Object.defineProperty(Clear, "__esModule", {
54837
- value: true
54838
- });
54839
- var default_1$5 = Clear["default"] = void 0;
54840
- var _createSvgIcon$5 = /*#__PURE__*/_interopRequireDefault$5(createSvgIcon);
54841
- var _jsxRuntime$5 = require$$2__default["default"];
54842
- var _default$5 = /*#__PURE__*/(0, _createSvgIcon$5["default"])( /*#__PURE__*/(0, _jsxRuntime$5.jsx)("path", {
54843
- d: "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
54844
- }), 'Clear');
54845
- default_1$5 = Clear["default"] = _default$5;
54846
-
54847
- var UnfoldLess = {};
54848
-
54849
- var _interopRequireDefault$4 = interopRequireDefault.exports;
54850
- Object.defineProperty(UnfoldLess, "__esModule", {
54851
- value: true
54852
- });
54853
- var default_1$4 = UnfoldLess["default"] = void 0;
54854
- var _createSvgIcon$4 = /*#__PURE__*/_interopRequireDefault$4(createSvgIcon);
54855
- var _jsxRuntime$4 = require$$2__default["default"];
54856
- var _default$4 = /*#__PURE__*/(0, _createSvgIcon$4["default"])( /*#__PURE__*/(0, _jsxRuntime$4.jsx)("path", {
54857
- d: "M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z"
54858
- }), 'UnfoldLess');
54859
- default_1$4 = UnfoldLess["default"] = _default$4;
54860
-
54861
- /* eslint-disable @typescript-eslint/unbound-method */
54862
- const useStyles$2 = mui.makeStyles()({
54863
- toolbar: {
54864
- width: '100%',
54865
- display: 'flex',
54866
- paddingRight: '2em',
54867
- flexDirection: 'row',
54868
- justifyContent: 'space-between',
54869
- position: 'absolute',
54870
- zIndex: 4,
54871
- },
54872
- filterText: {},
54873
- });
54874
- const ToolBar = mobxReact.observer(function ToolBar({ model: displayState, }) {
54875
- const model = displayState.tabularEditor;
54876
- const { classes } = useStyles$2();
54877
- return (React__default["default"].createElement("div", { className: classes.toolbar },
54878
- React__default["default"].createElement(material.Tooltip, { title: "Collapse all" },
54879
- React__default["default"].createElement(material.IconButton, { "aria-label": "collapse", sx: { marginTop: 0 }, onClick: model.collapseAllFeatures },
54880
- React__default["default"].createElement(default_1$4, null))),
54881
- React__default["default"].createElement(material.TextField, { className: classes.filterText, label: "Filter features", value: model.filterText, sx: { marginTop: 0 }, variant: "outlined", onChange: (event) => {
54882
- model.setFilterText(event.target.value);
54883
- }, InputProps: {
54884
- endAdornment: (React__default["default"].createElement(material.InputAdornment, { position: "end" },
54885
- React__default["default"].createElement(material.IconButton, { onClick: () => {
54886
- model.clearFilterText();
54887
- } },
54888
- React__default["default"].createElement(default_1$5, null)))),
54889
- } })));
54890
- });
54891
-
54892
- function stopPropagation(e) {
54893
- e.stopPropagation();
55611
+ const ctx = self.overlayCanvas?.getContext('2d');
55612
+ if (!ctx) {
55613
+ return;
55614
+ }
55615
+ ctx.clearRect(0, 0, self.lgv.dynamicBlocks.totalWidthPx, self.featuresHeight);
55616
+ const { apolloDragging, apolloHover } = self;
55617
+ if (!apolloHover) {
55618
+ return;
55619
+ }
55620
+ const { glyph } = apolloHover;
55621
+ // draw mouseover hovers
55622
+ glyph.drawHover(self, ctx);
55623
+ // draw tooltip on hover
55624
+ glyph.drawTooltip(self, ctx);
55625
+ // dragging previews
55626
+ if (apolloDragging) {
55627
+ // NOTE: the glyph where the drag started is responsible for drawing the preview.
55628
+ // it can call methods in other glyphs to help with this though.
55629
+ const glyph = self.getGlyph(apolloDragging.feature.topLevelFeature);
55630
+ glyph.drawDragPreview(self, ctx);
55631
+ }
55632
+ }, { name: 'LinearApolloDisplayRenderMouseoverAndDrag' }));
55633
+ },
55634
+ }));
54894
55635
  }
54895
- const TabularEditorPane = mobxReact.observer(function TabularEditorPane({ model: displayState, }) {
54896
- const model = displayState.tabularEditor;
54897
- if (!model.isShown) {
54898
- return null;
54899
- }
54900
- return (
54901
- // TODO: a11y
54902
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
54903
- React__default["default"].createElement("div", { onMouseDown: stopPropagation, onClick: stopPropagation, style: { width: '100%', height: '100%', position: 'relative' } },
54904
- React__default["default"].createElement(ToolBar, { model: displayState }),
54905
- React__default["default"].createElement(HybridGrid, { model: displayState })));
54906
- });
54907
-
54908
- const TabularEditorStateModelType = require$$1$3.types
54909
- .model('TabularEditor', {
54910
- isShown: true,
54911
- featureCollapsed: require$$1$3.types.map(require$$1$3.types.boolean),
54912
- filterText: '',
54913
- })
54914
- .actions((self) => ({
54915
- setFeatureCollapsed(id, state) {
54916
- self.featureCollapsed.set(id, state);
54917
- },
54918
- setFilterText(text) {
54919
- self.filterText = text;
54920
- },
54921
- clearFilterText() {
54922
- self.filterText = '';
54923
- },
54924
- collapseAllFeatures() {
54925
- // iterate over all seen features and set them to collapsed
54926
- const display = require$$1$3.getParent(self);
54927
- for (const [featureId] of display.seenFeatures.entries()) {
54928
- self.featureCollapsed.set(featureId, true);
54929
- }
54930
- },
54931
- togglePane() {
54932
- self.isShown = !self.isShown;
54933
- },
54934
- hidePane() {
54935
- self.isShown = false;
54936
- },
54937
- showPane() {
54938
- self.isShown = true;
54939
- },
54940
- // onPatch(patch: any) {
54941
- // console.log(patch)
54942
- // },
54943
- }));
54944
55636
 
54945
55637
  function stateModelFactory$1(pluginManager, configSchema) {
54946
55638
  // TODO: this needs to be refactored so that the final composition of the
@@ -54992,7 +55684,7 @@
54992
55684
  }), 'Error');
54993
55685
  default_1$1 = _Error["default"] = _default$1;
54994
55686
 
54995
- /* eslint-disable @typescript-eslint/no-unsafe-call */
55687
+ /* eslint-disable @typescript-eslint/unbound-method */
54996
55688
  const useStyles$1 = mui.makeStyles()((theme) => ({
54997
55689
  canvasContainer: {
54998
55690
  position: 'relative',
@@ -56042,8 +56734,34 @@
56042
56734
  configuration.readConfObject(ont, 'textIndexFields'),
56043
56735
  ];
56044
56736
  if (!ontologyManager.findOntology(name)) {
56737
+ const session = require$$1$2.getSession(self);
56738
+ const { jobsManager } = session;
56739
+ const controller = new AbortController();
56740
+ const jobName = `Loading ontology "${name}"`;
56741
+ const job = {
56742
+ name: jobName,
56743
+ statusMessage: `Loading ontology "${name}", version "${version}", this may take a while`,
56744
+ progressPct: 0,
56745
+ cancelCallback: () => {
56746
+ controller.abort();
56747
+ jobsManager.abortJob(job.name);
56748
+ },
56749
+ };
56750
+ const update = (message, progress) => {
56751
+ if (progress === 0) {
56752
+ jobsManager.runJob(job);
56753
+ return;
56754
+ }
56755
+ if (progress === 100) {
56756
+ jobsManager.done(job);
56757
+ return;
56758
+ }
56759
+ jobsManager.update(jobName, message, progress);
56760
+ return;
56761
+ };
56045
56762
  ontologyManager.addOntology(name, version, source, {
56046
56763
  textIndexing: { indexFields },
56764
+ update,
56047
56765
  });
56048
56766
  }
56049
56767
  }
@@ -56738,6 +57456,10 @@
56738
57456
  return codonLayout;
56739
57457
  },
56740
57458
  get featureLayout() {
57459
+ const { featureTypeOntology } = self.session.apolloDataStore.ontologyManager;
57460
+ if (!featureTypeOntology) {
57461
+ throw new Error('featureTypeOntology is undefined');
57462
+ }
56741
57463
  const featureLayout = new Map();
56742
57464
  for (const [refSeq, featuresForRefSeq] of this.features || []) {
56743
57465
  if (!featuresForRefSeq) {
@@ -56761,11 +57483,11 @@
56761
57483
  return start1 - start2 || end1 - end2;
56762
57484
  })) {
56763
57485
  for (const [, childFeature] of feature.children ?? new Map()) {
56764
- if (childFeature.type === 'mRNA') {
57486
+ if (featureTypeOntology.isTypeOf(childFeature.type, 'transcript')) {
56765
57487
  for (const [, grandChildFeature] of childFeature.children ||
56766
57488
  new Map()) {
56767
57489
  let startingRow;
56768
- if (grandChildFeature.type === 'CDS') {
57490
+ if (featureTypeOntology.isTypeOf(grandChildFeature.type, 'CDS')) {
56769
57491
  let discontinuousLocations;
56770
57492
  if (grandChildFeature.discontinuousLocations.length > 0) {
56771
57493
  ({ discontinuousLocations } = grandChildFeature);
@@ -56954,7 +57676,7 @@
56954
57676
  }));
56955
57677
  }
56956
57678
 
56957
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
57679
+ /* eslint-disable @typescript-eslint/unbound-method */
56958
57680
  function isApolloMessageData(data) {
56959
57681
  return (typeof data === 'object' &&
56960
57682
  data !== null &&
@@ -57088,6 +57810,7 @@
57088
57810
  return pluggableElement;
57089
57811
  });
57090
57812
  pluginManager.addToExtensionPoint('Core-extendPluggableElement', annotationFromPileup);
57813
+ pluginManager.addToExtensionPoint('Core-extendPluggableElement', annotationFromJBrowseFeature);
57091
57814
  if (!inWebWorker) {
57092
57815
  pluginManager.addToExtensionPoint('Core-extendWorker', (handle) => {
57093
57816
  if (!('on' in handle && handle.on)) {