diff --git a/packages/core/assemblyManager/loadRefNameMap.ts b/packages/core/assemblyManager/loadRefNameMap.ts new file mode 100644 index 0000000000..67928d2b7c --- /dev/null +++ b/packages/core/assemblyManager/loadRefNameMap.ts @@ -0,0 +1,65 @@ +import { BaseOptions, checkRefName, RefNameAliases } from './util' +import RpcManager from '../rpc/RpcManager' +import { when } from '../util' + +export interface BasicRegion { + start: number + end: number + refName: string + assemblyName: string +} + +export async function loadRefNameMap( + assembly: { + name: string + regions: BasicRegion[] | undefined + refNameAliases: RefNameAliases | undefined + getCanonicalRefName: (arg: string) => string + rpcManager: RpcManager + }, + adapterConfig: unknown, + options: BaseOptions, + signal?: AbortSignal, +) { + const { sessionId } = options + await when(() => !!(assembly.regions && assembly.refNameAliases), { + signal, + name: 'when assembly ready', + }) + + const refNames = (await assembly.rpcManager.call( + sessionId, + 'CoreGetRefNames', + { + adapterConfig, + signal, + ...options, + }, + { timeout: 1000000 }, + )) as string[] + + const { refNameAliases } = assembly + if (!refNameAliases) { + throw new Error(`error loading assembly ${assembly.name}'s refNameAliases`) + } + + const refNameMap = Object.fromEntries( + refNames.map(name => { + checkRefName(name) + return [assembly.getCanonicalRefName(name), name] + }), + ) + + // make the reversed map too + const reversed = Object.fromEntries( + Object.entries(refNameMap).map(([canonicalName, adapterName]) => [ + adapterName, + canonicalName, + ]), + ) + + return { + forwardMap: refNameMap, + reverseMap: reversed, + } +} diff --git a/packages/core/assemblyManager/util.ts b/packages/core/assemblyManager/util.ts new file mode 100644 index 0000000000..e779fec009 --- /dev/null +++ b/packages/core/assemblyManager/util.ts @@ -0,0 +1,70 @@ +import { AnyConfigurationModel } from '../configuration' +import jsonStableStringify from 'json-stable-stringify' +import { BaseRefNameAliasAdapter } from '../data_adapters/BaseAdapter' +import PluginManager from '../PluginManager' +import { BasicRegion } from './loadRefNameMap' + +export type RefNameAliases = Record + +export interface BaseOptions { + signal?: AbortSignal + sessionId: string + statusCallback?: Function +} + +export async function getRefNameAliases( + config: AnyConfigurationModel, + pm: PluginManager, + signal?: AbortSignal, +) { + const type = pm.getAdapterType(config.type) + const CLASS = await type.getAdapterClass() + const adapter = new CLASS(config, undefined, pm) as BaseRefNameAliasAdapter + return adapter.getRefNameAliases({ signal }) +} + +export async function getCytobands( + config: AnyConfigurationModel, + pm: PluginManager, +) { + const type = pm.getAdapterType(config.type) + const CLASS = await type.getAdapterClass() + const adapter = new CLASS(config, undefined, pm) + + // @ts-expect-error + return adapter.getData() +} + +export async function getAssemblyRegions( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assembly: any, + adapterConfig: AnyConfigurationModel, + signal?: AbortSignal, +): Promise { + const sessionId = 'loadRefNames' + return assembly.rpcManager.call( + sessionId, + 'CoreGetRegions', + { + adapterConfig, + sessionId, + signal, + }, + { timeout: 1000000 }, + ) +} + +const refNameRegex = new RegExp( + '[0-9A-Za-z!#$%&+./:;?@^_|~-][0-9A-Za-z!#$%&*+./:;=?@^_|~-]*', +) + +// Valid refName pattern from https://samtools.github.io/hts-specs/SAMv1.pdf +export function checkRefName(refName: string) { + if (!refNameRegex.test(refName)) { + throw new Error(`Encountered invalid refName: "${refName}"`) + } +} + +export function getAdapterId(adapterConf: unknown) { + return jsonStableStringify(adapterConf) +} diff --git a/packages/core/package.json b/packages/core/package.json index 3694ce3e06..e4d17dbdeb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/pluggableElementTypes/models/BaseTrackModel.ts b/packages/core/pluggableElementTypes/models/BaseTrackModel.ts index 4b9c887e03..9a55e8a75a 100644 --- a/packages/core/pluggableElementTypes/models/BaseTrackModel.ts +++ b/packages/core/pluggableElementTypes/models/BaseTrackModel.ts @@ -1,3 +1,4 @@ +import { lazy } from 'react' import { transaction } from 'mobx' import { getRoot, @@ -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) @@ -181,6 +191,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 @@ -194,6 +225,19 @@ export function createBaseTrackModel( return [ ...menuItems, + { + label: 'Save track data', + icon: Save, + onClick: () => { + getSession(self).queueDialog(handleClose => [ + SaveTrackDataDlg, + { + model: self, + handleClose, + }, + ]) + }, + }, ...(compatDisp.length > 1 ? [ { diff --git a/packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx b/packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx new file mode 100644 index 0000000000..078afa6f30 --- /dev/null +++ b/packages/core/pluggableElementTypes/models/components/SaveTrackData.tsx @@ -0,0 +1,185 @@ +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 +} +interface FileTypeExporter { + name: string + extension: string + callback: (arg: { + features: Feature[] + session: AbstractSessionModel + assemblyName: string + }) => Promise | string +} +const SaveTrackDataDialog = observer(function ({ + model, + handleClose, +}: { + model: AbstractTrackModel & { + saveTrackFileFormatOptions: () => Record + } + handleClose: () => void +}) { + const options = model.saveTrackFileFormatOptions() + const { classes } = useStyles() + const [error, setError] = useState() + const [features, setFeatures] = useState() + 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) { + 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 ( + + + {error ? : null} + {features && !features.length ? ( + No features found + ) : null} + + + File type + setType(e.target.value)}> + {Object.entries(options).map(([key, val]) => ( + } + label={val.name} + /> + ))} + + + 100_000 + ? 'Too large to view here, click "Download" to results to file' + : str + } + InputProps={{ + readOnly: true, + classes: { + input: classes.textAreaFont, + }, + }} + /> + + + + + + + + ) +}) + +export default SaveTrackDataDialog diff --git a/packages/core/pluggableElementTypes/models/saveTrackFileTypes/bed.ts b/packages/core/pluggableElementTypes/models/saveTrackFileTypes/bed.ts new file mode 100644 index 0000000000..627bbf1cef --- /dev/null +++ b/packages/core/pluggableElementTypes/models/saveTrackFileTypes/bed.ts @@ -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') +} diff --git a/packages/core/pluggableElementTypes/models/saveTrackFileTypes/genbank.ts b/packages/core/pluggableElementTypes/models/saveTrackFileTypes/genbank.ts new file mode 100644 index 0000000000..1b78eb0e3f --- /dev/null +++ b/packages/core/pluggableElementTypes/models/saveTrackFileTypes/genbank.ts @@ -0,0 +1,184 @@ +import { + AbstractSessionModel, + Feature, + max, + min, + Region, +} from '@jbrowse/core/util' +import { getConf } from '@jbrowse/core/configuration' + +const coreFields = new Set([ + 'uniqueId', + 'refName', + 'source', + 'type', + 'start', + 'end', + 'strand', + 'parent', + 'parentId', + 'score', + 'subfeatures', + 'phase', +]) + +const blank = ' ' + +const retitle = { + name: 'Name', +} as Record + +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.has(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.flatMap(sub => + formatFeatWithSubfeatures(sub, min, newParentId, newParentType), + ), + ].join('\n') +} + +export async function stringifyGBK({ + 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 +} diff --git a/packages/core/pluggableElementTypes/models/saveTrackFileTypes/gff3.ts b/packages/core/pluggableElementTypes/models/saveTrackFileTypes/gff3.ts new file mode 100644 index 0000000000..44605d6dae --- /dev/null +++ b/packages/core/pluggableElementTypes/models/saveTrackFileTypes/gff3.ts @@ -0,0 +1,85 @@ +import { Feature } from '@jbrowse/core/util' + +const coreFields = new Set([ + '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 Record + +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') === 1 ? '+' : f.get('strand') === -1 ? '-' : '.', + f.get('phase') || '.', + (parentId ? `Parent=${parentId};` : '') + + f + .tags() + .filter(tag => !coreFields.has(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( + feature: Feature, + parentId?: string, + parentRef?: string, +): string { + const featureRefName = parentRef || feature.get('refName') + const featureId = feature.get('id') + const primary = formatFeat(feature, parentId, featureRefName) + + return [ + primary, + ...(feature + .get('subfeatures') + ?.map(sub => formatMultiLevelFeat(sub, featureId, featureRefName)) || []), + ].join('\n') +} + +export function stringifyGFF3({ features }: { features: Feature[] }) { + return [ + '##gff-version 3', + ...features.map(f => formatMultiLevelFeat(f)), + ].join('\n') +} diff --git a/packages/core/rpc/coreRpcMethods.ts b/packages/core/rpc/coreRpcMethods.ts index e81beffae9..64f8296d3e 100644 --- a/packages/core/rpc/coreRpcMethods.ts +++ b/packages/core/rpc/coreRpcMethods.ts @@ -6,4 +6,5 @@ export { default as CoreGetFeatures } from './methods/CoreGetFeatures' export { default as CoreRender } from './methods/CoreRender' export { default as CoreFreeResources } from './methods/CoreFreeResources' export { default as CoreGetFeatureDensityStats } from './methods/CoreGetFeatureDensityStats' +export { default as CoreGetRegions } from './methods/CoreGetRegions' export { type RenderArgs } from './methods/util' diff --git a/packages/core/rpc/methods/CoreGetRefNames.ts b/packages/core/rpc/methods/CoreGetRefNames.ts index 936a218772..2a2bf329d4 100644 --- a/packages/core/rpc/methods/CoreGetRefNames.ts +++ b/packages/core/rpc/methods/CoreGetRefNames.ts @@ -19,10 +19,8 @@ export default class CoreGetRefNames extends RpcMethodType { const deserializedArgs = await this.deserializeArguments(args, rpcDriver) const { sessionId, adapterConfig } = deserializedArgs const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) - - if (isFeatureAdapter(dataAdapter)) { - return dataAdapter.getRefNames(deserializedArgs) - } - return [] + return isFeatureAdapter(dataAdapter) + ? dataAdapter.getRefNames(deserializedArgs) + : [] } } diff --git a/packages/core/rpc/methods/CoreGetRegions.ts b/packages/core/rpc/methods/CoreGetRegions.ts new file mode 100644 index 0000000000..0761c2c62f --- /dev/null +++ b/packages/core/rpc/methods/CoreGetRegions.ts @@ -0,0 +1,26 @@ +import { getAdapter } from '../../data_adapters/dataAdapterCache' +import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' + +import { RemoteAbortSignal } from '../remoteAbortSignals' +import { isRegionsAdapter } from '../../data_adapters/BaseAdapter' + +export default class CoreGetRegions extends RpcMethodType { + name = 'CoreGetRegions' + + async execute( + args: { + sessionId: string + signal: RemoteAbortSignal + adapterConfig: {} + }, + rpcDriver: string, + ) { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments(args, rpcDriver) + const { sessionId, adapterConfig } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + return isRegionsAdapter(dataAdapter) + ? dataAdapter.getRegions(deserializedArgs) + : [] + } +} diff --git a/packages/core/rpc/methods/CoreSaveFeatureData.ts b/packages/core/rpc/methods/CoreSaveFeatureData.ts new file mode 100644 index 0000000000..21b4067a1d --- /dev/null +++ b/packages/core/rpc/methods/CoreSaveFeatureData.ts @@ -0,0 +1,66 @@ +import { toArray } from 'rxjs/operators' +import { firstValueFrom } from 'rxjs' + +// locals +import { getAdapter } from '../../data_adapters/dataAdapterCache' +import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' +import { RenderArgs } from './util' +import { RemoteAbortSignal } from '../remoteAbortSignals' +import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' +import { renameRegionsIfNeeded, Region } from '../../util' +import SimpleFeature, { + SimpleFeatureSerialized, +} from '../../util/simpleFeature' + +export default class CoreGetFeatures extends RpcMethodType { + name = 'CoreGetFeatures' + + async deserializeReturn( + feats: SimpleFeatureSerialized[], + args: unknown, + rpcDriver: string, + ) { + const superDeserialized = (await super.deserializeReturn( + feats, + args, + rpcDriver, + )) as SimpleFeatureSerialized[] + return superDeserialized.map(feat => new SimpleFeature(feat)) + } + + async serializeArguments(args: RenderArgs, rpcDriver: string) { + const { rootModel } = this.pluginManager + const assemblyManager = rootModel!.session!.assemblyManager + const renamedArgs = await renameRegionsIfNeeded(assemblyManager, args) + return super.serializeArguments( + renamedArgs, + rpcDriver, + ) as Promise + } + + async execute( + args: { + sessionId: string + regions: Region[] + adapterConfig: {} + signal?: RemoteAbortSignal + // eslint-disable-next-line @typescript-eslint/no-explicit-any + opts?: any + }, + rpcDriver: string, + ) { + const pm = this.pluginManager + const deserializedArgs = await this.deserializeArguments(args, rpcDriver) + const { signal, sessionId, adapterConfig, regions, opts } = deserializedArgs + const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) + if (!isFeatureAdapter(dataAdapter)) { + throw new Error('Adapter does not support retrieving features') + } + const ret = dataAdapter.getFeaturesInMultipleRegions(regions, { + ...opts, + signal, + }) + const r = await firstValueFrom(ret.pipe(toArray())) + return r.map(f => f.toJSON()) + } +} diff --git a/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx b/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx index 507d4e9281..ee40206cb7 100644 --- a/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx +++ b/plugins/alignments/src/LinearPileupDisplay/components/SetMaxHeight.tsx @@ -40,30 +40,30 @@ const SetMaxHeightDialog = observer(function (props: { onChange={event => setMax(event.target.value)} placeholder="Enter max height for layout" /> - - - - + + + + ) }) diff --git a/plugins/dotplot-view/src/DotplotView/1dview.ts b/plugins/dotplot-view/src/1dview.ts similarity index 100% rename from plugins/dotplot-view/src/DotplotView/1dview.ts rename to plugins/dotplot-view/src/1dview.ts diff --git a/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts b/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts index 2991d72bb6..a9a5c0e90a 100644 --- a/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts +++ b/plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts @@ -17,7 +17,7 @@ import ComparativeRenderer, { import { MismatchParser } from '@jbrowse/plugin-alignments' // locals -import { Dotplot1DView, Dotplot1DViewModel } from '../DotplotView/model' +import { Dotplot1DView, Dotplot1DViewModel } from '../1dview' import { createJBrowseTheme } from '@jbrowse/core/ui' const { parseCigar } = MismatchParser diff --git a/plugins/dotplot-view/src/DotplotView/model.ts b/plugins/dotplot-view/src/DotplotView/model.ts index 5cbd7b422e..487a2de7cd 100644 --- a/plugins/dotplot-view/src/DotplotView/model.ts +++ b/plugins/dotplot-view/src/DotplotView/model.ts @@ -36,7 +36,7 @@ import FolderOpenIcon from '@mui/icons-material/FolderOpen' import PhotoCameraIcon from '@mui/icons-material/PhotoCamera' // locals -import { Dotplot1DView, DotplotHView, DotplotVView } from './1dview' +import { Dotplot1DView, DotplotHView, DotplotVView } from '../1dview' import { getBlockLabelKeysToHide, makeTicks } from './components/util' import { BaseBlock } from '@jbrowse/core/util/blockTypes' @@ -734,5 +734,4 @@ export default function stateModelFactory(pm: PluginManager) { export type DotplotViewStateModel = ReturnType export type DotplotViewModel = Instance - -export { type Dotplot1DViewModel, Dotplot1DView } from './1dview' +export { type Dotplot1DViewModel, Dotplot1DView } from '../1dview' diff --git a/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts b/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts index a2e617af88..a9e8fe76bc 100644 --- a/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts +++ b/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts @@ -7,7 +7,7 @@ import { doesIntersect2 } from '@jbrowse/core/util/range' import { Region } from '@jbrowse/core/util/types' import { openLocation } from '@jbrowse/core/util/io' import { ObservableCreate } from '@jbrowse/core/util/rxjs' -import SimpleFeature, { Feature } from '@jbrowse/core/util/simpleFeature' +import { SimpleFeature, Feature } from '@jbrowse/core/util' import { TabixIndexedFile } from '@gmod/tabix' import gff, { GFF3Feature, GFF3FeatureLineWithRefs } from '@gmod/gff' import { Observer } from 'rxjs' @@ -36,22 +36,18 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter { pluginManager?: PluginManager, ) { super(config, getSubAdapter, pluginManager) + const pm = this.pluginManager const gffGzLocation = readConfObject(config, 'gffGzLocation') const indexType = readConfObject(config, ['index', 'indexType']) const location = readConfObject(config, ['index', 'location']) const dontRedispatch = readConfObject(config, 'dontRedispatch') this.dontRedispatch = dontRedispatch || ['chromosome', 'contig', 'region'] + const loc = openLocation(location, pm) this.gff = new TabixIndexedFile({ - filehandle: openLocation(gffGzLocation, this.pluginManager), - csiFilehandle: - indexType === 'CSI' - ? openLocation(location, this.pluginManager) - : undefined, - tbiFilehandle: - indexType !== 'CSI' - ? openLocation(location, this.pluginManager) - : undefined, + filehandle: openLocation(gffGzLocation, pm), + csiFilehandle: indexType === 'CSI' ? loc : undefined, + tbiFilehandle: indexType !== 'CSI' ? loc : undefined, chunkCacheSize: 50 * 2 ** 20, renameRefSeqs: (n: string) => n, }) @@ -152,7 +148,8 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter { f.get('end'), originalQuery.start, originalQuery.end, - ) + ) && + f.get('type') !== 'region' ) { observer.next(f) } diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx index 5aeb9aabce..3f89f3383a 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx @@ -44,9 +44,6 @@ const useStyles = makeStyles()({ type LGV = LinearGenomeViewModel -/** - * Fetches and returns a list features for a given list of regions - */ async function fetchSequence( model: LGV, regions: Region[], @@ -69,7 +66,6 @@ async function fetchSequence( throw new Error(`assembly ${assemblyName} not found`) } const adapterConfig = getConf(assembly, ['sequence', 'adapter']) - const sessionId = 'getSequence' return rpcManager.call(sessionId, 'CoreGetFeatures', { adapterConfig, @@ -186,7 +182,6 @@ const GetSequenceDialog = observer(function ({ ) : null} setCopied(false), 500) }} disabled={loading || !!error || sequenceTooLarge} - color="primary" startIcon={} > {copied ? 'Copied' : 'Copy to clipboard'} @@ -253,7 +247,6 @@ const GetSequenceDialog = observer(function ({ ) }} disabled={loading || !!error} - color="primary" startIcon={} > Download FASTA diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx index 2540b56358..bdee5e14c8 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/TrackLabel.tsx @@ -30,7 +30,10 @@ const useStyles = makeStyles()(theme => ({ }, }, trackName: { + margin: '0 auto', + width: '90%', fontSize: '0.8rem', + pointerEvents: 'none', }, iconButton: { padding: theme.spacing(1), @@ -108,10 +111,6 @@ const TrackLabel = observer( variant="body1" component="span" className={classes.trackName} - onMouseDown={event => { - // avoid becoming a click-and-drag action on the lgv - event.stopPropagation() - }} > Foo Track @@ -1294,7 +1294,7 @@ exports[`renders two tracks, two regions 1`] = ` /> Foo Track @@ -1422,7 +1422,7 @@ exports[`renders two tracks, two regions 1`] = ` /> Bar Track diff --git a/plugins/linear-genome-view/src/LinearGenomeView/model.ts b/plugins/linear-genome-view/src/LinearGenomeView/model.ts index 36463f2523..2a1a0171a9 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/model.ts +++ b/plugins/linear-genome-view/src/LinearGenomeView/model.ts @@ -1657,6 +1657,10 @@ export function stateModelFactory(pluginManager: PluginManager) { ? this.pxToBp(self.width / 2) : undefined }, + + get visibleRegions() { + return self.dynamicBlocks.contentBlocks + }, })) .actions(self => ({ afterCreate() { diff --git a/plugins/variants/src/VariantTrack/index.ts b/plugins/variants/src/VariantTrack/index.ts index 4b6c79959c..afc155aa30 100644 --- a/plugins/variants/src/VariantTrack/index.ts +++ b/plugins/variants/src/VariantTrack/index.ts @@ -3,6 +3,8 @@ import TrackType from '@jbrowse/core/pluggableElementTypes/TrackType' import { createBaseTrackModel } from '@jbrowse/core/pluggableElementTypes/models' import configSchemaF from './configSchema' +import { stringifyVCF } from './saveTrackFormats/vcf' + export default function VariantTrackF(pm: PluginManager) { pm.addTrackType(() => { const configSchema = configSchemaF(pm) @@ -10,7 +12,19 @@ export default function VariantTrackF(pm: PluginManager) { name: 'VariantTrack', displayName: 'Variant track', configSchema, - stateModel: createBaseTrackModel(pm, 'VariantTrack', configSchema), + stateModel: createBaseTrackModel(pm, 'VariantTrack', configSchema).views( + () => ({ + saveTrackFileFormatOptions() { + return { + vcf: { + name: 'VCF', + extension: 'vcf', + callback: stringifyVCF, + }, + } + }, + }), + ), }) }) } diff --git a/plugins/variants/src/VariantTrack/saveTrackFormats/vcf.ts b/plugins/variants/src/VariantTrack/saveTrackFormats/vcf.ts new file mode 100644 index 0000000000..59a927efbb --- /dev/null +++ b/plugins/variants/src/VariantTrack/saveTrackFormats/vcf.ts @@ -0,0 +1,17 @@ +import { Feature } from '@jbrowse/core/util' + +function generateINFO(feature: Feature) { + return Object.entries(feature.get('INFO')) + .map(([key, value]) => `${key}=${value}`) + .join(';') +} +export function stringifyVCF({ features }: { features: Feature[] }) { + const fields = ['refName', 'start', 'name', 'REF', 'ALT', 'QUAL', 'FILTER'] + return features + .map(feature => { + return `${fields + .map(field => feature.get(field) || '.') + .join('\t')}\t${generateINFO(feature)}` + }) + .join('\n') +} diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap index 3e686743c1..50e439393f 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap @@ -969,7 +969,7 @@ exports[` renders successfully 1`] = ` /> Reference sequence (volvox)