Skip to content

Commit

Permalink
Save track data
Browse files Browse the repository at this point in the history
Rm
  • Loading branch information
cmdcolin committed Sep 12, 2024
1 parent 8b8fdb3 commit 3daccbe
Show file tree
Hide file tree
Showing 20 changed files with 678 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"dompurify": "^3.0.0",
"escape-html": "^1.0.3",
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.0",
"generic-filehandle": "^3.0.0",
"is-object": "^1.0.1",
"jexl": "^2.3.0",
Expand Down
44 changes: 44 additions & 0 deletions packages/core/pluggableElementTypes/models/BaseTrackModel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { lazy } from 'react'
import { transaction } from 'mobx'
import {
getRoot,
Expand All @@ -16,10 +17,19 @@ import {
} from '../../configuration'
import PluginManager from '../../PluginManager'
import { MenuItem } from '../../ui'
import { Save } from '../../ui/Icons'
import { getContainingView, getEnv, getSession } from '../../util'
import { isSessionModelWithConfigEditing } from '../../util/types'
import { ElementId } from '../../util/types/mst'

// locals
import { stringifyGFF3 } from './saveTrackFileTypes/gff3'
import { stringifyGBK } from './saveTrackFileTypes/genbank'
import { stringifyBED } from './saveTrackFileTypes/bed'

// lazies
const SaveTrackDataDlg = lazy(() => import('./components/SaveTrackData'))

export function getCompatibleDisplays(self: IAnyStateTreeNode) {
const { pluginManager } = getEnv(self)
const view = getContainingView(self)
Expand Down Expand Up @@ -183,6 +193,27 @@ export function createBaseTrackModel(
})
},
}))
.views(() => ({
saveTrackFileFormatOptions() {
return {
gff3: {
name: 'GFF3',
extension: 'gff3',
callback: stringifyGFF3,
},
genbank: {
name: 'GenBank',
extension: 'gbk',
callback: stringifyGBK,
},
bed: {
name: 'BED',
extension: 'bed',
callback: stringifyBED,
},
}
},
}))
.views(self => ({
/**
* #method
Expand All @@ -196,6 +227,19 @@ export function createBaseTrackModel(

return [
...menuItems,
{
label: 'Save track data',
icon: Save,
onClick: () => {
getSession(self).queueDialog(handleClose => [
SaveTrackDataDlg,
{
model: self,
handleClose,
},
])
},
},
...(compatDisp.length > 1
? [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useEffect, useState } from 'react'
import {
Button,
DialogActions,
DialogContent,
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
TextField,
Typography,
} from '@mui/material'
import { IAnyStateTreeNode } from 'mobx-state-tree'
import { makeStyles } from 'tss-react/mui'
import { saveAs } from 'file-saver'
import { observer } from 'mobx-react'
import { Dialog, ErrorMessage } from '@jbrowse/core/ui'
import {
getSession,
getContainingView,
Feature,
Region,
AbstractTrackModel,
AbstractSessionModel,
} from '@jbrowse/core/util'
import { getConf } from '@jbrowse/core/configuration'

// icons
import GetAppIcon from '@mui/icons-material/GetApp'

const useStyles = makeStyles()({
root: {
width: '80em',
},
textAreaFont: {
fontFamily: 'Courier New',
},
})

async function fetchFeatures(
track: IAnyStateTreeNode,
regions: Region[],
signal?: AbortSignal,
) {
const { rpcManager } = getSession(track)
const adapterConfig = getConf(track, ['adapter'])
const sessionId = 'getFeatures'
return rpcManager.call(sessionId, 'CoreGetFeatures', {
adapterConfig,
regions,
sessionId,
signal,
}) as Promise<Feature[]>
}
interface FileTypeExporter {
name: string
extension: string
callback: (arg: {
features: Feature[]
session: AbstractSessionModel
assemblyName: string
}) => Promise<string> | string
}
const SaveTrackDataDialog = observer(function ({
model,
handleClose,
}: {
model: AbstractTrackModel & {
saveTrackFileFormatOptions: () => Record<string, FileTypeExporter>
}
handleClose: () => void
}) {
const options = model.saveTrackFileFormatOptions()
const { classes } = useStyles()
const [error, setError] = useState<unknown>()
const [features, setFeatures] = useState<Feature[]>()
const [type, setType] = useState(Object.keys(options)[0])
const [str, setStr] = useState('')

useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
try {
const view = getContainingView(model) as { visibleRegions?: Region[] }
setError(undefined)
setFeatures(await fetchFeatures(model, view.visibleRegions || []))
} catch (e) {
console.error(e)
setError(e)
}
})()
}, [model])

useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
try {
const { visibleRegions } = getContainingView(model) as {
visibleRegions?: Region[]
}
const session = getSession(model)
if (!features || !visibleRegions?.length || !type) {
return
}
const generator = options[type] || {
callback: () => 'Unknown',
}
setStr(
await generator.callback({
features,
session,
assemblyName: visibleRegions[0]!.assemblyName,
}),
)
} catch (e) {
setError(e)
}
})()
}, [type, features, options, model])

const loading = !features
return (
<Dialog maxWidth="xl" open onClose={handleClose} title="Save track data">
<DialogContent className={classes.root}>
{error ? <ErrorMessage error={error} /> : null}
{features && !features.length ? (
<Typography>No features found</Typography>
) : null}

<FormControl>
<FormLabel>File type</FormLabel>
<RadioGroup
value={type}
onChange={e => {
setType(e.target.value)
}}
>
{Object.entries(options).map(([key, val]) => (
<FormControlLabel
key={key}
value={key}
control={<Radio />}
label={val.name}
/>
))}
</RadioGroup>
</FormControl>
<TextField
variant="outlined"
multiline
minRows={5}
maxRows={15}
fullWidth
value={
loading
? 'Loading...'
: str.length > 100_000
? 'Too large to view here, click "Download" to results to file'
: str
}
InputProps={{
readOnly: true,
classes: {
input: classes.textAreaFont,
},
}}
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
if (!type) {
return
}
const ext = options[type]?.extension || 'unknown'
const blob = new Blob([str], { type: 'text/plain;charset=utf-8' })
saveAs(blob, `jbrowse_track_data.${ext}`)
}}
startIcon={<GetAppIcon />}
>
Download
</Button>

<Button
variant="contained"
type="submit"
onClick={() => {
handleClose()
}}
>
Close
</Button>
</DialogActions>
</Dialog>
)
})

export default SaveTrackDataDialog
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Feature } from '@jbrowse/core/util'

export function stringifyBED({ features }: { features: Feature[] }) {
const fields = ['refName', 'start', 'end', 'name', 'score', 'strand']
return features
.map(feature =>
fields
.map(field => feature.get(field))
.join('\t')
.trim(),
)
.join('\n')
}
Loading

0 comments on commit 3daccbe

Please sign in to comment.