diff --git a/packages/core/assemblyManager/assemblyManager.ts b/packages/core/assemblyManager/assemblyManager.ts index 5b398fe78e..800f394e77 100644 --- a/packages/core/assemblyManager/assemblyManager.ts +++ b/packages/core/assemblyManager/assemblyManager.ts @@ -4,9 +4,8 @@ import { cast, getParent, IAnyType, - SnapshotOrInstance, - types, Instance, + types, } from 'mobx-state-tree' import { when } from '../util' import { readConfObject } from '../configuration' @@ -30,7 +29,12 @@ export default function assemblyManagerFactory( }, get assemblyList() { - return getParent(self).jbrowse.assemblies.slice() + // name is the explicit identifier and can be accessed without getConf, + // hence the union with {name:string} + return [ + ...getParent(self).jbrowse.assemblies, + ...(getParent(self).session.sessionAssemblies || []), + ] as (AnyConfigurationModel & { name: string })[] }, get rpcManager() { @@ -124,10 +128,7 @@ export default function assemblyManagerFactory( reaction( // have to slice it to be properly reacted to () => self.assemblyList, - ( - assemblyConfigs: Instance & - AnyConfigurationModel[], - ) => { + assemblyConfigs => { self.assemblies.forEach(asm => { if (!asm.configuration) { this.removeAssembly(asm) @@ -147,14 +148,19 @@ export default function assemblyManagerFactory( ), ) }, + + // this can take an active instance of an assembly, in which case it is + // referred to, or it can take an identifier e.g. assembly name, which is + // used as a reference. snapshots cannot be used addAssembly( - assemblyConfig: SnapshotOrInstance | string, + assemblyConfig: Instance | string, ) { self.assemblies.push({ configuration: assemblyConfig }) }, + replaceAssembly( idx: number, - assemblyConfig: SnapshotOrInstance | string, + assemblyConfig: Instance | string, ) { self.assemblies[idx] = cast({ configuration: assemblyConfig, diff --git a/packages/core/rpc/coreRpcMethods.ts b/packages/core/rpc/coreRpcMethods.ts index 6648d5ae14..9d03b48ed7 100644 --- a/packages/core/rpc/coreRpcMethods.ts +++ b/packages/core/rpc/coreRpcMethods.ts @@ -14,6 +14,7 @@ import { } from '../data_adapters/BaseAdapter' import { Region } from '../util/types' import { checkAbortSignal, renameRegionsIfNeeded } from '../util' +import SimpleFeature, { SimpleFeatureSerialized } from '../util/simpleFeature' export class CoreGetRefNames extends RpcMethodType { name = 'CoreGetRefNames' @@ -61,6 +62,12 @@ export class CoreGetFileInfo extends RpcMethodType { export class CoreGetFeatures extends RpcMethodType { name = 'CoreGetFeatures' + async deserializeReturn(feats: SimpleFeatureSerialized[]) { + return feats.map(feat => { + return new SimpleFeature(feat) + }) + } + async execute(args: { sessionId: string signal: RemoteAbortSignal @@ -76,8 +83,8 @@ export class CoreGetFeatures extends RpcMethodType { ) if (isFeatureAdapter(dataAdapter)) { const ret = dataAdapter.getFeatures(region) - const feats = await ret.pipe(toArray()).toPromise() - return JSON.parse(JSON.stringify(feats)) + const r = await ret.pipe(toArray()).toPromise() + return r.map(f => f.toJSON()) } return [] } diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index 57ec7c2262..dbdeea3e27 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -17,6 +17,7 @@ import { Feature } from './simpleFeature' import { TypeTestedByPredicate, isSessionModel, + isDisplayModel, isViewModel, isTrackModel, Region, @@ -257,6 +258,14 @@ export function getContainingTrack(node: IAnyStateTreeNode) { } } +export function getContainingDisplay(node: IAnyStateTreeNode) { + try { + return findParentThatIs(node, isDisplayModel) + } catch (e) { + throw new Error('no containing track found') + } +} + /** * Assemble a 1-based "locString" from an interbase genomic location * @param region - Region @@ -760,6 +769,7 @@ export async function renameRegionsIfNeeded< ...args, regions: [...(args.regions || [])], } + if (assemblyName) { const refNameMap = await assemblyManager.getRefNameMapForAdapter( adapterConfig, @@ -767,7 +777,6 @@ export async function renameRegionsIfNeeded< newArgs, ) - // console.log(`${JSON.stringify(regions)} ${JSON.stringify(refNameMap)}`) if (refNameMap && regions && newArgs.regions) { for (let i = 0; i < regions.length; i += 1) { newArgs.regions[i] = renameRegionIfNeeded(refNameMap, regions[i]) @@ -797,3 +806,53 @@ export function stringify({ export const isElectron = typeof window !== 'undefined' && Boolean(window.electron) + +export function revcom(seqString: string) { + return complement(seqString).split('').reverse().join('') +} + +export const complement = (() => { + const complementRegex = /[ACGT]/gi + + // from bioperl: tr/acgtrymkswhbvdnxACGTRYMKSWHBVDNX/tgcayrkmswdvbhnxTGCAYRKMSWDVBHNX/ + // generated with: + // perl -MJSON -E '@l = split "","acgtrymkswhbvdnxACGTRYMKSWHBVDNX"; print to_json({ map { my $in = $_; tr/acgtrymkswhbvdnxACGTRYMKSWHBVDNX/tgcayrkmswdvbhnxTGCAYRKMSWDVBHNX/; $in => $_ } @l})' + const complementTable = { + S: 'S', + w: 'w', + T: 'A', + r: 'y', + a: 't', + N: 'N', + K: 'M', + x: 'x', + d: 'h', + Y: 'R', + V: 'B', + y: 'r', + M: 'K', + h: 'd', + k: 'm', + C: 'G', + g: 'c', + t: 'a', + A: 'T', + n: 'n', + W: 'W', + X: 'X', + m: 'k', + v: 'b', + B: 'V', + s: 's', + H: 'D', + c: 'g', + D: 'H', + b: 'v', + R: 'Y', + G: 'C', + } as { [key: string]: string } + + return (seqString: string) => { + return seqString.replace(complementRegex, m => complementTable[m] || '') + } +})() diff --git a/packages/core/util/tracks.ts b/packages/core/util/tracks.ts index acc0dad864..96972621cb 100644 --- a/packages/core/util/tracks.ts +++ b/packages/core/util/tracks.ts @@ -19,7 +19,7 @@ export function getTrackAssemblyNames( return [readConfObject(parent, 'name')] } } - return trackAssemblyNames + return trackAssemblyNames as string[] } /** return the rpcSessionId of the highest parent node in the tree that has an rpcSessionId */ diff --git a/packages/core/util/types/index.ts b/packages/core/util/types/index.ts index e64b378f09..da627f50fe 100644 --- a/packages/core/util/types/index.ts +++ b/packages/core/util/types/index.ts @@ -57,6 +57,8 @@ export interface AbstractSessionModel extends AbstractViewContainer { assemblyManager: AssemblyManager version: string getTrackActionMenuItems?: Function + addAssembly?: Function + removeAssembly?: Function } export function isSessionModel(thing: unknown): thing is AbstractSessionModel { return ( @@ -140,6 +142,23 @@ export function isTrackModel(thing: unknown): thing is AbstractTrackModel { ) } +export interface AbstractDisplayModel { + id: string + parentTrack: AbstractTrackModel + renderDelay: number + rendererType: any // eslint-disable-line @typescript-eslint/no-explicit-any + cannotBeRenderedReason?: string +} +export function isDisplayModel(thing: unknown): thing is AbstractDisplayModel { + return ( + typeof thing === 'object' && + thing !== null && + 'configuration' in thing && + // @ts-ignore + thing.configuration.displayId + ) +} + export interface TrackViewModel extends AbstractViewModel { showTrack(trackId: string): void hideTrack(trackId: string): void diff --git a/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.test.ts b/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.test.ts index 75bc4f4879..f3a5637b99 100644 --- a/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.test.ts +++ b/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.test.ts @@ -2,53 +2,64 @@ import { toArray } from 'rxjs/operators' import Adapter from './FromConfigSequenceAdapter' import { sequenceConfigSchema } from './configSchema' -test('adapter can fetch sequences', async () => { +test('adapter can fetch sequences when there is just one feature representing whole refseq', async () => { const features = [ { uniqueId: 'one', refName: 'ctgA', - start: 250, - end: 400, + start: 0, + end: 150, seq: 'ccaaaccgtcaattaaccggtatcttctcggaaacggcggttctctcctagatagcgatctgtggtctcaccatgcaatttaaacaggtgagtaaagattgctacaaatacgagactagctgtcaccagatgctgttcatctgttggctc', }, - { - uniqueId: 'two', - refName: 'ctgA', - start: 150, - end: 250, - seq: - 'attctgattcagcctgacttctcttggaaccctgcccataaatcaaagggttagtgcggccaaaacgttggacaacggtattagaagaccaacctgacca', - }, - { - uniqueId: 'three', - refName: 'ctgB', - start: 50, - end: 60, - seq: 'TACATGCTAGC', - }, ] const adapter = new Adapter(sequenceConfigSchema.create({ features })) const result = adapter.getFeatures({ refName: 'ctgA', start: 0, - end: 500, + end: 50, }) const featuresArray = await result.pipe(toArray()).toPromise() - expect(featuresArray.length).toBe(2) - expect(featuresArray[0].toJSON()).toEqual(features[1]) + expect(featuresArray.length).toBe(1) + expect(featuresArray[0].get('seq')).toBe(features[0].seq.slice(0, 50)) + + const result2 = adapter.getFeatures({ + refName: 'ctgA', + start: 100, + end: 150, + }) + const featuresArray2 = await result2.pipe(toArray()).toPromise() + expect(featuresArray2.length).toBe(1) + expect(featuresArray2[0].get('seq')).toBe(features[0].seq.slice(100, 150)) }) -test('adapter can fetch regions 1', async () => { +test("adapter can fetch sequences when the config's sequence doesn't start at 0", async () => { const features = [ - { uniqueId: 'one', refName: 'ctgA', start: 250, end: 400 }, - { uniqueId: 'two', refName: 'ctgA', start: 150, end: 300 }, - { uniqueId: 'three', refName: 'ctgB', start: 50, end: 60 }, + { + uniqueId: 'one', + refName: 'ctgA', + start: 5000, + end: 5150, + seq: + 'ccaaaccgtcaattaaccggtatcttctcggaaacggcggttctctcctagatagcgatctgtggtctcaccatgcaatttaaacaggtgagtaaagattgctacaaatacgagactagctgtcaccagatgctgttcatctgttggctc', + }, ] const adapter = new Adapter(sequenceConfigSchema.create({ features })) - const result = await adapter.getRegions() - expect(result).toEqual([ - { refName: 'ctgA', start: 150, end: 400 }, - { refName: 'ctgB', start: 50, end: 60 }, - ]) + const result = adapter.getFeatures({ + refName: 'ctgA', + start: 4950, + end: 5050, + }) + const featuresArray = await result.pipe(toArray()).toPromise() + expect(featuresArray.length).toBe(1) + expect(featuresArray[0].get('seq')).toBe(features[0].seq.slice(0, 50)) + + const result2 = adapter.getFeatures({ + refName: 'ctgA', + start: 5050, + end: 5150, + }) + const featuresArray2 = await result2.pipe(toArray()).toPromise() + expect(featuresArray2.length).toBe(1) + expect(featuresArray2[0].get('seq')).toBe(features[0].seq.slice(50, 150)) }) diff --git a/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.ts b/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.ts index 11bcfe81ed..a5aff37d03 100644 --- a/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.ts +++ b/plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.ts @@ -11,33 +11,32 @@ export default class FromSequenceConfigAdapter extends FromConfigAdapter { * @returns Observable of Feature objects in the region */ getFeatures(region: NoAssemblyRegion) { - const { start, end } = region // TODO: restore commented version below once TSDX supports Rollup v2 // xref: https://github.com/rollup/rollup/blob/master/CHANGELOG.md#bug-fixes-45 + // return ObservableCreate(async observer => { + // const feats = await super.getFeatures(region).pipe(toArray()).toPromise() const superGetFeatures = super.getFeatures return ObservableCreate(async observer => { const feats = await superGetFeatures .call(this, region) .pipe(toArray()) .toPromise() - // return ObservableCreate(async observer => { - // const feats = await super.getFeatures(region).pipe(toArray()).toPromise() - feats.forEach(feat => { - const featStart = feat.get('start') - const seqStart = start - featStart - const seqEnd = seqStart + (end - start) - const seq = feat - .get('seq') - .slice(Math.max(seqStart, 0), Math.max(seqEnd, 0)) - observer.next( - new SimpleFeature({ - ...feat.toJSON(), - seq, - end: featStart + seq.length, - start: featStart, - }), - ) - }) + const feat = feats[0] + observer.next( + new SimpleFeature({ + ...feat.toJSON(), + uniqueId: `${feat.id()}:${region.start}-${region.end}`, + end: region.end, + start: region.start, + seq: feat + .get('seq') + .slice( + Math.max(region.start - feat.get('start'), 0), + Math.max(region.end - feat.get('start'), 0), + ), + }), + ) + observer.complete() }) } diff --git a/plugins/dotplot-view/src/index.ts b/plugins/dotplot-view/src/index.ts index 7f217b3740..fbfeb23d48 100644 --- a/plugins/dotplot-view/src/index.ts +++ b/plugins/dotplot-view/src/index.ts @@ -11,6 +11,7 @@ import { } from '@jbrowse/core/util' import { getConf } from '@jbrowse/core/configuration' import { Feature } from '@jbrowse/core/util/simpleFeature' +import { AbstractDisplayModel } from '@jbrowse/core/util/types' import TimelineIcon from '@material-ui/icons/Timeline' import { MismatchParser } from '@jbrowse/plugin-alignments' import { IAnyStateTreeNode } from 'mobx-state-tree' @@ -157,7 +158,11 @@ export default class DotplotPlugin extends Plugin { }) } - const cb = (feature: Feature, display: IAnyStateTreeNode) => { + const cb = ( + feature: Feature, + display: AbstractDisplayModel & IAnyStateTreeNode, + ) => { + const { parentTrack } = display return feature ? [ { @@ -174,11 +179,7 @@ export default class DotplotPlugin extends Plugin { : feature.get('SA')) || '' const readName = feature.get('name') const readAssembly = `${readName}_assembly` - const trackAssembly = getConf( - // @ts-ignore - display.parentTrack, - 'assemblyNames', - )[0] + const [trackAssembly] = getConf(parentTrack, 'assemblyNames') const assemblyNames = [trackAssembly, readAssembly] const trackId = `track-${Date.now()}` const trackName = `${readName}_vs_${trackAssembly}` @@ -191,7 +192,7 @@ export default class DotplotPlugin extends Plugin { const saLengthSansClipping = getLengthSansClipping(saCigar) const saStrandNormalized = saStrand === '-' ? -1 : 1 const saClipPos = getClip(saCigar, saStrandNormalized) - const saRealStart = +saStart - 1 + saClipPos + const saRealStart = +saStart - 1 return { refName: saRef, start: saRealStart, @@ -217,9 +218,10 @@ export default class DotplotPlugin extends Plugin { end: clipPos + getLengthSansClipping(cigar), } - // if secondary alignment or supplementary, calculate length from SA[0]'s CIGAR - // which is the primary alignments. otherwise it is the primary alignment just use - // seq.length if primary alignment + // if secondary alignment or supplementary, calculate length + // from SA[0]'s CIGAR which is the primary alignments. + // otherwise it is the primary alignment just use seq.length if + // primary alignment const totalLength = // eslint-disable-next-line no-bitwise flags & 2048 diff --git a/plugins/linear-comparative-view/src/LinearComparativeView/model.ts b/plugins/linear-comparative-view/src/LinearComparativeView/model.ts index cc99528fab..8b65ba51ba 100644 --- a/plugins/linear-comparative-view/src/LinearComparativeView/model.ts +++ b/plugins/linear-comparative-view/src/LinearComparativeView/model.ts @@ -56,10 +56,6 @@ export default function stateModelFactory(pluginManager: PluginManager) { viewTrackConfigs: types.array( pluginManager.pluggableConfigSchemaType('track'), ), - - // this represents assemblies in the specialized - // read vs ref dotplot view - viewAssemblyConfigs: types.array(types.frozen()), }), ) .volatile(() => ({ @@ -83,6 +79,7 @@ export default function stateModelFactory(pluginManager: PluginManager) { // // Get a composite map of featureId->feature map for a track // // across multiple views + // // getTrackFeatures(trackIds: string[]) { // // @ts-ignore // const tracks = trackIds.map(t => resolveIdentifier(getSession(self), t)) @@ -112,6 +109,17 @@ export default function stateModelFactory(pluginManager: PluginManager) { ) }, + // automatically removes session assemblies associated with this view + // e.g. read vs ref + beforeDestroy() { + const session = getSession(self) + self.assemblyNames.forEach(name => { + if (name.endsWith('-temp')) { + session.removeAssembly?.(name) + } + }) + }, + onSubviewAction(actionName: string, path: string, args: any[] = []) { self.views.forEach(view => { const ret = getPath(view) diff --git a/plugins/linear-comparative-view/src/LinearSyntenyView/model.test.ts b/plugins/linear-comparative-view/src/LinearSyntenyView/model.test.ts index 7932ada712..1863b968da 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyView/model.test.ts +++ b/plugins/linear-comparative-view/src/LinearSyntenyView/model.test.ts @@ -1,9 +1,13 @@ import PluginManager from '@jbrowse/core/PluginManager' -import LinearGenomeView from '@jbrowse/plugin-linear-genome-view' +import LinearGenomeViewPlugin from '@jbrowse/plugin-linear-genome-view' +import SequencePlugin from '@jbrowse/plugin-sequence' import stateModelFactory from './model' test('creation', () => { - const pluginManager = new PluginManager([new LinearGenomeView()]) + const pluginManager = new PluginManager([ + new LinearGenomeViewPlugin(), + new SequencePlugin(), + ]) .createPluggableElements() .configure() const model = stateModelFactory(pluginManager) diff --git a/plugins/linear-comparative-view/src/index.tsx b/plugins/linear-comparative-view/src/index.tsx index 56054f8e1a..30dde6f60a 100644 --- a/plugins/linear-comparative-view/src/index.tsx +++ b/plugins/linear-comparative-view/src/index.tsx @@ -1,19 +1,25 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import React, { useState } from 'react' +/* eslint-disable @typescript-eslint/no-explicit-any,no-bitwise */ +import React, { useEffect, useState } from 'react' import { makeStyles } from '@material-ui/core/styles' -import Button from '@material-ui/core/Button' -import TextField from '@material-ui/core/TextField' -import Typography from '@material-ui/core/Typography' -import Dialog from '@material-ui/core/Dialog' -import DialogContent from '@material-ui/core/DialogContent' -import DialogTitle from '@material-ui/core/DialogTitle' -import IconButton from '@material-ui/core/IconButton' +import { + Button, + CircularProgress, + Checkbox, + FormControlLabel, + TextField, + Typography, + Dialog, + DialogContent, + DialogTitle, + IconButton, +} from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' import AddIcon from '@material-ui/icons/Add' import CalendarIcon from '@material-ui/icons/CalendarViewDay' import { ConfigurationSchema, getConf } from '@jbrowse/core/configuration' import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType' import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' +import { Feature } from '@jbrowse/core/util/simpleFeature' import { createBaseTrackConfig, createBaseTrackModel, @@ -28,9 +34,10 @@ import { getContainingView, isAbstractMenuManager, } from '@jbrowse/core/util' -import { Feature } from '@jbrowse/core/util/simpleFeature' + import { MismatchParser } from '@jbrowse/plugin-alignments' import { autorun } from 'mobx' +import { getRpcSessionId } from '@jbrowse/core/util/tracks' import { configSchemaFactory as linearComparativeDisplayConfigSchemaFactory, ReactComponent as LinearComparativeDisplayReactComponent, @@ -121,6 +128,7 @@ interface ReducedFeature { start: number clipPos: number end: number + strand: number seqLength: number syntenyId?: number uniqueId: string @@ -145,6 +153,11 @@ const useStyles = makeStyles(theme => ({ }, })) +function getTag(f: Feature, tag: string) { + const tags = f.get('tags') + return tags ? tags[tag] : f.get(tag) +} + function WindowSizeDlg(props: { display: any handleClose: () => void @@ -153,24 +166,66 @@ function WindowSizeDlg(props: { const classes = useStyles() const { track, - display: { feature }, + display: { feature: preFeature }, handleClose, } = props const [window, setWindowSize] = useState('0') const [error, setError] = useState() const windowSize = +window + const [primaryFeature, setPrimaryFeature] = useState() + const [qualTrack, setQualTrack] = useState(false) + + // we need to fetch the primary alignment if the selected feature is 2048. + // this should be the first in the list of the SA tag + useEffect(() => { + let done = false + ;(async () => { + if (preFeature.get('flags') & 2048) { + const SA: string = getTag(preFeature, 'SA') || '' + const primaryAln = SA.split(';')[0] + const [saRef, saStart] = primaryAln.split(',') + const { rpcManager } = getSession(track) + const adapterConfig = getConf(track, 'adapter') + const sessionId = getRpcSessionId(track) + const feats = (await rpcManager.call(sessionId, 'CoreGetFeatures', { + adapterConfig, + sessionId, + region: { refName: saRef, start: +saStart - 1, end: +saStart }, + })) as any[] + const primaryFeat = feats.find( + f => + f.get('name') === preFeature.get('name') && + !(f.get('flags') & 2048), + ) + if (!done) { + setPrimaryFeature(primaryFeat) + } + } else { + setPrimaryFeature(preFeature) + } + })() + + return () => { + done = true + } + }, [preFeature, track]) function onSubmit() { try { + const feature = primaryFeature || preFeature const session = getSession(track) const view = getContainingView(track) - const clipPos = feature.get('clipPos') const cigar = feature.get('CIGAR') + const clipPos = getClip(cigar, 1) const flags = feature.get('flags') - const SA: string = - (feature.get('tags') ? feature.get('tags').SA : feature.get('SA')) || '' + const qual = feature.get('qual') as string + const SA: string = getTag(feature, 'SA') || '' const readName = feature.get('name') - const readAssembly = `${readName}_assembly` + + // the suffix -temp is used in the beforeDetach handler to + // automatically remove itself from the session when this view is + // destroyed + const readAssembly = `${readName}_assembly-temp` const [trackAssembly] = getConf(track, 'assemblyNames') const assemblyNames = [trackAssembly, readAssembly] const trackId = `track-${Date.now()}` @@ -190,8 +245,8 @@ function WindowSizeDlg(props: { const saLength = getLength(saCigar) const saLengthSansClipping = getLengthSansClipping(saCigar) const saStrandNormalized = saStrand === '-' ? -1 : 1 - const saClipPos = getClip(saCigar, saStrandNormalized) - const saRealStart = +saStart - 1 + saClipPos + const saClipPos = getClip(saCigar, 1) + const saRealStart = +saStart - 1 return { refName: saRef, start: saRealStart, @@ -211,6 +266,7 @@ function WindowSizeDlg(props: { }) const feat = feature.toJSON() + feat.clipPos = clipPos feat.mate = { refName: readName, @@ -218,11 +274,10 @@ function WindowSizeDlg(props: { end: clipPos + getLengthSansClipping(cigar), } - // if secondary alignment or supplementary, calculate length from SA[0]'s CIGAR - // which is the primary alignments. otherwise it is the primary alignment just use - // seq.length if primary alignment + // if secondary alignment or supplementary, calculate length from SA[0]'s + // CIGAR which is the primary alignments. otherwise it is the primary + // alignment just use seq.length if primary alignment const totalLength = - // eslint-disable-next-line no-bitwise flags & 2048 ? getLength(supplementaryAlignments[0].CIGAR) : getLength(cigar) @@ -237,6 +292,8 @@ function WindowSizeDlg(props: { }) features.sort((a, b) => a.clipPos - b.clipPos) + const featSeq = feature.get('seq') + // the config feature store includes synthetic mate features // mapped to the read assembly const configFeatureStore = features.concat( @@ -244,11 +301,47 @@ function WindowSizeDlg(props: { features.map(f => f.mate), ) + const expand = 2 * windowSize const refLength = features.reduce( - (a, f) => a + f.end - f.start + 2 * windowSize, + (a, f) => a + f.end - f.start + expand, 0, ) + const seqTrackId = `${readName}_${Date.now()}` + const sequenceTrackConf = getConf(assembly, 'sequence') + const lgvRegions = features + .map(f => { + return { + ...f, + start: Math.max(0, f.start - windowSize), + end: f.end + windowSize, + assemblyName: trackAssembly, + } + }) + .sort((a, b) => a.clipPos - b.clipPos) + + session.addAssembly?.({ + name: `${readAssembly}`, + sequence: { + type: 'ReferenceSequenceTrack', + trackId: seqTrackId, + assemblyNames: [readAssembly], + adapter: { + type: 'FromConfigSequenceAdapter', + noAssemblyManager: true, + features: [ + { + start: 0, + end: totalLength, + seq: featSeq, + refName: readName, + uniqueId: `${Math.random()}`, + }, + ], + }, + }, + }) + session.addView('LinearSyntenyView', { type: 'LinearSyntenyView', views: [ @@ -257,14 +350,25 @@ function WindowSizeDlg(props: { hideHeader: true, offsetPx: 0, bpPerPx: refLength / view.width, - displayedRegions: features.map(f => { - return { - start: f.start - windowSize, - end: f.end + windowSize, - refName: f.refName, - assemblyName: trackAssembly, - } - }), + displayedRegions: lgvRegions, + tracks: [ + { + id: `${Math.random()}`, + type: 'ReferenceSequenceTrack', + assemblyNames: [trackAssembly], + configuration: sequenceTrackConf.trackId, + displays: [ + { + id: `${Math.random()}`, + type: 'LinearReferenceSequenceDisplay', + showReverse: false, + showTranslation: false, + height: 35, + configuration: `${seqTrackId}-LinearReferenceSequenceDisplay`, + }, + ], + }, + ], }, { type: 'LinearGenomeView', @@ -279,6 +383,57 @@ function WindowSizeDlg(props: { refName: readName, }, ], + tracks: [ + { + id: `${Math.random()}`, + type: 'ReferenceSequenceTrack', + configuration: seqTrackId, + displays: [ + { + id: `${Math.random()}`, + type: 'LinearReferenceSequenceDisplay', + showReverse: false, + showTranslation: false, + height: 35, + configuration: `${seqTrackId}-LinearReferenceSequenceDisplay`, + }, + ], + }, + ...(qualTrack + ? [ + { + id: `${Math.random()}`, + type: 'QuantitativeTrack', + configuration: { + trackId: 'qualTrack', + assemblyNames: [readAssembly], + name: 'Read quality', + type: 'QuantitativeTrack', + adapter: { + type: 'FromConfigAdapter', + noAssemblyManager: true, + features: qual.split(' ').map((score, index) => { + return { + start: index, + end: index + 1, + refName: readName, + score: +score, + uniqueId: `feat_${index}`, + } + }), + }, + }, + displays: [ + { + id: `${Math.random()}`, + type: 'LinearWiggleDisplay', + height: 100, + }, + ], + }, + ] + : []), + ], }, ], viewTrackConfigs: [ @@ -289,9 +444,6 @@ function WindowSizeDlg(props: { type: 'FromConfigAdapter', features: configFeatureStore, }, - renderer: { - type: 'LinearSyntenyRenderer', - }, trackId, name: trackName, }, @@ -316,6 +468,7 @@ function WindowSizeDlg(props: { setError(e) } } + return ( -
- - Show an extra window size around each part of the split alignment. - Using a larger value can allow you to see more genomic context. - - {error ? {`${error}`} : null} - - { - setWindowSize(event.target.value) - }} - label="Set window size" - /> - -
+ {!primaryFeature ? ( +
+ + To accurately perform comparison we are fetching the primary + alignment. Loading primary feature... + + +
+ ) : ( +
+ + Show an extra window size around each part of the split alignment. + Using a larger value can allow you to see more genomic context. + + {error ? {`${error}`} : null} + + { + setWindowSize(event.target.value) + }} + label="Set window size" + /> + setQualTrack(val => !val)} + /> + } + label="Show qual track" + /> + +
+ )}
) diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/ServerSideRenderedBlockContent.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/ServerSideRenderedBlockContent.tsx index b4b48f5097..8979ebff55 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/ServerSideRenderedBlockContent.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/ServerSideRenderedBlockContent.tsx @@ -1,10 +1,9 @@ -import { makeStyles } from '@material-ui/core/styles' +import React, { useEffect, useState } from 'react' import Typography from '@material-ui/core/Typography' -import { observer, PropTypes as MobxPropTypes } from 'mobx-react' +import { makeStyles } from '@material-ui/core/styles' +import { observer } from 'mobx-react' import { getParent } from 'mobx-state-tree' import { getParentRenderProps } from '@jbrowse/core/util/tracks' -import PropTypes from 'prop-types' -import React, { useEffect, useState } from 'react' import Button from '@material-ui/core/Button' import RefreshIcon from '@material-ui/icons/Refresh' import ServerSideRenderedContent from './ServerSideRenderedContent' @@ -105,10 +104,6 @@ function BlockMessage({
{messageContent}
) } -BlockMessage.propTypes = { - messageContent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) - .isRequired, -} function BlockError({ error, @@ -141,13 +136,6 @@ function BlockError({ ) } -BlockError.propTypes = { - error: MobxPropTypes.objectOrObservableObject.isRequired, - reload: PropTypes.func, -} -BlockError.defaultProps = { - reload: undefined, -} const ServerSideRenderedBlockContent = observer( ({ diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts b/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts index 5d90c2be09..79042b986c 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts @@ -1,13 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { types, getParent, isAlive, cast, Instance } from 'mobx-state-tree' import { Component } from 'react' -import { getConf, readConfObject } from '@jbrowse/core/configuration' +import { readConfObject } from '@jbrowse/core/configuration' import { Region } from '@jbrowse/core/util/types/mst' import { assembleLocString, makeAbortableReaction, getSession, + getContainingDisplay, } from '@jbrowse/core/util' import { getTrackAssemblyNames, @@ -42,7 +43,7 @@ const blockState = types let renderInProgress: undefined | AbortController return { afterAttach() { - const display = getParent(self, 2) + const display = getContainingDisplay(self) makeAbortableReaction( self as any, renderBlockData, @@ -154,19 +155,22 @@ const blockState = types if (renderInProgress && !renderInProgress.signal.aborted) { renderInProgress.abort() } - const display = getParent(self, 2) + const display = getContainingDisplay(self) const { rpcManager } = getSession(self) const { rendererType } = display const { renderArgs } = renderBlockData(cast(self)) - rendererType - .freeResourcesInClient( - rpcManager, - JSON.parse(JSON.stringify(renderArgs)), - ) - .catch((e: Error) => { - // just console.error if it's something while it's being destroyed - console.warn('Error while destroying block', e) - }) + // renderArgs can be undefined if an error occured in this block + if (renderArgs) { + rendererType + .freeResourcesInClient( + rpcManager, + JSON.parse(JSON.stringify(renderArgs)), + ) + .catch((e: Error) => { + // just console.error if it's something while it's being destroyed + console.warn('Error while destroying block', e) + }) + } }, } }) @@ -179,44 +183,39 @@ export type BlockModel = Instance // work with autorun function renderBlockData(self: Instance) { const { assemblyManager, rpcManager } = getSession(self) - let display = getParent(self) - while (!(display.configuration && getConf(display, 'displayId'))) { - display = getParent(display) - } - const assemblyNames = getTrackAssemblyNames(display.parentTrack) - let cannotBeRenderedReason - if (!assemblyNames.includes(self.region.assemblyName)) { - let matchFound = false - assemblyNames.forEach((assemblyName: string) => { - const assembly = assemblyManager.get(assemblyName) - if (assembly && assembly.hasName(assemblyName)) { - matchFound = true - } - }) - if (!matchFound) { - cannotBeRenderedReason = `region assembly (${self.region.assemblyName}) does not match track assemblies (${assemblyNames})` - } - } - if (!cannotBeRenderedReason) { - cannotBeRenderedReason = display.regionCannotBeRendered(self.region) + const display = getContainingDisplay(self) as any + const { + adapterConfig, + renderProps, + rendererType, + error: displayError, + parentTrack, + } = display + const assemblyNames = getTrackAssemblyNames(parentTrack) + const regionAsm = self.region.assemblyName + if ( + !assemblyNames.includes(regionAsm) && + !assemblyNames.find(name => assemblyManager.get(name)?.hasName(regionAsm)) + ) { + throw new Error( + `region assembly (${regionAsm}) does not match track assemblies (${assemblyNames})`, + ) } - const { renderProps } = display - const { rendererType } = display + const { config } = renderProps // This line is to trigger the mobx reaction when the config changes // It won't trigger the reaction if it doesn't think we're accessing it readConfObject(config) - const { adapterConfig } = display - const sessionId = getRpcSessionId(display) + const cannotBeRenderedReason = display.regionCannotBeRendered(self.region) return { rendererType, rpcManager, renderProps, cannotBeRenderedReason, - displayError: display.error, + displayError, renderArgs: { statusCallback: (message: string) => { if (isAlive(self)) { diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/SequenceDialog.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/SequenceDialog.tsx index aae362b781..066026229a 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/SequenceDialog.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/SequenceDialog.tsx @@ -22,10 +22,7 @@ import TextField from '@material-ui/core/TextField' // core import { getSession } from '@jbrowse/core/util' -import SimpleFeature, { - Feature, - SimpleFeatureSerialized, -} from '@jbrowse/core/util/simpleFeature' +import { Feature } from '@jbrowse/core/util/simpleFeature' // other import { formatSeqFasta, SeqChunk } from '@jbrowse/core/util/formatFastaStrings' import { LinearGenomeViewModel } from '..' @@ -61,15 +58,10 @@ async function fetchSequence( const assemblyName = self.leftOffset?.assemblyName || self.rightOffset?.assemblyName || '' const { rpcManager, assemblyManager } = session - const assembly = assemblyManager.get(assemblyName) - if (!assembly) { - throw new Error(`Could not find assembly ${assemblyName}`) - } + const assemblyConfig = assemblyManager.get(assemblyName)?.configuration + // assembly configuration - const adapterConfig = readConfObject(assembly.configuration, [ - 'sequence', - 'adapter', - ]) + const adapterConfig = readConfObject(assemblyConfig, ['sequence', 'adapter']) const sessionId = 'getSequence' const chunks = (await Promise.all( @@ -80,10 +72,10 @@ async function fetchSequence( sessionId, }), ), - )) as SimpleFeatureSerialized[][] + )) as Feature[][] // assumes that we get whole sequence in a single getFeatures call - return chunks.map(chunk => new SimpleFeature(chunk[0])) + return chunks.map(chunk => chunk[0]) } function SequenceDialog({ diff --git a/plugins/linear-genome-view/src/LinearGenomeView/index.ts b/plugins/linear-genome-view/src/LinearGenomeView/index.ts index ad7c5e30b4..69b9ae8f33 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/index.ts +++ b/plugins/linear-genome-view/src/LinearGenomeView/index.ts @@ -146,7 +146,7 @@ export function stateModelFactory(pluginManager: PluginManager) { const assembliesInitialized = this.assemblyNames.every(assemblyName => { if ( assemblyManager.assemblyList - ?.map((asm: { name: string }) => asm.name) + ?.map(asm => asm.name) .includes(assemblyName) ) { return (assemblyManager.get(assemblyName) || {}).initialized diff --git a/plugins/sequence/src/DivSequenceRenderer/components/DivSequenceRendering.tsx b/plugins/sequence/src/DivSequenceRenderer/components/DivSequenceRendering.tsx index 73d1abf4f6..1367010859 100644 --- a/plugins/sequence/src/DivSequenceRenderer/components/DivSequenceRendering.tsx +++ b/plugins/sequence/src/DivSequenceRenderer/components/DivSequenceRendering.tsx @@ -6,7 +6,7 @@ import { Region } from '@jbrowse/core/util/types' import { createJBrowseTheme } from '@jbrowse/core/ui' import { observer } from 'mobx-react' import React from 'react' -import { bpSpanPx } from '@jbrowse/core/util' +import { bpSpanPx, revcom, complement } from '@jbrowse/core/util' interface MyProps { features: Map @@ -20,56 +20,6 @@ interface MyProps { showTranslation: boolean } -function revcom(seqString: string) { - return complement(seqString).split('').reverse().join('') -} - -const complement = (() => { - const complementRegex = /[ACGT]/gi - - // from bioperl: tr/acgtrymkswhbvdnxACGTRYMKSWHBVDNX/tgcayrkmswdvbhnxTGCAYRKMSWDVBHNX/ - // generated with: - // perl -MJSON -E '@l = split "","acgtrymkswhbvdnxACGTRYMKSWHBVDNX"; print to_json({ map { my $in = $_; tr/acgtrymkswhbvdnxACGTRYMKSWHBVDNX/tgcayrkmswdvbhnxTGCAYRKMSWDVBHNX/; $in => $_ } @l})' - const complementTable = { - S: 'S', - w: 'w', - T: 'A', - r: 'y', - a: 't', - N: 'N', - K: 'M', - x: 'x', - d: 'h', - Y: 'R', - V: 'B', - y: 'r', - M: 'K', - h: 'd', - k: 'm', - C: 'G', - g: 'c', - t: 'a', - A: 'T', - n: 'n', - W: 'W', - X: 'X', - m: 'k', - v: 'b', - B: 'V', - s: 's', - H: 'D', - c: 'g', - D: 'H', - b: 'v', - R: 'Y', - G: 'C', - } as { [key: string]: string } - - return (seqString: string) => { - return seqString.replace(complementRegex, m => complementTable[m] || '') - } -})() - const defaultStarts = ['ATG'] const defaultStops = ['TAA', 'TAG', 'TGA'] const defaultCodonTable = { diff --git a/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap b/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap index 7b6da74caa..326801de6a 100644 --- a/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap +++ b/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap @@ -611,6 +611,7 @@ Object { }, "assemblyNames": Array [ "volvox", + "volvox2", ], "category": Array [ "Integration test", diff --git a/products/jbrowse-web/src/__snapshots__/sessionModelFactory.test.js.snap b/products/jbrowse-web/src/__snapshots__/sessionModelFactory.test.js.snap index 1e3d479e9a..16c6851aba 100644 --- a/products/jbrowse-web/src/__snapshots__/sessionModelFactory.test.js.snap +++ b/products/jbrowse-web/src/__snapshots__/sessionModelFactory.test.js.snap @@ -7,6 +7,7 @@ Object { "drawerWidth": 384, "margin": 0, "name": "testSession", + "sessionAssemblies": Array [], "sessionConnections": Array [], "sessionTracks": Array [], "views": Array [], diff --git a/products/jbrowse-web/src/jbrowseModel.test.ts b/products/jbrowse-web/src/jbrowseModel.test.ts index 462a3eca6b..bb985d765d 100644 --- a/products/jbrowse-web/src/jbrowseModel.test.ts +++ b/products/jbrowse-web/src/jbrowseModel.test.ts @@ -14,7 +14,7 @@ describe('JBrowse model', () => { const pluginManager = new PluginManager(corePlugins.map(P => new P())) .createPluggableElements() .configure() - const Session = sessionModelFactory(pluginManager) + const { assemblyConfigSchemas, dispatcher } = AssemblyConfigSchemasFactory( pluginManager, ) @@ -22,6 +22,10 @@ describe('JBrowse model', () => { { dispatcher }, ...assemblyConfigSchemas, ) + const Session = sessionModelFactory( + pluginManager, + assemblyConfigSchemasType, + ) JBrowseModel = jbrowseModelFactory( pluginManager, Session, diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel.ts index e33495f902..6d07715458 100644 --- a/products/jbrowse-web/src/rootModel.ts +++ b/products/jbrowse-web/src/rootModel.ts @@ -97,7 +97,6 @@ export default function RootModel( pluginManager: PluginManager, adminMode = false, ) { - const Session = sessionModelFactory(pluginManager) const { assemblyConfigSchemas, dispatcher } = AssemblyConfigSchemasFactory( pluginManager, ) @@ -105,6 +104,7 @@ export default function RootModel( { dispatcher }, ...assemblyConfigSchemas, ) + const Session = sessionModelFactory(pluginManager, assemblyConfigSchemasType) const assemblyManagerType = assemblyManagerFactory( assemblyConfigSchemasType, pluginManager, diff --git a/products/jbrowse-web/src/sessionModelFactory.ts b/products/jbrowse-web/src/sessionModelFactory.ts index a8a68bdf2b..23d2c93f05 100644 --- a/products/jbrowse-web/src/sessionModelFactory.ts +++ b/products/jbrowse-web/src/sessionModelFactory.ts @@ -40,7 +40,10 @@ declare interface ReferringNode { key: string } -export default function sessionModelFactory(pluginManager: PluginManager) { +export default function sessionModelFactory( + pluginManager: PluginManager, + assemblyConfigSchemasType = types.frozen(), // if not using sessionAssemblies +) { const minDrawerWidth = 128 return types .model('JBrowseWebSessionModel', { @@ -69,6 +72,7 @@ export default function sessionModelFactory(pluginManager: PluginManager) { sessionConnections: types.array( pluginManager.pluggableConfigSchemaType('connection'), ), + sessionAssemblies: types.array(assemblyConfigSchemasType), }) .volatile((/* self */) => ({ pluginManager, @@ -172,6 +176,17 @@ export default function sessionModelFactory(pluginManager: PluginManager) { setName(str: string) { self.name = str }, + addAssembly(assemblyConfig: any) { + self.sessionAssemblies.push(assemblyConfig) + }, + removeAssembly(assemblyName: string) { + const index = self.sessionAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.sessionAssemblies.splice(index, 1) + } + }, makeConnection( configuration: AnyConfigurationModel, diff --git a/test_data/volvox/config.json b/test_data/volvox/config.json index b9ed535bbc..71ccb99df4 100644 --- a/test_data/volvox/config.json +++ b/test_data/volvox/config.json @@ -249,7 +249,7 @@ "trackId": "volvox_alignments", "name": "volvox-sorted.bam (ctgA, svg)", "category": ["Integration test"], - "assemblyNames": ["volvox"], + "assemblyNames": ["volvox", "volvox2"], "adapter": { "type": "BamAdapter", "bamLocation": {