Skip to content

Commit

Permalink
Merge pull request #1699 from GMOD/render_seq_track_linear_vs_ref
Browse files Browse the repository at this point in the history
Add sequence track for both read and reference genome in the "Linear read vs ref" comparison
  • Loading branch information
cmdcolin authored Feb 19, 2021
2 parents 9123f2c + f65b2b0 commit 31ce25f
Show file tree
Hide file tree
Showing 22 changed files with 493 additions and 256 deletions.
24 changes: 15 additions & 9 deletions packages/core/assemblyManager/assemblyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import {
cast,
getParent,
IAnyType,
SnapshotOrInstance,
types,
Instance,
types,
} from 'mobx-state-tree'
import { when } from '../util'
import { readConfObject } from '../configuration'
Expand All @@ -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() {
Expand Down Expand Up @@ -124,10 +128,7 @@ export default function assemblyManagerFactory(
reaction(
// have to slice it to be properly reacted to
() => self.assemblyList,
(
assemblyConfigs: Instance<typeof Assembly> &
AnyConfigurationModel[],
) => {
assemblyConfigs => {
self.assemblies.forEach(asm => {
if (!asm.configuration) {
this.removeAssembly(asm)
Expand All @@ -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<typeof assemblyConfigType> | string,
assemblyConfig: Instance<typeof assemblyConfigType> | string,
) {
self.assemblies.push({ configuration: assemblyConfig })
},

replaceAssembly(
idx: number,
assemblyConfig: SnapshotOrInstance<typeof assemblyConfigType> | string,
assemblyConfig: Instance<typeof assemblyConfigType> | string,
) {
self.assemblies[idx] = cast({
configuration: assemblyConfig,
Expand Down
11 changes: 9 additions & 2 deletions packages/core/rpc/coreRpcMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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 []
}
Expand Down
61 changes: 60 additions & 1 deletion packages/core/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Feature } from './simpleFeature'
import {
TypeTestedByPredicate,
isSessionModel,
isDisplayModel,
isViewModel,
isTrackModel,
Region,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -760,14 +769,14 @@ export async function renameRegionsIfNeeded<
...args,
regions: [...(args.regions || [])],
}

if (assemblyName) {
const refNameMap = await assemblyManager.getRefNameMapForAdapter(
adapterConfig,
assemblyName,
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])
Expand Down Expand Up @@ -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] || '')
}
})()
2 changes: 1 addition & 1 deletion packages/core/util/tracks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
19 changes: 19 additions & 0 deletions packages/core/util/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
37 changes: 18 additions & 19 deletions plugins/config/src/FromConfigAdapter/FromConfigSequenceAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Feature>(async observer => {
// const feats = await super.getFeatures(region).pipe(toArray()).toPromise()
const superGetFeatures = super.getFeatures
return ObservableCreate<Feature>(async observer => {
const feats = await superGetFeatures
.call(this, region)
.pipe(toArray())
.toPromise()
// return ObservableCreate<Feature>(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()
})
}
Expand Down
Loading

0 comments on commit 31ce25f

Please sign in to comment.