@apollo-annotation/jbrowse-plugin-apollo 0.3.8 → 0.3.10

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 (54) hide show
  1. package/dist/index.esm.js +10914 -10799
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +10979 -10865
  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 +8799 -11326
  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 +7 -7
  12. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -1
  13. package/src/ApolloInternetAccount/model.ts +87 -65
  14. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  15. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  16. package/src/BackendDrivers/CollaborationServerDriver.ts +60 -23
  17. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  18. package/src/ChangeManager.ts +22 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +68 -212
  22. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  23. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +6 -102
  24. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +33 -232
  25. package/src/LinearApolloDisplay/glyphs/util.ts +36 -0
  26. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +174 -0
  27. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +200 -0
  28. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +62 -386
  29. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +6 -0
  30. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +122 -70
  31. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +33 -2
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +101 -3
  33. package/src/components/AddAssembly.tsx +34 -38
  34. package/src/components/AddFeature.tsx +21 -18
  35. package/src/components/AddRefSeqAliases.tsx +56 -42
  36. package/src/components/CopyFeature.tsx +1 -1
  37. package/src/components/CreateApolloAnnotation.tsx +22 -10
  38. package/src/components/DeleteAssembly.tsx +2 -9
  39. package/src/components/DownloadGFF3.tsx +2 -2
  40. package/src/components/ImportFeatures.tsx +1 -1
  41. package/src/components/ManageChecks.tsx +2 -9
  42. package/src/components/ManageUsers.tsx +23 -22
  43. package/src/components/OntologyTermAutocomplete.tsx +3 -10
  44. package/src/components/OntologyTermMultiSelect.tsx +2 -2
  45. package/src/components/ViewChangeLog.tsx +25 -50
  46. package/src/components/ViewCheckResults.tsx +1 -7
  47. package/src/config.ts +3 -3
  48. package/src/index.ts +17 -16
  49. package/src/makeDisplayComponent.tsx +9 -13
  50. package/src/session/ClientDataStore.ts +33 -15
  51. package/src/session/session.ts +23 -27
  52. package/src/util/displayUtils.ts +28 -0
  53. package/src/util/glyphUtils.ts +196 -1
  54. package/src/util/loadAssemblyIntoClient.ts +3 -2
@@ -1,35 +1,19 @@
1
1
  /* eslint-disable @typescript-eslint/unbound-method */
2
2
  /* eslint-disable @typescript-eslint/no-misused-promises */
3
3
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
4
- import { type CheckResultI } from '@apollo-annotation/mst'
5
4
  import { Menu, type MenuItem } from '@jbrowse/core/ui'
6
- import {
7
- type AbstractSessionModel,
8
- doesIntersect2,
9
- getContainingView,
10
- } from '@jbrowse/core/util'
5
+ import { getContainingView } from '@jbrowse/core/util'
11
6
  import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
12
- import ErrorIcon from '@mui/icons-material/Error'
13
7
  import LockIcon from '@mui/icons-material/Lock'
14
- import {
15
- Alert,
16
- Avatar,
17
- Badge,
18
- Box,
19
- CircularProgress,
20
- Tooltip,
21
- useTheme,
22
- } from '@mui/material'
8
+ import { Alert, CircularProgress, Tooltip, useTheme } from '@mui/material'
23
9
  import { observer } from 'mobx-react'
24
10
  import React, { useEffect, useState } from 'react'
25
11
 
26
- import {
27
- type Coord,
28
- clusterResultByMessage,
29
- useStyles,
30
- } from '../../util/displayUtils'
12
+ import { type Coord, useStyles } from '../../util/displayUtils'
31
13
  import { type LinearApolloDisplay as LinearApolloDisplayI } from '../stateModel'
32
14
 
15
+ import { CheckResultWarnings } from './CheckResultWarnings'
16
+
33
17
  interface LinearApolloDisplayProps {
34
18
  model: LinearApolloDisplayI
35
19
  }
@@ -43,8 +27,6 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
43
27
  const { model } = props
44
28
  const {
45
29
  loading,
46
- apolloDragging,
47
- apolloRowHeight,
48
30
  contextMenuItems: getContextMenuItems,
49
31
  cursor,
50
32
  featuresHeight,
@@ -59,7 +41,6 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
59
41
  setCollaboratorCanvas,
60
42
  setOverlayCanvas,
61
43
  setTheme,
62
- showCheckResults,
63
44
  } = model
64
45
  const { classes } = useStyles()
65
46
  const lgv = getContainingView(model) as unknown as LinearGenomeViewModel
@@ -73,7 +54,6 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
73
54
  if (!isShown) {
74
55
  return null
75
56
  }
76
- const { assemblyManager } = session as unknown as AbstractSessionModel
77
57
  return (
78
58
  <>
79
59
  <div
@@ -153,83 +133,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
153
133
  style={{ cursor: cursor ?? 'default' }}
154
134
  data-testid="overlayCanvas"
155
135
  />
156
- {lgv.displayedRegions.flatMap((region, idx) => {
157
- const widthBp = lgv.bpPerPx * apolloRowHeight
158
- const assembly = assemblyManager.get(region.assemblyName)
159
- if (showCheckResults) {
160
- const filteredCheckResults = [
161
- ...session.apolloDataStore.checkResults.values(),
162
- ].filter(
163
- (checkResult) =>
164
- assembly?.isValidRefName(checkResult.refSeq) &&
165
- assembly.getCanonicalRefName(checkResult.refSeq) ===
166
- region.refName &&
167
- doesIntersect2(
168
- region.start,
169
- region.end,
170
- checkResult.start,
171
- checkResult.end,
172
- ),
173
- )
174
- const checkResults = clusterResultByMessage<CheckResultI>(
175
- filteredCheckResults,
176
- widthBp,
177
- true,
178
- )
179
- return checkResults.map((checkResult) => {
180
- const left =
181
- (lgv.bpToPx({
182
- refName: region.refName,
183
- coord: checkResult.start,
184
- regionNumber: idx,
185
- })?.offsetPx ?? 0) - lgv.offsetPx
186
- const [feature] = checkResult.featureIds
187
- if (!feature) {
188
- return null
189
- }
190
- let row = 0
191
- const featureLayout = model.getFeatureLayoutPosition(feature)
192
- if (featureLayout) {
193
- row = featureLayout.layoutRow + featureLayout.featureRow
194
- }
195
- const top = row * apolloRowHeight
196
- const height = apolloRowHeight
197
- return (
198
- <Tooltip key={checkResult._id} title={checkResult.message}>
199
- <Box
200
- className={classes.box}
201
- style={{
202
- top,
203
- left,
204
- height,
205
- width: height,
206
- pointerEvents: apolloDragging ? 'none' : 'auto',
207
- }}
208
- >
209
- <Badge
210
- className={classes.badge}
211
- badgeContent={checkResult.count}
212
- color="primary"
213
- overlap="circular"
214
- anchorOrigin={{
215
- vertical: 'bottom',
216
- horizontal: 'right',
217
- }}
218
- invisible={checkResult.count <= 1}
219
- >
220
- <Avatar className={classes.avatar}>
221
- <ErrorIcon
222
- data-testid={`ErrorIcon-${checkResult.start}`}
223
- />
224
- </Avatar>
225
- </Badge>
226
- </Box>
227
- </Tooltip>
228
- )
229
- })
230
- }
231
- return null
232
- })}
136
+ <CheckResultWarnings display={model} />
233
137
  <Menu
234
138
  open={contextMenuItems.length > 0}
235
139
  onMenuItemClick={(_, callback) => {
@@ -10,21 +10,24 @@ import {
10
10
  isSessionModelWithWidgets,
11
11
  } from '@jbrowse/core/util'
12
12
  import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
13
- import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded'
14
- import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded'
15
13
  import { alpha } from '@mui/material'
16
14
 
17
15
  import { type OntologyRecord } from '../../OntologyManager'
18
16
  import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
19
- import { type ApolloSessionModel } from '../../session'
20
17
  import {
21
18
  type MousePosition,
22
19
  type MousePositionWithFeature,
23
20
  containsSelectedFeature,
21
+ getAdjacentExons,
24
22
  getMinAndMaxPx,
25
23
  getOverlappingEdge,
24
+ getStreamIcon,
25
+ isCDSFeature,
26
+ isExonFeature,
26
27
  isMousePositionWithFeature,
28
+ isTranscriptFeature,
27
29
  navToFeatureCenter,
30
+ selectFeatureAndOpenWidget,
28
31
  } from '../../util'
29
32
  import { getRelatedFeatures } from '../../util/annotationFeatureUtils'
30
33
  import { type LinearApolloDisplay } from '../stateModel'
@@ -92,9 +95,9 @@ function drawBackground(
92
95
  stateModel: LinearApolloDisplayRendering,
93
96
  displayedRegionIndex: number,
94
97
  row: number,
95
- color: string,
98
+ color?: string,
96
99
  ) {
97
- const { apolloRowHeight, lgv, session } = stateModel
100
+ const { apolloRowHeight, lgv, session, theme } = stateModel
98
101
  const { bpPerPx, displayedRegions, offsetPx } = lgv
99
102
  const displayedRegion = displayedRegions[displayedRegionIndex]
100
103
  const { refName, reversed } = displayedRegion
@@ -118,7 +121,20 @@ function drawBackground(
118
121
  const topLevelFeatureHeight =
119
122
  getRowCount(feature, featureTypeOntology) * apolloRowHeight
120
123
 
121
- ctx.fillStyle = color
124
+ let selectedColor
125
+ if (color) {
126
+ selectedColor = color
127
+ } else {
128
+ selectedColor = readConfObject(
129
+ session.getPluginConfiguration(),
130
+ 'geneBackgroundColor',
131
+ { featureType: feature.type },
132
+ ) as string
133
+ if (!selectedColor) {
134
+ selectedColor = alpha(theme.palette.background.paper, 0.6)
135
+ }
136
+ }
137
+ ctx.fillStyle = selectedColor
122
138
  ctx.fillRect(
123
139
  topLevelFeatureStartPx,
124
140
  topLevelFeatureTop,
@@ -127,18 +143,6 @@ function drawBackground(
127
143
  )
128
144
  }
129
145
 
130
- function backgroundColorForFeature(
131
- session: ApolloSessionModel,
132
- featureType: string,
133
- ): string {
134
- const color = readConfObject(
135
- session.getPluginConfiguration(),
136
- 'backgroundColorForFeature',
137
- { featureType },
138
- ) as string
139
- return color
140
- }
141
-
142
146
  function draw(
143
147
  ctx: CanvasRenderingContext2D,
144
148
  feature: AnnotationFeature,
@@ -163,25 +167,7 @@ function draw(
163
167
  }
164
168
 
165
169
  // Draw background for gene
166
- drawBackground(
167
- ctx,
168
- feature,
169
- stateModel,
170
- displayedRegionIndex,
171
- row,
172
- alpha(theme.palette.background.paper, 0.6),
173
- )
174
-
175
- if (featureTypeOntology.isTypeOf(feature.type, 'pseudogene')) {
176
- drawBackground(
177
- ctx,
178
- feature,
179
- stateModel,
180
- displayedRegionIndex,
181
- row,
182
- backgroundColorForFeature(session, 'pseudogenic_transcript'),
183
- )
184
- }
170
+ drawBackground(ctx, feature, stateModel, displayedRegionIndex, row)
185
171
 
186
172
  // Draw lines on different rows for each transcript
187
173
  let currentRow = 0
@@ -197,30 +183,7 @@ function draw(
197
183
  if (!transcriptChildren) {
198
184
  continue
199
185
  }
200
-
201
186
  const cdsCount = getCDSCount(transcript, featureTypeOntology)
202
- if (cdsCount === 0) {
203
- drawBackground(
204
- ctx,
205
- transcript,
206
- stateModel,
207
- displayedRegionIndex,
208
- currentRow,
209
- backgroundColorForFeature(session, 'nonCodingTranscript'),
210
- )
211
- }
212
- if (
213
- featureTypeOntology.isTypeOf(transcript.type, 'pseudogenic_transcript')
214
- ) {
215
- drawBackground(
216
- ctx,
217
- transcript,
218
- stateModel,
219
- displayedRegionIndex,
220
- currentRow,
221
- backgroundColorForFeature(session, 'pseudogenic_transcript'),
222
- )
223
- }
224
187
 
225
188
  for (const [, childFeature] of transcriptChildren) {
226
189
  if (!featureTypeOntology.isTypeOf(childFeature.type, 'CDS')) {
@@ -471,19 +434,21 @@ function drawLine(
471
434
  ctx.strokeStyle = theme.palette.text.primary
472
435
  const { strand = 1 } = transcript
473
436
  ctx.beginPath()
437
+ // If view is reversed, draw forward as reverse and vice versa
438
+ const effectiveStrand = strand * (reversed ? -1 : 1)
474
439
  // Draw the transcript line, and extend it out a bit on the 3` end
475
- const lineStart = startPx - (strand === -1 ? 5 : 0)
476
- const lineEnd = startPx + widthPx + (strand === -1 ? 0 : 5)
440
+ const lineStart = startPx - (effectiveStrand === -1 ? 5 : 0)
441
+ const lineEnd = startPx + widthPx + (effectiveStrand === -1 ? 0 : 5)
477
442
  ctx.moveTo(lineStart, height)
478
443
  ctx.lineTo(lineEnd, height)
479
444
  // Now to draw arrows every 20 pixels along the line
480
445
  // Make the arrow range a bit shorter to avoid an arrow hanging off the 5` end
481
- const arrowsStart = lineStart + (strand === -1 ? 0 : 3)
482
- const arrowsEnd = lineEnd - (strand === -1 ? 3 : 0)
446
+ const arrowsStart = lineStart + (effectiveStrand === -1 ? 0 : 3)
447
+ const arrowsEnd = lineEnd - (effectiveStrand === -1 ? 3 : 0)
483
448
  // Offset determines if the arrows face left or right
484
- const offset = strand === -1 ? 3 : -3
449
+ const offset = effectiveStrand === -1 ? 3 : -3
485
450
  const arrowRange =
486
- strand === -1
451
+ effectiveStrand === -1
487
452
  ? range(arrowsStart, arrowsEnd, 20)
488
453
  : range(arrowsEnd, arrowsStart, 20)
489
454
  for (const arrowLocation of arrowRange) {
@@ -736,45 +701,6 @@ function getRowForFeature(
736
701
  return
737
702
  }
738
703
 
739
- function selectFeatureAndOpenWidget(
740
- stateModel: LinearApolloDisplayMouseEvents,
741
- feature: AnnotationFeature,
742
- ) {
743
- if (stateModel.apolloDragging) {
744
- return
745
- }
746
- stateModel.setSelectedFeature(feature)
747
- const { session } = stateModel
748
- const { apolloDataStore } = session
749
- const { featureTypeOntology } = apolloDataStore.ontologyManager
750
- if (!featureTypeOntology) {
751
- throw new Error('featureTypeOntology is undefined')
752
- }
753
-
754
- let containsCDSOrExon = false
755
- for (const [, child] of feature.children ?? []) {
756
- if (
757
- featureTypeOntology.isTypeOf(child.type, 'CDS') ||
758
- featureTypeOntology.isTypeOf(child.type, 'exon')
759
- ) {
760
- containsCDSOrExon = true
761
- break
762
- }
763
- }
764
- if (
765
- (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
766
- featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
767
- containsCDSOrExon
768
- ) {
769
- stateModel.showFeatureDetailsWidget(feature, [
770
- 'ApolloTranscriptDetails',
771
- 'apolloTranscriptDetails',
772
- ])
773
- } else {
774
- stateModel.showFeatureDetailsWidget(feature)
775
- }
776
- }
777
-
778
704
  function onMouseDown(
779
705
  stateModel: LinearApolloDisplay,
780
706
  currentMousePosition: MousePositionWithFeature,
@@ -899,131 +825,6 @@ function getDraggableFeatureInfo(
899
825
  return
900
826
  }
901
827
 
902
- function isTranscriptFeature(
903
- feature: AnnotationFeature,
904
- session: ApolloSessionModel,
905
- ): boolean {
906
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
907
- if (!featureTypeOntology) {
908
- throw new Error('featureTypeOntology is undefined')
909
- }
910
- return (
911
- featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
912
- featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
913
- )
914
- }
915
-
916
- function isExonFeature(
917
- feature: AnnotationFeature,
918
- session: ApolloSessionModel,
919
- ): boolean {
920
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
921
- if (!featureTypeOntology) {
922
- throw new Error('featureTypeOntology is undefined')
923
- }
924
- return featureTypeOntology.isTypeOf(feature.type, 'exon')
925
- }
926
-
927
- function isCDSFeature(
928
- feature: AnnotationFeature,
929
- session: ApolloSessionModel,
930
- ): boolean {
931
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
932
- if (!featureTypeOntology) {
933
- throw new Error('featureTypeOntology is undefined')
934
- }
935
- return featureTypeOntology.isTypeOf(feature.type, 'CDS')
936
- }
937
-
938
- interface AdjacentExons {
939
- upstream: AnnotationFeature | undefined
940
- downstream: AnnotationFeature | undefined
941
- }
942
-
943
- function getAdjacentExons(
944
- currentExon: AnnotationFeature,
945
- display: LinearApolloDisplayMouseEvents,
946
- mousePosition: MousePositionWithFeature,
947
- session: ApolloSessionModel,
948
- ): AdjacentExons {
949
- const lgv = getContainingView(
950
- display as BaseDisplayModel,
951
- ) as unknown as LinearGenomeViewModel
952
-
953
- // Genomic coords of current view
954
- const viewGenomicLeft = mousePosition.bp - lgv.bpPerPx * mousePosition.x
955
- const viewGenomicRight = viewGenomicLeft + lgv.coarseTotalBp
956
- if (!currentExon.parent) {
957
- return { upstream: undefined, downstream: undefined }
958
- }
959
- const transcript = currentExon.parent
960
- if (!transcript.children) {
961
- throw new Error(`Error getting children of ${transcript._id}`)
962
- }
963
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
964
- if (!featureTypeOntology) {
965
- throw new Error('featureTypeOntology is undefined')
966
- }
967
-
968
- let exons = []
969
- for (const [, child] of transcript.children) {
970
- if (featureTypeOntology.isTypeOf(child.type, 'exon')) {
971
- exons.push(child)
972
- }
973
- }
974
- const adjacentExons: AdjacentExons = {
975
- upstream: undefined,
976
- downstream: undefined,
977
- }
978
- exons = exons.sort((a, b) => (a.min < b.min ? -1 : 1))
979
- for (const exon of exons) {
980
- if (exon.min > viewGenomicRight) {
981
- adjacentExons.downstream = exon
982
- break
983
- }
984
- }
985
- exons = exons.sort((a, b) => (a.min > b.min ? -1 : 1))
986
- for (const exon of exons) {
987
- if (exon.max < viewGenomicLeft) {
988
- adjacentExons.upstream = exon
989
- break
990
- }
991
- }
992
- if (transcript.strand === -1) {
993
- const newUpstream = adjacentExons.downstream
994
- adjacentExons.downstream = adjacentExons.upstream
995
- adjacentExons.upstream = newUpstream
996
- }
997
- return adjacentExons
998
- }
999
-
1000
- function getStreamIcon(
1001
- strand: 1 | -1 | undefined,
1002
- isUpstream: boolean,
1003
- isFlipped: boolean | undefined,
1004
- ) {
1005
- // This is the icon you would use for strand=1, downstream, straight
1006
- // (non-flipped) view
1007
- let icon = SkipNextRoundedIcon
1008
-
1009
- if (strand === -1) {
1010
- icon = SkipPreviousRoundedIcon
1011
- }
1012
- if (isUpstream) {
1013
- icon =
1014
- icon === SkipPreviousRoundedIcon
1015
- ? SkipNextRoundedIcon
1016
- : SkipPreviousRoundedIcon
1017
- }
1018
- if (isFlipped) {
1019
- icon =
1020
- icon === SkipPreviousRoundedIcon
1021
- ? SkipNextRoundedIcon
1022
- : SkipPreviousRoundedIcon
1023
- }
1024
- return icon
1025
- }
1026
-
1027
828
  function getContextMenuItems(
1028
829
  display: LinearApolloDisplayMouseEvents,
1029
830
  mousePosition: MousePositionWithFeature,
@@ -1173,8 +974,8 @@ function getContextMenuItems(
1173
974
  },
1174
975
  })
1175
976
  if (isSessionModelWithWidgets(session)) {
1176
- contextMenuItemsForFeature.push({
1177
- label: 'Open transcript details',
977
+ contextMenuItemsForFeature.splice(1, 0, {
978
+ label: 'Open transcript editor',
1178
979
  onClick: () => {
1179
980
  const apolloTranscriptWidget = session.addWidget(
1180
981
  'ApolloTranscriptDetails',
@@ -0,0 +1,36 @@
1
+ import { type ContentBlock } from '@jbrowse/core/util/blockTypes'
2
+
3
+ import { type LinearApolloDisplay } from '../stateModel'
4
+
5
+ export function getLeftPx(
6
+ display: LinearApolloDisplay,
7
+ feature: { max: number; min: number },
8
+ block: ContentBlock,
9
+ ) {
10
+ const { lgv } = display
11
+ const { bpPerPx, offsetPx } = lgv
12
+ const blockLeftPx = block.offsetPx - offsetPx
13
+ const featureLeftBpDistanceFromBlockLeftBp = block.reversed
14
+ ? block.end - feature.max
15
+ : feature.min - block.start
16
+ const featureLeftPxDistanceFromBlockLeftPx =
17
+ featureLeftBpDistanceFromBlockLeftBp / bpPerPx
18
+ return blockLeftPx + featureLeftPxDistanceFromBlockLeftPx
19
+ }
20
+
21
+ /**
22
+ * Perform a canvas strokeRect, but have the stroke be contained within the
23
+ * given rect instead of centered on it.
24
+ */
25
+ export function strokeRectInner(
26
+ ctx: CanvasRenderingContext2D,
27
+ left: number,
28
+ top: number,
29
+ width: number,
30
+ height: number,
31
+ color: string,
32
+ ) {
33
+ ctx.strokeStyle = color
34
+ ctx.lineWidth = 1
35
+ ctx.strokeRect(left + 0.5, top + 0.5, width - 1, height - 1)
36
+ }
@@ -0,0 +1,174 @@
1
+ import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import { type Frame, getFrame } from '@jbrowse/core/util'
3
+ import { type BlockSet, type ContentBlock } from '@jbrowse/core/util/blockTypes'
4
+ import { type Theme } from '@mui/material'
5
+
6
+ import { type ApolloSessionModel, type HoveredFeature } from '../session'
7
+
8
+ function getSeqRow(
9
+ strand: 1 | -1 | undefined,
10
+ bpPerPx: number,
11
+ reversed?: boolean,
12
+ ): number | undefined {
13
+ if (bpPerPx > 1 || strand === undefined) {
14
+ return
15
+ }
16
+ if (reversed) {
17
+ return strand === 1 ? 4 : 3
18
+ }
19
+ return strand === 1 ? 3 : 4
20
+ }
21
+
22
+ function getTranslationRow(frame: Frame, bpPerPx: number, reversed?: boolean) {
23
+ const frameRows = bpPerPx <= 1 ? [2, 1, 0, 7, 6, 5] : [2, 1, 0, 5, 4, 3]
24
+ if (reversed) {
25
+ frameRows.reverse()
26
+ }
27
+ frameRows.unshift(0)
28
+ const row = frameRows.at(frame)
29
+ if (row === undefined) {
30
+ throw new Error('could not find row')
31
+ }
32
+ return row
33
+ }
34
+
35
+ function getLeftPx(
36
+ feature: { min: number; max: number },
37
+ bpPerPx: number,
38
+ offsetPx: number,
39
+ block: ContentBlock,
40
+ ) {
41
+ const blockLeftPx = block.offsetPx - offsetPx
42
+ const featureLeftBpDistanceFromBlockLeftBp = block.reversed
43
+ ? block.end - feature.max
44
+ : feature.min - block.start
45
+ const featureLeftPxDistanceFromBlockLeftPx =
46
+ featureLeftBpDistanceFromBlockLeftBp / bpPerPx
47
+ return blockLeftPx + featureLeftPxDistanceFromBlockLeftPx
48
+ }
49
+
50
+ function fillAndStrokeRect(
51
+ ctx: CanvasRenderingContext2D,
52
+ left: number,
53
+ top: number,
54
+ width: number,
55
+ height: number,
56
+ theme: Theme,
57
+ selected = false,
58
+ ) {
59
+ ctx.fillStyle = selected
60
+ ? theme.palette.action.disabled
61
+ : theme.palette.action.focus
62
+ ctx.fillRect(left, top, width, height)
63
+ ctx.strokeStyle = selected
64
+ ? theme.palette.text.secondary
65
+ : theme.palette.text.primary
66
+ ctx.strokeStyle = theme.palette.text.primary
67
+ ctx.strokeRect(left, top, width, height)
68
+ }
69
+
70
+ function drawHighlight(
71
+ ctx: CanvasRenderingContext2D,
72
+ feature: AnnotationFeature,
73
+ bpPerPx: number,
74
+ offsetPx: number,
75
+ rowHeight: number,
76
+ block: ContentBlock,
77
+ theme: Theme,
78
+ selected = false,
79
+ ) {
80
+ const row = getSeqRow(feature.strand, bpPerPx, block.reversed)
81
+ if (!row) {
82
+ return
83
+ }
84
+ const left = getLeftPx(feature, bpPerPx, offsetPx, block)
85
+ const width = feature.length / bpPerPx
86
+ const top = row * rowHeight
87
+ fillAndStrokeRect(ctx, left, top, width, rowHeight, theme, selected)
88
+ }
89
+
90
+ function drawCDSHighlight(
91
+ ctx: CanvasRenderingContext2D,
92
+ feature: AnnotationFeature,
93
+ bpPerPx: number,
94
+ offsetPx: number,
95
+ rowHeight: number,
96
+ block: ContentBlock,
97
+ theme: Theme,
98
+ selected = false,
99
+ ) {
100
+ const parentFeature = feature.parent
101
+ if (!parentFeature) {
102
+ return
103
+ }
104
+ const cdsLocs = parentFeature.cdsLocations.find((loc) => {
105
+ const min = loc.at(feature.strand === 1 ? 0 : -1)?.min
106
+ const max = loc.at(feature.strand === 1 ? -1 : 0)?.max
107
+ return feature.min === min && feature.max === max
108
+ })
109
+ if (!cdsLocs) {
110
+ return
111
+ }
112
+ for (const loc of cdsLocs) {
113
+ const frame = getFrame(loc.min, loc.max, feature.strand ?? 1, loc.phase)
114
+ const row = getTranslationRow(frame, bpPerPx, block.reversed)
115
+ const left = getLeftPx(loc, bpPerPx, offsetPx, block)
116
+ const top = row * rowHeight
117
+ const width = (loc.max - loc.min) / bpPerPx
118
+ fillAndStrokeRect(ctx, left, top, width, rowHeight, theme, selected)
119
+ }
120
+ }
121
+
122
+ export function drawSequenceOverlay(
123
+ canvas: HTMLCanvasElement,
124
+ ctx: CanvasRenderingContext2D,
125
+ hoveredFeature: HoveredFeature | undefined,
126
+ selectedFeature: AnnotationFeature | undefined,
127
+ rowHeight: number,
128
+ theme: Theme,
129
+ session: ApolloSessionModel,
130
+ bpPerPx: number,
131
+ offsetPx: number,
132
+ dynamicBlocks: BlockSet,
133
+ ) {
134
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
135
+ if (!featureTypeOntology) {
136
+ throw new Error('featureTypeOntology is undefined')
137
+ }
138
+ for (const block of dynamicBlocks.contentBlocks) {
139
+ ctx.save()
140
+ ctx.beginPath()
141
+ const blockLeftPx = block.offsetPx - offsetPx
142
+ ctx.rect(blockLeftPx, 0, block.widthPx, canvas.height)
143
+ ctx.clip()
144
+ for (const feature of [
145
+ selectedFeature,
146
+ hoveredFeature?.feature,
147
+ ].filter<AnnotationFeature>((f) => f !== undefined)) {
148
+ if (featureTypeOntology.isTypeOf(feature.type, 'CDS')) {
149
+ drawCDSHighlight(
150
+ ctx,
151
+ feature,
152
+ bpPerPx,
153
+ offsetPx,
154
+ rowHeight,
155
+ block,
156
+ theme,
157
+ feature._id === selectedFeature?._id,
158
+ )
159
+ } else {
160
+ drawHighlight(
161
+ ctx,
162
+ feature,
163
+ bpPerPx,
164
+ offsetPx,
165
+ rowHeight,
166
+ block,
167
+ theme,
168
+ feature._id === selectedFeature?._id,
169
+ )
170
+ }
171
+ }
172
+ ctx.restore()
173
+ }
174
+ }