react-msaview 5.9.0 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.js +76 -76
- package/bundle/index.js.map +3 -3
- package/dist/components/ConservationTrack.js +21 -16
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +17 -23
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/TextTrack.js +26 -21
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +39 -72
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +19 -16
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +13 -6
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +31 -31
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/overlayColors.d.ts +2 -0
- package/dist/components/overlayColors.js +5 -0
- package/dist/components/overlayColors.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +29 -24
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +19 -12
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
- package/dist/components/tree/renderTreeCanvas.js +17 -17
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/model.js +82 -113
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +12 -19
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.js +32 -43
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.js +3 -0
- package/dist/renderToSvg.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +6 -19
- package/dist/seqPosToGlobalCol.js +6 -33
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/useCanvasAutorun.d.ts +23 -1
- package/dist/useCanvasAutorun.js +20 -8
- package/dist/useCanvasAutorun.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ConservationTrack.tsx +7 -5
- package/src/components/MSAView.tsx +24 -30
- package/src/components/TextTrack.tsx +7 -12
- package/src/components/header/settingsMenuItems.ts +40 -72
- package/src/components/msa/MSACanvasBlock.tsx +16 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
- package/src/components/msa/renderMSAMouseover.ts +37 -32
- package/src/components/overlayColors.ts +6 -0
- package/src/components/tree/TreeCanvas.tsx +6 -4
- package/src/components/tree/TreeCanvasBlock.tsx +11 -12
- package/src/components/tree/renderTreeCanvas.ts +20 -17
- package/src/model.ts +97 -130
- package/src/modelFilehandleLoaders.test.ts +137 -0
- package/src/neighborJoining.test.ts +12 -0
- package/src/neighborJoining.ts +12 -20
- package/src/parseAsn1.ts +32 -40
- package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
- package/src/renderToSvg.tsx +3 -0
- package/src/seqPosToGlobalCol.test.ts +39 -51
- package/src/seqPosToGlobalCol.ts +6 -41
- package/src/useCanvasAutorun.test.tsx +98 -0
- package/src/useCanvasAutorun.ts +30 -11
- package/src/version.ts +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clickColor,
|
|
3
|
+
hoverColor,
|
|
4
|
+
multiRowHoverColor,
|
|
5
|
+
referenceColor,
|
|
6
|
+
} from '../overlayColors.ts'
|
|
2
7
|
|
|
3
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
4
9
|
|
|
5
|
-
const hoverColor = 'rgba(0,0,0,0.15)'
|
|
6
|
-
const highlightColor = 'rgba(128,128,0,0.2)'
|
|
7
10
|
// the persistent highlightColumns overlay: a stronger fill plus a solid border
|
|
8
11
|
// so a domain/motif band reads clearly over the colored alignment cells (the
|
|
9
12
|
// faint hover-style wash alone is invisible against clustalx coloring)
|
|
@@ -39,48 +42,50 @@ export function renderMouseover({
|
|
|
39
42
|
ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
|
|
40
43
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
41
44
|
|
|
42
|
-
//
|
|
45
|
+
// rows and columns sit at a fixed pitch, so an index is all it takes to place
|
|
46
|
+
// a full-width or full-height band (matching fillRow in TreeCanvas)
|
|
47
|
+
const rowBand = (index: number) => {
|
|
48
|
+
ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
|
|
49
|
+
}
|
|
50
|
+
const colBand = (index: number) => {
|
|
51
|
+
ctx.fillRect(index * colWidth + scrollX, 0, colWidth, height)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// the reference row (relativeTo) stays lit, as does every tip under a hovered
|
|
55
|
+
// tree node
|
|
43
56
|
if (referenceRowIndex !== undefined) {
|
|
44
57
|
ctx.fillStyle = referenceColor
|
|
45
|
-
|
|
58
|
+
rowBand(referenceRowIndex)
|
|
46
59
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ctx.fillStyle = multiRowHoverColor
|
|
51
|
-
for (const rowIndex of hoveredRowIndices) {
|
|
52
|
-
ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
|
|
53
|
-
}
|
|
60
|
+
ctx.fillStyle = multiRowHoverColor
|
|
61
|
+
for (const rowIndex of hoveredRowIndices) {
|
|
62
|
+
rowBand(rowIndex)
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
// band so a domain/motif highlight
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ctx.strokeRect(x, 0, w, height)
|
|
67
|
-
}
|
|
65
|
+
// each contiguous run of highlighted columns draws as one filled, bordered
|
|
66
|
+
// band, so a domain/motif highlight stays visible over the alignment
|
|
67
|
+
ctx.lineWidth = 2
|
|
68
|
+
for (const { start, end } of highlightedColumnRuns) {
|
|
69
|
+
const x = start * colWidth + scrollX
|
|
70
|
+
const w = (end - start + 1) * colWidth
|
|
71
|
+
ctx.fillStyle = highlightColumnsFill
|
|
72
|
+
ctx.fillRect(x, 0, w, height)
|
|
73
|
+
ctx.strokeStyle = highlightColumnsBorder
|
|
74
|
+
ctx.strokeRect(x, 0, w, height)
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
ctx.fillStyle = hoverColor
|
|
70
78
|
if (mouseCol !== undefined) {
|
|
71
|
-
|
|
72
|
-
ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
|
|
79
|
+
colBand(mouseCol)
|
|
73
80
|
}
|
|
74
81
|
if (mouseRow !== undefined) {
|
|
75
|
-
|
|
76
|
-
ctx.fillRect(0, mouseRow * rowHeight + scrollY, width, rowHeight)
|
|
82
|
+
rowBand(mouseRow)
|
|
77
83
|
}
|
|
84
|
+
ctx.fillStyle = clickColor
|
|
78
85
|
if (mouseClickCol !== undefined) {
|
|
79
|
-
|
|
80
|
-
ctx.fillRect(mouseClickCol * colWidth + scrollX, 0, colWidth, height)
|
|
86
|
+
colBand(mouseClickCol)
|
|
81
87
|
}
|
|
82
88
|
if (mouseClickRow !== undefined) {
|
|
83
|
-
|
|
84
|
-
ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
|
|
89
|
+
rowBand(mouseClickRow)
|
|
85
90
|
}
|
|
86
91
|
}
|
|
@@ -10,3 +10,9 @@ export const referenceColor = 'rgba(0,128,255,0.3)'
|
|
|
10
10
|
// while the alignment cells are already colored so a lighter wash reads better.
|
|
11
11
|
export const treeHoverColor = 'rgba(255,165,0,0.2)'
|
|
12
12
|
export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
|
|
13
|
+
|
|
14
|
+
// the hovered column/row, and the one pinned by a click. Drawn on the alignment
|
|
15
|
+
// overlay canvas and again as plain divs over the track area, which have to
|
|
16
|
+
// match or the crosshair appears to change color as it crosses the tracks.
|
|
17
|
+
export const hoverColor = 'rgba(0,0,0,0.15)'
|
|
18
|
+
export const clickColor = 'rgba(128,128,0,0.2)'
|
|
@@ -22,8 +22,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
22
|
)
|
|
23
23
|
const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
|
|
24
24
|
|
|
25
|
-
const mouseoverRef = useCanvasAutorun(
|
|
26
|
-
ctx => {
|
|
25
|
+
const mouseoverRef = useCanvasAutorun({
|
|
26
|
+
draw: ctx => {
|
|
27
27
|
if (isAlive(model)) {
|
|
28
28
|
const {
|
|
29
29
|
rowHeight,
|
|
@@ -61,8 +61,10 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
width: treeAreaWidth,
|
|
65
|
+
height,
|
|
66
|
+
deps: [model],
|
|
67
|
+
})
|
|
66
68
|
|
|
67
69
|
return (
|
|
68
70
|
<div
|
|
@@ -60,15 +60,12 @@ const TreeCanvasBlock = observer(function ({
|
|
|
60
60
|
const width = treeAreaWidth + padding
|
|
61
61
|
const height = blockSize
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const canvasWidth = width * highResScaleFactor
|
|
64
|
+
const canvasHeight = height * highResScaleFactor
|
|
65
|
+
const ref = useCanvasAutorun({
|
|
66
|
+
draw: ctx => {
|
|
65
67
|
ctx.resetTransform()
|
|
66
|
-
ctx.clearRect(
|
|
67
|
-
0,
|
|
68
|
-
0,
|
|
69
|
-
width * highResScaleFactor,
|
|
70
|
-
height * highResScaleFactor,
|
|
71
|
-
)
|
|
68
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
|
72
69
|
renderTreeCanvas({
|
|
73
70
|
ctx,
|
|
74
71
|
model,
|
|
@@ -77,8 +74,10 @@ const TreeCanvasBlock = observer(function ({
|
|
|
77
74
|
theme,
|
|
78
75
|
})
|
|
79
76
|
},
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
width: canvasWidth,
|
|
78
|
+
height: canvasHeight,
|
|
79
|
+
deps: [model, clickMap, offsetY, theme],
|
|
80
|
+
})
|
|
82
81
|
|
|
83
82
|
const style = {
|
|
84
83
|
width,
|
|
@@ -112,8 +111,8 @@ const TreeCanvasBlock = observer(function ({
|
|
|
112
111
|
|
|
113
112
|
<canvas
|
|
114
113
|
ref={ref}
|
|
115
|
-
width={
|
|
116
|
-
height={
|
|
114
|
+
width={canvasWidth}
|
|
115
|
+
height={canvasHeight}
|
|
117
116
|
style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
|
|
118
117
|
onMouseMove={event => {
|
|
119
118
|
onMouseMove(event)
|
|
@@ -45,7 +45,7 @@ export function getNodeX(
|
|
|
45
45
|
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function renderTree({
|
|
49
49
|
offsetY,
|
|
50
50
|
ctx,
|
|
51
51
|
model,
|
|
@@ -89,7 +89,7 @@ export function renderTree({
|
|
|
89
89
|
})
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
function renderCollapsedTriangles({
|
|
93
93
|
ctx,
|
|
94
94
|
clickMap,
|
|
95
95
|
offsetY,
|
|
@@ -98,6 +98,7 @@ export function renderCollapsedTriangles({
|
|
|
98
98
|
tipX,
|
|
99
99
|
maxDepthToLeaf,
|
|
100
100
|
blockSizeYOverride,
|
|
101
|
+
collapsedSet,
|
|
101
102
|
}: {
|
|
102
103
|
ctx: RenderCtx
|
|
103
104
|
clickMap?: ClickMapIndex
|
|
@@ -107,11 +108,11 @@ export function renderCollapsedTriangles({
|
|
|
107
108
|
tipX: number
|
|
108
109
|
maxDepthToLeaf: number
|
|
109
110
|
blockSizeYOverride?: number
|
|
111
|
+
collapsedSet: Set<string>
|
|
110
112
|
}) {
|
|
111
113
|
const {
|
|
112
114
|
hierarchy,
|
|
113
115
|
showBranchLenEffective: showBranchLen,
|
|
114
|
-
collapsed,
|
|
115
116
|
blockSize,
|
|
116
117
|
rowHeight,
|
|
117
118
|
fontSize,
|
|
@@ -119,10 +120,9 @@ export function renderCollapsedTriangles({
|
|
|
119
120
|
} = model
|
|
120
121
|
// nothing collapsed is the common case, and the traversal below visits every
|
|
121
122
|
// node in the tree on every block of every redraw
|
|
122
|
-
if (
|
|
123
|
+
if (collapsedSet.size === 0) {
|
|
123
124
|
return
|
|
124
125
|
}
|
|
125
|
-
const collapsedSet = new Set(collapsed)
|
|
126
126
|
const by = blockSizeYOverride ?? blockSize
|
|
127
127
|
const halfHeight = Math.max(2, rowHeight * 0.42)
|
|
128
128
|
forEachDescendant(hierarchy, node => {
|
|
@@ -171,7 +171,7 @@ export function renderCollapsedTriangles({
|
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
function renderNodeBubbles({
|
|
175
175
|
ctx,
|
|
176
176
|
clickMap,
|
|
177
177
|
offsetY,
|
|
@@ -179,6 +179,7 @@ export function renderNodeBubbles({
|
|
|
179
179
|
tipX,
|
|
180
180
|
maxDepthToLeaf,
|
|
181
181
|
blockSizeYOverride,
|
|
182
|
+
collapsedSet,
|
|
182
183
|
}: {
|
|
183
184
|
ctx: RenderCtx
|
|
184
185
|
clickMap?: ClickMapIndex
|
|
@@ -187,16 +188,15 @@ export function renderNodeBubbles({
|
|
|
187
188
|
tipX: number
|
|
188
189
|
maxDepthToLeaf: number
|
|
189
190
|
blockSizeYOverride?: number
|
|
191
|
+
collapsedSet: Set<string>
|
|
190
192
|
}) {
|
|
191
193
|
const {
|
|
192
194
|
hierarchy,
|
|
193
195
|
showBranchLenEffective: showBranchLen,
|
|
194
|
-
collapsed,
|
|
195
196
|
blockSize,
|
|
196
197
|
marginLeft: ml,
|
|
197
198
|
} = model
|
|
198
199
|
const by = blockSizeYOverride ?? blockSize
|
|
199
|
-
const collapsedSet = new Set(collapsed)
|
|
200
200
|
forEachDescendant(hierarchy, node => {
|
|
201
201
|
const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
|
|
202
202
|
if (x === undefined) {
|
|
@@ -231,7 +231,7 @@ export function renderNodeBubbles({
|
|
|
231
231
|
})
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
function renderTreeLabels({
|
|
235
235
|
theme,
|
|
236
236
|
model,
|
|
237
237
|
offsetY,
|
|
@@ -240,6 +240,7 @@ export function renderTreeLabels({
|
|
|
240
240
|
tipX,
|
|
241
241
|
maxDepthToLeaf,
|
|
242
242
|
blockSizeYOverride,
|
|
243
|
+
collapsedSet,
|
|
243
244
|
}: {
|
|
244
245
|
model: MsaViewModel
|
|
245
246
|
offsetY: number
|
|
@@ -249,6 +250,7 @@ export function renderTreeLabels({
|
|
|
249
250
|
tipX: number
|
|
250
251
|
maxDepthToLeaf: number
|
|
251
252
|
blockSizeYOverride?: number
|
|
253
|
+
collapsedSet: Set<string>
|
|
252
254
|
}) {
|
|
253
255
|
const {
|
|
254
256
|
fontSize,
|
|
@@ -262,16 +264,11 @@ export function renderTreeLabels({
|
|
|
262
264
|
marginLeft,
|
|
263
265
|
noTree,
|
|
264
266
|
labelWidthMap,
|
|
265
|
-
collapsed,
|
|
266
267
|
} = model
|
|
268
|
+
const by = blockSizeYOverride ?? blockSize
|
|
267
269
|
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
268
270
|
// same slice the MSA renderer uses instead of walking every tip per block
|
|
269
|
-
const visibleLeaves = getVisibleLeaves({
|
|
270
|
-
model,
|
|
271
|
-
offsetY,
|
|
272
|
-
blockSizeY: blockSizeYOverride ?? model.blockSize,
|
|
273
|
-
})
|
|
274
|
-
const by = blockSizeYOverride ?? blockSize
|
|
271
|
+
const visibleLeaves = getVisibleLeaves({ model, offsetY, blockSizeY: by })
|
|
275
272
|
const emHeight = ctx.measureText('M').width
|
|
276
273
|
if (labelsAlignRight) {
|
|
277
274
|
ctx.textAlign = 'right'
|
|
@@ -289,7 +286,7 @@ export function renderTreeLabels({
|
|
|
289
286
|
const displayName = treeMetadata[name]?.genome || name
|
|
290
287
|
// a collapsed clade is drawn as a triangle + tip count; suppress its leaf
|
|
291
288
|
// label when the "name" is just the auto-generated internal-node id
|
|
292
|
-
const isAnonymousCollapsed =
|
|
289
|
+
const isAnonymousCollapsed = collapsedSet.has(id) && name === id
|
|
293
290
|
if (!isAnonymousCollapsed && inYBlock(y, offsetY, by)) {
|
|
294
291
|
// note: +rowHeight/4 matches with -rowHeight/4 in msa
|
|
295
292
|
const yp = y + fontSize / 4
|
|
@@ -389,6 +386,9 @@ export function renderTreeCanvas({
|
|
|
389
386
|
// maxBranchLength for the cladogram would pin every node to x=0 for a tree
|
|
390
387
|
// with no branch lengths -- which is exactly what forces cladogram mode.
|
|
391
388
|
const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
|
|
389
|
+
// built once for all three passes: each of them asks about a different node on
|
|
390
|
+
// every block of every redraw, and `collapsed` is a list
|
|
391
|
+
const collapsedSet = new Set(model.collapsed)
|
|
392
392
|
|
|
393
393
|
if (!noTree && drawTree) {
|
|
394
394
|
renderTree({
|
|
@@ -410,6 +410,7 @@ export function renderTreeCanvas({
|
|
|
410
410
|
tipX,
|
|
411
411
|
maxDepthToLeaf,
|
|
412
412
|
blockSizeYOverride,
|
|
413
|
+
collapsedSet,
|
|
413
414
|
})
|
|
414
415
|
|
|
415
416
|
if (drawNodeBubbles) {
|
|
@@ -421,6 +422,7 @@ export function renderTreeCanvas({
|
|
|
421
422
|
tipX,
|
|
422
423
|
maxDepthToLeaf,
|
|
423
424
|
blockSizeYOverride,
|
|
425
|
+
collapsedSet,
|
|
424
426
|
})
|
|
425
427
|
}
|
|
426
428
|
}
|
|
@@ -435,6 +437,7 @@ export function renderTreeCanvas({
|
|
|
435
437
|
tipX,
|
|
436
438
|
maxDepthToLeaf,
|
|
437
439
|
blockSizeYOverride,
|
|
440
|
+
collapsedSet,
|
|
438
441
|
})
|
|
439
442
|
}
|
|
440
443
|
|
package/src/model.ts
CHANGED
|
@@ -1635,8 +1635,8 @@ function stateModelFactory() {
|
|
|
1635
1635
|
return 0
|
|
1636
1636
|
}
|
|
1637
1637
|
const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos]
|
|
1638
|
-
// past the end of the ungapped sequence: 0 for the degenerate
|
|
1639
|
-
//
|
|
1638
|
+
// past the end of the ungapped sequence: 0 for the degenerate all-gap
|
|
1639
|
+
// row, otherwise one past the last column
|
|
1640
1640
|
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
1641
1641
|
},
|
|
1642
1642
|
|
|
@@ -2084,79 +2084,100 @@ function stateModelFactory() {
|
|
|
2084
2084
|
})
|
|
2085
2085
|
}
|
|
2086
2086
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2087
|
+
/**
|
|
2088
|
+
* Fetch a filehandle whenever it changes, and hand the text to
|
|
2089
|
+
* `onLoad`.
|
|
2090
|
+
*
|
|
2091
|
+
* Every loader carries a generation guard: the filehandle can change
|
|
2092
|
+
* mid-fetch (a second file picked while the first is still in flight),
|
|
2093
|
+
* and the slower earlier request must not clobber the data, status, or
|
|
2094
|
+
* loading flag belonging to the newer one.
|
|
2095
|
+
*
|
|
2096
|
+
* `clearFilehandle` serves two purposes for the loaders that pass it.
|
|
2097
|
+
* A local file has no URL to refetch from, so the handle is dropped
|
|
2098
|
+
* once its bytes are in the model; and a fetch the user cancels drops
|
|
2099
|
+
* it too, returning the view to the import form rather than leaving a
|
|
2100
|
+
* stuck spinner.
|
|
2101
|
+
*/
|
|
2102
|
+
const loadOnFilehandleChange = ({
|
|
2103
|
+
getFilehandle,
|
|
2104
|
+
onLoad,
|
|
2105
|
+
setLoading,
|
|
2106
|
+
clearFilehandle,
|
|
2107
|
+
}: {
|
|
2108
|
+
getFilehandle: () => FileLocationType | undefined
|
|
2109
|
+
onLoad: (text: string) => void
|
|
2110
|
+
setLoading?: (arg: boolean) => void
|
|
2111
|
+
clearFilehandle?: () => void
|
|
2112
|
+
}) => {
|
|
2113
|
+
let generation = 0
|
|
2114
|
+
addDisposer(
|
|
2115
|
+
self,
|
|
2116
|
+
autorun(async () => {
|
|
2117
|
+
const filehandle = getFilehandle()
|
|
2118
|
+
if (!filehandle) {
|
|
2119
|
+
return
|
|
2120
|
+
}
|
|
2121
|
+
const current = ++generation
|
|
2122
|
+
const isCurrent = () => current === generation
|
|
2097
2123
|
try {
|
|
2098
|
-
|
|
2124
|
+
setLoading?.(true)
|
|
2125
|
+
self.setError(undefined)
|
|
2099
2126
|
const text = await fetchTextWithProgress(
|
|
2100
|
-
openLocation(
|
|
2127
|
+
openLocation(filehandle),
|
|
2101
2128
|
status => {
|
|
2102
|
-
if (
|
|
2129
|
+
if (isCurrent()) {
|
|
2103
2130
|
self.setStatus(status)
|
|
2104
2131
|
}
|
|
2105
2132
|
},
|
|
2106
2133
|
)
|
|
2107
|
-
if (
|
|
2134
|
+
if (isCurrent()) {
|
|
2108
2135
|
transaction(() => {
|
|
2109
|
-
|
|
2110
|
-
if (
|
|
2111
|
-
|
|
2112
|
-
self.setTreeFilehandle(undefined)
|
|
2136
|
+
onLoad(text)
|
|
2137
|
+
if (filehandle.locationType === 'BlobLocation') {
|
|
2138
|
+
clearFilehandle?.()
|
|
2113
2139
|
}
|
|
2114
2140
|
})
|
|
2115
2141
|
}
|
|
2116
2142
|
} catch (e) {
|
|
2117
|
-
if (
|
|
2143
|
+
if (isCurrent()) {
|
|
2118
2144
|
if (isAbortError(e)) {
|
|
2119
|
-
|
|
2120
|
-
// returns to the import form instead of a stuck spinner
|
|
2121
|
-
self.setTreeFilehandle(undefined)
|
|
2145
|
+
clearFilehandle?.()
|
|
2122
2146
|
} else {
|
|
2123
2147
|
console.error(e)
|
|
2124
2148
|
self.setError(e)
|
|
2125
2149
|
}
|
|
2126
2150
|
}
|
|
2127
2151
|
} finally {
|
|
2128
|
-
if (
|
|
2129
|
-
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
}),
|
|
2134
|
-
)
|
|
2135
|
-
// autorun opens treeMetadataFilehandle
|
|
2136
|
-
addDisposer(
|
|
2137
|
-
self,
|
|
2138
|
-
autorun(async () => {
|
|
2139
|
-
const { treeMetadataFilehandle } = self
|
|
2140
|
-
if (treeMetadataFilehandle) {
|
|
2141
|
-
try {
|
|
2142
|
-
self.setTreeMetadata(
|
|
2143
|
-
await fetchTextWithProgress(
|
|
2144
|
-
openLocation(treeMetadataFilehandle),
|
|
2145
|
-
status => {
|
|
2146
|
-
self.setStatus(status)
|
|
2147
|
-
},
|
|
2148
|
-
),
|
|
2149
|
-
)
|
|
2150
|
-
} catch (e) {
|
|
2151
|
-
// ignore user cancel (isAbortError); treeMetadata is optional
|
|
2152
|
-
if (!isAbortError(e)) {
|
|
2153
|
-
console.error(e)
|
|
2154
|
-
self.setError(e)
|
|
2152
|
+
if (isCurrent()) {
|
|
2153
|
+
setLoading?.(false)
|
|
2155
2154
|
}
|
|
2156
2155
|
}
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2156
|
+
}),
|
|
2157
|
+
)
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
loadOnFilehandleChange({
|
|
2161
|
+
getFilehandle: () => self.treeFilehandle,
|
|
2162
|
+
onLoad: text => {
|
|
2163
|
+
self.setTree(text)
|
|
2164
|
+
},
|
|
2165
|
+
setLoading: arg => {
|
|
2166
|
+
self.setLoadingTree(arg)
|
|
2167
|
+
},
|
|
2168
|
+
clearFilehandle: () => {
|
|
2169
|
+
self.setTreeFilehandle(undefined)
|
|
2170
|
+
},
|
|
2171
|
+
})
|
|
2172
|
+
|
|
2173
|
+
// treeMetadata is decorative and has no import-form step of its own, so
|
|
2174
|
+
// it keeps no loading flag and nothing to return to on cancel
|
|
2175
|
+
loadOnFilehandleChange({
|
|
2176
|
+
getFilehandle: () => self.treeMetadataFilehandle,
|
|
2177
|
+
onLoad: text => {
|
|
2178
|
+
self.setTreeMetadata(text)
|
|
2179
|
+
},
|
|
2180
|
+
})
|
|
2160
2181
|
|
|
2161
2182
|
// autorun parses inline gff text from data.gff
|
|
2162
2183
|
addDisposer(
|
|
@@ -2174,83 +2195,29 @@ function stateModelFactory() {
|
|
|
2174
2195
|
}),
|
|
2175
2196
|
)
|
|
2176
2197
|
|
|
2177
|
-
//
|
|
2178
|
-
|
|
2179
|
-
self,
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
self.setStatus(status)
|
|
2188
|
-
},
|
|
2189
|
-
)
|
|
2190
|
-
self.applyGFFText(gffText)
|
|
2191
|
-
if (gffFilehandle.locationType === 'BlobLocation') {
|
|
2192
|
-
self.setGFFFilehandle(undefined)
|
|
2193
|
-
}
|
|
2194
|
-
} catch (e) {
|
|
2195
|
-
// on user cancel (isAbortError) drop the filehandle
|
|
2196
|
-
if (isAbortError(e)) {
|
|
2197
|
-
self.setGFFFilehandle(undefined)
|
|
2198
|
-
} else {
|
|
2199
|
-
console.error(e)
|
|
2200
|
-
self.setError(e)
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
}),
|
|
2205
|
-
)
|
|
2198
|
+
// gffFilehandle carries InterProScan domains
|
|
2199
|
+
loadOnFilehandleChange({
|
|
2200
|
+
getFilehandle: () => self.gffFilehandle,
|
|
2201
|
+
onLoad: text => {
|
|
2202
|
+
self.applyGFFText(text)
|
|
2203
|
+
},
|
|
2204
|
+
clearFilehandle: () => {
|
|
2205
|
+
self.setGFFFilehandle(undefined)
|
|
2206
|
+
},
|
|
2207
|
+
})
|
|
2206
2208
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
openLocation(msaFilehandle),
|
|
2220
|
-
status => {
|
|
2221
|
-
if (generation === msaGeneration) {
|
|
2222
|
-
self.setStatus(status)
|
|
2223
|
-
}
|
|
2224
|
-
},
|
|
2225
|
-
)
|
|
2226
|
-
if (generation === msaGeneration) {
|
|
2227
|
-
transaction(() => {
|
|
2228
|
-
self.setMSA(txt)
|
|
2229
|
-
if (msaFilehandle.locationType === 'BlobLocation') {
|
|
2230
|
-
// clear filehandle after loading if from a local file
|
|
2231
|
-
self.setMSAFilehandle(undefined)
|
|
2232
|
-
}
|
|
2233
|
-
})
|
|
2234
|
-
}
|
|
2235
|
-
} catch (e) {
|
|
2236
|
-
if (generation === msaGeneration) {
|
|
2237
|
-
if (isAbortError(e)) {
|
|
2238
|
-
// cancelled by the user: drop the filehandle so the view
|
|
2239
|
-
// returns to the import form instead of a stuck spinner
|
|
2240
|
-
self.setMSAFilehandle(undefined)
|
|
2241
|
-
} else {
|
|
2242
|
-
console.error(e)
|
|
2243
|
-
self.setError(e)
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
} finally {
|
|
2247
|
-
if (generation === msaGeneration) {
|
|
2248
|
-
self.setLoadingMSA(false)
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
}),
|
|
2253
|
-
)
|
|
2209
|
+
loadOnFilehandleChange({
|
|
2210
|
+
getFilehandle: () => self.msaFilehandle,
|
|
2211
|
+
onLoad: text => {
|
|
2212
|
+
self.setMSA(text)
|
|
2213
|
+
},
|
|
2214
|
+
setLoading: arg => {
|
|
2215
|
+
self.setLoadingMSA(arg)
|
|
2216
|
+
},
|
|
2217
|
+
clearFilehandle: () => {
|
|
2218
|
+
self.setMSAFilehandle(undefined)
|
|
2219
|
+
},
|
|
2220
|
+
})
|
|
2254
2221
|
|
|
2255
2222
|
// Keep the parse chain warm: reading self.columns transitively holds
|
|
2256
2223
|
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|