@apollo-annotation/jbrowse-plugin-apollo 0.3.9 → 0.3.11

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 (31) hide show
  1. package/dist/index.esm.js +235 -120
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +233 -118
  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 +562 -298
  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 +4 -4
  12. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -1
  13. package/src/ApolloInternetAccount/model.ts +6 -2
  14. package/src/BackendDrivers/CollaborationServerDriver.ts +11 -5
  15. package/src/ChangeManager.ts +19 -4
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +29 -9
  17. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +2 -2
  18. package/src/LinearApolloDisplay/glyphs/util.ts +17 -0
  19. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +18 -25
  20. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +41 -59
  21. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +33 -2
  22. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +101 -3
  23. package/src/components/AddAssembly.tsx +1 -1
  24. package/src/components/ImportFeatures.tsx +1 -1
  25. package/src/components/OntologyTermAutocomplete.tsx +2 -2
  26. package/src/components/OntologyTermMultiSelect.tsx +2 -2
  27. package/src/makeDisplayComponent.tsx +1 -1
  28. package/src/session/ClientDataStore.ts +1 -1
  29. package/src/session/session.ts +4 -0
  30. package/src/util/displayUtils.ts +28 -0
  31. package/src/util/glyphUtils.ts +18 -0
@@ -122,7 +122,7 @@ var TrackChangesIcon__default = /*#__PURE__*/_interopDefaultLegacy(TrackChangesI
122
122
  var UndoIcon__default = /*#__PURE__*/_interopDefaultLegacy(UndoIcon);
123
123
  var SaveIcon__default = /*#__PURE__*/_interopDefaultLegacy(SaveIcon);
124
124
 
125
- var version = "0.3.9";
125
+ var version = "0.3.11";
126
126
 
127
127
  const ApolloConfigSchema = configuration.ConfigurationSchema('ApolloInternetAccount', {
128
128
  baseURL: {
@@ -557,6 +557,19 @@ function getContextMenuItemsForFeature$2(display, sourceFeature) {
557
557
  ]);
558
558
  },
559
559
  });
560
+ if (util.isSessionModelWithWidgets(session)) {
561
+ menuItems.push({
562
+ label: 'Open feature details',
563
+ onClick: () => {
564
+ const apolloGeneWidget = session.addWidget('ApolloFeatureDetailsWidget', 'apolloFeatureDetailsWidget', {
565
+ feature: sourceFeature,
566
+ assembly: currentAssemblyId,
567
+ refName: region.refName,
568
+ });
569
+ session.showWidget(apolloGeneWidget);
570
+ },
571
+ });
572
+ }
560
573
  return menuItems;
561
574
  }
562
575
  function navToFeatureCenter(feature, paddingPct, refSeqLength) {
@@ -805,7 +818,7 @@ function AddAssembly({ changeManager, handleClose, session, }) {
805
818
  statusMessage: 'Pre-validating',
806
819
  progressPct: 0,
807
820
  cancelCallback: () => {
808
- controller.abort();
821
+ controller.abort('AddAssembly');
809
822
  jobsManager.abortJob(job.name);
810
823
  },
811
824
  };
@@ -2203,7 +2216,7 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
2203
2216
  });
2204
2217
  }
2205
2218
  return () => {
2206
- controller.abort();
2219
+ controller.abort('OntologyTermAutocomplete matcher');
2207
2220
  };
2208
2221
  }, [session, valueString, filterTerms, ontologyStore, needToLoadCurrentTerm]);
2209
2222
  // effect for loading term autocompletions
@@ -2222,7 +2235,7 @@ function OntologyTermAutocomplete({ fetchValidTerms, filterTerms: filterTermsPro
2222
2235
  });
2223
2236
  }
2224
2237
  return () => {
2225
- controller.abort();
2238
+ controller.abort('OntologyTermAutocomplete loader');
2226
2239
  };
2227
2240
  }, [
2228
2241
  needToLoadTermChoices,
@@ -2374,17 +2387,24 @@ class ChangeManager {
2374
2387
  // pre-validate
2375
2388
  const session = util.getSession(this.dataStore);
2376
2389
  const controller = new AbortController();
2377
- const { jobsManager, isLocked } = util.getSession(this.dataStore);
2390
+ // eslint-disable-next-line @typescript-eslint/unbound-method
2391
+ const { jobsManager, isLocked, changeInProgress, setChangeInProgress } = util.getSession(this.dataStore);
2378
2392
  if (isLocked) {
2379
2393
  session.notify('Cannot submit changes in locked mode');
2394
+ setChangeInProgress(false);
2380
2395
  return;
2381
2396
  }
2397
+ if (changeInProgress) {
2398
+ session.notify('Could not submit change, there is another change still in progress');
2399
+ return;
2400
+ }
2401
+ setChangeInProgress(true);
2382
2402
  const job = {
2383
2403
  name: change.typeName,
2384
2404
  statusMessage: 'Pre-validating',
2385
2405
  progressPct: 0,
2386
2406
  cancelCallback: () => {
2387
- controller.abort();
2407
+ controller.abort('ChangeManager');
2388
2408
  },
2389
2409
  };
2390
2410
  if (updateJobsManager) {
@@ -2397,6 +2417,7 @@ class ChangeManager {
2397
2417
  jobsManager.abortJob(job.name, msg);
2398
2418
  }
2399
2419
  session.notify(msg, 'error');
2420
+ setChangeInProgress(false);
2400
2421
  return;
2401
2422
  }
2402
2423
  try {
@@ -2409,6 +2430,7 @@ class ChangeManager {
2409
2430
  }
2410
2431
  console.error(error);
2411
2432
  session.notify(`Error encountered in client: ${String(error)}. Data may be out of sync, please refresh the page`, 'error');
2433
+ setChangeInProgress(false);
2412
2434
  return;
2413
2435
  }
2414
2436
  // post-validate
@@ -2439,6 +2461,7 @@ class ChangeManager {
2439
2461
  console.error(error);
2440
2462
  session.notify(String(error), 'error');
2441
2463
  await this.undo(change, false);
2464
+ setChangeInProgress(false);
2442
2465
  return;
2443
2466
  }
2444
2467
  if (!backendResult.ok) {
@@ -2448,6 +2471,7 @@ class ChangeManager {
2448
2471
  }
2449
2472
  session.notify(msg, 'error');
2450
2473
  await this.undo(change, false);
2474
+ setChangeInProgress(false);
2451
2475
  return;
2452
2476
  }
2453
2477
  if (change.notification) {
@@ -2461,6 +2485,7 @@ class ChangeManager {
2461
2485
  if (updateJobsManager) {
2462
2486
  jobsManager.done(job);
2463
2487
  }
2488
+ setChangeInProgress(false);
2464
2489
  }
2465
2490
  async undo(change, submitToBackend = true) {
2466
2491
  const inverseChange = change.getInverse();
@@ -2566,17 +2591,22 @@ class CollaborationServerDriver extends BackendDriver {
2566
2591
  checkSocket(assembly, refSeq, internetAccount) {
2567
2592
  const { socket } = internetAccount;
2568
2593
  const token = internetAccount.retrieveToken();
2594
+ if (!token) {
2595
+ return;
2596
+ }
2597
+ const localSessionId = shared.makeUserSessionId(token);
2569
2598
  const channel = `${assembly}-${refSeq}`;
2570
2599
  const changeManager = new ChangeManager(this.clientStore);
2571
2600
  if (!socket.hasListeners(channel)) {
2572
2601
  socket.on(channel, async (message) => {
2573
2602
  // Save server last change sequence into session storage
2574
2603
  internetAccount.setLastChangeSequenceNumber(Number(message.changeSequence));
2575
- if (message.userSessionId !== token && message.channel === channel) {
2576
- const change = common.Change.fromJSON(message.changeInfo);
2577
- if (common.isFeatureChange(change) && this.haveDataForChange(change)) {
2578
- await changeManager.submit(change, { submitToBackend: false });
2579
- }
2604
+ if (message.userSessionId === localSessionId) {
2605
+ return; // we did this change, no need to apply it again
2606
+ }
2607
+ const change = common.Change.fromJSON(message.changeInfo);
2608
+ if (common.isFeatureChange(change) && this.haveDataForChange(change)) {
2609
+ await changeManager.submit(change, { submitToBackend: false });
2580
2610
  }
2581
2611
  });
2582
2612
  }
@@ -3989,7 +4019,7 @@ function ImportFeatures({ changeManager, handleClose, session, }) {
3989
4019
  statusMessage: 'Uploading file, this may take awhile',
3990
4020
  progressPct: 0,
3991
4021
  cancelCallback: () => {
3992
- controller.abort();
4022
+ controller.abort('ImportFeatures');
3993
4023
  jobsManager.abortJob(job.name);
3994
4024
  },
3995
4025
  };
@@ -5219,7 +5249,7 @@ const AuthTypeSelector = ({ baseURL, handleClose, name, }) => {
5219
5249
  }
5220
5250
  });
5221
5251
  return () => {
5222
- controller.abort();
5252
+ controller.abort('AuthTypeSelector');
5223
5253
  };
5224
5254
  }, [baseURL]);
5225
5255
  function handleClick(authType) {
@@ -5656,8 +5686,13 @@ const stateModelFactory$3 = (configSchema) => {
5656
5686
  return;
5657
5687
  }
5658
5688
  if (self.role) {
5659
- await self.initialize(self.role);
5660
- reaction.dispose();
5689
+ try {
5690
+ await self.initialize(self.role);
5691
+ reaction.dispose();
5692
+ }
5693
+ catch {
5694
+ // if initialize fails, do nothing so the autorun runs again
5695
+ }
5661
5696
  }
5662
5697
  }, { name: 'ApolloInternetAccount' });
5663
5698
  },
@@ -6932,6 +6967,7 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
6932
6967
  const refData = currentAssembly?.getByRefName(refName);
6933
6968
  const { changeManager } = session.apolloDataStore;
6934
6969
  const seqRef = React.useRef(null);
6970
+ const { changeInProgress } = session;
6935
6971
  if (!refData) {
6936
6972
  return null;
6937
6973
  }
@@ -7379,10 +7415,13 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
7379
7415
  // highlight start codon and stop codons
7380
7416
  if (codonSeq === 'ATG') {
7381
7417
  elements.push(React__default["default"].createElement(material.Typography, { component: 'span', style: {
7382
- backgroundColor: 'yellow',
7418
+ backgroundColor: changeInProgress ? 'lightgray' : 'yellow',
7383
7419
  cursor: 'pointer',
7384
7420
  border: '1px solid black',
7385
7421
  }, key: codonGenomicPos, onClick: () => {
7422
+ if (changeInProgress) {
7423
+ return;
7424
+ }
7386
7425
  // NOTE: codonGenomicPos is important here for calculating the genomic location
7387
7426
  // of the start codon. We are using the codonGenomicPos as the key in the typography
7388
7427
  // elements to maintain the genomic postion of the codon start
@@ -7466,20 +7505,21 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
7466
7505
  }
7467
7506
  // Trim any sequence before first start codon and after stop codon
7468
7507
  const startCodonIndex = translationSequence.indexOf('M');
7469
- const stopCodonIndex = translationSequence.indexOf('*') + 1;
7508
+ const stopCodonIndex = translationSequence.indexOf('*');
7470
7509
  const startCodonPos = translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos;
7471
7510
  const stopCodonPos = translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos;
7472
7511
  if (!startCodonPos || !stopCodonPos) {
7473
7512
  return;
7474
7513
  }
7475
7514
  const startCodonGenomicLoc = getCodonGenomicLocation(startCodonPos);
7476
- const stopCodonGenomicLoc = getCodonGenomicLocation(stopCodonPos);
7515
+ let stopCodonGenomicLoc = getCodonGenomicLocation(stopCodonPos);
7477
7516
  if (strand === 1) {
7478
7517
  if (startCodonGenomicLoc > stopCodonGenomicLoc) {
7479
7518
  notify('Start codon genomic location should be less than stop codon genomic location', 'error');
7480
7519
  return;
7481
7520
  }
7482
7521
  let promise;
7522
+ stopCodonGenomicLoc += 3; // move to end of stop codon
7483
7523
  if (startCodonGenomicLoc !== cdsMin) {
7484
7524
  promise = new Promise((resolve) => {
7485
7525
  updateCDSLocation(cdsMin, startCodonGenomicLoc, feature, true, () => {
@@ -7505,6 +7545,7 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
7505
7545
  return;
7506
7546
  }
7507
7547
  let promise;
7548
+ stopCodonGenomicLoc -= 3; // move to end of stop codon
7508
7549
  if (startCodonGenomicLoc !== cdsMax) {
7509
7550
  promise = new Promise((resolve) => {
7510
7551
  updateCDSLocation(cdsMax, startCodonGenomicLoc, feature, false, () => {
@@ -7548,27 +7589,29 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
7548
7589
  gap: 10,
7549
7590
  } },
7550
7591
  React__default["default"].createElement(material.Tooltip, { title: "Copy" },
7551
- React__default["default"].createElement(ContentCopyIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick: onCopyClick })),
7592
+ React__default["default"].createElement("button", { onClick: onCopyClick, style: { border: 'none', background: 'none', padding: 0 }, disabled: changeInProgress },
7593
+ React__default["default"].createElement(ContentCopyIcon__default["default"], { style: { fontSize: 15 } }))),
7552
7594
  React__default["default"].createElement(material.Tooltip, { title: "Trim" },
7553
- React__default["default"].createElement(ContentCutIcon__default["default"], { style: { fontSize: 15, cursor: 'pointer' }, onClick: trimTranslationSequence }))))),
7595
+ React__default["default"].createElement("button", { onClick: trimTranslationSequence, style: { border: 'none', background: 'none', padding: 0 }, disabled: changeInProgress },
7596
+ React__default["default"].createElement(ContentCutIcon__default["default"], { style: { fontSize: 15 } })))))),
7554
7597
  React__default["default"].createElement(material.Grid, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center', marginTop: 10 } },
7555
7598
  React__default["default"].createElement(material.Grid, { size: 1 }),
7556
7599
  strand === 1 ? (React__default["default"].createElement(material.Grid, { size: 4 },
7557
7600
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin + 1, onChangeCommitted: (newLocation) => {
7558
7601
  return updateCDSLocation(cdsMin, newLocation - 1, feature, true);
7559
- }, style: { border: '1px solid black', borderRadius: 5 } }))) : (React__default["default"].createElement(material.Grid, { size: 4 },
7602
+ }, style: { border: '1px solid black', borderRadius: 5 }, disabled: changeInProgress }))) : (React__default["default"].createElement(material.Grid, { size: 4 },
7560
7603
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
7561
7604
  return updateCDSLocation(cdsMax, newLocation, feature, false);
7562
- }, style: { border: '1px solid black', borderRadius: 5 } }))),
7605
+ }, style: { border: '1px solid black', borderRadius: 5 }, disabled: changeInProgress }))),
7563
7606
  React__default["default"].createElement(material.Grid, { size: 2 },
7564
7607
  React__default["default"].createElement(material.Typography, { component: 'span' }, "CDS")),
7565
7608
  strand === 1 ? (React__default["default"].createElement(material.Grid, { size: 4 },
7566
7609
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMax, onChangeCommitted: (newLocation) => {
7567
7610
  return updateCDSLocation(cdsMax, newLocation, feature, false);
7568
- }, style: { border: '1px solid black', borderRadius: 5 } }))) : (React__default["default"].createElement(material.Grid, { size: 4 },
7611
+ }, style: { border: '1px solid black', borderRadius: 5 }, disabled: changeInProgress }))) : (React__default["default"].createElement(material.Grid, { size: 4 },
7569
7612
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: cdsMin + 1, onChangeCommitted: (newLocation) => {
7570
7613
  return updateCDSLocation(cdsMin, newLocation - 1, feature, true);
7571
- }, style: { border: '1px solid black', borderRadius: 5 } }))),
7614
+ }, style: { border: '1px solid black', borderRadius: 5 }, disabled: changeInProgress }))),
7572
7615
  React__default["default"].createElement(material.Grid, { size: 1 })))),
7573
7616
  React__default["default"].createElement("div", { style: { marginTop: 5 } }, transcriptExonParts.map((loc, index) => {
7574
7617
  return (React__default["default"].createElement("div", { key: index }, loc.type === 'exon' && (React__default["default"].createElement(material.Grid, { container: true, justifyContent: "center", alignItems: "center", style: { textAlign: 'center' } },
@@ -7577,19 +7620,19 @@ const TranscriptWidgetEditLocation = mobxReact.observer(function TranscriptWidge
7577
7620
  strand === 1 ? (React__default["default"].createElement(material.Grid, { size: 4, style: { padding: 0 } },
7578
7621
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min + 1, onChangeCommitted: (newLocation) => {
7579
7622
  return handleExonLocationChange(loc.min, newLocation - 1, feature, true);
7580
- } }))) : (React__default["default"].createElement(material.Grid, { size: 4, style: { padding: 0 } },
7623
+ }, disabled: changeInProgress }))) : (React__default["default"].createElement(material.Grid, { size: 4, style: { padding: 0 } },
7581
7624
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
7582
7625
  return handleExonLocationChange(loc.max, newLocation, feature, false);
7583
- } }))),
7626
+ }, disabled: changeInProgress }))),
7584
7627
  React__default["default"].createElement(material.Grid, { size: 2 },
7585
7628
  React__default["default"].createElement(Strand, { strand: feature.strand })),
7586
7629
  strand === 1 ? (React__default["default"].createElement(material.Grid, { size: 4, style: { padding: 0 } },
7587
7630
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.max, onChangeCommitted: (newLocation) => {
7588
7631
  return handleExonLocationChange(loc.max, newLocation, feature, false);
7589
- } }))) : (React__default["default"].createElement(material.Grid, { size: 4, style: { padding: 0 } },
7632
+ }, disabled: changeInProgress }))) : (React__default["default"].createElement(material.Grid, { size: 4, style: { padding: 0 } },
7590
7633
  React__default["default"].createElement(StyledTextField, { margin: "dense", variant: "outlined", value: loc.min + 1, onChangeCommitted: (newLocation) => {
7591
7634
  return handleExonLocationChange(loc.min, newLocation - 1, feature, true);
7592
- } }))),
7635
+ }, disabled: changeInProgress }))),
7593
7636
  React__default["default"].createElement(material.Grid, { size: 1 }, index !== transcriptExonParts.length - 1 &&
7594
7637
  getThreePrimeSpliceSite(loc, index).map((site, idx) => (React__default["default"].createElement(material.Typography, { key: idx, component: 'span', color: site.color }, site.spliceSite))))))));
7595
7638
  }))));
@@ -9258,8 +9301,8 @@ function getContextMenuItems$2(display, mousePosition) {
9258
9301
  },
9259
9302
  });
9260
9303
  if (util.isSessionModelWithWidgets(session)) {
9261
- contextMenuItemsForFeature.push({
9262
- label: 'Open transcript details',
9304
+ contextMenuItemsForFeature.splice(1, 0, {
9305
+ label: 'Open transcript editor',
9263
9306
  onClick: () => {
9264
9307
  const apolloTranscriptWidget = session.addWidget('ApolloTranscriptDetails', 'apolloTranscriptDetails', {
9265
9308
  feature,
@@ -9607,6 +9650,25 @@ function clusterResultByMessage(items, width, touchesAsOverlap) {
9607
9650
  clusters.sort((a, b) => a.message.localeCompare(b.message) || a.start - b.start);
9608
9651
  return clusters;
9609
9652
  }
9653
+ function codonColorCode(letter, theme, highContrast) {
9654
+ if (letter === 'M') {
9655
+ return theme.palette.startCodon;
9656
+ }
9657
+ if (letter === '*') {
9658
+ return highContrast ? theme.palette.text.primary : theme.palette.stopCodon;
9659
+ }
9660
+ return;
9661
+ }
9662
+ function colorCode(letter, theme) {
9663
+ const letterUpper = letter.toUpperCase();
9664
+ if (letterUpper === 'A' ||
9665
+ letterUpper === 'C' ||
9666
+ letterUpper === 'G' ||
9667
+ letterUpper === 'T') {
9668
+ return theme.palette.bases[letterUpper].main.toString();
9669
+ }
9670
+ return 'lightgray';
9671
+ }
9610
9672
 
9611
9673
  const minDisplayHeight$2 = 20;
9612
9674
  function baseModelFactory$2(_pluginManager, configSchema) {
@@ -10432,34 +10494,26 @@ function stateModelFactory$2(pluginManager, configSchema) {
10432
10494
 
10433
10495
  const configSchema$1 = configuration.ConfigurationSchema('LinearApolloReferenceSequenceDisplay', {}, { explicitIdentifier: 'displayId', explicitlyTyped: true });
10434
10496
 
10435
- function getSeqRow(strand, bpPerPx) {
10497
+ function getSeqRow(strand, bpPerPx, reversed) {
10436
10498
  if (bpPerPx > 1 || strand === undefined) {
10437
10499
  return;
10438
10500
  }
10501
+ if (reversed) {
10502
+ return strand === 1 ? 4 : 3;
10503
+ }
10439
10504
  return strand === 1 ? 3 : 4;
10440
10505
  }
10441
- function getTranslationRow(frame, bpPerPx) {
10442
- const offset = bpPerPx <= 1 ? 2 : 0;
10443
- switch (frame) {
10444
- case 3: {
10445
- return 0;
10446
- }
10447
- case 2: {
10448
- return 1;
10449
- }
10450
- case 1: {
10451
- return 2;
10452
- }
10453
- case -1: {
10454
- return 3 + offset;
10455
- }
10456
- case -2: {
10457
- return 4 + offset;
10458
- }
10459
- case -3: {
10460
- return 5 + offset;
10461
- }
10506
+ function getTranslationRow(frame, bpPerPx, reversed) {
10507
+ const frameRows = bpPerPx <= 1 ? [2, 1, 0, 7, 6, 5] : [2, 1, 0, 5, 4, 3];
10508
+ if (reversed) {
10509
+ frameRows.reverse();
10462
10510
  }
10511
+ frameRows.unshift(0);
10512
+ const row = frameRows.at(frame);
10513
+ if (row === undefined) {
10514
+ throw new Error('could not find row');
10515
+ }
10516
+ return row;
10463
10517
  }
10464
10518
  function getLeftPx$1(feature, bpPerPx, offsetPx, block) {
10465
10519
  const blockLeftPx = block.offsetPx - offsetPx;
@@ -10481,7 +10535,7 @@ function fillAndStrokeRect(ctx, left, top, width, height, theme, selected = fals
10481
10535
  ctx.strokeRect(left, top, width, height);
10482
10536
  }
10483
10537
  function drawHighlight(ctx, feature, bpPerPx, offsetPx, rowHeight, block, theme, selected = false) {
10484
- const row = getSeqRow(feature.strand, bpPerPx);
10538
+ const row = getSeqRow(feature.strand, bpPerPx, block.reversed);
10485
10539
  if (!row) {
10486
10540
  return;
10487
10541
  }
@@ -10505,7 +10559,7 @@ function drawCDSHighlight(ctx, feature, bpPerPx, offsetPx, rowHeight, block, the
10505
10559
  }
10506
10560
  for (const loc of cdsLocs) {
10507
10561
  const frame = util.getFrame(loc.min, loc.max, feature.strand ?? 1, loc.phase);
10508
- const row = getTranslationRow(frame, bpPerPx);
10562
+ const row = getTranslationRow(frame, bpPerPx, block.reversed);
10509
10563
  const left = getLeftPx$1(loc, bpPerPx, offsetPx, block);
10510
10564
  const top = row * rowHeight;
10511
10565
  const width = (loc.max - loc.min) / bpPerPx;
@@ -10528,76 +10582,71 @@ function drawSequenceOverlay(canvas, ctx, hoveredFeature, selectedFeature, rowHe
10528
10582
  hoveredFeature?.feature,
10529
10583
  ].filter((f) => f !== undefined)) {
10530
10584
  if (featureTypeOntology.isTypeOf(feature.type, 'CDS')) {
10531
- drawCDSHighlight(ctx, feature, bpPerPx, offsetPx, rowHeight, block, theme, true);
10585
+ drawCDSHighlight(ctx, feature, bpPerPx, offsetPx, rowHeight, block, theme, feature._id === selectedFeature?._id);
10532
10586
  }
10533
10587
  else {
10534
- drawHighlight(ctx, feature, bpPerPx, offsetPx, rowHeight, block, theme, true);
10588
+ drawHighlight(ctx, feature, bpPerPx, offsetPx, rowHeight, block, theme, feature._id === selectedFeature?._id);
10535
10589
  }
10536
10590
  }
10537
10591
  ctx.restore();
10538
10592
  }
10539
10593
  }
10540
10594
 
10541
- function colorCode(letter, theme) {
10542
- const letterUpper = letter.toUpperCase();
10543
- if (letterUpper === 'A' ||
10544
- letterUpper === 'C' ||
10545
- letterUpper === 'G' ||
10546
- letterUpper === 'T') {
10547
- return theme.palette.bases[letterUpper].main.toString();
10548
- }
10549
- return 'lightgray';
10595
+ function getLeftPx(display, feature, block) {
10596
+ const { lgv } = display;
10597
+ const { bpPerPx, offsetPx } = lgv;
10598
+ const blockLeftPx = block.offsetPx - offsetPx;
10599
+ const featureLeftBpDistanceFromBlockLeftBp = block.reversed
10600
+ ? block.end - feature.max
10601
+ : feature.min - block.start;
10602
+ const featureLeftPxDistanceFromBlockLeftPx = featureLeftBpDistanceFromBlockLeftBp / bpPerPx;
10603
+ return blockLeftPx + featureLeftPxDistanceFromBlockLeftPx;
10550
10604
  }
10551
- function codonColorCode(letter, theme, highContrast) {
10552
- if (letter === 'M') {
10553
- return theme.palette.startCodon;
10554
- }
10555
- if (letter === '*') {
10556
- return highContrast ? theme.palette.text.primary : theme.palette.stopCodon;
10557
- }
10558
- return;
10605
+ /**
10606
+ * Perform a canvas strokeRect, but have the stroke be contained within the
10607
+ * given rect instead of centered on it.
10608
+ */
10609
+ function strokeRectInner(ctx, left, top, width, height, color) {
10610
+ ctx.strokeStyle = color;
10611
+ ctx.lineWidth = 1;
10612
+ ctx.strokeRect(left + 0.5, top + 0.5, width - 1, height - 1);
10559
10613
  }
10614
+
10560
10615
  function drawLetter(seqTrackctx, left, top, width, letter) {
10561
10616
  const fontSize = Math.min(width, 10);
10562
10617
  seqTrackctx.fillStyle = '#000';
10563
10618
  seqTrackctx.font = `${fontSize}px`;
10564
10619
  const textWidth = seqTrackctx.measureText(letter).width;
10565
- const textX = left + (width - textWidth) / 2;
10620
+ const textX = Math.round(left + (width - textWidth) / 2);
10566
10621
  seqTrackctx.fillText(letter, textX, top + 10);
10567
10622
  }
10568
- function drawTranslationFrameBackgrounds(canvas, ctx, bpPerPx, theme, dynamicBlocks, highContrast, sequenceRowHeight) {
10623
+ function drawTranslationFrameBackgrounds(ctx, bpPerPx, theme, highContrast, left, width, sequenceRowHeight, reversed) {
10569
10624
  const frames = bpPerPx <= 1 ? [3, 2, 1, 0, 0, -1, -2, -3] : [3, 2, 1, -1, -2, -3];
10625
+ if (reversed) {
10626
+ frames.reverse();
10627
+ }
10570
10628
  for (const [idx, frame] of frames.entries()) {
10571
10629
  const frameColor = theme.palette.framesCDS.at(frame)?.main;
10572
10630
  if (!frameColor) {
10573
10631
  continue;
10574
10632
  }
10575
10633
  const top = idx * sequenceRowHeight;
10576
- const { offsetPx } = dynamicBlocks;
10577
- const left = Math.max(0, -offsetPx);
10578
- const width = dynamicBlocks.totalWidthPx;
10579
10634
  ctx.fillStyle = highContrast ? theme.palette.background.default : frameColor;
10580
10635
  ctx.fillRect(left, top, width, sequenceRowHeight);
10581
10636
  if (highContrast) {
10582
10637
  // eslint-disable-next-line prefer-destructuring
10583
- ctx.strokeStyle = theme.palette.grey[200];
10584
- ctx.strokeRect(left, top, width, sequenceRowHeight);
10585
- }
10586
- }
10587
- // allows inter-region padding lines to show through
10588
- for (const block of dynamicBlocks.getBlocks()) {
10589
- if (block.type === 'InterRegionPaddingBlock') {
10590
- const left = block.offsetPx - dynamicBlocks.offsetPx;
10591
- ctx.clearRect(left, 0, block.widthPx, canvas.height);
10638
+ const strokeStyle = theme.palette.grey[200];
10639
+ strokeRectInner(ctx, left, top, width, sequenceRowHeight, strokeStyle);
10592
10640
  }
10593
10641
  }
10594
10642
  }
10595
10643
  function drawBase(ctx, base, index, leftPx, bpPerPx, rowHeight, theme) {
10596
- const width = 1 / bpPerPx;
10597
- if (width < 1) {
10644
+ if (1 / bpPerPx < 1) {
10598
10645
  return;
10599
10646
  }
10600
- const left = leftPx + index / bpPerPx;
10647
+ const left = Math.round(leftPx + index / bpPerPx);
10648
+ const nextLeft = Math.round(leftPx + (index + 1) / bpPerPx);
10649
+ const width = nextLeft - left;
10601
10650
  const strands = [-1, 1];
10602
10651
  for (const strand of strands) {
10603
10652
  const top = (strand === 1 ? 3 : 4) * rowHeight;
@@ -10605,13 +10654,13 @@ function drawBase(ctx, base, index, leftPx, bpPerPx, rowHeight, theme) {
10605
10654
  ctx.fillStyle = colorCode(baseCode, theme);
10606
10655
  ctx.fillRect(left, top, width, rowHeight);
10607
10656
  if (1 / bpPerPx >= 12) {
10608
- ctx.strokeStyle = theme.palette.text.disabled;
10609
- ctx.strokeRect(left, top, width, rowHeight);
10657
+ const strokeStyle = theme.palette.text.disabled;
10658
+ strokeRectInner(ctx, left, top, width, rowHeight, strokeStyle);
10610
10659
  drawLetter(ctx, left, top, width, baseCode);
10611
10660
  }
10612
10661
  }
10613
10662
  }
10614
- function drawCodon(ctx, codon, leftPx, index, theme, highContrast, bpPerPx, bp, rowHeight, showStartCodons, showStopCodons) {
10663
+ function drawCodon$1(ctx, codon, leftPx, index, theme, highContrast, bpPerPx, bp, rowHeight, showStartCodons, showStopCodons) {
10615
10664
  const frameOffsets = (bpPerPx <= 1 ? [0, 2, 1, 0, 7, 6, 5] : [0, 2, 1, 0, 5, 4, 3]).map((b) => b * rowHeight);
10616
10665
  const strands = [-1, 1];
10617
10666
  for (const strand of strands) {
@@ -10621,7 +10670,8 @@ function drawCodon(ctx, codon, leftPx, index, theme, highContrast, bpPerPx, bp,
10621
10670
  continue;
10622
10671
  }
10623
10672
  const left = Math.round(leftPx + index / bpPerPx);
10624
- const width = Math.round(3 / bpPerPx);
10673
+ const nextLeft = Math.round(leftPx + (index + 3) / bpPerPx);
10674
+ const width = nextLeft - left;
10625
10675
  const codonCode = strand === 1 ? codon : util.revcom(codon);
10626
10676
  const aminoAcidCode = util.defaultCodonTable[codonCode];
10627
10677
  const fillColor = codonColorCode(aminoAcidCode, theme, highContrast);
@@ -10632,8 +10682,8 @@ function drawCodon(ctx, codon, leftPx, index, theme, highContrast, bpPerPx, bp,
10632
10682
  ctx.fillRect(left, top, width, rowHeight);
10633
10683
  }
10634
10684
  if (1 / bpPerPx >= 4) {
10635
- ctx.strokeStyle = theme.palette.text.disabled;
10636
- ctx.strokeRect(left, top, width, rowHeight);
10685
+ const strokeStyle = theme.palette.text.disabled;
10686
+ strokeRectInner(ctx, left, top, width, rowHeight, strokeStyle);
10637
10687
  drawLetter(ctx, left, top, width, aminoAcidCode);
10638
10688
  }
10639
10689
  }
@@ -10644,9 +10694,10 @@ function drawSequenceTrack(canvas, theme, bpPerPx, offsetPx, dynamicBlocks, high
10644
10694
  return;
10645
10695
  }
10646
10696
  ctx.clearRect(0, 0, canvas.width, canvas.height);
10647
- drawTranslationFrameBackgrounds(canvas, ctx, bpPerPx, theme, dynamicBlocks, highContrast, sequenceRowHeight);
10648
10697
  const { apolloDataStore } = session;
10649
10698
  for (const block of dynamicBlocks.contentBlocks) {
10699
+ const totalOffsetPx = block.offsetPx - offsetPx;
10700
+ drawTranslationFrameBackgrounds(ctx, bpPerPx, theme, highContrast, totalOffsetPx, block.widthPx, sequenceRowHeight, block.reversed);
10650
10701
  const assembly = apolloDataStore.assemblies.get(block.assemblyName);
10651
10702
  const ref = assembly?.getByRefName(block.refName);
10652
10703
  const roundedStart = Math.floor(block.start);
@@ -10656,16 +10707,20 @@ function drawSequenceTrack(canvas, theme, bpPerPx, offsetPx, dynamicBlocks, high
10656
10707
  return;
10657
10708
  }
10658
10709
  seq = seq.toUpperCase();
10659
- const baseOffsetPx = (block.start - roundedStart) / bpPerPx;
10660
- const seqLeftPx = Math.round(block.offsetPx - offsetPx - baseOffsetPx);
10710
+ if (block.reversed) {
10711
+ seq = util.revcom(seq);
10712
+ }
10713
+ const baseOffsetPx = (block.reversed ? roundedEnd - block.end : block.start - roundedStart) /
10714
+ bpPerPx;
10715
+ const seqLeftPx = totalOffsetPx - baseOffsetPx;
10661
10716
  for (let i = 0; i < seq.length; i++) {
10662
- const bp = roundedStart + i;
10717
+ const bp = block.reversed ? roundedEnd - i : roundedStart + i;
10663
10718
  const codon = seq.slice(i, i + 3);
10664
10719
  drawBase(ctx, seq[i], i, seqLeftPx, bpPerPx, sequenceRowHeight, theme);
10665
10720
  if (codon.length !== 3) {
10666
10721
  continue;
10667
10722
  }
10668
- drawCodon(ctx, codon, seqLeftPx, i, theme, highContrast, bpPerPx, bp, sequenceRowHeight, showStartCodons, showStopCodons);
10723
+ drawCodon$1(ctx, codon, seqLeftPx, i, theme, highContrast, bpPerPx, bp, sequenceRowHeight, showStartCodons, showStopCodons);
10669
10724
  }
10670
10725
  }
10671
10726
  }
@@ -11729,6 +11784,8 @@ function baseModelFactory(_pluginManager, configSchema) {
11729
11784
  graphical: true,
11730
11785
  table: false,
11731
11786
  showFeatureLabels: true,
11787
+ showStartCodons: false,
11788
+ showStopCodons: true,
11732
11789
  showCheckResults: true,
11733
11790
  zoomThreshold: 200,
11734
11791
  heightPreConfig: mobxStateTree.types.maybe(mobxStateTree.types.refinement('displayHeight', mobxStateTree.types.number, (n) => n >= minDisplayHeight)),
@@ -11857,6 +11914,12 @@ function baseModelFactory(_pluginManager, configSchema) {
11857
11914
  toggleShowFeatureLabels() {
11858
11915
  self.showFeatureLabels = !self.showFeatureLabels;
11859
11916
  },
11917
+ toggleShowStartCodons() {
11918
+ self.showStartCodons = !self.showStartCodons;
11919
+ },
11920
+ toggleShowStopCodons() {
11921
+ self.showStopCodons = !self.showStopCodons;
11922
+ },
11860
11923
  toggleShowCheckResults() {
11861
11924
  self.showCheckResults = !self.showCheckResults;
11862
11925
  },
@@ -11871,7 +11934,7 @@ function baseModelFactory(_pluginManager, configSchema) {
11871
11934
  const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self;
11872
11935
  return {
11873
11936
  trackMenuItems() {
11874
- const { graphical, table, showFeatureLabels, showCheckResults } = self;
11937
+ const { graphical, table, showFeatureLabels, showStartCodons, showStopCodons, showCheckResults, } = self;
11875
11938
  return [
11876
11939
  ...superTrackMenuItems(),
11877
11940
  {
@@ -11910,6 +11973,22 @@ function baseModelFactory(_pluginManager, configSchema) {
11910
11973
  self.toggleShowFeatureLabels();
11911
11974
  },
11912
11975
  },
11976
+ {
11977
+ label: 'Show start codons',
11978
+ type: 'checkbox',
11979
+ checked: showStartCodons,
11980
+ onClick: () => {
11981
+ self.toggleShowStartCodons();
11982
+ },
11983
+ },
11984
+ {
11985
+ label: 'Show stop codons',
11986
+ type: 'checkbox',
11987
+ checked: showStopCodons,
11988
+ onClick: () => {
11989
+ self.toggleShowStopCodons();
11990
+ },
11991
+ },
11913
11992
  {
11914
11993
  label: 'Check Results',
11915
11994
  type: 'checkbox',
@@ -11986,7 +12065,7 @@ function baseModelFactory(_pluginManager, configSchema) {
11986
12065
  return;
11987
12066
  }
11988
12067
  void self.session.apolloDataStore.loadFeatures(self.regions);
11989
- if (self.lgv.bpPerPx <= 3) {
12068
+ if (self.lgv.bpPerPx <= self.zoomThreshold) {
11990
12069
  void self.session.apolloDataStore.loadRefSeq(self.regions);
11991
12070
  }
11992
12071
  }, { name: 'LinearApolloSixFrameDisplayLoadFeatures', delay: 1000 }));
@@ -12178,6 +12257,28 @@ function layoutsModelFactory(pluginManager, configSchema) {
12178
12257
  }));
12179
12258
  }
12180
12259
 
12260
+ function drawCodon(ctx, codon, leftPx, index, theme, highContrast, bpPerPx, bp, rowHeight, showFeatureLabels, showStartCodons, showStopCodons) {
12261
+ const frameOffsets = (showFeatureLabels ? [0, 4, 2, 0, 14, 12, 10] : [0, 2, 1, 0, 7, 6, 5]).map((b) => b * rowHeight);
12262
+ const strands = [-1, 1];
12263
+ for (const strand of strands) {
12264
+ const frame = util.getFrame(bp, bp + 3, strand, 0);
12265
+ const top = frameOffsets.at(frame);
12266
+ if (top === undefined) {
12267
+ continue;
12268
+ }
12269
+ const left = Math.round(leftPx + index / bpPerPx);
12270
+ const width = Math.round(3 / bpPerPx) === 0 ? 1 : Math.round(3 / bpPerPx);
12271
+ const codonCode = strand === 1 ? codon : util.revcom(codon);
12272
+ const aminoAcidCode = util.defaultCodonTable[codonCode];
12273
+ const fillColor = codonColorCode(aminoAcidCode, theme, highContrast);
12274
+ if (fillColor &&
12275
+ ((showStopCodons && aminoAcidCode == '*') ||
12276
+ (showStartCodons && aminoAcidCode != '*'))) {
12277
+ ctx.fillStyle = fillColor;
12278
+ ctx.fillRect(left, top, width, rowHeight);
12279
+ }
12280
+ }
12281
+ }
12181
12282
  function renderingModelFactory(pluginManager, configSchema) {
12182
12283
  const LinearApolloSixFrameDisplayLayouts = layoutsModelFactory(pluginManager, configSchema);
12183
12284
  return LinearApolloSixFrameDisplayLayouts.named('LinearApolloSixFrameDisplayRendering')
@@ -12267,11 +12368,11 @@ function renderingModelFactory(pluginManager, configSchema) {
12267
12368
  }
12268
12369
  }, { name: 'LinearApolloSixFrameDisplayRenderCollaborators' }));
12269
12370
  mobxStateTree.addDisposer(self, mobx.autorun(() => {
12270
- const { canvas, featureLayouts, featuresHeight, lgv } = self;
12371
+ const { apolloRowHeight, canvas, featureLayouts, featuresHeight, lgv, session, theme, showFeatureLabels, showStartCodons, showStopCodons, } = self;
12271
12372
  if (!lgv.initialized || self.regionCannotBeRendered()) {
12272
12373
  return;
12273
12374
  }
12274
- const { displayedRegions, dynamicBlocks } = lgv;
12375
+ const { bpPerPx, offsetPx, displayedRegions, dynamicBlocks } = lgv;
12275
12376
  const ctx = canvas?.getContext('2d');
12276
12377
  if (!ctx) {
12277
12378
  return;
@@ -12295,6 +12396,27 @@ function renderingModelFactory(pluginManager, configSchema) {
12295
12396
  }
12296
12397
  }
12297
12398
  }
12399
+ if (showStartCodons || showStopCodons) {
12400
+ const { apolloDataStore } = session;
12401
+ for (const block of dynamicBlocks.contentBlocks) {
12402
+ const assembly = apolloDataStore.assemblies.get(block.assemblyName);
12403
+ const ref = assembly?.getByRefName(block.refName);
12404
+ const roundedStart = Math.floor(block.start);
12405
+ const roundedEnd = Math.ceil(block.end);
12406
+ let seq = ref?.getSequence(roundedStart, roundedEnd);
12407
+ if (!seq) {
12408
+ break;
12409
+ }
12410
+ seq = seq.toUpperCase();
12411
+ const baseOffsetPx = (block.start - roundedStart) / bpPerPx;
12412
+ const seqLeftPx = Math.round(block.offsetPx - offsetPx - baseOffsetPx);
12413
+ for (let i = 0; i < seq.length; i++) {
12414
+ const bp = roundedStart + i;
12415
+ const codon = seq.slice(i, i + 3);
12416
+ drawCodon(ctx, codon, seqLeftPx, i, theme, true, bpPerPx, bp, apolloRowHeight, showFeatureLabels, showStartCodons, showStopCodons);
12417
+ }
12418
+ }
12419
+ }
12298
12420
  }, { name: 'LinearApolloSixFrameDisplayRenderFeatures' }));
12299
12421
  },
12300
12422
  }));
@@ -13245,17 +13367,6 @@ function annotationFromJBrowseFeature(pluggableElement) {
13245
13367
  return pluggableElement;
13246
13368
  }
13247
13369
 
13248
- function getLeftPx(display, feature, block) {
13249
- const { lgv } = display;
13250
- const { bpPerPx, offsetPx } = lgv;
13251
- const blockLeftPx = block.offsetPx - offsetPx;
13252
- const featureLeftBpDistanceFromBlockLeftBp = block.reversed
13253
- ? block.end - feature.max
13254
- : feature.min - block.start;
13255
- const featureLeftPxDistanceFromBlockLeftPx = featureLeftBpDistanceFromBlockLeftBp / bpPerPx;
13256
- return blockLeftPx + featureLeftPxDistanceFromBlockLeftPx;
13257
- }
13258
-
13259
13370
  const CheckResultWarnings = mobxReact.observer(function CheckResultWarnings({ display, }) {
13260
13371
  const { classes } = useStyles$1();
13261
13372
  const { apolloDragging, apolloRowHeight, lgv, session, showCheckResults } = display;
@@ -13587,7 +13698,7 @@ const ResizeHandle = ({ onResize, }) => {
13587
13698
  const controller = new AbortController();
13588
13699
  const { signal } = controller;
13589
13700
  function abortDrag() {
13590
- controller.abort();
13701
+ controller.abort('makeDisplayComponent');
13591
13702
  }
13592
13703
  globalThis.addEventListener('mousemove', mouseMove, { signal });
13593
13704
  globalThis.addEventListener('mouseup', abortDrag, { signal });
@@ -13991,7 +14102,7 @@ function clientDataStoreFactory(AnnotationFeatureExtended) {
13991
14102
  statusMessage: `Loading ontology "${name}", version "${version}", this may take a while`,
13992
14103
  progressPct: 0,
13993
14104
  cancelCallback: () => {
13994
- controller.abort();
14105
+ controller.abort('ClientDataStore');
13995
14106
  jobsManager.abortJob(job.name);
13996
14107
  },
13997
14108
  };
@@ -14130,6 +14241,7 @@ function extendSession(pluginManager, sessionModel) {
14130
14241
  apolloSelectedFeature: mobxStateTree.types.safeReference(AnnotationFeatureExtended),
14131
14242
  jobsManager: mobxStateTree.types.optional(ApolloJobModel, {}),
14132
14243
  isLocked: mobxStateTree.types.optional(mobxStateTree.types.boolean, false),
14244
+ changeInProgress: mobxStateTree.types.optional(mobxStateTree.types.boolean, false),
14133
14245
  })
14134
14246
  .volatile(() => ({
14135
14247
  apolloHoveredFeature: undefined,
@@ -14192,6 +14304,9 @@ function extendSession(pluginManager, sessionModel) {
14192
14304
  toggleLocked() {
14193
14305
  self.isLocked = !self.isLocked;
14194
14306
  },
14307
+ setChangeInProgress(changeInProgress) {
14308
+ self.changeInProgress = changeInProgress;
14309
+ },
14195
14310
  getPluginConfiguration() {
14196
14311
  const { jbrowse } = mobxStateTree.getRoot(self);
14197
14312
  const pluginConfiguration =