Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BEDPE adapter type #3341

Merged
merged 1 commit into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion plugins/bed/src/BedAdapter/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
export { default as configSchema } from './configSchema'
import PluginManager from '@jbrowse/core/PluginManager'
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'

import configSchema from './configSchema'

export default (pluginManager: PluginManager) => {
pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BedAdapter',
configSchema,
getAdapterClass: () => import('./BedAdapter').then(r => r.default),
}),
)
}
16 changes: 15 additions & 1 deletion plugins/bed/src/BedTabixAdapter/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
export { default as configSchema } from './configSchema'
import PluginManager from '@jbrowse/core/PluginManager'
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'

import configSchema from './configSchema'

export default (pluginManager: PluginManager) => {
pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BedTabixAdapter',
configSchema,
getAdapterClass: () => import('./BedTabixAdapter').then(r => r.default),
}),
)
}
204 changes: 204 additions & 0 deletions plugins/bed/src/BedpeAdapter/BedpeAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {
BaseFeatureDataAdapter,
BaseOptions,
} from '@jbrowse/core/data_adapters/BaseAdapter'
import { openLocation } from '@jbrowse/core/util/io'
import { ObservableCreate } from '@jbrowse/core/util/rxjs'
import { Region, Feature, SimpleFeature } from '@jbrowse/core/util'
import IntervalTree from '@flatten-js/interval-tree'
import { unzip } from '@gmod/bgzf-filehandle'

function isGzip(buf: Buffer) {
return buf[0] === 31 && buf[1] === 139 && buf[2] === 8
}

export function featureData(
line: string,
uniqueId: string,
flip: boolean,
names?: string[],
) {
const l = line.split('\t')
const ref1 = l[flip ? 3 : 0]
const start1 = +l[flip ? 4 : 1]
const end1 = +l[flip ? 5 : 2]
const ref2 = +l[!flip ? 3 : 0]
const start2 = +l[!flip ? 4 : 1]
const end2 = +l[!flip ? 5 : 2]
const name = l[6]
const score = +l[7]
const strand1 = parseStrand(l[8])
const strand2 = parseStrand(l[9])
const extra = l.slice(9)
const rest = names
? Object.fromEntries(names.slice(9).map((n, idx) => [n, extra[idx]]))
: extra

return new SimpleFeature({
start: start1,
end: end1,
refName: ref1,
strand: strand1,
name,
...rest,
score,
uniqueId,
mate: { refName: ref2, start: start2, end: end2, strand: strand2 },
})
}

function parseStrand(strand: string) {
if (strand === '+') {
return 1
} else if (strand === '-') {
return -1
} else if (strand === '.') {
return 0
} else {
return undefined
}
}

export default class BedpeAdapter extends BaseFeatureDataAdapter {
protected bedpeFeatures?: Promise<{
header: string
feats1: Record<string, string[]>
feats2: Record<string, string[]>
columnNames: string[]
}>

protected intervalTrees: {
[key: string]: Promise<IntervalTree | undefined> | undefined
} = {}

public static capabilities = ['getFeatures', 'getRefNames']

private async loadDataP(opts: BaseOptions = {}) {
const pm = this.pluginManager
const bedLoc = this.getConf('bedpeLocation')
const buf = await openLocation(bedLoc, pm).readFile(opts)
const buffer = isGzip(buf) ? await unzip(buf) : buf
// 512MB max chrome string length is 512MB
if (buffer.length > 536_870_888) {
throw new Error('Data exceeds maximum string length (512MB)')
}
const data = new TextDecoder('utf8', { fatal: true }).decode(buffer)
const lines = data.split(/\n|\r\n|\r/).filter(f => !!f)
const headerLines = []
let i = 0
for (; i < lines.length && lines[i].startsWith('#'); i++) {
headerLines.push(lines[i])
}
const header = headerLines.join('\n')
const feats1 = {} as Record<string, string[]>
const feats2 = {} as Record<string, string[]>
for (; i < lines.length; i++) {
const line = lines[i]
const cols = line.split('\t')
const r1 = cols[0]
const r2 = cols[3]
if (!feats1[r1]) {
feats1[r1] = []
}
if (!feats2[r2]) {
feats2[r2] = []
}
feats1[r1].push(line)
feats2[r2].push(line)
}
const columnNames = this.getConf('columnNames')

return {
header,
feats1,
feats2,
columnNames,
}
}

private async loadData(opts: BaseOptions = {}) {
if (!this.bedpeFeatures) {
this.bedpeFeatures = this.loadDataP(opts).catch(e => {
this.bedpeFeatures = undefined
throw e
})
}

return this.bedpeFeatures
}

public async getRefNames(opts: BaseOptions = {}) {
const { feats1, feats2 } = await this.loadData(opts)
return [...new Set([...Object.keys(feats1), ...Object.keys(feats2)])]
}

async getHeader(opts: BaseOptions = {}) {
const { header } = await this.loadData(opts)
return header
}

async getNames() {
const { header, columnNames } = await this.loadData()
if (columnNames.length) {
return columnNames
}
const defs = header.split(/\n|\r\n|\r/).filter(f => !!f)
const defline = defs[defs.length - 1]
return defline?.includes('\t')
? defline
.slice(1)
.split('\t')
.map(field => field.trim())
: undefined
}

private async loadFeatureTreeP(refName: string) {
const { feats1, feats2 } = await this.loadData()
const lines1 = feats1[refName]
const lines2 = feats2[refName]
const names = await this.getNames()

const intervalTree = new IntervalTree()
const ret1 = lines1?.map((f, i) => {
const uniqueId = `${this.id}-${refName}-${i}`
return featureData(f, uniqueId, false, names)
})
const ret2 = lines2?.map((f, i) => {
const uniqueId = `${this.id}-${refName}-${i}`
return featureData(f, uniqueId, true, names)
})

for (let i = 0; i < ret1.length; i++) {
const obj = ret1[i]
intervalTree.insert([obj.get('start'), obj.get('end')], obj)
}

for (let i = 0; i < ret2.length; i++) {
const obj = ret2[i]
intervalTree.insert([obj.get('start'), obj.get('end')], obj)
}

return intervalTree
}

private async loadFeatureTree(refName: string) {
if (!this.intervalTrees[refName]) {
this.intervalTrees[refName] = this.loadFeatureTreeP(refName).catch(e => {
this.intervalTrees[refName] = undefined
throw e
})
}
return this.intervalTrees[refName]
}

public getFeatures(query: Region, opts: BaseOptions = {}) {
return ObservableCreate<Feature>(async observer => {
const { start, end, refName } = query
const intervalTree = await this.loadFeatureTree(refName)
intervalTree?.search([start, end]).forEach(f => observer.next(f))
observer.complete()
}, opts.signal)
}

public freeResources(): void {}
}
34 changes: 34 additions & 0 deletions plugins/bed/src/BedpeAdapter/configSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ConfigurationSchema } from '@jbrowse/core/configuration'

/**
* #config BedpeAdapter
* intended for SVs in a single assembly
*/
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars

const BedpeAdapter = ConfigurationSchema(
'BedpeAdapter',
{
/**
* #slot
* can be plaintext or gzipped, not indexed so loaded into memory on startup
*/
bedpeLocation: {
type: 'fileLocation',
defaultValue: {
uri: '/path/to/my.bedpe.gz',
locationType: 'UriLocation',
},
},
/**
* #slot
*/
columnNames: {
type: 'stringArray',
description: 'List of column names',
defaultValue: [],
},
},
{ explicitlyTyped: true },
)
export default BedpeAdapter
15 changes: 15 additions & 0 deletions plugins/bed/src/BedpeAdapter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import PluginManager from '@jbrowse/core/PluginManager'
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'

import configSchema from './configSchema'

export default (pluginManager: PluginManager) => {
pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BedpeAdapter',
configSchema,
getAdapterClass: () => import('./BedpeAdapter').then(r => r.default),
}),
)
}
16 changes: 15 additions & 1 deletion plugins/bed/src/BigBedAdapter/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
export { default as configSchema } from './configSchema'
import PluginManager from '@jbrowse/core/PluginManager'
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'

import configSchema from './configSchema'

export default (pluginManager: PluginManager) => {
pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BigBedAdapter',
configSchema,
getAdapterClass: () => import('./BigBedAdapter').then(r => r.default),
}),
)
}
58 changes: 28 additions & 30 deletions plugins/bed/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'
import Plugin from '@jbrowse/core/Plugin'
import PluginManager from '@jbrowse/core/PluginManager'
import { configSchema as bigBedAdapterConfigSchema } from './BigBedAdapter'
import { configSchema as bedTabixAdapterConfigSchema } from './BedTabixAdapter'
import { configSchema as bedAdapterConfigSchema } from './BedAdapter'
import BigBedAdapterF from './BigBedAdapter'
import BedpeAdapterF from './BedpeAdapter'
import BedTabixAdapterF from './BedTabixAdapter'
import BedAdapterF from './BedAdapter'
import { FileLocation } from '@jbrowse/core/util/types'
import {
getFileName,
Expand All @@ -16,15 +16,10 @@ export default class BedPlugin extends Plugin {
name = 'BedPlugin'

install(pluginManager: PluginManager) {
pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BigBedAdapter',
configSchema: bigBedAdapterConfigSchema,
getAdapterClass: () =>
import('./BigBedAdapter/BigBedAdapter').then(r => r.default),
}),
)
BigBedAdapterF(pluginManager)
BedAdapterF(pluginManager)
BedpeAdapterF(pluginManager)
BedTabixAdapterF(pluginManager)
pluginManager.addToExtensionPoint(
'Core-guessAdapterForLocation',
(adapterGuesser: AdapterGuesser) => {
Expand All @@ -51,25 +46,28 @@ export default class BedPlugin extends Plugin {
},
)

pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BedTabixAdapter',
configSchema: bedTabixAdapterConfigSchema,
getAdapterClass: () =>
import('./BedTabixAdapter/BedTabixAdapter').then(r => r.default),
}),
pluginManager.addToExtensionPoint(
'Core-guessAdapterForLocation',
(adapterGuesser: AdapterGuesser) => {
return (
file: FileLocation,
index?: FileLocation,
adapterHint?: string,
) => {
const regexGuess = /\.bedpe\.gz$/i
const adapterName = 'BedpeAdapter'
const fileName = getFileName(file)
if (regexGuess.test(fileName) || adapterHint === adapterName) {
return {
type: adapterName,
bedpeLocation: file,
}
}
return adapterGuesser(file, index, adapterHint)
}
},
)

pluginManager.addAdapterType(
() =>
new AdapterType({
name: 'BedAdapter',
configSchema: bedAdapterConfigSchema,
getAdapterClass: () =>
import('./BedAdapter/BedAdapter').then(r => r.default),
}),
)
pluginManager.addToExtensionPoint(
'Core-guessAdapterForLocation',
(adapterGuesser: AdapterGuesser) => {
Expand Down
Loading