Skip to content

Commit ea45ca3

Browse files
committed
Save track genbank
1 parent 27ab10c commit ea45ca3

File tree

10 files changed

+392
-188
lines changed

10 files changed

+392
-188
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"dompurify": "^3.0.0",
4545
"escape-html": "^1.0.3",
4646
"fast-deep-equal": "^3.1.3",
47+
"file-saver": "^2.0.0",
4748
"generic-filehandle": "^3.0.0",
4849
"http-range-fetcher": "^1.4.0",
4950
"is-object": "^1.0.1",

packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Typography,
1313
} from '@mui/material'
1414
import { makeStyles } from 'tss-react/mui'
15+
import { saveAs } from 'file-saver'
1516
import { observer } from 'mobx-react'
1617
import { Dialog, ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
1718
import {
@@ -23,8 +24,12 @@ import {
2324
import { getConf } from '@jbrowse/core/configuration'
2425
import { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes'
2526

27+
// icons
28+
import GetAppIcon from '@mui/icons-material/GetApp'
29+
2630
// locals
27-
import { stringifyGenbank, stringifyGFF3 } from './util'
31+
import { stringifyGFF3 } from './gff3'
32+
import { stringifyGenbank } from './genbank'
2833

2934
const useStyles = makeStyles()({
3035
root: {
@@ -62,7 +67,11 @@ export default observer(function SaveTrackDataDlg({
6267
const [error, setError] = useState<unknown>()
6368
const [features, setFeatures] = useState<Feature[]>()
6469
const [type, setType] = useState('gff3')
65-
const options = { gff3: 'GFF3', genbank: 'GenBank' }
70+
const [str, setStr] = useState('')
71+
const options = {
72+
gff3: { name: 'GFF3', extension: 'gff3' },
73+
genbank: { name: 'GenBank', extension: 'genbank' },
74+
}
6675

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

83-
const str = features
84-
? type === 'gff3'
85-
? stringifyGFF3(features)
86-
: stringifyGenbank(features, {})
87-
: ''
92+
useEffect(() => {
93+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
94+
;(async () => {
95+
const view = getContainingView(model)
96+
const session = getSession(model)
97+
if (!features) {
98+
return
99+
}
100+
const str = await (type === 'gff3'
101+
? stringifyGFF3(features)
102+
: stringifyGenbank({
103+
features,
104+
session,
105+
assemblyName: view.dynamicBlocks.contentBlocks[0].assemblyName,
106+
}))
107+
108+
setStr(str)
109+
})()
110+
}, [type, features, model])
88111

89112
return (
90113
<Dialog maxWidth="xl" open onClose={handleClose} title="Save track data">
@@ -103,7 +126,12 @@ export default observer(function SaveTrackDataDlg({
103126
onChange={event => setType(event.target.value)}
104127
>
105128
{Object.entries(options).map(([key, val]) => (
106-
<FormControlLabel value={key} control={<Radio />} label={val} />
129+
<FormControlLabel
130+
key={key}
131+
value={key}
132+
control={<Radio />}
133+
label={val.name}
134+
/>
107135
))}
108136
</RadioGroup>
109137
</FormControl>
@@ -123,6 +151,23 @@ export default observer(function SaveTrackDataDlg({
123151
/>
124152
</DialogContent>
125153
<DialogActions>
154+
<Button
155+
onClick={() => {
156+
saveAs(
157+
new Blob([str || ''], {
158+
type: 'text/plain;charset=utf-8',
159+
}),
160+
`jbrowse_track_data.${
161+
options[type as keyof typeof options].extension
162+
}`,
163+
)
164+
}}
165+
disabled={!str || !!error}
166+
startIcon={<GetAppIcon />}
167+
>
168+
Download
169+
</Button>
170+
126171
<Button variant="contained" type="submit" onClick={() => handleClose()}>
127172
Close
128173
</Button>
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import {
2+
AbstractSessionModel,
3+
Feature,
4+
max,
5+
min,
6+
Region,
7+
} from '@jbrowse/core/util'
8+
import { getConf } from '@jbrowse/core/configuration'
9+
10+
const coreFields = [
11+
'uniqueId',
12+
'refName',
13+
'source',
14+
'type',
15+
'start',
16+
'end',
17+
'strand',
18+
'parent',
19+
'parentId',
20+
'score',
21+
'subfeatures',
22+
'phase',
23+
]
24+
25+
const blank = ' '
26+
27+
const retitle = {
28+
name: 'Name',
29+
} as { [key: string]: string | undefined }
30+
31+
function fmt(obj: unknown): string {
32+
if (Array.isArray(obj)) {
33+
return obj.map(o => fmt(o)).join(',')
34+
} else if (typeof obj === 'object') {
35+
return JSON.stringify(obj)
36+
} else {
37+
return `${obj}`
38+
}
39+
}
40+
41+
function formatTags(f: Feature, parentId?: string, parentType?: string) {
42+
return [
43+
parentId && parentType ? `${blank}/${parentType}="${parentId}"` : '',
44+
f.get('id') ? `${blank}/name=${f.get('id')}` : '',
45+
...f
46+
.tags()
47+
.filter(tag => !coreFields.includes(tag))
48+
.map(tag => [tag, fmt(f.get(tag))])
49+
.filter(tag => !!tag[1] && tag[0] !== parentType)
50+
.map(tag => `${blank}/${retitle[tag[0]] || tag[0]}="${tag[1]}"`),
51+
].filter(f => !!f)
52+
}
53+
54+
function rs(f: Feature, min: number) {
55+
return f.get('start') - min + 1
56+
}
57+
function re(f: Feature, min: number) {
58+
return f.get('end') - min
59+
}
60+
function loc(f: Feature, min: number) {
61+
return `${rs(f, min)}..${re(f, min)}`
62+
}
63+
function formatFeat(
64+
f: Feature,
65+
min: number,
66+
parentType?: string,
67+
parentId?: string,
68+
) {
69+
const type = `${f.get('type')}`.slice(0, 16)
70+
const l = loc(f, min)
71+
const locstrand = f.get('strand') === -1 ? `complement(${l})` : l
72+
return [
73+
` ${type.padEnd(16)}${locstrand}`,
74+
...formatTags(f, parentType, parentId),
75+
]
76+
}
77+
78+
function formatCDS(
79+
feats: Feature[],
80+
parentId: string,
81+
parentType: string,
82+
strand: number,
83+
min: number,
84+
) {
85+
const cds = feats.map(f => loc(f, min))
86+
const pre = `join(${cds})`
87+
const str = strand === -1 ? `complement(${pre})` : pre
88+
return feats.length
89+
? [` ${'CDS'.padEnd(16)}${str}`, `${blank}/${parentType}="${parentId}"`]
90+
: []
91+
}
92+
93+
export function formatFeatWithSubfeatures(
94+
feature: Feature,
95+
min: number,
96+
parentId?: string,
97+
parentType?: string,
98+
): string {
99+
const primary = formatFeat(feature, min, parentId, parentType)
100+
const subfeatures = feature.get('subfeatures') || []
101+
const cds = subfeatures.filter(f => f.get('type') === 'CDS')
102+
const sansCDS = subfeatures.filter(
103+
f => f.get('type') !== 'CDS' && f.get('type') !== 'exon',
104+
)
105+
const newParentId = feature.get('id')
106+
const newParentType = feature.get('type')
107+
const newParentStrand = feature.get('strand')
108+
return [
109+
...primary,
110+
...formatCDS(cds, newParentId, newParentType, newParentStrand, min),
111+
...sansCDS
112+
.map(sub =>
113+
formatFeatWithSubfeatures(sub, min, newParentId, newParentType),
114+
)
115+
.flat(),
116+
].join('\n')
117+
}
118+
119+
export async function stringifyGenbank({
120+
features,
121+
assemblyName,
122+
session,
123+
}: {
124+
assemblyName: string
125+
session: AbstractSessionModel
126+
features: Feature[]
127+
}) {
128+
const today = new Date()
129+
const month = today.toLocaleString('en-US', { month: 'short' }).toUpperCase()
130+
const day = today.toLocaleString('en-US', { day: 'numeric' })
131+
const year = today.toLocaleString('en-US', { year: 'numeric' })
132+
const date = `${day}-${month}-${year}`
133+
134+
const start = min(features.map(f => f.get('start')))
135+
const end = max(features.map(f => f.get('end')))
136+
const length = end - start
137+
const refName = features[0].get('refName')
138+
139+
const l1 = [
140+
`${'LOCUS'.padEnd(12)}`,
141+
`${refName}:${start + 1}..${end}`.padEnd(20),
142+
` ${`${length} bp`}`.padEnd(15),
143+
` ${'DNA'.padEnd(10)}`,
144+
`${'linear'.padEnd(10)}`,
145+
`${'UNK ' + date}`,
146+
].join('')
147+
const l2 = 'FEATURES Location/Qualifiers'
148+
const seq = await fetchSequence({
149+
session,
150+
assemblyName,
151+
regions: [{ assemblyName, start, end, refName }],
152+
})
153+
const contig = seq.map(f => f.get('seq') || '').join('')
154+
const lines = features.map(feat => formatFeatWithSubfeatures(feat, start))
155+
const seqlines = ['ORIGIN', `\t1 ${contig}`, '//']
156+
return [l1, l2, ...lines, ...seqlines].join('\n')
157+
}
158+
159+
async function fetchSequence({
160+
session,
161+
regions,
162+
signal,
163+
assemblyName,
164+
}: {
165+
assemblyName: string
166+
session: AbstractSessionModel
167+
regions: Region[]
168+
signal?: AbortSignal
169+
}) {
170+
const { rpcManager, assemblyManager } = session
171+
const assembly = assemblyManager.get(assemblyName)
172+
if (!assembly) {
173+
throw new Error(`assembly ${assemblyName} not found`)
174+
}
175+
176+
const sessionId = 'getSequence'
177+
return rpcManager.call(sessionId, 'CoreGetFeatures', {
178+
adapterConfig: getConf(assembly, ['sequence', 'adapter']),
179+
regions: regions.map(r => ({
180+
...r,
181+
refName: assembly.getCanonicalRefName(r.refName),
182+
})),
183+
sessionId,
184+
signal,
185+
}) as Promise<Feature[]>
186+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Feature } from '@jbrowse/core/util'
2+
3+
const coreFields = [
4+
'uniqueId',
5+
'refName',
6+
'source',
7+
'type',
8+
'start',
9+
'end',
10+
'strand',
11+
'parent',
12+
'parentId',
13+
'score',
14+
'subfeatures',
15+
'phase',
16+
]
17+
18+
const retitle = {
19+
id: 'ID',
20+
name: 'Name',
21+
alias: 'Alias',
22+
parent: 'Parent',
23+
target: 'Target',
24+
gap: 'Gap',
25+
derives_from: 'Derives_from',
26+
note: 'Note',
27+
description: 'Note',
28+
dbxref: 'Dbxref',
29+
ontology_term: 'Ontology_term',
30+
is_circular: 'Is_circular',
31+
} as { [key: string]: string }
32+
33+
function fmt(obj: unknown): string {
34+
if (Array.isArray(obj)) {
35+
return obj.map(o => fmt(o)).join(',')
36+
} else if (typeof obj === 'object') {
37+
return JSON.stringify(obj)
38+
} else {
39+
return `${obj}`
40+
}
41+
}
42+
43+
function formatFeat(f: Feature, parentId?: string, parentRef?: string) {
44+
return [
45+
f.get('refName') || parentRef,
46+
f.get('source') || '.',
47+
f.get('type') || '.',
48+
f.get('start') + 1,
49+
f.get('end'),
50+
f.get('score') || '.',
51+
f.get('strand') || '.',
52+
f.get('phase') || '.',
53+
(parentId ? `Parent=${parentId};` : '') +
54+
f
55+
.tags()
56+
.filter(tag => !coreFields.includes(tag))
57+
.map(tag => [tag, fmt(f.get(tag))])
58+
.filter(tag => !!tag[1])
59+
.map(tag => `${retitle[tag[0]] || tag[0]}=${tag[1]}`)
60+
.join(';'),
61+
].join('\t')
62+
}
63+
export function formatMultiLevelFeat(
64+
f: Feature,
65+
parentId?: string,
66+
parentRef?: string,
67+
): string {
68+
const fRef = parentRef || f.get('refName')
69+
const fId = f.get('id')
70+
const primary = formatFeat(f, parentId, fRef)
71+
const subs =
72+
f.get('subfeatures')?.map(sub => formatMultiLevelFeat(sub, fId, fRef)) || []
73+
return [primary, ...subs].join('\n')
74+
}
75+
76+
export function stringifyGFF3(feats: Feature[]) {
77+
return ['##gff-version 3', ...feats.map(f => formatMultiLevelFeat(f))].join(
78+
'\n',
79+
)
80+
}

0 commit comments

Comments
 (0)