Skip to content

Commit b1ce075

Browse files
committed
Save track data
1 parent f1d9526 commit b1ce075

File tree

19 files changed

+656
-17
lines changed

19 files changed

+656
-17
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"dompurify": "^3.2.0",
4747
"escape-html": "^1.0.3",
4848
"fast-deep-equal": "^3.1.3",
49+
"file-saver": "^2.0.0",
4950
"generic-filehandle": "^3.0.0",
5051
"is-object": "^1.0.1",
5152
"jexl": "^2.3.0",

packages/core/pluggableElementTypes/models/BaseTrackModel.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
import { lazy } from 'react'
2+
13
import { transaction } from 'mobx'
24
import { getRoot, resolveIdentifier, types } from 'mobx-state-tree'
35

6+
import { Save } from '@mui/icons-material'
7+
48
import { ConfigurationReference, getConf } from '../../configuration'
59
import { getContainingView, getEnv, getSession } from '../../util'
10+
import { stringifyBED } from './saveTrackFileTypes/bed'
11+
import { stringifyGBK } from './saveTrackFileTypes/genbank'
12+
import { stringifyGFF3 } from './saveTrackFileTypes/gff3'
613
import { isSessionModelWithConfigEditing } from '../../util/types'
714
import { ElementId } from '../../util/types/mst'
815

@@ -14,6 +21,9 @@ import type {
1421
import type { MenuItem } from '../../ui'
1522
import type { IAnyStateTreeNode, Instance } from 'mobx-state-tree'
1623

24+
// lazies
25+
const SaveTrackDataDlg = lazy(() => import('./components/SaveTrackData'))
26+
1727
export function getCompatibleDisplays(self: IAnyStateTreeNode) {
1828
const { pluginManager } = getEnv(self)
1929
const view = getContainingView(self)
@@ -177,6 +187,27 @@ export function createBaseTrackModel(
177187
})
178188
},
179189
}))
190+
.views(() => ({
191+
saveTrackFileFormatOptions() {
192+
return {
193+
gff3: {
194+
name: 'GFF3',
195+
extension: 'gff3',
196+
callback: stringifyGFF3,
197+
},
198+
genbank: {
199+
name: 'GenBank',
200+
extension: 'gbk',
201+
callback: stringifyGBK,
202+
},
203+
bed: {
204+
name: 'BED',
205+
extension: 'bed',
206+
callback: stringifyBED,
207+
},
208+
}
209+
},
210+
}))
180211
.views(self => ({
181212
/**
182213
* #method
@@ -190,6 +221,19 @@ export function createBaseTrackModel(
190221

191222
return [
192223
...menuItems,
224+
{
225+
label: 'Save track data',
226+
icon: Save,
227+
onClick: () => {
228+
getSession(self).queueDialog(handleClose => [
229+
SaveTrackDataDlg,
230+
{
231+
model: self,
232+
handleClose,
233+
},
234+
])
235+
},
236+
},
193237
...(compatDisp.length > 1
194238
? [
195239
{
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import React, { useEffect, useState } from 'react'
2+
3+
import { getConf } from '@jbrowse/core/configuration'
4+
import { Dialog, ErrorMessage } from '@jbrowse/core/ui'
5+
import { getContainingView, getSession } from '@jbrowse/core/util'
6+
import GetAppIcon from '@mui/icons-material/GetApp'
7+
import {
8+
Button,
9+
DialogActions,
10+
DialogContent,
11+
FormControl,
12+
FormControlLabel,
13+
FormLabel,
14+
Radio,
15+
RadioGroup,
16+
TextField,
17+
Typography,
18+
} from '@mui/material'
19+
import { saveAs } from 'file-saver'
20+
import { observer } from 'mobx-react'
21+
import { makeStyles } from 'tss-react/mui'
22+
23+
import type {
24+
AbstractSessionModel,
25+
AbstractTrackModel,
26+
Feature,
27+
Region,
28+
} from '@jbrowse/core/util'
29+
import type { IAnyStateTreeNode } from 'mobx-state-tree'
30+
31+
// icons
32+
33+
const useStyles = makeStyles()({
34+
root: {
35+
width: '80em',
36+
},
37+
textAreaFont: {
38+
fontFamily: 'Courier New',
39+
},
40+
})
41+
42+
async function fetchFeatures(
43+
track: IAnyStateTreeNode,
44+
regions: Region[],
45+
signal?: AbortSignal,
46+
) {
47+
const { rpcManager } = getSession(track)
48+
const adapterConfig = getConf(track, ['adapter'])
49+
const sessionId = 'getFeatures'
50+
return rpcManager.call(sessionId, 'CoreGetFeatures', {
51+
adapterConfig,
52+
regions,
53+
sessionId,
54+
signal,
55+
}) as Promise<Feature[]>
56+
}
57+
interface FileTypeExporter {
58+
name: string
59+
extension: string
60+
callback: (arg: {
61+
features: Feature[]
62+
session: AbstractSessionModel
63+
assemblyName: string
64+
}) => Promise<string> | string
65+
}
66+
const SaveTrackDataDialog = observer(function ({
67+
model,
68+
handleClose,
69+
}: {
70+
model: AbstractTrackModel & {
71+
saveTrackFileFormatOptions: () => Record<string, FileTypeExporter>
72+
}
73+
handleClose: () => void
74+
}) {
75+
const options = model.saveTrackFileFormatOptions()
76+
const { classes } = useStyles()
77+
const [error, setError] = useState<unknown>()
78+
const [features, setFeatures] = useState<Feature[]>()
79+
const [type, setType] = useState(Object.keys(options)[0])
80+
const [str, setStr] = useState('')
81+
82+
useEffect(() => {
83+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
84+
;(async () => {
85+
try {
86+
const view = getContainingView(model) as { visibleRegions?: Region[] }
87+
setError(undefined)
88+
setFeatures(await fetchFeatures(model, view.visibleRegions || []))
89+
} catch (e) {
90+
console.error(e)
91+
setError(e)
92+
}
93+
})()
94+
}, [model])
95+
96+
useEffect(() => {
97+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
98+
;(async () => {
99+
try {
100+
const { visibleRegions } = getContainingView(model) as {
101+
visibleRegions?: Region[]
102+
}
103+
const session = getSession(model)
104+
if (!features || !visibleRegions?.length || !type) {
105+
return
106+
}
107+
const generator = options[type] || {
108+
callback: () => 'Unknown',
109+
}
110+
setStr(
111+
await generator.callback({
112+
features,
113+
session,
114+
assemblyName: visibleRegions[0]!.assemblyName,
115+
}),
116+
)
117+
} catch (e) {
118+
setError(e)
119+
}
120+
})()
121+
}, [type, features, options, model])
122+
123+
const loading = !features
124+
return (
125+
<Dialog maxWidth="xl" open onClose={handleClose} title="Save track data">
126+
<DialogContent className={classes.root}>
127+
{error ? <ErrorMessage error={error} /> : null}
128+
{features && !features.length ? (
129+
<Typography>No features found</Typography>
130+
) : null}
131+
132+
<FormControl>
133+
<FormLabel>File type</FormLabel>
134+
<RadioGroup
135+
value={type}
136+
onChange={e => {
137+
setType(e.target.value)
138+
}}
139+
>
140+
{Object.entries(options).map(([key, val]) => (
141+
<FormControlLabel
142+
key={key}
143+
value={key}
144+
control={<Radio />}
145+
label={val.name}
146+
/>
147+
))}
148+
</RadioGroup>
149+
</FormControl>
150+
<TextField
151+
variant="outlined"
152+
multiline
153+
minRows={5}
154+
maxRows={15}
155+
fullWidth
156+
value={
157+
loading
158+
? 'Loading...'
159+
: str.length > 100_000
160+
? 'Too large to view here, click "Download" to results to file'
161+
: str
162+
}
163+
InputProps={{
164+
readOnly: true,
165+
classes: {
166+
input: classes.textAreaFont,
167+
},
168+
}}
169+
/>
170+
</DialogContent>
171+
<DialogActions>
172+
<Button
173+
onClick={() => {
174+
if (!type) {
175+
return
176+
}
177+
const ext = options[type]?.extension || 'unknown'
178+
const blob = new Blob([str], { type: 'text/plain;charset=utf-8' })
179+
saveAs(blob, `jbrowse_track_data.${ext}`)
180+
}}
181+
startIcon={<GetAppIcon />}
182+
>
183+
Download
184+
</Button>
185+
186+
<Button
187+
variant="contained"
188+
type="submit"
189+
onClick={() => {
190+
handleClose()
191+
}}
192+
>
193+
Close
194+
</Button>
195+
</DialogActions>
196+
</Dialog>
197+
)
198+
})
199+
200+
export default SaveTrackDataDialog
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Feature } from '@jbrowse/core/util'
2+
3+
export function stringifyBED({ features }: { features: Feature[] }) {
4+
const fields = ['refName', 'start', 'end', 'name', 'score', 'strand']
5+
return features
6+
.map(feature =>
7+
fields
8+
.map(field => feature.get(field))
9+
.join('\t')
10+
.trim(),
11+
)
12+
.join('\n')
13+
}

0 commit comments

Comments
 (0)