Skip to content

Commit

Permalink
Save track genbank
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin committed Feb 8, 2023
1 parent 18eb3e8 commit d269640
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 188 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"dompurify": "^2.3.4",
"escape-html": "^1.0.3",
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.0",
"generic-filehandle": "^3.0.0",
"http-range-fetcher": "^1.4.0",
"is-object": "^1.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Typography,
} from '@mui/material'
import { makeStyles } from 'tss-react/mui'
import { saveAs } from 'file-saver'
import { observer } from 'mobx-react'
import { Dialog, ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
import {
Expand All @@ -23,8 +24,12 @@ import {
import { getConf } from '@jbrowse/core/configuration'
import { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes'

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

// locals
import { stringifyGenbank, stringifyGFF3 } from './util'
import { stringifyGFF3 } from './gff3'
import { stringifyGenbank } from './genbank'

const useStyles = makeStyles()({
root: {
Expand Down Expand Up @@ -62,7 +67,11 @@ export default observer(function SaveTrackDataDlg({
const [error, setError] = useState<unknown>()
const [features, setFeatures] = useState<Feature[]>()
const [type, setType] = useState('gff3')
const options = { gff3: 'GFF3', genbank: 'GenBank' }
const [str, setStr] = useState('')
const options = {
gff3: { name: 'GFF3', extension: 'gff3' },
genbank: { name: 'GenBank', extension: 'genbank' },
}

useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Expand All @@ -80,11 +89,25 @@ export default observer(function SaveTrackDataDlg({
})()
}, [model])

const str = features
? type === 'gff3'
? stringifyGFF3(features)
: stringifyGenbank(features, {})
: ''
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
const view = getContainingView(model)
const session = getSession(model)
if (!features) {
return
}
const str = await (type === 'gff3'
? stringifyGFF3(features)
: stringifyGenbank({
features,
session,
assemblyName: view.dynamicBlocks.contentBlocks[0].assemblyName,
}))

setStr(str)
})()
}, [type, features, model])

return (
<Dialog maxWidth="xl" open onClose={handleClose} title="Save track data">
Expand All @@ -103,7 +126,12 @@ export default observer(function SaveTrackDataDlg({
onChange={event => setType(event.target.value)}
>
{Object.entries(options).map(([key, val]) => (
<FormControlLabel value={key} control={<Radio />} label={val} />
<FormControlLabel
key={key}
value={key}
control={<Radio />}
label={val.name}
/>
))}
</RadioGroup>
</FormControl>
Expand All @@ -123,6 +151,23 @@ export default observer(function SaveTrackDataDlg({
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
saveAs(
new Blob([str || ''], {
type: 'text/plain;charset=utf-8',
}),
`jbrowse_track_data.${
options[type as keyof typeof options].extension
}`,
)
}}
disabled={!str || !!error}
startIcon={<GetAppIcon />}
>
Download
</Button>

<Button variant="contained" type="submit" onClick={() => handleClose()}>
Close
</Button>
Expand Down
186 changes: 186 additions & 0 deletions packages/core/pluggableElementTypes/models/components/genbank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import {
AbstractSessionModel,
Feature,
max,
min,
Region,
} from '@jbrowse/core/util'
import { getConf } from '@jbrowse/core/configuration'

const coreFields = [
'uniqueId',
'refName',
'source',
'type',
'start',
'end',
'strand',
'parent',
'parentId',
'score',
'subfeatures',
'phase',
]

const blank = ' '

const retitle = {
name: 'Name',
} as { [key: string]: string | undefined }

function fmt(obj: unknown): string {
if (Array.isArray(obj)) {
return obj.map(o => fmt(o)).join(',')
} else if (typeof obj === 'object') {
return JSON.stringify(obj)
} else {
return `${obj}`
}
}

function formatTags(f: Feature, parentId?: string, parentType?: string) {
return [
parentId && parentType ? `${blank}/${parentType}="${parentId}"` : '',
f.get('id') ? `${blank}/name=${f.get('id')}` : '',
...f
.tags()
.filter(tag => !coreFields.includes(tag))
.map(tag => [tag, fmt(f.get(tag))])
.filter(tag => !!tag[1] && tag[0] !== parentType)
.map(tag => `${blank}/${retitle[tag[0]] || tag[0]}="${tag[1]}"`),
].filter(f => !!f)
}

function rs(f: Feature, min: number) {
return f.get('start') - min + 1
}
function re(f: Feature, min: number) {
return f.get('end') - min
}
function loc(f: Feature, min: number) {
return `${rs(f, min)}..${re(f, min)}`
}
function formatFeat(
f: Feature,
min: number,
parentType?: string,
parentId?: string,
) {
const type = `${f.get('type')}`.slice(0, 16)
const l = loc(f, min)
const locstrand = f.get('strand') === -1 ? `complement(${l})` : l
return [
` ${type.padEnd(16)}${locstrand}`,
...formatTags(f, parentType, parentId),
]
}

function formatCDS(
feats: Feature[],
parentId: string,
parentType: string,
strand: number,
min: number,
) {
const cds = feats.map(f => loc(f, min))
const pre = `join(${cds})`
const str = strand === -1 ? `complement(${pre})` : pre
return feats.length
? [` ${'CDS'.padEnd(16)}${str}`, `${blank}/${parentType}="${parentId}"`]
: []
}

export function formatFeatWithSubfeatures(
feature: Feature,
min: number,
parentId?: string,
parentType?: string,
): string {
const primary = formatFeat(feature, min, parentId, parentType)
const subfeatures = feature.get('subfeatures') || []
const cds = subfeatures.filter(f => f.get('type') === 'CDS')
const sansCDS = subfeatures.filter(
f => f.get('type') !== 'CDS' && f.get('type') !== 'exon',
)
const newParentId = feature.get('id')
const newParentType = feature.get('type')
const newParentStrand = feature.get('strand')
return [
...primary,
...formatCDS(cds, newParentId, newParentType, newParentStrand, min),
...sansCDS
.map(sub =>
formatFeatWithSubfeatures(sub, min, newParentId, newParentType),
)
.flat(),
].join('\n')
}

export async function stringifyGenbank({
features,
assemblyName,
session,
}: {
assemblyName: string
session: AbstractSessionModel
features: Feature[]
}) {
const today = new Date()
const month = today.toLocaleString('en-US', { month: 'short' }).toUpperCase()
const day = today.toLocaleString('en-US', { day: 'numeric' })
const year = today.toLocaleString('en-US', { year: 'numeric' })
const date = `${day}-${month}-${year}`

const start = min(features.map(f => f.get('start')))
const end = max(features.map(f => f.get('end')))
const length = end - start
const refName = features[0].get('refName')

const l1 = [
`${'LOCUS'.padEnd(12)}`,
`${refName}:${start + 1}..${end}`.padEnd(20),
` ${`${length} bp`}`.padEnd(15),
` ${'DNA'.padEnd(10)}`,
`${'linear'.padEnd(10)}`,
`${'UNK ' + date}`,
].join('')
const l2 = 'FEATURES Location/Qualifiers'
const seq = await fetchSequence({
session,
assemblyName,
regions: [{ assemblyName, start, end, refName }],
})
const contig = seq.map(f => f.get('seq') || '').join('')
const lines = features.map(feat => formatFeatWithSubfeatures(feat, start))
const seqlines = ['ORIGIN', `\t1 ${contig}`, '//']
return [l1, l2, ...lines, ...seqlines].join('\n')
}

async function fetchSequence({
session,
regions,
signal,
assemblyName,
}: {
assemblyName: string
session: AbstractSessionModel
regions: Region[]
signal?: AbortSignal
}) {
const { rpcManager, assemblyManager } = session
const assembly = assemblyManager.get(assemblyName)
if (!assembly) {
throw new Error(`assembly ${assemblyName} not found`)
}

const sessionId = 'getSequence'
return rpcManager.call(sessionId, 'CoreGetFeatures', {
adapterConfig: getConf(assembly, ['sequence', 'adapter']),
regions: regions.map(r => ({
...r,
refName: assembly.getCanonicalRefName(r.refName),
})),
sessionId,
signal,
}) as Promise<Feature[]>
}
80 changes: 80 additions & 0 deletions packages/core/pluggableElementTypes/models/components/gff3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Feature } from '@jbrowse/core/util'

const coreFields = [
'uniqueId',
'refName',
'source',
'type',
'start',
'end',
'strand',
'parent',
'parentId',
'score',
'subfeatures',
'phase',
]

const retitle = {
id: 'ID',
name: 'Name',
alias: 'Alias',
parent: 'Parent',
target: 'Target',
gap: 'Gap',
derives_from: 'Derives_from',
note: 'Note',
description: 'Note',
dbxref: 'Dbxref',
ontology_term: 'Ontology_term',
is_circular: 'Is_circular',
} as { [key: string]: string }

function fmt(obj: unknown): string {
if (Array.isArray(obj)) {
return obj.map(o => fmt(o)).join(',')
} else if (typeof obj === 'object') {
return JSON.stringify(obj)
} else {
return `${obj}`
}
}

function formatFeat(f: Feature, parentId?: string, parentRef?: string) {
return [
f.get('refName') || parentRef,
f.get('source') || '.',
f.get('type') || '.',
f.get('start') + 1,
f.get('end'),
f.get('score') || '.',
f.get('strand') || '.',
f.get('phase') || '.',
(parentId ? `Parent=${parentId};` : '') +
f
.tags()
.filter(tag => !coreFields.includes(tag))
.map(tag => [tag, fmt(f.get(tag))])
.filter(tag => !!tag[1])
.map(tag => `${retitle[tag[0]] || tag[0]}=${tag[1]}`)
.join(';'),
].join('\t')
}
export function formatMultiLevelFeat(
f: Feature,
parentId?: string,
parentRef?: string,
): string {
const fRef = parentRef || f.get('refName')
const fId = f.get('id')
const primary = formatFeat(f, parentId, fRef)
const subs =
f.get('subfeatures')?.map(sub => formatMultiLevelFeat(sub, fId, fRef)) || []
return [primary, ...subs].join('\n')
}

export function stringifyGFF3(feats: Feature[]) {
return ['##gff-version 3', ...feats.map(f => formatMultiLevelFeat(f))].join(
'\n',
)
}
Loading

0 comments on commit d269640

Please sign in to comment.